Skip to content

perf: optimize primitive array mutation loops#147

Merged
siyul-park merged 6 commits into
mainfrom
feature/perf_sieve
Jul 15, 2026
Merged

perf: optimize primitive array mutation loops#147
siyul-park merged 6 commits into
mainfrom
feature/perf_sieve

Conversation

@siyul-park

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

Copy link
Copy Markdown
Owner

Summary

  • allow module loop roots at address zero and cache JIT attempts per exact anchor
  • observe exact unconditional back-edges so loop traces compile without repeated failed attempts
  • keep guarded primitive ARRAY_SET operations inside ARM64 native traces while preserving safe side exits
  • reduce register pressure in mutation lowering and isolate speculative array/struct writes during tracing
  • expand Sieve and JIT regression coverage
  • refresh all current benchmark documentation from sequential three-sample medians

Review follow-up

Repeated code-quality reviews tightened the implementation beyond the initial performance fix:

  • replace the shared cache's single pending slot with a coalescing exact-anchor queue
  • retain distinct loop roots, prioritize side exits in FIFO order, and deduplicate active/pending requests
  • keep cold functions on the ordinary BR handler and rethread once only after periodic sampling marks them hot
  • preserve installed native handlers while updating their threaded fallbacks
  • preserve typed-array alias topology during speculative capture without constructing out-of-range unsafe slices
  • isolate speculative mutation, dynamic-function reclamation, trace metadata, and external finalizers from live interpreter state
  • remove dead tracer error plumbing and ambiguous Tracer.exit naming
  • merge mutation lookup/classification/cloning and remove mutationTarget, primitiveArray, branchIndex, isLeaf, and duplicate zeroValue
  • make generated-dispatch construction side-effect free and reuse the sample count across hotness decisions
  • rename loop, trace-clone, continuation-tail, and ARM64 heap-load helpers by role; inline the single-use sampled-loop helper
  • align helper declaration order, field grouping, and test ownership with docs/coding-patterns.md
  • inline important bytecode fixtures so tests remain executable public behavior specifications

Local added-executable-line coverage now covers every new shared-cache branch; the remaining uncovered ARM64 paths are primarily register-exhaustion and compile-rejection defenses.

Performance

Apple M4 Pro, darwin/arm64, Go 1.26.2:

Sieve(256) mode Before Documented after Review follow-up median Allocation
default ~36.6–37.4 us 5.269 us 5.052 us 1,048 B/op, 2 allocs/op
threaded ~15.4–15.6 us 16.143 us 15.385 us 1,048 B/op, 2 allocs/op
eager JIT ~36.8–37.0 us 5.261 us 5.017 us 1,048 B/op, 2 allocs/op

Default and eager JIT remain about 3x faster than threaded execution for this workload. Repeated JIT compilation allocations remain reduced from roughly 407 allocs/op to 2 allocs/op.

Verification

  • make check
  • go test -race ./interp -count=1
  • cd benchmarks && go test ./... -count=1 -timeout=120s
  • go test -run='^$' -bench='^BenchmarkInterpreter_ColdBackedge$' -benchmem -benchtime=300ms -count=3 — median 2.446 us, 0 allocs/op
  • go test -run='^$' -bench='^BenchmarkControl_Sieve' -benchmem -benchtime=300ms -count=3
  • full external comparison with -benchtime=300ms -count=3
  • automated documentation cross-check: 90 comparison rows, 19 public API rows, 20 threaded rows, and 7 reference traversal rows

Summary by CodeRabbit

  • Performance

    • Improved ARM64 JIT handling for primitive typed-array and struct/array mutations, enabling safer continuation for primitive typed-array writes.
    • Refined JIT warmup, loop/backedge capture timing, and threshold-zero behavior.
    • Improved compilation eligibility and request queuing behavior for more predictable warmup outcomes.
  • Documentation

    • Refreshed benchmark methodology and results (including updated labels and medians) and clarified the “current measurements” framing.
    • Updated JIT internals, architecture, and profiling notes.
  • Tests

    • Expanded ARM64 backedge and typed-array mutation coverage, plus stronger cache and tracing isolation checks.

