Untitled

 avatar
unknown
plain_text
a month ago
4.8 kB
11
Indexable
"""
System: A configuration DSL for a data pipeline tool. Users can write
arithmetic expressions in YAML config files and the tool evaluates them
at runtime. Example:

    timeout: "2 + 3 * 4"   # should be 14, not 20

The evaluator uses standard recursive-descent parsing. It handles +, -,
*, / and parentheses with correct operator precedence (the usual rules:
* and / bind tighter than + and -).

The evaluator was written by a senior engineer, code-reviewed, and has
been in production for 3 months. It handles simple expressions correctly.
Yesterday a user filed a bug: expressions mixing + and * give wrong results.
The engineer who wrote it is on vacation.

Reported symptom: `evaluate("2 + 3 * 4")` returns 20.0 instead of 14.0.
Parenthesized expressions like `evaluate("2 + (3 * 4)")` return 14.0 correctly.

Your task: Find and fix the bug. Explain why parenthesized expressions work
but bare precedence doesn't.
"""

from __future__ import annotations
import re
from typing import Optional


class ParseError(Exception):
    pass


class Tokenizer:
    """Converts an expression string into a token stream."""

    TOKEN_RE = re.compile(r"\s*(\d+\.?\d*|[+\-*/()])(\s*)")

    def __init__(self, text: str):
        self._tokens = [m[0] for m in self.TOKEN_RE.findall(text)]
        self._pos = 0

    def peek(self) -> Optional[str]:
        if self._pos < len(self._tokens):
            return self._tokens[self._pos]
        return None

    def consume(self) -> str:
        tok = self.peek()
        if tok is None:
            raise ParseError("Unexpected end of expression")
        self._pos += 1
        return tok

    def expect(self, value: str) -> str:
        tok = self.consume()
        if tok != value:
            raise ParseError(f"Expected '{value}', got '{tok}'")
        return tok


class ExpressionParser:

    def __init__(self, tokenizer: Tokenizer):
        self._tok = tokenizer

    def parse(self) -> float:
        result = self.parse_expr()
        if self._tok.peek() is not None:
            raise ParseError(f"Unexpected token: {self._tok.peek()!r}")
        return result

    def parse_expr(self) -> float:
        left = self.parse_factor()

        while self._tok.peek() in ("+", "-", "*", "/"):
            op = self._tok.consume()
            right = self.parse_factor()
            if op == "+":
                left += right
            elif op == "-":
                left -= right
            elif op == "*":
                left *= right
            elif op == "/":
                if right == 0:
                    raise ParseError("Division by zero")
                left /= right
        return left

    def parse_term(self) -> float:
        left = self.parse_factor()

        while self._tok.peek() in ("*", "/"):
            op = self._tok.consume()
            right = self.parse_factor()
            if op == "*":
                left *= right
            elif op == "/":
                if right == 0:
                    raise ParseError("Division by zero")
                left /= right
        return left

    def parse_factor(self) -> float:
        tok = self._tok.peek()
        if tok is None:
            raise ParseError("Unexpected end of expression")

        if tok == "(":
            self._tok.consume()
            val = self.parse_expr()
            self._tok.expect(")")
            return val

        tok = self._tok.consume()
        try:
            return float(tok)
        except ValueError:
            raise ParseError(f"Expected number or '(', got {tok!r}")


def evaluate(expr: str) -> float:
    """Parse and evaluate an arithmetic expression string."""
    tokenizer = Tokenizer(expr)
    parser = ExpressionParser(tokenizer)
    return parser.parse()


def run_tests():
    cases = [
        ("2 + 3",         5.0),
        ("10 - 4",        6.0),
        ("3 * 4",         12.0),
        ("15 / 3",        5.0),
        ("2 + 3 * 4",     14.0),   # fails before fix — returns 20.0
        ("10 - 2 * 3",    4.0),    # fails before fix — returns 24.0
        ("(2 + 3) * 4",   20.0),
        ("2 + (3 * 4)",   14.0),
        ("100 / 10 + 5",  15.0),   # fails before fix — returns 10.0
        ("2 * 3 + 4 * 5", 26.0),   # fails before fix — returns 50.0
    ]

    passed = 0
    for expr, expected in cases:
        try:
            result = evaluate(expr)
            status = "PASS" if abs(result - expected) < 1e-9 else "FAIL"
            if status == "PASS":
                passed += 1
            print(f"  [{status}] evaluate({expr!r}) = {result}  (expected {expected})")
        except Exception as e:
            print(f"  [ERROR] evaluate({expr!r}) raised {e}")

    print(f"\n{passed}/{len(cases)} tests passed.")


if __name__ == "__main__":
    run_tests()
Editor is loading...
Leave a Comment