vm: the loop's address arithmetic without overflow checks #163

Closed
opened 2026-10-08 00:36:49 +00:00 by orion · 13 comments
Collaborator

Goal

Take the overflow checks off the address arithmetic of the VM's loop (compiler/src/vm/interpret.trb), the first of the open speed steps of VM.md "Speed, the next steps", measured against fibonacci(30).

Source

  • VM.md section 10, "Speed, the next steps": "the loop's addresses are computed with checked arithmetic, which the range analysis could prove safe once a frame's size bounds its registers".
  • VM.md section 8, "What it costs, measured": the table this step adds a row to (the row is written by the measuring issue, see Dependencies).
  • compiler/src/ir/ranges.trb: the range analysis is a forward, per-function interval analysis whose facts come from constants, the operation that computed a slot, and the comparison a Branch stands on - nothing else.

Split out of #8. Written down by the refine step; the mechanism is not settled by any design record, see the owner decision below.

Why this is not simply "teach the analysis"

Every Machine.load(frame + 8 * a) of the loop adds to frame, a raw address (an Int64 nothing bounds), so the sum can overflow as far as the analysis can tell, whatever a is (head >> 8 & 65535 is bounded, frame is not). A per-function interval analysis cannot bound an address that is loop-carried and set in other functions. So the choice is between a new fact source for the analysis, a new kernel native that takes the base and the offset, or a library-level unchecked/wrapping operation.

