Skip to content

fix(compiler)!: write a run inside one of a run at two running indexes - #257

Merged
siracozmen01 merged 1 commit into
mainfrom
run-in-a-field
Nov 5, 2026
Merged

siracozmen01 merged 1 commit into
mainfrom
run-in-a-field

Conversation

@siracozmen01

Copy link
Copy Markdown
Contributor

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

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

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 fix(compiler)!: write runs of runs and local runs at running indexes #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 fix(compiler)!: write runs of runs and local runs at running indexes #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.

A write to 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`), was refused as a fault
in the compiler, in a local and inside an element. The inner run is now
copied out of the one of the outer it is in, written and copied back, the
way a read of one already went; inside an element the copy is taken after
the value is worked out.

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 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 a8e5493: no change needed. I read the code; I did not run it.

The re-take after the value (inside an element). assign_through_runs keeps the array handle, the element's index and the outermost run's index on the stack, from compile_address with place_apart and compile_run_index. It does not keep an address. Each through_one for the outermost way, out or back, makes a new elem_place from those refs. The element is therefore found against the array as it is at that access, and the copy taken after the value comes from the element as the value left it:

  • A value that grows the array is covered: grown pushes 64, and xs[1] must read 85.
  • A value that writes other parts of the element being written is covered: tagged writes the tag and xs[0], and bumped writes xs[0] under -=.
  • Shrinking the array is not covered. If a value left k outside the array, the re-take's load.elem.run would be refused against the array's current length. That is the same K0604 element refusal any element read gives ("index 5 is outside an array of length 0"); the write would not go to a stale block. The program does not exercise this.
  • i itself cannot go out of range from the value. It indexes a fixed run inside the element, and it was worked out and kept before the value, so the value cannot move it. The run's count is the type's, and in_the_run holds i to it at each access.

The other paths.

  • In a local, = takes the copies after the value. An operator takes them before the value and does not take them again. That relies on nothing in a value writing the caller's frame, which is the same assumption the existing single-run operator path (read_in_place) already makes.
  • Each copy holds only the inner run, so writing it back rewrites the inner run and not its siblings. ss[i].tag and rows[i].seen checked after the write pin that.
  • The copies are written back innermost first, and the three-deep rows[i].s[i - 1].xs[j] case pins that ordering.

Other checks.

  • The four K0604 rows refuse the outer and the inner index, in a local and inside an element, the last with +=.
  • The body has Problem, Change and Verification.
  • The code block's column-0 lines (module m, fn main() -> i32 {, error[K0505]: ...) contain no word( with an open scope and no type(scope): line.

@siracozmen01
siracozmen01 merged commit 9702918 into main Nov 5, 2026
24 checks passed
@siracozmen01
siracozmen01 deleted the run-in-a-field branch November 5, 2026 14:54
@rawframe-release rawframe-release Bot mentioned this pull request Nov 5, 2026
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 is written at two running indexes only when nothing is between them

1 participant