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.
54 lines
2.1 KiB
Plaintext
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)
|