Untitled
unknown
plain_text
5 days ago
4.8 kB
7
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