Skip to content

perf(interp): expand JIT coverage for container ops and equality#158

Merged
siyul-park merged 14 commits into
mainfrom
perf/jit-struct-coverage
Jul 17, 2026
Merged

perf(interp): expand JIT coverage for container ops and equality#158
siyul-park merged 14 commits into
mainfrom
perf/jit-struct-coverage

Conversation

@siyul-park

@siyul-park siyul-park commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Expands ARM64 JIT coverage in four steps, plus two enabling fixes uncovered along the way.

1. Scalar STRUCT_SET becomes continuable

  • Trace capture keeps recording past a scalar struct-field store (cloneTarget reports it continuable; ref-field and post-call stores stay terminal).
  • structSet gains a continuable branch mirroring the primitive ARRAY_SET state-barrier pattern: one resumable snapshot shared by the shape/bounds/value/kind guards, aggressive guard-register recycling, owned-container rc release after all guards, deferred containers elide the release.
  • Hot loops mutating scalar struct fields now stay native instead of deopting every iteration.

2. REF_EQ/REF_NE/STRING_EQ/STRING_NE lower natively

  • Interned-string and ref equality are boxed-word compares. The new refEq lowering keeps the compare native with at most one owned operand (single in-place release, like REF_IS_NULL) and falls back terminally with two owned operands (double-release hazard across a deopt).

3. Bulk mutations stop killing traces

  • ARRAY_FILL/ARRAY_COPY/ARRAY_APPEND/MAP_SET move from trace abort to ERROR_NEW-style terminal boundaries: the compiled prefix runs native, the threaded handler performs the mutation, and the loop re-enters at the header.
  • Loop anchors now also accept a returned straight-line root, so such loops compile their prefix as a per-entry fragment.

4. STRUCT_GET works in static plans

  • The static frontend tracks declared struct types and known i32 constants, so a constant-index field read resolves its result kind and compiles without trace warmup. Runtime itab/type/field-kind guards are unchanged.

Enabling fixes

  • asm: the linear-scan rewriter never released a vreg whose final reference was a def, leaking one register per dead store; long no-spill loop emissions exhausted the bank and rejected with register pressure. Dead final defs now release.
  • interp: string constants register their pool cell in the intern table at load, so runtime interning and compile-time constant embedding agree on one canonical heap address (also removes a latent stale-embedded-ref hazard).

Test plan

  • go test -race ./... (all packages)
  • New parity+refcount suites: TestARM64_StructSetLoop, TestARM64_RefEqLoop, TestARM64_TerminalMutationLoop, TestARM64_StructGetStaticPlan (each diffs results and exact refcounts against a threaded twin and asserts native entries)
  • Trace-shape pins: scalar STRUCT_SET continuable, ref-field still terminal, bulk mutation records a terminal boundary, allocation still aborts
  • Planner pins: static STRUCT_GET kind resolution, unknown-index rejection
  • make fuzz (bounded parity soak), make lint, make benchmark-pr (no regression; JITWarm ≈ Fused on the quick suite)
  • make coverage-check fails from the known goenv go1.25/1.26 toolchain split, unrelated to this diff

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance
    • Improved native ARM64 lowering for REF_EQ/REF_NE and STRING_EQ/STRING_NE, with correct terminal fallback and refcount/ownership handling.
    • Treated bulk mutations (ARRAY_FILL, ARRAY_COPY, ARRAY_APPEND, MAP_SET) as terminal boundaries to keep native prefixes safer.
    • Enhanced STRUCT_SET continuability in traces and improved static STRUCT_GET planning.
    • Released destination registers at last use to avoid lingering bindings.
  • Documentation
    • Updated opcode reference and JIT internals for native vs terminal behavior across comparisons, struct access, and bulk operations.
  • Tests
    • Expanded ARM64 parity and tracer/plan coverage; refactored and renamed multiple test suites and adjusted assertions.
    • Removed interpreter parity fuzz and several removed example tests; added pool benchmarks.

