Recover TCO-elided frames in uncaught-error stack traces

On the eval path nothing registers a source map, so jolt-backtrace-string
dropped every walkable frame and printed no trace at all. Keep any named,
non-plumbing continuation frame (rendered as a bare name when unmapped) so a
runtime error shows the surviving non-tail spine — "print what is available".

Add an opt-in tail-frame history behind JOLT_TRACE for the frames TCO erases.
Each compiled fn records itself on entry into a bounded ring-of-rings, MIT
Scheme's "history" shape: the outer ring holds one rib per non-tail subproblem,
each rib a small inner ring of the tail-calls made at that level. A tight tail
loop churns one rib instead of flushing the spine, so the non-tail caller
context survives and total space stays bounded. The reporter prefers this
history over the continuation when it's present, and resets it per top-level
form so an error's trace isn't padded with earlier REPL frames.

The emitter marks a tail call with (jolt-trace-mark! #t) so the runtime routes
the callee into the current rib vs a fresh one; a *tail?* dynamic var tracks
tail position (cleared by default, passed through if/do/let/loop/fn-body). It's
all gated on trace-frames?, which compile-eval turns on for JOLT_TRACE and
emit-image/`jolt build` force off — so non-trace emitted output is byte-identical
(prelude unchanged, seed re-minted), and a built binary carries no per-call cost.
This commit is contained in:
Yogthos 2026-07-04 15:00:52 -04:00
parent 773e647b4a
commit a3e2365217
8 changed files with 511 additions and 170 deletions

View file

@ -182,6 +182,20 @@ a root, transitively.
- Source only; compiled `.class` files in a git dep are ignored.
- git `:git/sha` must be a full SHA (`git fetch` can't resolve a short one).
## Stack traces
An uncaught error prints the message, the top-level source location, and — when
frames are available — a `trace:` backtrace. In an AOT `jolt build --direct-link`
binary the frames map to `ns/name (file:line)`; on the runtime eval path they are
the surviving fn names. Tail-call optimization erases tail-called frames, so the
default trace shows only the non-tail spine.
Set `JOLT_TRACE=1` to opt into a fuller **tail-frame history**. Each compiled fn
then records itself on entry into a bounded ring-of-rings buffer, so the trace
recovers TCO-elided frames (including the immediate error site) while a tight tail
loop stays bounded and its non-tail caller context is preserved. It costs a small
per-call overhead, so it is off by default and never emitted into a built binary.
## Conformance
The known-working libraries (see [libraries.md](libraries.md)) and the