Skip to content

fix(verifier)!: find a run inside one of a run at a running index - #254

Merged
siracozmen01 merged 2 commits into
mainfrom
run-in-one-of-a-run
Nov 5, 2026
Merged

siracozmen01 merged 2 commits into
mainfrom
run-in-one-of-a-run

Conversation

@siracozmen01

@siracozmen01 siracozmen01 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A program reading or writing one of a fixed run at an index worked out while running, where the run is inside one of another run at an index written down, inside an element of an array, was refused by the verifier as a fault in the compiler (#253). With struct S { tag: i32; xs: [i32; 4] }, struct Box { ss: [S; 2] }, boxes holding one Box of S(1, [1, 2, 3, 4]) and S(2, [5, 6, 7, 8]), and j worked out while running as 2,

io.print("{boxes[0].ss[1].xs[j]}")
boxes[0].ss[1].xs[j] = 9
io.print("{boxes[0].ss[1].xs[2]}")

was refused under every build and switch with

error[K0411]: `o.main`: `load.at` at 102 reads layout 0 at 24 bytes into what an address is at, which has no such field there

and, without the read, "store.elem.run at 96 reads 4 of layout 0 at 24 bytes into an element that has no such run there". It now prints 7 and then 9. A run of runs with its first index written down is the same case: with struct B { tag: i32; grid: [[i32; 3]; 2] }, reading bs[0].grid[1][j] was refused at 16 bytes.

The compiler writes the right offset. The verifier asks run_holds whether a run of four i32 sits 24 bytes into a Box, and run_holds looked into the fields of a struct but not into one of a fixed run: it found ss, a run of two, and answered no. field_of, which the same reads ask when no index is worked out while running, already looks into one of a run.

Change

  • src/verify.c: run_holds asks the one of a fixed run that the offset falls in, when the offset is inside the run, as field_of does. Each of them is laid out alike, so which one it is does not matter.
  • tools/check-verifier.sh: the program it changes carries outers[k].ins[1].xs[j] read and written, and four new cases change the byte offset of that load.at and store.elem.run to where the verifier has to refuse them with K0411. At 20 bytes in, the second Inner's tag, no run is. At 44, where the xs of a third Inner would be, the run's place in one Inner is a run, and only the outer run's length says it is past the end of the 40 bytes an Outer has.
  • test/regress/runinrun.kest: reads, =, +=, *= in a loop and a bool written, into a run inside the second of two structs in an element, with the struct's other fields and the other element read back; and a run of runs read and written with its first index written down.

A program refused before now runs, so this takes !. Nothing the verifier accepted before changes, and no instruction the compiler writes changes. No version moves.

Verification

  • test/regress/runinrun.kest is refused with K0411 by a kest built from main (d692b17). With this src/verify.c linked into the same build in its place, it answers 0, and compiled to C it answers 0; the program above prints 7 and 9, and the bs[0].grid[1][j] read prints 6.
  • tools/check-verifier.sh on this branch: "27 way(s) a chunk can name what it has not got, move the stack wrong or read a slot as what it does not hold, each refused, and the program as it was compiled held". Against main's library its program does not build: the verifier refuses inside with K0411, as it does the program above.
  • Against a copy of this branch's library with offset < whole->byte_size taken out of run_holds, the check fails with "a run read past the last of the run it is in was held" and "a run written past the last of the run it is in was held": at 44 bytes, 44 % 20 is 4, the xs of an Inner, so only the bound refuses them.
  • Light compile check of src/verify.c with clang-20, clang-format in the CI image, and kest fmt of the new program.
  • Through heavy.sh, on a dev build of this branch (on d692b17) before the verifier cases were added: examples/embed, examples/embed-debug and examples/embed-debug with KEST_WALK_EVERY=1 pass, and ctest --preset dev -j4 passes all 21 tests, regressions among them, which runs the new program under both builds, the heap walked at every allocation, KEST_PLAIN, KEST_NOOPT and compiled to C. The cases change only the check, which was run on its own against the same build, above.
  • The full gate runs in CI.

Closes #253

BREAKING CHANGE: a program reading or writing one of a fixed run at an index worked out while running, where the run is inside one of another run at an index written down, inside an element of an array, was refused with K0411 and now compiles and runs.

The verifier asked whether a run sits at an offset by looking into the
fields of a struct and not into one of a fixed run, so a read or write
of `boxes[k].ss[1].xs[j]` or `boards[k].grid[1][j]` inside an element
was refused with K0411 as a fault in the compiler. It now looks into
the one of the run the offset falls in, as `field_of` already does.

Closes #253

BREAKING CHANGE: a program reading or writing one of a fixed run at an index worked out while running, where the run is inside one of another run at an index written down, inside an element of an array, was refused with K0411 and now compiles and runs.

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review at 2fd786d: changes needed (1). Read, not run.

  1. The "past the last" refusal case cannot fail on the bound it is meant to guard.
    • In the check's program, Inner is tag: i32 plus xs: [i32; 4]: 20 bytes, with xs at 4. Outer is 40 bytes.
    • The case sets the load.at offset to 40. With offset < whole->byte_size removed from the new branch, 40 % 20 = 0 lands on the second Inner's tag. The struct walk finds no run there and refuses anyway, so the case passes with or without the bound.
    • The offset that tests the bound is 44. With the bound, 44 < 40 fails and it is refused. Without it, 44 % 20 = 4 lands on xs, the base case accepts a run of four i32 4 bytes into a third Inner, and the read runs 4 to 20 bytes past the 40-byte element.
    • So the bound is what keeps the widened walk inside the element, and 44 (for load.at, and the same for store.elem.run if you want both) is the case that pins it.

The widened walk itself is right. Read against field_of and the existing branches:

  • No straddling. The new branch hands offset % element->byte_size to the element's own type. Every way run_holds answers true from there ends at the base case: a fixed run of exactly count elements of step bytes, with offset < step, found as a member or element inside that one element. So the run it accepts lies wholly inside one element of the outer run, and offset < whole->byte_size keeps that element inside the whole.
  • No padding. The base case still asks field_of(element, offset, field), and field_of's struct walk takes a member only when offset lies within [byte_offset, byte_offset + size). An offset in padding between members, or at the end of an element, matches no member and is refused.
  • No new over-acceptance from the existing struct branch, which has no upper bound on offset. It recurses with offset - byte_offset into a member whose own branches are bounded (offset < step, and now offset < byte_size).
  • The 20-byte cases (the tag, for both load.at and store.elem.run) do test the walk: 20 % 20 = 0 lands on tag, so the widened walk refuses only because nothing there is a run.

Not verified: whether the verifier docs list K0411's cases in a way that now needs the nested run named. The message text is unchanged, and I cannot search the tree. A search for K0411 in docs/ would settle it.

The ! and footer are as the body explains. The new regress file is confirmed as new.

@siracozmen01 siracozmen01 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Delta 2fd786d..b2c2272: ready. The finding is fixed. Read, not run.

  • The past-the-last cases are at 44 now, for both load.at and store.elem.run. 44 % 20 = 4 lands on xs, so only offset < whole->byte_size refuses them. The session reports that removing the bound in a copy fails exactly these two cases, so the bound is pinned for reads and writes. The two tag cases at 20 still test that the widened walk finds no run on tag.
  • The comment over the cases names both points. src/verify.c and the regress file are unchanged since 2fd786d.
  • docs/internals/verifier.md lists the passes and no K0411 cases, so the docs owe nothing here.

@siracozmen01
siracozmen01 merged commit b95fbf3 into main Nov 5, 2026
25 checks passed
@siracozmen01
siracozmen01 deleted the run-in-one-of-a-run branch November 5, 2026 11:54
@rawframe-release rawframe-release Bot mentioned this pull request Nov 5, 2026
siracozmen01 added a commit that referenced this pull request Nov 5, 2026
…256)