The linear-scan rewriter released a virtual register only when its last
reference appeared as a read; a value whose final reference was a write
kept its physical register bound for the rest of the stream. Long
no-spill emissions (trace loops with many blocks) leaked one register
per dead store and rejected compilation with register pressure.
Scalar struct-field stores now mirror the primitive ARRAY_SET pattern:
capture keeps recording past the store (cloneTarget reports a scalar
struct store as continuable), and lowering emits a state barrier whose
shape/bounds/value/kind guards share one resumable snapshot before the
guarded store. Ref-field stores and stores after inlined calls remain
terminal. Hot loops that mutate scalar struct fields now stay native
instead of deopting every iteration.
Threaded CONST_GET interns a string at runtime while the JIT embeds the
constant-pool ref, so the two runtimes disagreed on a string's canonical
heap address (and an embedded interned ref could dangle after a free).
Registering each string constant's pool cell in the intern table at load
makes runtime interning return the pinned pool address, aligning both
paths for the interpreter's whole lifetime.
…4 JIT

Ref and interned-string equality are boxed-word compares. The new refEq
lowering keeps the compare native when at most one operand owns its
retain (releasing the one owned operand in place, like REF_IS_NULL) and
falls back terminally when both operands are owned, where releasing both
natively could double-release across a deopt.
ARRAY_FILL, ARRAY_COPY, ARRAY_APPEND, and MAP_SET previously aborted the
whole trace, so a hot loop containing one of them never went native.
They now record as ERROR_NEW-style terminal boundaries: the trace stops
without stepping the clone, the JIT lowers the op to an unconditional
deopt, and the threaded handler performs the real mutation. Loop anchors
additionally accept a returned straight-line root, so such a loop
compiles its prefix natively and re-enters at the header each iteration.
Any function containing STRUCT_GET was rejected by the static frontend
because the opcode's generic KindAny result aborted the dataflow pass.
The planner now tracks declared struct types and known i32 constants, so
a constant-index field read of a struct-typed local, upvalue, or known
heap struct resolves its result kind and compiles without trace warmup.
The synthesized step.seen only conveys the kind; the lowering's runtime
itab, type-pointer, and per-field kind guards stay in place.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR expands ARM64 JIT support for comparisons, scalar struct stores, terminal mutations, and static struct access. It also updates tracing, runtime string interning, register release, documentation, benchmarks, and broadens or reorganizes test coverage.

Changes

ARM64 JIT planning and execution

Layer / File(s) Summary
Static struct access planning
interp/jit_plan.go, interp/jit_plan_test.go, docs/jit-internals.md
Static planning tracks declared struct types and constant indices to resolve STRUCT_GET field kinds and validate static plans.
Trace mutation boundaries
interp/trace.go, interp/trace_test.go, docs/jit-internals.md
Primitive and scalar stores can remain continuable, while bulk and reference-bearing mutations become terminal trace boundaries.
ARM64 comparison and store lowering
interp/jit_arm64.go, interp/jit_arm64_test.go, docs/instruction-set.md
Native boxed comparisons are added with ownership-sensitive fallback; scalar STRUCT_SET can continue natively, and bulk mutations deopt at terminal boundaries.
Runtime support and register lifetime
interp/interp.go, interp/interp_test.go, asm/rewriter.go
Boxed string constants are interned, duplicate interning is tested, and unused destination registers are released.
Validation, benchmarks, and test maintenance
interp/*_test.go, types/*_test.go, benchmarks/*, analysis/blocks_test.go, asm/arm64/encoder_test.go, Makefile
Parity tests, type assertions, benchmark organization, test naming, and fuzz targets are updated; pool benchmarks are added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Tracer
  participant StaticPlanner
  participant ARM64JIT
  participant Interpreter
  Tracer->>StaticPlanner: capture mutation and struct access metadata
  StaticPlanner->>ARM64JIT: emit native plan
  ARM64JIT->>ARM64JIT: compare refs or store scalar struct field
  ARM64JIT->>Interpreter: deopt at owned or bulk mutation boundary
  Interpreter-->>ARM64JIT: resume execution at fallback opcode
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main JIT coverage expansion in the patch.
Description check ✅ Passed The description covers the main changes and test plan, though it doesn't use the repository's exact section headings or include related issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/jit-struct-coverage

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.46154% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.68%. Comparing base (1d652d6) to head (6169d12).

Files with missing lines Patch % Lines
interp/jit_plan.go 50.00% 22 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #158      +/-   ##
==========================================
+ Coverage   29.63%   29.68%   +0.04%     
==========================================
  Files          86       86              
  Lines       60292    60350      +58     
==========================================
+ Hits        17870    17917      +47     
- Misses      41146    41156      +10     
- Partials     1276     1277       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
interp/jit_arm64_test.go (1)

2201-2325: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every newly enabled opcode in the parity suites.

REF_NE, ARRAY_COPY, and ARRAY_APPEND are lowered by this PR but never emitted here. Add parity subtests for their result, refcounts, and native entry/deopt behavior.

As per coding guidelines, test changes must follow the ownership and opcode-coverage guidance in docs/testing.md.

Also applies to: 2332-2410

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interp/jit_arm64_test.go` around lines 2201 - 2325, Add parity subtests
alongside the existing cases in TestARM64_RefEqLoop for REF_NE, ARRAY_COPY, and
ARRAY_APPEND, emitting each newly lowered opcode through the program.Builder
helpers. Validate threaded versus JIT results and reference counts via
runParity, and ensure each case exercises native entry and deoptimization
behavior while following the ownership rules in docs/testing.md.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@interp/interp.go`:
- Around line 305-313: Update constant-pool loading around the default branch
and i.interned so duplicate types.String values reuse the previously interned
boxed reference instead of allocating another cell. Ensure both compile-time
constants and runtime interning resolve equal strings to the same address, while
preserving unique-string allocation and interpreter lifetime behavior. Add a
regression test with two separate constant-pool entries containing identical
string values.

---

Nitpick comments:
In `@interp/jit_arm64_test.go`:
- Around line 2201-2325: Add parity subtests alongside the existing cases in
TestARM64_RefEqLoop for REF_NE, ARRAY_COPY, and ARRAY_APPEND, emitting each
newly lowered opcode through the program.Builder helpers. Validate threaded
versus JIT results and reference counts via runParity, and ensure each case
exercises native entry and deoptimization behavior while following the ownership
rules in docs/testing.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: da3e65df-ebc4-4792-ac81-ef48121611b9

📥 Commits

Reviewing files that changed from the base of the PR and between 1d652d6 and 3059f19.

📒 Files selected for processing (10)
  • asm/rewriter.go
  • docs/instruction-set.md
  • docs/jit-internals.md
  • interp/interp.go
  • interp/jit_arm64.go
  • interp/jit_arm64_test.go
  • interp/jit_plan.go
  • interp/jit_plan_test.go
  • interp/trace.go
  • interp/trace_test.go

Comment thread interp/interp.go

@siyul-park siyul-park left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed the full change set at 3059f19a69511a7b0315cf101ccf3d1e6fda9de0.

One blocking correctness issue remains in interp/interp.go: duplicate equal types.String entries in the constant pool are each allocated before i.interned is updated, so the constant slots can retain different heap references. Both threaded and native STRING_EQ compare boxed references, which makes two equal string constants compare unequal when the pool was constructed directly (for example with program.WithConstants). The existing unresolved inline thread on lines 305–313 correctly identifies this.

The current equality tests use program.Builder, whose Const method deduplicates comparable equal values, so they cannot expose the bug. Please canonicalize string constants through i.intern before assigning the constant slot and add a regression with two distinct constant-pool entries containing the same string, covering at least threaded execution and ARM64 JIT parity.

I did not find another blocking correctness defect in the register-release, scalar STRUCT_SET, terminal-mutation, or static STRUCT_GET changes from source inspection. The Remote Desktop Commander host was offline, so I could not independently rerun the repository checks locally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@interp/interp_test.go`:
- Around line 3999-4003: Strengthen the “interns duplicate string constants”
test by asserting observable string-constant behavior rather than equality of
private i.constants entries. Verify both constants are valid KindRef values,
then compare their Ref() results or dereferenced strings, using the
interpreter’s public evaluation/access path instead of directly relying on
i.constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7612a6ff-2156-486e-a6f2-d19a3bf14a62

📥 Commits

Reviewing files that changed from the base of the PR and between 3059f19 and d8b9786.

📒 Files selected for processing (2)
  • interp/interp.go
  • interp/interp_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • interp/interp.go

Comment thread interp/interp_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
interp/jit_plan_test.go (1)

132-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep helper cases under a single top-level test.

TestMergeSlot, TestStore, and TestNoSpill test unexported helpers and were split from grouped subtests into separate top-level tests. Keep these cases under the owning top-level test using t.Run instead.

As per coding guidelines, **/*_test.go: define one top-level test per public symbol and put sub-cases under t.Run rather than splitting them into parallel top-level tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interp/jit_plan_test.go` around lines 132 - 166, Group the helper coverage
for mergeSlot, store, and noSpill under a single owning top-level test,
replacing TestMergeSlot, TestStore, and TestNoSpill with t.Run subtests.
Preserve each subtest’s existing assertions and inputs while ensuring the file
has one top-level test for these unexported helper cases.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@interp/jit_plan_test.go`:
- Around line 132-166: Group the helper coverage for mergeSlot, store, and
noSpill under a single owning top-level test, replacing TestMergeSlot,
TestStore, and TestNoSpill with t.Run subtests. Preserve each subtest’s existing
assertions and inputs while ensuring the file has one top-level test for these
unexported helper cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 61f443cc-8238-4dfc-acf8-87aedb78a420

