Add expression-evaluator: DAGs & state machines tutorial project
Educational calculator teaching FSMs (explicit transition table tokenizer) and DAGs (recursive descent parser with AST evaluation). Includes CLI with REPL, graphviz visualization, and 61 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
163
python/expression-evaluator/main.py
Normal file
163
python/expression-evaluator/main.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Expression Evaluator -- Learn DAGs & State Machines
|
||||
====================================================
|
||||
CLI entry point and interactive REPL.
|
||||
|
||||
Usage:
|
||||
python main.py "3 + 4 * 2" # evaluate
|
||||
python main.py # REPL mode
|
||||
python main.py --show-tokens --show-ast --trace "expr" # show internals
|
||||
python main.py --dot "3 + 4 * 2" | dot -Tpng -o ast.png
|
||||
python main.py --dot-fsm | dot -Tpng -o fsm.png
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from tokenizer import tokenize, TokenError
|
||||
from parser import Parser, ParseError
|
||||
from evaluator import evaluate, evaluate_traced, EvalError
|
||||
from visualize import ast_to_dot, fsm_to_dot, ast_to_text
|
||||
|
||||
|
||||
def process_expression(expr, args):
|
||||
"""Tokenize, parse, and evaluate a single expression."""
|
||||
try:
|
||||
tokens = tokenize(expr)
|
||||
except TokenError as e:
|
||||
_print_error(expr, e)
|
||||
return
|
||||
|
||||
if args.show_tokens:
|
||||
print("\nTokens:")
|
||||
for tok in tokens:
|
||||
print(f" {tok}")
|
||||
|
||||
try:
|
||||
ast = Parser(tokens).parse()
|
||||
except ParseError as e:
|
||||
_print_error(expr, e)
|
||||
return
|
||||
|
||||
if args.show_ast:
|
||||
print("\nAST (text tree):")
|
||||
print(ast_to_text(ast))
|
||||
|
||||
if args.dot:
|
||||
print(ast_to_dot(ast))
|
||||
return # dot output goes to stdout, skip numeric result
|
||||
|
||||
if args.trace:
|
||||
try:
|
||||
result, steps = evaluate_traced(ast)
|
||||
except EvalError as e:
|
||||
print(f"Eval error: {e}")
|
||||
return
|
||||
print("\nEvaluation trace (topological order):")
|
||||
for step in steps:
|
||||
print(step)
|
||||
print(f"\nResult: {_format_result(result)}")
|
||||
else:
|
||||
try:
|
||||
result = evaluate(ast)
|
||||
except EvalError as e:
|
||||
print(f"Eval error: {e}")
|
||||
return
|
||||
print(_format_result(result))
|
||||
|
||||
|
||||
def repl(args):
|
||||
"""Interactive read-eval-print loop."""
|
||||
print("Expression Evaluator REPL")
|
||||
print("Type an expression, or 'quit' to exit.")
|
||||
flags = []
|
||||
if args.show_tokens:
|
||||
flags.append("--show-tokens")
|
||||
if args.show_ast:
|
||||
flags.append("--show-ast")
|
||||
if args.trace:
|
||||
flags.append("--trace")
|
||||
if flags:
|
||||
print(f"Active flags: {' '.join(flags)}")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = input(">>> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
break
|
||||
if line.lower() in ("quit", "exit", "q"):
|
||||
break
|
||||
if not line:
|
||||
continue
|
||||
process_expression(line, args)
|
||||
print()
|
||||
|
||||
|
||||
def _print_error(expr, error):
|
||||
"""Print an error with a caret pointing to the position."""
|
||||
print(f"Error: {error}")
|
||||
if hasattr(error, 'position') and error.position is not None:
|
||||
print(f" {expr}")
|
||||
print(f" {' ' * error.position}^")
|
||||
|
||||
|
||||
def _format_result(v):
|
||||
"""Format a numeric result: show as int when possible."""
|
||||
if isinstance(v, float) and v == int(v) and abs(v) < 1e15:
|
||||
return str(int(v))
|
||||
return str(v)
|
||||
|
||||
|
||||
def main():
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
description="Expression Evaluator -- learn DAGs and state machines",
|
||||
epilog="Examples:\n"
|
||||
" python main.py '3 + 4 * 2'\n"
|
||||
" python main.py --show-tokens --trace '-(3 + 4) ^ 2'\n"
|
||||
" python main.py --dot '(2+3)*4' | dot -Tpng -o ast.png\n"
|
||||
" python main.py --dot-fsm | dot -Tpng -o fsm.png",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"expression", nargs="?",
|
||||
help="Expression to evaluate (omit for REPL mode)",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--show-tokens", action="store_true",
|
||||
help="Display tokenizer output",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--show-ast", action="store_true",
|
||||
help="Display AST as indented text tree",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--trace", action="store_true",
|
||||
help="Show step-by-step evaluation trace",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--dot", action="store_true",
|
||||
help="Output AST as graphviz dot (pipe to: dot -Tpng -o ast.png)",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--dot-fsm", action="store_true",
|
||||
help="Output tokenizer FSM as graphviz dot",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
# Special mode: just print the FSM diagram and exit
|
||||
if args.dot_fsm:
|
||||
print(fsm_to_dot())
|
||||
return
|
||||
|
||||
# REPL mode if no expression given
|
||||
if args.expression is None:
|
||||
repl(args)
|
||||
else:
|
||||
process_expression(args.expression, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user