fix(vm): widen OP_LINE operand to 32-bit — line numbers wrapped at 65536 (#630)#663
Merged
Merged
Conversation
…536 (#630) The lexer, parser, and EigsChunk.lines[] all carry line as int and were correct, but the OP_LINE bytecode operand was 16-bit, so every line the VM tracked was line % 65536. Two assignments exactly 65536 lines apart collapsed onto one line identity: what is x at 4464 -> 222 (should be 111 — 222 is bound at line 70000, which wrapped onto stamp 4464) rc=0, no diagnostic. Everything reading g_vm.current_line was affected: temporal at/when/state_at, runtime-error and stack-trace line numbers, and the trace-tape line context — all silently wrong past line 65535, which is exactly the regime generated programs (iLambdaAi) reach. Fix: OP_LINE carries a 32-bit operand. A program would need 2^31 lines to overflow that. Line stamps are deduped (emit only on change), so the 2 extra bytes per change are negligible, and this is the correctness-first VM tier (not a perf path). No opcode number changed (ABI static-asserts unaffected); no bytecode is serialized to disk, so no tape/bundle format bump. Touched every site that encodes or strides the operand — a missed one desyncs the bytecode walk silently: - emit: chunk_emit_u32 / emit_op_u32 / emit_line (compiler.c, chunk.c) - read: read_u32 + CASE(LINE) + leaf-accessor mini-exec (vm.c) - verify: chunk_verify special-case + op_verify_operands (chunk.c) - disasm: dedicated 32-bit branch + removed from op_has_u16 (chunk.c) - leaf-accessor compile scan (chunk.c) - JIT: supported-prefix scanner + thunk emitter, 3->5 byte stride (jit.c) Regression test [120] (generated at test time, like the [114] constant-pool sibling): two assignments 65536 apart, asserts 'what is x at L' = 111/222, error line > 65535, and JIT-tier agreement — each fails on the pre-fix VM (222/222, line 4469). Verifier path (hand-built bytecode, test_vm_run_bytecode) green under ASan, confirming the chunk_verify stride stays in sync. Suite 3078/3078 release, 3082/3082 ASan+UBSan (detect_leaks=1, tally 0).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #630
The lexer, parser, and
EigsChunk.lines[]all carrylineasintand were correct. But theOP_LINEbytecode operand was 16-bit, so every line the VM tracked at runtime wasline % 65536.The sharp form: rc=0, wrong answer, no diagnostic
Two assignments exactly 65536 lines apart collapse onto one line identity:
222is the value bound on line 70000, which wrapped onto stamp 4464. No error, exit 0. Everything readingg_vm.current_linewas affected — temporalat/when/state_at, runtime-error and stack-trace line numbers, and the trace-tape line context — all silently wrong past line 65535. That is exactly the regime generated programs (iLambdaAi) reach, and it's the one consumer that can't eyeball a wrong line.Fix
OP_LINEcarries a 32-bit operand (a program would need 2³¹ lines to overflow that). Line stamps are deduped — emitted only when the line changes — so the 2 extra bytes per change are negligible, and this is the correctness-first VM tier, not a perf path.test_opcode_abi.cstatic-asserts are unaffected.--bundleattaches source, not bytecode) → no tape/bundle format bump.Every operand-stride site (a missed one desyncs the walk silently)
compiler.cchunk_emit_u32/emit_op_u32/emit_lineemits 4 bytesvm.cread_u32+CASE(LINE)+ leaf-accessor mini-executor stride 4chunk.cverifierchunk_verifyspecial-cases OP_LINE (4-byte skip); removed fromop_verify_operandschunk.cdisasmop_has_u16chunk.cjit.cVerification
111, 222under interpreter and JIT (was222, 222). A hot loop forced to JIT-compile past line 65536 returns the correct result and agrees byte-for-byte withEIGS_JIT_OFF.test_vm_run_bytecode, hand-built bytecode) green under ASan — confirms thechunk_verifystride stays in lockstep.Gates
detect_leaks=13082/3082, leak tally 0, no sanitizer errorsmake jit-smokepasses🤖 Generated with Claude Code