Initial Query

Given this BNF-style grammar write a Python function `evaluate(str)` that evaluates the expression <expression> ::= <term> | <expression> '+' <term> | <expression> '-' <term> <term> ::= <factor> | <term> '*' <factor> | <term> '/' <factor> <factor> ::= <number> | '(' <expression> ')' | <prefix-op> <factor> <prefix-op> ::= 'sin' | 'cos' <number> ::= <digit> | <number> <digit> <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' For example you can evaluate this sin(3 + 2 * 4) - cos(15) = -0.240302

LLM Generation

Query

Given this BNF-style grammar write a Python function `evaluate(str)` that evaluates the expression <expression> ::= <term> | <expression> '+' <term> | <expression> '-' <term> <term> ::= <factor> | <term> '*' <factor> | <term> '/' <factor> <factor> ::= <number> | '(' <expression> ')' | <prefix-op> <factor> <prefix-op> ::= 'sin' | 'cos' <number> ::= <digit> | <number> <digit> <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' For example you can evaluate this sin(3 + 2 * 4) - cos(15) = -0.240302

Output

Okay, let's implement a recursive descent parser in Python to evaluate expressions based on the provided BNF grammar. We'll create a `Parser` class to manage the input string and the current parsing position. The methods within the class will correspond to the non-terminals in the grammar.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import math
import re # For potential whitespace skipping, though manual skipping is often clearer here

class ParseError(Exception):
    """Custom exception for parsing errors."""
    pass

class Parser:
    def __init__(self, text):
        self.text = text
        self.pos = 0
        # Ensure trigonometric functions use radians as standard in math module
        print("Note: Trigonometric functions (sin, cos) assume input is in radians.")

    def _peek(self):
        """Return the next character without consuming it, or None if EOF."""
        if self.pos < len(self.text):
            return self.text[self.pos]
        return None

    def _consume(self, char=None):
        """Consume the next character. If char is provided, check for match."""
        if self.pos < len(self.text):
            current_char = self.text[self.pos]
            if char is not None and current_char != char:
                raise ParseError(f"Expected '{char}' but found '{current_char}' at position {self.pos}")
            self.pos += 1
            return current_char
        elif char is not None:
             raise ParseError(f"Expected '{char}' but reached end of input")
        return None # Reached end of input

    def _skip_whitespace(self):
        """Consume whitespace characters."""
        while self.pos < len(self.text) and self.text[self.pos].isspace():
            self.pos += 1

    def _parse_number(self):
        """Parses a sequence of digits into an integer."""
        start_pos = self.pos
        while self.pos < len(self.text) and self.text[self.pos].isdigit():
            self.pos += 1
        if start_pos == self.pos:
            # This case should ideally be caught by factor parsing logic before calling number
            raise ParseError(f"Expected number but found '{self._peek()}' at position {start_pos}")
        num_str = self.text[start_pos:self.pos]
        return int(num_str) # The grammar only defines integers

    def _parse_factor(self):
        """Parses a factor: <number> | '(' <expression> ')' | <prefix-op> <factor>"""
        self._skip_whitespace()
        char = self._peek()

        if char is None:
             raise ParseError("Unexpected end of input while parsing factor")

        if char.isdigit():
            return float(self._parse_number()) # Return float for calculations
        elif char == '(':
            self._consume('(')
            expr_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return expr_val
        elif self.text.startswith('sin', self.pos):
            self.pos += 3 # Consume 'sin'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'sin' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside sin()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after sin argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.sin(arg_val) # Assumes radians
        elif self.text.startswith('cos', self.pos):
            self.pos += 3 # Consume 'cos'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'cos' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside cos()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after cos argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.cos(arg_val) # Assumes radians
        else:
            raise ParseError(f"Unexpected character '{char}' while parsing factor at position {self.pos}")

    def _parse_term(self):
        """Parses a term: <factor> | <term> '*' <factor> | <term> '/' <factor>"""
        # Parse the first factor
        value = self._parse_factor()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '*':
                self._consume('*')
                right_factor = self._parse_factor()
                value *= right_factor
            elif op == '/':
                self._consume('/')
                right_factor = self._parse_factor()
                if right_factor == 0:
                    raise ParseError("Division by zero")
                value /= right_factor # Use float division
            else:
                # No more '*' or '/' found for this term
                break
        return value

    def _parse_expression(self):
        """Parses an expression: <term> | <expression> '+' <term> | <expression> '-' <term>"""
        # Parse the first term
        value = self._parse_term()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '+':
                self._consume('+')
                right_term = self._parse_term()
                value += right_term
            elif op == '-':
                self._consume('-')
                right_term = self._parse_term()
                value -= right_term
            else:
                # No more '+' or '-' found for this expression
                break
        return value

    def evaluate(self):
        """Evaluates the entire expression text."""
        result = self._parse_expression()
        self._skip_whitespace()
        # Check if we consumed the entire string
        if self.pos < len(self.text):
            raise ParseError(f"Unexpected character '{self.text[self.pos]}' after expression at position {self.pos}")
        return result

