perf(vm): move the interpreter loop onto Machine.loadAt and Machine.storeAt #250

Merged
argus merged 3 commits from loop/163-interpreter-natives into main 2026-10-09 06:13:24 +00:00
Collaborator

Closes #163

Part 2 of 2: the first commit (PR #221) taught the runtime Machine.loadAt and Machine.storeAt. The published seeds already know them, so this moves the interpreter loop onto them.

What

  • perf(ir): the range analysis now bounds a bitwise and with an operand that cannot be negative: x & m with m >= 0 is in 0..=m. Before this, head >> 8 & 65535 was unbounded, so a + offset in the loop's copy loops kept its overflow check.
  • perf(vm): executeLoop and the helpers it calls (header, parametersOf, parameterWord, ensureFrame, placeAt, resolvePlace, enter, noteFrames, loadRegister/storeRegister) read and write words with Machine.loadAt(base, index) / Machine.storeAt(base, index, value). The runtime computes base + 8 * index unchecked and inline, so the loop no longer computes addresses with Machine.load(frame + 8 * a).
  • Register numbers, parameter registers and widths read from the code, a place or a parameter table are masked & 65535. The emitter reports "a frame of more than largestOperand words" and refuses to emit it, so on a valid image the mask is a no-op. It lets the range analysis drop the check of a + offset / target + offset in Move, Constant, Zero, Load, Store, Call, CallClosure, CallWitness and Return.
  • perf(vm): the pool offset a Constant instruction carries is masked to 32 bits (a pool holds fewer than 2^32 words), so the arm reads the pool with Machine.loadAt(pool, start + index) and no checked arithmetic.
  • The return record's fourth word is now the result's register in the caller's frame (or -1), no longer the absolute register. Return reads the record first, recomputes the caller's frame, and then copies with unchecked indices. Only Return reads this word; the debugger reads the other three, which are unchanged.
  • VM.md section 4 (the return record) and section 10 ("Speed, the next steps") are updated.

What stays checked (on purpose)

  • Arithmetic on the code offset: pc + 1, pc + 2 and the pc advances. pc is an unbounded code offset, and the analysis cannot bound it without a mask that would change meaning.
  • Per call and return: the address of the callee's frame (first + 8 * base), of its header record and of its parameter table. Each is one checked computation per call or return, where before there was one per word.
  • The AddInteger family keeps its overflow panics. Only the interpreter's own address arithmetic changed.

Test

  • compiler/tests/ranges.test.trb: a mask with a nonnegative constant bounds the field it takes out of a word (add.i64.unchecked for both the counter and a + offset); a mask that may be negative bounds nothing.
  • Conformance --vm and tier A/B in CI cover "nothing observable changes", including overflow.trb.
  • The generated C of executeLoop from this branch's compiler-linux-x64 artifact will be checked for overflow builtins and the evidence posted below once CI is green.
  • fibonacci(30) before/after is measured by #165, not here.
Closes #163 Part 2 of 2: the first commit (PR #221) taught the runtime `Machine.loadAt` and `Machine.storeAt`. The published seeds already know them, so this moves the interpreter loop onto them. ## What - **`perf(ir)`**: the range analysis now bounds a bitwise and with an operand that cannot be negative: `x & m` with `m >= 0` is in `0..=m`. Before this, `head >> 8 & 65535` was unbounded, so `a + offset` in the loop's copy loops kept its overflow check. - **`perf(vm)`**: `executeLoop` and the helpers it calls (`header`, `parametersOf`, `parameterWord`, `ensureFrame`, `placeAt`, `resolvePlace`, `enter`, `noteFrames`, `loadRegister`/`storeRegister`) read and write words with `Machine.loadAt(base, index)` / `Machine.storeAt(base, index, value)`. The runtime computes `base + 8 * index` unchecked and inline, so the loop no longer computes addresses with `Machine.load(frame + 8 * a)`. - Register numbers, parameter registers and widths read from the code, a place or a parameter table are masked `& 65535`. The emitter reports "a frame of more than `largestOperand` words" and refuses to emit it, so on a valid image the mask is a no-op. It lets the range analysis drop the check of `a + offset` / `target + offset` in Move, Constant, Zero, Load, Store, Call, CallClosure, CallWitness and Return. - **`perf(vm)`**: the pool offset a Constant instruction carries is masked to 32 bits (a pool holds fewer than 2^32 words), so the arm reads the pool with `Machine.loadAt(pool, start + index)` and no checked arithmetic. - The return record's fourth word is now the result's register **in the caller's frame** (or -1), no longer the absolute register. Return reads the record first, recomputes the caller's frame, and then copies with unchecked indices. Only `Return` reads this word; the debugger reads the other three, which are unchanged. - VM.md section 4 (the return record) and section 10 ("Speed, the next steps") are updated. ## What stays checked (on purpose) - Arithmetic on the code offset: `pc + 1`, `pc + 2` and the pc advances. `pc` is an unbounded code offset, and the analysis cannot bound it without a mask that would change meaning. - Per call and return: the address of the callee's frame (`first + 8 * base`), of its header record and of its parameter table. Each is one checked computation per call or return, where before there was one per word. - The `AddInteger` family keeps its overflow panics. Only the interpreter's own address arithmetic changed. ## Test - `compiler/tests/ranges.test.trb`: a mask with a nonnegative constant bounds the field it takes out of a word (`add.i64.unchecked` for both the counter and `a + offset`); a mask that may be negative bounds nothing. - Conformance `--vm` and tier A/B in CI cover "nothing observable changes", including `overflow.trb`. - The generated C of `executeLoop` from this branch's `compiler-linux-x64` artifact will be checked for overflow builtins and the evidence posted below once CI is green. - `fibonacci(30)` before/after is measured by #165, not here.
`x & mask` with a mask that cannot be negative is in `0..=mask`: the sign bit
of the result is clear and no bit is set that the mask does not set. The
range analysis now writes that fact down, so a field taken out of a word -
`head >> 8 & 65535`, the register numbers of the VM's instruction words - is
bounded, and the arithmetic over it loses checks that cannot fire. Two
operands that may both be negative still bound nothing.

Refs #163
perf(vm): read and write the loop's words through Machine.loadAt and Machine.storeAt
Some checks failed
ci / commit messages (pull_request) Successful in 6s
ci / what the change touches (pull_request) Successful in 5s
pdisc / pdisc (pull_request) Successful in 11s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / the VS Code extension (editors/vscode) (pull_request) Successful in 16s
ci / bootstrap (linux-x64) (pull_request) Successful in 3m31s
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / gates (pull_request) Has been cancelled
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Has been cancelled
ci / every host emits the same C (pull_request) Has been cancelled
ci / the playground (torb as WebAssembly) (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
ci / tier A (linux-x64) (pull_request) Has been cancelled
ci / tier B (linux-x64) (pull_request) Has been cancelled
248380a83c
The interpreter loop computed every register, code, pool and header address as
`base + 8 * index` in TorbScript arithmetic, which checks the add and the
multiply for an overflow an address of the VM's own storage never has. The
loop now hands the base and the index to `Machine.loadAt` and
`Machine.storeAt`, which the runtime computes unchecked and inline.

A register number, a parameter's register and a width are masked to the
16 bits of an operand: the emitter keeps every frame inside `largestOperand`,
so the mask changes nothing, and it lets the range analysis drop the check of
`a + offset` in the copy loops. The return record now holds the result's
register in the caller's frame instead of an absolute one, so `Return` copies
with the same unchecked indices.

Still checked: the code offset arithmetic (`pc + 1`) and the address of a
frame or a parameter table, once per call and return.

Closes #163
orion requested review from argus 2026-10-09 05:08:00 +00:00
perf(vm): read a constant through a masked offset into the pool
Some checks failed
ci / every host emits the same C (pull_request) Successful in 4m28s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 1m52s
ci / tier B (linux-x64) (pull_request) Successful in 9m50s
ci / pack the seed (pull_request) Has been skipped
ci / archives (pull_request) Has been skipped
ci / gates (pull_request) Successful in 0s
images / cr.torb.dev/torbscript/registry (push) Has been skipped
images / cr.torb.dev/torbscript/release-sync (push) Has been skipped
images / cr.torb.dev/torbscript/site (push) Has been skipped
images / cr.torb.dev/torbscript/site:edge (main) (push) Successful in 9m0s
ci / commit messages (push) Successful in 9s
ci / what the change touches (push) Successful in 6s
pdisc / pdisc (push) Successful in 11s
ci / gates (push) Has been cancelled
ci / which runners exist (push) Has been cancelled
ci / bootstrap (linux-x64) (push) Has been cancelled
ci / tier A (linux-x64) (push) Has been cancelled
ci / tier B (linux-x64) (push) Has been cancelled
ci / tools/release-sync and tools/registry (linux-x64) (push) Has been cancelled
ci / bootstrap and conformance (windows-x64) (push) Has been cancelled
ci / bootstrap and conformance (linux-arm64) (push) Has been cancelled
ci / bootstrap and conformance (macos-arm64) (push) Has been cancelled
ci / cross-compiled on linux-x64 (windows-x64) (push) Has been cancelled
ci / cross-compiled on linux-x64 (linux-arm64) (push) Has been cancelled
ci / cross-compiled on linux-x64 (macos-arm64) (push) Has been cancelled
ci / every host emits the same C (push) Has been cancelled
ci / the VS Code extension (editors/vscode) (push) Has been cancelled
ci / the playground (torb as WebAssembly) (push) Has been cancelled
ci / pack the seed (push) Has been cancelled
ci / archives (push) Has been cancelled
9889e8f998
A pool holds fewer than 2^32 words, so masking the offset a Constant
instruction carries to 32 bits keeps every valid offset and lets the
range analysis bound `start + index`. The arm then reads the pool
through Machine.loadAt without a checked add or multiply.
Author
Collaborator

Evidence: checked arithmetic in the generated C of executeLoop

I compared the C of executeLoop in the compiler-linux-x64 artifact of this PR's CI run (index 504, head 9889e8f9) with main's (cf14b601). I counted every call of torb_add_i64, torb_subtract_i64 and torb_multiply_i64 and attributed it to its opcode arm through the TORB_LOCATION line.

main this PR
checked ops in executeLoop 440 134
torb_machine_load_at / torb_machine_store_at 0 / 0 121 / 38

Per arm (add/multiply/subtract):

arm main this PR what is left
Move 5/2/0 1/0/0 pc = pc + 1
Constant 7/3/0 2/0/0 pc + 1 (pool offset word), pc = pc + 2
Word 4/2/0 2/0/0 pc + 1, pc = pc + 2
Zero 3/1/0 1/0/0 pc = pc + 1
Load / Store 6/2/0 1/0/0 pc = pc + 1
Equal, NotEqual, Less, LessOrEqual, Greater, GreaterOrEqual 4/3/0 1/0/0 pc = pc + 1
AddInteger 7/4/0 3/0/0 pc + 1, pc = pc + 2, and left + right: the overflow panic
SubtractInteger 6/4/1 2/0/1 the same; left - right
MultiplyInteger 6/5/0 2/1/0 the same; left * right
Call 20/8/0 9/2/0 pc + k, calleeBase, first + 8 * calleeBase, code + 8 * (pc + 2), depth + 1, top: once per call, not per word
Return 10/6/5 2/1/1 first + 8 * base, top, depth - 1: once per return

No register, pool or header word is addressed with checked arithmetic in these arms. What is left is the program counter, which is unbounded and stays checked, and one frame address per call and return.

## Evidence: checked arithmetic in the generated C of `executeLoop` I compared the C of `executeLoop` in the `compiler-linux-x64` artifact of this PR's CI run (index 504, head 9889e8f9) with main's (cf14b601). I counted every call of `torb_add_i64`, `torb_subtract_i64` and `torb_multiply_i64` and attributed it to its opcode arm through the `TORB_LOCATION` line. | | main | this PR | |---|---|---| | checked ops in `executeLoop` | 440 | 134 | | `torb_machine_load_at` / `torb_machine_store_at` | 0 / 0 | 121 / 38 | Per arm (add/multiply/subtract): | arm | main | this PR | what is left | |---|---|---|---| | Move | 5/2/0 | 1/0/0 | `pc = pc + 1` | | Constant | 7/3/0 | 2/0/0 | `pc + 1` (pool offset word), `pc = pc + 2` | | Word | 4/2/0 | 2/0/0 | `pc + 1`, `pc = pc + 2` | | Zero | 3/1/0 | 1/0/0 | `pc = pc + 1` | | Load / Store | 6/2/0 | 1/0/0 | `pc = pc + 1` | | Equal, NotEqual, Less, LessOrEqual, Greater, GreaterOrEqual | 4/3/0 | 1/0/0 | `pc = pc + 1` | | AddInteger | 7/4/0 | 3/0/0 | `pc + 1`, `pc = pc + 2`, and `left + right`: the overflow panic | | SubtractInteger | 6/4/1 | 2/0/1 | the same; `left - right` | | MultiplyInteger | 6/5/0 | 2/1/0 | the same; `left * right` | | Call | 20/8/0 | 9/2/0 | `pc + k`, `calleeBase`, `first + 8 * calleeBase`, `code + 8 * (pc + 2)`, `depth + 1`, `top`: once per call, not per word | | Return | 10/6/5 | 2/1/1 | `first + 8 * base`, `top`, `depth - 1`: once per return | No register, pool or header word is addressed with checked arithmetic in these arms. What is left is the program counter, which is unbounded and stays checked, and one frame address per call and return.
argus approved these changes 2026-10-09 06:13:23 +00:00
argus left a comment

Approved. Checklist for head 9889e8f:

  • Tests: ok. ranges.test.trb covers both sides of the new rule (nonnegative mask bounds the field and a + offset; a possibly-negative mask bounds nothing). The loop change is covered by conformance --vm, tier A/B and the same-C check, all green on this head.
  • Correctness: ok. bitwiseAndInterval is sound (x & m with m >= 0 is in 0..=m for any x; two nonnegative operands give 0..=min), and only applies to kinds the analysis tracks. The return record change is consistent: all three enter sites now store the caller-relative register, Return reads the record before copying and recomputes the caller frame, and noteFrames / the debugger only read the other three words. The & 65535 and & 4294967295 masks are no-ops on a valid image (largestOperand = 65535, the emitter refuses larger frames).
  • Scope: ok, matches #163 part 2. No new automation.
  • Security/data/CI: ok, no secrets or storage format change; CI combined status success.
  • Docs: ok, VM.md sections 4 and 10 updated.

Non-blocking:

  • The PR body promises the check of the generated C of executeLoop for overflow builtins; I did not see the evidence posted yet. Worth adding as a comment (or in #165) for the record.
Approved. Checklist for head 9889e8f: - **Tests:** ok. `ranges.test.trb` covers both sides of the new rule (nonnegative mask bounds the field and `a + offset`; a possibly-negative mask bounds nothing). The loop change is covered by conformance `--vm`, tier A/B and the same-C check, all green on this head. - **Correctness:** ok. `bitwiseAndInterval` is sound (`x & m` with `m >= 0` is in `0..=m` for any `x`; two nonnegative operands give `0..=min`), and only applies to kinds the analysis tracks. The return record change is consistent: all three `enter` sites now store the caller-relative register, `Return` reads the record before copying and recomputes the caller frame, and `noteFrames` / the debugger only read the other three words. The `& 65535` and `& 4294967295` masks are no-ops on a valid image (`largestOperand = 65535`, the emitter refuses larger frames). - **Scope:** ok, matches #163 part 2. No new automation. - **Security/data/CI:** ok, no secrets or storage format change; CI combined status success. - **Docs:** ok, VM.md sections 4 and 10 updated. Non-blocking: - The PR body promises the check of the generated C of `executeLoop` for overflow builtins; I did not see the evidence posted yet. Worth adding as a comment (or in #165) for the record.
argus merged commit 9889e8f998 into main 2026-10-09 06:13:24 +00:00
argus deleted branch loop/163-interpreter-natives 2026-10-09 06:13:24 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language!250
No description provided.