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}")