@coderabbitai

coderabbitai Bot commented Jul 14, 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

This PR adds anchor-specific backedge tracing, queued JIT compilation, continuable primitive typed-array writes with ARM64 lowering, expanded runtime tests, and refreshed JIT, opcode, and benchmark documentation.

Changes

Backedge tracing and primitive array-set JIT support

Layer / File(s) Summary
Threader backedge wiring and loop observation
internal/cmd/geninterp/*, interp/interp.go, interp/threaded.go, interp/jit_stub.go
Backward branches can invoke loop observation; interpreter state tracks exact anchors, eager warmup, backedge enablement, compilation attempts, and cleanup.
JIT plan selection and cache scheduling
interp/jit.go, interp/jit_plan.go, interp/cache.go, interp/cache_test.go
Compilation filters plans by root anchor, permits module-loop anchors, disables spilling for mutation-containing plans, and queues distinct requests with side-exit priority.
Tracer mutation isolation
interp/trace.go, interp/trace_test.go
Primitive typed-array writes may continue under bounded conditions; mutation targets, aliases, speculative state, and tracer snapshots are cloned.
ARM64 ARRAY_SET lowering
interp/jit_arm64.go
Primitive stores use continuations, shared guards, updated refcount ordering, leaf-aware addressing, register reuse, and scratch-register boxing.
Runtime and compiler validation
interp/jit_arm64_test.go, interp/jit_plan_test.go, interp/interp_test.go, benchmarks/control_test.go
Tests cover backedge thresholds, cache requests, module loops, mutation compilation and continuation, and repeated sieve execution across interpreter configurations.

Documentation and benchmark refresh

Layer / File(s) Summary
JIT and opcode documentation
docs/architecture.md, docs/jit-internals.md, docs/profile.md, docs/instruction-set.md, docs/testing.md
Documents root-based compilation, queued cache behavior, mutation and spilling rules, loop warmup, fallback handling, and primitive versus reference ARRAY_SET behavior.
Benchmark documentation and results
README.md, benchmarks/README.md, docs/benchmarks.md
Updates measured results, runtime tables, environment details, reproduction commands, API and threaded samples, methodology, and related-document references.

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

Possibly related issues

  • siyul-park/minivm#115: Overlaps the interpreter, tracer, JIT, and shared-cache runtime-correctness changes.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% 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
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.
Title check ✅ Passed The title clearly matches the main change: optimizing primitive array mutation loops.
Description check ✅ Passed The description is mostly complete and covers changes, context, performance, and verification, though it does not use the template's exact section headings.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/perf_sieve

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

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.19741% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.54%. Comparing base (7717228) to head (35cac9f).

Files with missing lines Patch % Lines
interp/interp.go 50.54% 36 Missing and 9 partials ⚠️
interp/trace.go 75.00% 33 Missing and 6 partials ⚠️
interp/jit.go 0.00% 2 Missing ⚠️
interp/threaded.go 66.66% 2 Missing ⚠️
interp/cache.go 96.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #147      +/-   ##
==========================================
+ Coverage   29.36%   29.54%   +0.17%     
==========================================
  Files          86       86              
  Lines       59969    60158     +189     
==========================================
+ Hits        17612    17775     +163     
- Misses      41109    41116       +7     
- Partials     1248     1267      +19     

☔ 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: 3

🧹 Nitpick comments (3)
interp/jit.go (1)

231-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve caller-before-callee ordering for the new helpers.

  • interp/jit.go#L231-L239: move requestedPlans below (*compiler).Compile.
  • interp/trace.go#L353-L385: move continuableArraySet above primitiveArray, or inline the helper.

As per coding guidelines, Go declarations should follow the repository's declaration-order rule: callers before callees.

🤖 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.go` around lines 231 - 239, Reorder the new helper declarations to
follow caller-before-callee ordering: in interp/jit.go lines 231-239, move
requestedPlans below (*compiler).Compile; in interp/trace.go lines 353-385, move
continuableArraySet above primitiveArray or inline it. No behavioral changes are
needed.

Source: Coding guidelines

interp/jit_arm64.go (1)

586-593: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated terminal-detection logic between dispatcher and arraySet.

terminal := op.terminal || ctx.count() > 0 && ctx.values[len(ctx.values)-1].kind == types.KindRef re-derives exactly the condition arraySet itself computes internally (kind == types.KindRef || op.terminal) to decide whether to call l.exit. Keeping this logic in two places risks drift if one side changes independently (e.g., if arraySet gains another terminal condition in future).

Consider having arraySet report whether it treated the op as terminal (e.g., via its second bool-ish behavior or a small out-param) instead of recomputing the predicate in steps.

🤖 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.go` around lines 586 - 593, Remove the duplicated terminal
predicate in the dispatcher around arraySet and have arraySet return or
otherwise report whether the operation was treated as terminal. Update the
caller to use that result for its terminal return path, ensuring terminal
detection remains defined solely by arraySet.
interp/jit_arm64_test.go (1)

120-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the three new TestARM64_Backedge* functions under one top-level test with t.Run subtests.

TestARM64_BackedgeCompilesModuleLoop, TestARM64_BackedgeWarmsEagerLoop, and TestARM64_BackedgeKeepsSampleThreshold are three separate top-level tests covering closely related backedge-observation scenarios. As per coding guidelines, "Use one top-level test per public symbol (Test<Func> or Test<Type>_<Method>), put sub-cases under t.Run."

The TestCompiler_Compile test in this same file already follows this pattern well; consider folding these three into a single TestARM64_Backedge with t.Run("compiles module loop", ...), t.Run("warms eager loop", ...), t.Run("keeps sample threshold", ...).

🤖 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 120 - 205, Consolidate
TestARM64_BackedgeCompilesModuleLoop, TestARM64_BackedgeWarmsEagerLoop, and
TestARM64_BackedgeKeepsSampleThreshold into one top-level TestARM64_Backedge
function. Move each scenario into a descriptive t.Run subtest, preserving its
existing setup, assertions, and arm64 skip behavior.

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 `@docs/architecture.md`:
- Line 176: Update the spill-frame documentation sentence near the “noSpill”
behavior to state that spilling is disabled specifically for plans containing
ARRAY_SET or STRUCT_SET operations, rather than broadly for all heap mutation.
Preserve the existing explanation about control-flow requirements and avoid
implying that other heap operations are rejected.

In `@docs/benchmarks.md`:
- Around line 35-51: Update the benchmark command block so the initial
benchmarks command runs from the benchmarks directory without changing the
caller’s working directory, using a subshell or restoring the repository root
before the ./interp and ./types commands. Keep all existing command arguments
and benchmark selections unchanged.

In `@interp/jit_arm64.go`:
- Around line 3196-3208: Remove the redundant arm64.MOV(n, addr) emission in the
non-reference, non-terminal branch of the ARRAY_SET logic, while preserving
rcAddr, rc, and guardRCTo setup and leaving n available as the later scratch
destination.

---

Nitpick comments:
In `@interp/jit_arm64_test.go`:
- Around line 120-205: Consolidate TestARM64_BackedgeCompilesModuleLoop,
TestARM64_BackedgeWarmsEagerLoop, and TestARM64_BackedgeKeepsSampleThreshold
into one top-level TestARM64_Backedge function. Move each scenario into a
descriptive t.Run subtest, preserving its existing setup, assertions, and arm64
skip behavior.

In `@interp/jit_arm64.go`:
- Around line 586-593: Remove the duplicated terminal predicate in the
dispatcher around arraySet and have arraySet return or otherwise report whether
the operation was treated as terminal. Update the caller to use that result for
its terminal return path, ensuring terminal detection remains defined solely by
arraySet.

In `@interp/jit.go`:
- Around line 231-239: Reorder the new helper declarations to follow
caller-before-callee ordering: in interp/jit.go lines 231-239, move
requestedPlans below (*compiler).Compile; in interp/trace.go lines 353-385, move
continuableArraySet above primitiveArray or inline it. No behavioral changes are
needed.
🪄 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: 699477c1-5c28-4634-bd83-d595868a9311

📥 Commits

Reviewing files that changed from the base of the PR and between 7717228 and ea8eafa.

📒 Files selected for processing (19)
  • README.md
  • benchmarks/README.md
  • benchmarks/control_test.go
  • docs/architecture.md
  • docs/benchmarks.md
  • docs/jit-internals.md
  • docs/profile.md
  • internal/cmd/geninterp/generate.go
  • internal/cmd/geninterp/lower.go
  • interp/interp.go
  • interp/jit.go
  • interp/jit_arm64.go
  • interp/jit_arm64_test.go
  • interp/jit_plan.go
  • interp/jit_plan_test.go
  • interp/jit_stub.go
  • interp/threaded.go
  • interp/trace.go
  • interp/trace_test.go

Comment thread docs/architecture.md
- Guard failure materializes VM state and resumes threaded dispatch.
- ARM64 label branches are range-checked and relaxed only to replacements that are already in range; an unreachable target falls back to threaded execution.
- Spill frames use a stable base register, and every internal call resume point must restore the active spill-frame depth. Loop traces and terminal mutation traces disable spilling when control flow cannot preserve that contract.
- Spill frames use a stable base register, and every internal call resume point must restore the active spill-frame depth. Loop traces and plans containing heap mutation disable spilling when control flow cannot preserve that contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the exact no-spill condition.

Line 176 says plans containing heap mutation disable spilling, but noSpill specifically rejects plans containing ARRAY_SET or STRUCT_SET; other unsupported heap operations remain threaded. Narrow this wording to the implemented condition.

🤖 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 `@docs/architecture.md` at line 176, Update the spill-frame documentation
sentence near the “noSpill” behavior to state that spilling is disabled
specifically for plans containing ARRAY_SET or STRUCT_SET operations, rather
than broadly for all heap mutation. Preserve the existing explanation about
control-flow requirements and avoid implying that other heap operations are
rejected.

Comment thread docs/benchmarks.md
Comment on lines 35 to +51
```bash
# Full canonical package and VM-kernel suite
make benchmark-core

# Pull-request-sized benchmark suite
make benchmark-pr

# Cross-runtime comparison, matching the table below
# Full external comparison used by the complete matrix
cd benchmarks
go test -tags=compare -run='^$' -bench='.' -benchmem -benchtime=300ms -count=3 ./...
```

The cross-runtime command runs each benchmark three times. The tables below report the median `ns/op`, `B/op`, and `allocs/op` for every workload/runtime combination.
# Public interpreter and pool API costs
go test -run='^$' \
-bench='^(BenchmarkNew|BenchmarkInterpreter_(Reset|Push|Pop|PopBoxed|Peek|Alloc|Retain|Release)|BenchmarkPool_(Get|Put))$' \
-benchmem -benchtime=300ms -count=3 ./interp

The comparison is informational rather than a strict end-to-end latency ranking. minivm reports execution-only `Interpreter.Run` time and excludes result extraction and reset, while other runtimes may include host-call result materialization and conversion. This boundary difference is most significant for short workloads, so small ratios should not be treated as precise runtime speedups.
# Exact threaded execution samples
go test -run='^$' -bench='^BenchmarkInterpreter_Run/.*/Threaded$' \
-benchmem -benchtime=300ms -count=3 ./interp

## Summary
# Reference traversal
go test -run='^$' -bench='^Benchmark(Array|Struct|TypedMap|Map)_Refs$' \
-benchmem -benchtime=300ms -count=3 ./types

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep reproduction commands in a consistent working directory.

The block changes into benchmarks at Line 37, then runs the ./interp and ./types commands at Lines 41-51. When copied as one shell block, those paths are resolved under benchmarks and fail. Use a subshell for the benchmarks command or return to the repository root before the interp and types commands.

🤖 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 `@docs/benchmarks.md` around lines 35 - 51, Update the benchmark command block
so the initial benchmarks command runs from the benchmarks directory without
changing the caller’s working directory, using a subshell or restoring the
repository root before the ./interp and ./types commands. Keep all existing
command arguments and benchmark selections unchanged.

Comment thread interp/jit_arm64.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.

Caution

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

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

345-404: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

STRUCT_SET needs to isolate *HostObject before the store runs. cloneMutation handles typed arrays, *types.Array, and *types.Struct, but not *HostObject. Because the heap clone is shallow and STRUCT_SET still reaches SetField/SetRaw on host objects, speculative capture can mutate the same receiver as the live interpreter. Either deep-clone that case or abort capture here.

🤖 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/trace.go` around lines 345 - 404, Update cloneMutation to handle
*HostObject before speculative STRUCT_SET storage can proceed: either create an
independent deep clone that preserves host-object behavior, or return/abort
capture for that target when safe cloning is unavailable. Ensure HostObject
mutations cannot reach the live interpreter through shared shallow state, while
preserving existing typed-array, array, and struct handling.
🧹 Nitpick comments (1)
interp/trace_test.go (1)

76-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM! Once the *HostObject gap in cloneMutation (see interp/trace.go) is fixed, consider extending this table with a *HostObject-backed STRUCT_SET case — it would have caught that isolation gap directly.

🤖 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/trace_test.go` around lines 76 - 130, Extend the “isolates captured
mutations” table in the trace tests with a *HostObject-backed STRUCT_SET case,
using the existing cloneMutation behavior in interp/trace.go and verifying the
original host object remains unchanged after capture. Follow the existing struct
test setup and read callback pattern.
🤖 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.

Outside diff comments:
In `@interp/trace.go`:
- Around line 345-404: Update cloneMutation to handle *HostObject before
speculative STRUCT_SET storage can proceed: either create an independent deep
clone that preserves host-object behavior, or return/abort capture for that
target when safe cloning is unavailable. Ensure HostObject mutations cannot
reach the live interpreter through shared shallow state, while preserving
existing typed-array, array, and struct handling.

---

Nitpick comments:
In `@interp/trace_test.go`:
- Around line 76-130: Extend the “isolates captured mutations” table in the
trace tests with a *HostObject-backed STRUCT_SET case, using the existing
cloneMutation behavior in interp/trace.go and verifying the original host object
remains unchanged after capture. Follow the existing struct test setup and read
callback pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b87a72f8-058f-43a5-a094-db8b3c0a4a30

📥 Commits

Reviewing files that changed from the base of the PR and between ea8eafa and 78e6a74.

📒 Files selected for processing (13)
  • docs/instruction-set.md
  • docs/jit-internals.md
  • docs/testing.md
  • internal/cmd/geninterp/generate.go
  • internal/cmd/geninterp/lower.go
  • interp/interp.go
  • interp/jit.go
  • interp/jit_arm64.go
  • interp/jit_arm64_test.go
  • interp/jit_plan_test.go
  • interp/threaded.go
  • interp/trace.go
  • interp/trace_test.go
💤 Files with no reviewable changes (2)
  • internal/cmd/geninterp/generate.go
  • interp/jit_plan_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/cmd/geninterp/lower.go
  • docs/jit-internals.md
  • interp/jit_arm64.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.

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

7004-7016: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant b.ResetTimer() with b.Loop().

testing.B.Loop() already resets the timer on its first call, so the explicit b.ResetTimer() on line 7009 has no effect. Harmless but unnecessary.

🧹 Optional cleanup
 	ctx := context.Background()
 	b.ReportAllocs()
-	b.ResetTimer()
 	for b.Loop() {
🤖 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 7004 - 7016, Remove the redundant
b.ResetTimer() call from BenchmarkInterpreter_PreHotBackedge, leaving
b.ReportAllocs() and the b.Loop() benchmark flow unchanged.
🤖 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/interp_test.go`:
- Around line 7004-7016: Remove the redundant b.ResetTimer() call from
BenchmarkInterpreter_PreHotBackedge, leaving b.ReportAllocs() and the b.Loop()
benchmark flow unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a006e8a-53e9-4031-84e2-88ea5d013473

📥 Commits

Reviewing files that changed from the base of the PR and between 78e6a74 and 071f8fd.

📒 Files selected for processing (11)
  • README.md
  • docs/benchmarks.md
  • docs/jit-internals.md
  • docs/profile.md
  • interp/cache.go
  • interp/cache_test.go
  • interp/interp.go
  • interp/interp_test.go
  • interp/jit_arm64_test.go
  • interp/trace.go
  • interp/trace_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • README.md
  • docs/profile.md
  • interp/trace_test.go
  • interp/trace.go
  • interp/jit_arm64_test.go
  • docs/jit-internals.md
  • docs/benchmarks.md

@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: 3

Caution

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

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

165-177: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Assert observable JIT behavior instead of cache internals.

i.tried and i.exits couple this test to private state. Use profiler/native execution metrics to verify warmup and installation while retaining the result assertions.

As per coding guidelines, tests must assert 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/jit_arm64_test.go` around lines 165 - 177, Update the JIT test loop
around i.Run and PopBoxed to stop asserting i.tried and i.exits; retain the
returned value assertions, and use the available profiler/native execution
metrics to verify the expected warmup attempts and installed-JIT behavior for
each test case.

Source: Coding guidelines


443-450: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Prove that ARRAY_SET executes natively.

These cases also pass if the compiled entry side-exits at ARRAY_SET and threaded dispatch performs the write. Assert a profiler signal that execution continues natively past the store, with no terminal ARRAY_SET fallback.

As per coding guidelines, tests must assert behavior rather than private implementation shape.

Also applies to: 500-508, 528-537

🤖 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 443 - 450, Strengthen the ARRAY_SET
test cases around compiler.Compile and i.Run to verify execution continues
natively past the store, rather than allowing threaded dispatch to perform it
after a side exit. Use the existing profiler or execution signal to assert
native continuation and confirm no terminal ARRAY_SET fallback occurs, while
retaining the current result assertions; apply the same validation to the
additional cases at 500-508 and 528-537.

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/cache_test.go`:
- Around line 32-33: Update both cache cleanup sites in the affected subtests to
handle the error returned by cache.Close instead of deferring it directly. Use a
deferred closure that checks or reports the Close error, preserving cleanup
behavior and satisfying errcheck.

In `@interp/jit_arm64.go`:
- Around line 3725-3730: Move the sliceHeaderTo method below the sliceHeader
wrapper in the arm64Lowerer declarations, preserving its implementation
unchanged and ensuring the caller appears before the callee.

In `@interp/trace.go`:
- Around line 146-158: Move the anchor bounds validation for a.addr and a.ip to
the start of this flow, before the nil-tree branch invokes r.tree(a) or
tree.attempts is checked and incremented. Return CaptureOutcomeRejected with
CaptureReasonInvalidAnchor immediately for invalid anchors, while preserving the
existing attempt-limit handling for valid anchors.

---

Outside diff comments:
In `@interp/jit_arm64_test.go`:
- Around line 165-177: Update the JIT test loop around i.Run and PopBoxed to
stop asserting i.tried and i.exits; retain the returned value assertions, and
use the available profiler/native execution metrics to verify the expected
warmup attempts and installed-JIT behavior for each test case.
- Around line 443-450: Strengthen the ARRAY_SET test cases around
compiler.Compile and i.Run to verify execution continues natively past the
store, rather than allowing threaded dispatch to perform it after a side exit.
Use the existing profiler or execution signal to assert native continuation and
confirm no terminal ARRAY_SET fallback occurs, while retaining the current
result assertions; apply the same validation to the additional cases at 500-508
and 528-537.
🪄 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: 007fadca-60db-4a3c-849f-f798bca7cbd3

📥 Commits

Reviewing files that changed from the base of the PR and between 071f8fd and 2ef499e.

📒 Files selected for processing (13)
  • README.md
  • docs/benchmarks.md
  • docs/jit-internals.md
  • interp/cache.go
  • interp/cache_test.go
  • interp/interp.go
  • interp/interp_test.go
  • interp/jit.go
  • interp/jit_arm64.go
  • interp/jit_arm64_test.go
  • interp/pool_test.go
  • interp/trace.go
  • interp/trace_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • interp/jit.go
  • README.md
  • interp/cache.go
  • interp/trace_test.go
  • docs/benchmarks.md
  • interp/interp.go

Comment thread interp/cache_test.go
Comment on lines +32 to +33
cache := NewCache(program.New(nil))
defer cache.Close()

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 | 🟡 Minor | ⚡ Quick win

Unchecked cache.Close() error (errcheck).

Both new subtests use defer cache.Close() without checking the returned error, flagged by golangci-lint.

🧹 Proposed fix
-		cache := NewCache(program.New(nil))
-		defer cache.Close()
+		cache := NewCache(program.New(nil))
+		defer func() { require.NoError(t, cache.Close()) }()

Apply the same change at both flagged sites (lines 33 and 77).

Also applies to: 76-77

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 33-33: Error return value of cache.Close is not checked

(errcheck)

🤖 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/cache_test.go` around lines 32 - 33, Update both cache cleanup sites
in the affected subtests to handle the error returned by cache.Close instead of
deferring it directly. Use a deferred closure that checks or reports the Close
error, preserving cleanup behavior and satisfying errcheck.

Source: Linters/SAST tools

Comment thread interp/jit_arm64.go
Comment on lines +3725 to 3730
func (arm64Lowerer) sliceHeaderTo(ctx *lowering, data, ptr, n asm.VReg, base int16) {
ctx.assembler.Emit(
arm64.LDR(ptr, data, base+sliceData),
arm64.LDR(n, data, base+sliceLen),
)
return ptr, n
}

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 | ⚡ Quick win

Place sliceHeaderTo after sliceHeader.

The new helper precedes its caller, reversing the required declaration order. Move it below the sliceHeader wrapper.

As per coding guidelines, Go declarations should follow the repository's declaration-order rule: callers before callees.

🤖 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.go` around lines 3725 - 3730, Move the sliceHeaderTo method
below the sliceHeader wrapper in the arm64Lowerer declarations, preserving its
implementation unchanged and ensuring the caller appears before the callee.

Source: Coding guidelines

Comment thread interp/trace.go
Comment on lines 146 to 158
if tree == nil {
tree = r.tree(a)
}
if tree.attempts >= attemptLimit {
r.mu.Unlock()
return captureResult{outcome: prof.CaptureOutcomeRejected, reason: prof.CaptureReasonAttemptLimit}, nil
return captureResult{outcome: prof.CaptureOutcomeRejected, reason: prof.CaptureReasonAttemptLimit}
}
tree.attempts++
r.mu.Unlock()

if a.addr < 0 || a.addr >= len(i.instrs) || a.ip < 0 || a.ip >= len(i.instrs[a.addr]) {
return captureResult{outcome: prof.CaptureOutcomeRejected, reason: prof.CaptureReasonInvalidAnchor}, nil
return captureResult{outcome: prof.CaptureOutcomeRejected, reason: prof.CaptureReasonInvalidAnchor}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect the relevant section.
ast-grep outline interp/trace.go --view expanded || true
echo '---'
wc -l interp/trace.go
echo '---'
sed -n '120,180p' interp/trace.go

Repository: siyul-park/minivm

Length of output: 4249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the tree constructor and capture callers to see whether invalid anchors are reachable.
sed -n '660,690p' interp/trace.go
echo '---'
rg -n "capture\\(" interp -g '*.go'

Repository: siyul-park/minivm

Length of output: 2411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check how InvalidAnchor and AttemptLimit are exercised in tests.
rg -n "CaptureReasonInvalidAnchor|CaptureReasonAttemptLimit|attemptLimit|anchor\\{" interp/trace_test.go interp/*.go

Repository: siyul-park/minivm

Length of output: 11652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' interp/trace_test.go

Repository: siyul-park/minivm

Length of output: 8336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '260,420p' interp/trace_test.go

Repository: siyul-park/minivm

Length of output: 4061


Validate anchor bounds before creating the tree Move the a.addr/a.ip bounds check before r.tree(a) and tree.attempts++ so invalid anchors don’t create persistent tree entries or burn attempts before being rejected.

🤖 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/trace.go` around lines 146 - 158, Move the anchor bounds validation
for a.addr and a.ip to the start of this flow, before the nil-tree branch
invokes r.tree(a) or tree.attempts is checked and incremented. Return
CaptureOutcomeRejected with CaptureReasonInvalidAnchor immediately for invalid
anchors, while preserving the existing attempt-limit handling for valid anchors.

@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.

♻️ Duplicate comments (1)
interp/jit_arm64.go (1)

3129-3263: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Redundant MOV(n, addr) still present — previously flagged and marked addressed.

This exact sequence (rcBase = scratch; l.rcBaseTo(ctx, rcBase); ctx.assembler.Emit(arm64.MOV(n, addr)); rcAddr = addr; rc = n; l.guardRCTo(...)) was flagged in an earlier review round as a dead instruction — guardRCTo immediately overwrites rc (aliased to n) with the loaded refcount, so the preceding MOV value is never consumed. That earlier comment was marked "Addressed", but the same redundant MOV is back in the continuable primitive-store path at Line 3197. This still adds one extra instruction to the hot ARRAY_SET mutation-loop path this PR is trying to optimize.

🐛 Proposed fix
 		rcBase = scratch
 		l.rcBaseTo(ctx, rcBase)
-		ctx.assembler.Emit(arm64.MOV(n, addr))
 		rcAddr = addr
 		rc = n
 		l.guardRCTo(ctx, rc, rcAddr, rcBase, valueFail)
#!/bin/bash
# Re-confirm guardRCTo overwrites rc unconditionally (making the MOV dead).
rg -n -A20 'func .*guardRCTo\(' interp/jit_arm64.go
🤖 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.go` around lines 3129 - 3263, Remove the redundant arm64.MOV
emitted in the continuable branch of the array-store refcount setup. In the
sequence assigning rcBase, rcAddr, and rc before guardRCTo, pass the existing
registers directly to guardRCTo without copying addr into n; preserve the
surrounding refcount guard and store behavior.
🤖 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.

Duplicate comments:
In `@interp/jit_arm64.go`:
- Around line 3129-3263: Remove the redundant arm64.MOV emitted in the
continuable branch of the array-store refcount setup. In the sequence assigning
rcBase, rcAddr, and rc before guardRCTo, pass the existing registers directly to
guardRCTo without copying addr into n; preserve the surrounding refcount guard
and store behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a824db8f-98a3-4d58-8f20-ce91258e3c81

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef499e and 35cac9f.

📒 Files selected for processing (6)
  • docs/benchmarks.md
  • docs/jit-internals.md
  • interp/interp.go
  • interp/interp_test.go
  • interp/jit_arm64.go
  • interp/trace.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • interp/interp_test.go
  • docs/benchmarks.md
  • docs/jit-internals.md
  • interp/trace.go

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