Skip to content

a function address answers the function in every tier; a module-qualified builtin call stops crashing JIT codegen - #3974

Merged
borisbat merged 3 commits into
masterfrom
aleksisch/fix-reported-bugs
Sep 9, 2026
Merged

a function address answers the function in every tier; a module-qualified builtin call stops crashing JIT codegen#3974
borisbat merged 3 commits into
masterfrom
aleksisch/fix-reported-bugs

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Behavior change: reinterpret on a function address answers the function in every tier. The JIT caches invalidate once (codegen 0x78).

Why. A function's address kept as an opaque handle answered 0 in the interpreter once stored, would not JIT, and was a stack address under AOT. A module-qualified builtin call - math::sqrt - killed JIT codegen or emitted the wrong function.

What changes.

  • Intrinsic emitters key off the declared name, not the call-site spelling.
  • A name the emitter builds goes through intrinsic_id, which panics naming it; three handlers stop using the builder's null.
  • visitExprCast gains function <-> pointer / int64 arms through the { ptr } aggregate.
  • The two function-address SimNodes implement their pointer and 64-bit slots.
  • das_cast<TT*> reads a Func's target, not the temporary's address.

Observable behavior.

  • interp: reinterpret<uint64>(@@fn) once stored -> 0 -> the address.
  • -jit: math::sqrt(x) -> SIGSEGV -> runs; math::min -> max -> min.
  • -jit: that cast -> Invalid bitcast -> compiles.
  • AOT: reinterpret<void?>(@@fn) -> a stack address -> the function.
  • an intrinsic name LLVM has none of -> SIGSEGV -> no LLVM intrinsic named '<name>'.

Where to look. llvm_jit_intrin.das's ten key sites and the visitExprCast arms.

Fixes #3968
Fixes #3969
Fixes #3970

Validation, claims, ledger

Validation

  • Full local lanes on the tip, all green: tests-interp, tests-jit, tests-aot (the full test_aot build and sweep, which per-PR CI does not run), docs, utils-tests, tests-cpp, compile-sweep (759 roots).
  • Every intrinsic key and every new cast branch carries a negative control: the key was mutated back to the call-site spelling one at a time and the new test failed - math::min returned 7 for min(3, 7), llvm.math::sqrt.f32 was reported as no intrinsic. The AOT overload was controlled by building test_aot_subset at the base, where func_addr.das fails exactly one assert, with a stack address.
  • modules/dasLLVM/tests is 83/84. The failure is test_vector_log_special_values, which fails identically on origin/master: it pins the aarch64 inline rail's non-IEEE log2 specials and this host is x86_64.
  • The external codex round was skipped - no codex on this box.
  • The untracked gate is red on 1803 files, all pre-existing scratch in the working tree, none of it staged.

Claims - stated, not tested

  • The aarch64 vector arms. On aarch64 a qualified cos, cosh or tanh selected a neighbouring arm before this change - build_vector_hyper dispatches on the name, so a qualified tanh emitted sinh. This host is x86_64, so that path never ran here. Verified by reading the dispatch, and pinned by asserting each arm against the value its named function has at zero, where the neighbouring arm answers a different number. The emitted-vs-interpreted cell over every vector width that modules/dasLLVM/REVIEW.md asks for needs an ARM box. A break shows as test_qualified_vector_arm_identity failing on aarch64.
  • The intrinsic_id panic. No committed test reaches it, because after the name fix no call site builds a name LLVM has none of. Verified by reverting the name fix alone, which turns the SIGSEGV into no LLVM intrinsic named 'llvm.math::sqrt.f32' at the panic's own line, with exit 1. A break returns to a hard crash with no line. The 19 target-specific literals keep the raw lookup and their fallbacks: a trimmed LLVM_TARGETS_TO_BUILD is the one case where a missing intrinsic is not an emitter defect, and on a full LLVM every buildable name resolves (probed: 84 of 84, foreign targets included).
  • A daslang defect found while writing this, left alone: an inline cond ? "a" : "b" passed as the name argument of LLVMBuildInsertValue crashes codegen with a SIGSEGV at 0x50, while a literal, or the same ternary hoisted into a let, is fine. A standalone def take(s : string) does not reproduce it, so it needs the extern string-substitution path. The emitter uses the let spelling; the defect is unreported and unfixed.