📥 Commits

Reviewing files that changed from the base of the PR and between d8b9786 and 94c440b.

📒 Files selected for processing (4)
  • interp/example_test.go
  • interp/fuzz_test.go
  • interp/jit_arm64_test.go
  • interp/jit_plan_test.go
💤 Files with no reviewable changes (2)
  • interp/fuzz_test.go
  • interp/example_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • interp/jit_arm64_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
interp/jit_arm64_test.go (1)

1957-1976: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Register cleanup before running assertions.

A require failure exits the helper before Lines 1975-1976, leaking both interpreters and native resources into later subtests.

Proposed fix
 	jit := New(prog, WithTick(1), WithThreshold(0), WithProfiler(profile))
+	t.Cleanup(func() { require.NoError(t, jit.Close()) })
 	threaded := New(prog, WithTick(1), WithThreshold(-1))
+	t.Cleanup(func() { require.NoError(t, threaded.Close()) })
@@
-	require.NoError(t, jit.Close())
-	require.NoError(t, threaded.Close())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interp/jit_arm64_test.go` around lines 1957 - 1976, Update the runParity
helper to register cleanup for both jit and threaded immediately after they are
created, before any assertions or Run calls. Ensure the cleanup closes both
interpreters and releases their native resources even when a require assertion
fails, while preserving the existing comparison loop.
🧹 Nitpick comments (1)
interp/pool_test.go (1)

560-581: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid asserting private stub state.

Use profiler/cache outcomes plus execution behavior to establish the warmed shared-JIT condition, rather than coupling the benchmark to Interpreter.stub internals. As per coding guidelines, tests must assert observable behavior rather than private implementation shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interp/pool_test.go` around lines 560 - 581, Update the benchmark setup and
assertions around pool.Get to stop calling first.stub(0) and second.stub(0).
Establish the warmed shared-JIT condition using profiler or cache outcomes, then
verify it through observable execution behavior while preserving the existing
allocation measurement and pool.Get timing checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@interp/jit_arm64_test.go`:
- Around line 2060-2069: Update the checkExits assertion in the profile.Metrics
loop to require that vm_jit_native_exits_total contains a reason label with
value "loop-exit". Track whether this matching metric/label is observed and
assert it after scanning all metrics, while preserving the existing label
validation.

In `@interp/pool_test.go`:
- Around line 501-526: Assert the errors from pool.Get and pool.Close
immediately within each benchmark iteration so failures cannot be overwritten by
later iterations. In interp/pool_test.go lines 501-526, check both results
before the next loop iteration; in lines 551-589, assert pool.Close in each
iteration instead of retaining only the final error.

---

Outside diff comments:
In `@interp/jit_arm64_test.go`:
- Around line 1957-1976: Update the runParity helper to register cleanup for
both jit and threaded immediately after they are created, before any assertions
or Run calls. Ensure the cleanup closes both interpreters and releases their
native resources even when a require assertion fails, while preserving the
existing comparison loop.

---

Nitpick comments:
In `@interp/pool_test.go`:
- Around line 560-581: Update the benchmark setup and assertions around pool.Get
to stop calling first.stub(0) and second.stub(0). Establish the warmed
shared-JIT condition using profiler or cache outcomes, then verify it through
observable execution behavior while preserving the existing allocation
measurement and pool.Get timing checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 680d14b8-c463-434e-9ec0-03a345c99cc4

📥 Commits

Reviewing files that changed from the base of the PR and between 94c440b and 667316f.

📒 Files selected for processing (4)
  • interp/interp_test.go
  • interp/jit_arm64_test.go
  • interp/pool_test.go
  • interp/trace_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • interp/trace_test.go

Comment thread interp/jit_arm64_test.go
Comment thread interp/pool_test.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/benchmark_test.go`:
- Around line 93-110: Update the benchmark loop around vm.Run and vm.PopBoxed to
detect the first non-nil runErr or popErr and stop iterating immediately,
preserving that failure for the existing assertions after the loop. Ensure the
VM is reset appropriately before exiting and avoid allowing later iterations to
overwrite the captured error.

