Repository navigation
fix(compiler)!: write runs of runs and local runs at running indexes - #256
Merged
Merged
Conversation
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
commented
Nov 5, 2026
siracozmen01
left a comment
Contributor
Author
There was a problem hiding this comment.
Review at ae63f28: ready. No findings. Read, not run. I read verify.c, vm.c, emitc.c, value.c/h, ir and lower in full in the diff. compile.c's 235 lines I read for the places it builds, not every line.
all_a_run, against every caller:
- It is reached only through
run_holds(verify.c:1381), whose callers areelement_run(1415) and the in-place read at 1920.run_holdsitself recurses through struct members (1388) and, since #254, through one element of a fixed run (1398). So every path that accepts a run now ends inall_a_run. all_a_runaccepts only a fixed run that is all of the run: exactlycountelements ofstepbytes with the field atoffset < step. Or it accepts a fixed run ofc0elements whose element is itself all of a run ofcount / c0.- Because it recurses only into a FIXED element, never into a struct, and requires
count % c0 == 0at each level, the flattened run covers the whole outer run with no gap. - A fixed run's elements are laid out back to back at
element->byte_size, which the base case pins tocount' × step.
- Because it recurses only into a FIXED element, never into a struct, and requires
- The combined case is refused on every route.
Many.nest([Inner; 2], Inner{tag, xs: [i32; 4]}) read as 8 i32: 8 % 2 = 0 recurses intoInner, a struct, so false.- The #254 branch goes into one
Innerand thenxs:all_a_run([i32; 4], 4, 8)recurses intoi32, not FIXED, so false. - So nothing steps from an
xsinto the nexttag. The offset-36 case pins it.
constant_run_holdsis the constants' own check and is not touched.
VM and C backend, same addresses:
load.slotsandstore.slotsnow carrywidthbesidestride. The VM copieswidthslots atbase + index × stride(vm.c:5193-5233). The C backend writesf[slot + k + which × stride]fork < slots(emitc.c:1644-1699). Those are the same slots.- The bounds agree everywhere:
base + (count - 1) × stride + widthin the VM'sown_slots, the verifier's reach (verify.c:724) and emitc'smark_frame(467-471). Each is guarded bycount > 0or comes afterIN_RUN, socount - 1cannot underflow. in.runholds one index to its own count on both sides: the VM'sIN_RUN(top[-1].integer, count), and C'skest_run_at(rt, x.integer, count). It leaves the value in place, and the verifier only asks for a number on top. So each index of a run of runs is checked against its own bound before the combined index is checked against the whole.- The verifier's slot kinds for both ops iterate
k < widthacross rows atstride, matching the copies.
Merged
siracozmen01
added a commit
that referenced
this pull request
Nov 5, 2026
#257) ## Problem A program writing one of a run that is a field of one of another run, each 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 (#252). With ```kest module m import std.io struct S { tag: i32 xs: [i32; 4] } fn main() -> i32 { let ss: [S; 2] = [S(1, [1, 2, 3, 4]), S(2, [5, 6, 7, 8])] let i = 1 let j = 2 ss[i].xs[j] = 9 io.print("{ss[1].xs[2]}") return 0 } ``` `kest run` refused it under every build and switch with ```text error[K0505]: this assigns to something that is not a place, which the checker allowed ``` and it now prints `9`. `boxes[k].ss[i].xs[j] = 9`, with `ss` a field of an element of `boxes: [Box]`, was refused the same way, and so was every operator into either. A write at more than one running index reaches the runs as one run of what the innermost holds, which needs each run to be the whole of one of the run around it; with a field between them, one of `ss` is a `tag` and then the run, so there was no single run to write into. Reading `ss[i].xs[j]` already ran: it copies `xs` out of `ss[i]` and indexes the copy. ## Change - `src/compile.c`: `run_ways` finds the ways a write goes through to its target, innermost first, out to one whose runs are in a local's slots or an element's bytes; each way is what `run_way` finds since #256. `assign_through_runs` writes through more than one: the indexes are worked out in the order they were written and kept, the run each way past the innermost reaches is copied out of the copy of the next way out (or of the local or the element), the target is written in the innermost copy, and the copies are written back, innermost first. In a local the copies are taken once, before the value is worked out, since a call reaches nothing in the caller's frame. Inside an element they are taken again after it, because working it out may write the element or grow the array. The operator and the value of an assignment are worked out by one function, `assigned_value`, for both this and the ways that were there. A place of any other shape compiles exactly as before. No instruction is added: the copies are `load.slots`, `store.slots`, `load.elem.run` and `store.elem.run`, each held to its own run's count, so an index outside its run is refused with that run's `K0604` sentence. - `test/regress/runinfield.kest`: in a local, `=` and every operator into `ss[i].xs[j]`, a field of one of a run of structs, a run of runs inside, a run of `vec2` added to, three runs deep with a field between each (`rows[i].s[i - 1].xs[j]`), and writes in a loop. Inside an element, the same shapes, a write whose value writes the tag and another of the same run of the element being written, the same with `-=`, and `+=` with a value that pushes 64 elements onto the array being written. - `tools/check-commands.sh`: `K0604` for the outer index past its run and the inner past its own, in a local, and the same inside an element, the second with `+=`. - `docs/internals/compiler.md`: how such a write goes. A program refused before now runs, so this takes `!`. A program that compiled before compiles to the same instructions. No version moves: neither the ABI, the JSON a command writes, nor what a deterministic program that ran before observes changes. ## Verification - The example above, `test/regress/runinfield.kest` and the four new `K0604` programs are refused with `K0505` by a `kest` built from main (cd939e9). On this branch the example prints `9`, the program answers 0 under `kest` and `kest-debug`, each with nothing, `KEST_PLAIN=1`, `KEST_NOOPT=1` and `KEST_WALK_EVERY=1`, and built as C with `kest build --release`, and each `K0604` program is refused with the line its row asks for. - In the example, `kest emit` writes `ss[i].xs[j] = 9` as `load 10` and `load.slots +1 5 of 2 4 of each` (`xs` of `ss[i]`, copied out), `store.n 12 4`, `load2 11 16` and `store.slots +12 1 of 4` (the copy written at `j`), then `load 10`, `load.n 12 4` and `store.slots +1 5 of 2 4 of each` (written back). - The new program against three changed copies of `src/compile.c`, each linked into its own `kest`: with the copy inside an element taken before the value for `=`, it answers 6 (the value's own write to the element is undone); with no second copy after the value of an operator inside an element, it answers 8; with the copies written back outermost first, it answers 3 (the three-deep write is lost). - Light compile checks of `src/compile.c` with `clang-20`, `clang-format` in the CI image, and `kest fmt` of the new program. - `kest emit` of every other Kest file in the tree (170 of them) writes the same bytes as a `kest` built from main. - Through `heavy.sh`, on a `dev` build of this branch (on cd939e9, whose tree is the one #256 was checked at): `examples/embed`, `examples/embed-debug` and `examples/embed-debug` with `KEST_WALK_EVERY=1` pass, `ctest --preset dev -j4` passes all 21 tests (`regressions` runs the new program under both builds, the heap walked at every allocation, `KEST_PLAIN`, `KEST_NOOPT` and compiled to C; `commands` has the new `K0604` rows) and `tools/check-costs.sh` passes. `tools/check-c.sh` over every Kest file of `examples`, `lib` and `bench` passes. - The full gate runs in CI. Closes #252 BREAKING CHANGE: a program writing one of a run inside one of another run, each at an index worked out while running, with a field or an index written down between them, 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 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
(
struct P { x: i32; y: i32 }), each of the three writes was refused under every build and switch withand the program now prints
9 7 9.grid[i][2] = 9,-=,*=and/=, andboards[k].grid[i][j] = 9inside 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.slotswrote 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_wayfinds 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.slotsandstore.slotstake 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 theK0604sentence 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 operationin.runbeside them, and aRUNplace says how many slots of one it reaches.src/verify.c:all_a_runanswers whether all of a fixed run is a run ofcountthingsstepbytes wide, read through runs of runs as one run of what the innermost holds, which is howstore.elem.runreaches one inside an element;run_holdsasks 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, whichrun_holdsfinds since fix(verifier)!: find a run inside one of a run at a running index #254, is not enough: eighti32from thexsof anInnerwould step on into the next one'stag.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 ofbool, of structs and three deep inside a struct, with an index written down in the middle, and a run ofvec2added 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'sdeclarationsstep seein.runrun. What it prints is the same.tools/check-commands.sh:K0604for 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 carriesmanys[k].grid[i][j] = 9, and one more case moves thatstore.elem.runto 36 bytes into aMany, thexsof the firstInnerofnest: [[Inner; 3]; 2], where the verifier has to refuse it withK0411.first_withcan be asked for the first instruction in one function, since the firststore.elem.runin the module isinside'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 ownK0604line, 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, withload.slotsandstore.slotseach 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
test/regress/runofruns.kest, and the newK0604programs are refused withK0505by akestbuilt from main (d692b17). On this branch the example prints9 7 9, and the program answers 0 underkestandkest-debug, each with nothing,KEST_PLAIN=1,KEST_NOOPT=1andKEST_WALK_EVERY=1, and compiled to C.kest emitwrites it with 54in.run.kest emitwritesgrid[i][j] = 9asin.run 2,mul.iby 3,in.run 3,add.iandstore.slots +0 1 of 6;xs[i] += 5asstore.k,load2,load.slots +6 1 of 3,add.i.narrow.candstore.slots +6 1 of 3; andps[i].y = 9asstore.slots +10 2 of 2 1 of each.in.runinsrc/vm.cdoing nothing, the three run-of-runsK0604rows 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 thekest_run_atline taken out ofin.runinsrc/emitc.c, the C check fails both ways: the body compiled to C says nothing and answers. The rows forxs[i] +=andps[i].y =past the end areload.slotsandstore.slotsrefusing, and pass under both.tools/check-verifier.shsays "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 whoserun_holdsasks 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".clang-20,clang-formatin the CI image, andkest fmtof both Kest files.heavy.sh, on adevbuild of this branch (on b95fbf3):examples/embed,examples/embed-debugandexamples/embed-debugwithKEST_WALK_EVERY=1pass,ctest --preset dev -j4passes all 21 tests andtools/check-costs.shpasses.tools/check-c.shover every Kest file ofexamples,libandbenchpasses, 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".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.