Skip to content

aot: state the VCALL verdict in level-1 lowered code - #743

Merged
tannevaled merged 2 commits into
mainfrom
wave53-aot-vcall
Sep 29, 2026
Merged

tannevaled merged 2 commits into
mainfrom
wave53-aot-vcall

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The same source answered differently depending on whether it had been through rbgo build.

def probe_bare; nope_xyz; end
probe_bare
MRI 4.0.5 NameError: undefined local variable or method 'nope_xyz' for main
interpreted NameError ✅
AOT-built NoMethodError ❌

A program changing meaning because it was compiled is the worst shape a divergence can take: neither lane is obviously wrong to look at, and nothing in the source says which one you are running.

Cause

internal/aot/codegen.go lowers OpSend to a bare dispatchSend preceded by setSendNoKW, so the call site's keyword verdict travelled into generated code and its VCALL verdict did not.

The level-2 lane already restates it per send (aotSend). This is the level-1 lane, which lowers whole method bodies and has no OpSend handler to inherit it from.

Fix

VM.setSendVCall joins setSendNoKW as a seam generated code can call, and codegen emits both verdicts together.

How it was reproduced — and why a first attempt could not have found it

Through rbgo build, before and after. A runtime probe — call the method a few thousand times and watch for it to be lowered — can never see this: level-1 lowering happens at build time, not at execution, so that probe was measuring the interpreter no matter how long it ran.

The control, read carefully

nope_xyz() answers NameError in both lanes on this branch. That is the ast.Call.Paren gap which #742 closes, not this one.

What this change fixes is the two lanes disagreeing. Whether the answer they now agree on is MRI's depends on #742 landing.

Test

Both directions — FlagSendVCall set and unset — because emitting the call with a constant would satisfy a one-sided check. Neither subtest skips, verified explicitly, since a skipped subtest reads as a pass.

Ablated: with the codegen line reverted, both fail naming the missing verdict:

--- FAIL: TestLoweredSendStatesTheVCallVerdict/bare_identifier
    lowered source does not state the verdict "vm.setSendVCall(true)"
--- FAIL: TestLoweredSendStatesTheVCallVerdict/ordinary_call

It also asserts the keyword verdict is still stated beside it, so adding one cannot displace the other.

Gates

go test ./... green on Go 1.27.1 with CGO_ENABLED=0; gofmt clean; GOOS=wasip1, GOOS=js GOARCH=wasm and -tags rbgo_closed ./internal/... all build.

🤖 Generated with Claude Code

tannevaled and others added 2 commits September 29, 2026 12:32
The same source answered differently depending on whether it had been through
`rbgo build`:

    def probe_bare; nope_xyz; end
    probe_bare
      MRI          NameError: undefined local variable or method 'nope_xyz'
      interpreted  NameError
      AOT-built    NoMethodError      <-- the lowered lane disagreed

internal/aot/codegen.go lowers OpSend to a bare dispatchSend preceded by
setSendNoKW, so the keyword verdict of the call site travelled and the VCALL
verdict did not. The level-2 lane already restates it per send (aotSend); this
is the level-1 lane, which lowers whole method bodies and has no OpSend handler
to inherit it from.

So VM.setSendVCall joins setSendNoKW as a seam generated code can call, and
codegen emits both verdicts together. Reproduced through `rbgo build` before and
after -- the runtime probe that first looked for this could never have found it,
since level-1 lowering happens at build time and not at execution.

The parenthesised form is the control worth reading carefully: `nope_xyz()`
answers NameError in BOTH lanes on this branch, which is the ast.Call.Paren gap
that #742 closes, not this one. What this change fixes is the lanes DISAGREEING;
whether the answer they agree on is MRI's depends on #742 landing.

The unit test asserts both directions, because emitting the call with a constant
would satisfy a one-sided check, and neither subtest skips. Ablated: with the
codegen line reverted, both fail naming the missing verdict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-function coverage ratchet failed this PR on both POSIX lanes, naming
one function:

    internal/vm/vm.go  setSendVCall  0.0% (was fully covered)

That is the gate working, and the diagnosis is exact: the seam is called only
from AOT-GENERATED Go, which no unit test compiles or runs, so nothing in the
suite reached it. Its sibling setSendNoKW is at 100% because three tests drive
it directly -- wave29_kwargs_test.go, aot_e2e_gen_test.go and
wave_g_vcall_test.go -- and the new one had no such test.

Covered by exercising the BEHAVIOUR, not the line: a compiled body of exactly
the shape codegen.go now emits for a bare identifier -- both verdicts stated,
then a bare dispatchSend -- following the pattern
TestWaveGVCallVerdictIsClearedForACompiledBody already uses. Both directions,
because a seam that always said "true" would satisfy one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 4870628 into main Sep 29, 2026
9 checks passed
@tannevaled
tannevaled deleted the wave53-aot-vcall branch September 29, 2026 20:03
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