# --- Top-level function ---
def evaluate(expression_string):
    """
    Evaluates a mathematical expression string based on the defined grammar.

    Args:
        expression_string: The string containing the mathematical expression.

    Returns:
        The numerical result of the evaluation (float).

    Raises:
        ParseError: If the string does not conform to the grammar or contains errors
                    like division by zero or mismatched parentheses.
    """
    try:
        parser = Parser(expression_string)
        return parser.evaluate()
    except ParseError as e:
        print(f"Parsing Error: {e}")
        # Or re-raise if you want the caller to handle it: raise e
        return None # Indicate failure
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
        return None # Indicate failure

# --- Example Usage ---
expression1 = "sin(3 + 2 * 4) - cos(15)"
result1 = evaluate(expression1)
if result1 is not None:
    print(f"'{expression1}' = {result1}")
    # Expected output (approx): 'sin(3 + 2 * 4) - cos(15)' = -0.240302306889

expression2 = "10 + 2 * 6"
result2 = evaluate(expression2)
if result2 is not None:
    print(f"'{expression2}' = {result2}") # Expected: 22.0

expression3 = "(10 + 2) * 6"
result3 = evaluate(expression3)
if result3 is not None:
    print(f"'{expression3}' = {result3}") # Expected: 72.0

expression4 = "100 / 10 * 2"
result4 = evaluate(expression4)
if result4 is not None:
    print(f"'{expression4}' = {result4}") # Expected: 20.0 (Left-associative)

expression5 = "cos(0) + sin(0)"
result5 = evaluate(expression5)
if result5 is not None:
    print(f"'{expression5}' = {result5}") # Expected: 1.0

expression6 = "5 / (4 - 2 * 2)" # Division by zero
result6 = evaluate(expression6)
if result6 is not None:
    print(f"'{expression6}' = {result6}")
else:
    print(f"Evaluation failed for '{expression6}'")

expression7 = "3 + " # Incomplete expression
result7 = evaluate(expression7)
if result7 is not None:
    print(f"'{expression7}' = {result7}")
else:
    print(f"Evaluation failed for '{expression7}'")

expression8 = "sin(1" # Mismatched parenthesis
result8 = evaluate(expression8)
if result8 is not None:
    print(f"'{expression8}' = {result8}")
else:
    print(f"Evaluation failed for '{expression8}'")

expression9 = "tan(5)" # Invalid operator
result9 = evaluate(expression9)
if result9 is not None:
    print(f"'{expression9}' = {result9}")
else:
    print(f"Evaluation failed for '{expression9}'")

expression10 = "  5  * ( sin ( 1 ) + cos( 2 ) ) / 2 " # With whitespace
result10 = evaluate(expression10)
if result10 is not None:
    print(f"'{expression10}' = {result10}")