In `@interp/interp_test.go`:
- Around line 2526-2660: Replace the parity assertions in the parityCases test
with observable behavior checks rather than private interpreter state such as
fr, stack layout, interned size, or aggregate rc counts. For each case, compare
returned values, errors, globals, resumability, and subsequent reference
usability across WithTick(1) and WithThreshold(-1), including equivalent trap
behavior. Apply the same approach to the additional parity test range noted in
the review, while preserving coverage of ownership and execution semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 432a42ba-978c-4baa-9ef0-03a9b73bd6d0

📥 Commits

Reviewing files that changed from the base of the PR and between 667316f and 33fceff.

📒 Files selected for processing (13)
  • Makefile
  • analysis/blocks_test.go
  • asm/arm64/encoder_test.go
  • benchmarks/benchmark_test.go
  • benchmarks/call_test.go
  • benchmarks/control_test.go
  • benchmarks/memory_test.go
  • benchmarks/numeric_test.go
  • cli/display_test.go
  • interp/interp_test.go
  • types/error_test.go
  • types/primitive_test.go
  • types/string_test.go
💤 Files with no reviewable changes (6)
  • cli/display_test.go
  • Makefile
  • benchmarks/control_test.go
  • benchmarks/memory_test.go
  • benchmarks/numeric_test.go
  • benchmarks/call_test.go

Comment thread benchmarks/benchmark_test.go
Comment thread interp/interp_test.go
Comment on lines +2526 to +2660
type parityState struct {
code types.ErrorCode
ip int
fp int
sp int
stack []types.Boxed
globals []types.Boxed
live int
interned int
}

t.Run("fused ref drops preserve exact ownership", func(t *testing.T) {
type snapshot struct {
sp int
stack []types.Boxed
live int
interned int
}
run := func(t *testing.T, prog *program.Program, opts ...func(*option)) snapshot {
t.Helper()
i := New(prog, opts...)
defer i.Close()
require.NoError(t, i.Run(context.Background()))
live := 0
for _, rc := range i.rc[1:] {
if rc > 0 {
live += rc
huge := int64(1) << 50
fn := types.NewFunctionBuilder(nil).Emit(instr.New(instr.RETURN)).MustBuild()
parityCases := []struct {
name string
prog *program.Program
err error
}{
{
name: "promoted i64 eqz branch preserves state",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.I64_EQZ),
instr.New(instr.BR_IF, 0),
}),
},
{
name: "promoted i64 comparison branch preserves state",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.I64_EQ),
instr.New(instr.BR_IF, 0),
}),
},
{
name: "promoted i64 local binary preserves state",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(1)),
instr.New(instr.LOCAL_SET, 0),
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I64_ADD),
instr.New(instr.DROP),
}, program.WithLocals(types.TypeI64)),
},
{
name: "local ref drop preserves ownership",
prog: program.New([]instr.Instruction{
instr.New(instr.I32_CONST, 7),
instr.New(instr.REF_NEW),
instr.New(instr.LOCAL_SET, 0),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.DROP),
}, program.WithLocals(types.TypeRef)),
},
{
name: "function constant drop preserves ownership",
prog: program.New([]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.DROP),
}, program.WithConstants(fn)),
},
{
name: "string constant drop preserves ownership",
prog: program.New([]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.DROP),
}, program.WithConstants(types.String("value"))),
},
{
name: "i32 divide by zero preserves trap state",
prog: program.New([]instr.Instruction{
instr.New(instr.I32_CONST, 90),
instr.New(instr.I32_CONST, 0),
instr.New(instr.I32_DIV_S),
}),
err: ErrDivideByZero,
},
{
name: "promoted i64 divide by zero preserves trap state",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.I64_CONST, 0),
instr.New(instr.I64_DIV_S),
}),
err: ErrDivideByZero,
},
{
name: "promoted i64 local divide by zero preserves trap state",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.LOCAL_SET, 0),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I64_CONST, 0),
instr.New(instr.I64_DIV_S),
}, program.WithLocals(types.TypeI64)),
err: ErrDivideByZero,
},
}
for _, tt := range parityCases {
t.Run(tt.name, func(t *testing.T) {
states := make([]parityState, 0, 2)
for _, opts := range [][]func(*option){
{WithTick(1)},
{WithThreshold(-1)},
} {
i := New(tt.prog, opts...)
err := i.Run(context.Background())
if tt.err == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tt.err)
}
}
return snapshot{
sp: i.sp,
stack: append([]types.Boxed(nil), i.stack[:i.sp]...),
live: live,
interned: len(i.interned),
}
}

