Skip to content

fix(interp): trap negative array.copy size#140

Merged
siyul-park merged 2 commits into
mainfrom
fix/geninterp-array-copy
Jul 12, 2026
Merged

fix(interp): trap negative array.copy size#140
siyul-park merged 2 commits into
mainfrom
fix/geninterp-array-copy

Conversation

@siyul-park

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

Copy link
Copy Markdown
Owner

Summary

  • Bug fix: ARRAY_COPY with a negative size passed the offset<0 || offset+size>length bounds guard and escaped as a raw Go slice bounds out of range runtime panic instead of the VM's typed ErrIndexOutOfRange. The generator (lowerArrayCopy) now emits a single if size < 0 trap right after popping size, consistent with array.slice's start > end trap. array.fill's negative-size no-op is deliberate (it compensates ref ownership when size <= 0) and stays unchanged. No JIT mirror needed — ARRAY_COPY stays threaded per docs/instruction-set.md.
  • Efficiency: I32_OR shared I32_XOR's tag-restore formula. Both operands are verified KindI32 before apply(), so their tag bits are identical and OR preserves them directly — I32_OR now lowers to types.Boxed(uint64(lhs) | uint64(rhs)), the same shape as the existing I32_AND case. All 15 fused sites plus the standalone handler lose two 64-bit ANDs and the masking per execution. XOR keeps the tag-restore formula (XOR zeroes the tag bits).
  • Quality: the kept-scan release loop was hand-inlined four times in lowerCall / lowerReturnCall's *HostFunction cases; they now call the existing release() builder. Generated output changes only local variable names. No new helpers introduced.

All changes are in internal/cmd/geninterp/lower.go; interp/threaded.go is regenerated output (make generate, verified by make check-generated).

Test plan

  • New TestInterpreter_Run subtest: ARRAY_COPY with size = -1 and offsets chosen so offset+size <= length → expects ErrIndexOutOfRange (RED before fix: runtime error: slice bounds out of range [2:1]; GREEN after)
  • I32_OR result value covered by existing test (12 | 10 == 14)
  • make generate + make check-generated clean
  • go test -race ./... full suite passes
  • make lint clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Corrected 32-bit bitwise operation handling for more reliable results.
    • Added validation for invalid or negative array-copy sizes, which now consistently return an index-out-of-range error instead of proceeding unexpectedly.
    • Improved cleanup during host-function calls and returns to help prevent incorrect reference handling.
    • Updated interpreter behavior to keep boxed arithmetic and bitwise values accurate.

Follow-up

  • refactor(geninterp): dropped the lower prefix from all 210 opcode-handler functions in internal/cmd/geninterp/lower.go — the lowerings table already names their role. CALL/RETURN/SELECT take an Op suffix (callOp, returnOp, selectOp): call collides with the fused-call builder, return/select are Go keywords. Generated interp/threaded.go is byte-identical.

A negative size passed the offset<0 || offset+size>length guard and
escaped as a raw Go slice-bounds panic instead of ErrIndexOutOfRange.
Guard size < 0 once after popping it, consistent with array.slice's
start > end trap. array.fill's negative-size no-op stays as designed.

Also simplify I32_OR to Boxed(lhs | rhs) — operands are verified
KindI32 so their tags are identical and OR preserves them; only XOR
needs the tag-restore formula. Reuse the existing release() builder
for the four hand-inlined kept-scan loops in CALL/RETURN_CALL host
paths; generated output changes only local variable names.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The interpreter updates boxed bitwise operation handling, adds negative-size validation for array copies, and refactors host-call reference cleanup in generated and threaded execution paths. A new test covers invalid array-copy parameters.

Changes

Interpreter correctness

