CODE-0006 · claude-sonnet-4-6 (high)"""
Arithmetic expression evaluator using a recursive descent parser.
Grammar:
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := ('+' | '-')? atom
atom := NUMBER | '(' expr ')'
Supports: +, -, *, / with standard precedence, parentheses, unary +/-,
integer and decimal literals, and arbitrary whitespace.
"""
def eval_expr(s):
"""Evaluate an arithmetic expression string and return its numeric value.
Returns an int if the result is an exact integer value, otherwise a float.
Raises ValueError for malformed expressions or division by zero.
"""
tokens = _tokenize(s)
if not tokens:
raise ValueError("Empty expression")
parser = _Parser(tokens)
result = parser.parse_expr()
if parser.pos != len(parser.tokens):
raise ValueError(
f"Unexpected token at position {parser.pos}: {parser.tokens[parser.pos]!r}"
)
# Return int if the value is exactly representable as an integer
if isinstance(result, float) and result == int(result) and not (result != result):
return int(result)
return result
# ---------------------------------------------------------------------------
# Tokeniser
# ---------------------------------------------------------------------------
def _tokenize(s):
"""Convert the input string into a list of tokens.
Each token is either a float (number) or a single-character string for
operators and parentheses. Whitespace is ignored.
Raises ValueError for unknown characters.
"""
tokens = []
i = 0
while i < len(s):
c = s[i]
if c.isspace():
i += 1
continue
if c in ('+', '-', '*', '/', '(', ')'):
tokens.append(c)
i += 1
continue
if c.isdigit() or c == '.':
j = i
while j < len(s) and (s[j].isdigit() or s[j] == '.'):
j += 1
num_str = s[i:j]
# Reject bare '.' or multiple dots
if num_str.count('.') > 1 or num_str == '.':
raise ValueError(f"Invalid number literal: {num_str!r}")
tokens.append(float(num_str))
i = j
continue
raise ValueError(f"Unknown character: {c!r}")
return tokens
# ---------------------------------------------------------------------------
# Parser (recursive descent)
# ---------------------------------------------------------------------------
class _Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
# -- helpers ----------------------------------------------------------
def peek(self):
if self.pos < len(self.tokens):
return self.tokens[self.pos]
return None
def consume(self):
tok = self.tokens[self.pos]
self.pos += 1
return tok
def expect(self, value):
tok = self.peek()
if tok != value:
raise ValueError(f"Expected {value!r}, got {tok!r}")
return self.consume()
# -- grammar rules ----------------------------------------------------
def parse_expr(self):
"""expr := term (('+' | '-') term)*"""
left = self.parse_term()
while self.peek() in ('+', '-'):
op = self.consume()
right = self.parse_term()
if op == '+':
left = left + right
else:
left = left - right
return left
def parse_term(self):
"""term := factor (('*' | '/') factor)*"""
left = self.parse_factor()
while self.peek() in ('*', '/'):
op = self.consume()
right = self.parse_factor()
if op == '*':
left = left * right
else:
if right == 0:
raise ValueError("Division by zero")
left = left / right
return left
def parse_factor(self):
"""factor := ('+' | '-')? factor | atom
Recursing into parse_factor (not parse_atom) lets unary operators chain,
e.g. '--3' or '+-3'.
"""
if self.peek() == '+':
self.consume()
return +self.parse_factor()
if self.peek() == '-':
self.consume()
return -self.parse_factor()
return self.parse_atom()
def parse_atom(self):
"""atom := NUMBER | '(' expr ')'"""
tok = self.peek()
if tok is None:
raise ValueError("Unexpected end of expression")
if tok == '(':
self.consume() # consume '('
val = self.parse_expr()
self.expect(')')
return val
if isinstance(tok, float):
self.consume()
return tok
# Anything else at this point is malformed (operator where value expected)
raise ValueError(f"Unexpected token: {tok!r}")