Errors Worth Reading
The difference between a toy and a tool is what happens when the input is wrong.
What you're wiring up
Right now a bad program dies with whatever string was handy. This stage makes errors first-class. Every diagnostic carries the line and column you stamped onto tokens back in Stage 1, prints the offending source line, and points a caret at the exact column.
The parser learns to recover: on an unexpected token it records a diagnostic, skips forward to a statement boundary, and keeps going — so one typo stops hiding the next three. This is panic-mode plus synchronize, two small functions, and real compilers mostly do not do anything cleverer.
Runtime errors get the same treatment plus a call trace: the evaluator keeps a stack of active call positions and prints frames innermost-first. This is the stage that explains why compiler engineers obsess over spans, and it is pure plumbing you already laid — positions flow token to node to value, and now you finally spend them.
# blu run play.blu (stderr) play.blu:3:11: type mismatch: INTEGER + BOOLEAN let z = 5 + true; ^ in fib at play.blu:7:3 in main at play.blu:12:1
Assembly steps
hint
Keep the source in memory keyed by line. The lexer already counts lines, so slicing the right one is a map lookup, not a re-read.
hint
Two functions total. Resist anything smarter — you will just hide errors differently.
hint
defer makes the pop unforgettable.
hint
Cap the printed trace at ten frames so infinite recursion stays readable.
Go deeper (after it passes)
The blueprint is complete — solid ink. Pair this stage with Clean Code M5 — Errors Without the Clutter, which is exactly what you just built for real. Victory lap: write something real in blu. Then take the lexer and parser straight into build-your-own-database, whose SQL front end is these two units reapplied.