DISCUSS.org's "need a value-producing array constructor": the author wanted grid filled with 255 as part of its declaration and could not write it. (array n T) produces the zeroed array only, and dotimes is Unit, so it can mutate a place that already exists but cannot be the initialiser expression -- which has to produce the whole value in one go. The grid was declared zeroed and filled in main instead. Two forms, both expressions, both any rank: (array-fill [rows cols] 255) every element that value (array-gen [rows cols] cell) every element (cell i j) Spelled apart rather than one form dispatching on the third element's type, because an array *of* function values is a thing to want and one form would have to decide whether (array-fill [4] f) meant four copies of f or four calls of it. The dimensions are read in Parse, and that is the whole reason they are recognised there: handed through as an ordinary call, [rows cols] is an array literal of two names, and where those names are defconsts it is a perfectly good two-element array of integers -- the wrong reading, and a silent one. Read in Parse they are the same len the [n T] type spelling takes, resolved by the same array_len, with one extra condition of their own: the fill counts in i32 like every index in the language, so a dimension no i32 can reach has no loop that could end. The lowering is a loop over a slot, not an aggregate. Tast.Arr is the node the backends have and both build it element by element from a list as long as the array; a fill of [600 [800 u8]] is half a million elements and there is no list to be had. So these bind the array to a slot, zero it, run one While per dimension writing through Set of a Pindex, and answer with the slot -- While, Set and Pindex, which is the argument check_loop already makes for recur. Nothing new reaches a backend and all three get the form with no edit. The value stays value-like: the slot is the form's own, and the Local at the end copies out the way any array-typed expression does. Row-major is pinned, not incidental: the first dimension is the outermost loop, and a generator that counts observes it. The fill value and the generator value are each bound once before any loop starts, so (array-fill [n] (next-id)) is one call and n copies of its answer. What falls out for the defvar the note was written about, and neither half is a carve-out: (defvar grid [rows [cols u8]] (array-fill [rows cols] 255)) is the spelling that works -- a typed global with a computed initialiser, which is the startup-lifted path with the init-once guard that defvar already had, so the fill runs once and the value survives a re-run like any other computed one. The three-element spelling means what the 2026-09-20 rule says it means: not a type, so a dyn global, and a typed fixed array crosses into dyn only as a view of storage that outlives the view. A freshly built array is a temporary, so it is refused -- by the element rule where the elements are themselves an array, by the lifetime rule where they are one of the three scalars a view carries. Both refusals are the ones any other temporary gets. The type an array-fill builds never goes through resolve, so resolve's own guard is asked again where it is built: a fixed array of function values would be zeroed, and a zeroed function value is a null pointer.
88 lines
4.2 KiB
Plaintext
88 lines
4.2 KiB
Plaintext
;;;; What a re-run does to a global, which is decided by the form that
|
|
;;;; defined it and not by the daemon.
|
|
;;;;
|
|
;;;; A [defvar] is Common Lisp's [defvar]: its initialiser runs only if the
|
|
;;;; variable is not already initialised, so its value survives a re-run. A
|
|
;;;; plain zeroed one always did — .bss is untouched by a second entry into
|
|
;;;; main — and a computed one did not, because the startup function [main]
|
|
;;;; calls ran again from the top and stored the initial value back over
|
|
;;;; whatever the last run had left. A [defconst] whose initialiser is a
|
|
;;;; compile-time constant is written into the image and no startup code
|
|
;;;; reaches it at all, so the question does not arise for it. Since
|
|
;;;; 2026-09-20 there is no other kind: a defconst's initialiser has to be a
|
|
;;;; compile-time constant, refused in the checker so that both backends
|
|
;;;; refuse the same program. Before that the LLVM backend refused a computed
|
|
;;;; one while x86 guarded it at startup like a defvar.
|
|
;;;;
|
|
;;;; So each line printed below is a claim about one of those cases, and the
|
|
;;;; run number is the first of them: [runs] is computed, so before the fix it
|
|
;;;; counted 1, 1, 1.
|
|
(import agent "vendor:agent")
|
|
|
|
(defconst base i64 40)
|
|
|
|
;; Computed, and the whole reproduction: the initialiser is a call, so it is
|
|
;; lifted into the startup function rather than written into the image.
|
|
(defn start [] i64 base)
|
|
|
|
(defvar counter i64 (start))
|
|
|
|
;; Zero-valued, which needs no startup at all and must keep needing none.
|
|
(defvar zeroed i64)
|
|
|
|
;; A computed dyn global: the map is built by a function, rooted before the
|
|
;; startup function runs, and mutated by every run. Its contents have to
|
|
;; survive a re-run for the same reason [counter]'s value does, and its root
|
|
;; has to survive collection either way.
|
|
(defn table [] dyn {:runs 0})
|
|
|
|
(defvar state dyn (table))
|
|
|
|
;; The same thing written the short way: the third element is not a type, so
|
|
;; this is a dyn global initialised at startup — the same declaration [state]
|
|
;; is, down to the guard flag, because the rule lowers to that form and not to
|
|
;; a second one. Its value has to survive a re-run for exactly [counter]'s
|
|
;; reason, and if the new spelling had grown a startup path of its own this is
|
|
;; the line that would count 1, 1, 1, 1.
|
|
(defvar tally 0)
|
|
|
|
;; A typed array with a computed initialiser: (array-fill ...) is an
|
|
;; expression, so it is lifted into the startup function and guarded there
|
|
;; exactly as [counter]'s call is. If it were not — if a fill re-ran on every
|
|
;; entry into main — this would count 251, 251, 251, 251 instead of climbing,
|
|
;; which is [counter]'s own failure in the one shape that only an array can
|
|
;; have.
|
|
(defvar grid [2 [3 u8]] (array-fill [2 3] 250))
|
|
|
|
;; The guard flags the fix adds are the compiler's own globals, and they used
|
|
;; to be spelled [.init-once.<name>] — a name a program can write, since [.]
|
|
;; is an ordinary symbol constituent. This one is exactly the old spelling of
|
|
;; [counter]'s flag. It compiles only because the flag is mangled with a [~]
|
|
;; now; before that the dev build died at the assembler with the symbol
|
|
;; defined twice, and the flag's Bool retyped this i64 on the way. It stays
|
|
;; unprinted on purpose — the expected output is what it was.
|
|
(defvar .init-once.counter i64 7)
|
|
|
|
(defn main [] i32
|
|
(agent/start "/tmp/flan-dev-rerun-fallback.sock")
|
|
(set counter (+ counter 1))
|
|
(set zeroed (+ zeroed 2))
|
|
(set .init-once.counter (+ .init-once.counter 1))
|
|
(put state :runs (+ (get state :runs) 1))
|
|
(set tally (+ tally 1))
|
|
(set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1)))
|
|
(print "counter ") (print counter) (println "")
|
|
(print "zeroed ") (print zeroed) (println "")
|
|
(print "runs ") (print (get state :runs)) (println "")
|
|
(print "tally ") (print tally) (println "")
|
|
(print "grid ") (print (i32 (at grid 0 0))) (println "")
|
|
;; The element the run never writes, which says the fill ran at all: 250
|
|
;; on every run, and 0 if the initialiser had been skipped outright.
|
|
(print "grid-far ") (print (i32 (at grid 1 2))) (println "")
|
|
(print "base ") (print base) (println "")
|
|
;; Long enough for a client to be served, short enough to park well inside
|
|
;; any watchdog — dev-macro.flan's clock, for its reason.
|
|
(dotimes [i 100]
|
|
(agent/wait 5))
|
|
0)
|