**Explanation:** 1. **`ParseError` Class:** A simple custom exception for clarity when parsing fails. 2. **`Parser` Class:** * `__init__`: Stores the input `text` and initializes the current position `pos` to 0. It also prints a note about radians. * `_peek`: Looks at the character at the current `pos` without advancing. * `_consume`: Advances `pos` by one. Optionally checks if the consumed character matches an expected one. * `_skip_whitespace`: Advances `pos` past any whitespace characters. * `_parse_number`: Reads consecutive digits and converts them to an integer (then returned as a float for consistency in calculations). * `_parse_factor`: Implements the `<factor>` rule. It checks for numbers, opening parentheses (triggering a recursive call to `_parse_expression`), or the keywords 'sin'/'cos' (triggering recursive calls for their arguments and applying the `math` functions). * `_parse_term`: Implements the `<term>` rule. It first parses a factor, then loops, looking for `*` or `/`. If found, it consumes the operator and parses the next factor, performing the calculation. This loop handles the left-associativity and precedence of multiplication/division over addition/subtraction. * `_parse_expression`: Implements the `<expression>` rule. It first parses a term, then loops, looking for `+` or `-`. If found, it consumes the operator and parses the next term, performing the calculation. This handles left-associativity for addition/subtraction. * `evaluate`: The main entry point for the parser. It calls `_parse_expression` to start the process and then checks if the entire input string has been consumed. If not, it means there were trailing unexpected characters. 3. **`evaluate(expression_string)` Function:** * This is the public function users will call. * It creates a `Parser` instance. * It calls the parser's `evaluate` method within a `try...except` block to catch `ParseError` and other potential issues (like `ValueError` from `int()` if something unexpected happened, though `_parse_number` should prevent that). * It prints error messages and returns `None` on failure, or the calculated float result on success. This implementation directly translates the BNF grammar rules into parsing functions, respecting operator precedence and associativity defined by the grammar structure.

Extract Code

I extracted the following code from that output:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import math
import re # For potential whitespace skipping, though manual skipping is often clearer here

class ParseError(Exception):
    """Custom exception for parsing errors."""
    pass

class Parser:
    def __init__(self, text):
        self.text = text
        self.pos = 0
        # Ensure trigonometric functions use radians as standard in math module
        print("Note: Trigonometric functions (sin, cos) assume input is in radians.")

    def _peek(self):
        """Return the next character without consuming it, or None if EOF."""
        if self.pos < len(self.text):
            return self.text[self.pos]
        return None

    def _consume(self, char=None):
        """Consume the next character. If char is provided, check for match."""
        if self.pos < len(self.text):
            current_char = self.text[self.pos]
            if char is not None and current_char != char:
                raise ParseError(f"Expected '{char}' but found '{current_char}' at position {self.pos}")
            self.pos += 1
            return current_char
        elif char is not None:
             raise ParseError(f"Expected '{char}' but reached end of input")
        return None # Reached end of input

    def _skip_whitespace(self):
        """Consume whitespace characters."""
        while self.pos < len(self.text) and self.text[self.pos].isspace():
            self.pos += 1

    def _parse_number(self):
        """Parses a sequence of digits into an integer."""
        start_pos = self.pos
        while self.pos < len(self.text) and self.text[self.pos].isdigit():
            self.pos += 1
        if start_pos == self.pos:
            # This case should ideally be caught by factor parsing logic before calling number
            raise ParseError(f"Expected number but found '{self._peek()}' at position {start_pos}")
        num_str = self.text[start_pos:self.pos]
        return int(num_str) # The grammar only defines integers

    def _parse_factor(self):
        """Parses a factor: <number> | '(' <expression> ')' | <prefix-op> <factor>"""
        self._skip_whitespace()
        char = self._peek()

        if char is None:
             raise ParseError("Unexpected end of input while parsing factor")

        if char.isdigit():
            return float(self._parse_number()) # Return float for calculations
        elif char == '(':
            self._consume('(')
            expr_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return expr_val
        elif self.text.startswith('sin', self.pos):
            self.pos += 3 # Consume 'sin'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'sin' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside sin()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after sin argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.sin(arg_val) # Assumes radians
        elif self.text.startswith('cos', self.pos):
            self.pos += 3 # Consume 'cos'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'cos' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside cos()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after cos argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.cos(arg_val) # Assumes radians
        else:
            raise ParseError(f"Unexpected character '{char}' while parsing factor at position {self.pos}")

    def _parse_term(self):
        """Parses a term: <factor> | <term> '*' <factor> | <term> '/' <factor>"""
        # Parse the first factor
        value = self._parse_factor()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '*':
                self._consume('*')
                right_factor = self._parse_factor()
                value *= right_factor
            elif op == '/':
                self._consume('/')
                right_factor = self._parse_factor()
                if right_factor == 0:
                    raise ParseError("Division by zero")
                value /= right_factor # Use float division
            else:
                # No more '*' or '/' found for this term
                break
        return value

    def _parse_expression(self):
        """Parses an expression: <term> | <expression> '+' <term> | <expression> '-' <term>"""
        # Parse the first term
        value = self._parse_term()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '+':
                self._consume('+')
                right_term = self._parse_term()
                value += right_term
            elif op == '-':
                self._consume('-')
                right_term = self._parse_term()
                value -= right_term
            else:
                # No more '+' or '-' found for this expression
                break
        return value

    def evaluate(self):
        """Evaluates the entire expression text."""
        result = self._parse_expression()
        self._skip_whitespace()
        # Check if we consumed the entire string
        if self.pos < len(self.text):
            raise ParseError(f"Unexpected character '{self.text[self.pos]}' after expression at position {self.pos}")
        return result

# --- Top-level function ---
def evaluate(expression_string):
    """
    Evaluates a mathematical expression string based on the defined grammar.

    Args:
        expression_string: The string containing the mathematical expression.

    Returns:
        The numerical result of the evaluation (float).

    Raises:
        ParseError: If the string does not conform to the grammar or contains errors
                    like division by zero or mismatched parentheses.
    """
    try:
        parser = Parser(expression_string)
        return parser.evaluate()
    except ParseError as e:
        print(f"Parsing Error: {e}")
        # Or re-raise if you want the caller to handle it: raise e
        return None # Indicate failure
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
        return None # Indicate failure

# --- Example Usage ---
expression1 = "sin(3 + 2 * 4) - cos(15)"
result1 = evaluate(expression1)
if result1 is not None:
    print(f"'{expression1}' = {result1}")
    # Expected output (approx): 'sin(3 + 2 * 4) - cos(15)' = -0.240302306889

expression2 = "10 + 2 * 6"
result2 = evaluate(expression2)
if result2 is not None:
    print(f"'{expression2}' = {result2}") # Expected: 22.0

expression3 = "(10 + 2) * 6"
result3 = evaluate(expression3)
if result3 is not None:
    print(f"'{expression3}' = {result3}") # Expected: 72.0

expression4 = "100 / 10 * 2"
result4 = evaluate(expression4)
if result4 is not None:
    print(f"'{expression4}' = {result4}") # Expected: 20.0 (Left-associative)

expression5 = "cos(0) + sin(0)"
result5 = evaluate(expression5)
if result5 is not None:
    print(f"'{expression5}' = {result5}") # Expected: 1.0

expression6 = "5 / (4 - 2 * 2)" # Division by zero
result6 = evaluate(expression6)
if result6 is not None:
    print(f"'{expression6}' = {result6}")
else:
    print(f"Evaluation failed for '{expression6}'")

expression7 = "3 + " # Incomplete expression
result7 = evaluate(expression7)
if result7 is not None:
    print(f"'{expression7}' = {result7}")
else:
    print(f"Evaluation failed for '{expression7}'")

expression8 = "sin(1" # Mismatched parenthesis
result8 = evaluate(expression8)
if result8 is not None:
    print(f"'{expression8}' = {result8}")
else:
    print(f"Evaluation failed for '{expression8}'")

expression9 = "tan(5)" # Invalid operator
result9 = evaluate(expression9)
if result9 is not None:
    print(f"'{expression9}' = {result9}")
else:
    print(f"Evaluation failed for '{expression9}'")

expression10 = "  5  * ( sin ( 1 ) + cos( 2 ) ) / 2 " # With whitespace
result10 = evaluate(expression10)
if result10 is not None:
    print(f"'{expression10}' = {result10}")

Run Code Interpreter

Running the following program:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import math
import re # For potential whitespace skipping, though manual skipping is often clearer here

class ParseError(Exception):
    """Custom exception for parsing errors."""
    pass

