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:
dl92
2026-02-08 18:09:42 +00:00
parent 3a8705ece8
commit 01d5532823
11 changed files with 1557 additions and 0 deletions

View 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()