Skip to content

greenhorn: fix JZ encoder bug and expand VM test suite#1

Open
SuperInstance wants to merge 1 commit intomainfrom
greenhorn/fix-jz-expand-tests
Open

greenhorn: fix JZ encoder bug and expand VM test suite#1
SuperInstance wants to merge 1 commit intomainfrom
greenhorn/fix-jz-expand-tests

Conversation

@SuperInstance
Copy link
Copy Markdown
Owner

@SuperInstance SuperInstance commented Apr 12, 2026

Bug fix: JZ/JNZ/JE/JNE/JL/JGE label resolution now uses bytecode offsets instead of instruction indices.
Added 108 tests (up from ~20) covering all opcodes, jumps, stack, flags, and complex programs.

Changes

  • encoder.ts: Three-pass encoder now computes byte offsets for labels (was using instruction indices)
  • encoder.ts: JE/JNE/JL/JGE support both JE label and JE R0, label syntax
  • vm.ts: Added ISHL and ISHR instruction execution
  • vm.test.ts: 108 tests covering arithmetic, bitwise, comparison, conditional/unconditional jumps, stack ops, negative values, trace, max cycles, error states, multi-register programs, complex programs (fibonacci, sum, conditional swap)

Bug Details

Labels were resolved to instruction indices (0, 1, 2...) instead of byte offsets (0, 4, 8...). For variable-length instructions (MOVI=4 bytes, INC=2 bytes, HALT=1 byte), this caused jumps to land in the middle of instructions. The fix adds a size-estimation pass to convert instruction indices to byte offsets before final encoding.

  • Total tests: ~20 → 108 (all passing)
  • Bug fixed: conditional jump encoding
  • All tests passing

Staging: Open in Devin

Copy link
Copy Markdown

@beta-devin-ai-integration beta-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Staging: Open in Devin

Comment thread src/encoder.ts
Comment on lines +51 to +62
const idxToByteOffset = new Map<number, number>();
let byteOffset = 0;
for (let i = 0; i < sizes.length; i++) {
idxToByteOffset.set(i, byteOffset);
byteOffset += sizes[i];
}

// Build final label map with byte offsets
const labels = new Map<string, number>();
for (const [name, instrIdx] of labelIndices) {
labels.set(name, idxToByteOffset.get(instrIdx) ?? instrIdx);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Label at end of program resolves to instruction index instead of byte offset

When a label is defined at the very end of the assembly (after all instructions, with no instruction following it), idxToByteOffset.get(instrIdx) returns undefined because the map only contains entries for indices 0 to sizes.length - 1. The fallback ?? instrIdx then incorrectly uses the raw instruction index as the byte offset, instead of the correct total byte size.

Reproduction demonstrating incorrect label resolution

For JMP end\nMOVI R0, 42\nend::

  • Label end maps to instruction index 2
  • idxToByteOffset only has keys 0 and 1 (for 2 instructions)
  • idxToByteOffset.get(2)undefined → falls back to ?? 2
  • Correct byte offset should be 7 (3 bytes for JMP + 4 bytes for MOVI)
  • JMP encodes target as byte 2, which points into the middle of its own instruction
Suggested change
const idxToByteOffset = new Map<number, number>();
let byteOffset = 0;
for (let i = 0; i < sizes.length; i++) {
idxToByteOffset.set(i, byteOffset);
byteOffset += sizes[i];
}
// Build final label map with byte offsets
const labels = new Map<string, number>();
for (const [name, instrIdx] of labelIndices) {
labels.set(name, idxToByteOffset.get(instrIdx) ?? instrIdx);
}
const idxToByteOffset = new Map<number, number>();
let byteOffset = 0;
for (let i = 0; i < sizes.length; i++) {
idxToByteOffset.set(i, byteOffset);
byteOffset += sizes[i];
}
// Also map the end-of-program index so labels after the last instruction resolve correctly
idxToByteOffset.set(sizes.length, byteOffset);
// Build final label map with byte offsets
const labels = new Map<string, number>();
for (const [name, instrIdx] of labelIndices) {
labels.set(name, idxToByteOffset.get(instrIdx) ?? instrIdx);
}
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

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