class Parser:
    def __init__(self, text):
        self.text = text
        self.pos = 0
        # Ensure trigonometric functions use radians as standard in math module
        print("Note: Trigonometric functions (sin, cos) assume input is in radians.")

    def _peek(self):
        """Return the next character without consuming it, or None if EOF."""
        if self.pos < len(self.text):
            return self.text[self.pos]
        return None

    def _consume(self, char=None):
        """Consume the next character. If char is provided, check for match."""
        if self.pos < len(self.text):
            current_char = self.text[self.pos]
            if char is not None and current_char != char:
                raise ParseError(f"Expected '{char}' but found '{current_char}' at position {self.pos}")
            self.pos += 1
            return current_char
        elif char is not None:
             raise ParseError(f"Expected '{char}' but reached end of input")
        return None # Reached end of input

    def _skip_whitespace(self):
        """Consume whitespace characters."""
        while self.pos < len(self.text) and self.text[self.pos].isspace():
            self.pos += 1

    def _parse_number(self):
        """Parses a sequence of digits into an integer."""
        start_pos = self.pos
        while self.pos < len(self.text) and self.text[self.pos].isdigit():
            self.pos += 1
        if start_pos == self.pos:
            # This case should ideally be caught by factor parsing logic before calling number
            raise ParseError(f"Expected number but found '{self._peek()}' at position {start_pos}")
        num_str = self.text[start_pos:self.pos]
        return int(num_str) # The grammar only defines integers

    def _parse_factor(self):
        """Parses a factor: <number> | '(' <expression> ')' | <prefix-op> <factor>"""
        self._skip_whitespace()
        char = self._peek()

        if char is None:
             raise ParseError("Unexpected end of input while parsing factor")

        if char.isdigit():
            return float(self._parse_number()) # Return float for calculations
        elif char == '(':
            self._consume('(')
            expr_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return expr_val
        elif self.text.startswith('sin', self.pos):
            self.pos += 3 # Consume 'sin'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'sin' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside sin()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after sin argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.sin(arg_val) # Assumes radians
        elif self.text.startswith('cos', self.pos):
            self.pos += 3 # Consume 'cos'
            self._skip_whitespace()
            if self._peek() != '(':
                 raise ParseError(f"Expected '(' after 'cos' but found '{self._peek()}' at position {self.pos}")
            self._consume('(')
            # Recursively call parse_expression for the argument inside cos()
            arg_val = self._parse_expression()
            self._skip_whitespace()
            if self._peek() != ')':
                 raise ParseError(f"Expected ')' after cos argument but found '{self._peek()}' at position {self.pos}")
            self._consume(')')
            return math.cos(arg_val) # Assumes radians
        else:
            raise ParseError(f"Unexpected character '{char}' while parsing factor at position {self.pos}")

    def _parse_term(self):
        """Parses a term: <factor> | <term> '*' <factor> | <term> '/' <factor>"""
        # Parse the first factor
        value = self._parse_factor()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '*':
                self._consume('*')
                right_factor = self._parse_factor()
                value *= right_factor
            elif op == '/':
                self._consume('/')
                right_factor = self._parse_factor()
                if right_factor == 0:
                    raise ParseError("Division by zero")
                value /= right_factor # Use float division
            else:
                # No more '*' or '/' found for this term
                break
        return value

    def _parse_expression(self):
        """Parses an expression: <term> | <expression> '+' <term> | <expression> '-' <term>"""
        # Parse the first term
        value = self._parse_term()

        while True:
            self._skip_whitespace()
            op = self._peek()

            if op == '+':
                self._consume('+')
                right_term = self._parse_term()
                value += right_term
            elif op == '-':
                self._consume('-')
                right_term = self._parse_term()
                value -= right_term
            else:
                # No more '+' or '-' found for this expression
                break
        return value

    def evaluate(self):
        """Evaluates the entire expression text."""
        result = self._parse_expression()
        self._skip_whitespace()
        # Check if we consumed the entire string
        if self.pos < len(self.text):
            raise ParseError(f"Unexpected character '{self.text[self.pos]}' after expression at position {self.pos}")
        return result

# --- Top-level function ---
def evaluate(expression_string):
    """
    Evaluates a mathematical expression string based on the defined grammar.

    Args:
        expression_string: The string containing the mathematical expression.

    Returns:
        The numerical result of the evaluation (float).

    Raises:
        ParseError: If the string does not conform to the grammar or contains errors
                    like division by zero or mismatched parentheses.
    """
    try:
        parser = Parser(expression_string)
        return parser.evaluate()
    except ParseError as e:
        print(f"Parsing Error: {e}")
        # Or re-raise if you want the caller to handle it: raise e
        return None # Indicate failure
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
        return None # Indicate failure

# --- Example Usage ---
expression1 = "sin(3 + 2 * 4) - cos(15)"
result1 = evaluate(expression1)
if result1 is not None:
    print(f"'{expression1}' = {result1}")
    # Expected output (approx): 'sin(3 + 2 * 4) - cos(15)' = -0.240302306889

expression2 = "10 + 2 * 6"
result2 = evaluate(expression2)
if result2 is not None:
    print(f"'{expression2}' = {result2}") # Expected: 22.0

expression3 = "(10 + 2) * 6"
result3 = evaluate(expression3)
if result3 is not None:
    print(f"'{expression3}' = {result3}") # Expected: 72.0

expression4 = "100 / 10 * 2"
result4 = evaluate(expression4)
if result4 is not None:
    print(f"'{expression4}' = {result4}") # Expected: 20.0 (Left-associative)

expression5 = "cos(0) + sin(0)"
result5 = evaluate(expression5)
if result5 is not None:
    print(f"'{expression5}' = {result5}") # Expected: 1.0

expression6 = "5 / (4 - 2 * 2)" # Division by zero
result6 = evaluate(expression6)
if result6 is not None:
    print(f"'{expression6}' = {result6}")
else:
    print(f"Evaluation failed for '{expression6}'")

expression7 = "3 + " # Incomplete expression
result7 = evaluate(expression7)
if result7 is not None:
    print(f"'{expression7}' = {result7}")
else:
    print(f"Evaluation failed for '{expression7}'")

expression8 = "sin(1" # Mismatched parenthesis
result8 = evaluate(expression8)
if result8 is not None:
    print(f"'{expression8}' = {result8}")
else:
    print(f"Evaluation failed for '{expression8}'")

expression9 = "tan(5)" # Invalid operator
result9 = evaluate(expression9)
if result9 is not None:
    print(f"'{expression9}' = {result9}")
else:
    print(f"Evaluation failed for '{expression9}'")

expression10 = "  5  * ( sin ( 1 ) + cos( 2 ) ) / 2 " # With whitespace
result10 = evaluate(expression10)
if result10 is not None:
    print(f"'{expression10}' = {result10}")





answer = round(evaluate('sin(35+2*4)-cos(15)'), 5)
expected = -0.07209
assert answer == expected, f'Wrong answer; got {answer} instead of {expected}'
print('All tests passed')
And got the output: Note: Trigonometric functions (sin, cos) assume input is in radians. 'sin(3 + 2 * 4) - cos(15)' = -0.24030229369188216 Note: Trigonometric functions (sin, cos) assume input is in radians. '10 + 2 * 6' = 22.0 Note: Trigonometric functions (sin, cos) assume input is in radians. '(10 + 2) * 6' = 72.0 Note: Trigonometric functions (sin, cos) assume input is in radians. '100 / 10 * 2' = 20.0 Note: Trigonometric functions (sin, cos) assume input is in radians. 'cos(0) + sin(0)' = 1.0 Note: Trigonometric functions (sin, cos) assume input is in radians. Parsing Error: Division by zero Evaluation failed for '5 / (4 - 2 * 2)' Note: Trigonometric functions (sin, cos) assume input is in radians. Parsing Error: Unexpected end of input while parsing factor Evaluation failed for '3 + ' Note: Trigonometric functions (sin, cos) assume input is in radians. Parsing Error: Expected ')' after sin argument but found 'None' at position 5 Evaluation failed for 'sin(1' Note: Trigonometric functions (sin, cos) assume input is in radians. Parsing Error: Unexpected character 't' while parsing factor at position 0 Evaluation failed for 'tan(5)' Note: Trigonometric functions (sin, cos) assume input is in radians. ' 5 * ( sin ( 1 ) + cos( 2 ) ) / 2 ' = 1.0633103706518852 Note: Trigonometric functions (sin, cos) assume input is in radians. All tests passed

Substring Evaluation

Testing if the previous output contains the string All tests passed: True