flan/test/programs/dev-halfwrite.flan
Joseph Ferano 290d322b44 An assignment is whole or it never happened, on x86 too
Two bugs, both in lib/x86.ml, both reached by signalling a condition from
inside the value being assigned.

The backend has no register wide enough to hold a struct, so it built every
aggregate in its destination, element by element as they were computed. A
transfer out of the middle left the destination in the middle: a global of
four numbers read part old and part new, and so did a local, a struct field,
an array element, a place behind a pointer. A union case was worse than the
rest, since its destination is zeroed before the fields are written.

The second one only looks like the first. An aggregate result is written
through a hidden pointer the caller supplies, and the transfer exit zeroed
that result on its way out, on the correct reasoning that the caller never
reads it. At (set g (f)) the pointer is g, so it zeroed the caller's
variable. It zeroes scalars only now — and with the first half in place
nothing reaches it, so that one is defence in depth and FIX.org says so
rather than claiming a test it does not have.

Assignment builds into a frame temporary and copies, which is the shape
emit.ml always had. The copy is paid where it buys something: [settles] says
whether lowering an expression is bound to reach the end of it, and a
right-hand side that settles is still built in place. That includes a
conversion, except the one direction that is checked — a float narrowed to an
integer — because an array of this language's literals is written
[(u32 1) (u32 2)] and refusing every cast would have taxed the commonest
aggregate there is. A let pays only inside a loop, because a slot reached
once per frame is recorded as bound after the value lands.

half-write.flan is every destination crossed with every right-hand side, run
on both backends, with a scalar row so a regression in one is not read as the
other. dev-halfwrite.flan asks the break loop the half a running program
cannot ask itself: the local, which is invisible to the program and plain to
an editor reading the frame.

The def-edit paragraph in docs/BUILT.md described this as open and is now the
claim it was waiting for. FIX.org records the two lanes the review of this one
turned up and this one does not take: an aggregate built in place can read its
own destination, which is an aliasing question rather than a transfer one; and
the temporary is a frame buffer no root table names, which matters the day a
struct may hold a dyn field.
2026-09-21 12:37:08 +07:00

54 lines
2.1 KiB
Plaintext

;;;; What a variable reads as while an assignment to it is stopped part-way.
;;;;
;;;; half-write.flan asks the same question from inside the program, after the
;;;; frame has been abandoned. This one asks it from outside, at the break —
;;;; which is where it was first noticed, and where a local can be looked at
;;;; at all: a half-built local is invisible to the program (its name is not
;;;; in scope until the value lands) and perfectly visible to an editor
;;;; reading the frame.
;;;;
;;;; Three turns of one loop and two stops, so a single session answers both
;;;; halves. Nothing here handles the condition, so each stop lands in the
;;;; break loop with the program's own `continue` on offer.
;;;;
;;;; turn 0 — both the local and the global are built whole: 1 1 5 1.
;;;; turn 1 — signalled while the *local* is being built. At the stop the
;;;; slot still belongs to turn 0 and has to read 1 1 5 1 whole,
;;;; not 2 2 . 1 with the first half of turn 2 in it.
;;;; turn 2 — resumed, the local is rebuilt, and then the signal comes from
;;;; inside the *global's* assignment. At the stop the global has
;;;; to read turn 0's 1 1 5 1, not 3 3 5 1.
(import agent "vendor:agent")
(defstruct Boom [why i32])
(defonce colors [4 u32] [9 9 9 9])
(defonce spare [4 u32] [9 9 9 9])
(defonce skipped i64)
(defn blow [] u32
(error (Boom {.why 1}))
0)
;;; One element of each literal, arranged so that each turn stops in at most
;;; one place and the two stops are in different ones.
(defn in-local [n i32] u32 (if (= n 1) (blow) (u32 5)))
(defn in-global [n i32] u32 (if (= n 2) (blow) (u32 5)))
(defn work [] ()
(dotimes [i 3]
(restart-case
(do
(let [v [(u32 (+ i 1)) (u32 (+ i 1)) (in-local i) (u32 (+ i 1))]]
(set spare v))
(set colors
[(u32 (+ i 1)) (u32 (+ i 1)) (in-global i) (u32 (+ i 1))]))
(continue [] (set skipped (+ skipped 1))))))
(defn main [] i32
(agent/start "/tmp/flan-dev-halfwrite-fallback.sock")
(work)
(dotimes [i 4000]
(agent/wait 5))
0)