Layer / File(s) Summary
Boxed bitwise operation updates
internal/cmd/geninterp/lower.go, interp/threaded.go
Separates i32 XOR and OR lowering, and changes threaded boxed-value construction to direct operand OR operations.
Array-copy size validation
internal/cmd/geninterp/lower.go, interp/threaded.go, interp/interp_test.go
Adds ErrIndexOutOfRange handling for negative computed copy sizes and tests an invalid array-copy execution.
Host-call reference cleanup
internal/cmd/geninterp/lower.go, interp/threaded.go
Uses shared release helpers in generated host-call paths and direct result-membership checks in threaded cleanup paths.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not use the required Changes Made/Related Issues/Additional Information template headings. Reformat it to the repo template and add the three required sections, including Related Issues and Additional Information.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 accurately summarizes the main change: trapping negative ARRAY_COPY sizes in interp.
✨ 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 fix/geninterp-array-copy

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

Every entry in the lowerings table was named lower<Opcode>; the table
already says what they are. Rename to the bare opcode name. CALL,
RETURN, and SELECT take an Op suffix: call collides with the fused-call
builder, return and select are Go keywords. Generated output is
unchanged.

Copy link
Copy Markdown
Owner Author

PR Review Summary — fix(interp): trap negative array.copy size

Status: ✅ Merge Ready

Test Results

All tests passed:

  • go test -race ./... full suite passes
  • geninterp tests completed (132.859s)
  • No test failures
  • Code generation verified with make generate

Merge Readiness Assessment

Decision: Approve

This PR is focused, well-scoped, and ready to merge.

Scope & Correctness

ARRAY_COPY Negative Size Trap

  • Bug fix converts raw Go slice bounds out of range panic to proper ErrIndexOutOfRange
  • Trap placed immediately after popping size, consistent with array.slice pattern
  • Test case correctly isolates the edge case: offset=2, size=-1, length=3 where offset+size <= length

I32_OR Efficiency Optimization

  • Simplified from tag-restore formula to direct bitwise OR: Boxed(uint64(lhs) | uint64(rhs))
  • Safe because bytecode verifier enforces both operands are KindI32
  • In NaN boxing, identical tags OR to identical tags
  • Matches existing I32_AND pattern
  • No JIT mirror needed per docs/instruction-set.md

Code Quality Refactoring

  • Hand-inlined ref-release loops in lowerCall and lowerReturnCall now call release() helper
  • Generated code is deterministic; make generate verified no diff
  • Reduces code duplication in generator output

Risk Assessment

No Breaking Changes

  • Bug fix improves safety
  • Optimization reduces instruction overhead (neutral to positive performance)
  • Refactoring is internal, no public API impact

Generated Code Consistency

  • make generate confirmed threaded.go matches lower.go
  • All package tests pass (analysis, asm, cli, debug, instr, interp, etc.)

Readiness Checklist

  • Scope control: Three clearly separated concerns, no unrelated cleanup
  • Correctness: All three changes verified as correct
  • Test coverage: New test covers ARRAY_COPY negative size edge case
  • Architecture: Follows existing patterns, consistent with codebase conventions
  • Risk & compatibility: No breaking changes, improved safety
  • Maintainability: Code quality improved, duplication reduced
  • CI/validation: Full test suite passes, code generation verified

Minimal Path to Merge: No changes required. Ready to merge once CI completes.


Generated by Claude Code

@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)
internal/cmd/geninterp/lower.go (1)

1865-1877: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated HostFunction lowering block into a shared helper.

The entire HostFunction case body (args slice, fn.Fn call, err check, both release() calls, sp adjustment, copy of out, ip++) is identical between lowerCall (1865-1877) and lowerReturnCall (4343-4355). This PR already extracted release() for reuse; extending that pattern to the whole block would prevent the two copies from drifting again in the future.

