fix(jit): a REPL fn calling a prior REPL binding no longer emits invalid IR (both backends) - #340
Merged
Conversation
… not double-free the module (ORC REPL segfault on fn after let-bound lambda)
…d (no fib speedup); close phase 2
… instead of killing REPL startup
…zardly-cray-333762
…lid IR (both backends)
A fn-defining fragment exposes prior bindings via prev-slot loader fns
(define @<name>()), but the EApp path knew nothing about them: the call
fell into the unknown-function fallback, which declared the very symbol
the loader defines in the same module — invalid redefinition, whole
fragment lost ("I cannot find `g`").
Route such calls through the loader + closure dispatch instead: a new
Llvm_ctx.repl_slot_fns table (populated by emit_slot_loader_fns) drives
an emit_atom arm that materialises the slot's current closure via the
loader, an EApp->ECallPtr redirect, and an exclusion in the ECallPtr
no-var-slot catch-all. No declare is emitted, and the call follows the
slot's current value instead of pinning the callee version compiled at
definition time (see the redefinition-semantics preference in
specs/progress/2026-08-24-repl-jit-fn-redefinition-silently-ignored.md).
4 new subprocess session tests in test/test_jit.ml (clang+orc x
fn-calls-prior-fn / fn-calls-let-lambda), failing-first.
…333762 # Conflicts: # CHANGELOG.md # test/dune # test/test_jit.ml
With ORC as the default (libLLVM present), run_codegen's 21 in-process
repl_jit_cross_line / repl_jit_regression tests failed: each test creates
its own Repl_jit session, but the ORC backend keeps one process-global
LLJIT, so the second session's repl_0 re-defines prelude-synthesized
symbols in the shared JITDylib ("duplicate definition of symbol
'_Eq$Int.eq'").
Pin set_backend_for_tests `Clang in setup_jit_runtime — the gate every
such test goes through — so they keep exercising the clang + dlopen
pipeline they were written against. ORC stays covered end-to-end by
test_jit.ml's subprocess sessions. The multi-session-per-process ORC
limitation is filed as
specs/todos/2026-08-24-orc-multi-session-per-process-duplicate-symbols.md.
CI red on both OSes: ensure_home did a non-recursive mkdir of dune's
HOME=%{project_root}/_build/jit_home, which expands to a RELATIVE path
whose parent does not exist from the test cwd — ENOENT, 6 of 13 tests.
Use the same per-pid tmp session_home the repl_session harness already
uses (now shared by both), passing it to the child REPL explicitly.
Ch4s3
added a commit
that referenced
this pull request
Aug 25, 2026
…urface Kept both harnesses in test_jit.ml (check_session + redefinition trio) under one suite; dune rule keeps MARCH_BIN + both source trees. Validated: cross_line 12/12, regression 15/15, test_jit 13/13, parity 16/16.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Same failure with a prior let-bound lambda as the callee, on both the clang and ORC backends. Interpreter mode (
MARCH_REPL_INTERP=1) was fine. Filed as the "Follow-up noticed, NOT fixed here" section ofspecs/progress/2026-08-24-orc-repl-segfault-fn-def-after-let-lambda.md.Root cause
Fn-defining REPL fragments expose prior bindings via module-level slot loaders (
Llvm_repl.emit_slot_loader_fnsemitsdefine ptr @f()), unlike expression fragments, which bridge slots intovar_slotallocas. TheEAppemission path knew about neithertop_fnsnorvar_slotentries forf, so the call fell into the unknown-function fallback, which emitteddeclare ptr @f(i64)for the very symbol the loader defines in the same module — invalid IR, whole fragment (includingg) lost.Note the collision partner is that fallback declare, not
partition_fns/extern_fns—fis absent from the second fragment's TIR entirely. Even without the collision, a direct declare+call would be wrong twice over: the erased call-site signature (ptr @f(i64)) mismatches the real fragment-0 define (i64 @f(i64)), and a direct extern call pins the callee version compiled atg's definition time instead of following the slot (the semantics2026-08-24-repl-jit-fn-redefinition-silently-ignored.mdsays to prefer).Fix
Route calls to slot-loader names through the loader + closure dispatch:
Llvm_ctx.repl_slot_fns— new ctx table;emit_slot_loader_fnsrecords each bare name it emits a loader for. Only REPL fn-fragment emission ever populates it, so non-REPL compilation is untouched.emit_atom: newAVararm — a reference to a slot-loader name emitscall <ty> @<name>(), materialising the slot's current value (the closure, for fn/lambda bindings). Placed above the runtime-prefix/builtin first-class arms so a REPL binding shadows a same-named builtin, exactly as the expression-fragmentvar_slotbridge does.EApp: new arm mirroring the existing var_slot→ECallPtrredirect.ECallPtrno-var-slot catch-all: excludes slot-loader names so they reach the generic closure-dispatch arm.No declare is emitted, and the call goes through the closure's
$clo_wrap(uniform tagged-ptr ABI — correct for odd/even Ints, Floats, Strings), reading the slot at call time.Testing
test/test_jit.ml({clang,orc} × {fn-calls-prior-fn, fn-calls-let-lambda}), written first and failing 4/4 before the fix; 8/8 green after. The harness was generalised to take a backend (clang sessions don't require libLLVM to run).k → g → f), odd/even Int results, String concat through two slot fns, Float chain, recursion + slot call, prior-letcapture, let-bound-lambda callee, helper-lambda callee — all correct.run_codegen: 587 run with 21 pre-existing failures that are ORC-by-default fallout from the included branch (in-process sessions sharing one LLJIT dylib collide on_Eq$Int.eqinrepl_0, before any slot loader exists; all 21 pass withMARCH_JIT_BACKEND=clang). A separate task is under way for that.Notes
claude/jit-repl-interpreted-perf-68373c(ORC default + internal-linkage slot loaders + pre-warm); origin's copy of that branch ref was stale, so those commits ride along here.fn f→fn gcallsf→ redefinef→g(x)picks up the new body.