Not done

  • daslib/REVIEW.md's pair rules only trigger from the daslib side, so a C++-side change routed there has no receiving rule. Merging them into one side-agnostic rule, and recording the das_cast/Func pair in daslib/ARCHITECTURE_EMIT.md sec. 5, is queued rather than done here.
  • modules/dasLLVM/REVIEW.md's two platform rules trigger on get_platform_name() and on the target triple, while the emitters branch on the resolved g_target_* globals a step later. A property-shaped criterion covering that gap is queued.
  • Two test-discipline rules the comment harvest proposed are queued, not landed: a backend test feeds its operands from mutable globals, and a test's expected value never comes from the spelling under test. Neither tests/math/ nor tests/language/ has a checklist today, and creating the first one there is not this change's call.
  • The expr.name-as-key check is mechanically decidable from the tree and belongs in modules/dasLLVM/REVIEW.das as a gate. Not written here.
  • A selftest enumerating every intrinsic name the emitter can build, asserting each resolves, would have caught this with no JIT run at all. Queued.
  • failed_E records and the visit continues; the error list is read only after the whole function. Making a reported failure stop emission is a separate change.

…value casts through its { ptr } aggregate

Ten intrinsic handlers keyed on `expr.name`, the call-site spelling, while the dispatch tables
key on the declaration. A module-qualified call kept its qualifier: as an intrinsic-name
fragment that built `llvm.math::sqrt.f32`, which LLVM does not know and the C API dereferenced
anyway; as a branch key it picked the other arm, so `math::min` emitted max. They read
`expr.func.name` now, and the wrapper refuses intrinsic id 0.

A missing intrinsic is no state to return: `LLVMLookupIntrinsicID` answers 0 only for a name
that is no intrinsic, and LLVM's table is target-independent, so a foreign target's names
resolve on any host built with it. It means the emitter built the name wrongly, and no caller
can recover. The twenty-two lookups of a name the emitter builds go through `intrinsic_id`,
which panics naming the string; the nineteen target-specific literals keep the raw lookup and
their own fallback, since a trimmed LLVM can genuinely lack those. Three handlers also stop using the null the intrinsic builder returns for
an operand type it does not serve, which is a different and reachable path.

`visitExprCast` had no arm for `Type.tFunction`, so a function-address reinterpret emitted a
bitcast between the `{ ptr }` aggregate and a scalar and the program failed to simulate. Two
arms go through element 0 instead.

Fixes #3968
Fixes #3969
…a silent 0

The two `@@fn` const nodes left every typed slot as `DAS_ASSERT(0); return 0;`, and
`DAS_NO_ASSERTIONS` is on in Release - so `reinterpret<uint64>(@@fn)` read a silent 0 in every
store position, while the same cast through a parameter, and the JIT and AOT tiers, answered
the address. `evalPtr`, `evalInt64` and `evalUInt64` compute it; the narrower slots keep the
assert.

`tests/language/func_addr.das` covers the store positions and the handle round-trip, and
`func_addr_solid.das` covers the solid-context node, which no test in the tree reached. This
folder's checklist gets three fixes its own audit of this change found.

Fixes #3970
…y's address

`das_cast<TT*>` bound a `Func` to its catch-all `cast(const QQ &)`, which returns `&expr`. The
emitter spells `@@fn` as a `Func(...)` temporary, so `reinterpret<void?>(@@fn)` answered a
stack address where the interpreter and the JIT answer the function. A `Func` overload reads
`PTR`, the way the smart-pointer overloads beside it read `get()`.

The two function-address tests gain their `void?` cells, which could not be green before this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment