The init-once flag moves out of the namespace a program can write
The guard flag was named .init-once.<global>, and . and - are ordinary symbol constituents, so (defvar .init-once.x i64 7) beside a computed x emitted the same symbol twice: the dev build died at the assembler on both backends, and the flag's Bool was registered over the user's global in Emit.globals so the store came out as an i1. It is .init~once.<global> now; ~ terminates a symbol in the reader, the same trick destructure~N uses. test/programs/dev-rerun.flan carries such a global and no new printed line. Three places said a defconst is the linker's image on one backend and a constructor's stores on the other and a re-run reaches neither. Tast.const_init splits on the initialiser and not on the form, so that holds only for a constant initialiser; a computed defconst is guarded like a defvar on x86 and refused outright by emit.ml's const. The sentences now say that, including the divergence. emit.ml also claimed Check.no_transfer_in_init made it impossible to leave the guarded branch between the store and the flag. It is syntactic over the written initialiser only: a callee can signal unhandled and take the call's transfer edge out, leaving the flag false — which is what should happen, since the next run retries. Read off the emitted IR for such a program. The x86 float-Rem comment says why the dead movabs before fmod is kept, and its mid-sentence line break is gone; math3.flan's first float-% line prints six values, not four.
This commit is contained in:
parent
1829cd43b6
commit
5b39730f07
21
FIX.org
21
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.<global>]. It was [.init-once.<global>] 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
|
||||
|
||||
35
lib/emit.ml
35
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
|
||||
|
||||
15
lib/x86.ml
15
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")
|
||||
|
||||
@ -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.<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))
|
||||
(print "counter ") (print counter) (println "")
|
||||
(print "zeroed ") (print zeroed) (println "")
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user