diff --git a/FIX.org b/FIX.org index 6804dfd..52a4bd4 100644 --- a/FIX.org +++ b/FIX.org @@ -823,9 +823,14 @@ globals at all. what the daemon has always promised in its own words — "the globals are as the last run left them" — and what a zeroed one already got for free, since .bss is untouched by a second entry into main. -- [defconst] is a constant and the question does not arise: it is the linker's - image on one backend and a constructor's stores on the other, and a re-run - reaches neither. +- [defconst] with a compile-time-constant initialiser is written into the + image — the linker's on one backend, [flan..init-data]'s stores on the + other — and no startup code reaches it, so a re-run reaches neither. The + split is [Tast.const_init]'s and it is over the *initialiser*, not over the + form: a computed [defconst] would be guarded exactly like a computed + [defvar]. The x86 backend does guard one; the LLVM backend refuses the + program instead, because [Emit.const] has nowhere to run a computed value. + That divergence is older than this rule and is noted here rather than fixed. - If the language grows a [def]-style form that re-evaluates, that form recomputes on every run. None exists today and none was invented for this; the rule is written so that adding one is a new case and not a revision. @@ -838,6 +843,16 @@ because the rule belongs to the form; dev builds only, so a release build's .ll and .s are byte for byte what they were, which was measured on both backends rather than argued. +The flag's name is [.init~once.]. It was [.init-once.] until +2026-09-20, which a program could collide with: [.] and [-] are both ordinary +symbol constituents, so [(defvar .init-once.x i64 7)] beside a computed [x] +emitted the same symbol twice and the dev build died at the assembler on both +backends — and worse, the flag's Bool was registered over the user's global in +[Emit.globals], so the store to it came out as an [i1]. [~] is a terminator in +the reader, so no symbol a program can write contains one; [destructure~N] +uses the same trick. [test/programs/dev-rerun.flan] carries a global named +[.init-once.counter] to keep it pinned. + Verified against a live daemon on both backends with [test/programs/dev-rerun.flan]: a computed i64 counts 41, 42, 43, 44 across four runs where it counted 41, 41, 41, 41 before; a computed dyn map keeps the diff --git a/lib/emit.ml b/lib/emit.ml index 020d9cd..49f294f 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -3418,9 +3418,17 @@ let startup_sym = fname ".init-globals" is what the daemon has always promised ("the globals are as it left them") and what a plain zeroed [defvar] already got for free, since .bss is untouched by a second call. A computed one used to be the exception, wiped - back to its initial value every re-run. A [defconst] is a constant and the - question does not arise for it: it is the linker's image on one backend and - a constructor's stores on the other, and neither is reached from here. + back to its initial value every re-run. A [defconst] whose initialiser is a + compile-time constant is not reached from here at all: it is the linker's + image on one backend and [.init-data]'s stores on the other, and a re-run + reaches neither. + + The split is [Tast.const_init]'s, and it is over the *initialiser* and not + over the form — nothing below asks [gconst]. A [defconst] with a computed + initialiser would therefore be guarded here like any [defvar], and on the + x86 backend it is. On this one it never arrives: [const] refuses a computed + [defconst] by name, because a constant has nowhere to run. The two backends + disagree about that program and only about that program. So each computed initialiser guards itself with a flag of its own. Per global and not per startup function, because the rule belongs to the form: @@ -3433,10 +3441,23 @@ let startup_sym = fname ".init-globals" byte. The flag is a global of its own rather than a sentinel value in the variable, because there is no value a [defvar] cannot hold. - Writing the flag *after* the store is safe rather than merely tidy: - [Check.no_transfer_in_init] refuses a signal or a restart out of an - initialiser, so nothing leaves the guarded branch between the two. *) -let init_flag n = ".init-once." ^ n + Writing the flag *after* the store is what makes a failed initialiser retry + rather than be skipped. [Check.no_transfer_in_init] refuses a [signal] or an + [invoke-restart] written *in* the initialiser, but it is syntactic and over + that expression only: the initialiser is lifted into a function of its own, + and a callee of that function can signal unhandled and transfer. The guarded + branch then leaves through the call's transfer edge with the store not done + and the flag still false — which is the behaviour to want, because the next + run will try the initialiser again instead of proceeding with a global that + was never given its value. + + The flag's name is mangled with a [~], which the reader treats as a + terminator and so cannot appear in any symbol a program can write — the same + trick [destructure~N] uses. A [.]-separated name would not do: [.] is an + ordinary symbol constituent, so [(defvar .init-once.x ...)] beside a + computed [x] used to emit the same symbol twice and the dev build died at + the assembler. *) +let init_flag n = ".init~once." ^ n (* The computed globals, the flags that guard them, and the body of the startup function — built here so that the two backends cannot disagree diff --git a/lib/x86.ml b/lib/x86.ml index 90dc357..f32b12b 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -2778,18 +2778,21 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = globals — a folded answer is evidence about the folder. Calling the same function is what makes the two backends agree by - construction - rather than by a hand-written identity that would have to get every - rounding, every sign of zero and every infinity right on its own. - The prelude already declares both symbols ([fmod-f32], + construction rather than by a hand-written identity that would have + to get every rounding, every sign of zero and every infinity right + on its own. The prelude already declares both symbols ([fmod-f32], [fmod-f64]) and every link passes -lm, so nothing new has to be arranged for the call to resolve. The two operands are already in xmm0 and xmm1, which are exactly where SysV wants the arguments of [double fmod(double, double)], and the result comes back in xmm0, which is where the store below - reads it. [rax] carries the count of SSE argument registers, the - same thing [call_c] puts there: a fixed-arity callee ignores it. *) + reads it. The [rax] below carries the count of SSE argument + registers, and [fmod] is fixed-arity and ignores it: it is dead, and + it is kept deliberately so that every call this backend makes into C + is preceded by the same instruction [call_c] emits. Deleting it + would save one [movabs] and make this the one call site that reads + differently in a disassembly. *) | Tast.Rem -> imm_into f ~reg:rax 2L; call_sym f.b (if f64 then "fmod" else "fmodf") diff --git a/test/programs/dev-rerun.flan b/test/programs/dev-rerun.flan index 8ce6091..1e8fb59 100644 --- a/test/programs/dev-rerun.flan +++ b/test/programs/dev-rerun.flan @@ -6,8 +6,11 @@ ;;;; 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] is a constant and the -;;;; question does not arise. +;;;; 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. The split is +;;;; over the initialiser and not over the form — a computed [defconst] would +;;;; be guarded like a [defvar], and the LLVM backend refuses one outright. ;;;; ;;;; 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 @@ -33,10 +36,20 @@ (defvar state dyn (table)) +;; The guard flags the fix adds are the compiler's own globals, and they used +;; to be spelled [.init-once.] — 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)) (print "counter ") (print counter) (println "") (print "zeroed ") (print zeroed) (println "") diff --git a/test/programs/math3.flan b/test/programs/math3.flan index 3b8e8ac..fca8255 100644 --- a/test/programs/math3.flan +++ b/test/programs/math3.flan @@ -111,11 +111,11 @@ ;; and the folded answer would be evidence about the constant folder and ;; not about the lowering. ;; - ;; The four signs are the first line, because that is where a modulo - ;; written in place of a remainder disagrees: the sign follows the - ;; dividend. The second line is the two answers IEEE defines where an - ;; integer % would have died — a zero divisor and a NaN dividend are both - ;; NaN, not a signal. + ;; Six values on the first line: the four sign combinations, where a modulo + ;; written in place of a remainder disagrees — the sign follows the dividend + ;; — and then the f32 pair, asking fmodf the same. The second line is the + ;; two answers IEEE defines where an integer % would have died: a zero + ;; divisor and a NaN dividend are both NaN, not a signal. (show64 (% rem-a rem-b)) ; 1.5 (show64 (% rem-na rem-b)) ; -1.5 (show64 (% rem-a rem-nb)) ; 1.5