fn := types.NewFunctionBuilder(nil).Emit(instr.New(instr.RETURN)).MustBuild()
cases := []struct {
name string
prog *program.Program
}{
{
name: "local ref",
prog: program.New([]instr.Instruction{
instr.New(instr.I32_CONST, 7),
instr.New(instr.REF_NEW),
instr.New(instr.LOCAL_SET, 0),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.DROP),
}, program.WithLocals(types.TypeRef)),
},
{
name: "function constant",
prog: program.New([]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.DROP),
}, program.WithConstants(fn)),
},
{
name: "string constant",
prog: program.New([]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.DROP),
}, program.WithConstants(types.String("value"))),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
exact := run(t, tt.prog, WithTick(1))
fused := run(t, tt.prog, WithThreshold(-1))
require.Equal(t, exact, fused)
})
}
})

t.Run("fused numeric traps preserve exact state", func(t *testing.T) {
type snapshot struct {
ip int
fp int
sp int
stack []types.Boxed
live int
}
run := func(t *testing.T, prog *program.Program, opts ...func(*option)) (snapshot, error) {
t.Helper()
i := New(prog, opts...)
defer i.Close()
err := i.Run(context.Background())
live := 0
for _, rc := range i.rc[1:] {
if rc > 0 {
live += rc
state := parityState{
code: ErrorCode(err),
ip: i.fr.ip,
fp: i.fp,
sp: i.sp,
stack: append([]types.Boxed(nil), i.stack[:i.sp]...),
globals: append([]types.Boxed(nil), i.globals...),
interned: len(i.interned),
}
for _, rc := range i.rc[1:] {
if rc > 0 {
state.live += rc
}
}
states = append(states, state)
require.NoError(t, i.Close())
}
return snapshot{
ip: i.fr.ip,
fp: i.fp,
sp: i.sp,
stack: append([]types.Boxed(nil), i.stack[:i.sp]...),
live: live,
}, err
}

huge := int64(1) << 50
cases := []struct {
name string
prog *program.Program
}{
{
name: "i32 constants",
prog: program.New([]instr.Instruction{
instr.New(instr.I32_CONST, 90),
instr.New(instr.I32_CONST, 0),
instr.New(instr.I32_DIV_S),
}),
},
{
name: "promoted i64 constants",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.I64_CONST, 0),
instr.New(instr.I64_DIV_S),
}),
},
{
name: "promoted i64 local",
prog: program.New([]instr.Instruction{
instr.New(instr.I64_CONST, i64operand(huge)),
instr.New(instr.LOCAL_SET, 0),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I64_CONST, 0),
instr.New(instr.I64_DIV_S),
}, program.WithLocals(types.TypeI64)),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
exact, exactErr := run(t, tt.prog, WithTick(1))
fused, fusedErr := run(t, tt.prog, WithThreshold(-1))
require.ErrorIs(t, exactErr, ErrDivideByZero)
require.ErrorIs(t, fusedErr, ErrDivideByZero)
require.Equal(t, exact, fused)
})
}
})
require.Equal(t, states[0], states[1])
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Test runtime parity through observable behavior, not private state.

Comparing fr, stack layout, intern-table size, and aggregate rc totals is brittle; the summed live value can also hide offsetting ownership errors. Validate returned values, errors, globals, resumability, and subsequent reference usability instead.

As per coding guidelines: “Tests must assert observable behavior rather than private implementation shape.”

Also applies to: 2676-2745

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@interp/interp_test.go` around lines 2526 - 2660, Replace the parity
assertions in the parityCases test with observable behavior checks rather than
private interpreter state such as fr, stack layout, interned size, or aggregate
rc counts. For each case, compare returned values, errors, globals,
resumability, and subsequent reference usability across WithTick(1) and
WithThreshold(-1), including equivalent trap behavior. Apply the same approach
to the additional parity test range noted in the review, while preserving
coverage of ownership and execution semantics.

Source: Coding guidelines

@siyul-park
siyul-park merged commit a601c71 into main Jul 17, 2026
3 checks passed
@siyul-park
siyul-park deleted the perf/jit-struct-coverage branch July 17, 2026 15:04
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