Repository navigation
fix(verifier)!: find a run inside one of a run at a running index - #254
Merged
Merged
Conversation
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
commented
Nov 5, 2026
siracozmen01
left a comment
Contributor
Author
There was a problem hiding this comment.
Review at 2fd786d: changes needed (1). Read, not run.
- The "past the last" refusal case cannot fail on the bound it is meant to guard.
- In the check's program,
Inneristag: i32plusxs: [i32; 4]: 20 bytes, withxsat 4.Outeris 40 bytes. - The case sets the
load.atoffset to 40. Withoffset < whole->byte_sizeremoved from the new branch, 40 % 20 = 0 lands on the secondInner'stag. 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 thirdInner, 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 forstore.elem.runif you want both) is the case that pins it.
- In the check's program,
The widened walk itself is right. Read against field_of and the existing branches:
- No straddling. The new branch hands
offset % element->byte_sizeto the element's own type. Every wayrun_holdsanswers true from there ends at the base case: a fixed run of exactlycountelements ofstepbytes, withoffset < 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, andoffset < whole->byte_sizekeeps that element inside the whole. - No padding. The base case still asks
field_of(element, offset, field), andfield_of's struct walk takes a member only whenoffsetlies 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 withoffset - byte_offsetinto a member whose own branches are bounded (offset < step, and nowoffset < byte_size). - The 20-byte cases (the tag, for both
load.atandstore.elem.run) do test the walk: 20 % 20 = 0 lands ontag, 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
commented
Nov 5, 2026
siracozmen01
left a comment
Contributor
Author
There was a problem hiding this comment.
Delta 2fd786d..b2c2272: ready. The finding is fixed. Read, not run.
- The past-the-last cases are at 44 now, for both
load.atandstore.elem.run. 44 % 20 = 4 lands onxs, so onlyoffset < whole->byte_sizerefuses 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 ontag. - 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.
Merged
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))
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.
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] },boxesholding oneBoxofS(1, [1, 2, 3, 4])andS(2, [5, 6, 7, 8]), andjworked out while running as 2,was refused under every build and switch with
and, without the read, "
store.elem.runat 96 reads 4 of layout 0 at 24 bytes into an element that has no such run there". It now prints7and then9. A run of runs with its first index written down is the same case: withstruct B { tag: i32; grid: [[i32; 3]; 2] }, readingbs[0].grid[1][j]was refused at 16 bytes.The compiler writes the right offset. The verifier asks
run_holdswhether a run of fouri32sits 24 bytes into aBox, andrun_holdslooked into the fields of a struct but not into one of a fixed run: it foundss, 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_holdsasks the one of a fixed run that the offset falls in, when the offset is inside the run, asfield_ofdoes. Each of them is laid out alike, so which one it is does not matter.tools/check-verifier.sh: the program it changes carriesouters[k].ins[1].xs[j]read and written, and four new cases change the byte offset of thatload.atandstore.elem.runto where the verifier has to refuse them withK0411. At 20 bytes in, the secondInner'stag, no run is. At 44, where thexsof a thirdInnerwould be, the run's place in oneInneris a run, and only the outer run's length says it is past the end of the 40 bytes anOuterhas.test/regress/runinrun.kest: reads,=,+=,*=in a loop and aboolwritten, 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.kestis refused withK0411by akestbuilt from main (d692b17). With thissrc/verify.clinked into the same build in its place, it answers 0, and compiled to C it answers 0; the program above prints7and9, and thebs[0].grid[1][j]read prints6.tools/check-verifier.shon 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 refusesinsidewithK0411, as it does the program above.offset < whole->byte_sizetaken out ofrun_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, thexsof anInner, so only the bound refuses them.src/verify.cwithclang-20,clang-formatin the CI image, andkest fmtof the new program.heavy.sh, on adevbuild of this branch (on d692b17) before the verifier cases were added:examples/embed,examples/embed-debugandexamples/embed-debugwithKEST_WALK_EVERY=1pass, andctest --preset dev -j4passes all 21 tests,regressionsamong them, which runs the new program under both builds, the heap walked at every allocation,KEST_PLAIN,KEST_NOOPTand compiled to C. The cases change only the check, which was run on its own against the same build, above.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.