Skip to content

Activation-stack backtrace; trace userland + test-runner integration#176

Merged
kollhof merged 7 commits into
mainfrom
call-trace
Jun 13, 2026
Merged

Activation-stack backtrace; trace userland + test-runner integration#176
kollhof merged 7 commits into
mainfrom
call-trace

Conversation

@kollhof

@kollhof kollhof commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds a portable in-band activation-stack backtrace for fink, exposed as userland std/trace.fnk wrappers and integrated into the fink-native test runner so a failing assertion shows its source call chain.

WasmGC universal tail calls (return_call) collapse the native wasm stack, so WasmBacktrace only ever shows one frame. The runtime maintains its own bounded activation-stack ring in linear memory: push on real userland fn entry, pop on return-continuation, mark the call site into the top frame at each user-fn call. get_loc resolves (module_id, cps_id) to a source line via the compiled debug marks (host import).

Notable fixes in this branch

  • Trace-push gated on source-level fns. Synth match-block functions (m_0/mp_N) are classified CpsFunction but are not user-authored. Pushing frames for them injected 2-3 :0 frames between a match call site and the match body, eating the backtrace window and hiding the real frame. Now gated on is_source_fn (origin resolves to NodeKind::Fn), mirroring the existing is_source_apply gate.
  • JS host host_resolve_loc stub. rt/trace.wat is always linked and unconditionally imports env.host_resolve_loc; the wasm+js host did not provide it, causing a LinkError under Node. Stubbed to return 0 (the import's "unknown" sentinel).

Testing

Full suite green: 1333 lib + 42 CLI + 1 interop_js, 0 failed, 3 ignored. get_trace / get_module_url backtrace tests pass; the fink-native suite runs trace.test.fnk via the committed gate.

Follow-ups

  • Ship the compiled loc-map into the wasm+js artifact so the JS host resolves real source lines instead of 0.
  • Residual known gaps: :0 mark-coverage for some cps_ids; a per-write io-frame balance edge case.

kollhof added 7 commits June 8, 2026 15:26
emit.rs becomes the single owner of linear-memory byte layout. Reserve a
512-byte region at the bottom of memory ([0, RING_BYTES)) for the trace
buffer; the literal data pool now starts at RING_BYTES instead of 0.

Add a compile-time assert that the data pool cannot grow into the host-IO
scratch window, turning a previously silent collision into a hard error.
Add rt/trace.wat: a fixed-size ring of recent user-fn call sites in
linear memory. Because Fink compiles every call to a tail call, there is
no native wasm stack to walk; this ring is the portable substitute, in
linear memory so a host can read it even after a hard trap, on any
runtime.

trace_push(module_id, cps_id) records a call site and advances the ring
index modulo trace_len. Lowering emits it at every user-function call
site (the Callable::Val apply_3 arm), before dispatch. Continuation
returns and builtin/runtime calls are not instrumented.

Both interop WATs import trace_push to keep rt/trace.wat in the link and
the func alive. Snapshots re-blessed for the added trace_push lines.
- register_module/get_module_url: a growable module_id->url GC array in
  rt/modules.wat, populated by each fink_module self-registering its
  (id, url) at entry; Fn3-callable get_module_url resolves a trace
  frame's module id back to a source url.
- fix: module_id was always 0 during lowering. compile_package set
  frag.module_id AFTER lower() returned, too late for the constants
  trace_push/register_module bake in at emit time. Now threaded into
  lower() and set before any instrumentation is emitted.
- trace_push only at userland (source Apply) call sites, gated by
  is_source_apply; desugar-synthesized applies (pipe, partial app) are
  no longer traced.
- get_trace Fn3 reader over the trace buffer.
- snapshots re-blessed for the register_module + trace_push changes;
  removed a stray pre-existing base64 fragment in test_records.fnk.
Replace the push-only recency ring with a real backtrace: a bounded
stack of userland function activations.

- rt/trace.wat: bounded activation stack (window of 64 16-byte frames
  {fn_mid, fn_cid, call_mid, call_cid}). trace_push(mid,cid) on fn entry,
  trace_mark(mid,cid) at userland call sites (updates the top frame's
  current call site), trace_pop(mid,cid) on return-cont invocation.
  read_trace walks newest-first (innermost call site at index 0).
- lowering: push at userland fn-body entry, mark at source-Apply call
  sites, pop at App(ContRef(Ret)). A CpsFunction is a real activation
  (pushes its own frame); a CpsClosure is a lifted continuation (no push,
  its ret-cont pops the enclosing fn) - bundled as TraceFrame.
- get_loc(mid,cid) -> source line: host_resolve_loc host import in the
  wasmtime runner resolves a frame to a line via the debug marks; Fn3
  wrapper in rt/trace.wat. Makes traces readable as url:line.
- emit.rs: TRACE_BYTES region sized to 64*16=1024 for the wider frames.
- snapshots re-blessed for the push/mark/pop instrumentation.
Synth match-block functions (m_0/mp_N) are classified CpsFunction but
are not user-authored, so pushing trace frames for them injected 2-3
:0 frames between a match call site and the match body, eating the
backtrace window and hiding the real frame. Gate the push on a new
is_source_fn check (origin resolves to NodeKind::Fn), mirroring the
existing is_source_apply gate; synth CpsFunctions inherit the
enclosing frame like a CpsClosure. Re-bless the match/recursion/
set-destructure snapshots whose synth trace_push lines now drop.
rt/trace.wat is always linked and unconditionally imports
env.host_resolve_loc; the wasm+js host did not provide it, so every
module failed to instantiate under Node with a LinkError. Stub it to
return 0 (the import's documented "unknown" sentinel) like the host's
other unsupported imports. Source-line resolution in JS needs the
compiled debug-mark map shipped into the artifact -- a follow-up.
Add std/trace.fnk (get_trace/get_module_url/get_loc wrappers over the
runtime trace buffer) and std/trace.test.fnk. Wire trace.test.fnk into
std/all.test.fnk, and integrate backtraces into the fink-native test
runner: equals/fail now render a Traceback from get_trace so a failing
assertion shows its source call chain.
@github-actions

Copy link
Copy Markdown

📦 This PR will release v0.85.0 (minor) when merged.

@kollhof kollhof merged commit 12c3169 into main Jun 13, 2026
14 checks passed
@kollhof kollhof deleted the call-trace branch June 13, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant