Skip to content

refactor: simplification implement - #168

Merged
siyul-park merged 18 commits into
mainfrom
codex/vm-simplification
Jul 31, 2026
Merged

refactor: simplification implement#168
siyul-park merged 18 commits into
mainfrom
codex/vm-simplification

Conversation

@siyul-park

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

Copy link
Copy Markdown
Owner

Changes Made

Related Issues

Additional Information

Summary by CodeRabbit

  • Bug Fixes

    • Improved coroutine completion, tail-call handling, value cleanup, and reference behavior.
    • Prevented replacement of function, closure, and coroutine heap entries.
    • Expanded supported scalar reference operations, including boolean and 8-bit integer values.
    • Improved interpreter lifecycle, pooling, garbage collection, and host integration reliability.
  • Documentation

    • Clarified reference mutation, coroutine return behavior, heap ownership, memory management, and testing guidance.
  • Tests

    • Expanded end-to-end coverage for generated output, parser limits, coroutine behavior, and public API interactions.

Replace private cache queue and refcount assertions with public Pool execution, profiler, and native-code lifetime contracts. Queue ordering remains unobservable implementation state.
…eturn handling

- Make function, closure, and coroutine heap slots immutable (Store rejects)
- Add type guard in REF_SET/REF_GET for scalar-only cell access
- Refactor retire into shared coroutine/normal return paths
- Release discarded return values when coroutine completes
- Preserve coroutine identity through tail calls
- Add externalize-tests tool for test package refactoring
- Update docs: memory model, host integration, instruction set
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR converts Go tests to external _test packages, adds a test externalization tool, rebuilds benchmark helpers inside tests, validates generated interpreter artifacts through end-to-end tests, and updates coroutine, reference, heap, and callable lifecycle handling.

Changes

External test migration

Layer / File(s) Summary
Testing policy and migration tool
.claude/CLAUDE.md, AGENTS.md, docs/testing.md, docs/coding-patterns.md, externalize_tests_tmp.go
Testing guidance requires external client packages and public observable boundaries. The new tool rewrites selected tests to use production-package imports.
Package test migration
analysis/*_test.go, asm/**/*_test.go, cli/*_test.go, debug/*_test.go, instr/*_test.go, optimize/*_test.go, pass/*_test.go, prof/*_test.go, program/*_test.go, transform/*_test.go, types/*_test.go
Tests use external package names and qualified exported APIs. Private representation checks were removed or replaced with public assertions.
Benchmark builders
benchmarks/*_test.go
Benchmark program builders and native reference implementations now reside in external benchmark tests.
Interpreter lifecycle
interp/interp.go, interp/coroutine.go, interp/threaded.go, internal/cmd/geninterp/lower.go
Callable slots become immutable. Coroutine completion, frame reuse, stored values, reference access, and scalar heap validation are updated.
Interpreter boundary tests
interp/*_test.go
External integration tests cover coroutine identity, collection, callable references, scalar references, host integration, marshaling, and pool lifecycle.
Generator boundary tests
internal/cmd/geninterp/generate_test.go
The generator runs in a temporary directory. Tests validate generated files, stale-file checks, declarations, and forbidden helpers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • siyul-park/minivm#115 — The PR updates interpreter coroutine, heap, and externalized interpreter tests.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains only the template headings and comments, with no summary of changes, issues, or additional context. Complete each section with the key changes, related issue references, and relevant implementation or testing details.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title indicates a refactor but does not describe the primary changes to public-contract tests and interpreter behavior. Use a specific title such as "refactor: externalize tests and simplify coroutine handling".
✅ Passed checks (2 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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/vm-simplification

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.45.0)
interp/threaded.go

ast-grep timed out on this file


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@siyul-park
siyul-park merged commit 8b4e647 into main Jul 31, 2026
2 of 4 checks passed
@siyul-park
siyul-park deleted the codex/vm-simplification branch July 31, 2026 10:30

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

Caution

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

⚠️ Outside diff range comments (2)
internal/cmd/geninterp/lower.go (1)

3754-3812: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use types.BoxedNull when clearing co.value.

types.BoxedNull is BoxRef(0), but types.Boxed(0) is the literal boxed constant and has kind 0/Default. Since releaseBox(co.value) acts on ref values, this path can incorrectly release or later treat the reset value as non-null.

🤖 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 `@internal/cmd/geninterp/lower.go` around lines 3754 - 3812, In the coroutine
resume branch generated by resume, replace the reset assignment for co.value
after releaseBox with types.BoxedNull instead of types.Boxed(0). Keep the
existing releaseBox call and all other coroutine state handling unchanged.
interp/pool_test.go (1)

245-278: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the loop exit was compiled, not only that an attempt occurred.

runs = 64 exceeds the current exitThreshold, but that is hard-coded here while the assertion checks only vm_jit_attempts_total. Add one of the same positive indicators used in the nearby tests, such as vm_jit_emits_total > 0, so the test fails if the threshold changes and no side-exit compilation happens.

🤖 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 245 - 278, Extend the assertions after the
`metrics.Metric("vm_jit_attempts_total")` check in this pool test to verify a
positive JIT emission indicator, such as `vm_jit_emits_total` being present and
greater than zero. Keep the existing attempts assertion and use the same
metric-validation pattern as nearby tests so the test confirms loop-exit
compilation occurred.
🧹 Nitpick comments (11)
internal/cmd/geninterp/generate_test.go (1)

16-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Place the scenarios under t.Run sub-cases.

TestGenerator runs five distinct scenarios in one linear body: build, first generation, clean -check, stale file, and missing file. A failure in an early scenario aborts the rest, and the failure output does not name the scenario. Wrap each scenario in t.Run while keeping them sequential so the shared temporary directory state stays valid.

Based on the coding guideline "Use one top-level test per public symbol (Test<Func> or Test<Type>_<Method>), place sub-cases under t.Run".

🤖 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 `@internal/cmd/geninterp/generate_test.go` around lines 16 - 58, Refactor
TestGenerator so each scenario—build, initial generation, clean -check,
stale-file check, and missing-file check—is enclosed in a descriptive sequential
t.Run sub-case. Preserve the shared temp directory and generated-file state
between sub-cases, and keep the existing assertions and command ordering intact.

Source: Coding guidelines

benchmarks/control_test.go (2)

149-171: 📐 Maintainability & Code Quality | 🔵 Trivial

Verify program.Builder.MustBuild() and reduce duplicated build-and-panic boilerplate.

iterativeFib repeats prog, err := b.Build(); if err != nil { panic(err) }; return prog. call_test.go already uses types.NewFunctionBuilder(...).MustBuild() for function builders. Check whether program.Builder exposes an equivalent MustBuild() method. This duplication repeats in sieve in this file, and in benchmarks/memory_test.go and benchmarks/numeric_test.go. See the consolidated comment for the full list of sites.

🤖 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 `@benchmarks/control_test.go` around lines 149 - 171, Replace the manual Build
error-handling sequence in iterativeFib with program.Builder’s MustBuild method
if available, returning its result directly. Apply the same change to the
corresponding sieve and benchmark builder sites identified in the consolidated
review, while preserving each function’s existing behavior.

149-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared program-builder panic helper.

program.Builder only offers Build(), while these benchmark helpers repeat the same if err != nil { panic(err) } return prog block five times across three files. Encapsulate this pattern in a small package-level helper in benchmarks/*; avoid using MustBuild() on types.FunctionBuilder unless constructing that API for program.Builder as well.

🤖 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 `@benchmarks/control_test.go` around lines 149 - 218, Extract a package-level
helper for building a program.Builder that panics on Build errors and returns
the resulting program. Replace the repeated build/error/return blocks in
iterativeFib and sieve in benchmarks/control_test.go, and the corresponding
blocks in benchmarks/memory_test.go and benchmarks/numeric_test.go, with this
helper; do not use types.FunctionBuilder.MustBuild unless the same API is added
for program.Builder.
internal/cmd/geninterp/lower.go (2)

1238-1323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the intentional asymmetric coro handling in replace().

In the targetSlots == 1 branch, the fresh-frame path sets f.coro = 0 (Line 1257), but the frame-reuse path (Lines 1264-1278) never resets f.coro, leaving it at whatever value the replaced frame held. The same pattern repeats in the targetSlots != 1 branch: fresh-frame sets f.coro = 0 (Line 1300), reuse path (Lines 1307-1320) leaves it untouched. This is intentional: replace() implements tail calls (RETURN_CALL), and preserving f.coro keeps a tail-called function running on behalf of the same coroutine, matching the "preserves coroutine identity through function tail calls" test.

The intent is not obvious from the generated code alone. A future edit that "fixes" the missing reset in the reuse path would silently break coroutine identity across tail calls. Add a jen.Comment(...) at each reuse site to record why the reset is skipped.

📝 Proposed comment additions
 			jen.Id("f").Dot("release").Op("=").Add(jen.Lit(releaseTarget)),
+			jen.Comment("coro is intentionally preserved across a tail call so a resumed coroutine keeps its identity."),
 			jen.Id("i").Dot("sp").Op("=").Id("base").Op("+").Id("params").Op("+").Id("locals"),
 			jen.Id(label).Op(":").Add(jen.Null()),

Apply the equivalent comment before the second i.sp = base + params + locals at Line 1320.

🤖 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 `@internal/cmd/geninterp/lower.go` around lines 1238 - 1323, Add
jen.Comment(...) immediately before each frame-reuse path’s `i.sp = base +
params + locals` assignment in `replace`, covering both the `targetSlots == 1`
and `targetSlots != 1` branches. Document that `f.coro` is intentionally not
reset during tail-call frame reuse so coroutine identity is preserved; leave the
existing fresh-frame resets unchanged.

3673-3736: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the repeated scalar-kind case list in refGet and refSet.

Both functions embed the identical case list types.I1, types.I8, types.I32, types.I64, types.F32, types.F64 (Lines 3680-3684 and 3725-3729). Extract a shared helper that returns this case list so the two opcodes cannot drift out of sync if a new scalar kind is added later.

♻️ Proposed extraction
func scalarKindCases() []jen.Code {
	return []jen.Code{
		jen.Id("types").Dot("I1"), jen.Id("types").Dot("I8"),
		jen.Id("types").Dot("I32"), jen.Id("types").Dot("I64"),
		jen.Id("types").Dot("F32"), jen.Id("types").Dot("F64"),
	}
}

Then reference scalarKindCases()... in place of the inlined list in both refGet and refSet.

🤖 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 `@internal/cmd/geninterp/lower.go` around lines 3673 - 3736, Extract the shared
scalar-kind case list into a scalarKindCases helper returning the six existing
jen.Code entries, then use its expanded result in the jen.Case calls within
refGet and refSet. Preserve the current scalar kinds and default ErrTypeMismatch
behavior while removing both duplicated inline lists.
interp/coroutine_test.go (3)

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

Name the top-level test after the public symbol under test.

The repository test convention requires Test<Func> or Test<Type>_<Method>. TestCoroutineReferences does not map to an exported symbol of package interp. The behaviors here are observed through Interpreter.Run, so TestInterpreter_Run with these sub-cases matches the convention. If a separate grouping is preferred, keep the sub-cases and rename the top-level function to a public symbol name.

As per coding guidelines: "Use one top-level test per public symbol (Test<Func> or Test<Type>_<Method>), place sub-cases under t.Run".

🤖 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/coroutine_test.go` at line 26, Rename the top-level test function
TestCoroutineReferences to TestInterpreter_Run so it follows the repository
convention and reflects the public symbol under test. Keep the existing
coroutine scenarios as sub-cases under t.Run.

Source: Coding guidelines


69-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explain the 19 instruction-pointer literal.

19 is the encoded byte offset of the RESUME instruction in this module. A change to instruction widths moves that offset. require.True(t, collected) keeps the failure visible, so correctness is preserved. Add a short comment that states which instruction the offset targets, so a future edit can be updated quickly.

🤖 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/coroutine_test.go` around lines 69 - 73, Add a brief comment next to
the IP comparison in the interp.WithHook callback explaining that 19 is the
encoded byte offset of the RESUME instruction, making clear the literal must be
updated if instruction widths change.

220-228: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

State how the heap cap proves the release.

require.Equal(t, 3, vm.Len()) checks the stack only. The real proof is require.NoError(t, vm.Run(...)) with WithHeapLimit(5): if the discarded completion reference leaked, the last REF_NEW would return ErrHeapExhausted. Add a one-line comment that records this dependency, so a future heap-size change does not silently remove the coverage.

🤖 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/coroutine_test.go` around lines 220 - 228, The test currently relies
on the heap limit to detect leaked completion references, but this dependency is
undocumented. Add a one-line comment near the vm.Run assertion explaining that
WithHeapLimit(5) makes a leaked discarded reference cause the final REF_NEW to
return ErrHeapExhausted, preserving the release coverage.
interp/pool_test.go (2)

584-643: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the second declaration into the loop.

second is declared at line 584 but assigned and read only inside the iteration body. Declaring it inside the loop keeps the measured value local and removes the cross-iteration carry-over. The timer handling is correct: b.StopTimer() before the loop and b.StartTimer() only around pool.Get measures the shared-JIT miss path.

🤖 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 584 - 643, Move the var second
*interp.Interpreter declaration into the benchmark iteration loop, immediately
before its first assignment from pool.Get. Keep all existing uses, timer
handling, and pool lifecycle logic unchanged while eliminating cross-iteration
state.

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

Record that 0xffffffff encodes -1.

The removed i32operand helper made the sign explicit. instr.New takes an unsigned operand, so the literal is correct. Add a named constant or a short comment, so the negative branch value stays readable.

♻️ Proposed change
 			Bind(neg).
-			Emit(instr.New(instr.I32_CONST, 0xffffffff)).
+			// 0xffffffff is the two's-complement encoding of i32 -1.
+			Emit(instr.New(instr.I32_CONST, 0xffffffff)).
🤖 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` at line 120, Document the `0xffffffff` operand in the
test around `instr.New` as the encoded `-1` branch value, using a named constant
or concise comment. Keep the unsigned literal and existing instruction behavior
unchanged.
interp/host_test.go (1)

490-499: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Close the interpreter with defer inside the loop body.

Line 498 closes the interpreter as the last statement. If any earlier require in the same iteration fails, the test goroutine stops and the interpreter is never closed. The same pattern exists at lines 254-264 and 290-295. Wrap each iteration in a closure with defer i.Close(), or register t.Cleanup. This keeps failures from leaking interpreter resources into the rest of the test binary.

🤖 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/host_test.go` around lines 490 - 499, Update the test loop iterations
that create interpreters, including the shown host-object case and the analogous
sections near the other referenced cases, to register interpreter cleanup
immediately after creation using a closure-scoped defer or t.Cleanup. Remove the
explicit final i.Close calls where cleanup is registered, while preserving the
existing assertions and error handling.
🤖 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 `@cli/cli_test.go`:
- Line 18: Update TestWithFS to inject a distinct cli.WriteFS implementation
instead of cli.OS(), ensuring it can detect whether cli.WithFS is honored.
Configure the test filesystem with main.vm and verify the rendered result is
read from that injected filesystem through cli.Root.

In `@docs/instruction-set.md`:
- Around line 299-300: Update docs/instruction-set.md lines 299-300 to state
that REF_GET, like REF_SET, traps when targeting a non-scalar reference. In
interp/interp_test.go lines 287-294, add a callable-reference REF_GET case
asserting ErrTypeMismatch; no other sites require changes.

In `@externalize_tests_tmp.go`:
- Around line 102-112: Update the source rewrite flow around os.Create and
format.Node to format each file into memory, write the complete result to an
adjacent temporary file, close it with error handling, and rename it over the
target only after all operations succeed. Ensure failures leave the original
compiled Go source intact and remove the direct truncating write path.

In `@interp/interp_test.go`:
- Around line 24-37: Move the public-contract tests from interp/interp_test.go
ranges 24-37, 1552-1585, and 4328-4341 into an external package interp_test
file. Relocate optionCustom and qualify its Interpreter, types, and errors
through interp as needed; move the scalar REF_* contract cases and
converter-precedence case unchanged in behavior, ensuring all three sites test
the importing-client boundary.
- Around line 1575-1576: Update the test cleanup around the interpreter created
with New and WithThreshold(-1) to check the error returned by Close using
require.NoError, ensuring cleanup failures are reported instead of discarded.

In `@types/function_test.go`:
- Around line 12-41: Replace representation-based assertions with checks of
exported, observable behavior across types/function_test.go ranges 12-41, 79-84,
115-125, and 153-165; avoid direct FunctionType, Function, Handlers, Locals,
Code, and slot-storage access, adding public accessors only for intentional API
contracts. Apply the same change in types/array_test.go:11-18 and
types/closure_test.go:50-60 by testing array and closure behavior instead of
Typ, Elems, Fn, or Upvals fields. Update optimize/optimizer_test.go:175-180,
278-283, and 338-344 to validate optimization results through public behavior
rather than constants, local counts, bytecode layout, or handler storage.

In `@types/map_test.go`:
- Around line 11-79: Remove representation-level assertions from
types/map_test.go lines 11-79 by testing public map behavior and type methods
instead of Typ, Zero, and map-type fields; update types/struct_test.go lines
15-34 to use Struct.Type and Field behavior rather than Struct.Typ/Data; update
types/struct_test.go lines 126-130 to verify public field lookup rather than
StructType.Fields; update program/parse_test.go lines 15-88 to compare
serialized output or use public accessors instead of direct program section
fields.

---

Outside diff comments:
In `@internal/cmd/geninterp/lower.go`:
- Around line 3754-3812: In the coroutine resume branch generated by resume,
replace the reset assignment for co.value after releaseBox with types.BoxedNull
instead of types.Boxed(0). Keep the existing releaseBox call and all other
coroutine state handling unchanged.

In `@interp/pool_test.go`:
- Around line 245-278: Extend the assertions after the
`metrics.Metric("vm_jit_attempts_total")` check in this pool test to verify a
positive JIT emission indicator, such as `vm_jit_emits_total` being present and
greater than zero. Keep the existing attempts assertion and use the same
metric-validation pattern as nearby tests so the test confirms loop-exit
compilation occurred.

---

Nitpick comments:
In `@benchmarks/control_test.go`:
- Around line 149-171: Replace the manual Build error-handling sequence in
iterativeFib with program.Builder’s MustBuild method if available, returning its
result directly. Apply the same change to the corresponding sieve and benchmark
builder sites identified in the consolidated review, while preserving each
function’s existing behavior.
- Around line 149-218: Extract a package-level helper for building a
program.Builder that panics on Build errors and returns the resulting program.
Replace the repeated build/error/return blocks in iterativeFib and sieve in
benchmarks/control_test.go, and the corresponding blocks in
benchmarks/memory_test.go and benchmarks/numeric_test.go, with this helper; do
not use types.FunctionBuilder.MustBuild unless the same API is added for
program.Builder.

In `@internal/cmd/geninterp/generate_test.go`:
- Around line 16-58: Refactor TestGenerator so each scenario—build, initial
generation, clean -check, stale-file check, and missing-file check—is enclosed
in a descriptive sequential t.Run sub-case. Preserve the shared temp directory
and generated-file state between sub-cases, and keep the existing assertions and
command ordering intact.

In `@internal/cmd/geninterp/lower.go`:
- Around line 1238-1323: Add jen.Comment(...) immediately before each
frame-reuse path’s `i.sp = base + params + locals` assignment in `replace`,
covering both the `targetSlots == 1` and `targetSlots != 1` branches. Document
that `f.coro` is intentionally not reset during tail-call frame reuse so
coroutine identity is preserved; leave the existing fresh-frame resets
unchanged.
- Around line 3673-3736: Extract the shared scalar-kind case list into a
scalarKindCases helper returning the six existing jen.Code entries, then use its
expanded result in the jen.Case calls within refGet and refSet. Preserve the
current scalar kinds and default ErrTypeMismatch behavior while removing both
duplicated inline lists.

In `@interp/coroutine_test.go`:
- Line 26: Rename the top-level test function TestCoroutineReferences to
TestInterpreter_Run so it follows the repository convention and reflects the
public symbol under test. Keep the existing coroutine scenarios as sub-cases
under t.Run.
- Around line 69-73: Add a brief comment next to the IP comparison in the
interp.WithHook callback explaining that 19 is the encoded byte offset of the
RESUME instruction, making clear the literal must be updated if instruction
widths change.
- Around line 220-228: The test currently relies on the heap limit to detect
leaked completion references, but this dependency is undocumented. Add a
one-line comment near the vm.Run assertion explaining that WithHeapLimit(5)
makes a leaked discarded reference cause the final REF_NEW to return
ErrHeapExhausted, preserving the release coverage.

In `@interp/host_test.go`:
- Around line 490-499: Update the test loop iterations that create interpreters,
including the shown host-object case and the analogous sections near the other
referenced cases, to register interpreter cleanup immediately after creation
using a closure-scoped defer or t.Cleanup. Remove the explicit final i.Close
calls where cleanup is registered, while preserving the existing assertions and
error handling.

In `@interp/pool_test.go`:
- Around line 584-643: Move the var second *interp.Interpreter declaration into
the benchmark iteration loop, immediately before its first assignment from
pool.Get. Keep all existing uses, timer handling, and pool lifecycle logic
unchanged while eliminating cross-iteration state.
- Line 120: Document the `0xffffffff` operand in the test around `instr.New` as
the encoded `-1` branch value, using a named constant or concise comment. Keep
the unsigned literal and existing instruction behavior unchanged.
🪄 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: 7f94739c-6cce-40b1-aaa9-98da0dee6ff8

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc7e72 and 5a50f61.

📒 Files selected for processing (85)
  • .claude/CLAUDE.md
  • AGENTS.md
  • analysis/blocks_test.go
  • analysis/gvn_test.go
  • asm/amd64/encoder_test.go
  • asm/arm64/encoder_test.go
  • asm/arm64/frame_test.go
  • asm/arm64/instr_test.go
  • asm/arm64/relax_test.go
  • asm/buffer_test.go
  • asm/instr_test.go
  • asm/operand_test.go
  • asm/reg_test.go
  • benchmarks/benchmark_test.go
  • benchmarks/call.go
  • benchmarks/call_test.go
  • benchmarks/compare_test.go
  • benchmarks/control.go
  • benchmarks/control_test.go
  • benchmarks/memory.go
  • benchmarks/memory_test.go
  • benchmarks/numeric.go
  • benchmarks/numeric_test.go
  • cli/cli_test.go
  • cli/fs_test.go
  • cli/repl_test.go
  • cli/run_test.go
  • debug/debugger_test.go
  • docs/coding-patterns.md
  • docs/host-integration.md
  • docs/instruction-set.md
  • docs/memory-model.md
  • docs/testing.md
  • externalize_tests_tmp.go
  • instr/builder_test.go
  • instr/code_test.go
  • instr/fuzz_test.go
  • instr/instr_test.go
  • instr/kind_test.go
  • instr/opcode_test.go
  • instr/parse_test.go
  • instr/type_test.go
  • internal/cmd/geninterp/generate_test.go
  • internal/cmd/geninterp/lower.go
  • interp/cache_test.go
  • interp/coroutine.go
  • interp/coroutine_test.go
  • interp/error_test.go
  • interp/host_test.go
  • interp/interp.go
  • interp/interp_test.go
  • interp/marshal_test.go
  • interp/pool_test.go
  • interp/threaded.go
  • optimize/fuzz_test.go
  • optimize/optimizer_test.go
  • pass/manager_test.go
  • pass/pipeline_test.go
  • prof/collector_test.go
  • prof/jit_test.go
  • prof/profiler_test.go
  • program/builder_test.go
  • program/fuzz_test.go
  • program/parse_test.go
  • program/program_test.go
  • program/verify_test.go
  • transform/as_test.go
  • transform/cd_test.go
  • transform/cf_test.go
  • transform/dce_test.go
  • transform/gvn_test.go
  • transform/rewrite_test.go
  • types/array_test.go
  • types/boxed_test.go
  • types/closure_test.go
  • types/error_test.go
  • types/function_test.go
  • types/fuzz_test.go
  • types/iterator_test.go
  • types/map_test.go
  • types/parse_test.go
  • types/primitive_test.go
  • types/string_test.go
  • types/struct_test.go
  • types/value_test.go
💤 Files with no reviewable changes (6)
  • benchmarks/numeric.go
  • benchmarks/control.go
  • benchmarks/call.go
  • transform/rewrite_test.go
  • benchmarks/memory.go
  • interp/cache_test.go
👮 Files not reviewed due to content moderation or server errors (1)
  • interp/threaded.go

Comment thread cli/cli_test.go

out := bytes.NewBuffer(nil)
root := Root(WithFS(OS()))
root := cli.Root(cli.WithFS(cli.OS()))

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

Use a distinct filesystem in TestWithFS.

cli.Root already uses cli.OS() by default. Passing cli.WithFS(cli.OS()) has no observable effect. This test cannot detect an ignored WithFS option. Inject a distinct cli.WriteFS implementation and read main.vm from that implementation.

As per coding guidelines, tests must verify public observable behavior.

🤖 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 `@cli/cli_test.go` at line 18, Update TestWithFS to inject a distinct
cli.WriteFS implementation instead of cli.OS(), ensuring it can detect whether
cli.WithFS is honored. Configure the test filesystem with main.vm and verify the
rendered result is read from that injected filesystem through cli.Root.

Source: Coding guidelines

Comment thread docs/instruction-set.md
Comment on lines +299 to +300
`REF_SET` mutates scalar cells created by `REF_NEW`; non-scalar targets trap.
Coroutine tail calls preserve the current coroutine. On completion, `CORO_VALUE` exposes the last declared return; earlier returns are discarded.

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 and test non-scalar REF_GET traps.

The PR objective requires scalar-only checks for both REF_SET and REF_GET. The documentation and added rejection test cover only REF_SET.

  • docs/instruction-set.md#L299-L300: State that REF_GET also traps for non-scalar targets.
  • interp/interp_test.go#L287-L294: Add a callable-reference REF_GET case that expects ErrTypeMismatch.

As per coding guidelines, “Update the owning documentation when behavior [or] invariants ... change.”

📍 Affects 2 files
  • docs/instruction-set.md#L299-L300 (this comment)
  • interp/interp_test.go#L287-L294
🤖 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/instruction-set.md` around lines 299 - 300, Update
docs/instruction-set.md lines 299-300 to state that REF_GET, like REF_SET, traps
when targeting a non-scalar reference. In interp/interp_test.go lines 287-294,
add a callable-reference REF_GET case asserting ErrTypeMismatch; no other sites
require changes.

Source: Coding guidelines

Comment thread externalize_tests_tmp.go
Comment on lines +102 to +112
output, err := os.Create(pkg.CompiledGoFiles[index])
if err != nil {
panic(err)
}
if err := format.Node(output, pkg.Fset, file); err != nil {
output.Close()
panic(err)
}
if err := output.Close(); err != nil {
panic(err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent partial source-file rewrites.

os.Create truncates the target before formatting and writing complete. If format.Node, a write, or Close fails, the tool can leave a partial Go source file. Format into memory, write an adjacent temporary file, close it, and rename it only after success. This also removes the unchecked Close error on Line 107.

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 107-107: Error return value of output.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 `@externalize_tests_tmp.go` around lines 102 - 112, Update the source rewrite
flow around os.Create and format.Node to format each file into memory, write the
complete result to an adjacent temporary file, close it with error handling, and
rename it over the target only after all operations succeed. Ensure failures
leave the original compiled Go source intact and remove the direct truncating
write path.

Source: Linters/SAST tools

Comment thread interp/interp_test.go
Comment on lines +24 to +37
type optionCustom int32

func (v optionCustom) MarshalVM(*Interpreter) (types.Value, error) {
return types.I32(v), nil
}

func (v *optionCustom) UnmarshalVM(_ *Interpreter, value types.Value) error {
n, ok := value.(types.I32)
if !ok {
return ErrTypeMismatch
}
*v = optionCustom(n)
return nil
}

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

Move these public-contract tests to package interp_test.

These additions use the internal interp package. They do not validate the importing-client boundary required by this PR.

  • interp/interp_test.go#L24-L37: Move optionCustom to an external test file and qualify interpreter types through interp.
  • interp/interp_test.go#L1552-L1585: Move the scalar REF_* contract cases to an external test file.
  • interp/interp_test.go#L4328-L4341: Move the converter-precedence case to an external test file.

As per coding guidelines, “Every test package must use the production package name plus _test and act as an importing client.”

📍 Affects 1 file
  • interp/interp_test.go#L24-L37 (this comment)
  • interp/interp_test.go#L1552-L1585
  • interp/interp_test.go#L4328-L4341
🤖 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 24 - 37, Move the public-contract tests
from interp/interp_test.go ranges 24-37, 1552-1585, and 4328-4341 into an
external package interp_test file. Relocate optionCustom and qualify its
Interpreter, types, and errors through interp as needed; move the scalar REF_*
contract cases and converter-precedence case unchanged in behavior, ensuring all
three sites test the importing-client boundary.

Source: Coding guidelines

Comment thread interp/interp_test.go
Comment on lines +1575 to +1576
i := New(prog, WithThreshold(-1))
defer i.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

Check the Close error.

Line 1576 discards a cleanup failure. Register cleanup with require.NoError so the test reports leaked-reference or shutdown failures.

Proposed fix
 i := New(prog, WithThreshold(-1))
- defer i.Close()
+ t.Cleanup(func() { require.NoError(t, i.Close()) })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
i := New(prog, WithThreshold(-1))
defer i.Close()
i := New(prog, WithThreshold(-1))
t.Cleanup(func() { require.NoError(t, i.Close()) })
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 1576-1576: Error return value of i.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/interp_test.go` around lines 1575 - 1576, Update the test cleanup
around the interpreter created with New and WithThreshold(-1) to check the error
returned by Close using require.NoError, ensuring cleanup failures are reported
instead of discarded.

Source: Linters/SAST tools

Comment thread types/function_test.go
Comment on lines 12 to 41
func TestNewFunctionBuilder(t *testing.T) {
b := NewFunctionBuilder(nil)
b := types.NewFunctionBuilder(nil)
fn, err := b.Build()
require.NoError(t, err)
require.Equal(t, &FunctionType{}, fn.Typ)
require.Equal(t, &types.FunctionType{}, fn.Typ)
}

func TestFunctionBuilder_Params(t *testing.T) {
fn := NewFunctionBuilder(nil).Params(TypeI32, TypeRef).MustBuild()
require.Equal(t, []Type{TypeI32, TypeRef}, fn.Typ.Params)
fn := types.NewFunctionBuilder(nil).Params(types.TypeI32, types.TypeRef).MustBuild()
require.Equal(t, []types.Type{types.TypeI32, types.TypeRef}, fn.Typ.Params)
}

func TestFunctionBuilder_Returns(t *testing.T) {
fn := NewFunctionBuilder(nil).Returns(TypeI32, TypeRef).MustBuild()
require.Equal(t, []Type{TypeI32, TypeRef}, fn.Typ.Returns)
fn := types.NewFunctionBuilder(nil).Returns(types.TypeI32, types.TypeRef).MustBuild()
require.Equal(t, []types.Type{types.TypeI32, types.TypeRef}, fn.Typ.Returns)
}

func TestFunctionBuilder_Locals(t *testing.T) {
fn := NewFunctionBuilder(nil).Locals(TypeI32, TypeRef).MustBuild()
require.Equal(t, []Type{TypeI32, TypeRef}, fn.Locals)
fn := types.NewFunctionBuilder(nil).Locals(types.TypeI32, types.TypeRef).MustBuild()
require.Equal(t, []types.Type{types.TypeI32, types.TypeRef}, fn.Locals)
}

func TestFunctionBuilder_Captures(t *testing.T) {
fn := NewFunctionBuilder(nil).Captures(TypeI32, TypeF64).MustBuild()
require.Equal(t, []Type{TypeI32, TypeF64}, fn.Captures)
fn := types.NewFunctionBuilder(nil).Captures(types.TypeI32, types.TypeF64).MustBuild()
require.Equal(t, []types.Type{types.TypeI32, types.TypeF64}, fn.Captures)
}

func TestFunctionBuilder_Emit(t *testing.T) {
fn := NewFunctionBuilder(nil).Emit(instr.New(instr.I32_CONST, 42), instr.New(instr.RETURN)).MustBuild()
fn := types.NewFunctionBuilder(nil).Emit(instr.New(instr.I32_CONST, 42), instr.New(instr.RETURN)).MustBuild()
require.Equal(t, []instr.Instruction{instr.New(instr.I32_CONST, 42), instr.New(instr.RETURN)}, instr.Unmarshal(fn.Code))

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

Replace representation assertions with public behavior assertions.

The external-test migration still reads implementation representations. These assertions couple client tests to storage layout and encoded implementation details. Test exported behavior instead. Add a public accessor only if that detail is an intended supported contract.

  • types/function_test.go#L12-L41: Replace direct Function and FunctionType field checks with exported behavior checks.
  • types/function_test.go#L79-L84: Do not assert Function.Handlers directly.
  • types/function_test.go#L115-L125: Do not assert Function.Typ, Function.Locals, or Function.Code directly.
  • types/function_test.go#L153-L165: Do not inspect function fields to test slot construction.
  • types/array_test.go#L11-L18: Do not assert Array.Typ or Array.Elems directly.
  • types/closure_test.go#L50-L60: Do not assert Closure.Typ, Closure.Fn, or Closure.Upvals directly.
  • optimize/optimizer_test.go#L175-L180: Do not inspect optimized constants, locals, or bytecode layout.
  • optimize/optimizer_test.go#L278-L283: Do not inspect optimized constants or local counts.
  • optimize/optimizer_test.go#L338-L344: Do not inspect optimized constants or handler storage.

As per coding guidelines, tests “must not access private symbols or representations and must assert public observable behavior.”

📍 Affects 4 files
  • types/function_test.go#L12-L41 (this comment)
  • types/function_test.go#L79-L84
  • types/function_test.go#L115-L125
  • types/function_test.go#L153-L165
  • types/array_test.go#L11-L18
  • types/closure_test.go#L50-L60
  • optimize/optimizer_test.go#L175-L180
  • optimize/optimizer_test.go#L278-L283
  • optimize/optimizer_test.go#L338-L344
🤖 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 `@types/function_test.go` around lines 12 - 41, Replace representation-based
assertions with checks of exported, observable behavior across
types/function_test.go ranges 12-41, 79-84, 115-125, and 153-165; avoid direct
FunctionType, Function, Handlers, Locals, Code, and slot-storage access, adding
public accessors only for intentional API contracts. Apply the same change in
types/array_test.go:11-18 and types/closure_test.go:50-60 by testing array and
closure behavior instead of Typ, Elems, Fn, or Upvals fields. Update
optimize/optimizer_test.go:175-180, 278-283, and 338-344 to validate
optimization results through public behavior rather than constants, local
counts, bytecode layout, or handler storage.

Source: Coding guidelines

Comment thread types/map_test.go
Comment on lines 11 to 79
func TestNewTypedMap(t *testing.T) {
typ := NewMapType(TypeI32, TypeRef)
m := NewTypedMap[int32](typ, 4)
typ := types.NewMapType(types.TypeI32, types.TypeRef)
m := types.NewTypedMap[int32](typ, 4)
require.Same(t, typ, m.Typ)
require.Equal(t, BoxedNull, m.Zero)
require.Equal(t, types.BoxedNull, m.Zero)
require.Zero(t, m.Len())
}

func TestNewMap(t *testing.T) {
typ := NewMapType(TypeRef, TypeI32)
m := NewMap(typ)
typ := types.NewMapType(types.TypeRef, types.TypeI32)
m := types.NewMap(typ)
require.Same(t, typ, m.Typ)
require.Equal(t, BoxI32(0), m.Zero)
require.Equal(t, types.BoxI32(0), m.Zero)
require.Zero(t, m.Len())
}

func TestNewMapWithCapacity(t *testing.T) {
typ := NewMapType(TypeI32, TypeRef)
m := NewMapWithCapacity(typ, 8)
typ := types.NewMapType(types.TypeI32, types.TypeRef)
m := types.NewMapWithCapacity(typ, 8)
require.Same(t, typ, m.Typ)
require.Equal(t, BoxedNull, m.Zero)
require.Equal(t, types.BoxedNull, m.Zero)
require.Zero(t, m.Len())
}

func TestNewMapForType(t *testing.T) {
structType := NewStructType(NewStructField(TypeI32))
structType := types.NewStructType(types.NewStructField(types.TypeI32))
tests := []struct {
typ *MapType
typ *types.MapType
want any
}{
{typ: NewMapType(TypeI32, TypeI32), want: (*TypedMap[int32])(nil)},
{typ: NewMapType(TypeI64, TypeI32), want: (*TypedMap[int64])(nil)},
{typ: NewMapType(TypeF32, TypeI32), want: (*TypedMap[float32])(nil)},
{typ: NewMapType(TypeF64, TypeI32), want: (*TypedMap[float64])(nil)},
{typ: NewMapType(TypeRef, TypeI32), want: (*Map)(nil)},
{typ: NewMapType(TypeString, TypeI32), want: (*Map)(nil)},
{typ: NewMapType(structType, TypeI32), want: (*Map)(nil)},
{typ: types.NewMapType(types.TypeI32, types.TypeI32), want: (*types.TypedMap[int32])(nil)},
{typ: types.NewMapType(types.TypeI64, types.TypeI32), want: (*types.TypedMap[int64])(nil)},
{typ: types.NewMapType(types.TypeF32, types.TypeI32), want: (*types.TypedMap[float32])(nil)},
{typ: types.NewMapType(types.TypeF64, types.TypeI32), want: (*types.TypedMap[float64])(nil)},
{typ: types.NewMapType(types.TypeRef, types.TypeI32), want: (*types.Map)(nil)},
{typ: types.NewMapType(types.TypeString, types.TypeI32), want: (*types.Map)(nil)},
{typ: types.NewMapType(structType, types.TypeI32), want: (*types.Map)(nil)},
}
for _, tt := range tests {
t.Run(tt.typ.Key.String(), func(t *testing.T) {
require.IsType(t, tt.want, NewMapForType(tt.typ, 0))
require.IsType(t, tt.want, types.NewMapForType(tt.typ, 0))
})
}
}

func TestNewMapIterator(t *testing.T) {
m := NewTypedMap[int64](NewMapType(TypeI64, TypeI32), 0)
it := NewMapIterator(7, m)
require.Equal(t, NewIteratorType(TypeI64), it.Type())
m := types.NewTypedMap[int64](types.NewMapType(types.TypeI64, types.TypeI32), 0)
it := types.NewMapIterator(7, m)
require.Equal(t, types.NewIteratorType(types.TypeI64), it.Type())
require.True(t, it.Done())
}

func TestNewMapType(t *testing.T) {
t.Run("reference key and i64 value", func(t *testing.T) {
typ := NewMapType(TypeString, TypeI64)
require.Equal(t, TypeString, typ.Key)
require.Equal(t, TypeI64, typ.Elem)
require.Equal(t, KindRef, typ.KeyKind)
require.Equal(t, KindI64, typ.ElemKind)
typ := types.NewMapType(types.TypeString, types.TypeI64)
require.Equal(t, types.TypeString, typ.Key)
require.Equal(t, types.TypeI64, typ.Elem)
require.Equal(t, types.KindRef, typ.KeyKind)
require.Equal(t, types.KindI64, typ.ElemKind)
require.True(t, typ.TraceKeys)
require.True(t, typ.TraceValues)
})

t.Run("primitive key and value", func(t *testing.T) {
typ := NewMapType(TypeI32, TypeI32)
typ := types.NewMapType(types.TypeI32, types.TypeI32)
require.False(t, typ.TraceKeys)
require.False(t, typ.TraceValues)
})
}

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

Remove direct representation checks from external tests.

The external test migration still validates internal storage fields. Replace these checks with public observable behavior.

  • types/map_test.go#L11-L79: Replace direct Typ, Zero, and map-type field checks with public map behavior and type methods.
  • types/struct_test.go#L15-L34: Replace direct Struct.Typ and Struct.Data checks with Type and Field behavior.
  • types/struct_test.go#L126-L130: Replace direct StructType.Fields checks with public field lookup behavior.
  • program/parse_test.go#L15-L88: Replace direct program section field comparisons with serialization or public accessors.

As per coding guidelines, tests must not access private symbols or representations and must assert public observable behavior.

📍 Affects 3 files
  • types/map_test.go#L11-L79 (this comment)
  • types/struct_test.go#L15-L34
  • types/struct_test.go#L126-L130
  • program/parse_test.go#L15-L88
🤖 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 `@types/map_test.go` around lines 11 - 79, Remove representation-level
assertions from types/map_test.go lines 11-79 by testing public map behavior and
type methods instead of Typ, Zero, and map-type fields; update
types/struct_test.go lines 15-34 to use Struct.Type and Field behavior rather
than Struct.Typ/Data; update types/struct_test.go lines 126-130 to verify public
field lookup rather than StructType.Fields; update program/parse_test.go lines
15-88 to compare serialized output or use public accessors instead of direct
program section fields.

Source: Coding guidelines

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