def re-runs its initialiser, and defvar is renamed defonce

The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.

One parse arm reads both forms; the difference is Ast.reinit, carried to
Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and
Check.check_global lifts every def initialiser — zero and literal
included — into global/<n>, so the host's startup reaches it through the
function cell and a re-evaluated def swaps it (Session's def_inits;
Emit.redefinition declares the cell for a non-sibling target). The old
defvar spelling is refused with the rename and both compiling spellings,
and every program, test, doc and editor list is swept — except sand.flan,
the author's live WIP, whose seven defvar lines are flagged in FIX.org
and keep its three dependent tests red on this branch.
This commit is contained in:
Joseph Ferano 2026-09-20 22:46:19 +07:00
parent 9bb01b77df
commit a4c6b996ff
170 changed files with 1297 additions and 875 deletions

73
FIX.org
View File

@ -4343,3 +4343,76 @@ builds, which is a lane of its own.
Not queued. The window is narrow, the fields are right, and the wrong sentence Not queued. The window is narrow, the fields are right, and the wrong sentence
is a great deal better than the one it replaced. is a great deal better than the one it replaced.
* def, and defvar renamed to defonce, 2026-09-20
** The gap, hit dogfooding
The author edited a ~defvar colors [4 u32] [...]~ initialiser and the colours
did not change on C-c C-c — which is defvar working exactly as designed, and
the wrong form for what he was doing. The re-run rule (the entry "Per-form
initialisation semantics on re-run" above) had already decided the trio in
his words: "It doesn't matter if you rerun that startup function, those 3
forms decide what happens." Two of the three were built; ~def~ — foreseen
there as "a def-style form that re-evaluates, recomputes on every run" — was
not. Now it is.
** The rename, author's words
"Rename to defonce, I think Clojure's name is better and more descriptive."
Clojure's ~defonce~ has exactly these semantics — define only if unbound — so
the name now says what the form does, next to a ~def~ that follows the
source. ~defvar~ is refused by name (~parse/defvar-renamed~) with the two
spellings that compile; every program, test, doc and editor list in the repo
is swept, except sand.flan, which is the author's live WIP and stays for the
merge.
** The trio
| form | initialiser runs | on a re-run |
|----------+-------------------------+------------------------------------|
| def | at startup, every run | repaints — the source's value wins |
| defonce | at startup, first run | keeps — the program's value wins |
| defconst | never — it is the image | untouched; it was never storage |
~def~ is Common Lisp's ~defparameter~, ~defonce~ is CL's ~defvar~ under
Clojure's name, and ~defconst~ is a compiler constant. Every spelling of the
third element (the four spellings pinned in test_flan.ml) holds for ~def~
exactly as for ~defonce~ — same parse arm, same collision rules, same
third-element dispatch — with one field of difference, [Ast.reinit], carried
to [Tast.global]'s [grerun].
** How a def reaches the next re-run
Two mechanisms, one per half of the promise:
- No guard flag. [Emit.startup_plan] gives a [defonce]'s computed initialiser
the [.init~once.] flag and gives a [def]'s none, so the store runs on every
entry into main. Shared plan, so the two backends cannot disagree.
- Always lifted. [Check.check_global] lifts *every* def initialiser into
[global/<n>] — zero and literal included, where a defonce keeps constants
inline in the image. The host's startup calls the initialiser through its
function cell, so when the author edits the form and C-c C-c's it,
[Session]'s [def_inits] hands [global/<n>] to the redefinition, the cell
swaps, and the next re-run stores the *edited* value into the same storage
— a native ~[4 u32]~ def repaints in place, so every reference sees the new
values. A constant left inline would have baked the stale value into the
host's startup body for ever. [Emit.redefinition] declares the cell for a
non-sibling target (a lifted initialiser's [fparent] is its global); the
x86 side already reached host cells through the GOT.
~uninit~ is the one exception on both forms: nothing to run, nothing to lift,
nothing repaints.
Pinned in test/programs/dev-rerun.flan (a ~(def c 3)~ printing 4, 4, 4, 4
beside a ~tally~ climbing 1..4; a ~(def hues [4 u32] ...)~ element printing 8
every run; then ~(def c 9)~ evaluated and the next re-run printing 10 while
the defonce beside it climbs on), in test_flan.ml (parse shapes for every
spelling of both forms, [grerun] on the pair, the lifted-initialiser claim,
def/defn and def/defonce collisions, the defvar teaching error verbatim), and
in programs/global-init.flan's last four lines (all def spellings start
identically on both backends and at both -O0 and -O2).
** Red on this branch, for the merger
sand.flan spells ~defvar~ at lines 15, 16, 24, 25, 26, 115 and 116 and was
not touched — same situation as the raylib keywords above. The three
sand-dependent tests (test_flan's parse pin, test_session's create,
test_acceptance's "a package's main is not visible") are red here and turn
green when those seven lines say ~defonce~ (or ~def~, where the author wants
the initialiser to follow the source — ~colors~ was the motivating one).

20
NEXT.md
View File

@ -414,7 +414,7 @@ forms now and uses the one resolver that always existed.
see [`docs/BUILT.md`](docs/BUILT.md), "`C-x C-e` expands too, and a declaration is not an expression". It was see [`docs/BUILT.md`](docs/BUILT.md), "`C-x C-e` expands too, and a declaration is not an expression". It was
the wrap `Parse.decl` already had, and the decision it was waiting on came out as: an expression that the wrap `Parse.decl` already had, and the decision it was waiting on came out as: an expression that
expands to a declaration is **refused by name**, in `Parse.expr`'s head dispatch, so a `defn` nested expands to a declaration is **refused by name**, in `Parse.expr`'s head dispatch, so a `defn` nested
anywhere in the expansion and a `defvar` typed by hand get the same sentence. The spin refusal fires anywhere in the expansion and a `defonce` typed by hand get the same sentence. The spin refusal fires
on this path; the ring never can, because a ring is refused while its own package is parsed. What on this path; the ring never can, because a ring is refused while its own package is parsed. What
expands is the prelude's macros and the imported packages' — and, since the lane recorded just below, expands is the prelude's macros and the imported packages' — and, since the lane recorded just below,
the file's own as well. the file's own as well.
@ -455,7 +455,7 @@ the Emacs side". **Superseded** — the registry lane built that properly. Delet
## For siam-farmer ## For siam-farmer
`docs/PORTING.md`'s verdict is unconditional now: the game's state is **fixed arrays with counts**, so it `docs/PORTING.md`'s verdict is unconditional now: the game's state is **fixed arrays with counts**, so it
fits in `defvar` globals, nothing is move-only, and generics is off its critical path. Tier 0 and Tier 1 fits in `defonce` globals, nothing is move-only, and generics is off its critical path. Tier 0 and Tier 1
items 4, 5 and 6 are all done. Nothing blocks writing it. items 4, 5 and 6 are all done. Nothing blocks writing it.
## Landed: a pointer from C needs a length before it can be indexed ## Landed: a pointer from C needs a length before it can be indexed
@ -555,10 +555,10 @@ field. Nor does a `pop` from a `Vec`. The case that wants both is **a fixed-capa
allocation**, which is Odin's `Small_Array` and a good fit for a game that refuses to allocate in a frame: allocation**, which is Odin's `Small_Array` and a good fit for a game that refuses to allocate in a frame:
```lisp ```lisp
(defvar enemies (Small-Array 64 Enemy)) (defonce enemies (Small-Array 64 Enemy))
``` ```
`docs/PORTING.md`'s recommendation for the game's state was fixed arrays with counts held in `defvar` globals, kept `docs/PORTING.md`'s recommendation for the game's state was fixed arrays with counts held in `defonce` globals, kept
deliberately away from `Vec` so nothing is move-only. `(Small-Array $n $t)` is that pattern with a type behind it deliberately away from `Vec` so nothing is move-only. `(Small-Array $n $t)` is that pattern with a type behind it
instead of two variables kept in step by hand. That is the argument for building this, and it is stronger than instead of two variables kept in step by hand. That is the argument for building this, and it is stronger than
"Odin has it". "Odin has it".
@ -1420,7 +1420,7 @@ program, at that program's next frame boundary. Verified against sand: an unsave
consecutive frames drew it. consecutive frames drew it.
Steps 1, 2 and 3 are done — see *The reload primitive* in `docs/BUILT.md`. A list of top-level forms can be recompiled and installed Steps 1, 2 and 3 are done — see *The reload primitive* in `docs/BUILT.md`. A list of top-level forms can be recompiled and installed
into a running process; call sites compiled before they existed follow them, and a `defn` or `defvar` the process was into a running process; call sites compiled before they existed follow them, and a `defn` or `defonce` the process was
never built with can be added and then redefined again. That is the whole of `C-c C-c`, minus an editor: sand.flan takes never built with can be added and then redefined again. That is the whole of `C-c C-c`, minus an editor: sand.flan takes
a redefinition over a socket and installs it between frames. a redefinition over a socket and installs it between frames.
@ -2064,7 +2064,7 @@ binding takes no type, so `(let [pts [4 rl/Vector2]] ...)` reads `[4 rl/Vector2]
fails with *unknown name rl/Vector2*. It cost 32 hand-written `Vector2`s in one raylib example. fails with *unknown name rl/Vector2*. It cost 32 hand-written `Vector2`s in one raylib example.
`[4 T]` is not a special syntax — it is the ordinary type syntax and already works everywhere a type is expected: `[4 T]` is not a special syntax — it is the ordinary type syntax and already works everywhere a type is expected:
`(defvar points [4 rl/Vector2] ...)`, `(defn draw [pts [4 rl/Vector2]] ...)`. A `let` binding is the single position `(defonce points [4 rl/Vector2] ...)`, `(defn draw [pts [4 rl/Vector2]] ...)`. A `let` binding is the single position
with no type slot, which is the whole of the bug. with no type slot, which is the whole of the bug.
`(zeroed [4 rl/Vector2])` was proposed first and rejected on how it reads: in argument position the bracket form is `(zeroed [4 rl/Vector2])` was proposed first and rejected on how it reads: in argument position the bracket form is
@ -2228,13 +2228,13 @@ Ranked by how often they were hit, top two first because they are walls rather t
chose between the three surfaces, and the first of them is not what was taken — see docs/BUILT.md, "`(array 4 chose between the three surfaces, and the first of them is not what was taken — see docs/BUILT.md, "`(array 4
rl/Vector2)`, and the one position with no type slot". The original entry: rl/Vector2)`, and the one position with no type slot". The original entry:
A fixed array is either a top-level `defvar` or a literal with A fixed array is either a top-level `defonce` or a literal with
every element spelled out. `(let [pts [4 rl/Vector2]] …)` parses as a two-element array literal and fails with every element spelled out. `(let [pts [4 rl/Vector2]] …)` parses as a two-element array literal and fails with
*unknown name rl/Vector2*. Cost: 32 hand-written `Vector2`s in one example. **Looked at and stopped — it is a *unknown name rl/Vector2*. Cost: 32 hand-written `Vector2`s in one example. **Looked at and stopped — it is a
grammar question, not a missing feature.** Everything under the surface is already there: `Ast.binding` carries a grammar question, not a missing feature.** Everything under the surface is already there: `Ast.binding` carries a
`bty`, `load.ml` renames through it, and `check.ml:723` consumes it as the `want` for the value. Only the way it `bty`, `load.ml` renames through it, and `check.ml:723` consumes it as the `want` for the value. Only the way it
is written is open, and the parser says so where it refuses (`parse.ml:366`): `let` is a flat list of pairs, so it is written is open, and the parser says so where it refuses (`parse.ml:366`): `let` is a flat list of pairs, so it
cannot disambiguate by *count* the way `defvar` and `defconst` do — those read `[n t v]` as three arguments to a cannot disambiguate by *count* the way `defonce` and `defconst` do — those read `[n t v]` as three arguments to a
form, and there is no such boundary between one pair and the next. Three surfaces, in the order they are worth form, and there is no such boundary between one pair and the next. Three surfaces, in the order they are worth
considering: considering:
- **`(zeroed [4 rl/Vector2])``zeroed` takes its type as an argument.** Recommended. It is one extra branch in - **`(zeroed [4 rl/Vector2])``zeroed` takes its type as an argument.** Recommended. It is one extra branch in
@ -2264,7 +2264,7 @@ crosses as a parameter — a C function that returns one returns something Flan
**The negative result is worth as much.** None of the gaps expected blocked anything — no generics, no allocator, no **The negative result is worth as much.** None of the gaps expected blocked anything — no generics, no allocator, no
`Vec`/`Map`, no escaping closures, and function-scoped `defer` never came up. Input-and-draw over fixed-size state is `Vec`/`Map`, no escaping closures, and function-scoped `defer` never came up. Input-and-draw over fixed-size state is
the shape the language already has. Three constructs unexercised anywhere else in the repo worked first try: a fixed the shape the language already has. Three constructs unexercised anywhere else in the repo worked first try: a fixed
array with a struct element, a 2-D struct array, and `[N string]` as both `defconst` and mutable `defvar`. array with a struct element, a 2-D struct array, and `[N string]` as both `defconst` and mutable `defonce`.
### The web target: what it does not reach yet ### The web target: what it does not reach yet
@ -2768,7 +2768,7 @@ index works — anything above 2³¹ truncates to a negative `i32` and the unsig
is refused with the reason: 2³²+5 truncates to 5 and would read the wrong element with no trap at all. is refused with the reason: 2³²+5 truncates to 5 and would read the wrong element with no trap at all.
- **There is one top-level namespace, and `check.ml` now enforces it.** The environment's tables are per-kind — structs, - **There is one top-level namespace, and `check.ml` now enforces it.** The environment's tables are per-kind — structs,
unions, aliases, enums, functions, externs and globals each have their own — so only a function was ever checked for a unions, aliases, enums, functions, externs and globals each have their own — so only a function was ever checked for a
duplicate. `(defn item …)` beside `(defvar item …)` type checked and then died in LLVM as `redefinition of function duplicate. `(defn item …)` beside `(defonce item …)` type checked and then died in LLVM as `redefinition of function
'@flan.item'`, a message about an emitted symbol with no source location left, and two colliding *type* declarations '@flan.item'`, a message about an emitted symbol with no source location left, and two colliding *type* declarations
were not caught anywhere. One pass over `Ast.declared_name` now runs before every other collection pass and rejects the were not caught anywhere. One pass over `Ast.declared_name` now runs before every other collection pass and rejects the
second declaration of a name whatever kind either one is. `declared_name` lives in `ast.ml` because `Load` needs exactly second declaration of a name whatever kind either one is. `declared_name` lives in `ast.ml` because `Load` needs exactly

View File

@ -83,7 +83,8 @@ let summarise (d : Flan.Ast.decl) =
| Defdata (n, vs) -> Printf.sprintf "defdata %s (%d cases)" n (List.length vs) | Defdata (n, vs) -> Printf.sprintf "defdata %s (%d cases)" n (List.length vs)
| Defunion (n, ms) -> | Defunion (n, ms) ->
Printf.sprintf "defunion %s (%d members)" n (List.length ms) Printf.sprintf "defunion %s (%d members)" n (List.length ms)
| Defvar (n, _, _) -> Printf.sprintf "defvar %s" n | Defvar (n, _, _, Once) -> Printf.sprintf "defonce %s" n
| Defvar (n, _, _, Every) -> Printf.sprintf "def %s" n
| Defconst (n, _, _) -> Printf.sprintf "defconst %s" n | Defconst (n, _, _) -> Printf.sprintf "defconst %s" n
| Declare (fn, csym) -> | Declare (fn, csym) ->
Printf.sprintf "declare %s (%d params) = %s" fn.name (List.length fn.params) Printf.sprintf "declare %s (%d params) = %s" fn.name (List.length fn.params)

View File

@ -10,8 +10,8 @@
;;; Handlers cannot see the establishing function's locals yet, so anything a ;;; Handlers cannot see the establishing function's locals yet, so anything a
;;; handler accumulates into has to be a global. ;;; handler accumulates into has to be a global.
(defvar seen i64) (defonce seen i64)
(defvar ticks i64) (defonce ticks i64)
;;; Two frames under the restart-case, so a transfer has something to cross. ;;; Two frames under the restart-case, so a transfer has something to cross.
(defn probe [n i32] i32 (defn probe [n i32] i32

View File

@ -723,11 +723,11 @@ forms handed to an evaluation are the one thing that was sent. `C-c C-c` has alw
reason, and a `defmacro` typed at the REPL becomes an ordinary `Ast.Defn` that nothing records as a macro. Left where reason, and a `defmacro` typed at the REPL becomes an ordinary `Ast.Defn` that nothing records as a macro. Left where
it was rather than half-fixed here, and pinned in `test_session` so that changing it is a decision. it was rather than half-fixed here, and pinned in `test_session` so that changing it is a decision.
**An expression that expands to a declaration is refused, by name.** `defn`, `defvar`, `defconst`, `defstruct`, **An expression that expands to a declaration is refused, by name.** `defn`, `def`, `defonce`, `defconst`, `defstruct`,
`defdata`, `defenum`, `defalias`, `defmacro` and `import` are heads `Parse.expr` now rejects — the arm that used to `defdata`, `defenum`, `defalias`, `defmacro` and `import` are heads `Parse.expr` now rejects — the arm that used to
say it for `defmacro` alone, generalised. It sits in the head dispatch and not in a walk over what the expander say it for `defmacro` alone, generalised. It sits in the head dispatch and not in a walk over what the expander
answered, so it catches a declaration nested anywhere in the expansion for free, catches one **typed** by hand with the answered, so it catches a declaration nested anywhere in the expansion for free, catches one **typed** by hand with the
same sentence instead of "unknown name defvar", and cannot drift out of sync with `decl`'s list the way a second copy same sentence instead of "unknown name defonce", and cannot drift out of sync with `decl`'s list the way a second copy
would. A *quasiquoted* declaration is deliberately not caught: after desugaring, the name in `` `(defn ...) `` is a would. A *quasiquoted* declaration is deliberately not caught: after desugaring, the name in `` `(defn ...) `` is a
string inside a `Form.Sym` argument rather than a head — the same property that makes a quasiquoted call output rather string inside a `Form.Sym` argument rather than a head — the same property that makes a quasiquoted call output rather
than a dependency. Building a declaration as a value is what a macro is for; evaluating one is not a thing an than a dependency. Building a declaration as a value is what a macro is for; evaluating one is not a thing an
@ -808,7 +808,7 @@ fact without cutting anything in half. See "The browser is the third target" bel
- `(defconst gravity 0.05)``(defconst gravity f32 0.05)`. An untyped float constant is `f64`, `velocity` is `[f32]`, - `(defconst gravity 0.05)``(defconst gravity f32 0.05)`. An untyped float constant is `f64`, `velocity` is `[f32]`,
and `f64` into `f32` is a narrowing — still written, and still written after implicit widening landed (FIX.org and `f64` into `f32` is a narrowing — still written, and still written after implicit widening landed (FIX.org
2026-09-20), because widening is only the conversions that cannot change the number and this one can. 2026-09-20), because widening is only the conversions that cannot change the number and this one can.
- `(defvar current-color u32)` → `i32`. It is an index into `colors`, and `(len colors)` is an `i32`. - `(defonce current-color u32)` → `i32`. It is an index into `colors`, and `(len colors)` is an `i32`.
- `(defn main [])` is unchanged — the short form, as plan.org says. - `(defn main [])` is unchanged — the short form, as plan.org says.
Painting is on **hold left mouse button** rather than on space, since the mouse bindings exist now. Space is still what Painting is on **hold left mouse button** rather than on space, since the mouse bindings exist now. Space is still what
@ -836,7 +836,7 @@ of those is not what passing `-fsanitize=address` to the clang run over the `.ll
**AddressSanitizer is an LLVM pass, but it instruments only functions carrying the `sanitize_address` attribute.** That **AddressSanitizer is an LLVM pass, but it instruments only functions carrying the `sanitize_address` attribute.** That
attribute is put there by clang's C frontend. `Emit` writes `.ll` by hand, so it wrote none, so the pass walked past attribute is put there by clang's C frontend. `Emit` writes `.ll` by hand, so it wrote none, so the pass walked past
every Flan function and instrumented `flan_rt.c`. The measurement that settled it: an out-of-bounds read of a `defvar` every Flan function and instrumented `flan_rt.c`. The measurement that settled it: an out-of-bounds read of a `defonce`
array in a `--no-bounds-checks` build printed its garbage and exited 0; with an `attributes #0 = { sanitize_address }` array in a `--no-bounds-checks` build printed its garbage and exited 0; with an `attributes #0 = { sanitize_address }`
group named on every `define`, the same program reports `global-buffer-overflow in flan.main`. Globals are the exception group named on every `define`, the same program reports `global-buffer-overflow in flan.main`. Globals are the exception
— the module pass redzones them whether or not any function is attributed — which is why the *shape* of a sanitized — the module pass redzones them whether or not any function is attributed — which is why the *shape* of a sanitized
@ -862,7 +862,7 @@ wrong answer instead of touching memory. Same family as `(<< 1 32)` compiling to
`bounds.flan`'s six deliberate out-of-bounds cases are caught. A negative index into a global is not, and the reason is `bounds.flan`'s six deliberate out-of-bounds cases are caught. A negative index into a global is not, and the reason is
layout rather than anything about the access: ASan lays a global out as `{data, redzone}`, so reading before one lands layout rather than anything about the access: ASan lays a global out as `{data, redzone}`, so reading before one lands
in whatever precedes it, which is a redzone if something instrumented is there and ordinary memory if nothing is. in whatever precedes it, which is a redzone if something instrumented is there and ordinary memory if nothing is.
Measured both ways — silent in `bounds.flan`, reported as soon as another `defvar` is declared in front of `arr`. A Measured both ways — silent in `bounds.flan`, reported as soon as another `defonce` is declared in front of `arr`. A
reversed slice is not caught either, having computed a negative length and read nothing at all. And ASan sees reversed slice is not caught either, having computed a negative length and read nothing at all. And ASan sees
out-of-*object* access, not out-of-subobject, so a slice into the middle of a larger array can overrun its logical out-of-*object* access, not out-of-subobject, so a slice into the middle of a larger array can overrun its logical
bounds without crossing a redzone. Three of six is a ceiling on what it covers, not a measurement of the risk. It is a bounds without crossing a redzone. Three of six is a ceiling on what it covers, not a measurement of the risk. It is a
@ -1097,7 +1097,7 @@ the sand hash is the case that matters, since it is the one result that would no
### Names that did not exist when the process started ### Names that did not exist when the process started
Editing a `defvar` or a `defn` is a symbol the host exports. *Adding* one is not: there is no symbol to bind to and ELF Editing a `defonce` or a `defn` is a symbol the host exports. *Adding* one is not: there is no symbol to bind to and ELF
cannot grow one. Those go through `runtime/flan_dev.c`, which is two lookups and nothing else: cannot grow one. Those go through `runtime/flan_dev.c`, which is two lookups and nothing else:
``` ```
@ -1230,7 +1230,7 @@ Open decision #6 records it the same way. None of the three parts exists: there
the alternative to refusing is not the new design, it is a silent argument mismatch. It is a stopgap and the message the alternative to refusing is not the new design, it is a silent argument mismatch. It is a stopgap and the message
should not be read as the final answer. should not be read as the final answer.
A `defvar`'s *initial value* is deliberately **not** in that table. Its storage holds live state the program moved past A `defonce`'s *initial value* is deliberately **not** in that table. Its storage holds live state the program moved past
long ago, and refusing to change the initialiser would be refusing "edit the code, keep the sand". Same `Tast.global` long ago, and refusing to change the initialiser would be refusing "edit the code, keep the sand". Same `Tast.global`
record as a `defconst`, opposite answers, told apart by `gconst`. The enum comparison runs over declarations rather than record as a `defconst`, opposite answers, told apart by `gconst`. The enum comparison runs over declarations rather than
the checked program, because `Tast.program` carries no enums at all — they are erased to `i32` in the checker, which is the checked program, because `Tast.program` carries no enums at all — they are erased to `i32` in the checker, which is
@ -1238,7 +1238,7 @@ the same fact that makes them unreloadable.
Note what the checker catches on its own: change `helper`'s parameter type and the *caller* fails to type check first, Note what the checker catches on its own: change `helper`'s parameter type and the *caller* fails to type check first,
loudly. The session's rules only get a turn on a change the checker accepts — one to a name nothing else in the program loudly. The session's rules only get a turn on a change the checker accepts — one to a name nothing else in the program
uses, which is exactly where the silent version lives. The fixtures carry an unused `defvar` and a C-called `defn` for uses, which is exactly where the silent version lives. The fixtures carry an unused `defonce` and a C-called `defn` for
that reason. that reason.
A `defconst` the checker never consumed is a different matter and **can** be changed: it is only ever bytes in memory. A A `defconst` the checker never consumed is a different matter and **can** be changed: it is only ever bytes in memory. A
@ -1276,7 +1276,7 @@ the real buffer path, which is the same key CIDER's `eval` carries.
### The daemon — `flan dev` ### The daemon — `flan dev`
`flan dev <program.flan>` holds one `Session`, builds the program, launches it, and listens on `.flan-dev.sock` beside `flan dev <program.flan>` holds one `Session`, builds the program, launches it, and listens on `.flan-dev.sock` beside
the source. What it adds over `flan reload` is that the session *persists* — a `defvar` added by one evaluation is part the source. What it adds over `flan reload` is that the session *persists* — a `defonce` added by one evaluation is part
of what the next one is checked against — and that it **owns the build**, which is what makes its layout rules describe of what the next one is checked against — and that it **owns the build**, which is what makes its layout rules describe
the process that is actually running rather than a guess about it. the process that is actually running rather than a guess about it.
@ -1557,7 +1557,7 @@ which is ordinary rather than an error — `flan dev` ends when its program does
socket it was on. Strictly *before* a send, never after one: a connection that died mid-request may have died after the socket it was on. Strictly *before* a send, never after one: a connection that died mid-request may have died after the
daemon ran what it was given, and resending would install it twice or evaluate a side-effecting expression twice. daemon ran what it was given, and resending would install it twice or evaluate a side-effecting expression twice.
`C-c C-k` sends one module rather than a form at a time on purpose: a `defvar` and the function that uses it have to `C-c C-k` sends one module rather than a form at a time on purpose: a `defonce` and the function that uses it have to
arrive in the same load, or the first refers to storage that does not exist yet. arrive in the same load, or the first refers to storage that does not exist yet.
**Framing is in bytes and Emacs counts characters.** Every length goes through `string-bytes` and the process is binary, **Framing is in bytes and Emacs counts characters.** Every length goes through `string-bytes` and the process is binary,
@ -2107,7 +2107,7 @@ size of the slot — 40KB for sand's grid — and the walk only ever shows eight
### Globals of a stopped stack ### Globals of a stopped stack
The other half, and in this language arguably the more useful one: a game keeps most of its state in top-level The other half, and in this language arguably the more useful one: a game keeps most of its state in top-level
`defvar`s and `sand.flan` holds its entire grid that way, so "what is this program's state right now" was a question `defonce`s and `sand.flan` holds its entire grid that way, so "what is this program's state right now" was a question
with nowhere to ask it. with nowhere to ask it.
``` ```
@ -2477,7 +2477,7 @@ container and not a Vec question.
`let` has no type annotation — `parse.ml` settles that a triple binding is ambiguous and that types are inferred — so a `let` has no type annotation — `parse.ml` settles that a triple binding is ambiguous and that types are inferred — so a
local Vec has nowhere to say what it holds and the element type is written at the call: `(vec-new i32)`. This is *not* local Vec has nowhere to say what it holds and the element type is written at the call: `(vec-new i32)`. This is *not*
the explicit instantiation syntax the generics section rules out: nothing here is generic, and the name resolves as an the explicit instantiation syntax the generics section rules out: nothing here is generic, and the name resolves as an
ordinary type rather than binding a type variable. Where the context does say — a `defvar`'s type, a return type, an ordinary type rather than binding a type variable. Where the context does say — a `defonce`'s type, a return type, an
argument — it may be left out. argument — it may be left out.
A zeroed Vec has a null allocator, and the first operation that needs storage adopts the context allocator, which is A zeroed Vec has a null allocator, and the first operation that needs storage adopts the context allocator, which is
@ -2833,7 +2833,7 @@ cannot collide. `(Name {.field value})` is one syntax for both, and which it is,
Tag *n* is the *n*th declared case, from zero. The consequence is that an all-bytes-zero union is **the first declared Tag *n* is the *n*th declared case, from zero. The consequence is that an all-bytes-zero union is **the first declared
case with a zeroed payload** — which is exactly the rule that makes an `Option`'s zero a `None`, and it is what makes case with a zeroed payload** — which is exactly the rule that makes an `Option`'s zero a `None`, and it is what makes
`(defvar u U)` and an omitted union-typed struct field mean something rather than nothing. Reordering a union's cases `(defonce u U)` and an omitted union-typed struct field mean something rather than nothing. Reordering a union's cases
is a layout change, the same way reordering a struct's fields is. is a layout change, the same way reordering a struct's fields is.
### What is refused, and why each ### What is refused, and why each
@ -2856,7 +2856,7 @@ is a layout change, the same way reordering a struct's fields is.
cannot be reached. That is the one place where garbage becomes "the optimiser may do anything". cannot be reached. That is the one place where garbage becomes "the optimiser may do anything".
- **`(.x u)`.** A union's fields belong to a case, and which case is being held is what the tag says. `match` is how - **`(.x u)`.** A union's fields belong to a case, and which case is being held is what the tag says. `match` is how
one is opened, and its arms bind the fields of the case they matched. one is opened, and its arms bind the fields of the case they matched.
- **A global initialised with a case.** `(defvar g U (U.B {.x 1}))` would mean serialising the fields into the payload - **A global initialised with a case.** `(defonce g U (U.B {.x 1}))` would mean serialising the fields into the payload
blob at link time, which is a byte-level encoder this compiler does not have and which could not express a `string` blob at link time, which is a byte-level encoder this compiler does not have and which could not express a `string`
field at all — that is a pointer the linker has to relocate and a byte array has nowhere to put a relocation. A field at all — that is a pointer the linker has to relocate and a byte array has nowhere to put a relocation. A
*zeroed* global is fine and needs none of it: it is the first declared case. *zeroed* global is fine and needs none of it: it is the first declared case.
@ -3510,7 +3510,7 @@ on the way round, so the rule is not about them; `in_loop` takes the loop's own
## `(array 4 rl/Vector2)`, and the one position with no type slot ## `(array 4 rl/Vector2)`, and the one position with no type slot
`[4 T]` is the ordinary type spelling and is unchanged. It already works everywhere a type is expected — `(defvar `[4 T]` is the ordinary type spelling and is unchanged. It already works everywhere a type is expected — `(defonce
points [4 rl/Vector2])`, `(defn draw [pts [4 rl/Vector2]] ...)`, a `defstruct` field, a return. A **`let` binding is points [4 rl/Vector2])`, `(defn draw [pts [4 rl/Vector2]] ...)`, a `defstruct` field, a return. A **`let` binding is
the single position with no type slot**, and there the brackets are read as what they are in expression position: an the single position with no type slot**, and there the brackets are read as what they are in expression position: an
array *literal* of two elements, whose second element is a type name nothing declares. So `(let [pts [4 rl/Vector2]] array *literal* of two elements, whose second element is a type name nothing declares. So `(let [pts [4 rl/Vector2]]
@ -3688,7 +3688,7 @@ call already went through.
(map double xs) (map double xs)
``` ```
and not Common Lisp's `#'double`. **This is a Lisp-1 — one top-level namespace, enforced, so a `defn` and a `defvar` and not Common Lisp's `#'double`. **This is a Lisp-1 — one top-level namespace, enforced, so a `defn` and a `defonce`
cannot share a name** — which is exactly what makes the bare name safe to read: there is no second binding of cannot share a name** — which is exactly what makes the bare name safe to read: there is no second binding of
`double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does `double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does
not ask. not ask.
@ -3857,7 +3857,7 @@ the thing it calls. `Prelude.bootstrap` is the hook, a ref rather than a paramet
`Check.program` and it cannot be told. `Check.program` and it cannot be told.
Only `defn`s are dropped: the functions that survive still mention the prelude's types, and a reduced prelude missing Only `defn`s are dropped: the functions that survive still mention the prelude's types, and a reduced prelude missing
them would not check. A `defstruct`, `defdata`, `defalias`, `defenum` or `defvar` therefore stays whatever it names. them would not check. A `defstruct`, `defdata`, `defalias`, `defenum` or `defonce` therefore stays whatever it names.
There used to be a sharper reason — `Parse.prelude_types` memoised the prelude's type names for the parser's There used to be a sharper reason — `Parse.prelude_types` memoised the prelude's type names for the parser's
return-type guess, and it can be forced for the first time inside a bootstrap build, so a reduced set cached there return-type guess, and it can be forced for the first time inside a bootstrap build, so a reduced set cached there
would have been wrong for every compile afterwards. That set is gone with the guess; see *The return type is the slot* would have been wrong for every compile afterwards. That set is gone with the guess; see *The return type is the slot*
@ -4113,7 +4113,7 @@ something else ever needs to talk to it.
- ~~**Do cells cover globals, or only functions?**~~ **Settled by step 1: functions only.** A redefinition module - ~~**Do cells cover globals, or only functions?**~~ **Settled by step 1: functions only.** A redefinition module
declares every global `external`, so globals live in the host and survive a reload — which is what "edit the code, keep declares every global `external`, so globals live in the host and survive a reload — which is what "edit the code, keep
the sand" needs. The consequence to watch is the other half: adding a `defvar` to a file cannot take effect on reload, the sand" needs. The consequence to watch is the other half: adding a `defonce` to a file cannot take effect on reload,
and changing one's type is a silent mismatch against storage the host already laid out. Nothing detects that yet. and changing one's type is a silent mismatch against storage the host already laid out. Nothing detects that yet.
- **What is a redefinition unit — one function, or a file?** A file is much easier to make correct and is what - **What is a redefinition unit — one function, or a file?** A file is much easier to make correct and is what
`load-file` wants anyway; one function is what `C-c C-c` wants and is where the 16ms number comes from. `load-file` wants anyway; one function is what `C-c C-c` wants and is where the 16ms number comes from.
@ -5081,7 +5081,7 @@ escaped `alloca` is one mem2reg will not promote. So a release build's IR is the
### It is armed by a constructor, and that is not fastidiousness ### It is armed by a constructor, and that is not fastidiousness
`@llvm.global_ctors` in the dev module, not a line at the top of `main`. A `defvar` initialiser can allocate, and it `@llvm.global_ctors` in the dev module, not a line at the top of `main`. A `defonce` initialiser can allocate, and it
runs before `main` does; a note that arrived before the flag was set would be a block the table never heard of, which runs before `main` does; a note that arrived before the flag was set would be a block the table never heard of, which
is a live pointer the inspector would call dead. That is the one failure mode worse than no registry at all. is a live pointer the inspector would call dead. That is the one failure mode worse than no registry at all.
@ -5752,7 +5752,7 @@ and `Parse.expr`'s head dispatch already names `defmacro` first among the heads
top-level declaration, not an expression". It matters more now than when it was written: before this, nobody would top-level declaration, not an expression". It matters more now than when it was written: before this, nobody would
type a `defmacro` at `C-x C-e`, because the session forgot it either way. Now that one typed at the editor means type a `defmacro` at `C-x C-e`, because the session forgot it either way. Now that one typed at the editor means
something, reaching for `C-x C-e` on it is a reflex, and what comes back says what it is rather than complaining something, reaching for `C-x C-e` on it is a reflex, and what comes back says what it is rather than complaining
about an unknown function. `test_repl` pins it beside the `defvar` case. about an unknown function. `test_repl` pins it beside the `defonce` case.
Both editor paths work and they are different wraps — `Parse.expr`'s for `C-x C-e` and `Parse.decl`'s for Both editor paths work and they are different wraps — `Parse.expr`'s for `C-x C-e` and `Parse.decl`'s for
`C-c C-c`. `test/test_session.ml` covers both, plus a `defmacro` the file never had followed by a call to it, `C-c C-c`. `test/test_session.ml` covers both, plus a `defmacro` the file never had followed by a call to it,
@ -5913,7 +5913,7 @@ become incorrect because its callers improved.
`(/ 0.0 0.0)` printed `nan` through LLVM and `-nan` through the x86 backend. Neither is `(/ 0.0 0.0)` printed `nan` through LLVM and `-nan` through the x86 backend. Neither is
wrong about the arithmetic: IEEE 754 does not specify the sign of a NaN any operation wrong about the arithmetic: IEEE 754 does not specify the sign of a NaN any operation
produces, LLVM's constant folder answers with the positive quiet NaN at compile time, and produces, LLVM's constant folder answers with the positive quiet NaN at compile time, and
`divsd` answers with the negative one at run time. Putting the operands in `defvar` `divsd` answers with the negative one at run time. Putting the operands in `defonce`
globals so nothing folds makes both say `-nan`, which is what confirms the divergence is globals so nothing folds makes both say `-nan`, which is what confirms the divergence is
the folding path and not a disagreement about float arithmetic. the folding path and not a disagreement about float arithmetic.
@ -6188,3 +6188,28 @@ depends on.
A string still does not cross to a `[u8]` parameter, and `(slice s)` does not make it one. That crossing is A string still does not cross to a `[u8]` parameter, and `(slice s)` does not make it one. That crossing is
`bytes-view`'s. And `slice` over a `dyn` is still refused while `(at d i)` over one works — the asymmetry is `bytes-view`'s. And `slice` over a `dyn` is still refused while `(at d i)` over one works — the asymmetry is
pre-existing and wants closing, since the two spaces are meant to compute the same thing. pre-existing and wants closing, since the two spaces are meant to compute the same thing.
## `def` re-runs its initialiser; `defvar` is renamed `defonce`
Three forms define a global, and the difference between them is entirely what a daemon re-run does. `defonce` — Common
Lisp's `defvar` under Clojure's name, renamed because the old name said nothing — initialises once and keeps its value:
the value the *program* built is the one that survives, which is "edit the code, keep the sand". `def` is CL's
`defparameter`: its initialiser runs on every re-run, so the value the *source* spells is the one that wins — edit a
`(def colors [4 u32] [...])`, `C-c C-c`, `M-x flan-rerun`, and the colours change, which is the gap the author hit
dogfooding and the reason the form exists. `defconst` is the image and no run reaches it. The old `defvar` spelling is
refused with the rename and both new spellings, each of which compiles as written.
Both forms take the same spellings — `(def x Type)` zeroed, `(def x Type v)`, `(def x v)` a dyn global — through the
same parse arm and the same third-element dispatch, and collide with a `defn` under the same Lisp-1 rule. The
difference is one field, `Ast.reinit`, and two consequences of it. In `Emit.startup_plan` a `defonce`'s computed
initialiser hides behind its `.init~once.` flag and a `def`'s store is bare, so re-entering main repaints a `def` in
place — same storage, no reallocation, every reference sees the new bytes. And `Check.check_global` lifts *every*
`def` initialiser into `global/<n>`, constants and zeros included, where a `defonce` keeps constants inline: the host's
startup calls the initialiser through its function cell, so a re-evaluated `def` swaps the cell (`Session`'s
`def_inits`, plus `Emit.redefinition` declaring the cell for a target whose `fparent` is a global) and the next re-run
stores the edited value. A constant left inline would have baked the stale number into the host's startup for ever.
`uninit` opts out on both forms — nothing to run, nothing repaints.
One shared plan feeds both backends, so x86 and LLVM cannot disagree; `programs/global-init.flan`'s last four lines pin
every `def` spelling's startup on both, at `-O0` and `-O2`, and `programs/dev-rerun.flan` pins the live loop — a
`(def c 3)` printing 4 on every run beside a defonce that climbs, and the edited `(def c 9)` printing 10 after the next
re-run.

View File

@ -196,7 +196,7 @@ nowhere in a buffer to draw one, as at the REPL. `flan-echo-result` decides the
echo area, and it also covers the sentence an *install* reports — which names echo area, and it also covers the sentence an *install* reports — which names
landed and what it cost — since an install has no value to draw. landed and what it cost — since an install has no value to draw.
The form before point can also be a declaration — a `defvar` typed at the top of The form before point can also be a declaration — a `defonce` typed at the top of
a file — in which case `C-x C-e` installs it rather than refusing it, and says a file — in which case `C-x C-e` installs it rather than refusing it, and says
which names changed instead of printing a value. which names changed instead of printing a value.
@ -267,7 +267,7 @@ drawn on the call — it does not hang.
### `C-c C-k` — the whole buffer ### `C-c C-k` — the whole buffer
The whole buffer, sent as **one** module rather than as a form at a time. That The whole buffer, sent as **one** module rather than as a form at a time. That
matters: a `defvar` and the function that uses it have to arrive together, or the matters: a `defonce` and the function that uses it have to arrive together, or the
function refers to storage that does not exist yet. function refers to storage that does not exist yet.
Use this when you have changed several things at once, or when you have added a Use this when you have changed several things at once, or when you have added a
@ -1174,7 +1174,7 @@ still answers.
Under the stack there is one more section: the globals this stopped stack Under the stack there is one more section: the globals this stopped stack
reaches. In this language that is often the more useful half — a game keeps reaches. In this language that is often the more useful half — a game keeps
most of its state in top-level `defvar`s, and `sand.flan` holds its entire grid most of its state in top-level `defonce`s, and `sand.flan` holds its entire grid
that way. that way.
It is **one section, not a fold under each frame**. A global is not part of a It is **one section, not a fold under each frame**. A global is not part of a

View File

@ -119,7 +119,7 @@
;; patched only when somebody notices a gap is the list that is always a ;; patched only when somebody notices a gap is the list that is always a
;; release behind the parser. ;; release behind the parser.
(defconst flan--definers (defconst flan--definers
'("defn" "defmacro" "defvar" "defconst" "defstruct" "defdata" "defunion" '("defn" "defmacro" "def" "defonce" "defconst" "defstruct" "defdata" "defunion"
"defenum" "defalias" "defenum" "defalias"
;; The object and dispatch heads (lib/parse.ml:1277-1334). ;; The object and dispatch heads (lib/parse.ml:1277-1334).
"defclass" "defgeneric" "defmulti" "defmethod" "defclass" "defgeneric" "defmulti" "defmethod"
@ -185,7 +185,9 @@ below — and not again here.")
("Types" ,(concat "^(def\\(?:struct\\|data\\|union\\|enum\\|alias\\)\\s-+" ("Types" ,(concat "^(def\\(?:struct\\|data\\|union\\|enum\\|alias\\)\\s-+"
flan--name-re) flan--name-re)
1) 1)
("Variables" ,(concat "^(def\\(?:var\\|const\\)\\s-+" flan--name-re) 1) ;; `def', `defonce' and `defconst'. `def' has to be matched as itself —
;; `\_>' keeps it from swallowing every other definer's prefix.
("Variables" ,(concat "^(def\\(?:once\\|const\\)?\\_>\\s-+" flan--name-re) 1)
;; A forward declaration is not a definition, and a file with both would ;; A forward declaration is not a definition, and a file with both would
;; otherwise show the same name twice with nothing to tell them apart. ;; otherwise show the same name twice with nothing to tell them apart.
("Declared" ,(concat "^(declare\\s-+" flan--name-re) 1)) ("Declared" ,(concat "^(declare\\s-+" flan--name-re) 1))
@ -601,7 +603,7 @@ decision to `calculate-lisp-indent'."
((eq method :defn) (+ lisp-body-indent head-column)) ((eq method :defn) (+ lisp-body-indent head-column))
;; No spec. Anything else spelled `def…' is a definition and indents ;; No spec. Anything else spelled `def…' is a definition and indents
;; like one, which covers `defstruct', `defdata', `defunion', ;; like one, which covers `defstruct', `defdata', `defunion',
;; `defenum', `defvar', `defconst' and `defalias' without naming ;; `defenum', `defonce', `defconst' and `defalias' without naming
;; them. ;; them.
((and name (string-match-p "\\`def" name)) ((and name (string-match-p "\\`def" name))
(+ lisp-body-indent head-column)) (+ lisp-body-indent head-column))

View File

@ -51,10 +51,10 @@
;; ;;
;; A *declaration* before point takes the other path and is installed, the way ;; A *declaration* before point takes the other path and is installed, the way
;; C-c C-c installs one. The key is dispatched on the form it would send, so ;; C-c C-c installs one. The key is dispatched on the form it would send, so
;; a `defvar' at the top of a file evaluates as a declaration while an ;; a `defonce' at the top of a file evaluates as a declaration while an
;; expression inside a `defn' body still evaluates as an expression. The ;; expression inside a `defn' body still evaluates as an expression. The
;; split used to be by keybinding, which meant a top-level `defvar' under ;; split used to be by keybinding, which meant a top-level `defonce' under
;; C-x C-e came back as "defvar is a top-level declaration, not an ;; C-x C-e came back as "defonce is a top-level declaration, not an
;; expression" — an editor artifact, not a limit of the compiler, which has ;; expression" — an editor artifact, not a limit of the compiler, which has
;; had both evaluators all along. ;; had both evaluators all along.
@ -1857,7 +1857,7 @@ Nothing is offered when nothing is known — an empty table would look like
in this program; C-c C-v describes it" (car d))) in this program; C-c C-v describes it" (car d)))
((equal (nth 3 d) "") ((equal (nth 3 d) "")
;; Tast.global and Tast.extern carry no Loc, so there is nothing to go ;; Tast.global and Tast.extern carry no Loc, so there is nothing to go
;; to. Guessing by searching for "(defvar ticks" would find the wrong ;; to. Guessing by searching for "(defonce ticks" would find the wrong
;; one in a program of several files, which is worse than refusing. ;; one in a program of several files, which is worse than refusing.
(user-error "flan: %s is a %s, and the daemon reports no location for one" (user-error "flan: %s is a %s, and the daemon reports no location for one"
(car d) (nth 1 d))) (car d) (nth 1 d)))
@ -2031,7 +2031,7 @@ someone editing Flan with no program running and this file never loaded."
;; silently, so every accepted evaluation reports what landed in the running ;; silently, so every accepted evaluation reports what landed in the running
;; program and what it cost. The names come from the reply rather than from ;; program and what it cost. The names come from the reply rather than from
;; what was typed: the daemon is the one that knows which of them it installed, ;; what was typed: the daemon is the one that knows which of them it installed,
;; and a `defvar' the program already had is not among them. ;; and a `defonce' the program already had is not among them.
(defun flan--names-phrase (names fallback) (defun flan--names-phrase (names fallback)
"NAMES as a phrase for the echo area, or FALLBACK when there are none. "NAMES as a phrase for the echo area, or FALLBACK when there are none.
@ -2091,7 +2091,7 @@ has nothing to sit beside."
;; everything the evaluation declared; a buffer of five functions ;; everything the evaluation declared; a buffer of five functions
;; and two vars should not report as "five". ;; and two vars should not report as "five".
;; ;;
;; A declaration with no body at all — a `defvar', a `defstruct' — ;; A declaration with no body at all — a `defonce', a `defstruct' —
;; has none of the first, and leading with WHAT then read as "form ;; has none of the first, and leading with WHAT then read as "form
;; installed in 4 ms (also ticks)": the one name that actually ;; installed in 4 ms (also ticks)": the one name that actually
;; changed, parenthesised as an afterthought, beside a label that ;; changed, parenthesised as an afterthought, beside a label that
@ -2266,7 +2266,7 @@ daemon reads that as stopping on entry instead."
;; and `package' is a single named exception rather than the edge of a subtler ;; and `package' is a single named exception rather than the edge of a subtler
;; rule that was never quite true. ;; rule that was never quite true.
(defconst flan--declaration-heads (defconst flan--declaration-heads
'("defmacro" "defn" "defvar" "defconst" '("defmacro" "defn" "def" "defonce" "defconst"
"defstruct" "defdata" "defunion" "defenum" "defalias" "defstruct" "defdata" "defunion" "defenum" "defalias"
;; The object and dispatch heads (lib/parse.ml:1277-1334), which are ;; The object and dispatch heads (lib/parse.ml:1277-1334), which are
;; declarations in exactly the way `defn' is: each introduces a top-level ;; declarations in exactly the way `defn' is: each introduces a top-level
@ -2378,7 +2378,7 @@ this draws on success would otherwise be competing with one drawn at line 1."
(defun flan-eval-defun (&optional arg) (defun flan-eval-defun (&optional arg)
"Evaluate the top-level form at point in the running program. "Evaluate the top-level form at point in the running program.
A declaration a `defn', a `defvar', anything in `flan--defun-heads' is A declaration a `defn', a `defonce', anything in `flan--defun-heads' is
recompiled and installed, which is what this key has always done. Anything recompiled and installed, which is what this key has always done. Anything
else is an expression, and is evaluated and its value shown, because a bare else is an expression, and is evaluated and its value shown, because a bare
`(+ 1 1)' written at the top of a file is a form like any other and refusing `(+ 1 1)' written at the top of a file is a form like any other and refusing
@ -2464,11 +2464,11 @@ arrive in the same load or the first refers to storage that does not exist."
"Evaluate the form before point in the running program and report it. "Evaluate the form before point in the running program and report it.
An expression is compiled into a thunk the program runs at its next frame An expression is compiled into a thunk the program runs at its next frame
boundary, and its value is shown. A top-level declaration a `defvar', a boundary, and its value is shown. A top-level declaration a `defonce', a
`defn', anything in `flan--declaration-heads' is compiled and installed `defn', anything in `flan--declaration-heads' is compiled and installed
instead, and the reply names what changed. The compiler has always had both instead, and the reply names what changed. The compiler has always had both
paths; this key used to reach only the first, so a `defvar' typed at the top paths; this key used to reach only the first, so a `defonce' typed at the top
of a file came back as \"defvar is a top-level declaration, not an of a file came back as \"defonce is a top-level declaration, not an
expression\" and the only way to evaluate it was `C-c C-c'. That was an expression\" and the only way to evaluate it was `C-c C-c'. That was an
editor artifact and not a property of the language. editor artifact and not a property of the language.

View File

@ -344,7 +344,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; A declaration the program already has installs nothing, and must say so ;; A declaration the program already has installs nothing, and must say so
;; rather than reporting a time for a build that did not happen. ;; rather than reporting a time for a build that did not happen.
(let ((said (test-flan--said (let ((said (test-flan--said
(flan--eval "(defvar ticks i64)" "form")))) (flan--eval "(defonce ticks i64)" "form"))))
(test-flan--check "an evaluation with nothing to install says so" (test-flan--check "an evaluation with nothing to install says so"
(and said (string-match-p "nothing to install" said) (and said (string-match-p "nothing to install" said)
(not (string-match-p "installed" said))))) (not (string-match-p "installed" said)))))
@ -482,9 +482,9 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; only looked at the head. No daemon round trip here — nothing is sent — ;; only looked at the head. No daemon round trip here — nothing is sent —
;; which is why all four are affordable. ;; which is why all four are affordable.
(goto-char (point-min)) (goto-char (point-min))
(search-forward "(defvar ticks i64)") (search-forward "(defonce ticks i64)")
(test-flan--check "a top-level defvar takes the declaration path" (test-flan--check "a top-level defonce takes the declaration path"
(equal (car (flan--declaration-before-point)) "defvar")) (equal (car (flan--declaration-before-point)) "defonce"))
(goto-char (point-min)) (goto-char (point-min))
(search-forward " ticks)") (search-forward " ticks)")
(test-flan--check "and so does a top-level defn" (test-flan--check "and so does a top-level defn"
@ -658,7 +658,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; back. Read off `Parse.decl', so the check is the derivation. ;; back. Read off `Parse.decl', so the check is the derivation.
(with-temp-buffer (with-temp-buffer
(flan-mode) (flan-mode)
(dolist (head '("defmacro" "defn" "defvar" "defconst" "defstruct" (dolist (head '("defmacro" "defn" "def" "defonce" "defconst" "defstruct"
"defdata" "defunion" "defenum" "defalias" "defdata" "defunion" "defenum" "defalias"
"defclass" "defgeneric" "defmulti" "defmethod" "defclass" "defgeneric" "defmulti" "defmethod"
"import" "declare" "declare-c")) "import" "declare" "declare-c"))
@ -748,7 +748,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; was the only thing being asked and a sentence would have been compiled. ;; was the only thing being asked and a sentence would have been compiled.
(goto-char (point-max)) (goto-char (point-max))
(let ((beg (point))) (let ((beg (point)))
(insert "\n;; (defvar commented i64 1)\n\"(defvar inside i64 1)\"") (insert "\n;; (defonce commented i64 1)\n\"(defonce inside i64 1)\"")
;; Point after the form's own closing paren rather than at the end of the ;; Point after the form's own closing paren rather than at the end of the
;; line: `backward-sexp' walks over a whole comment, so from the end of ;; line: `backward-sexp' walks over a whole comment, so from the end of
;; one it never reaches the paren inside it and the guard is never asked. ;; one it never reaches the paren inside it and the guard is never asked.
@ -777,15 +777,15 @@ already rely on it — so nothing here is a stand-in for the real thing."
(user-error (setq raised (error-message-string err)))) (user-error (setq raised (error-message-string err))))
(and raised (string-match-p "no form before point" raised)))) (and raised (string-match-p "no form before point" raised))))
;; The bug this key had: a `defvar' typed at the top of a file could only be ;; The bug this key had: a `defonce' typed at the top of a file could only be
;; evaluated with C-c C-c, because C-x C-e sent it to the expression ;; evaluated with C-c C-c, because C-x C-e sent it to the expression
;; evaluator and the parser refused it as a declaration. One round trip, on ;; evaluator and the parser refused it as a declaration. One round trip, on
;; the form the report has to be able to name. ;; the form the report has to be able to name.
(goto-char (point-max)) (goto-char (point-max))
(let ((beg (point))) (let ((beg (point)))
(insert "\n(defvar spark i64 9)") (insert "\n(defonce spark i64 9)")
(let ((said (test-flan--said (flan-eval-last-sexp)))) (let ((said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "C-x C-e on a top-level defvar installs it" (test-flan--check "C-x C-e on a top-level defonce installs it"
(and said (string-match-p "\\_<spark\\_>" said) (and said (string-match-p "\\_<spark\\_>" said)
(string-match-p "installed" said))) (string-match-p "installed" said)))
;; The two paths share a key now, so the echo area is the only thing ;; The two paths share a key now, so the echo area is the only thing
@ -795,18 +795,18 @@ already rely on it — so nothing here is a stand-in for the real thing."
(and said (not (string-match-p "=>" said))))) (and said (not (string-match-p "=>" said)))))
(delete-region beg (point-max))) (delete-region beg (point-max)))
;; And the literal complaint, on the `defvar' actually written in the file ;; And the literal complaint, on the `defonce' actually written in the file
;; rather than on one typed in for the occasion. What is asserted is only ;; rather than on one typed in for the occasion. What is asserted is only
;; what the client decides: the var is named and no value is printed. ;; what the client decides: the var is named and no value is printed.
;; Whether the daemon answers "installed" or "nothing to install" is its ;; Whether the daemon answers "installed" or "nothing to install" is its
;; call and depends on what this session has been through by now, and a test ;; call and depends on what this session has been through by now, and a test
;; that pinned one of them here would be testing the order of the checks ;; that pinned one of them here would be testing the order of the checks
;; above it. A defvar declares no functions either way, which is what makes ;; above it. A defonce declares no functions either way, which is what makes
;; the name — and not the kind of form — the subject of the sentence. ;; the name — and not the kind of form — the subject of the sentence.
(goto-char (point-min)) (goto-char (point-min))
(search-forward "(defvar ticks i64)") (search-forward "(defonce ticks i64)")
(let ((said (test-flan--said (flan-eval-last-sexp)))) (let ((said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "C-x C-e on the file's own defvar reports the var" (test-flan--check "C-x C-e on the file's own defonce reports the var"
(and said (string-match-p "\\_<ticks\\_>" said) (and said (string-match-p "\\_<ticks\\_>" said)
(not (string-match-p "=>" said))))) (not (string-match-p "=>" said)))))
@ -1536,7 +1536,8 @@ already rely on it — so nothing here is a stand-in for the real thing."
(with-temp-buffer (with-temp-buffer
(insert ";;;; A file with one of everything.\n" (insert ";;;; A file with one of everything.\n"
"(defstruct Missing [id i32])\n" "(defstruct Missing [id i32])\n"
"(defvar ticks i64)\n" "(defonce ticks i64)\n"
"(def speed 2)\n"
"(defconst limit i64 10)\n" "(defconst limit i64 10)\n"
"(declare later [] i64)\n" "(declare later [] i64)\n"
"(defn step [] i64\n" "(defn step [] i64\n"
@ -1557,6 +1558,8 @@ already rely on it — so nothing here is a stand-in for the real thing."
(test-flan--check "and both kinds of global" (test-flan--check "and both kinds of global"
(and (assoc "ticks" (funcall group "Variables")) (and (assoc "ticks" (funcall group "Variables"))
(assoc "limit" (funcall group "Variables")))) (assoc "limit" (funcall group "Variables"))))
(test-flan--check "and a def, which is a variable too"
(assoc "speed" (funcall group "Variables")))
;; A forward declaration is not a definition; listing it beside one ;; A forward declaration is not a definition; listing it beside one
;; would show the same name twice with nothing to tell them apart. ;; would show the same name twice with nothing to tell them apart.
(test-flan--check "and a declaration, said to be one" (test-flan--check "and a declaration, said to be one"
@ -1901,7 +1904,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; mark outlives the next command where a rejection does not. ;; mark outlives the next command where a rejection does not.
(with-temp-buffer (with-temp-buffer
(flan-mode) (flan-mode)
(insert "(defvar ticks i64)\n\n(defn step [] i64\n (set ticks (+ ticks 1))\n ticks)\n") (insert "(defonce ticks i64)\n\n(defn step [] i64\n (set ticks (+ ticks 1))\n ticks)\n")
(goto-char (point-min)) (goto-char (point-min))
(search-forward "(+ ticks 1)") (search-forward "(+ ticks 1)")
(goto-char (1- (match-end 0))) ; inside the (+ ...), before its ")" (goto-char (1- (match-end 0))) ; inside the (+ ...), before its ")"

View File

@ -8,7 +8,7 @@
;;;; acceptance table pins that layout with get-screen-to-world-2d, and this ;;;; acceptance table pins that layout with get-screen-to-world-2d, and this
;;;; is the same struct going into the call that actually draws with it. ;;;; is the same struct going into the call that actually draws with it.
;;;; ;;;;
;;;; The hundred buildings are a `defvar` of fixed arrays rather than a Vec: ;;;; The hundred buildings are a `defonce` of fixed arrays rather than a Vec:
;;;; a global cannot hold a Vec (docs/PORTING.md §3) and does not need to here, ;;;; a global cannot hold a Vec (docs/PORTING.md §3) and does not need to here,
;;;; because the count is a constant in the C too. `(array n T)` is the zeroed ;;;; because the count is a constant in the C too. `(array n T)` is the zeroed
;;;; fixed array, and the C's `= { 0 }` is exactly that. ;;;; fixed array, and the C's `= { 0 }` is exactly that.
@ -29,10 +29,10 @@
(defconst max-buildings 100) (defconst max-buildings 100)
(defvar buildings [100 rl/Rectangle]) (defonce buildings [100 rl/Rectangle])
(defvar build-colors [100 rl/Color]) (defonce build-colors [100 rl/Color])
(defvar player rl/Rectangle) (defonce player rl/Rectangle)
(defvar camera rl/Camera2D) (defonce camera rl/Camera2D)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -41,16 +41,16 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar camera rl/Camera3D) (defonce camera rl/Camera3D)
(defvar cube-position rl/Vector3) (defonce cube-position rl/Vector3)
(defvar cube-size rl/Vector3) (defonce cube-size rl/Vector3)
;; The picking ray, kept between frames because draw-ray draws it every frame ;; The picking ray, kept between frames because draw-ray draws it every frame
;; whether or not it hit anything — that is how the example shows where the ;; whether or not it hit anything — that is how the example shows where the
;; click went. ;; click went.
(defvar ray rl/Ray) (defonce ray rl/Ray)
(defvar collision rl/RayCollision) (defonce collision rl/RayCollision)
;; The same centre/size to min/max conversion as in ;; The same centre/size to min/max conversion as in
;; examples/models-box-collisions.flan. Written out here rather than shared ;; examples/models-box-collisions.flan. Written out here rather than shared

View File

@ -19,7 +19,7 @@
;;;; here rather than instead of it, so the file is not quietly asserting that ;;;; here rather than instead of it, so the file is not quietly asserting that
;;;; the original was right. ;;;; the original was right.
;;;; ;;;;
;;;; The two circle positions are `defvar`s for the usual reason: they are ;;;; The two circle positions are `defonce`s for the usual reason: they are
;;;; state between frames and a `let` inside the loop would reset them. ;;;; state between frames and a `let` inside the loop would reset them.
(import rl "vendor:raylib") (import rl "vendor:raylib")
@ -31,9 +31,9 @@
(defconst speed f32 10.0) (defconst speed f32 10.0)
(defconst circle-radius f32 32.0) (defconst circle-radius f32 32.0)
(defvar delta-circle rl/Vector2) (defonce delta-circle rl/Vector2)
(defvar frame-circle rl/Vector2) (defonce frame-circle rl/Vector2)
(defvar current-fps i32) (defonce current-fps i32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -47,7 +47,7 @@
(defconst stick-deadzone f32 0.1) (defconst stick-deadzone f32 0.1)
(defconst trigger-deadzone f32 -0.9) (defconst trigger-deadzone f32 -0.9)
(defvar gamepad i32) (defonce gamepad i32)
;; The C's deadzone test, which is a band around zero and not a clamp: inside ;; The C's deadzone test, which is a band around zero and not a clamp: inside
;; it the axis reads exactly 0, outside it the raw value passes through ;; it the axis reads exactly 0, outside it the raw value passes through

View File

@ -48,9 +48,9 @@
;;;; ;;;;
;;;; **No local fixed arrays.** The C declares `char gestureLog[20][12]` and ;;;; **No local fixed arrays.** The C declares `char gestureLog[20][12]` and
;;;; `Vector2 touchPosition[32]` inside main. A `let` binding takes no type ;;;; `Vector2 touchPosition[32]` inside main. A `let` binding takes no type
;;;; annotation, so a fixed array can only be a top-level `defvar` or a literal ;;;; annotation, so a fixed array can only be a top-level `defonce` or a literal
;;;; with every element written out — thirty-two Vector2s, here. They are ;;;; with every element written out — thirty-two Vector2s, here. They are
;;;; `defvar`s, which is what the C's storage amounts to anyway. ;;;; `defonce`s, which is what the C's storage amounts to anyway.
;;;; ;;;;
;;;; The log itself came out simpler than the C's: the names are compile-time ;;;; The log itself came out simpler than the C's: the names are compile-time
;;;; literals, so a slot holds a `string` and there is no TextCopy and no ;;;; literals, so a slot holds a `string` and there is no TextCopy and no
@ -67,18 +67,18 @@
(defconst gesture-log-size 20) (defconst gesture-log-size 20)
(defconst max-touch-count 32) (defconst max-touch-count 32)
(defvar gesture-log [gesture-log-size string]) (defonce gesture-log [gesture-log-size string])
;; The C's inverted circular queue: the index counts DOWN and wraps at the top, ;; The C's inverted circular queue: the index counts DOWN and wraps at the top,
;; so the newest entry is always at gesture-log-index and the draw loop walks ;; so the newest entry is always at gesture-log-index and the draw loop walks
;; forward from there. Starting at the size rather than at size-1 is the C's ;; forward from there. Starting at the size rather than at size-1 is the C's
;; too — the first write decrements before storing. ;; too — the first write decrements before storing.
(defvar gesture-log-index i32) (defonce gesture-log-index i32)
(defvar previous-gesture rl/Gesture) (defonce previous-gesture rl/Gesture)
(defvar last-gesture rl/Gesture) (defonce last-gesture rl/Gesture)
(defvar gesture-color rl/Color) (defonce gesture-color rl/Color)
(defvar log-mode i32) (defonce log-mode i32)
(defvar current-angle f32) (defonce current-angle f32)
(defvar touch-positions [max-touch-count rl/Vector2]) (defonce touch-positions [max-touch-count rl/Vector2])
;; ── The comparisons the C makes on the raw bitfield ───────────────── ;; ── The comparisons the C makes on the raw bitfield ─────────────────
;; ;;

View File

@ -18,7 +18,7 @@
;;;; catch. ;;;; catch.
;;;; ;;;;
;;;; A slot that has not been written yet holds a zero-length string, because a ;;;; A slot that has not been written yet holds a zero-length string, because a
;;;; `defvar` with no initialiser is all-bytes-zero and a string is ptr+len — ;;;; `defonce` with no initialiser is all-bytes-zero and a string is ptr+len —
;;;; a null pointer with a length of 0. draw-text draws nothing for it. That is ;;;; a null pointer with a length of 0. draw-text draws nothing for it. That is
;;;; the C's `{ "" }` initialiser arriving by a different route, and it is why ;;;; the C's `{ "" }` initialiser arriving by a different route, and it is why
;;;; resetting the log below only has to reset the counter. ;;;; resetting the log below only has to reset the counter.
@ -30,10 +30,10 @@
(defconst max-gesture-strings 20) (defconst max-gesture-strings 20)
(defvar gesture-log [max-gesture-strings string]) (defonce gesture-log [max-gesture-strings string])
(defvar gestures-count i32) (defonce gestures-count i32)
(defvar current-gesture rl/Gesture) (defonce current-gesture rl/Gesture)
(defvar last-gesture rl/Gesture) (defonce last-gesture rl/Gesture)
;; The C's switch, as a function. `:else` is its `default:` — an unnamed ;; The C's switch, as a function. `:else` is its `default:` — an unnamed
;; gesture logs the empty string, which draws nothing, which is what falling ;; gesture logs the empty string, which draws nothing, which is what falling

View File

@ -5,7 +5,7 @@
;;;; ;;;;
;;;; The C moves the ball by writing `ballPosition.x += 2.0f` on a local ;;;; The C moves the ball by writing `ballPosition.x += 2.0f` on a local
;;;; struct. Flan has the same thing — a local IS an assignable place ;;;; struct. Flan has the same thing — a local IS an assignable place
;;;; (spec-memory.md) — but the local has to be a `defvar` here rather than a ;;;; (spec-memory.md) — but the local has to be a `defonce` here rather than a
;;;; `let` inside the loop, because a `let` binding is rebound every iteration ;;;; `let` inside the loop, because a `let` binding is rebound every iteration
;;;; and the position has to survive between frames. The C's variable is ;;;; and the position has to survive between frames. The C's variable is
;;;; outside its while loop for the same reason; this is that, spelled with the ;;;; outside its while loop for the same reason; this is that, spelled with the
@ -19,7 +19,7 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar ball rl/Vector2) (defonce ball rl/Vector2)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -25,7 +25,7 @@
(defconst scroll-speed 4) (defconst scroll-speed 4)
(defvar box-y i32) (defonce box-y i32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -9,7 +9,7 @@
;;;; matters in both — two buttons pressed on the same frame give the earlier ;;;; matters in both — two buttons pressed on the same frame give the earlier
;;;; one, which is the C's behaviour and not an accident of the port. ;;;; one, which is the C's behaviour and not an accident of the port.
;;;; ;;;;
;;;; The colour has to be a `defvar` rather than a `let`, for the reason ;;;; The colour has to be a `defonce` rather than a `let`, for the reason
;;;; core-input-keys' position does: it is state between frames. ;;;; core-input-keys' position does: it is state between frames.
(import rl "vendor:raylib") (import rl "vendor:raylib")
@ -17,7 +17,7 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar ball-color rl/Color) (defonce ball-color rl/Color)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -10,7 +10,7 @@
;;;; assigned a whole struct. Nothing in the repository used one before, so it ;;;; assigned a whole struct. Nothing in the repository used one before, so it
;;;; is worth saying that it does. ;;;; is worth saying that it does.
;;;; ;;;;
;;;; It is a top-level `defvar` and not a local, which is NOT a stylistic ;;;; It is a top-level `defonce` and not a local, which is NOT a stylistic
;;;; choice. A `let` binding takes no type annotation, so the only way to make ;;;; choice. A `let` binding takes no type annotation, so the only way to make
;;;; a fixed array inside a function is to initialise it from a literal with ;;;; a fixed array inside a function is to initialise it from a literal with
;;;; every element written out — ten `(rl/Vector2 {.x 0.0 .y 0.0})`s here, and ;;;; every element written out — ten `(rl/Vector2 {.x 0.0 .y 0.0})`s here, and
@ -29,7 +29,7 @@
(defconst max-touch-points 10) (defconst max-touch-points 10)
(defvar touch-positions [max-touch-points rl/Vector2]) (defonce touch-positions [max-touch-points rl/Vector2])
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -33,7 +33,7 @@
;;;; ;;;;
;;;; The button geometry is `defconst` arrays of struct literals. That works — ;;;; The button geometry is `defconst` arrays of struct literals. That works —
;;;; and it is the only way a fixed array can be made inside anything but a ;;;; and it is the only way a fixed array can be made inside anything but a
;;;; top-level `defvar`, since a `let` binding takes no type annotation. ;;;; top-level `defonce`, since a `let` binding takes no type annotation.
(import rl "vendor:raylib") (import rl "vendor:raylib")
@ -67,7 +67,7 @@
;;;; ── The part with no raylib in it ────────────────────────────────── ;;;; ── The part with no raylib in it ──────────────────────────────────
(defvar player rl/Vector2) (defonce player rl/Vector2)
(defn reset-player [] () (defn reset-player [] ()
(set player (rl/Vector2 {.x (/ (f32 screen-width) 2.0) (set player (rl/Vector2 {.x (/ (f32 screen-width) 2.0)

View File

@ -29,8 +29,8 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar scissor rl/Rectangle) (defonce scissor rl/Rectangle)
(defvar scissor-mode bool) (defonce scissor-mode bool)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -38,9 +38,9 @@
;; window somewhere it cannot be typed at, so the only way back is a count. ;; window somewhere it cannot be typed at, so the only way back is a count.
(defconst restore-after 240) (defconst restore-after 240)
(defvar ball-pos rl/Vector2) (defonce ball-pos rl/Vector2)
(defvar ball-speed rl/Vector2) (defonce ball-speed rl/Vector2)
(defvar frames i32) (defonce frames i32)
(defconst ball-radius f32 20.0) (defconst ball-radius f32 20.0)

View File

@ -12,7 +12,7 @@
;;;; flag raylib latches: it is "the close button was clicked, or the exit key ;;;; flag raylib latches: it is "the close button was clicked, or the exit key
;;;; is down", recomputed each frame, and it goes back to false on its own. ;;;; is down", recomputed each frame, and it goes back to false on its own.
;;;; That is what lets this program ask for confirmation and then carry on — ;;;; That is what lets this program ask for confirmation and then carry on —
;;;; and it is also why the loop is driven by a `defvar` of its own rather ;;;; and it is also why the loop is driven by a `defonce` of its own rather
;;;; than by the predicate, which is the one structural difference from every ;;;; than by the predicate, which is the one structural difference from every
;;;; other example in this directory. ;;;; other example in this directory.
;;;; ;;;;
@ -25,8 +25,8 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar exit-requested bool) (defonce exit-requested bool)
(defvar exiting bool) (defonce exiting bool)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -39,8 +39,8 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar camera rl/Camera3D) (defonce camera rl/Camera3D)
(defvar cube rl/Vector3) (defonce cube rl/Vector3)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -38,15 +38,15 @@
(defconst screen-width 800) (defconst screen-width 800)
(defconst screen-height 450) (defconst screen-height 450)
(defvar camera rl/Camera3D) (defonce camera rl/Camera3D)
(defvar player-position rl/Vector3) (defonce player-position rl/Vector3)
(defvar player-size rl/Vector3) (defonce player-size rl/Vector3)
(defvar player-color rl/Color) (defonce player-color rl/Color)
(defvar enemy-box-pos rl/Vector3) (defonce enemy-box-pos rl/Vector3)
(defvar enemy-box-size rl/Vector3) (defonce enemy-box-size rl/Vector3)
(defvar enemy-sphere-pos rl/Vector3) (defonce enemy-sphere-pos rl/Vector3)
(defconst enemy-sphere-size f32 1.5) (defconst enemy-sphere-size f32 1.5)

View File

@ -38,7 +38,7 @@
(defconst col-mid 400) (defconst col-mid 400)
(defconst col-right 600) (defconst col-right 600)
(defvar rotation f32) (defonce rotation f32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -28,12 +28,12 @@
;; not allowed above. ;; not allowed above.
(defconst screen-upper-limit 40) (defconst screen-upper-limit 40)
(defvar box-a rl/Rectangle) ; moves by itself, bounces off the sides (defonce box-a rl/Rectangle) ; moves by itself, bounces off the sides
(defvar box-b rl/Rectangle) ; follows the mouse (defonce box-b rl/Rectangle) ; follows the mouse
(defvar box-collision rl/Rectangle) ; their overlap, valid only while touching (defonce box-collision rl/Rectangle) ; their overlap, valid only while touching
(defvar box-a-speed-x i32) (defonce box-a-speed-x i32)
(defvar paused bool) (defonce paused bool)
(defvar collision bool) (defonce collision bool)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -45,10 +45,10 @@
(defconst sclera-radius f32 80.0) (defconst sclera-radius f32 80.0)
(defconst iris-radius f32 24.0) (defconst iris-radius f32 24.0)
(defvar sclera-left rl/Vector2) (defonce sclera-left rl/Vector2)
(defvar sclera-right rl/Vector2) (defonce sclera-right rl/Vector2)
(defvar iris-left rl/Vector2) (defonce iris-left rl/Vector2)
(defvar iris-right rl/Vector2) (defonce iris-right rl/Vector2)
;; One eye's pupil, given where the mouse is and where the eye is. Answers the ;; One eye's pupil, given where the mouse is and where the eye is. Answers the
;; mouse position unchanged while it is inside the eye, and the point on the ;; mouse position unchanged while it is inside the eye, and the point on the

View File

@ -90,8 +90,8 @@
;; ceiling on: this is the number that decides how big the font texture gets. ;; ceiling on: this is the number that decides how big the font texture gets.
(defconst max-codepoints 128) (defconst max-codepoints 128)
(defvar unique-codepoints [max-codepoints i32]) (defonce unique-codepoints [max-codepoints i32])
(defvar unique-count i32) (defonce unique-count i32)
;; Is `cp` already in the first `n` of the table? ;; Is `cp` already in the first `n` of the table?
(defn seen? [cp i32 n i32] bool (defn seen? [cp i32 n i32] bool
@ -174,15 +174,15 @@
(do (rl/get-codepoint-previous b off (addr size)) (do (rl/get-codepoint-previous b off (addr size))
(if (< (- off size) 0) 0 (- off size)))))) (if (< (- off size) 0) 0 (- off size))))))
(defvar font rl/Font) (defonce font rl/Font)
;; Whether the TTF was there, asked once. A `defvar` and not the call itself ;; Whether the TTF was there, asked once. A `defonce` and not the call itself
;; in the draw loop: path-file? is a stat, and a syscall per frame to answer a ;; in the draw loop: path-file? is a stat, and a syscall per frame to answer a
;; question whose answer cannot change while the program runs is exactly what ;; question whose answer cannot change while the program runs is exactly what
;; the per-frame rule in docs/PORTING.md is about. ;; the per-frame rule in docs/PORTING.md is about.
(defvar font-present bool) (defonce font-present bool)
(defvar show-font-atlas bool) (defonce show-font-atlas bool)
(defvar cursor i32) (defonce cursor i32)
(defvar codepoint-count i32) (defonce codepoint-count i32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -52,12 +52,12 @@
(defconst max-input-chars 9) (defconst max-input-chars 9)
;; No +1: see the header comment. There is no NUL to leave room for. ;; No +1: see the header comment. There is no NUL to leave room for.
(defvar name [9 u8]) (defonce name [9 u8])
(defvar letter-count i32) (defonce letter-count i32)
(defvar text-box rl/Rectangle) (defonce text-box rl/Rectangle)
(defvar mouse-on-text bool) (defonce mouse-on-text bool)
(defvar frames-counter i32) (defonce frames-counter i32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -40,7 +40,7 @@
(defconst message (defconst message
"This sample illustrates a text writing\nanimation effect! Check it out! ;)") "This sample illustrates a text writing\nanimation effect! Check it out! ;)")
(defvar frames-counter i32) (defonce frames-counter i32)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -58,18 +58,18 @@
(defconst tiles-y 15) (defconst tiles-y 15)
;; 0 or 1, picked once: which of the two blues a tile is drawn in. ;; 0 or 1, picked once: which of the two blues a tile is drawn in.
(defvar tile-ids [375 u8]) (defonce tile-ids [375 u8])
;; 0 = never seen (solid black), 1 = visible now (no fog), 2 = seen before ;; 0 = never seen (solid black), 1 = visible now (no fog), 2 = seen before
;; (mostly black). The three-way state is why this is a byte per tile and not ;; (mostly black). The three-way state is why this is a byte per tile and not
;; a bit. ;; a bit.
(defvar tile-fog [375 u8]) (defonce tile-fog [375 u8])
(defvar player-position rl/Vector2) (defonce player-position rl/Vector2)
(defvar player-tile-x i32) (defonce player-tile-x i32)
(defvar player-tile-y i32) (defonce player-tile-y i32)
(defvar fog-of-war rl/RenderTexture2D) (defonce fog-of-war rl/RenderTexture2D)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -37,8 +37,8 @@
;; same generator with a different `direction`. ;; same generator with a different `direction`.
(defconst num-textures 9) (defconst num-textures 9)
(defvar textures [9 rl/Texture2D]) (defonce textures [9 rl/Texture2D])
(defvar current-texture i32) (defonce current-texture i32)
;; Generate on the CPU, upload, drop the pixels. The texture holds a GL name ;; Generate on the CPU, upload, drop the pixels. The texture holds a GL name
;; and nothing of the Image, so the CPU copy can go as soon as the upload is ;; and nothing of the Image, so the CPU copy can go as soon as the upload is

View File

@ -80,10 +80,10 @@
(defconst proc-flip-vertical 7) (defconst proc-flip-vertical 7)
(defconst proc-flip-horizontal 8) (defconst proc-flip-horizontal 8)
(defvar process-names [num-processes string]) (defonce process-names [num-processes string])
;; The nine toggle buttons down the left-hand side, laid out once at startup. ;; The nine toggle buttons down the left-hand side, laid out once at startup.
(defvar toggle-recs [num-processes rl/Rectangle]) (defonce toggle-recs [num-processes rl/Rectangle])
;; The generated picture's size. Small on purpose — see the header comment. ;; The generated picture's size. Small on purpose — see the header comment.
(defconst source-width 200) (defconst source-width 200)
@ -127,11 +127,11 @@
(= which proc-flip-vertical) (rl/image-flip-vertical img) (= which proc-flip-vertical) (rl/image-flip-vertical img)
(= which proc-flip-horizontal) (rl/image-flip-horizontal img))) (= which proc-flip-horizontal) (rl/image-flip-horizontal img)))
(defvar texture rl/Texture2D) (defonce texture rl/Texture2D)
(defvar im-origin rl/Image) (defonce im-origin rl/Image)
(defvar im-copy rl/Image) (defonce im-copy rl/Image)
(defvar current-process i32) (defonce current-process i32)
(defvar mouse-hover-rec i32) (defonce mouse-hover-rec i32)
;; Throw the working copy away, take a fresh one from the original, run the ;; Throw the working copy away, take a fresh one from the original, run the
;; filter over it, and push the result at the texture that is already on the ;; filter over it, and push the result at the texture that is already on the

View File

@ -45,21 +45,21 @@
;; The palette strip along the top. colors[0] is also the canvas's clear ;; The palette strip along the top. colors[0] is also the canvas's clear
;; colour and the "eraser", which is why raywhite is first and not simply ;; colour and the "eraser", which is why raywhite is first and not simply
;; another entry. ;; another entry.
(defvar colors [23 rl/Color]) (defonce colors [23 rl/Color])
(defvar colors-recs [23 rl/Rectangle]) (defonce colors-recs [23 rl/Rectangle])
(defvar color-selected i32) (defonce color-selected i32)
(defvar color-selected-prev i32) (defonce color-selected-prev i32)
(defvar color-mouse-hover i32) (defonce color-mouse-hover i32)
(defvar brush-size f32) (defonce brush-size f32)
(defvar mouse-was-pressed bool) (defonce mouse-was-pressed bool)
(defvar btn-save-rec rl/Rectangle) (defonce btn-save-rec rl/Rectangle)
(defvar btn-save-mouse-hover bool) (defonce btn-save-mouse-hover bool)
(defvar show-save-message bool) (defonce show-save-message bool)
(defvar save-message-counter i32) (defonce save-message-counter i32)
(defvar target rl/RenderTexture2D) (defonce target rl/RenderTexture2D)
(defn main [] () (defn main [] ()
(rl/init-window screen-width screen-height (rl/init-window screen-width screen-height

View File

@ -241,8 +241,16 @@ and decl_kind =
(* Inline name/value pairs, as everywhere else. The members are what a (* Inline name/value pairs, as everywhere else. The members are what a
keyword at a call site resolves against. *) keyword at a call site resolves against. *)
| Defenum of string * (string * int64) list | Defenum of string * (string * int64) list
(* value is optional: ZII. `uninit` opts out and is recorded as Uninit. *) (* value is optional: ZII. `uninit` opts out and is recorded as Uninit.
| Defvar of string * texpr option * init One constructor for the two defining forms that declare a mutable
global, told apart by the [reinit]: [defonce] is [Once] its
initialiser runs only if the global is not already initialised, so the
value survives a daemon re-run and [def] is [Every], Common Lisp's
defparameter: the initialiser runs on every re-run, so an edited one
takes effect on the next C-c C-c + re-run. They share everything else
the spellings, the collision rules, the lowering which is why the
difference is a field and not a second constructor. *)
| Defvar of string * texpr option * init * reinit
| Defconst of string * texpr option * expr | Defconst of string * texpr option * expr
(* ── The dyn side's classes and generic functions ────────────────── (* ── The dyn side's classes and generic functions ──────────────────
None of these four reaches [Check]. [Classes.expand] turns the whole set None of these four reaches [Check]. [Classes.expand] turns the whole set
@ -293,7 +301,7 @@ and methd = { mgen : string; mkey : dispatch; mfn : fn; mkloc : Loc.t }
and variant = { vname : string; vfields : field list; vloc : Loc.t } and variant = { vname : string; vfields : field list; vloc : Loc.t }
(* [Ambiguous] is the three-element [(defvar x foo)] and [(defvar x (f y))]: (* [Ambiguous] is the three-element [(defonce x foo)] and [(defonce x (f y))]:
forms whose third element parses as a type *and* as an expression, so which forms whose third element parses as a type *and* as an expression, so which
one it is cannot be decided until names exist. The [texpr] beside it in one it is cannot be decided until names exist. The [texpr] beside it in
[Defvar] is the type reading and this is the value reading; [Check.collect] [Defvar] is the type reading and this is the value reading; [Check.collect]
@ -303,6 +311,11 @@ and variant = { vname : string; vfields : field list; vloc : Loc.t }
these. *) these. *)
and init = Zeroed | Uninit | Init of expr | Ambiguous of expr and init = Zeroed | Uninit | Init of expr | Ambiguous of expr
(* What a daemon re-run does to the global: [Once] is [defonce] — initialise
if not already initialised, keep the value otherwise and [Every] is
[def], which runs its initialiser on every re-run. *)
and reinit = Once | Every
(* Every top-level name a declaration introduces, whatever kind it is. There is (* Every top-level name a declaration introduces, whatever kind it is. There is
one top-level namespace, so this is both the set [Load] renames on an import one top-level namespace, so this is both the set [Load] renames on an import
and the set [Check] refuses to see twice one definition, so the two cannot and the set [Check] refuses to see twice one definition, so the two cannot
@ -333,7 +346,7 @@ let method_name (m : methd) = m.mgen ^ "@" ^ dispatch_text m.mkey
let declared_name (d : decl) = let declared_name (d : decl) =
match d.d with match d.d with
| Defenum (n, _) | Defalias (n, _) | Defstruct (n, _) | Defdata (n, _) | Defenum (n, _) | Defalias (n, _) | Defstruct (n, _) | Defdata (n, _)
| Defunion (n, _) | Defvar (n, _, _) | Defconst (n, _, _) | Defunion (n, _) | Defvar (n, _, _, _) | Defconst (n, _, _)
| Defclass (n, _) -> Some n | Defclass (n, _) -> Some n
| Declare (fn, _) | DeclareC (fn, _) | Defn fn | Declare (fn, _) | DeclareC (fn, _) | Defn fn
| Defgeneric fn | Defmulti fn -> Some fn.name | Defgeneric fn | Defmulti fn -> Some fn.name
@ -452,12 +465,12 @@ let mark_pause ~line ~col (ds : decl list) : decl list option =
| Defmethod m -> | Defmethod m ->
{ d with d = Defmethod { m with mfn = { m.mfn with fbody = body m.mfn.fbody } } } { d with d = Defmethod { m with mfn = { m.mfn with fbody = body m.mfn.fbody } } }
| Defmulti f -> { d with d = Defmulti { f with fbody = body f.fbody } } | Defmulti f -> { d with d = Defmulti { f with fbody = body f.fbody } }
| Defvar (n, t, Init e) -> { d with d = Defvar (n, t, Init (walk e)) } | Defvar (n, t, Init e, k) -> { d with d = Defvar (n, t, Init (walk e), k) }
(* The value reading of an undecided [defvar] is walked too: if it is the (* The value reading of an undecided [defonce] is walked too: if it is the
one that wins it is an initialiser like any other, and if the type one that wins it is an initialiser like any other, and if the type
reading wins the expression is dropped whole and the mark with it. *) reading wins the expression is dropped whole and the mark with it. *)
| Defvar (n, t, Ambiguous e) -> | Defvar (n, t, Ambiguous e, k) ->
{ d with d = Defvar (n, t, Ambiguous (walk e)) } { d with d = Defvar (n, t, Ambiguous (walk e), k) }
| Defconst (n, t, e) -> { d with d = Defconst (n, t, walk e) } | Defconst (n, t, e) -> { d with d = Defconst (n, t, walk e) }
| _ -> d | _ -> d
in in

View File

@ -833,7 +833,7 @@ let map_type ?(preds = []) loc (k : Types.t) (v : Types.t) =
they are all the same position: something zeroes it. they are all the same position: something zeroes it.
ZII is the language's rule an omitted struct field, a fixed array's ZII is the language's rule an omitted struct field, a fixed array's
elements, a [defvar] with no initialiser are all all-bytes-zero and a elements, a [defonce] with no initialiser are all all-bytes-zero and a
zeroed function value is a null pointer with a signature on it, which is the zeroed function value is a null pointer with a signature on it, which is the
one kind of zero that cannot be used for anything. Every other type's zero one kind of zero that cannot be used for anything. Every other type's zero
is a value: 0, false, an empty slice, [None], a data type's first case. So these is a value: 0, false, an empty slice, [None], a data type's first case. So these
@ -986,7 +986,7 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
is no longer a typo, it is a guess. *) is no longer a typo, it is a guess. *)
and near_miss env ?(also = []) n = and near_miss env ?(also = []) n =
(* [also] widens the candidate list past the types, and exactly one caller (* [also] widens the candidate list past the types, and exactly one caller
passes it: the defvar whose third element has to be a type *or* a value, passes it: the defonce whose third element has to be a type *or* a value,
whose suggestion is worth nothing if it can only ever name a type. *) whose suggestion is worth nothing if it can only ever name a type. *)
let candidates = let candidates =
also also
@ -1256,24 +1256,24 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list =
| _ -> d) | _ -> d)
decls decls
(* ── The third element of a defvar, decided ──────────────────────────── (* ── The third element of a defonce, decided ────────────────────────────
The author's rule, 2026-09-20: "if it's 3 atoms then it's dyn", and The author's rule, 2026-09-20: "if it's 3 atoms then it's dyn", and
"dispatch the if it's a type do the right thing". [(defvar current-color "dispatch the if it's a type do the right thing". [(defonce current-color
i32)] is the zeroed static it has always been, and [(defvar score 0)] is a i32)] is the zeroed static it has always been, and [(defonce score 0)] is a
dyn global holding 0 the same thing [(defvar score dyn 0)] spells out, dyn global holding 0 the same thing [(defonce score dyn 0)] spells out,
lowered by the same path and not by a second one. lowered by the same path and not by a second one.
[Parse] settled every shape a shape can settle and handed the rest over [Parse] settled every shape a shape can settle and handed the rest over
carrying both readings ([Ast.Ambiguous], beside the type reading in the carrying both readings ([Ast.Ambiguous], beside the type reading in the
same [Defvar]). What is left is the two forms only a name can settle, and same [Defvar]). What is left is the two forms only a name can settle, and
this is the first point where every type name is in hand: the same point this is the first point where every type name is in hand: the same point
[pair_params] reads, for the same reason a defvar may name a struct [pair_params] reads, for the same reason a defonce may name a struct
declared fifty lines below it. declared fifty lines below it.
The type reading wins wherever there is one. That is what keeps today's The type reading wins wherever there is one. That is what keeps today's
programs meaning today's thing: [(defvar p Point)] is a zeroed [Point], programs meaning today's thing: [(defonce p Point)] is a zeroed [Point],
[(defvar v (Vec i32))] is a zeroed [Vec], and a wrong type argument inside [(defonce v (Vec i32))] is a zeroed [Vec], and a wrong type argument inside
one stays a type error rather than becoming an unknown function. It is also one stays a type error rather than becoming an unknown function. It is also
why a built-in constructor is checked by name rather than by whether why a built-in constructor is checked by name rather than by whether
[resolve] happens to accept it [(Vec i32 i32)] is a malformed [Vec] and [resolve] happens to accept it [(Vec i32 i32)] is a malformed [Vec] and
@ -1307,14 +1307,14 @@ let defvar_reads_as_type env (t : Ast.texpr) =
name; and a plain type typo, where a confident one-edit suggestion turns a name; and a plain type typo, where a confident one-edit suggestion turns a
one-line answer into four lines of unrelated reading. The paragraph is for one-line answer into four lines of unrelated reading. The paragraph is for
the name that genuinely could have been either and is neither. *) the name that genuinely could have been either and is neither. *)
let defvar_neither env loc gname n ~values ~cases = let defvar_neither env loc ~form gname n ~values ~cases =
(match List.assoc_opt n cases with (match List.assoc_opt n cases with
| Some dname -> | Some dname ->
Loc.failk "check/defvar-case-not-type" loc Loc.failk "check/defvar-case-not-type" loc
"%s is a case of the data type %s, and a case is not a type of its \ "%s is a case of the data type %s, and a case is not a type of its \
own the global's type is the data type: (defvar %s %s). Assign the \ own the global's type is the data type: (%s %s %s). Assign the \
case you want, as (set %s (%s.%s {.field value ...}))" case you want, as (set %s (%s.%s {.field value ...}))"
n dname gname dname gname dname n n dname form gname dname gname dname n
| None -> ()); | None -> ());
(match foreign_spelling n with (match foreign_spelling n with
| Some m -> | Some m ->
@ -1330,15 +1330,15 @@ let defvar_neither env loc gname n ~values ~cases =
| None -> "" | None -> ""
in in
Loc.failk "check/defvar-neither-type-nor-value" loc Loc.failk "check/defvar-neither-type-nor-value" loc
"%s is neither a type nor a value, and the third element of a defvar has \ "%s is neither a type nor a value, and the third element of a %s has \
to be one or the other: a type there declares a zeroed global of that \ to be one or the other: a type there declares a zeroed global of that \
type (defvar %s i64) and a value there declares a dyn global holding \ type (%s %s i64) and a value there declares a dyn global holding \
it (defvar %s 0). Nothing named %s is declared as either%s" it (%s %s 0). Nothing named %s is declared as either%s"
n gname gname n hint n form form gname form gname n hint
(* Every name a value could be written under, which is every declaration that (* Every name a value could be written under, which is every declaration that
is not a type plus whatever a session already has. The list is only ever is not a type plus whatever a session already has. The list is only ever
asked "is this name declared at all", so a global that is itself a defvar asked "is this name declared at all", so a global that is itself a defonce
still undecided belongs on it: what it resolves to is the next pass's still undecided belongs on it: what it resolves to is the next pass's
question, not this one's. *) question, not this one's. *)
(* Case name -> the data type it belongs to, read off the declarations rather (* Case name -> the data type it belongs to, read off the declarations rather
@ -1361,7 +1361,7 @@ let value_names env (decls : Ast.decl list) =
List.filter_map List.filter_map
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with match d.Ast.d with
| Ast.Defvar (n, _, _) | Ast.Defconst (n, _, _) -> Some n | Ast.Defvar (n, _, _, _) | Ast.Defconst (n, _, _) -> Some n
| Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) -> | Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) ->
Some fn.Ast.name Some fn.Ast.name
| _ -> None) | _ -> None)
@ -1371,21 +1371,21 @@ let value_names env (decls : Ast.decl list) =
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.globals [] @ Hashtbl.fold (fun k _ acc -> k :: acc) env.globals []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.fns [] @ Hashtbl.fold (fun k _ acc -> k :: acc) env.fns []
(* The decision, applied: an undecided defvar leaves this pass as one of the (* The decision, applied: an undecided defonce leaves this pass as one of the
two forms that already existed, so no pass after it the signature loop two forms that already existed, so no pass after it the signature loop
below, [check_global], either backend has a third case to know about. The below, [check_global], either backend has a third case to know about. The
dyn reading is rewritten into exactly [(defvar x dyn <expr>)], which is the dyn reading is rewritten into exactly [(defonce x dyn <expr>)], which is the
whole of "it lowers to the same thing": the startup lifting, the re-run whole of "it lowers to the same thing": the startup lifting, the re-run
guard and the collector root are the ones that form already had. *) guard and the collector root are the ones that form already had. *)
(* A bracket form whose element names a value. [(defvar g [a b])] parses as a (* A bracket form whose element names a value. [(defonce g [a b])] parses as a
type and stays one type wins wherever there is a type reading, which is type and stays one type wins wherever there is a type reading, which is
the rule so the element had to name an element type, and [b] names a the rule so the element had to name an element type, and [b] names a
defvar. Left alone this reaches [resolve_name], where a lowercase name that defonce. Left alone this reaches [resolve_name], where a lowercase name that
is no type is a type variable, and the answer is a paragraph about generic is no type is a type variable, and the answer is a paragraph about generic
code the writer was not asking for. code the writer was not asking for.
Both readings, and both spellings, at the element that decided it. The dyn Both readings, and both spellings, at the element that decided it. The dyn
spelling is the one that actually works: [(defvar g dyn [a b])] is a dyn spelling is the one that actually works: [(defonce g dyn [a b])] is a dyn
global holding a vector, which is what the brackets meant to whoever wrote global holding a vector, which is what the brackets meant to whoever wrote
them. *) them. *)
let rec bracket_value_element env values (t : Ast.texpr) = let rec bracket_value_element env values (t : Ast.texpr) =
@ -1407,35 +1407,37 @@ let settle_defvars env (decls : Ast.decl list) : Ast.decl list =
[Zeroed] outright, because a bracket that parses as a type has no second [Zeroed] outright, because a bracket that parses as a type has no second
reading to carry. So the element check runs on both, and it is the only reading to carry. So the element check runs on both, and it is the only
thing the [Zeroed] arm does. *) thing the [Zeroed] arm does. *)
let brackets gname (t : Ast.texpr) = let brackets ~form gname (t : Ast.texpr) =
match bracket_value_element env (Lazy.force values) t with match bracket_value_element env (Lazy.force values) t with
| Some (v, vloc) -> | Some (v, vloc) ->
Loc.failk "check/defvar-bracket-element-is-a-value" vloc Loc.failk "check/defvar-bracket-element-is-a-value" vloc
"%s names a value, not a type, and the brackets around it were read \ "%s names a value, not a type, and the brackets around it were read \
as a type a defvar's third element is a type wherever there is a \ as a type a %s's third element is a type wherever there is a \
type reading, so %s had to be the element type. Write a type there \ type reading, so %s had to be the element type. Write a type there \
for a zeroed global, or put dyn in front of the same brackets \ for a zeroed global, or put dyn in front of the same brackets \
(defvar %s dyn ...) for a dyn global holding the vector you wrote" (%s %s dyn ...) for a dyn global holding the vector you wrote"
v v gname v form v form gname
| None -> () | None -> ()
in in
let word = function Ast.Once -> "defonce" | Ast.Every -> "def" in
List.map List.map
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with match d.Ast.d with
| Ast.Defvar (n, Some t, Ast.Zeroed) -> brackets n t; d | Ast.Defvar (n, Some t, Ast.Zeroed, k) -> brackets ~form:(word k) n t; d
| Ast.Defvar (n, Some t, Ast.Ambiguous e) -> | Ast.Defvar (n, Some t, Ast.Ambiguous e, k) ->
let form = word k in
if defvar_reads_as_type env t then begin if defvar_reads_as_type env t then begin
brackets n t; brackets ~form n t;
{ d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed) } { d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed, k) }
end end
else begin else begin
(match t.Ast.t with (match t.Ast.t with
| Ast.Tname s when not (List.mem s (Lazy.force values)) -> | Ast.Tname s when not (List.mem s (Lazy.force values)) ->
defvar_neither env t.Ast.tloc n s ~values:(Lazy.force values) defvar_neither env t.Ast.tloc ~form n s
~cases:(Lazy.force cases) ~values:(Lazy.force values) ~cases:(Lazy.force cases)
| _ -> ()); | _ -> ());
let dyn = { Ast.t = Ast.Tname "dyn"; tloc = t.Ast.tloc } in let dyn = { Ast.t = Ast.Tname "dyn"; tloc = t.Ast.tloc } in
{ d with Ast.d = Ast.Defvar (n, Some dyn, Ast.Init e) } { d with Ast.d = Ast.Defvar (n, Some dyn, Ast.Init e, k) }
end end
| _ -> d) | _ -> d)
decls decls
@ -1871,7 +1873,7 @@ let to_bytes ctx loc pr (x : Tast.expr) =
Emitted at every site that can *allocate* for such a container, not only at Emitted at every site that can *allocate* for such a container, not only at
its construction, and the extra sites are not belt and braces. ZII means a its construction, and the extra sites are not belt and braces. ZII means a
container can exist without ever passing through [vec-new]: a data type container can exist without ever passing through [vec-new]: a data type
case's field left out of a literal, a [(defvar xs (Vec Value))] a global case's field left out of a literal, a [(defonce xs (Vec Value))] a global
starts as. Those are zeroed, they have no allocator at all, and the first starts as. Those are zeroed, they have no allocator at all, and the first
[push] is what adopts the context so a guard only at the construction [push] is what adopts the context so a guard only at the construction
would have a hole exactly the width of ZII. would have a hole exactly the width of ZII.
@ -2117,7 +2119,7 @@ let view_not_permanent loc (container : Types.t) =
"%s does not cross into dyn as a view here — its storage is not known \ "%s does not cross into dyn as a view here — its storage is not known \
to outlive the view, and a view is exactly as stale-safe as the thing \ to outlive the view, and a view is exactly as stale-safe as the thing \
it is a view of, no more and no less. A global's storage does outlive \ it is a view of, no more and no less. A global's storage does outlive \
it: (defvar g %s ...) viewed from anywhere reads storage fixed for the \ it: (defonce g %s ...) viewed from anywhere reads storage fixed for the \
process, and so does a field or an array element of one. A local, a \ process, and so does a field or an array element of one. A local, a \
parameter, a temporary, anything reached through a slice at any index \ parameter, a temporary, anything reached through a slice at any index \
level even a global one, which holds only ptr+len and can point at a \ level even a global one, which holds only ptr+len and can point at a \
@ -2163,7 +2165,7 @@ let box loc (e : Tast.expr) : Tast.expr =
both, and they share [flan_dyn_view_flat]. *) both, and they share [flan_dyn_view_flat]. *)
(* The element check runs before the lifetime one in all three arms, and (* The element check runs before the lifetime one in all three arms, and
the order is load-bearing rather than incidental: the lifetime message the order is load-bearing rather than incidental: the lifetime message
points at [(defvar g ...)] as the spelling that works, and for an points at [(defonce g ...)] as the spelling that works, and for an
element type no view can carry a string, an i32 the global spelling element type no view can carry a string, an i32 the global spelling
is refused too, so the wrong order hands the programmer advice that is refused too, so the wrong order hands the programmer advice that
fails when they take it. Whichever refusal is unconditional wins. *) fails when they take it. Whichever refusal is unconditional wins. *)
@ -2442,7 +2444,7 @@ let expect ctx loc ~want (got : Tast.expr) =
| _, Types.Dyn -> unbox loc w got | _, Types.Dyn -> unbox loc w got
(* Implicit widening, and this single arm is the whole of its surface. (* Implicit widening, and this single arm is the whole of its surface.
[expect] is called by every site that annotates and by nothing else, [expect] is called by every site that annotates and by nothing else,
so an argument, a return, a let or defvar with a type, a struct field so an argument, a return, a let or defonce with a type, a struct field
initialiser, a push into a Vec and a C import's parameter all get it initialiser, a push into a Vec and a C import's parameter all get it
here at once and none of them had to learn about it. here at once and none of them had to learn about it.
@ -3282,7 +3284,7 @@ and int_literal loc ~want ?(preds = []) ?(default = Types.I32) n =
mk loc (Types.Var v) (Tast.Int (n, Types.I64)) mk loc (Types.Var v) (Tast.Int (n, Types.I64))
(* A literal in dyn position takes i64 and not the i32 an unconstrained one (* A literal in dyn position takes i64 and not the i32 an unconstrained one
defaults to. This is where "dyn integers are i64" stops being a statement defaults to. This is where "dyn integers are i64" stops being a statement
about the ABI and becomes one about the language: [(defvar x dyn 5)] holds about the ABI and becomes one about the language: [(defonce x dyn 5)] holds
an i64 five, and the defaulting question a wider set of boxes would raise an i64 five, and the defaulting question a wider set of boxes would raise
never arises because there is only the one box. Handled here rather than never arises because there is only the one box. Handled here rather than
left to [expect] so the literal is *built* at the right width the range left to [expect] so the literal is *built* at the right width the range
@ -3430,7 +3432,7 @@ and var ctx ?(qualified = false) loc ~want name =
write %s.%s" name dname dname c.Tast.vname write %s.%s" name dname dname c.Tast.vname
| None -> | None ->
(* A bare function name *is* the function. This is a Lisp-1 — one (* A bare function name *is* the function. This is a Lisp-1 — one
top-level namespace, enforced, so a defn and a defvar cannot share top-level namespace, enforced, so a defn and a defonce cannot share
a name and that is exactly what makes (map double xs) safe to a name and that is exactly what makes (map double xs) safe to
read: there is no second binding of [double] for it to have meant read: there is no second binding of [double] for it to have meant
instead, so Common Lisp's #'double would be punctuation answering instead, so Common Lisp's #'double would be punctuation answering
@ -4807,7 +4809,7 @@ and check_arr ctx ~want loc items =
DISCUSS.org's "need a value-producing array constructor". [(array 4 T)] is DISCUSS.org's "need a value-producing array constructor". [(array 4 T)] is
the zeroed array and [dotimes] is Unit, so between them there was no way to the zeroed array and [dotimes] is Unit, so between them there was no way to
write "an array of these" as an *expression* which is what a defvar write "an array of these" as an *expression* which is what a defonce
initialiser has to be. These are that expression, at any rank. initialiser has to be. These are that expression, at any rank.
**The lowering, and why it is not an aggregate value.** [Tast.Arr] is the **The lowering, and why it is not an aggregate value.** [Tast.Arr] is the
@ -4823,7 +4825,7 @@ and check_arr ctx ~want loc items =
The value stays value-like for all that: the slot is the form's own, nothing The value stays value-like for all that: the slot is the form's own, nothing
else can name it, and the [Local] at the end is copied out exactly as any else can name it, and the [Local] at the end is copied out exactly as any
other array-typed expression is. In a [defvar] initialiser the copy is the other array-typed expression is. In a [defonce] initialiser the copy is the
store into the global that the startup function does; in a [let] it is the store into the global that the startup function does; in a [let] it is the
binding's own store. An in-place fill of the *destination*, skipping the binding's own store. An in-place fill of the *destination*, skipping the
temporary, would be the faster lowering and is deliberately not what this temporary, would be the faster lowering and is deliberately not what this
@ -5392,7 +5394,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
Loc.failk "check/set-constant" loc ~notes Loc.failk "check/set-constant" loc ~notes
"%s is a constant, and a constant is not assignable — it is written \ "%s is a constant, and a constant is not assignable — it is written \
into the image and there is nothing to assign to. Declare it with \ into the image and there is nothing to assign to. Declare it with \
defvar if it has to change" name defonce if it has to change" name
| Some (ty, false) -> Tast.Pglobal name, ty | Some (ty, false) -> Tast.Pglobal name, ty
| None -> captured ctx loc name; unknown_name ~setting:true ctx loc name) | None -> captured ctx loc name; unknown_name ~setting:true ctx loc name)
| Ast.Pfield (target, name) -> | Ast.Pfield (target, name) ->
@ -6489,7 +6491,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
it holds, and the element type is written at the call instead. This is not it holds, and the element type is written at the call instead. This is not
the explicit instantiation syntax the generics section rules out: nothing the explicit instantiation syntax the generics section rules out: nothing
here is generic, and the name is resolved as an ordinary type, not bound here is generic, and the name is resolved as an ordinary type, not bound
to a type variable. Where the context does say a defvar's type, a to a type variable. Where the context does say a defonce's type, a
function's return type, an argument it is not needed and may be left function's return type, an argument it is not needed and may be left
out. *) out. *)
| "vec-new" -> | "vec-new" ->
@ -6797,7 +6799,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
(* (map-new), (map-new K V), (map-new a), (map-new K V a). The same shape (* (map-new), (map-new K V), (map-new a), (map-new K V a). The same shape
[vec-new] has and for the same reason: a [let] has no type annotation, so [vec-new] has and for the same reason: a [let] has no type annotation, so
a local map has nowhere else to say what it holds. Where the context does a local map has nowhere else to say what it holds. Where the context does
say a defvar's type, a parameter, a return type the pair may be left say a defonce's type, a parameter, a return type the pair may be left
out. *) out. *)
| "map-new" -> | "map-new" ->
let k, v, args = map_new_types ctx ~want loc args in let k, v, args = map_new_types ctx ~want loc args in
@ -8039,10 +8041,10 @@ and ordinary_call ctx ~want loc name args =
"unknown function %s — did you mean %s?" name m "unknown function %s — did you mean %s?" name m
| None -> | None ->
if args <> [] && capitalised then if args <> [] && capitalised then
(* [(defvar p (Pair i32))]. A capitalised head with arguments and (* [(defonce p (Pair i32))]. A capitalised head with arguments and
no near miss anywhere is somebody reaching for a parameterised no near miss anywhere is somebody reaching for a parameterised
type, which is what the type resolver says about [(Pair i32)] type, which is what the type resolver says about [(Pair i32)]
when the same text lands in a type position. Before defvar took when the same text lands in a type position. Before defonce took
either reading, that is the message this text got; it says the either reading, that is the message this text got; it says the
same thing here so the answer does not depend on which side of same thing here so the answer does not depend on which side of
the fork the form fell down. *) the fork the form fell down. *)
@ -8081,7 +8083,7 @@ and ordinary_call ctx ~want loc name args =
carries a slash answers correctly everywhere a call sits inside a carries a slash answers correctly everywhere a call sits inside a
function and wrongly in the one place a call does not: a package's function and wrongly in the one place a call does not: a package's
global initialiser, which is checked with no owner at all. An importer global initialiser, which is checked with no owner at all. An importer
defining [len] reached inside an imported [(defvar sz i32 (len "abcd"))] defining [len] reached inside an imported [(defonce sz i32 (len "abcd"))]
and changed what it computed. The files were never wrong about it. and changed what it computed. The files were never wrong about it.
What it costs is the REPL: an expression evaluated with no file behind it What it costs is the REPL: an expression evaluated with no file behind it
@ -9121,7 +9123,7 @@ let rec const_int env (e : Ast.expr) : int64 option =
A type name inside an array literal is unambiguous evidence, because a type A type name inside an array literal is unambiguous evidence, because a type
and a value cannot share a name: [collect]'s claimed table is over every and a value cannot share a name: [collect]'s claimed table is over every
declaration kind there is. So finding one means the whole form was meant as declaration kind there is. So finding one means the whole form was meant as
a type, and the form that takes one is [defvar]. *) a type, and the form that takes one is [defonce]. *)
let rec defconst_type_shaped env gname (v : Ast.expr) = let rec defconst_type_shaped env gname (v : Ast.expr) =
match v.Ast.e with match v.Ast.e with
| Ast.Arr items -> | Ast.Arr items ->
@ -9133,7 +9135,7 @@ let rec defconst_type_shaped env gname (v : Ast.expr) =
"%s is a type, and this is a value: a two-element defconst has no \ "%s is a type, and this is a value: a two-element defconst has no \
type slot, so the brackets around it were read as an array \ type slot, so the brackets around it were read as an array \
literal and %s as a name in it. A global declared by its type is \ literal and %s as a name in it. A global declared by its type is \
a defvar write (defvar %s ...) with the same brackets" a defonce write (defonce %s ...) with the same brackets"
n n gname n n gname
| _ -> defconst_type_shaped env gname i) | _ -> defconst_type_shaped env gname i)
items items
@ -9144,7 +9146,7 @@ let collect env (decls : Ast.decl list) =
the tables below are per-kind structs, data types, aliases, enums, functions the tables below are per-kind structs, data types, aliases, enums, functions
and globals each have their own and a collision between two of them and globals each have their own and a collision between two of them
would otherwise be found by LLVM, as [redefinition of function would otherwise be found by LLVM, as [redefinition of function
'@flan.item'], or not at all. A [defn item] and a [defvar item] are two '@flan.item'], or not at all. A [defn item] and a [defonce item] are two
declarations of one name and are rejected here. *) declarations of one name and are rejected here. *)
(* The qualifier is reserved on this side too. [(defn builtin/len ...)] (* The qualifier is reserved on this side too. [(defn builtin/len ...)]
reads the reader treats [/] as an ordinary symbol character and would reads the reader treats [/] as an ordinary symbol character and would
@ -9279,7 +9281,7 @@ let collect env (decls : Ast.decl list) =
signature may name a type declared further down and pairing must not depend signature may name a type declared further down and pairing must not depend
on the order the file was written in. *) on the order the file was written in. *)
let decls = pair_decls env decls in let decls = pair_decls env decls in
(* And for the same reason, at the same point: a three-element defvar is a (* And for the same reason, at the same point: a three-element defonce is a
type or a value by name, and every type name is registered by here. *) type or a value by name, and every type name is registered by here. *)
let decls = settle_defvars env decls in let decls = settle_defvars env decls in
List.iter List.iter
@ -9487,10 +9489,12 @@ let collect env (decls : Ast.decl list) =
Hashtbl.replace env.generics fn.Ast.name fn; Hashtbl.replace env.generics fn.Ast.name fn;
Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret) Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret)
end end
| Ast.Defvar (n, t, _) -> | Ast.Defvar (n, t, _, k) ->
let ty = match t with let ty = match t with
| Some t -> resolve env t | Some t -> resolve env t
| None -> fail loc "defvar %s needs a type" n | None ->
fail loc "%s %s needs a type"
(match k with Ast.Once -> "defonce" | Ast.Every -> "def") n
in in
Hashtbl.replace env.globals n (ty, false); Hashtbl.replace env.globals n (ty, false);
Hashtbl.replace env.global_locs n loc Hashtbl.replace env.global_locs n loc
@ -9794,7 +9798,7 @@ let () = check_fn_ref := check_fn
backends: a computed initialiser is lifted into a function of its own and backends: a computed initialiser is lifted into a function of its own and
the program calls it from [main], after the runtime is up and before any of the program calls it from [main], after the runtime is up and before any of
the program's own code runs. So the premise is gone and the refusal goes the program's own code runs. So the premise is gone and the refusal goes
with it. (defvar g (Vec u8) (slurp "level.edn")) is an ordinary program now, with it. (defonce g (Vec u8) (slurp "level.edn")) is an ordinary program now,
and it is the shape the author kept writing. and it is the shape the author kept writing.
What is still refused is [uninit] on one, and that is a different rule with What is still refused is [uninit] on one, and that is a different rule with
@ -9824,12 +9828,12 @@ let container_global_init loc n (ty : Types.t) (init : Ast.init) =
fail loc fail loc
"the global %s is %s, and uninit on one is refused: its block pointer \ "the global %s is %s, and uninit on one is refused: its block pointer \
steers every read of it, and garbage there is not a garbage number \ steers every read of it, and garbage there is not a garbage number \
the way it is for an f64. Write (defvar %s %s) with no initialiser \ the way it is for an f64. Write (defonce %s %s) with no initialiser \
a zeroed %s is an empty one, and that is a value, not a placeholder" a zeroed %s is an empty one, and that is a value, not a placeholder"
n (Types.to_string ty) n (Types.to_string ty) (Types.to_string ty) n (Types.to_string ty) n (Types.to_string ty) (Types.to_string ty)
| _ -> () | _ -> ()
(* A container global has to be a [defvar]. A [defconst] is not an assignable (* A container global has to be a [defonce]. A [defconst] is not an assignable
place [check_place] refuses one by name and a container's only constant place [check_place] refuses one by name and a container's only constant
is the zeroed one, so a constant Vec could only ever hold the empty value is the zeroed one, so a constant Vec could only ever hold the empty value
it was declared with: nothing could ever put the file's bytes in it. it was declared with: nothing could ever put the file's bytes in it.
@ -9838,10 +9842,10 @@ let container_global_init loc n (ty : Types.t) (init : Ast.init) =
let no_container_defconst loc n (ty : Types.t) = let no_container_defconst loc n (ty : Types.t) =
if zero_only ty then if zero_only ty then
fail loc fail loc
"the global %s is %s, and a %s global is a defvar and not a defconst: \ "the global %s is %s, and a %s global is a defonce and not a defconst: \
a constant is not an assignable place, so nothing could ever load \ a constant is not an assignable place, so nothing could ever load \
this one it would stay the empty %s it was declared as. Write \ this one it would stay the empty %s it was declared as. Write \
(defvar %s %s) and fill it in a function" (defonce %s %s) and fill it in a function"
n (Types.to_string ty) (Types.to_string ty) (Types.to_string ty) n (Types.to_string ty) (Types.to_string ty) (Types.to_string ty)
n (Types.to_string ty) n (Types.to_string ty)
@ -9851,7 +9855,7 @@ let no_container_defconst loc n (ty : Types.t) =
store. Refused here, where the message can name the way through, rather than store. Refused here, where the message can name the way through, rather than
at the emitter as "this one is computed", which is true and says nothing. at the emitter as "this one is computed", which is true and says nothing.
A defvar is no longer any of this and no longer asks. Its computed A defonce is no longer any of this and no longer asks. Its computed
initialiser is lifted into a function that runs at startup, so the member is initialiser is lifted into a function that runs at startup, so the member is
written by exactly the store that writes one anywhere else the encoder was written by exactly the store that writes one anywhere else the encoder was
only ever needed because there was nothing to run. only ever needed because there was nothing to run.
@ -9867,7 +9871,7 @@ let no_union_const env loc n (v : Tast.expr) =
fail loc fail loc
"the constant %s is the union %s, and a union member cannot be written \ "the constant %s is the union %s, and a union member cannot be written \
into a constant: a constant is what the linker writes into the image \ into a constant: a constant is what the linker writes into the image \
and storing a member is a store. Leave it zeroed, or make it a defvar \ and storing a member is a store. Leave it zeroed, or make it a defonce \
and let its initialiser run at startup" and let its initialiser run at startup"
n un n un
| _ -> () | _ -> ()
@ -9885,7 +9889,7 @@ let no_union_const env loc n (v : Tast.expr) =
because a backend can only refuse the program it is asked to emit, and the because a backend can only refuse the program it is asked to emit, and the
two were not asking the same question [Emit.const] refused a computed two were not asking the same question [Emit.const] refused a computed
defconst by name while the x86 backend ran it through the startup function defconst by name while the x86 backend ran it through the startup function
behind an [.init~once.] flag, like a defvar. One refusal in the checker is behind an [.init~once.] flag, like a defonce. One refusal in the checker is
the same program refused the same way on both, and it is the only place the same program refused the same way on both, and it is the only place
that can say what to do instead. that can say what to do instead.
@ -9923,7 +9927,7 @@ let const_defconst_init env loc n (v : Tast.expr) =
"a constant cannot be %s.%s — a data type's payload is a blob, and \ "a constant cannot be %s.%s — a data type's payload is a blob, and \
writing a case into one at link time needs a byte-level encoder that \ writing a case into one at link time needs a byte-level encoder that \
does not exist (a string field could not be encoded at all). Make it a \ does not exist (a string field could not be encoded at all). Make it a \
defvar, whose initialiser runs at startup and stores the case, or \ defonce, whose initialiser runs at startup and stores the case, or \
declare it zeroed, which is %s.%s" declare it zeroed, which is %s.%s"
dname case dname dname case dname
(match Hashtbl.find_opt env.datas dname with (match Hashtbl.find_opt env.datas dname with
@ -9932,9 +9936,9 @@ let const_defconst_init env loc n (v : Tast.expr) =
| Some _ -> | Some _ ->
fail loc fail loc
"a constant's value must be a compile-time constant — the constant %s \ "a constant's value must be a compile-time constant — the constant %s \
is computed. A defvar may have a computed initialiser, because it runs \ is computed. A defonce may have a computed initialiser, because it runs \
at startup and stores the result; a defconst is what the linker writes \ at startup and stores the result; a defconst is what the linker writes \
into the image and has nowhere to run. Write (defvar %s ...), or give \ into the image and has nowhere to run. Write (defonce %s ...), or give \
the constant a literal integer constants may also be written as \ the constant a literal integer constants may also be written as \
arithmetic over literals and other constants, which is folded here" arithmetic over literals and other constants, which is folded here"
n n n n
@ -9954,7 +9958,7 @@ let const_defconst_init env loc n (v : Tast.expr) =
A [handler-bind] or a [restart-case] *inside* an initialiser is ordinary A [handler-bind] or a [restart-case] *inside* an initialiser is ordinary
code: it pushes its frames, runs, and pops them, all before the initialiser code: it pushes its frames, runs, and pops them, all before the initialiser
returns, and nothing it does is visible outside. Both backends run it returns, and nothing it does is visible outside. Both backends run it
exactly as they run it in any other function which is what makes (defvar exactly as they run it in any other function which is what makes (defonce
data (Vec u8) (slurp "level.edn")) an ordinary program, since [slurp] is a data (Vec u8) (slurp "level.edn")) an ordinary program, since [slurp] is a
restart-case with its own signal inside it, and that is the shape the author restart-case with its own signal inside it, and that is the shape the author
kept reaching for. kept reaching for.
@ -10016,7 +10020,7 @@ let no_transfer_in_init n (v : Tast.expr) =
A function rather than the expression left in place, for a reason that is A function rather than the expression left in place, for a reason that is
not tidiness: an initialiser can contain a [let], and a [let] needs a frame. not tidiness: an initialiser can contain a [let], and a [let] needs a frame.
The slots were allocated on a context this function discarded, so what the The slots were allocated on a context this function discarded, so what the
backend got was a slot index into a frame of size zero [(defvar c i64 (let backend got was a slot index into a frame of size zero [(defonce c i64 (let
[x (i64 5)] (+ x 1)))] crashed the x86 backend with an out-of-bounds index, [x (i64 5)] (+ x 1)))] crashed the x86 backend with an out-of-bounds index,
and there was no frame to give it without inventing one. This is that frame, and there was no frame to give it without inventing one. This is that frame,
and it is the one every other body already has. and it is the one every other body already has.
@ -10055,12 +10059,25 @@ let lift_ginit ctx loc n ty (v : Tast.expr) =
let check_global env (d : Ast.decl) : Tast.global option = let check_global env (d : Ast.decl) : Tast.global option =
let ctx () = invented_ctx env Types.Unit in let ctx () = invented_ctx env Types.Unit in
match d.Ast.d with match d.Ast.d with
| Ast.Defvar (n, _, init) -> | Ast.Defvar (n, _, init, kind) ->
let ty, _ = Hashtbl.find env.globals n in let ty, _ = Hashtbl.find env.globals n in
no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty; no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty;
container_global_init d.Ast.dloc n ty init; container_global_init d.Ast.dloc n ty init;
(* A [def]'s initialiser is lifted into [global/<n>] whatever it is — a
zero, a literal, a computed expression where a [defonce]'s is lifted
only when it is computed. The lifting is what makes the form's promise
reachable: the host's startup function calls the initialiser through
its function cell, so a re-evaluated [def] swaps the cell and the next
re-run stores the *edited* value. A constant left inline would be
baked into the host's startup body, and every re-run would paint the
stale value back. [uninit] is the one exception on both forms: there
is nothing to run, so there is nothing to lift. *)
let lift_always = (match kind with Ast.Once -> false | Ast.Every -> true) in
let ginit = let ginit =
match init with match init with
| Ast.Zeroed when lift_always ->
let c = ctx () in
lift_ginit c d.Ast.dloc n ty { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc }
| Ast.Zeroed -> { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc } | Ast.Zeroed -> { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc }
| Ast.Uninit -> | Ast.Uninit ->
(* Everywhere else [uninit] is an opt-out from ZII and the bytes are (* Everywhere else [uninit] is an opt-out from ZII and the bytes are
@ -10086,16 +10103,19 @@ let check_global env (d : Ast.decl) : Tast.global option =
| Ast.Init v -> | Ast.Init v ->
let c = ctx () in let c = ctx () in
let v = check c ~want:ty v in let v = check c ~want:ty v in
if Tast.const_init v then v else lift_ginit c d.Ast.dloc n ty v if Tast.const_init v && not lift_always then v
else lift_ginit c d.Ast.dloc n ty v
(* [settle_defvars] turned every one of these into a [Zeroed] or an (* [settle_defvars] turned every one of these into a [Zeroed] or an
[Init] during [collect], and this pass runs over the list that pass [Init] during [collect], and this pass runs over the list that pass
handed back. One arriving here is a driver that checked a global handed back. One arriving here is a driver that checked a global
without collecting first. *) without collecting first. *)
| Ast.Ambiguous _ -> | Ast.Ambiguous _ ->
fail d.Ast.dloc fail d.Ast.dloc
"internal: the third element of (defvar %s ...) was never decided" n "internal: the third element of (%s %s ...) was never decided"
(match kind with Ast.Once -> "defonce" | Ast.Every -> "def") n
in in
Some { Tast.gname = n; gty = ty; ginit; gconst = false; gfolded = false } Some { Tast.gname = n; gty = ty; ginit; gconst = false; gfolded = false;
grerun = (match kind with Ast.Once -> false | Ast.Every -> true) }
| Ast.Defconst (n, _, v) -> | Ast.Defconst (n, _, v) ->
let ty, _ = Hashtbl.find env.globals n in let ty, _ = Hashtbl.find env.globals n in
no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty; no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty;
@ -10121,7 +10141,7 @@ let check_global env (d : Ast.decl) : Tast.global option =
(* [env.consts] holds exactly the constants the folding pass consumed, so (* [env.consts] holds exactly the constants the folding pass consumed, so
membership is the question "is this value in the program's shape?" *) membership is the question "is this value in the program's shape?" *)
Some { Tast.gname = n; gty = ty; ginit; gconst = true; Some { Tast.gname = n; gty = ty; ginit; gconst = true;
gfolded = Hashtbl.mem env.consts n } gfolded = Hashtbl.mem env.consts n; grerun = false }
| _ -> None | _ -> None
(* The entry point, plan.org: (defn main [args [string]] i32), with both the (* The entry point, plan.org: (defn main [args [string]] i32), with both the
@ -10176,8 +10196,8 @@ let check_main env decls =
(* ── The order the initialisers run in ─────────────────────────────── *) (* ── The order the initialisers run in ─────────────────────────────── *)
(* Declaration order is the order a program's globals are started in, and it is (* Declaration order is the order a program's globals are started in, and it is
the wrong one as soon as one of them is computed from another: [(defvar b the wrong one as soon as one of them is computed from another: [(defonce b
i64 (+ a 10))] written above [(defvar a i64 (+ 1 2))] read a zero and i64 (+ a 10))] written above [(defonce a i64 (+ 1 2))] read a zero and
answered 10 without saying anything. So the computed ones are sorted answered 10 without saying anything. So the computed ones are sorted
by what they need, which is what Odin does (src/checker.cpp, by what they need, which is what Odin does (src/checker.cpp,
[calculate_global_init_order]) and for the same reason the alternative is [calculate_global_init_order]) and for the same reason the alternative is
@ -10193,7 +10213,7 @@ let check_main env decls =
edge. edge.
The dependency is transitive through calls, not just through what the The dependency is transitive through calls, not just through what the
initialiser names: [(defvar a i64 (f))] where [f] reads [b] needs [b] initialiser names: [(defonce a i64 (f))] where [f] reads [b] needs [b]
started first, and an analysis that only looked at the initialiser's own started first, and an analysis that only looked at the initialiser's own
text would order that pair by luck. Odin's graph is transitive for the same text would order that pair by luck. Odin's graph is transitive for the same
reason. reason.
@ -10487,7 +10507,7 @@ let rec dyn_behind_pointer p seen (t : Types.t) =
cap above is about. Only the by-value shapes contribute; the rest are cap above is about. Only the by-value shapes contribute; the rest are
refused by [hidden_dyn] before this number matters. *) refused by [hidden_dyn] before this number matters. *)
(* Saturated at one past the cap, because the number only ever has to be (* Saturated at one past the cap, because the number only ever has to be
compared with it. That is not tidiness: [(defvar big [4611686018427387904 compared with it. That is not tidiness: [(defonce big [4611686018427387904
S])] is a length an [Int64.to_int] multiplication wraps *negative* on, so an S])] is a length an [Int64.to_int] multiplication wraps *negative* on, so an
honest product made the test [n > desc_offsets_max] false, the declaration honest product made the test [n > desc_offsets_max] false, the declaration
was accepted, and the emitter then sat building the offset list until was accepted, and the emitter then sat building the offset list until
@ -11158,7 +11178,7 @@ let memory_sites ?file (p : Tast.program) : Loc.diag list =
| _ -> () | _ -> ()
in in
(* A global's initialiser runs at startup and allocates there as much as a (* A global's initialiser runs at startup and allocates there as much as a
body does [(defvar names (vec-new dyn))] is a heap object before main body does [(defonce names (vec-new dyn))] is a heap object before main
has a line of its own so the globals are walked and not only the has a line of its own so the globals are walked and not only the
functions. *) functions. *)
List.iter (fun (g : Tast.global) -> Tast.walk look g.Tast.ginit) p.Tast.globals; List.iter (fun (g : Tast.global) -> Tast.walk look g.Tast.ginit) p.Tast.globals;

View File

@ -2,7 +2,7 @@
beside it, and a socket an editor talks to. beside it, and a socket an editor talks to.
This is the piece between an editor and everything else. What it adds over This is the piece between an editor and everything else. What it adds over
[flan reload] is that the session *persists*: a [defvar] added by one [flan reload] is that the session *persists*: a [defonce] added by one
evaluation is part of the program the next one is checked against, and the evaluation is part of the program the next one is checked against, and the
set of names the running process was built with is the one from the build set of names the running process was built with is the one from the build
this daemon actually made. A CLI that rebuilds its session from source each this daemon actually made. A CLI that rebuilds its session from source each
@ -2621,7 +2621,7 @@ let reg_listing t ~verb ~note =
Locals were the half the shadow stack was built for; these are arguably the Locals were the half the shadow stack was built for; these are arguably the
more useful half in this language. A game keeps most of its state in more useful half in this language. A game keeps most of its state in
top-level [defvar]s and sand.flan holds its entire grid that way, so "what top-level [defonce]s and sand.flan holds its entire grid that way, so "what
is the program's state right now" is a question about globals and there was is the program's state right now" is a question about globals and there was
nowhere to ask it. nowhere to ask it.
@ -2979,9 +2979,10 @@ let abort t =
Nothing here does the keeping, and that is deliberate: a re-run re-enters Nothing here does the keeping, and that is deliberate: a re-run re-enters
[flan_program_main] from the top, the startup function that runs the [flan_program_main] from the top, the startup function that runs the
computed initialisers included, and what each of those does the second time computed initialisers included, and what each of those does the second time
is decided by the form that defined the global. A [defvar] is CL's, so its is decided by the form that defined the global. A [defonce] is CL's defvar,
initialiser runs only if the variable is not already initialised so its initialiser runs only if the variable is not already initialised
[Emit.startup_plan] emits the flag that makes that true, and FIX.org's entry [Emit.startup_plan] emits the flag that makes that true and a [def] is
CL's defparameter, whose initialiser runs on every re-run; FIX.org's entry
of 2026-09-20 is the rule. This op says what it sees rather than arranging of 2026-09-20 is the rule. This op says what it sees rather than arranging
for it. for it.

View File

@ -62,7 +62,7 @@ let sname n = "%" ^ quoted n
have no cells and call the symbol directly. *) have no cells and call the symbol directly. *)
let cellname n = "@" ^ quoted (Mangle.cell n) let cellname n = "@" ^ quoted (Mangle.cell n)
(* A name the host was never built with — a defn or a defvar typed in after the (* A name the host was never built with — a defn or a defonce typed in after the
process started has no symbol to bind to, so it is keyed by string through process started has no symbol to bind to, so it is keyed by string through
[flan_dev_cell] / [flan_dev_global] and the answer is cached in one of these [flan_dev_cell] / [flan_dev_global] and the answer is cached in one of these
module-local slots. One indirection more than a name the host has, which is module-local slots. One indirection more than a name the host has, which is
@ -3478,7 +3478,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
no init-at-startup path, by design". There is one now, and it is a call: no init-at-startup path, by design". There is one now, and it is a call:
[Check] lifts a computed initialiser into a function and [emit_startup] [Check] lifts a computed initialiser into a function and [emit_startup]
below stores its result before [main] runs. So what reaches here is what below stores its result before [main] runs. So what reaches here is what
needs no code every [defvar] whose initialiser [Tast.const_init] accepts, needs no code every [defonce] whose initialiser [Tast.const_init] accepts,
and every [defconst], because a defconst's initialiser is one of those too: and every [defconst], because a defconst's initialiser is one of those too:
the checker refuses a computed one outright (2026-09-20). A constant is what the checker refuses a computed one outright (2026-09-20). A constant is what
the linker writes, and a value that has to be computed is not one. *) the linker writes, and a value that has to be computed is not one. *)
@ -3500,7 +3500,7 @@ let rec const m (e : Tast.expr) =
| _ -> "{ " ^ String.concat ", " inner ^ " }") | _ -> "{ " ^ String.concat ", " inner ^ " }")
| Tast.Some_ v -> | Tast.Some_ v ->
Printf.sprintf "{ i8 1, %s %s }" (ll v.Tast.ty) (const m v) Printf.sprintf "{ i8 1, %s %s }" (ll v.Tast.ty) (const m v)
(* No program reaches this. A [defvar] whose initialiser is computed never (* No program reaches this. A [defonce] whose initialiser is computed never
asks it was lifted into a function and this one is only called for the asks it was lifted into a function and this one is only called for the
constant ones and a computed [defconst] is refused by constant ones and a computed [defconst] is refused by
[Check.const_defconst_init], which is where the two messages that used to [Check.const_defconst_init], which is where the two messages that used to
@ -3535,7 +3535,7 @@ let emit_global m ?(hidden = false) (g : Tast.global) =
(* The initialiser decides, and the form no longer has to be asked: a (* The initialiser decides, and the form no longer has to be asked: a
[defconst]'s initialiser is always one [const] can write, because the [defconst]'s initialiser is always one [const] can write, because the
checker refuses a computed one. So a zeroinitializer here is always a checker refuses a computed one. So a zeroinitializer here is always a
[defvar] waiting for the startup function. *) [defonce] waiting for the startup function. *)
(if Tast.const_init g.Tast.ginit then const m g.Tast.ginit (if Tast.const_init g.Tast.ginit then const m g.Tast.ginit
else "zeroinitializer")) else "zeroinitializer"))
@ -3564,13 +3564,17 @@ let startup_sym = fname ".init-globals"
calls [flan_program_main] from the top, so every line of [main] above runs a calls [flan_program_main] from the top, so every line of [main] above runs a
second time, this call included. second time, this call included.
What that must mean is decided by the defining form, not by the daemon. A What that must mean is decided by the defining form, not by the daemon
[defvar] is Common Lisp's [defvar]: its initialiser runs only if the def, defonce and defconst decide what a re-run does. A [defonce] is Common
Lisp's [defvar] under Clojure's name: its initialiser runs only if the
variable is not already initialised, so its value survives a re-run which variable is not already initialised, so its value survives a re-run which
is what the daemon has always promised ("the globals are as it left them") 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 and what a plain zeroed [defonce] already got for free, since .bss is
untouched by a second call. A computed one used to be the exception, wiped untouched by a second call. A computed one used to be the exception, wiped
back to its initial value every re-run. A [defconst] whose initialiser is a back to its initial value every re-run. A [def] is Common Lisp's
[defparameter]: its initialiser runs on every re-run no flag so an
edited initialiser repaints the same storage on the next re-run, which is
the reason the form exists. A [defconst] whose initialiser is a
compile-time constant is not reached from here at all: it is the linker's 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 image on one backend and [.init-data]'s stores on the other, and a re-run
reaches neither. reaches neither.
@ -3582,19 +3586,18 @@ let startup_sym = fname ".init-globals"
The two therefore coincide for a constant and no [defconst] reaches the The two therefore coincide for a constant and no [defconst] reaches the
plan below. Until then they did not, and the backends disagreed about that plan below. Until then they did not, and the backends disagreed about that
one program: [const] refused a computed [defconst] by name while the x86 one program: [const] refused a computed [defconst] by name while the x86
backend guarded it here like any [defvar]. One refusal in the checker is backend guarded it here like any [defonce]. One refusal in the checker is
what ended it. what ended it.
So each computed initialiser guards itself with a flag of its own. Per So each [defonce]'s computed initialiser guards itself with a flag of its
global and not per startup function, because the rule belongs to the form: own, and a [def]'s takes none. Per global and not per startup function,
a global whose initialiser is added by a later build, or a future form that because the rule belongs to the form: a [def] beside a [defonce] decides
*does* recompute, decides its own case without the other globals' having to its own case without the other globals' having to agree.
agree.
Dev builds only. A release build has no re-run to guard against and pays Dev builds only. A release build has no re-run to guard against and pays
nothing the body below is then the bare store it always was, byte for nothing the body below is then the bare store it always was, byte for
byte. The flag is a global of its own rather than a sentinel value in the 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. variable, because there is no value a [defonce] cannot hold.
Writing the flag *after* the store is what makes a failed initialiser retry 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 rather than be skipped. [Check.no_transfer_in_init] refuses a [signal] or an
@ -3611,7 +3614,7 @@ let startup_sym = fname ".init-globals"
The flag's name is mangled with a [~], which the reader treats as a 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 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 trick [destructure~N] uses. A [.]-separated name would not do: [.] is an
ordinary symbol constituent, so [(defvar .init-once.x ...)] beside a ordinary symbol constituent, so [(defonce .init-once.x ...)] beside a
computed [x] used to emit the same symbol twice and the dev build died at computed [x] used to emit the same symbol twice and the dev build died at
the assembler. *) the assembler. *)
let init_flag n = ".init~once." ^ n let init_flag n = ".init~once." ^ n
@ -3620,23 +3623,34 @@ let init_flag n = ".init~once." ^ n
startup function built here so that the two backends cannot disagree startup function built here so that the two backends cannot disagree
about any of the three. [flags] is empty in a release build. *) about any of the three. [flags] is empty in a release build. *)
let startup_plan m (globals : Tast.global list) = let startup_plan m (globals : Tast.global list) =
(* Which globals the startup function stores at all: every initialiser
that is not a constant the image already holds. A [def]'s is *never*
such a constant [Check.check_global] lifts every one of them,
zero and literal included, into a [global/<n>] call so every [def]
except an [uninit] one is in here, which is what makes its store run
on each re-run and its initialiser reachable through the function
cell a re-evaluation swaps. *)
let computed = let computed =
List.filter List.filter
(fun (g : Tast.global) -> not (Tast.const_init g.Tast.ginit)) (fun (g : Tast.global) -> not (Tast.const_init g.Tast.ginit))
globals globals
in in
let flags = (* One guard flag per [defonce] with something to run, and none for a
if not m.dev then [] [def]: the flag is exactly what makes an initialiser run once, and a
else [def]'s runs every time that unguarded store is the whole difference
List.map between the two forms. *)
(fun (g : Tast.global) -> let flag_for (g : Tast.global) =
{ Tast.gname = init_flag g.Tast.gname; gty = Types.Bool; if m.dev && not g.Tast.grerun then
ginit = Some
{ Tast.e = Tast.Bool false; ty = Types.Bool; { Tast.gname = init_flag g.Tast.gname; gty = Types.Bool;
loc = g.Tast.ginit.Tast.loc }; ginit =
gconst = false; gfolded = false }) { Tast.e = Tast.Bool false; ty = Types.Bool;
computed loc = g.Tast.ginit.Tast.loc };
gconst = false; gfolded = false; grerun = false }
else None
in in
let flagged = List.map (fun g -> (g, flag_for g)) computed in
let flags = List.filter_map snd flagged in
(* Registered so that [place] and the x86 backend's [lower] can find a (* Registered so that [place] and the x86 backend's [lower] can find a
flag's type the same way they find any other global's. Not added to the flag's type the same way they find any other global's. Not added to the
program's own [globals] list: nothing the programmer wrote names one, and program's own [globals] list: nothing the programmer wrote names one, and
@ -3645,8 +3659,8 @@ let startup_plan m (globals : Tast.global list) =
(fun (g : Tast.global) -> Hashtbl.replace m.globals g.Tast.gname g.Tast.gty) (fun (g : Tast.global) -> Hashtbl.replace m.globals g.Tast.gname g.Tast.gty)
flags; flags;
let body = let body =
List.map2 List.map
(fun (g : Tast.global) (flag : Tast.global option) -> (fun ((g : Tast.global), (flag : Tast.global option)) ->
let loc = g.Tast.ginit.Tast.loc in let loc = g.Tast.ginit.Tast.loc in
let store = let store =
{ Tast.e = Tast.Set (Tast.Pglobal g.Tast.gname, g.Tast.ginit); { Tast.e = Tast.Set (Tast.Pglobal g.Tast.gname, g.Tast.ginit);
@ -3668,9 +3682,7 @@ let startup_plan m (globals : Tast.global list) =
{ Tast.e = Tast.Unit; ty = Types.Unit; loc }, { Tast.e = Tast.Unit; ty = Types.Unit; loc },
{ Tast.e = Tast.Do [ store; mark ]; ty = Types.Unit; loc }); { Tast.e = Tast.Do [ store; mark ]; ty = Types.Unit; loc });
ty = Types.Unit; loc }) ty = Types.Unit; loc })
computed flagged
(if flags = [] then List.map (fun _ -> None) computed
else List.map (fun f -> Some f) flags)
in in
(computed, flags, body) (computed, flags, body)
@ -4339,7 +4351,7 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
it a dev-build feature at run time. A constructor rather than a line in it a dev-build feature at run time. A constructor rather than a line in
[main]: the notes are emitted into every function, and a note that [main]: the notes are emitted into every function, and a note that
arrived before the flag was set would be a block the table never heard arrived before the flag was set would be a block the table never heard
of. That used to be an argument about a [defvar] initialiser allocating of. That used to be an argument about a [defonce] initialiser allocating
before [main] ran, which it no longer does [emit_startup] is called before [main] ran, which it no longer does [emit_startup] is called
from [main] now. What survives is the weaker and sufficient version: from [main] now. What survives is the weaker and sufficient version:
arming has to precede the first allocation, a constructor is the only arming has to precede the first allocation, a constructor is the only
@ -4502,6 +4514,19 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
Printf.sprintf "%s = internal global ptr null\n" Printf.sprintf "%s = internal global ptr null\n"
(cellptr f.Tast.name))) (cellptr f.Tast.name)))
siblings; siblings;
(* A lifted initialiser handed in as a target — [global/<n>] when a [def]
is re-evaluated is not a sibling: its [fparent] is the global it
initialises, not a function in [fns]. Its cell is the host's like any
other (the host declares one per function, lifted ones included), so
the publish store below needs the declaration the sibling loop above
could not write. *)
List.iter
(fun (f : Tast.fn) ->
if f.Tast.fparent <> None && known f.Tast.name
&& not (transient f.Tast.name) then
Buffer.add_string m.out
(Printf.sprintf "%s = external global ptr\n" (cellname f.Tast.name)))
targets;
if new_fns <> [] || new_globals <> [] then if new_fns <> [] || new_globals <> [] then
Buffer.add_string m.out Buffer.add_string m.out
"\ndeclare ptr @flan_dev_cell(ptr)\n\ "\ndeclare ptr @flan_dev_cell(ptr)\n\
@ -4550,7 +4575,7 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
would have to agree with LLVM's on every target. *) would have to agree with LLVM's on every target. *)
(* Its declared initial value travels with it, as a constant the (* Its declared initial value travels with it, as a constant the
runtime copies on the allocation and ignores afterwards. Without runtime copies on the allocation and ignores afterwards. Without
this a new (defvar n i64 42) or a new defconst would silently be this a new (defonce n i64 42) or a new defconst would silently be
zero calloc is only the right answer for ZII. zero calloc is only the right answer for ZII.
A *computed* initialiser sends a null instead, and the allocation A *computed* initialiser sends a null instead, and the allocation

View File

@ -448,11 +448,11 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
able to name the type. *) able to name the type. *)
| Ast.Defunion (n, ms) -> | Ast.Defunion (n, ms) ->
Ast.Defunion (qualify alias n, List.map (rename_field owned alias) ms) Ast.Defunion (qualify alias n, List.map (rename_field owned alias) ms)
| Ast.Defvar (n, t, init) -> | Ast.Defvar (n, t, init, k) ->
Ast.Defvar (qualify alias n, Option.map (rename_texpr owned alias) t, Ast.Defvar (qualify alias n, Option.map (rename_texpr owned alias) t,
(match init with (match init with
| Ast.Init v -> Ast.Init (rename_expr owned alias [] v) | Ast.Init v -> Ast.Init (rename_expr owned alias [] v)
(* An undecided three-element defvar carries both readings (* An undecided three-element defonce carries both readings
and neither has been picked yet, so both are renamed and neither has been picked yet, so both are renamed
the type half by [rename_texpr] above, the value half the type half by [rename_texpr] above, the value half
here. Renaming only one would make the import decide here. Renaming only one would make the import decide
@ -460,7 +460,8 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
pass's. *) pass's. *)
| Ast.Ambiguous v -> | Ast.Ambiguous v ->
Ast.Ambiguous (rename_expr owned alias [] v) Ast.Ambiguous (rename_expr owned alias [] v)
| other -> other)) | other -> other),
k)
| Ast.Defn fn -> | Ast.Defn fn ->
let params = List.map (rename_field owned alias) fn.Ast.params in let params = List.map (rename_field owned alias) fn.Ast.params in
let praw = Option.map (List.map (rename_pitem owned alias)) fn.Ast.praw in let praw = Option.map (List.map (rename_pitem owned alias)) fn.Ast.praw in
@ -848,9 +849,9 @@ let decl_uses acc (d : Ast.decl) =
| Ast.Declare (f, _) | Ast.DeclareC (f, _) -> | Ast.Declare (f, _) | Ast.DeclareC (f, _) ->
List.iter field f.Ast.params; List.iter field f.Ast.params;
Option.iter (texpr_uses acc) f.Ast.ret Option.iter (texpr_uses acc) f.Ast.ret
| Ast.Defvar (_, t, init) -> | Ast.Defvar (_, t, init, _) ->
Option.iter (texpr_uses acc) t; Option.iter (texpr_uses acc) t;
(* Both readings again: an undecided defvar may turn out to be the one (* Both readings again: an undecided defonce may turn out to be the one
whose initialiser calls the function, and a dependency this pass misses whose initialiser calls the function, and a dependency this pass misses
is a declaration dropped from the module. *) is a declaration dropped from the module. *)
(match init with (match init with

View File

@ -140,7 +140,7 @@ let building = ref false
calling a dropped one is as unbuildable as the dropped one itself. calling a dropped one is as unbuildable as the dropped one itself.
Only [defn]s are dropped. A [defstruct], [defdata], [defalias], [defenum] Only [defn]s are dropped. A [defstruct], [defdata], [defalias], [defenum]
or [defvar] stays whatever it names: the functions that survive still or [defonce] stays whatever it names: the functions that survive still
mention those types, and a reduced prelude missing them would not check. mention those types, and a reduced prelude missing them would not check.
There used to be a sharper reason [Parse.prelude_types] memoised the There used to be a sharper reason [Parse.prelude_types] memoised the
prelude's type names for the parser's return-type guess, and a reduced prelude's type names for the parser's return-type guess, and a reduced

View File

@ -25,7 +25,7 @@
let prefix = "flan." let prefix = "flan."
(* A function or a global. One namespace, because the language has one: a (* A function or a global. One namespace, because the language has one: a
[defn] and a [defvar] cannot share a name, so nothing here has to keep [defn] and a [defonce] cannot share a name, so nothing here has to keep
them apart. The compiler's own names go through this too [.init-globals] them apart. The compiler's own names go through this too [.init-globals]
and [.init-data] start with a dot no reader token can produce. *) and [.init-data] start with a dot no reader token can produce. *)
let sym n = prefix ^ n let sym n = prefix ^ n

View File

@ -669,9 +669,9 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
a head, which is the same property that makes a quasiquoted macro call a head, which is the same property that makes a quasiquoted macro call
output rather than a dependency. Building a declaration as a value is what output rather than a dependency. Building a declaration as a value is what
a macro is for. *) a macro is for. *)
| Sym ("defmacro" | "defn" | "defvar" | "defconst" | "defstruct" | "defdata" | Sym ("defmacro" | "defn" | "def" | "defonce" | "defconst" | "defstruct"
| "defunion" | "defclass" | "defgeneric" | "defmulti" | "defmethod" | "defdata" | "defunion" | "defclass" | "defgeneric" | "defmulti"
| "defenum" | "defalias" | "import" as name) -> | "defmethod" | "defenum" | "defalias" | "import" as name) ->
fail f fail f
"%s is a top-level declaration, not an expression. A quasiquoted one is \ "%s is a top-level declaration, not an expression. A quasiquoted one is \
a value and a macro may answer with it; an evaluated one is not a thing \ a value and a macro may answer with it; an evaluated one is not a thing \
@ -1180,18 +1180,20 @@ and pattern (f : Form.t) : Ast.pattern =
Ast.Pctor (ctor, List.map sym binds) Ast.Pctor (ctor, List.map sym binds)
| _ -> fail f "expected a pattern, found %s" (Form.to_string f) | _ -> fail f "expected a pattern, found %s" (Form.to_string f)
(* ── The third element of a defvar ───────────────────────────────────── (* ── The third element of a defonce or a def ───────────────────────────
[(defvar x i32)] declares a zeroed static and [(defvar score 0)] declares a [(defonce x i32)] declares a zeroed static and [(defonce score 0)] declares
dyn global holding 0, and which one a form is is decided by whether the a dyn global holding 0, and which one a form is is decided by whether the
third element is a type. The author's rule, 2026-09-20: "if it's 3 atoms third element is a type. [def] takes exactly the same spellings the two
then it's dyn", and "dispatch the if it's a type do the right thing". forms differ only in what a re-run does. The author's rule, 2026-09-20:
"if it's 3 atoms then it's dyn", and "dispatch the if it's a type do the
right thing".
Most forms are settled by their shape alone and are settled here: [0], a Most forms are settled by their shape alone and are settled here: [0], a
string, a map, [[1 2 3]] and [(f "x")] are not types by any reading, so the string, a map, [[1 2 3]] and [(f "x")] are not types by any reading, so the
global is dyn and its initialiser is the expression; [[4 u32]], [()] and global is dyn and its initialiser is the expression; [[4 u32]], [()] and
[(Fn [i32] i32)] are types by any reading and keep exactly the meaning they [(Fn [i32] i32)] are types by any reading and keep exactly the meaning they
have today. Note which side the bracket falls on: [[n T]] stays a fixed have today. Note which side the bracket falls on: [[n T]] stays a fixed
array, so [(defvar rows [4 u32])] is the zeroed grid it always was, and a array, so [(defonce rows [4 u32])] is the zeroed grid it always was, and a
*vector literal* of two names is not reachable in this position. *vector literal* of two names is not reachable in this position.
Two shapes are left over, and they are the ones a name decides rather than Two shapes are left over, and they are the ones a name decides rather than
@ -1633,18 +1635,39 @@ let rec decl (f : Form.t) : Ast.decl =
"defenum is (defenum Name [member value? ...]). A member with no \ "defenum is (defenum Name [member value? ...]). A member with no \
value takes the previous member's plus one, and the first takes 0") value takes the previous member's plus one, and the first takes 0")
| List ({ v = Sym "defvar"; _ } :: args) -> (* The two forms share one arm because they share everything but what a
daemon re-run does: [defonce] initialises once and keeps its value, [def]
runs its initialiser on every re-run. The author's rule (FIX.org,
2026-09-20): def, defonce and defconst decide what a re-run does, not the
daemon. *)
| List ({ v = Sym ("defonce" | "def" as form); _ } :: args) ->
let kind = if String.equal form "def" then Ast.Every else Ast.Once in
(match args with (match args with
| [ n; t ] -> | [ n; t ] ->
let ty, init = defvar3 t in let ty, init = defvar3 t in
mk (Ast.Defvar (sym n, Some ty, init)) mk (Ast.Defvar (sym n, Some ty, init, kind))
| [ n; t; { v = Sym "uninit"; _ } ] -> | [ n; t; { v = Sym "uninit"; _ } ] ->
mk (Ast.Defvar (sym n, Some (texpr t), Ast.Uninit)) mk (Ast.Defvar (sym n, Some (texpr t), Ast.Uninit, kind))
| [ n; t; v ] -> mk (Ast.Defvar (sym n, Some (texpr t), Ast.Init (expr v))) | [ n; t; v ] ->
mk (Ast.Defvar (sym n, Some (texpr t), Ast.Init (expr v), kind))
| _ -> | _ ->
fail f fail f
"defvar is (defvar name Type value?) or (defvar name value) — a \ "%s is (%s name Type value?) or (%s name value) — a third element \
third element that is not a type is the value of a dyn global") that is not a type is the value of a dyn global"
form form form)
(* The old name of [defonce], refused by name rather than left to fall
through to "unknown function": every program written before the rename
spells it, and the message is the migration. *)
| List ({ v = Sym "defvar"; _ } :: args) ->
let rest =
String.concat " " (List.map Form.to_string args)
in
Loc.failk "parse/defvar-renamed" f.loc
"defvar is now called defonce — the name says what it does: it \
initialises once and keeps its value across re-runs. Write (defonce \
%s), or (def %s) if the value should follow the source on every re-run"
rest rest
| List ({ v = Sym "defconst"; _ } :: args) -> | List ({ v = Sym "defconst"; _ } :: args) ->
(match args with (match args with

View File

@ -200,7 +200,7 @@ let source = {flan|
;; regression test if the sequence is byte-identical on native and wasm32 ;; regression test if the sequence is byte-identical on native and wasm32
;; (plan.org, RNG is ours). PCG-XSH-RR 32: one u64 LCG step per draw, folded ;; (plan.org, RNG is ours). PCG-XSH-RR 32: one u64 LCG step per draw, folded
;; down to 32 bits by an xorshift and rotated by the state's top five bits. ;; down to 32 bits by an xorshift and rotated by the state's top five bits.
(defvar rand-state u64 6364136223846793005) (defonce rand-state u64 6364136223846793005)
(defn rand-seed [seed u64] () (defn rand-seed [seed u64] ()
(set rand-state (+ (* seed 6364136223846793005) 1442695040888963407))) (set rand-state (+ (* seed 6364136223846793005) 1442695040888963407)))
@ -2073,7 +2073,7 @@ let source = {flan|
;; per compiler process and every macro in a program shares it, so the counter ;; per compiler process and every macro in a program shares it, so the counter
;; is process-wide in practice; a second module would restart it, and the day ;; is process-wide in practice; a second module would restart it, and the day
;; there is one, the fix is to seed this from the module's index. ;; there is one, the fix is to seed this from the module's index.
(defvar gensym-n i64 0) (defonce gensym-n i64 0)
(defn gensym [] Form (defn gensym [] Form
(set gensym-n (+ gensym-n 1)) (set gensym-n (+ gensym-n 1))

View File

@ -21,7 +21,7 @@
Only [fns] and [externs] are pruned. Globals, structs and data types stay: Only [fns] and [externs] are pruned. Globals, structs and data types stay:
a dropped function is a loud link error, a dropped global would be a a dropped function is a loud link error, a dropped global would be a
silently different program, and an unreferenced global is bytes in BSS that silently different program, and an unreferenced global is bytes in BSS that
cost nothing. A [defvar brush rl/Texture2D] in a headless build is exactly cost nothing. A [defonce brush rl/Texture2D] in a headless build is exactly
that. that.
Dev builds are not pruned at all. A REPL redefines a function that the Dev builds are not pruned at all. A REPL redefines a function that the

View File

@ -290,7 +290,7 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
with with
(* A [defconst] is folded into its call sites — into an array length, at (* A [defconst] is folded into its call sites — into an array length, at
worst, which is decided before any type resolves so its value is in worst, which is decided before any type resolves so its value is in
the running program's code and not only in its storage. A [defvar]'s the running program's code and not only in its storage. A [defonce]'s
initial value is the opposite case and must *not* be refused: the initial value is the opposite case and must *not* be refused: the
storage holds live state the program has long since moved past, which storage holds live state the program has long since moved past, which
is the whole of "edit the code, keep the sand". Same record, opposite is the whole of "edit the code, keep the sand". Same record, opposite
@ -779,6 +779,30 @@ let eval ?(origin = "<eval>") ?pause t src : change =
program.Tast.fns) program.Tast.fns)
names names
in in
(* A re-evaluated [def] is a promise about the *next re-run*: its
initialiser runs every time, so the edited one has to be the one that
runs. The initialiser is the lifted [global/<n>] [Check.check_global]
lifts every [def] initialiser, constants included, for exactly this
and the host's startup function calls it through its cell, so
republishing that one function is the whole delivery. Only when the
host already has the cell: a [def] the process has never seen gets its
storage from [flan_dev_global] like any new global, and there is no
startup call to swap. *)
let def_inits =
List.concat_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defvar (n, _, _, Ast.Every) ->
let g = "global/" ^ n in
if known t g
&& List.exists
(fun (f : Tast.fn) -> String.equal f.Tast.name g)
program.Tast.fns
then [ g ]
else []
| _ -> [])
incoming
in
let from_generics = let from_generics =
List.concat_map List.concat_map
(fun n -> (fun n ->
@ -797,7 +821,7 @@ let eval ?(origin = "<eval>") ?pause t src : change =
in in
let fns = let fns =
List.sort_uniq String.compare List.sort_uniq String.compare
(declared_fns @ from_generics @ new_instances) (declared_fns @ def_inits @ from_generics @ new_instances)
in in
(* A constant that changed and can be published: known to the host, not (* A constant that changed and can be published: known to the host, not
consumed by the checker. The module stores its new value at the frame consumed by the checker. The module stores its new value at the frame
@ -1831,7 +1855,7 @@ let write_slot ?(origin = "<set>") t ~frame ~(fn : Tast.fn) ~slot ~path
(* ── The globals a stopped stack reaches ───────────────────────────── *) (* ── The globals a stopped stack reaches ───────────────────────────── *)
(* The other half of what a break loop can show, and in this language arguably (* The other half of what a break loop can show, and in this language arguably
the more useful one: a game keeps most of its state in top-level [defvar]s, the more useful one: a game keeps most of its state in top-level [defonce]s,
and sand.flan holds its entire grid that way. and sand.flan holds its entire grid that way.
Almost the same thunk as [render_locals] with a different root, and the Almost the same thunk as [render_locals] with a different root, and the

View File

@ -321,6 +321,10 @@ type global = {
ginit : expr; ginit : expr;
gconst : bool; gconst : bool;
gfolded : bool; gfolded : bool;
(* [def] rather than [defonce]: the initialiser runs on every daemon
re-run instead of once behind a flag, so an edited initialiser takes
effect on the next re-run. [Emit.startup_plan] is the consumer. *)
grerun : bool;
} }
(* A foreign function: no body, and [esym] is the symbol the linker sees. The (* A foreign function: no body, and [esym] is the symbol the linker sees. The

View File

@ -51,7 +51,7 @@ type t =
| Fn of t list * t (* (Fn [T ...] R) *) | Fn of t list * t (* (Fn [T ...] R) *)
| Var of string (* a type variable — milestone 5 *) | Var of string (* a type variable — milestone 5 *)
(* [dyn]: one machine word whose contents the runtime knows and this module (* [dyn]: one machine word whose contents the runtime knows and this module
does not. It is a written type [(defvar x dyn 5)] boxes the 5 and it does not. It is a written type [(defonce x dyn 5)] boxes the 5 and it
is also what an unannotated [defn] parameter means, which is why it is a is also what an unannotated [defn] parameter means, which is why it is a
case here and not a Named type the prelude declares: the checker has to case here and not a Named type the prelude declares: the checker has to
recognise it to choose the boxing and the dyn op lowering, and a name in a recognise it to choose the boxing and the dyn op lowering, and a name in a
@ -103,7 +103,7 @@ let fkind_of_name = function
is spelled [()] in source, and [Parse.texpr] refuses the word. is spelled [()] in source, and [Parse.texpr] refuses the word.
[int] and [float] are on it for the same reason they are in the two [int] and [float] are on it for the same reason they are in the two
functions above: the places that ask this list whether a [defvar]'s third functions above: the places that ask this list whether a [defonce]'s third
element is a type, whether [(vec-new int)] names an element type, whether a element is a type, whether [(vec-new int)] names an element type, whether a
[let] binding vector has an annotation wedged into it must answer the [let] binding vector has an annotation wedged into it must answer the
same for [int] as for [i32], or the alias is a type-position-only spelling same for [int] as for [i32], or the alias is a type-position-only spelling

View File

@ -24,7 +24,7 @@ facility (see plan.org, "Managed classes").
- `[n T]` is a value. It lives wherever it is declared, copies on assignment and - `[n T]` is a value. It lives wherever it is declared, copies on assignment and
on pass-by-value, and is what `defconst colors [4 u32] ...` and on pass-by-value, and is what `defconst colors [4 u32] ...` and
`(defvar grid [rows [cols u32]] ...)` are. `(defonce grid [rows [cols u32]] ...)` are.
- `[T]` is a **non-owning slice**: a borrowed window into a `[n T]`, a `(Vec T)`, - `[T]` is a **non-owning slice**: a borrowed window into a `[n T]`, a `(Vec T)`,
or a literal in read-only memory. Copying a slice copies ptr+len, never the or a literal in read-only memory. Copying a slice copies ptr+len, never the
elements. A slice may be `const`-qualified; freeing through one is not possible elements. A slice may be `const`-qualified; freeing through one is not possible
@ -136,7 +136,7 @@ A container global **starts zeroed** — a zeroed `Vec` is an empty `Vec` — an
loaded by whichever function loads it, with an ordinary assignment: loaded by whichever function loads it, with an ordinary assignment:
``` ```
(defvar the-data (Vec u8)) (defonce the-data (Vec u8))
(defn load [] () (set the-data (slurp "game-data.edn"))) (defn load [] () (set the-data (slurp "game-data.edn")))
``` ```
@ -217,7 +217,7 @@ provenance pass above may enforce it.
A fixed set of assignable forms, not a `setf`-style extensible place mechanism: A fixed set of assignable forms, not a `setf`-style extensible place mechanism:
``` ```
(set x v) ; a local or a defvar (set x v) ; a local or a defonce
(set (.field x) v) ; struct field; x may be a struct, (Ptr S) or (Handle S) (set (.field x) v) ; struct field; x may be a struct, (Ptr S) or (Handle S)
(set (at a i ...) v) ; fixed array, slice, or Vec element (set (at a i ...) v) ; fixed array, slice, or Vec element
(set (deref p) v) ; whole-object store through a pointer (set (deref p) v) ; whole-object store through a pointer

View File

@ -45,7 +45,7 @@ let generic_src n =
\ (set j (- j 1))))\n\ \ (set j (- j 1))))\n\
\ (set i (+ i 1)))))\n\n"; \ (set i (+ i 1)))))\n\n";
for i = 0 to n - 1 do for i = 0 to n - 1 do
Buffer.add_string b (Printf.sprintf "(defvar xs-%s [8 %s])\n" tys.(i) tys.(i)) Buffer.add_string b (Printf.sprintf "(defonce xs-%s [8 %s])\n" tys.(i) tys.(i))
done; done;
Buffer.add_string b "\n(defn main [] ()\n"; Buffer.add_string b "\n(defn main [] ()\n";
for i = 0 to n - 1 do for i = 0 to n - 1 do
@ -76,7 +76,7 @@ let mono_src n =
\ (set j (- j 1))))\n\ \ (set j (- j 1))))\n\
\ (set i (+ i 1)))))\n\n" \ (set i (+ i 1)))))\n\n"
t t t t t t t); t t t t t t t);
Buffer.add_string b (Printf.sprintf "(defvar xs-%s [8 %s])\n\n" t t) Buffer.add_string b (Printf.sprintf "(defonce xs-%s [8 %s])\n\n" t t)
done; done;
Buffer.add_string b "(defn main [] ()\n"; Buffer.add_string b "(defn main [] ()\n";
for i = 0 to n - 1 do for i = 0 to n - 1 do

View File

@ -29,8 +29,8 @@
(set i (+ i 1)) (set i (+ i 1))
(set j (- j 1))))) (set j (- j 1)))))
(defvar ns [5 i32]) (defonce ns [5 i32])
(defvar fs [5 f32]) (defonce fs [5 f32])
(defn main [] () (defn main [] ()
(let [xs (slice ns 0 5) (let [xs (slice ns 0 5)

View File

@ -10,34 +10,34 @@
;;;; arith.flan gives: a literal operand is exactly what a constant folder ;;;; arith.flan gives: a literal operand is exactly what a constant folder
;;;; removes, and a folded program does not contain the code being tested. ;;;; removes, and a folded program does not contain the code being tested.
(defvar i8hi i8 127) (defonce i8hi i8 127)
(defvar i16hi i16 32767) (defonce i16hi i16 32767)
(defvar i32hi i32 2147483647) (defonce i32hi i32 2147483647)
(defvar u8hi u8 255) (defonce u8hi u8 255)
(defvar u16hi u16 65535) (defonce u16hi u16 65535)
(defvar u32hi u32 4294967295) (defonce u32hi u32 4294967295)
(defvar i64hi i64 9223372036854775807) (defonce i64hi i64 9223372036854775807)
(defvar u64zero u64 0) (defonce u64zero u64 0)
(defvar one8 i8 1) (defonce one8 i8 1)
(defvar one16 i16 1) (defonce one16 i16 1)
(defvar one32 i32 1) (defonce one32 i32 1)
(defvar oneu8 u8 1) (defonce oneu8 u8 1)
(defvar oneu16 u16 1) (defonce oneu16 u16 1)
(defvar oneu32 u32 1) (defonce oneu32 u32 1)
(defvar one64 i64 1) (defonce one64 i64 1)
(defvar oneu64 u64 1) (defonce oneu64 u64 1)
(defvar big32 i32 123456789) (defonce big32 i32 123456789)
(defvar big64 i64 1234567890123) (defonce big64 i64 1234567890123)
(defvar three i32 3) (defonce three i32 3)
(defvar three64 i64 3) (defonce three64 i64 3)
(defvar seven i32 7) (defonce seven i32 7)
(defvar negsev i32 -7) (defonce negsev i32 -7)
(defvar shift i32 33) (defonce shift i32 33)
(defvar shift8 i8 9) (defonce shift8 i8 9)
(defvar f32one f32 1.0) (defonce f32one f32 1.0)
(defvar f32big f32 16777217.0) (defonce f32big f32 16777217.0)
(defn main [] i32 (defn main [] i32
;; ── Wrapping at every width ────────────────────────────────────── ;; ── Wrapping at every width ──────────────────────────────────────

View File

@ -18,7 +18,7 @@
(defstruct Point [x i32 y i32]) (defstruct Point [x i32 y i32])
(defstruct Box [lo Point hi Point n i32]) (defstruct Box [lo Point hi Point n i32])
(defvar gp Point) (defonce gp Point)
;; A parameter is a copy: writing to it must not reach the caller's value. ;; A parameter is a copy: writing to it must not reach the caller's value.
(defn bump [p Point] Point (defn bump [p Point] Point

View File

@ -7,7 +7,7 @@
;;;; the LLVM side says what the check costs when a compiler is allowed to ;;;; the LLVM side says what the check costs when a compiler is allowed to
;;;; hoist it out of the loop. ;;;; hoist it out of the loop.
(defvar xs [1024 i32]) (defonce xs [1024 i32])
(defn main [] i32 (defn main [] i32
(dotimes [i 1024] (dotimes [i 1024]

View File

@ -25,7 +25,7 @@
(defstruct Blip [n i32]) (defstruct Blip [n i32])
(defvar log i64) (defonce log i64)
(defn deep [n i32] i32 (defn deep [n i32] i32
(signal (Blip {.n n})) (signal (Blip {.n n}))

View File

@ -14,32 +14,32 @@
;;;; shift that can reach either backend out of range is a computed one, and a ;;;; shift that can reach either backend out of range is a computed one, and a
;;;; literal would be folded away before the code under test ran anyway. ;;;; literal would be folded away before the code under test ran anyway.
(defvar i8min i8 -128) (defonce i8min i8 -128)
(defvar i16min i16 -32768) (defonce i16min i16 -32768)
(defvar i32min i32 -2147483648) (defonce i32min i32 -2147483648)
(defvar i64min i64 -9223372036854775808) (defonce i64min i64 -9223372036854775808)
(defvar u8hi u8 255) (defonce u8hi u8 255)
(defvar u16hi u16 65535) (defonce u16hi u16 65535)
(defvar u32hi u32 4294967295) (defonce u32hi u32 4294967295)
(defvar one8 i8 1) (defonce one8 i8 1)
(defvar one16 i16 1) (defonce one16 i16 1)
(defvar one32 i32 1) (defonce one32 i32 1)
(defvar oneu8 u8 1) (defonce oneu8 u8 1)
(defvar oneu16 u16 1) (defonce oneu16 u16 1)
(defvar oneu32 u32 1) (defonce oneu32 u32 1)
(defvar one64 i64 1) (defonce one64 i64 1)
(defvar c8 i8 8) (defonce c8 i8 8)
(defvar c9 i8 9) (defonce c9 i8 9)
(defvar cu8 u8 8) (defonce cu8 u8 8)
(defvar c16 i16 16) (defonce c16 i16 16)
(defvar cu16 u16 17) (defonce cu16 u16 17)
(defvar c32 i32 32) (defonce c32 i32 32)
(defvar c33 i32 33) (defonce c33 i32 33)
(defvar cu32 u32 32) (defonce cu32 u32 32)
(defvar cu33 u32 33) (defonce cu33 u32 33)
(defvar c64 i64 64) (defonce c64 i64 64)
(defvar c65 i64 65) (defonce c65 i64 65)
(defn main [] i32 (defn main [] i32
;; ── The count exactly at the width: masks to 0, so nothing moves ── ;; ── The count exactly at the width: masks to 0, so nothing moves ──

View File

@ -23,7 +23,7 @@
;;;; condition corpus is written in. ;;;; condition corpus is written in.
(defstruct Oops [id i32]) (defstruct Oops [id i32])
(defvar trace i64) (defonce trace i64)
;;; The shape that had no answer. The handler-bind is this function's last ;;; The shape that had no answer. The handler-bind is this function's last
;;; form, so what it yields is what the function returns, two frames above the ;;; form, so what it yields is what the function returns, two frames above the

View File

@ -2,7 +2,7 @@
;; continue, the bitwise family, unsigned arithmetic and shifts, and the ;; continue, the bitwise family, unsigned arithmetic and shifts, and the
;; conversions in both directions. ;; conversions in both directions.
(defvar counter i64 0) (defonce counter i64 0)
(defconst limit i32 6) (defconst limit i32 6)

View File

@ -23,7 +23,7 @@
(defstruct Blip [n i32]) (defstruct Blip [n i32])
(defvar log i64) (defonce log i64)
;;; Two frames down, with a defer between, so the transfer crosses a function ;;; Two frames down, with a defer between, so the transfer crosses a function
;;; boundary and a transfer exit that has work to do. ;;; boundary and a transfer exit that has work to do.

View File

@ -14,7 +14,7 @@
;;;; Both cases go through a restart-case, so what is compared is the message ;;;; Both cases go through a restart-case, so what is compared is the message
;;;; on stderr as well as the fact that something was signalled. ;;;; on stderr as well as the fact that something was signalled.
(defvar a [4 i32]) (defonce a [4 i32])
(defn promised [n i32] i32 (defn promised [n i32] i32
;; n is a parameter, so the checker has no literal to look at. ;; n is a parameter, so the checker has no literal to look at.

View File

@ -1,4 +1,4 @@
(defvar log i64) (defonce log i64)
(defn quiet [n i32] i32 (defn quiet [n i32] i32
(defer (set log (+ log 1))) (defer (set log (+ log 1)))

View File

@ -11,7 +11,7 @@
;;;; case that has to keep working. ;;;; case that has to keep working.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
(defn tick [] i64 (defn tick [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -14,7 +14,7 @@
;;;; constructor either. See flan_agent.c's [auto_start]. ;;;; constructor either. See flan_agent.c's [auto_start].
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
(defn tick [] i64 (defn tick [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -13,7 +13,7 @@
;;; nothing for a shim to do. ;;; nothing for a shim to do.
(declare stdin-byte [] i32 "getchar") (declare stdin-byte [] i32 "getchar")
(defvar ticks i64) (defonce ticks i64)
(defn tick [] i64 (defn tick [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -13,7 +13,7 @@
;;;; vendor/agent/agent.flan. ;;;; vendor/agent/agent.flan.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
(defn tick [] i64 (defn tick [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -10,7 +10,7 @@
;; A zeroed Allocator. A global rather than a local because the arena has to ;; A zeroed Allocator. A global rather than a local because the arena has to
;; outlive the frame that makes it, and because a handler cannot see a local ;; outlive the frame that makes it, and because a handler cannot see a local
;; (check.ml's `captured` says so by name). ;; (check.ml's `captured` says so by name).
(defvar frame Allocator) (defonce frame Allocator)
;;; The context is a dynamic variable, so a function called from inside a ;;; The context is a dynamic variable, so a function called from inside a
;;; with-allocator body sees the rebinding without anything being passed. ;;; with-allocator body sees the rebinding without anything being passed.

View File

@ -17,7 +17,7 @@
;;;; two ways this dies. Each death is the whole test of its case, so they are ;;;; two ways this dies. Each death is the whole test of its case, so they are
;;;; separate runs rather than one program that could pass by dying early. ;;;; separate runs rather than one program that could pass by dying early.
(defvar frame Allocator) (defonce frame Allocator)
(defalias Row (Vec i32)) (defalias Row (Vec i32))

View File

@ -14,7 +14,7 @@
;;;; There is no drop, no destructor, no finalizer and no per-element teardown ;;;; There is no drop, no destructor, no finalizer and no per-element teardown
;;;; anywhere below. The release at the bottom of main is one call. ;;;; anywhere below. The release at the bottom of main is one call.
(defvar frame Allocator) (defonce frame Allocator)
(defdata Value (defdata Value
[(Nil []) [(Nil [])

View File

@ -44,22 +44,22 @@
;;;; is allowed to elide — a literal divisor that is neither 0 nor -1, and ;;;; is allowed to elide — a literal divisor that is neither 0 nor -1, and
;;;; unsigned division, which has no overflow case at all. ;;;; unsigned division, which has no overflow case at all.
(defvar frames i64) (defonce frames i64)
(defvar skipped i64) (defonce skipped i64)
(defvar cleaned i64) (defonce cleaned i64)
(defvar op i32) (defonce op i32)
(defvar lhs i64) (defonce lhs i64)
(defvar rhs i64) (defonce rhs i64)
;;; Globals rather than locals because a handler cannot see the locals of the ;;; Globals rather than locals because a handler cannot see the locals of the
;;; function that established it — check.ml refuses a capture by name and says ;;; function that established it — check.ml refuses a capture by name and says
;;; to use a global. ;;; to use a global.
(defvar zero i64) (defonce zero i64)
(defvar neg1 i64 -1) (defonce neg1 i64 -1)
(defvar big i64 9223372036854775807) (defonce big i64 9223372036854775807)
(defvar huge f64 1e300) (defonce huge f64 1e300)
(defvar small f64 -1e300) (defonce small f64 -1e300)
(defvar uz u32) (defonce uz u32)
(defn show [name string n i64] () (defn show [name string n i64] ()
(print name) (print " ") (print n) (println "")) (print name) (print " ") (print n) (println ""))

View File

@ -14,21 +14,21 @@
;;;; allowed to elide, and folding these away would leave the test asserting on ;;;; allowed to elide, and folding these away would leave the test asserting on
;;;; a program that does not contain the check. ;;;; a program that does not contain the check.
(defvar zero i64) (defonce zero i64)
(defvar neg1 i64 -1) (defonce neg1 i64 -1)
(defvar big i64 9223372036854775807) (defonce big i64 9223372036854775807)
(defvar ten i64 10) (defonce ten i64 10)
(defvar uz u32) (defonce uz u32)
(defvar huge f64 1e300) (defonce huge f64 1e300)
;; The narrow versions of the same two failures. They are here because they are ;; The narrow versions of the same two failures. They are here because they are
;; the ones the two backends reach by different routes: the overflow test ;; the ones the two backends reach by different routes: the overflow test
;; compares against the *narrow* type's most negative value inside a 64-bit ;; compares against the *narrow* type's most negative value inside a 64-bit
;; register, and the f32 range test is compared in f32 on one backend and in a ;; register, and the f32 range test is compared in f32 on one backend and in a
;; double on the other. Both routes are supposed to give the same answer and ;; double on the other. Both routes are supposed to give the same answer and
;; the survey is what says so. ;; the survey is what says so.
(defvar i32big i32 2147483647) (defonce i32big i32 2147483647)
(defvar m1-32 i32 -1) (defonce m1-32 i32 -1)
(defvar wide f32 1e30) (defonce wide f32 1e30)
(defn main [args [string]] i32 (defn main [args [string]] i32
(let [n (i32 (bytes->i64 (bytes-view (at args 1)))) (let [n (i32 (bytes->i64 (bytes-view (at args 1))))

View File

@ -7,7 +7,7 @@
(defstruct V2 [x f32 y f32]) (defstruct V2 [x f32 y f32])
(defconst n 3) (defconst n 3)
(defvar points [3 V2]) (defonce points [3 V2])
(defn sumx [ps [3 V2]] i32 (defn sumx [ps [3 V2]] i32
(let [t (f32 0.0)] (let [t (f32 0.0)]

View File

@ -3,7 +3,7 @@
;;;; DISCUSS.org's "need a value-producing array constructor": (array n T) is ;;;; DISCUSS.org's "need a value-producing array constructor": (array n T) is
;;;; the zeroed array and dotimes is Unit, so neither could be the initialiser ;;;; the zeroed array and dotimes is Unit, so neither could be the initialiser
;;;; expression of a declaration. These are expressions, so they compose where ;;;; expression of a declaration. These are expressions, so they compose where
;;;; a bracket literal does — including as a defvar's initialiser, which is the ;;;; a bracket literal does — including as a defonce's initialiser, which is the
;;;; line the note was written about. ;;;; line the note was written about.
;;;; ;;;;
;;;; The dimensions are in brackets and are the same compile-time lengths the ;;;; The dimensions are in brackets and are the same compile-time lengths the
@ -13,8 +13,8 @@
(defconst cols 4) (defconst cols 4)
;; The line from the note. A typed declaration with a computed initialiser, ;; The line from the note. A typed declaration with a computed initialiser,
;; which is the startup-lifted path a defvar already had. ;; which is the startup-lifted path a defonce already had.
(defvar grid [rows [cols u8]] (array-fill [rows cols] 255)) (defonce grid [rows [cols u8]] (array-fill [rows cols] 255))
;; One index per dimension, i32 each, and the return type is the element type. ;; One index per dimension, i32 each, and the return type is the element type.
(defn cell [r i32 c i32] i32 (+ (* r 100) c)) (defn cell [r i32 c i32] i32 (+ (* r 100) c))
@ -24,7 +24,7 @@
;; Row-major order is pinned, so a generator that counts observes it: this one ;; Row-major order is pinned, so a generator that counts observes it: this one
;; is called once per element and answers the call number, so the array it ;; is called once per element and answers the call number, so the array it
;; fills is 0 1 2 ... in the order the elements are written. ;; fills is 0 1 2 ... in the order the elements are written.
(defvar ticks i32) (defonce ticks i32)
(defn tick [r i32 c i32] i32 (defn tick [r i32 c i32] i32
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))
@ -34,7 +34,7 @@
(defstruct Cell [row i32 col i32]) (defstruct Cell [row i32 col i32])
;; Counts its own calls, for the evaluated-once line below. ;; Counts its own calls, for the evaluated-once line below.
(defvar calls i32) (defonce calls i32)
(defn bump [] i32 (defn bump [] i32
(set calls (+ calls 1)) (set calls (+ calls 1))
@ -75,7 +75,7 @@
(print (at t 0 0)) (print " ") (print (at t 0 1)) (print " ") (print (at t 0 0)) (print " ") (print (at t 0 1)) (print " ")
(print (at t 1 0)) (print " ") (print (at t 2 3)) (println "")) ; 0 1 4 11 (print (at t 1 0)) (print " ") (print (at t 2 3)) (println "")) ; 0 1 4 11
;; The defvar from the top: 255 everywhere, read back as an i32 so the ;; The defonce from the top: 255 everywhere, read back as an i32 so the
;; printed value is the number and not a byte. ;; printed value is the number and not a byte.
(print (i32 (at grid 0 0))) (print " ") (print (i32 (at grid 0 0))) (print " ")
(print (i32 (at grid 2 3))) (println "") ; 255 255 (print (i32 (at grid 2 3))) (println "") ; 255 255

View File

@ -44,17 +44,17 @@
;;;; runtime rather than in emitted IR, so those two are plumbed ;;;; runtime rather than in emitted IR, so those two are plumbed
;;;; separately and are the ones most likely to be left behind. ;;;; separately and are the ones most likely to be left behind.
(defvar grid [4 i32]) (defonce grid [4 i32])
;;; Handlers cannot see the locals of the function that established them — ;;; Handlers cannot see the locals of the function that established them —
;;; check.ml refuses a capture by name and says to use a global — so ;;; check.ml refuses a capture by name and says to use a global — so
;;; everything this program counts lives up here. ;;; everything this program counts lives up here.
(defvar frames i64) (defonce frames i64)
(defvar skipped i64) (defonce skipped i64)
(defvar cleaned i64) (defonce cleaned i64)
(defvar low i64) (defonce low i64)
(defvar high i64) (defonce high i64)
(defvar length i64) (defonce length i64)
;;; Two frames deep, with a defer on the way, so the transfer has something to ;;; Two frames deep, with a defer on the way, so the transfer has something to
;;; cross and something to run on its way out. ;;; cross and something to run on its way out.

View File

@ -13,7 +13,7 @@
;;;; The selector is also the index wherever it can be, which is what keeps the ;;;; The selector is also the index wherever it can be, which is what keeps the
;;;; index dynamic — a literal would let the checker reject it outright one day ;;;; index dynamic — a literal would let the checker reject it outright one day
;;;; (that is a separate job) and lets LLVM fold the branch away here. ;;;; (that is a separate job) and lets LLVM fold the branch away here.
(defvar arr [3 i32]) (defonce arr [3 i32])
(defn main [args [string]] i32 (defn main [args [string]] i32
(let [n (i32 (bytes->i64 (bytes-view (at args 1)))) (let [n (i32 (bytes->i64 (bytes-view (at args 1))))

View File

@ -7,9 +7,9 @@
(defstruct Missing [id i32]) (defstruct Missing [id i32])
(defstruct Other [id i32]) (defstruct Other [id i32])
(defvar log i64) (defonce log i64)
(defvar order i64) (defonce order i64)
(defvar seen i64) (defonce seen i64)
(defn note [n i64] () (set order (+ (* order 10) n))) (defn note [n i64] () (set order (+ (* order 10) n)))

View File

@ -8,8 +8,8 @@
(defstruct AssetMissing [id i32]) (defstruct AssetMissing [id i32])
(defstruct Corrupt [id i32]) (defstruct Corrupt [id i32])
(defvar seen i64) (defonce seen i64)
(defvar other i64) (defonce other i64)
;;; Signals twice and keeps going both times — that is the whole of §1. ;;; Signals twice and keeps going both times — that is the whole of §1.
(defn load-all [] () (defn load-all [] ()

View File

@ -9,7 +9,7 @@
;;;; ;;;;
;;;; The numbers differ per failure, so a wrong answer names its own cause. ;;;; The numbers differ per failure, so a wrong answer names its own cause.
(defvar order i64) (defonce order i64)
(defn note [n i32] () (set order (+ (* order 10) (i64 n)))) (defn note [n i32] () (set order (+ (* order 10) (i64 n))))

View File

@ -1,4 +1,4 @@
;;;; The third element of a defvar, both ways. ;;;; The third element of a defonce, both ways.
;;;; ;;;;
;;;; The rule, 2026-09-20: a type there is the zeroed static global it has ;;;; The rule, 2026-09-20: a type there is the zeroed static global it has
;;;; always been, and anything else is a dyn global initialised from that ;;;; always been, and anything else is a dyn global initialised from that
@ -7,7 +7,7 @@
;;;; one of its declarations got. ;;;; one of its declarations got.
;;;; ;;;;
;;;; There is no new lowering under any of it. The dyn half is exactly what ;;;; There is no new lowering under any of it. The dyn half is exactly what
;;;; [(defvar x dyn <expr>)] already compiled to — the initialiser lifted into ;;;; [(defonce x dyn <expr>)] already compiled to — the initialiser lifted into
;;;; the startup function that main calls after the runtime is up, and a ;;;; the startup function that main calls after the runtime is up, and a
;;;; collector root pushed for the global before it runs — which is why this ;;;; collector root pushed for the global before it runs — which is why this
;;;; program runs identically on both backends and why neither of them ;;;; program runs identically on both backends and why neither of them
@ -18,29 +18,29 @@
;; ── The type reading, which is every declaration that worked before ── ;; ── The type reading, which is every declaration that worked before ──
;; A primitive, a fixed array, a struct and a container: all four are types in ;; A primitive, a fixed array, a struct and a container: all four are types in
;; the third position, so all four are the zeroed statics they were. ;; the third position, so all four are the zeroed statics they were.
(defvar current-color i32) (defonce current-color i32)
(defvar grid [2 [3 u32]]) (defonce grid [2 [3 u32]])
(defvar origin Point) (defonce origin Point)
(defvar bytes (Vec u8)) (defonce bytes (Vec u8))
;; ── The value reading, which is the new spelling ───────────────────── ;; ── The value reading, which is the new spelling ─────────────────────
;; None of these names a type, so each is a dyn global holding the value its ;; None of these names a type, so each is a dyn global holding the value its
;; expression produced before the program's own code ran. ;; expression produced before the program's own code ran.
(defvar score 0) (defonce score 0)
(defvar label "start") (defonce label "start")
(defvar config {:level 1 :name "one"}) (defonce config {:level 1 :name "one"})
(defvar tally seeded) (defonce tally seeded)
;; A call, which is the shape the author kept writing: the file is read once, ;; A call, which is the shape the author kept writing: the file is read once,
;; at startup, into a global that outlives main. ;; at startup, into a global that outlives main.
(defn load [] dyn {:rows 3 :cols 4}) (defn load [] dyn {:rows 3 :cols 4})
(defvar game-data (load)) (defonce game-data (load))
;; And the value the bare symbol above was initialised from, declared *below* ;; And the value the bare symbol above was initialised from, declared *below*
;; it on purpose: which reading a defvar gets is decided with every name in ;; it on purpose: which reading a defonce gets is decided with every name in
;; hand, not in the order the file was written. ;; hand, not in the order the file was written.
(defvar seeded i64 7) (defonce seeded i64 7)
(defn main [] () (defn main [] ()
;; The statics, untouched: zero, zero, zero, and an empty Vec that is a real ;; The statics, untouched: zero, zero, zero, and an empty Vec that is a real

View File

@ -16,7 +16,7 @@
(defstruct Point [x i32 y i32]) (defstruct Point [x i32 y i32])
(defstruct Line [a Point b Point]) (defstruct Line [a Point b Point])
(defvar calls i32) (defonce calls i32)
(defn make-point [] Point (defn make-point [] Point
(set calls (+ calls 1)) (set calls (+ calls 1))

View File

@ -20,8 +20,8 @@
;;;; bounds failure now lands somewhere a session can be recovered from. ;;;; bounds failure now lands somewhere a session can be recovered from.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar grid [4 i32]) (defonce grid [4 i32])
(defvar skipped i64) (defonce skipped i64)
;;; One frame deep under the restart-case, so the transfer has something to ;;; One frame deep under the restart-case, so the transfer has something to
;;; cross and the backtrace has something to show. ;;; cross and the backtrace has something to show.
@ -33,7 +33,7 @@
(do (touch i) (println "frame done")) (do (touch i) (println "frame done"))
(continue [] (set skipped (+ skipped 1))))) (continue [] (set skipped (+ skipped 1)))))
(defvar ticks i64) (defonce ticks i64)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-break-bounds-fallback.sock") (agent/start "/tmp/flan-dev-break-bounds-fallback.sock")

View File

@ -18,7 +18,7 @@
(use-placeholder [] -1) (use-placeholder [] -1)
(retry [] 7))) (retry [] 7)))
(defvar ticks i64) (defonce ticks i64)
(defn step [] i64 (defn step [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -17,7 +17,7 @@
;;; Counted so that an evaluation has something of the program's own to read, ;;; Counted so that an evaluation has something of the program's own to read,
;;; and so a transcript can be checked for progress rather than only for text. ;;; and so a transcript can be checked for progress rather than only for text.
(defvar frames i64) (defonce frames i64)
(defn chatter [] i64 (defn chatter [] i64
;; Sixty-four lines of sixty-three characters and a newline: 4096 bytes a ;; Sixty-four lines of sixty-three characters and a newline: 4096 bytes a

View File

@ -36,7 +36,7 @@
(defmethod area point [p] (* (get p :x) (get p :y))) (defmethod area point [p] (* (get p :x) (get p :y)))
(defvar instances dyn) (defonce instances dyn)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-classes-fallback.sock") (agent/start "/tmp/flan-dev-classes-fallback.sock")

View File

@ -16,7 +16,7 @@
;;;; and everything this fixture is for happens after it. ;;;; and everything this fixture is for happens after it.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar config dyn) (defonce config dyn)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-dyn-global-fallback.sock") (agent/start "/tmp/flan-dev-dyn-global-fallback.sock")

View File

@ -2,7 +2,7 @@
;;;; ;;;;
;;;; dev-locals.flan is about what one frame holds. This is about what the ;;;; dev-locals.flan is about what one frame holds. This is about what the
;;;; whole stopped stack is reading, which in this language is most of the ;;;; whole stopped stack is reading, which in this language is most of the
;;;; program: a game keeps its state in top-level defvars, and sand.flan holds ;;;; program: a game keeps its state in top-level defonces, and sand.flan holds
;;;; its entire grid that way. ;;;; its entire grid that way.
;;;; ;;;;
;;;; The globals are declared in an order the answer must *not* come back in. ;;;; The globals are declared in an order the answer must *not* come back in.
@ -16,10 +16,10 @@
(defstruct Boom [why i32]) (defstruct Boom [why i32])
(defvar label string) (defonce label string)
(defvar grid [4 i32]) (defonce grid [4 i32])
(defvar pressure i64) (defonce pressure i64)
(defvar untouched i64 99) (defonce untouched i64 99)
;; The inner frame. It writes two globals and then errors with nothing ;; The inner frame. It writes two globals and then errors with nothing
;; handling the condition, so the program stops here with [main] under it. ;; handling the condition, so the program stops here with [main] under it.

View File

@ -24,7 +24,7 @@
;; The discriminator. `outer' binds a local of this name to something else, so ;; The discriminator. `outer' binds a local of this name to something else, so
;; every claim about which frame answered is visible in the value itself. ;; every claim about which frame answered is visible in the value itself.
(defvar mark i64) (defonce mark i64)
;; The innermost frame, and it is deliberately dull: it holds nothing worth ;; The innermost frame, and it is deliberately dull: it holds nothing worth
;; inspecting, so that the frame worth inspecting is not the one an expression ;; inspecting, so that the frame worth inspecting is not the one an expression
@ -41,7 +41,7 @@
s (Shape.Rect {.w 3 .h 6})] s (Shape.Rect {.w 3 .h 6})]
(deeper))) (deeper)))
(defvar ticks i64) (defonce ticks i64)
(defn main [] i32 (defn main [] i32
(set mark 99) (set mark 99)

View File

@ -20,7 +20,7 @@
;;;; a real daemon as well. ;;;; a real daemon as well.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar frames i64) (defonce frames i64)
(defn step [] i64 7) (defn step [] i64 7)

View File

@ -39,7 +39,7 @@
(let [after (i64 99)] after)) (let [after (i64 99)] after))
(carry-on [] 5))))) (carry-on [] 5)))))
(defvar ticks i64) (defonce ticks i64)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-locals-fallback.sock") (agent/start "/tmp/flan-dev-locals-fallback.sock")

View File

@ -9,7 +9,7 @@
;;;; landing there is the zero-argument form working end to end. ;;;; landing there is the zero-argument form working end to end.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
;;; A condition and something that signals it, so that a body typed in later ;;; A condition and something that signals it, so that a body typed in later
;;; can establish a handler and transfer past this frame — spec-conditions.md ;;; can establish a handler and transfer past this frame — spec-conditions.md

View File

@ -16,7 +16,7 @@
;;;; running beside it. ;;;; running beside it.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
(defn parity [n i64] string (defn parity [n i64] string
(let [out "even"] (let [out "even"]

View File

@ -10,7 +10,7 @@
;;;; back to a fresh park in one op. ;;;; back to a fresh park in one op.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar runs i64) (defonce runs i64)
(defn step [] i64 7) (defn step [] i64 7)

View File

@ -10,7 +10,7 @@
;;;; tail with nothing in front of it. ;;;; tail with nothing in front of it.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
;;; Something to stop on that is *not* a breakpoint, so that a marked ;;; Something to stop on that is *not* a breakpoint, so that a marked
;;; expression sent to an already-stopped program can be told from the break it ;;; expression sent to an already-stopped program can be told from the break it

View File

@ -34,8 +34,8 @@
;;; a printed line because a global is reachable by name from `C-x C-e' while ;;; a printed line because a global is reachable by name from `C-x C-e' while
;;; the program is stopped, and a local is not: the test asks the session for ;;; the program is stopped, and a local is not: the test asks the session for
;;; them the way a person at the break loop would. ;;; them the way a person at the break loop would.
(defvar live-addr i64) (defonce live-addr i64)
(defvar dead-addr i64) (defonce dead-addr i64)
(defn deeper [] i64 (defn deeper [] i64
(restart-case (restart-case
@ -54,7 +54,7 @@
(set dead-addr (ptr-num dead)) (set dead-addr (ptr-num dead))
(deeper)))) (deeper))))
(defvar ticks i64) (defonce ticks i64)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-ptr-fallback.sock") (agent/start "/tmp/flan-dev-ptr-fallback.sock")

View File

@ -11,7 +11,7 @@
;;; because the break loop matches on a class the *host* was compiled with. ;;; because the break loop matches on a class the *host* was compiled with.
(defstruct Missing [id i32]) (defstruct Missing [id i32])
(defvar ticks i64) (defonce ticks i64)
(defn step [] i64 (defn step [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))
ticks) ticks)

View File

@ -1,7 +1,8 @@
;;;; What a re-run does to a global, which is decided by the form that ;;;; What a re-run does to a global, which is decided by the form that
;;;; defined it and not by the daemon. ;;;; defined it and not by the daemon.
;;;; ;;;;
;;;; A [defvar] is Common Lisp's [defvar]: its initialiser runs only if the ;;;; A [defonce] is Common Lisp's [defvar] under Clojure's name: its
;;;; initialiser runs only if the
;;;; variable is not already initialised, so its value survives a re-run. A ;;;; 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 ;;;; 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] ;;;; main — and a computed one did not, because the startup function [main]
@ -12,7 +13,7 @@
;;;; 2026-09-20 there is no other kind: a defconst's initialiser has to be a ;;;; 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 ;;;; compile-time constant, refused in the checker so that both backends
;;;; refuse the same program. Before that the LLVM backend refused a computed ;;;; refuse the same program. Before that the LLVM backend refused a computed
;;;; one while x86 guarded it at startup like a defvar. ;;;; one while x86 guarded it at startup like a defonce.
;;;; ;;;;
;;;; So each line printed below is a claim about one of those cases, and the ;;;; 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 ;;;; run number is the first of them: [runs] is computed, so before the fix it
@ -25,10 +26,10 @@
;; lifted into the startup function rather than written into the image. ;; lifted into the startup function rather than written into the image.
(defn start [] i64 base) (defn start [] i64 base)
(defvar counter i64 (start)) (defonce counter i64 (start))
;; Zero-valued, which needs no startup at all and must keep needing none. ;; Zero-valued, which needs no startup at all and must keep needing none.
(defvar zeroed i64) (defonce zeroed i64)
;; A computed dyn global: the map is built by a function, rooted before the ;; 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 ;; startup function runs, and mutated by every run. Its contents have to
@ -36,7 +37,7 @@
;; has to survive collection either way. ;; has to survive collection either way.
(defn table [] dyn {:runs 0}) (defn table [] dyn {:runs 0})
(defvar state dyn (table)) (defonce state dyn (table))
;; The same thing written the short way: the third element is not a type, so ;; 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] ;; this is a dyn global initialised at startup — the same declaration [state]
@ -44,7 +45,7 @@
;; a second one. Its value has to survive a re-run for exactly [counter]'s ;; 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 ;; 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. ;; the line that would count 1, 1, 1, 1.
(defvar tally 0) (defonce tally 0)
;; A typed array with a computed initialiser: (array-fill ...) is an ;; A typed array with a computed initialiser: (array-fill ...) is an
;; expression, so it is lifted into the startup function and guarded there ;; expression, so it is lifted into the startup function and guarded there
@ -52,7 +53,7 @@
;; entry into main — this would count 251, 251, 251, 251 instead of climbing, ;; 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 ;; which is [counter]'s own failure in the one shape that only an array can
;; have. ;; have.
(defvar grid [2 [3 u8]] (array-fill [2 3] 250)) (defonce grid [2 [3 u8]] (array-fill [2 3] 250))
;; The guard flags the fix adds are the compiler's own globals, and they used ;; 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 [.] ;; to be spelled [.init-once.<name>] — a name a program can write, since [.]
@ -61,7 +62,20 @@
;; now; before that the dev build died at the assembler with the symbol ;; 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 ;; 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. ;; unprinted on purpose — the expected output is what it was.
(defvar .init-once.counter i64 7) (defonce .init-once.counter i64 7)
;; [def], Common Lisp's defparameter: its initialiser runs on every re-run,
;; with no guard flag, so the increment each run makes is painted over before
;; main reads it back. Where [tally] climbs 1, 2, 3, 4, this prints 4, 4, 4, 4
;; — and after the daemon evaluates an edited (def c 9), the next re-run
;; prints 10, which is the whole reason the form exists: an edited
;; initialiser takes effect on C-c C-c plus re-run.
(def c 3)
;; The typed-array spelling of the same form. The re-run repaints the same
;; storage — no reallocation — so the element main increments is 7 again by
;; the time it is read: 8 on every run, never 9.
(def hues [4 u32] (array-fill [4] 7))
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-rerun-fallback.sock") (agent/start "/tmp/flan-dev-rerun-fallback.sock")
@ -71,6 +85,8 @@
(put state :runs (+ (get state :runs) 1)) (put state :runs (+ (get state :runs) 1))
(set tally (+ tally 1)) (set tally (+ tally 1))
(set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1))) (set (at grid 0 0) (u8 (+ (i32 (at grid 0 0)) 1)))
(set c (+ c 1))
(set (at hues 0) (u32 (+ (i32 (at hues 0)) 1)))
(print "counter ") (print counter) (println "") (print "counter ") (print counter) (println "")
(print "zeroed ") (print zeroed) (println "") (print "zeroed ") (print zeroed) (println "")
(print "runs ") (print (get state :runs)) (println "") (print "runs ") (print (get state :runs)) (println "")
@ -80,6 +96,8 @@
;; on every run, and 0 if the initialiser had been skipped outright. ;; on every run, and 0 if the initialiser had been skipped outright.
(print "grid-far ") (print (i32 (at grid 1 2))) (println "") (print "grid-far ") (print (i32 (at grid 1 2))) (println "")
(print "base ") (print base) (println "") (print "base ") (print base) (println "")
(print "c ") (print c) (println "")
(print "hue ") (print (i32 (at hues 0))) (println "")
;; Long enough for a client to be served, short enough to park well inside ;; Long enough for a client to be served, short enough to park well inside
;; any watchdog — dev-macro.flan's clock, for its reason. ;; any watchdog — dev-macro.flan's clock, for its reason.
(dotimes [i 100] (dotimes [i 100]

View File

@ -14,7 +14,7 @@
;;;; leave a process of this behind for the rest of the afternoon. ;;;; leave a process of this behind for the rest of the afternoon.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar ticks i64) (defonce ticks i64)
(defn step [] i64 (defn step [] i64
(set ticks (+ ticks 1)) (set ticks (+ ticks 1))

View File

@ -1,7 +1,7 @@
;;;; The same claim as dev-trap-free-all.flan over the other allocator trap, ;;;; The same claim as dev-trap-free-all.flan over the other allocator trap,
;;;; and with nothing on the restart stack. ;;;; and with nothing on the restart stack.
;;;; ;;;;
;;;; `nowhere` is a zeroed `Allocator` — a `defvar` nobody assigned — and ;;;; `nowhere` is a zeroed `Allocator` — a `defonce` nobody assigned — and
;;;; `free-all` on it is the "I released the region" / "I never made one" ;;;; `free-all` on it is the "I released the region" / "I never made one"
;;;; collapse the runtime refuses to let a program spell the same way. The ;;;; collapse the runtime refuses to let a program spell the same way. The
;;;; refusal is a trap with no transfer channel, so it parks rather than ;;;; refusal is a trap with no transfer channel, so it parks rather than
@ -10,7 +10,7 @@
;;;; can fix the program in and a daemon that is gone. ;;;; can fix the program in and a daemon that is gone.
(import agent "vendor:agent") (import agent "vendor:agent")
(defvar nowhere Allocator) (defonce nowhere Allocator)
(defn main [] i32 (defn main [] i32
(agent/start "/tmp/flan-dev-trap-null-alloc-fallback.sock") (agent/start "/tmp/flan-dev-trap-null-alloc-fallback.sock")

View File

@ -20,7 +20,7 @@
;; exists for; see flan_dev.c, "A number sampled thousands of times a frame". ;; exists for; see flan_dev.c, "A number sampled thousands of times a frame".
(declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64") (declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64")
(defvar ticks i64) (defonce ticks i64)
(defn loop-cells [] i32 (defn loop-cells [] i32
(let [i 0] (let [i 0]

View File

@ -10,10 +10,10 @@
;;;; be unboxed at are the ones that do: a parameter, a return type, and a ;;;; be unboxed at are the ones that do: a parameter, a return type, and a
;;;; global's declared type. All three are here. ;;;; global's declared type. All three are here.
(defvar seven i64 7) (defonce seven i64 7)
(defvar boxed dyn 21) (defonce boxed dyn 21)
;; The other direction at a global: a dyn initialiser meeting a written type. ;; The other direction at a global: a dyn initialiser meeting a written type.
(defvar unboxed i64 boxed) (defonce unboxed i64 boxed)
(defn take-dyn [d dyn] dyn (defn take-dyn [d dyn] dyn
(+ d 100)) (+ d 100))

View File

@ -7,8 +7,8 @@
;;;; the computed globals already use, which is the point: a dyn global is a ;;;; the computed globals already use, which is the point: a dyn global is a
;;;; computed global and needed no new mechanism. ;;;; computed global and needed no new mechanism.
(defvar counter dyn 0) (defonce counter dyn 0)
(defvar label dyn "start") (defonce label dyn "start")
(defn bump [] () (defn bump [] ()
(set counter (+ counter 1))) (set counter (+ counter 1)))

Some files were not shown because too many files have changed in this diff Show More