## Problem

A program writing one of a run of runs at an index worked out while running was refused as a fault in the compiler, in a local and inside an element of an array (#249), and so was a program writing one of a run in a local at such an index with an operator or into a field of one (#251). With

```kest
let grid: [[i32; 3]; 2] = [[1, 2, 3], [4, 5, 6]]
let xs: [i32; 3] = [1, 2, 3]
let ps: [P; 2] = [P(1, 2), P(3, 4)]
let i = 1
let j = i + 1
grid[i][j] = 9
xs[i] += 5
ps[i].y = 9
io.print("{grid[1][2]} {xs[1]} {ps[1].y}")
```

(`struct P { x: i32; y: i32 }`), each of the three writes was refused under every build and switch with

```text
error[K0505]: this assigns to something that is not a place, which the checker allowed
```

and the program now prints `9 7 9`. `grid[i][2] = 9`, `-=`, `*=` and `/=`, and `boards[k].grid[i][j] = 9` inside an element were refused the same way. A run in a local could be written at a running index only with `=` and only whole: `store.slots` wrote as many slots as one of the run is wide, the compiler had no way to keep the index for a read and then a write, and a run of runs at a running first index was no place at all. Inside an element, the change for #245 held one run's index apart, not two.

## Change

- `src/compile.c`: `run_way` finds the place a write reaches through runs at indexes worked out while running: what holds the outermost run, a local's slots or an element's bytes, and how far into one of the innermost run the place is, through fields and indexes written down after the last running index. A run of runs is reached as one run of what the innermost holds, at one index worked out from all of them, outermost first, in the order they were written. Where more than one index is worked out while running, each is held to its own count first, so a refusal names the index that was outside and its own count. In a local, `=`, `+=`, `-=`, `*=` and `/=` all go through it, and a read before a write keeps the index in a slot of its own, since each takes it. A place of any other shape compiles exactly as before.
- `src/value.h`, `src/value.c`, `src/vm.c`, `src/verify.c`, `src/lower.c`, `src/emitc.c`: `load.slots` and `store.slots` take how many slots of one of the run they read or write, which is fewer than the stride for a field of one; the verifier holds them to the slots they can reach with it. One instruction, `in.run`, holds the index at the top to a count and leaves it, refusing with the `K0604` sentence a run gets ("index 3 is outside 3 of them"), in the machine and in a body compiled to C. It reaches neither the heap nor the host.
- `src/ir.h`, `src/ir.c`: the operation `in.run` beside them, and a `RUN` place says how many slots of one it reaches.
- `src/verify.c`: `all_a_run` answers whether all of a fixed run is a run of `count` things `step` bytes wide, read through runs of runs as one run of what the innermost holds, which is how `store.elem.run` reaches one inside an element; `run_holds` asks it first. Each of the outer run's has to be all of a run of the inner. A run found inside one of them, which `run_holds` finds since #254, is not enough: eight `i32` from the `xs` of an `Inner` would step on into the next one's `tag`.
- `test/regress/runofruns.kest`: in a local, `=` and every operator into a run of runs at two running indexes and at a running first index, `+=` and `-=` into a run, a field of one written and added to, a whole struct written, a run of runs of `bool`, of structs and three deep inside a struct, with an index written down in the middle, and a run of `vec2` added to. Inside an element, the same shapes, writes in a loop, and a write whose right-hand side pushes 64 elements onto the array being written.
- `examples/rows.kest`: check 16 writes a run of runs of cells at two indexes worked out while running, which is what has the gate's `declarations` step see `in.run` run. What it prints is the same.
- `tools/check-commands.sh`: `K0604` for the first index past its run and the second past its own, in a local; the second past its own with `+=` inside an element; `+=` into a run past its end; and a field written past the end of a run.
- `tools/check-verifier.sh`: the program it changes carries `manys[k].grid[i][j] = 9`, and one more case moves that `store.elem.run` to 36 bytes into a `Many`, the `xs` of the first `Inner` of `nest: [[Inner; 3]; 2]`, where the verifier has to refuse it with `K0411`. `first_with` can be asked for the first instruction in one function, since the first `store.elem.run` in the module is `inside`'s.
- `tools/check-c.sh`: the second index past its own run, in a local and inside an element, in a body compiled to C, has to be refused with the machine's own `K0604` line, byte for byte.
- `docs/internals/compiler.md`: 226 instructions, and how a run of runs is reached.

A program refused before now runs, so this takes `!`. A program that compiled before compiles to the same instructions, with `load.slots` and `store.slots` each two bytes wider for the new operand. No version moves: neither the ABI, the JSON a command writes, nor what a deterministic program that ran before observes changes.

A run inside one of a run written at two running indexes, with a field between them (`ss[i].xs[j] = 9`), is still refused; that is #252. A run inside one of a run at an index written down, inside an element (`bs[k].grid[1][j]`), was #253, fixed by #254, which this is built on.

## Verification

- The example above, `test/regress/runofruns.kest`, and the new `K0604` programs are refused with `K0505` by a `kest` built from main (d692b17). On this branch the example prints `9 7 9`, and the program answers 0 under `kest` and `kest-debug`, each with nothing, `KEST_PLAIN=1`, `KEST_NOOPT=1` and `KEST_WALK_EVERY=1`, and compiled to C. `kest emit` writes it with 54 `in.run`.
- In the example, `kest emit` writes `grid[i][j] = 9` as `in.run 2`, `mul.i` by 3, `in.run 3`, `add.i` and `store.slots +0  1 of 6`; `xs[i] += 5` as `store.k`, `load2`, `load.slots +6  1 of 3`, `add.i.narrow.c` and `store.slots +6  1 of 3`; and `ps[i].y = 9` as `store.slots +10  2 of 2  1 of each`.
- The new tests against two changed copies. With `in.run` in `src/vm.c` doing nothing, the three run-of-runs `K0604` rows fail: the first index past its run is said as "index 6 is outside 6 of them", and the second index past its own writes into the next row and answers. With the `kest_run_at` line taken out of `in.run` in `src/emitc.c`, the C check fails both ways: the body compiled to C says nothing and answers. The rows for `xs[i] +=` and `ps[i].y =` past the end are `load.slots` and `store.slots` refusing, and pass under both.
- `tools/check-verifier.sh` says "28 way(s) a chunk can name what it has not got, move the stack wrong or read a slot as what it does not hold, each refused, and the program as it was compiled held". Against a copy of this branch's library whose `run_holds` asks the whole of itself about the inner run, as this change first did, it fails with "a run of runs written where a run of them holds a shorter run was held".
- Light compile checks of every touched file with `clang-20`, `clang-format` in the CI image, and `kest fmt` of both Kest files.
- Through `heavy.sh`, on a `dev` build of this branch (on b95fbf3): `examples/embed`, `examples/embed-debug` and `examples/embed-debug` with `KEST_WALK_EVERY=1` pass, `ctest --preset dev -j4` passes all 21 tests and `tools/check-costs.sh` passes. `tools/check-c.sh` over every Kest file of `examples`, `lib` and `bench` passes, saying "a run of runs written past its second run, in a local and in an element, refused in C with the machine's line".
- The full gate runs in CI.

Closes #249
Closes #251

BREAKING CHANGE: a program writing one of a run of runs at an index worked out while running, in a local or inside an element of an array, or writing one of a run in a local at such an index with an operator or into a field of one, was refused with K0505 and now compiles and runs.
siracozmen01 pushed a commit that referenced this pull request Nov 5, 2026
🤖 I have created a release *beep* *boop*
---


## 0.7.0 (2026-11-05)

### Changed

- **Breaking:** A program reads and writes one of a fixed run at an index worked out while running where the run is inside one of another run at an index written down, inside an element of an array (`boxes[0].ss[1].xs[j]`). The verifier refused it under every build and switch as a fault in the compiler, with `K0411` ("`load.at` at 102 reads layout 0 at 24 bytes into what an address is at, which has no such field there"): with `struct S { tag: i32; xs: [i32; 4] }`, `struct Box { ss: [S; 2] }`, `boxes` holding one `Box` of `S(1, [1, 2, 3, 4])` and `S(2, [5, 6, 7, 8])` and `j` worked out while running as 2, a program printing `boxes[0].ss[1].xs[j]`, writing 9 there and printing `boxes[0].ss[1].xs[2]` now prints `7` and `9`. A run of runs with its first index written down (`bs[0].grid[1][j]`) is the same case. Nothing the verifier accepted before changes, and no instruction the compiler writes changes. ([#254](#254))
- **Breaking:** A program writes one of a run of runs at indexes worked out while running, in a local and inside an element of an array, and writes one of a run in a local at such an index with an operator or into a field of one. Each was refused under every build and switch as a fault in the compiler, with `K0505` ("this assigns to something that is not a place, which the checker allowed"): with `struct P { x: i32; y: i32 }`, `grid: [[i32; 3]; 2]` holding `[[1, 2, 3], [4, 5, 6]]`, `xs: [i32; 3]` holding `[1, 2, 3]`, `ps: [P; 2]` holding `[P(1, 2), P(3, 4)]`, and `i` and `j` worked out while running as 1 and 2, `grid[i][j] = 9`, `xs[i] += 5` and `ps[i].y = 9` were each refused, and printing `{grid[1][2]} {xs[1]} {ps[1].y}` after them now prints `9 7 9`. Where more than one index is worked out while running, each is held to its own count, so an index past its run is refused with `K0604` naming that index and that run's count ("index 3 is outside 3 of them"), in every engine and in a body compiled to C. `kest emit` writes one new instruction, `in.run`, and `load.slots` and `store.slots` carry how many slots of one of the run they read or write. A program that compiled before compiles to the same instructions, with `load.slots` and `store.slots` each two bytes wider. ([#256](#256))
- **Breaking:** A program writes one of a run that is a field of one of another run, each at an index worked out while running (`ss[i].xs[j] = 9`), in a local and inside an element of an array, with `=` or an operator. Each was refused under every build and switch as a fault in the compiler, with `K0505` ("this assigns to something that is not a place, which the checker allowed"): with `struct S { tag: i32; xs: [i32; 4] }`, `ss: [S; 2]` holding `S(1, [1, 2, 3, 4])` and `S(2, [5, 6, 7, 8])`, and `i` and `j` worked out while running as 1 and 2, `ss[i].xs[j] = 9` followed by printing `ss[1].xs[2]` now prints `9`. An index past its run is refused with `K0604` and that run's count. Inside an element, a write whose right-hand side writes the same element, or grows the array, keeps what the right-hand side wrote. A program that compiled before compiles to the same instructions. ([#257](#257))
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.

A run inside one of a run in an element is refused by the verifier at a running index

1 participant