Skip to content

A bytecode operand-width change is an ABI break for external producers, and nothing gates it #704

Description

@InauguralPhysicist

What happened

#630 widened OP_LINE's operand from 16 to 32 bits — a correct fix (line
numbers wrapped at 65536). Its commit message says "no opcode number or
on-disk format changed", which is true and also the wrong frame: the operand
width is the bytecode ABI for anything that produces a chunk and hands it
to vm_run_bytecode.

Two consumers do exactly that, and both broke silently at rc=0:

  • ouroboros — the self-hosted codegen emits OP_LINE + a 2-byte operand.
    Under v0.33.0 the VM reads 4 bytes, so it consumes the next instruction's
    first two bytes as the high half of the line number and resumes misaligned.
    Every behavioral-parity program produced empty output with exit 0. The
    bootstrap fixed point still PASSED, because self-host-compiling itself is
    byte-comparison, not execution — so the suite's loudest signal stayed green
    while every executed program produced nothing.
  • iLambdaAi — its grading ladder compiles a candidate with the vendored
    ouroboros codegen and runs it through sandbox_run. Every "runs" rung
    scored 1 instead of 2: a grader that quietly lost the ability to award its
    top grade, which is the exact failure mode that research loop cannot
    self-detect.

Minimal repro (from an ouroboros checkout, pre-fix):

load_file of "src/frontend.eigs"
load_file of "src/codegen.eigs"
r is vm_run_bytecode of (ouro_compile of "print of (2 + 3)")

v0.32.0 prints 5. v0.33.0 prints nothing, returns null, exits 0.

Why CI could not catch it

Consumers pin a release. A mid-release operand-width change therefore cannot
fail any consumer's CI until the version bump — by which time the release is
already cut. The pre-bump consumer sweep caught this one; that sweep is a
manual ritual, not a gate.

What would catch it

Options, cheapest first:

  1. An in-repo external-producer canary. A test that hand-assembles a tiny
    chunk (including OP_LINE) and runs it through vm_run_bytecode, asserting
    the output. Any operand-width or opcode-numbering change fails it in the
    same PR. This is the one that scales — it needs no sibling checkout.
  2. A bytecode ABI version. Stamp an ABI revision the VM checks when it is
    handed a chunk, so a stale producer gets a loud error instead of executing
    garbage. Turns a silent-wrong into a named failure even when the canary is
    not updated.
  3. Document the seam. The eigenscript-extend-vm checklist covers adding
    an opcode; it should also cover changing an existing opcode's operand
    width
    , and name vm_run_bytecode's external producers as a site to update.

(1) + (3) are small and I would do both; (2) is the real fix if we intend
vm_run_bytecode to be a supported public seam rather than an ouroboros
private door.

Found during the v0.33.0 pre-bump sweep. Downstream emitter fixes are landing
in ouroboros and iLambdaAi.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions