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>
164 lines
4.6 KiB
Python
164 lines
4.6 KiB
Python
"""
|
|
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()
|