Repository navigation
fix(compiler)!: write a run inside an element at a running index - #250
Conversation
siracozmen01
left a comment
There was a problem hiding this comment.
Review of 88738d8: changes needed (1). Read, not run. Checks were still pending apart from docs, gate and title, which pass.
1. The run-index refusal is tested on one of its four paths.
The new check-commands.sh row writes rows[0].count[i] = 9 with i 3 and runs it with kest run, which is the machine's store.elem.run. Three paths have no test that fails if their check goes:
- the machine's
load.elem.run(IN_RUN, vm.c), which a+=reaches first; - both instructions compiled to C (the
kest_run_atblock inwrite_elem).
runwrite.kest stays in range throughout. A mutant that drops IN_RUN from KEST_OP_LOAD_ELEM_RUN, or the place->count > 0 block from write_elem, passes everything here, and the load then reads past the run: past the element for a run at its end. One more row, such as rows[0].count[i] += 9, covers the machine's load. A refusal of the same program compiled to C, wherever the suite runs K0604 cases through the C backend, covers kest_run_at.
What checks out
- The same three indexes in the same order, with the same sentence:
- The machine checks
HOLD(the handle), thenIN_ARRAY(the element), thenIN_RUN(the run).IN_RUNsays "index %lld is outside %u of them" under K0604. write_elemdoes the handle and element test inline, falls back tokest_elem_at, which is the machine's own answer, and then callskest_run_at, which says the same K0604 sentence (vm.c:9761-9772).- The run check is emitted on all three element paths (guarded, in bounds and plain), so a walk that proves the element in bounds still checks the run index.
- The machine checks
- The address is taken after the right-hand side. The assignment pushes the handle, the element's index and the run's index, then computes the value, and only
store.elem.run(vm.c) or the C block at the PUT formsbytes + index * stride + offset + within * stride. For+=,load.elem.runleaves the place in place, and the store forms the address again.runwrite.kest's last write, whose right-hand side pushes 64 rows, exercises this. - Stack effects:
kest_op_stackgivesload.elem.run3 in and 3 plus the layout out, andstore.elem.run3 plus the layout in and nothing out.- verify.c checks a number at the top and the next, an array third,
element_run(run_holdson the element's layout, with the offset pointing at the field in the first of the run, which is whatin_the_runwrites: the run's start plusinside), andkept_infor the store. Then it dropsslots + 3.
- Lowering:
read_placeandwrite_placereturn forcount > 0before any fused path. A consuming read andtake_addressof such a place fault rather than lower toload.elemorelem.addr. - Docs: compiler.md's 225 instructions is the table plus two, and the new sentence describes the element place.
!with the footer is right at 0.x, since a refused program now runs. The body's code block lines (rows[0]...,io.print(...)) are not commit headers, and the title check passes.
siracozmen01
left a comment
There was a problem hiding this comment.
Delta review at 8e588a1: ready. CI was pending when I read it (title passes), so this assumes it goes green. Read, not run.
The delta touches only tests. Comparing the trees of 88738d8 and 8e588a1, the only change at the root is tools/. Within it, only tools/check-commands.sh and tools/check-c.sh change. src/ and test/ are the same trees.
My point had two parts, and the new rows cover both:
- The VM's read check (
IN_RUNinload.elem.run): the/=row divides byz = 0. With the read check, the read past the run is refused first withK0604 index 3 is outside 3 of them. Without it, the read goes on and the division is refused withK0601, so the row fails. The+=row cannot tell the two apart, since the write's check says the same sentence, and the body says so. It is there for the compound path, which is fine. - The C backend's check (
kest_run_at):check-c.shcompiles= 9and/= noughtwithat = len(rows) + 2andnought = len(rows) - 1, values found at run time, so the C compiler cannot fold them. Each must print anerrorline containingK0604andindex 3 is outside 3 of them, and that line must equalkest run's byte for byte. A missing write check makes=print nothing. A missing check anywhere before the division makes/=printK0601. Either way the C check fails, which matches the two mutants in the body. - A build or compile failure counts as wrong too, so a C backend that cannot compile the program does not pass silently.
$runpast_saidis added to the summary line.
The body's Verification describes the three rows, the C check and both mutants as the scripts and the reported runs show them. No line in the body starts at column 0 with a name and (.
🤖 I have created a release *beep* *boop*
---
## 0.6.0 (2026-11-04)
### Changed
- **Breaking:** A program reads one of a fixed run inside an element of an array at an index worked out while running, wherever the run sits in the element. When the run was not the element's first field, the program was refused under every build and switch as a fault in the compiler, with `K0505` ("this leaves the stack 1 deep and a statement leaves it as it found it"): with `struct Row { tag: i32; count: [i32; 3] }`, a function returning `rows[k].count[at]` did not compile, and now, with `rows` holding `Row(7, [10, 20, 30])`, it answers 30 for `k` 0 and `at` 2. The same held for a run of `bool` and for a run inside a struct inside the element. A program that compiled before compiles to the same instructions. ([#246](#246))
- **Breaking:** A program writes one of a fixed run inside an element of an array at an index worked out while running, with `=`, `+=`, `-=`, `*=` or `/=`. Every such write was refused as a fault in the compiler, with `K0505` ("this assigns to something that is not a place, which the checker allowed"), whichever field the run was; only a run held in a local could be written at such an index. With `struct Row { count: [i32; 3] }`, `rows` holding `Row([10, 20, 30])` and `at` worked out while running as 1, `rows[0].count[at] = 99` followed by `rows[0].count[at - 1] += 5` now leaves the run `15 99 30`. An index past the run is refused with `K0604` ("index 3 is outside 3 of them"), the sentence a run in a local gets, in every engine and in a body compiled to C. A write whose right-hand side grows the array is written into the array as it is after it grew. `kest emit` writes these as two new instructions, `load.elem.run` and `store.elem.run`. A program that compiled before compiles to the same instructions. A run of runs written at two indexes worked out while running (`grid[i][j] = 9`) is still refused, in a local as well as in an element ([#249](#249)). ([#250](#250))
- A `bool`, or a set of flags, read through an address worked out before the read (`load.at`) is looked at only when the address is in memory a host lent, as a read that knows its array already was. Every such read was looked at, of the machine's own memory too. Nothing a program or a host sees changes: every read refused before is refused with the same line. A program reading `rows[k].cells[i]` 800,000 times from a `[Row]` of 1000 `struct Row { cells: [bool; 4] }` it filled itself runs in 131,499,448 instructions where it took 162,700,128, under `perf stat -e instructions:u` with the program and commands the pull request gives. ([#247](#247))
Problem
A program writing one of a fixed run inside an element of an array, at an index worked out while running, was refused as a fault in the compiler, whichever field the run was (#245). With
struct Row { count: [i32; 3] },rowsholdingRow([10, 20, 30])andatworked out while running as 1,was refused under every build and switch with
once for each write, and now prints
15 99 30. Only a run held in a local could be written at such an index. A write into an element is held apart as the array and its index until the moment it is written, so that what the right-hand side does to the array (growing it most of all) cannot leave the write an address into a block that moved;store.elemandload.elemcarry the run's place as a byte offset fixed in the instruction, so there was no way to hold the run's index apart too.Change
src/value.h,src/value.c: two instructions,load.elem.runandstore.elem.run, eachload.elemandstore.elemwith the run's index as the third thing under the value and the run's stride and count beside the offset and layout. Neither reaches the heap.src/vm.c: both ask what the handle is and whether the element's index is in the array, asload.elemandstore.elemdo, and whether the run's index is one of the run, with the sameK0604sentence a run in a local gets ("index 3 is outside 3 of them"). The address is worked out when the instruction runs.src/verify.c:element_runholds the offset, layout, stride and count to a run the element's layout has there (run_holds, whichload.atalready uses).src/compile.c:run_in_elementfinds such a run under the place written, with fields of one of the run after it (rows[k].cells[at].hp = 44), and the assignment holds the run's index apart above the element's.=,+=,-=,*=and/=all go through it. A place not of that shape compiles exactly as before.src/lower.c,src/ir.h: an element place with a count is lowered to the two instructions, and never fused.src/emitc.c: a body compiled to C askskest_run_atabout the run's index, which is the machine's own sentence, and steps into the run after the element is found.test/regress/runwrite.kest:=,+=and-=into runs ofi32andboolafter the first field and as the first field, a field of one of a run of structs and a whole struct written into one, a run inside a struct inside the element, writes in a loop, and a write whose right-hand side pushes 64 rows onto the array being written.examples/rows.kest: checks 14 and 15 write a cell's field through a handle with both indexes worked out while running, which is what has the gate'sdeclarationsstep see both instructions run. What it prints is the same.tools/check-commands.sh: a run's index past the run is refused withK0604written with=, with+=, and with/=by nought. The last tells the read's check apart from the write's: without it, the read goes past the run and the division is refused first, withK0601.tools/check-c.sh: the same=and/=by nought in a body compiled to C have to be refused with the machine's ownK0604line, byte for byte.docs/internals/compiler.md: 225 instructions, and what an element place holds.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.A run of runs written at two indexes worked out while running (
grid[i][j] = 9) is still refused, in a local as well as in an element; that is #249.Verification
K0505andtest/regress/runwrite.kestwith eleven by akestbuilt from 66caeab, whosesrc/compile.c,src/lower.candsrc/emitc.care main's (ca6f9c2). On this branch the example prints15 99 30, and the program answers 0 underkestandkest-debug, each with nothing,KEST_PLAIN=1,KEST_NOOPT=1andKEST_WALK_EVERY=1.kest emitwrites it with 19load.elem.runandstore.elem.run.*=and/=by hand:[10, 20, 30]withcount[1] *= 3andcount[2] /= 4prints10 60 7underkest-debug, with and withoutKEST_NOOPT=1.kest-debug's sanitizers: the place is worked out after the 64 pushes.IN_RUNtaken out ofload.elem.runinsrc/vm.c, the/=row sayserror[K0601]: division by zeroand fails, and so does the C check, because the machine's line is no longer the same. With thekest_run_atblock taken out ofwrite_eleminsrc/emitc.c, the C check fails both ways: the=says nothing and answers, and the/=saysK0601. The+=row passes under both, because the write's check says the same sentence after the read; it is there for the compound path.heavy.sh, after the tests were added (8e588a1):tables,commands,backendanddeclarationspass,backendsaying "a run inside an element written and divided into past its end, refused in C with the machine's line".clang-20andgcc-14, andclang-formatin the CI image.kest fmtof both Kest files.heavy.sh, on adevbuild of this branch (on ca6f9c2):examples/embed,examples/embed-debugandexamples/embed-debugwithKEST_WALK_EVERY=1pass, andctest --preset dev -j4passes all 21 tests:regressionsruns the new program under both builds, the heap walked at every allocation,KEST_PLAIN,KEST_NOOPTand compiled to C;gaterunsexamples/rows.kestevery way and holds it to the same output;declarationssees both instructions run;commandshas the newK0604. A first run without the example faileddeclarationswith "nothing emitsload.elem.run, so no example has run it", which is whyexamples/rows.kestwrites one.Closes #245
BREAKING CHANGE: a program writing one of a fixed run inside an element of an array at an index worked out while running was refused with K0505 and now compiles and runs.