♻️ Sketch of a shared helper
+func lowerHostCall() jen.Code {
+	return jen.Block(
+		jen.List(jen.Id("fn")).Op(":=").List(jen.Id("fn")),
+		jen.List(jen.Id("params")).Op(":=").List(jen.Id("len").Call(jen.Id("fn").Dot("Typ").Dot("Params"))),
+		jen.List(jen.Id("returns")).Op(":=").List(jen.Id("len").Call(jen.Id("fn").Dot("Typ").Dot("Returns"))),
+		jen.If(jen.Id("i").Dot("sp").Op("<=").Add(jen.Id("params"))).Block(jen.Id("panic").Call(jen.Id("ErrStackUnderflow"))),
+		jen.If(jen.Id("i").Dot("sp").Op("+").Add(jen.Id("returns")).Op("-").Add(jen.Id("params")).Op("-").Add(jen.Lit(1)).Op(">").Add(jen.Id("len").Call(jen.Id("i").Dot("stack")))).Block(jen.Id("panic").Call(jen.Id("ErrStackOverflow"))),
+		jen.List(jen.Id("args")).Op(":=").List(jen.Id("i").Dot("stack").Index(jen.Id("i").Dot("sp").Op("-").Add(jen.Id("params")).Op("-").Add(jen.Lit(1)).Op(":").Add(jen.Id("i").Dot("sp").Op("-").Add(jen.Lit(1))))),
+		jen.List(jen.Id("out"), jen.Id("err")).Op(":=").List(jen.Id("fn").Dot("Fn").Call(jen.Id("i"), jen.Id("args"))),
+		jen.If(jen.Id("err").Op("!=").Add(jen.Id("nil"))).Block(jen.Id("panic").Call(jen.Id("err"))),
+		release(jen.Id("args"), jen.Id("out")),
+		release(jen.Id("i").Dot("stack").Index(jen.Id("i").Dot("sp").Op("-").Add(jen.Lit(1)).Op(":").Add(jen.Id("i").Dot("sp"))), jen.Id("out")),
+		jen.List(jen.Id("i").Dot("sp")).Op("+=").List(jen.Id("returns").Op("-").Add(jen.Id("params")).Op("-").Add(jen.Lit(1))),
+		jen.Id("copy").Call(jen.Id("i").Dot("stack").Index(jen.Id("i").Dot("sp").Op("-").Add(jen.Id("returns")).Op(":").Add(jen.Id("i").Dot("sp"))), jen.Id("out")),
+		jen.Id("i").Dot("fr").Dot("ip").Op("++"),
+	)
+}

Then both lowerCall and lowerReturnCall call lowerHostCall() in place of the repeated block.

Also applies to: 4343-4355

🤖 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 1865 - 1877, Extract the
duplicated HostFunction case body from lowerCall and lowerReturnCall into a
shared lowerHostCall helper, including argument slicing, fn.Fn invocation and
error handling, both release calls, stack-pointer adjustment, output copy, and
instruction-pointer increment. Replace both inline case bodies with calls to
lowerHostCall while preserving their existing behavior and using the established
release helper.
🤖 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 `@internal/cmd/geninterp/lower.go`:
- Around line 1865-1877: Extract the duplicated HostFunction case body from
lowerCall and lowerReturnCall into a shared lowerHostCall helper, including
argument slicing, fn.Fn invocation and error handling, both release calls,
stack-pointer adjustment, output copy, and instruction-pointer increment.
Replace both inline case bodies with calls to lowerHostCall while preserving
their existing behavior and using the established release helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e0f5c26-b3f9-43d1-a003-1daec2abefa2

📥 Commits

Reviewing files that changed from the base of the PR and between b87e746 and ee84c2f.

📒 Files selected for processing (3)
  • internal/cmd/geninterp/lower.go
  • interp/interp_test.go
  • interp/threaded.go
👮 Files not reviewed due to content moderation or server errors (1)
  • interp/threaded.go

@siyul-park
siyul-park merged commit ecf095b into main Jul 12, 2026
3 checks passed
@siyul-park
siyul-park deleted the fix/geninterp-array-copy branch July 12, 2026 07:55
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