Acceptance criteria (once the mechanism is decided)

  • The register, code, pool and header reads of executeLoop for Move, Constant, Word, Zero, Load, Store, the arithmetic and comparison opcodes, Call and Return compute their addresses without a checked add/multiply in the generated C (shown with torb build --emit-c or the equivalent on the touched function, or by a test in compiler/tests/ on the IR).
  • Nothing observable changes: tools/conformance.sh --vm and tier A/B are green, including the overflow panics of program arithmetic (the AddInteger family keeps its checks; only the interpreter's own address arithmetic loses them).
  • fibonacci(30) measured before and after on one machine, best of five, by the method of VM.md section 8 (in the measuring issue, not in the worker's exec).

Seed

Depends on the mechanism: a new Machine native or a new std name is a seed-refresh change (CLAUDE.md "Seed and breaking changes"); an extension of ir/ranges.trb alone is not.

Dependencies

Owner decision (comment below). Measured by #165.

## Goal Take the overflow checks off the address arithmetic of the VM's loop (`compiler/src/vm/interpret.trb`), the first of the open speed steps of VM.md "Speed, the next steps", measured against `fibonacci(30)`. ## Source - [VM.md section 10, "Speed, the next steps"](https://git.torb.dev/torbscript/language/src/commit/d55c8d9c10edbaf9f48665bd782480736efbea03/docs/design/VM.md#L469): "the loop's addresses are computed with checked arithmetic, which the range analysis could prove safe once a frame's size bounds its registers". - [VM.md section 8, "What it costs, measured"](https://git.torb.dev/torbscript/language/src/commit/d55c8d9c10edbaf9f48665bd782480736efbea03/docs/design/VM.md#L378): the table this step adds a row to (the row is written by the measuring issue, see Dependencies). - [`compiler/src/ir/ranges.trb`](https://git.torb.dev/torbscript/language/src/commit/d55c8d9c10edbaf9f48665bd782480736efbea03/compiler/src/ir/ranges.trb): the range analysis is a forward, per-function interval analysis whose facts come from constants, the operation that computed a slot, and the comparison a `Branch` stands on - nothing else. Split out of #8. Written down by the refine step; **the mechanism is not settled by any design record**, see the owner decision below. ## Why this is not simply "teach the analysis" Every `Machine.load(frame + 8 * a)` of the loop adds to `frame`, a raw address (an `Int64` nothing bounds), so the sum can overflow as far as the analysis can tell, whatever `a` is (`head >> 8 & 65535` is bounded, `frame` is not). A per-function interval analysis cannot bound an address that is loop-carried and set in other functions. So the choice is between a new fact source for the analysis, a new kernel native that takes the base and the offset, or a library-level unchecked/wrapping operation. ## Acceptance criteria (once the mechanism is decided) - [ ] The register, code, pool and header reads of `executeLoop` for `Move`, `Constant`, `Word`, `Zero`, `Load`, `Store`, the arithmetic and comparison opcodes, `Call` and `Return` compute their addresses without a checked add/multiply in the generated C (shown with `torb build --emit-c` or the equivalent on the touched function, or by a test in `compiler/tests/` on the IR). - [ ] Nothing observable changes: `tools/conformance.sh --vm` and tier A/B are green, including the overflow panics of program arithmetic (the `AddInteger` family keeps its checks; only the interpreter's own address arithmetic loses them). - [ ] `fibonacci(30)` measured before and after on one machine, best of five, by the method of VM.md section 8 (in the measuring issue, not in the worker's exec). ## Seed Depends on the mechanism: a new `Machine` native or a new std name is a seed-refresh change (CLAUDE.md "Seed and breaking changes"); an extension of `ir/ranges.trb` alone is not. ## Dependencies Owner decision (comment below). Measured by #165.
Author
Collaborator

[loop:torbscript] owner decision: how does the VM loop drop the overflow checks of its own address arithmetic?

What. executeLoop computes every register, code and header address as frame + 8 * offset on Int64; each add and multiply keeps its overflow check in the generated C because frame is an unbounded address. VM.md says the range analysis "could prove safe once a frame's size bounds its registers", but names no mechanism, and the analysis (ir/ranges.trb) is per function with facts only from constants, operations and branch comparisons, so it cannot bound a loop-carried address.

Options.

  1. Kernel natives that take base and offset: Machine.loadAt(base, index) / storeAt(base, index, value), static inline in torb.h like Machine.load, computing base + 8 * index unchecked. Internal to std/machine ("not for programs"). Needs two commits and a seed refresh (new native, CLAUDE.md "Seed and breaking changes"), which the loop may not do (the seed workflow is the merge gate's or Torben's).
  2. Extend the range analysis with an address-like fact (for example a masked offset x & 65535 bounded, plus a trusted-base fact for a slot loaded from the registers). No seed, but it widens the analysis's trust model (a fact it cannot derive), the risk the file's header calls "the one mistake this file may not make".
  3. A wrapping or unchecked integer operation in std (public API): general, but a library and language design choice and a much larger surface than this speed step.

Recommendation. Option 1: smallest, local to the kernel, mirrors how Machine.load and store were added, and keeps the analysis sound. Cost: one seed-refresh cycle (first commit teaches the natives, the seed workflow runs, the second migrates the loop). Say "go option 1/2/3", or "measure first" to hold this until #8 has landed and #165 has put a number on how much of the remaining per-call cost the checks are.

[loop:torbscript] owner decision: how does the VM loop drop the overflow checks of its own address arithmetic? **What.** `executeLoop` computes every register, code and header address as `frame + 8 * offset` on `Int64`; each add and multiply keeps its overflow check in the generated C because `frame` is an unbounded address. VM.md says the range analysis "could prove safe once a frame's size bounds its registers", but names no mechanism, and the analysis (`ir/ranges.trb`) is per function with facts only from constants, operations and branch comparisons, so it cannot bound a loop-carried address. **Options.** 1. **Kernel natives that take base and offset**: `Machine.loadAt(base, index)` / `storeAt(base, index, value)`, `static inline` in `torb.h` like `Machine.load`, computing `base + 8 * index` unchecked. Internal to `std/machine` ("not for programs"). Needs two commits and a seed refresh (new native, CLAUDE.md "Seed and breaking changes"), which the loop may not do (the `seed` workflow is the merge gate's or Torben's). 2. **Extend the range analysis** with an address-like fact (for example a masked offset `x & 65535` bounded, plus a trusted-base fact for a slot loaded from the registers). No seed, but it widens the analysis's trust model (a fact it cannot derive), the risk the file's header calls "the one mistake this file may not make". 3. **A wrapping or unchecked integer operation in `std`** (public API): general, but a library and language design choice and a much larger surface than this speed step. **Recommendation.** Option 1: smallest, local to the kernel, mirrors how `Machine.load` and `store` were added, and keeps the analysis sound. Cost: one seed-refresh cycle (first commit teaches the natives, the `seed` workflow runs, the second migrates the loop). Say "go option 1/2/3", or "measure first" to hold this until #8 has landed and #165 has put a number on how much of the remaining per-call cost the checks are.
Author
Collaborator

Question q1 for torben (decision, blocking): How does the VM loop drop the overflow checks of its own address arithmetic?

v: 1
id: q1
to: torben
kind: decision
blocking: true
question: "How does the VM loop drop the overflow checks of its own address arithmetic (frame + 8 * offset)? Details in the owner decision comment above."
options:
  - id: kernel-natives
    label: Kernel natives loadAt/storeAt
    detail: Internal to std/machine, static inline, unchecked base + 8 * index; needs a seed refresh
  - id: range-analysis
    label: Extend the range analysis
    detail: Address-like and trusted-base facts; no seed, widens the trust model
  - id: std-unchecked
    label: Public wrapping/unchecked integer operation
    detail: General, but a larger library and language design choice
recommendation: kernel-natives
asked_by: orion
**Question q1** for torben (decision, blocking): How does the VM loop drop the overflow checks of its own address arithmetic? ```hos-question v: 1 id: q1 to: torben kind: decision blocking: true question: "How does the VM loop drop the overflow checks of its own address arithmetic (frame + 8 * offset)? Details in the owner decision comment above." options: - id: kernel-natives label: Kernel natives loadAt/storeAt detail: Internal to std/machine, static inline, unchecked base + 8 * index; needs a seed refresh - id: range-analysis label: Extend the range analysis detail: Address-like and trusted-base facts; no seed, widens the trust model - id: std-unchecked label: Public wrapping/unchecked integer operation detail: General, but a larger library and language design choice recommendation: kernel-natives asked_by: orion ```
Author
Collaborator

[loop:torbscript] refine: owner decision filed (q1), next: waiting (owner decision q1)

[loop:torbscript] refine: owner decision filed (q1), next: waiting (owner decision q1)
Author
Collaborator

[loop:torbscript] owner decision recorded (torben via WhatsApp, 2026-10-08 16:35 Europe/Berlin): "Ja" (recommendation accepted).

Resolution: option kernel-natives: internal loadAt/storeAt in std/machine, static inline, unchecked base + 8 * index, seed refresh with the two-commit rule. Labelled ready.

Standing guidance from the same answer: "Das meiste davon konntest du selbst beantworten" (most of this you could have answered yourself). Decide performance steps, seed refreshes that follow an existing plan, naming that follows the conventions, and anything measurable on your own; measure before asking; large refactors are fine when they make sense, size is no argument against an option.

[loop:torbscript] owner decision recorded (torben via WhatsApp, 2026-10-08 16:35 Europe/Berlin): "Ja" (recommendation accepted). Resolution: option `kernel-natives`: internal `loadAt`/`storeAt` in std/machine, static inline, unchecked `base + 8 * index`, seed refresh with the two-commit rule. Labelled `ready`. Standing guidance from the same answer: "Das meiste davon konntest du selbst beantworten" (most of this you could have answered yourself). Decide performance steps, seed refreshes that follow an existing plan, naming that follows the conventions, and anything measurable on your own; measure before asking; large refactors are fine when they make sense, size is no argument against an option.
Author
Collaborator

[question-closed] q=q1 reason=answered-elsewhere

[question-closed] q=q1 reason=answered-elsewhere
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-09T02:29Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-09T02:29Z
Author
Collaborator

[loop:torbscript] release implement: no worker could be started (sessions_spawn: the model account is unavailable, usage limit). Nothing was changed. next: implement (ready)

[loop:torbscript] release implement: no worker could be started (sessions_spawn: the model account is unavailable, usage limit). Nothing was changed. next: implement (ready)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-09T03:04Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-09T03:04Z
Author
Collaborator

[loop:torbscript] implement: PR #221, CI success (run 393, 91c009ff), next: review (waiting on the merge gate argus)

This is part 1 of 2 under the two-commit rule. It adds Machine.loadAt/Machine.storeAt (static inline, wrapping base + 8 * index), the manifest rows, the regenerated natives files, the runtime tests and VM.md. The interpreter does not use the natives yet, because the published seed cannot lower them. After the merge, the seed workflow (or the nightly) has to publish a seed that knows them. Then part 2 migrates compiler/src/vm/interpret.trb and brings the fibonacci(30) numbers, so this issue stays open until then (the PR says Refs, not Closes).

Worker usage: one implement subagent, no further workers.

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/221, CI success (run 393, 91c009ff), next: review (waiting on the merge gate argus) This is part 1 of 2 under the two-commit rule. It adds `Machine.loadAt`/`Machine.storeAt` (static inline, wrapping `base + 8 * index`), the manifest rows, the regenerated natives files, the runtime tests and VM.md. The interpreter does not use the natives yet, because the published seed cannot lower them. After the merge, the `seed` workflow (or the nightly) has to publish a seed that knows them. Then part 2 migrates `compiler/src/vm/interpret.trb` and brings the `fibonacci(30)` numbers, so this issue stays open until then (the PR says `Refs`, not `Closes`). Worker usage: one implement subagent, no further workers.
Author
Collaborator

[loop:torbscript] implement: PR 221 merged by argus (91c009ff), part 1 of 2 landed. Part 2 (interpreter migration) needs a published seed that knows Machine.loadAt/Machine.storeAt; the seed refresh is Torben's, forbidden for the loop. next: waiting (seed refresh publishing the new natives)

[loop:torbscript] implement: PR 221 merged by argus (91c009ff), part 1 of 2 landed. Part 2 (interpreter migration) needs a published seed that knows `Machine.loadAt`/`Machine.storeAt`; the seed refresh is Torben's, forbidden for the loop. next: waiting (seed refresh publishing the new natives)
Author
Collaborator

[post-merge] PR #221 (part 1 of 2) merged by argus as 91c009ff. After-merge step 1 done: dispatched the seed workflow on main (run 439, queued behind CI runs on the busy runner). Not yet verified: the seed is published once run 439 is green. Next: when the published seed knows Machine.loadAt/Machine.storeAt (or the nightly has published it), the loop opens part 2 (interpret.trb onto the natives, fibonacci(30) before/after numbers). #163 stays open until then.

[post-merge] PR #221 (part 1 of 2) merged by argus as 91c009ff. After-merge step 1 done: dispatched the `seed` workflow on main (run 439, queued behind CI runs on the busy runner). Not yet verified: the seed is published once run 439 is green. Next: when the published seed knows `Machine.loadAt`/`Machine.storeAt` (or the nightly has published it), the loop opens part 2 (interpret.trb onto the natives, fibonacci(30) before/after numbers). #163 stays open until then.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T10:45Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T10:45Z
argus closed this issue 2026-10-09 06:13:24 +00:00
Author
Collaborator

[loop:torbscript] close: part 2 landed. PR #250 approved and merged by argus at 2026-10-09T06:13Z as 9889e8f9 (three commits: perf(ir) bound a bitwise and with a nonnegative operand, perf(vm) the loop's words through Machine.loadAt/Machine.storeAt, perf(vm) a constant through a masked pool offset). PR CI green (tier A, tier B, bootstrap, gates). Criterion on generated C: checked operations in executeLoop 440 on main vs 134 with the PR, 121 loadAt / 38 storeAt calls (evidence comment on the PR); the remaining checks are pc steps and the integer overflow panics, as the criteria require. The fibonacci(30) before/after numbers are taken by #165 from a nightly built after this. Main CI on 9889e8f9 is running at the time of this comment. Worker usage: not reported (the worker's run ended with an oversized output after the PR was merged). Claim released.

[loop:torbscript] close: part 2 landed. PR https://git.torb.dev/torbscript/language/pulls/250 approved and merged by argus at 2026-10-09T06:13Z as 9889e8f9 (three commits: `perf(ir)` bound a bitwise and with a nonnegative operand, `perf(vm)` the loop's words through `Machine.loadAt`/`Machine.storeAt`, `perf(vm)` a constant through a masked pool offset). PR CI green (tier A, tier B, bootstrap, gates). Criterion on generated C: checked operations in `executeLoop` 440 on main vs 134 with the PR, 121 `loadAt` / 38 `storeAt` calls (evidence comment on the PR); the remaining checks are `pc` steps and the integer overflow panics, as the criteria require. The fibonacci(30) before/after numbers are taken by #165 from a nightly built after this. Main CI on 9889e8f9 is running at the time of this comment. Worker usage: not reported (the worker's run ended with an oversized output after the PR was merged). Claim released.
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
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#163
No description provided.