Skip to content

fix(lower-go): unbox a native prim's direct-call result into a typed slot - #1084

Merged
mparrett merged 2 commits into
mainfrom
fix/1081-native-prim-unbox
Oct 21, 2026
Merged

mparrett merged 2 commits into
mainfrom
fix/1081-native-prim-unbox

Conversation

@mparrett

@mparrett mparrett commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Why

#1057 gave aget, alength and byte-array guarded direct calls to their Go entries, taken while rt.NativePrimsIntact() holds. Those entries (rt.CoreAgetf and friends) return (vm.Value, error), but typeinfer can give the call's node a native slot: float64 for aget on a ^doubles array, int64 for alength, *vm.TypedArray for a byte-array local. typed-call-result-stmts assigns the vm.Value straight into that slot, so the lowered Go doesn't build:

if rt.NativePrimsIntact() {
	arg__7, callErr = rt.CoreAgetf(arg0, arg__6)   // arg__7 is float64
	...
} else {
	tb23, callErr = ec.Invoke2(rt.CachedVarFnEC(ec, &__v_clojure_core_aget, ...), arg0, arg__6)
	...
	tv23, tk23 = tb23.(vm.Float)
	...
	arg__7 = float64(tv23)
}

The else branch is already right: it reuses trampoline-call-stmts, which unboxes through boxed-call-stmts since #1044. The direct branch never got the same step.

What

The (vm.Value, error) case of typed-call-result-stmts calls boxed-call-stmts instead of emit-call-stmts. Both branches now take the result into tb<nid>, check the error, and comma-ok assert it to the slot's vm type, returning a vm.TypeError on mismatch. They register the same temp names and types, and the prologue's distinct emits each declaration once.

A vm.Value slot or a dead result goes through emit-call-stmts unchanged, so only sites that were Go type errors emit different code.

Only the Go-AOT lowerer changes. pkg/compiler and *ir-compile* are unaffected.

Verification

  • New fixture test/gogen/native_prim_typed_slot.lg: aget with an index boxed by a var, alength of an unhinted array into an int slot, and a byte-array local filled in a loop. On main its lowered Go fails to build at all three sites. With this change the gogen trampoline builds it and matches the bytecode VM ([13.0 3.0 [0 2 4 6]]).
  • All 24 test/gogen fixtures pass the trampoline.
  • make check-generated-manifest and make lint pass; go test ./pkg/ir/ passes. I didn't run the full make test locally.
  • The 12-line repro from lg compile: aget with a boxed index emits Go that does not build #1081 builds and prints 13.0.
  • phosphene's kernels, which hit this at 37 sites, compile with lg compile with no workarounds, and the native binary renders plasma and milk scenes.

Fixes #1081

🤖 Generated with Claude Code

@mparrett
mparrett requested a review from nnunley October 19, 2026 16:50

@nooga nooga left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. This fixes the root cause: the (vm.Value, error) branch now unboxes into a natively typed slot through boxed-call-stmts, the same path the trampoline branch has used since #1044. boxed-call-stmts falls back to emit-call-stmts when the slot is vm.Value or the result is dead, so only sites that used to be Go type errors emit different code. The tb/tv/tk temps are registered the same way in both branches, so the prologue doesn't get duplicate declarations. The fixture covers all three shapes from #1081, and the .lg edit is regenerated.

Nit for the record: this touches only the Go-AOT lowerer, so pkg/compiler and ir-compile are unaffected. A line saying so in the body would satisfy the three-paths note in AGENTS.md.

mparrett and others added 2 commits October 21, 2026 16:19
…slot

#1057 gave aget, alength and byte-array guarded direct calls to their Go
entries (rt.CoreAgetf and friends), taken while rt.NativePrimsIntact
holds. Those entries return (vm.Value, error), but typeinfer can give the
call's node a native slot: float64 for aget on a ^doubles array, int64
for alength, *vm.TypedArray for byte-array. typed-call-result-stmts
assigned the vm.Value straight into that slot, so the generated Go did
not build. The trampoline branch of the same if already unboxed, through
boxed-call-stmts (#1044).

The (vm.Value, error) case of typed-call-result-stmts now goes through
boxed-call-stmts too, so both branches take the result into tb<nid>,
check the error, and comma-ok assert it to the slot's type. A vm.Value
slot, or a dead result, emits what it did before.

test/gogen/native_prim_typed_slot.lg covers aget with a boxed index,
alength of an unhinted array, and a byte-array local. On main all three
sites fail to build.

Fixes #1081

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mparrett
mparrett force-pushed the fix/1081-native-prim-unbox branch from ba66471 to 8d5ac38 Compare October 21, 2026 23:20
@mparrett
mparrett merged commit 88a86e1 into main Oct 21, 2026
21 checks passed
@mparrett
mparrett deleted the fix/1081-native-prim-unbox branch October 21, 2026 23:29
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.

lg compile: aget with a boxed index emits Go that does not build

2 participants