Skip to content

feat(runtime): wire closures to child_process.spawn via trampolines (PR2 of closure-cabi series)#564

Merged
cs01 merged 2 commits intomainfrom
feat/trampoline-closures-pr2
Apr 19, 2026
Merged

feat(runtime): wire closures to child_process.spawn via trampolines (PR2 of closure-cabi series)#564
cs01 merged 2 commits intomainfrom
feat/trampoline-closures-pr2

Conversation

@cs01
Copy link
Copy Markdown
Owner

@cs01 cs01 commented Apr 19, 2026

Summary

  • Arrow-function callbacks now work with child_process.spawn(). Captures like per-session state, counters, and object pointers are lifted into a GC-allocated env struct and dispatched through a per-shape C-ABI trampoline (PR1 infrastructure, feat(runtime): C-ABI trampoline closure infrastructure (PR1/4) #560).
  • child_process.spawnTagged is hard-removed — the closure form is a drop-in replacement:
    // before:
    child_process.spawnTagged(sessionId, cmd, args, onOut, onErr, onExit);
    // now:
    child_process.spawn(cmd, args,
      (d) => onOut(sessionId, d),
      (d) => onErr(sessionId, d),
      (c) => onExit(sessionId, c));
  • Named function references still work unchanged — when the codegen sees a VariableNode, it passes -1 as the trampoline handle and the bridge dispatches directly to the bare C fn ptr.

Breaking change

child_process.spawnTagged is removed with no deprecation period. A clear compile error points users to the closure form. The C function cs_spawn_tagged and its LLVM declaration are gone.

Implementation notes

  • cs_spawn in c_bridges/child-process-spawn.c is now a shim over a widened cs_spawn_v2 that takes {fn_ptr, tramp_handle} per callback. Codegen emits cs_spawn_v2 directly.
  • Trampoline slot freeing: stdout/stderr slots free in the pipe-close callback; exit slot frees immediately after its cb returns. uv_spawn failure also drains any registered slots.
  • TrampolineEmitter is now instantiated on LLVMGenerator, with emitAll() wired in after lifted lambdas. The env struct stores the user fn as i8* to sidestep the codegen store-type validator's inability to parse parenthesised LLVM function-pointer types — the trampoline bitcasts it back before dispatch.
  • When the arrow captures nothing, we skip the trampoline path entirely and pass the lifted lambda's bare fn ptr with handle = -1. Keeps the simple case simple.

Scope

  • In: child_process.spawn closure wiring, spawnTagged removal, three new fixtures, bridge ABI change.
  • Out (flagged for later PRs): setTimeout / setInterval (PR3), libwebsockets / treesitter bridges (PR4), fat-pointer function values, capture-by-reference.

Test plan

  • npm run verify — all 809 unit tests + stage 0/1/2 self-hosting pass locally
  • tests/fixtures/closures-cabi/spawn-closure-single.ts — arrow closure captures a class pointer and mutates fields across stdout + exit events
  • tests/fixtures/closures-cabi/spawn-closure-multi-session.ts — two concurrent spawns, each captures its own session, no cross-talk (replaces the spawnTagged demonstrator)
  • tests/fixtures/closures-cabi/spawn-named-fn-backcompat.ts — classic named-function-reference form still green
  • tests/fixtures/builtins/cp-spawn*.ts — existing spawn tests untouched and passing
  • tests/fixtures/closures-cabi/trampoline-bridge-smoke.ts — PR1 smoke still passes
  • Removed cp-spawn-tagged.ts — the closure-form spawn-closure-multi-session subsumes it
  • child_process.spawnTagged(...) now emits a clear compile error referencing the closure form

Depends on #560.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 19, 2026

Benchmark Results (Linux x86-64)

Benchmark C ChadScript Go Node Place
Binary Trees 1.340s 1.266s 2.688s 1.160s 🥈
Cold Start 1.2ms 0.8ms 1.2ms 25.9ms 🥇
Fibonacci 0.814s 0.814s 1.564s 3.199s 🥇
File I/O 0.121s 0.092s 0.087s 0.199s 🥈
JSON Parse/Stringify 0.004s 0.005s 0.017s 0.016s 🥈
Matrix Multiply 0.474s 0.992s 0.560s 0.363s #4
Monte Carlo Pi 0.389s 0.410s 0.404s 2.248s 🥉
N-Body Simulation 1.665s 2.118s 2.206s 2.552s 🥈
Quicksort 0.214s 0.244s 0.213s 0.268s 🥉
SQLite 0.354s 0.373s 0.407s 🥈
Sieve of Eratosthenes 0.014s 0.024s 0.018s 0.038s 🥉
String Manipulation 0.008s 0.019s 0.016s 0.036s 🥉

CLI Tool Benchmarks

Benchmark ChadScript grep node xxd Place
Hex Dump 0.560s 1.051s 0.131s 🥈
Recursive Grep 0.020s 0.010s 0.101s 🥈

cs01 added 2 commits April 19, 2026 10:19
…remove spawnTagged (pr2 of closure-cabi series)

arrow-function callbacks now work with child_process.spawn(). captured
variables are lifted into a gc-allocated env struct, registered with the
trampoline slot table (pr1 infrastructure), and dispatched through a
per-shape trampoline that the c bridge invokes after recovering env via
cs_tramp_get(handle).

this removes a long-standing constraint that spawn callbacks had to be
named function references. multi-session demuxing (formerly spawnTagged,
which is hard-removed here) is now a natural capture:

  child_process.spawn(cmd, args,
    (d) => onOut(sessionId, d),
    (d) => onErr(sessionId, d),
    (c) => onExit(sessionId, c))

changes:
- c_bridges/child-process-spawn.c: cs_spawn_v2 takes per-callback
  {fn_ptr, tramp_handle} pairs. handle == -1 keeps the legacy bare-fn
  path; handle >= 0 routes through the trampoline. slots freed in the
  pipe-close / post-exit callbacks.
- src/codegen/stdlib/child-process.ts: spawn() accepts arrow functions
  alongside variable refs; generates trampoline env + cs_tramp_alloc.
- src/codegen/infrastructure/trampoline-emitter.ts: instantiated on the
  llvm generator, emitAll() called after lifted lambdas. stores user fn
  as i8* to sidestep the store-type validator.
- chadscript.d.ts, llvm-declarations.ts: spawnTagged and cs_spawn_tagged
  removed. spawn signatures updated to document closure support.
- src/codegen/expressions/method-calls/named-object-dispatch.ts: emits a
  clear compile error if anyone still calls spawnTagged.
- tests/fixtures/closures-cabi/: three new fixtures cover single-session
  closure capture, multi-session demux, and named-fn back-compat.
- tests/fixtures/builtins/cp-spawn-tagged.ts removed (closure form
  subsumes it).
…DE.md rule #5)

stage1 self-compiled binary crashed on Array.isArray because inserting a
new field mid-class shifted GEP indices for subsequent fields (most
notably lastInlineLambdaEnvPtr in BaseGenerator, although the same rule
applies to LLVMGenerator's own fields). moving trampolineEmitter to the
END of both IGeneratorContext and LLVMGenerator's field lists restores
the expected struct layout and unblocks stage2 self-hosting.
@cs01 cs01 force-pushed the feat/trampoline-closures-pr2 branch from e0ba0e5 to 8d745e7 Compare April 19, 2026 17:19
@cs01 cs01 merged commit 879c7d4 into main Apr 19, 2026
13 checks passed
@cs01 cs01 deleted the feat/trampoline-closures-pr2 branch April 24, 2026 20:53
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