From a4c6b996ff741aff5f351cba0d97b53e5974e334 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 22:46:19 +0700 Subject: [PATCH] def re-runs its initialiser, and defvar is renamed defonce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/, 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. --- FIX.org | 73 +++ NEXT.md | 20 +- bin/main.ml | 3 +- conditions-play.flan | 4 +- docs/BUILT.md | 67 ++- emacs/MANUAL.md | 6 +- emacs/flan-mode.el | 8 +- emacs/flan.el | 22 +- emacs/test-flan.el | 33 +- examples/core-2d-camera.flan | 10 +- examples/core-3d-picking.flan | 10 +- examples/core-delta-time.flan | 8 +- examples/core-input-gamepad.flan | 2 +- examples/core-input-gestures-testbed.flan | 20 +- examples/core-input-gestures.flan | 10 +- examples/core-input-keys.flan | 4 +- examples/core-input-mouse-wheel.flan | 2 +- examples/core-input-mouse.flan | 4 +- examples/core-input-multitouch.flan | 4 +- examples/core-input-virtual-controls.flan | 4 +- examples/core-scissor-test.flan | 4 +- examples/core-window-flags.flan | 6 +- examples/core-window-should-close.flan | 6 +- examples/core-world-screen.flan | 4 +- examples/models-box-collisions.flan | 14 +- examples/shapes-basic-shapes.flan | 2 +- examples/shapes-collision-area.flan | 12 +- examples/shapes-following-eyes.flan | 8 +- examples/text-codepoints-loading.flan | 16 +- examples/text-input-box.flan | 10 +- examples/text-writing-anim.flan | 2 +- examples/textures-fog-of-war.flan | 12 +- examples/textures-image-generation.flan | 4 +- examples/textures-image-processing.flan | 14 +- examples/textures-mouse-painting.flan | 24 +- lib/ast.ml | 29 +- lib/check.ml | 174 +++---- lib/dev.ml | 11 +- lib/emit.ml | 93 ++-- lib/load.ml | 11 +- lib/macro.ml | 2 +- lib/mangle.ml | 2 +- lib/parse.ml | 53 ++- lib/prelude.ml | 4 +- lib/reach.ml | 2 +- lib/session.ml | 30 +- lib/tast.ml | 4 + lib/types.ml | 4 +- spec-memory.md | 6 +- spike/generics/measure.ml | 4 +- spike/generics/prelude-shapes.flan | 4 +- spike/js/p1-int-semantics.flan | 52 +-- spike/js/p2-value-copies.flan | 2 +- spike/x86/bench/b2-bounds.flan | 2 +- spike/x86/p10-defer-transfer.flan | 2 +- spike/x86/p11-shift-edges.flan | 50 +- spike/x86/p12-handler-value.flan | 2 +- spike/x86/p5-core.flan | 2 +- spike/x86/p6-transfer.flan | 2 +- spike/x86/p7-slice-from-ptr.flan | 2 +- spike/x86/p9-dead-defers.flan | 2 +- test/programs/agent-auto.flan | 2 +- test/programs/agent-nostart.flan | 2 +- test/programs/agent-queue.flan | 2 +- test/programs/agent.flan | 2 +- test/programs/allocators.flan | 2 +- test/programs/arena-region.flan | 2 +- test/programs/arena-value.flan | 2 +- test/programs/arith-condition.flan | 24 +- test/programs/arith.flan | 18 +- test/programs/array-ctor.flan | 2 +- test/programs/array-fill.flan | 12 +- test/programs/bounds-condition.flan | 14 +- test/programs/bounds.flan | 2 +- test/programs/cleanup.flan | 6 +- test/programs/conditions.flan | 4 +- test/programs/defer-let.flan | 2 +- .../{defvar-dyn.flan => defonce-dyn.flan} | 26 +- test/programs/destructure.flan | 2 +- test/programs/dev-break-bounds.flan | 6 +- test/programs/dev-break.flan | 2 +- test/programs/dev-chatty.flan | 2 +- test/programs/dev-classes.flan | 2 +- test/programs/dev-dyn-global.flan | 2 +- test/programs/dev-globals.flan | 10 +- test/programs/dev-inspect.flan | 4 +- test/programs/dev-lateagent.flan | 2 +- test/programs/dev-locals.flan | 2 +- test/programs/dev-loop.flan | 2 +- test/programs/dev-macro.flan | 2 +- test/programs/dev-parknote.flan | 2 +- test/programs/dev-pause.flan | 2 +- test/programs/dev-ptr.flan | 6 +- test/programs/dev-repl.flan | 2 +- test/programs/dev-rerun.flan | 34 +- test/programs/dev-robust.flan | 2 +- test/programs/dev-trap-null-alloc.flan | 4 +- test/programs/dev-watch.flan | 2 +- test/programs/dyn-boundary.flan | 6 +- test/programs/dyn-global.flan | 4 +- test/programs/dyn-map.flan | 2 +- test/programs/dyn-struct.flan | 6 +- test/programs/dyn-view.flan | 10 +- test/programs/edn-read.flan | 6 +- test/programs/exhausted.flan | 10 +- test/programs/files.flan | 6 +- test/programs/fill.flan | 4 +- test/programs/fn-values.flan | 2 +- test/programs/frame-rollback.flan | 20 +- test/programs/global-init.flan | 34 +- test/programs/handler-case.flan | 4 +- test/programs/init-conditions.flan | 14 +- test/programs/int-float.flan | 4 +- test/programs/int-generic.flan | 4 +- test/programs/into.flan | 4 +- test/programs/json.flan | 2 +- test/programs/machine.flan | 4 +- test/programs/map-exhausted.flan | 8 +- test/programs/math2.flan | 2 +- test/programs/math3.flan | 20 +- test/programs/nil-option.flan | 2 +- test/programs/nth-gone.flan | 2 +- test/programs/pkg-data.flan | 2 +- test/programs/pkgs/shadowed/shadowed.flan | 2 +- test/programs/prelude-macros.flan | 2 +- test/programs/printers.flan | 10 +- test/programs/println.flan | 18 +- test/programs/raylib-audio.flan | 2 +- test/programs/raylib-codepoints.flan | 8 +- test/programs/raylib-font.flan | 4 +- test/programs/reach-walk.flan | 4 +- test/programs/registry.flan | 2 +- test/programs/reload-agg-v2.flan | 2 +- test/programs/reload-agg.flan | 2 +- test/programs/reload-generic.flan | 2 +- test/programs/reload-v2.flan | 4 +- test/programs/reload-v3.flan | 8 +- test/programs/reload-v4.flan | 4 +- test/programs/reload-v5.flan | 4 +- test/programs/reload-v6.flan | 6 +- test/programs/reload.flan | 6 +- test/programs/restarts.flan | 2 +- test/programs/shadow-builtin.flan | 4 +- test/programs/shadow-pkg.flan | 2 +- test/programs/slice-from-ptr.flan | 2 +- test/programs/slices.flan | 6 +- test/programs/slurp.flan | 8 +- test/programs/unions.flan | 4 +- test/programs/utf8.flan | 2 +- test/programs/values.flan | 2 +- test/programs/vec-global.flan | 4 +- test/programs/widening.flan | 24 +- test/test_acceptance.ml | 43 +- test/test_agent.ml | 2 +- test/test_dev.ml | 71 ++- test/test_flan.ml | 429 +++++++++++------- test/test_reload.ml | 4 +- test/test_repl.ml | 4 +- test/test_sanitize.ml | 2 +- test/test_session.ml | 12 +- test/test_valgrind.ml | 2 +- vendor/raylib/raylib.flan | 6 +- web/examples/arith.flan | 2 +- web/examples/arrays.flan | 2 +- web/examples/cast.flan | 2 +- web/examples/conds.flan | 2 +- web/examples/globals.flan | 4 +- web/examples/places.flan | 6 +- web/examples/restart.flan | 2 +- web/index.html | 26 +- 170 files changed, 1297 insertions(+), 875 deletions(-) rename test/programs/{defvar-dyn.flan => defonce-dyn.flan} (85%) diff --git a/FIX.org b/FIX.org index 3167823..7d21695 100644 --- a/FIX.org +++ b/FIX.org @@ -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 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/] — 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/] 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). diff --git a/NEXT.md b/NEXT.md index 6683727..eae09e3 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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 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 -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 expands is the prelude's macros and the imported packages' — and, since the lane recorded just below, the file's own as well. @@ -455,7 +455,7 @@ the Emacs side". **Superseded** — the registry lane built that properly. Delet ## For siam-farmer `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. ## 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: ```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 instead of two variables kept in step by hand. That is the argument for building this, and it is stronger than "Odin has it". @@ -1420,7 +1420,7 @@ program, at that program's next frame boundary. Verified against sand: an unsave 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 -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 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. `[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. `(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 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 *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 `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 - 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 considering: - **`(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 `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 -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 @@ -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. - **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 -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 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 diff --git a/bin/main.ml b/bin/main.ml index f950233..76016f2 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -83,7 +83,8 @@ let summarise (d : Flan.Ast.decl) = | Defdata (n, vs) -> Printf.sprintf "defdata %s (%d cases)" n (List.length vs) | Defunion (n, 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 | Declare (fn, csym) -> Printf.sprintf "declare %s (%d params) = %s" fn.name (List.length fn.params) diff --git a/conditions-play.flan b/conditions-play.flan index 05e0451..d825e24 100644 --- a/conditions-play.flan +++ b/conditions-play.flan @@ -10,8 +10,8 @@ ;;; Handlers cannot see the establishing function's locals yet, so anything a ;;; handler accumulates into has to be a global. -(defvar seen i64) -(defvar ticks i64) +(defonce seen i64) +(defonce ticks i64) ;;; Two frames under the restart-case, so a transfer has something to cross. (defn probe [n i32] i32 diff --git a/docs/BUILT.md b/docs/BUILT.md index 6a7ed43..714cb84 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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 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 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 -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 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 @@ -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]`, 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. -- `(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. 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 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 }` 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 @@ -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 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. -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 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 @@ -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 -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: ``` @@ -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 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` 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 @@ -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, 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. 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` `flan dev ` 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 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 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. **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 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. ``` @@ -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 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 -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. 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 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. ### 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". - **`(.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. -- **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` 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. @@ -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 -`[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 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]] @@ -3688,7 +3688,7 @@ call already went through. (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 `double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does 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. 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 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* @@ -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 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. - **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. @@ -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 -`@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 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 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 -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 `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 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 -`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 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 `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. +## `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/`, 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. diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 56ee88d..a793866 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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 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 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 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. 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 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. It is **one section, not a fold under each frame**. A global is not part of a diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index f6c7379..8aff887 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -119,7 +119,7 @@ ;; patched only when somebody notices a gap is the list that is always a ;; release behind the parser. (defconst flan--definers - '("defn" "defmacro" "defvar" "defconst" "defstruct" "defdata" "defunion" + '("defn" "defmacro" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" ;; The object and dispatch heads (lib/parse.ml:1277-1334). "defclass" "defgeneric" "defmulti" "defmethod" @@ -185,7 +185,9 @@ below — and not again here.") ("Types" ,(concat "^(def\\(?:struct\\|data\\|union\\|enum\\|alias\\)\\s-+" flan--name-re) 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 ;; otherwise show the same name twice with nothing to tell them apart. ("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)) ;; No spec. Anything else spelled `def…' is a definition and indents ;; like one, which covers `defstruct', `defdata', `defunion', - ;; `defenum', `defvar', `defconst' and `defalias' without naming + ;; `defenum', `defonce', `defconst' and `defalias' without naming ;; them. ((and name (string-match-p "\\`def" name)) (+ lisp-body-indent head-column)) diff --git a/emacs/flan.el b/emacs/flan.el index e306533..121ee36 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -51,10 +51,10 @@ ;; ;; 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 -;; 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 -;; split used to be by keybinding, which meant a top-level `defvar' under -;; C-x C-e came back as "defvar is a top-level declaration, not an +;; split used to be by keybinding, which meant a top-level `defonce' under +;; 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 ;; 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))) ((equal (nth 3 d) "") ;; 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. (user-error "flan: %s is a %s, and the daemon reports no location for one" (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 ;; 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, -;; 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) "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 ;; 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 ;; installed in 4 ms (also ticks)": the one name that actually ;; 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 ;; rule that was never quite true. (defconst flan--declaration-heads - '("defmacro" "defn" "defvar" "defconst" + '("defmacro" "defn" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" ;; The object and dispatch heads (lib/parse.ml:1277-1334), which are ;; 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) "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 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 @@ -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. 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 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 -of a file came back as \"defvar is a top-level declaration, not an +paths; this key used to reach only the first, so a `defonce' typed at the top +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 editor artifact and not a property of the language. diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 377f442..e925d8a 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -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 ;; rather than reporting a time for a build that did not happen. (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" (and said (string-match-p "nothing to install" 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 — ;; which is why all four are affordable. (goto-char (point-min)) - (search-forward "(defvar ticks i64)") - (test-flan--check "a top-level defvar takes the declaration path" - (equal (car (flan--declaration-before-point)) "defvar")) + (search-forward "(defonce ticks i64)") + (test-flan--check "a top-level defonce takes the declaration path" + (equal (car (flan--declaration-before-point)) "defonce")) (goto-char (point-min)) (search-forward " ticks)") (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. (with-temp-buffer (flan-mode) - (dolist (head '("defmacro" "defn" "defvar" "defconst" "defstruct" + (dolist (head '("defmacro" "defn" "def" "defonce" "defconst" "defstruct" "defdata" "defunion" "defenum" "defalias" "defclass" "defgeneric" "defmulti" "defmethod" "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. (goto-char (point-max)) (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 ;; 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. @@ -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)))) (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 ;; evaluator and the parser refused it as a declaration. One round trip, on ;; the form the report has to be able to name. (goto-char (point-max)) (let ((beg (point))) - (insert "\n(defvar spark i64 9)") + (insert "\n(defonce spark i64 9)") (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 "\\_" said) (string-match-p "installed" said))) ;; 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))))) (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 ;; what the client decides: the var is named and no value is printed. ;; 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 ;; 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. (goto-char (point-min)) - (search-forward "(defvar ticks i64)") + (search-forward "(defonce ticks i64)") (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 "\\_" 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 (insert ";;;; A file with one of everything.\n" "(defstruct Missing [id i32])\n" - "(defvar ticks i64)\n" + "(defonce ticks i64)\n" + "(def speed 2)\n" "(defconst limit i64 10)\n" "(declare later [] 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" (and (assoc "ticks" (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 ;; would show the same name twice with nothing to tell them apart. (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. (with-temp-buffer (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)) (search-forward "(+ ticks 1)") (goto-char (1- (match-end 0))) ; inside the (+ ...), before its ")" diff --git a/examples/core-2d-camera.flan b/examples/core-2d-camera.flan index 70802f5..c8e8339 100644 --- a/examples/core-2d-camera.flan +++ b/examples/core-2d-camera.flan @@ -8,7 +8,7 @@ ;;;; 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. ;;;; -;;;; 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, ;;;; 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. @@ -29,10 +29,10 @@ (defconst max-buildings 100) -(defvar buildings [100 rl/Rectangle]) -(defvar build-colors [100 rl/Color]) -(defvar player rl/Rectangle) -(defvar camera rl/Camera2D) +(defonce buildings [100 rl/Rectangle]) +(defonce build-colors [100 rl/Color]) +(defonce player rl/Rectangle) +(defonce camera rl/Camera2D) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-3d-picking.flan b/examples/core-3d-picking.flan index 0292342..4d125b7 100644 --- a/examples/core-3d-picking.flan +++ b/examples/core-3d-picking.flan @@ -41,16 +41,16 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar camera rl/Camera3D) +(defonce camera rl/Camera3D) -(defvar cube-position rl/Vector3) -(defvar cube-size rl/Vector3) +(defonce cube-position rl/Vector3) +(defonce cube-size rl/Vector3) ;; 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 ;; click went. -(defvar ray rl/Ray) -(defvar collision rl/RayCollision) +(defonce ray rl/Ray) +(defonce collision rl/RayCollision) ;; The same centre/size to min/max conversion as in ;; examples/models-box-collisions.flan. Written out here rather than shared diff --git a/examples/core-delta-time.flan b/examples/core-delta-time.flan index f01eb9b..75faee0 100644 --- a/examples/core-delta-time.flan +++ b/examples/core-delta-time.flan @@ -19,7 +19,7 @@ ;;;; here rather than instead of it, so the file is not quietly asserting that ;;;; 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. (import rl "vendor:raylib") @@ -31,9 +31,9 @@ (defconst speed f32 10.0) (defconst circle-radius f32 32.0) -(defvar delta-circle rl/Vector2) -(defvar frame-circle rl/Vector2) -(defvar current-fps i32) +(defonce delta-circle rl/Vector2) +(defonce frame-circle rl/Vector2) +(defonce current-fps i32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan index 5752a46..474167a 100644 --- a/examples/core-input-gamepad.flan +++ b/examples/core-input-gamepad.flan @@ -47,7 +47,7 @@ (defconst stick-deadzone f32 0.1) (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 ;; it the axis reads exactly 0, outside it the raw value passes through diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan index 6b90d74..136b6ab 100644 --- a/examples/core-input-gestures-testbed.flan +++ b/examples/core-input-gestures-testbed.flan @@ -48,9 +48,9 @@ ;;;; ;;;; **No local fixed arrays.** The C declares `char gestureLog[20][12]` and ;;;; `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 -;;;; `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 ;;;; literals, so a slot holds a `string` and there is no TextCopy and no @@ -67,18 +67,18 @@ (defconst gesture-log-size 20) (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, ;; 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 ;; too — the first write decrements before storing. -(defvar gesture-log-index i32) -(defvar previous-gesture rl/Gesture) -(defvar last-gesture rl/Gesture) -(defvar gesture-color rl/Color) -(defvar log-mode i32) -(defvar current-angle f32) -(defvar touch-positions [max-touch-count rl/Vector2]) +(defonce gesture-log-index i32) +(defonce previous-gesture rl/Gesture) +(defonce last-gesture rl/Gesture) +(defonce gesture-color rl/Color) +(defonce log-mode i32) +(defonce current-angle f32) +(defonce touch-positions [max-touch-count rl/Vector2]) ;; ── The comparisons the C makes on the raw bitfield ───────────────── ;; diff --git a/examples/core-input-gestures.flan b/examples/core-input-gestures.flan index dbf7a5b..26c69c0 100644 --- a/examples/core-input-gestures.flan +++ b/examples/core-input-gestures.flan @@ -18,7 +18,7 @@ ;;;; catch. ;;;; ;;;; 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 ;;;; the C's `{ "" }` initialiser arriving by a different route, and it is why ;;;; resetting the log below only has to reset the counter. @@ -30,10 +30,10 @@ (defconst max-gesture-strings 20) -(defvar gesture-log [max-gesture-strings string]) -(defvar gestures-count i32) -(defvar current-gesture rl/Gesture) -(defvar last-gesture rl/Gesture) +(defonce gesture-log [max-gesture-strings string]) +(defonce gestures-count i32) +(defonce current-gesture rl/Gesture) +(defonce last-gesture rl/Gesture) ;; 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 diff --git a/examples/core-input-keys.flan b/examples/core-input-keys.flan index feb5a36..30bf6b6 100644 --- a/examples/core-input-keys.flan +++ b/examples/core-input-keys.flan @@ -5,7 +5,7 @@ ;;;; ;;;; 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 -;;;; (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 ;;;; 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 @@ -19,7 +19,7 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar ball rl/Vector2) +(defonce ball rl/Vector2) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-input-mouse-wheel.flan b/examples/core-input-mouse-wheel.flan index b0bc1c5..739bbf4 100644 --- a/examples/core-input-mouse-wheel.flan +++ b/examples/core-input-mouse-wheel.flan @@ -25,7 +25,7 @@ (defconst scroll-speed 4) -(defvar box-y i32) +(defonce box-y i32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-input-mouse.flan b/examples/core-input-mouse.flan index d3e57e7..3bc852f 100644 --- a/examples/core-input-mouse.flan +++ b/examples/core-input-mouse.flan @@ -9,7 +9,7 @@ ;;;; 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. ;;;; -;;;; 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. (import rl "vendor:raylib") @@ -17,7 +17,7 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar ball-color rl/Color) +(defonce ball-color rl/Color) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-input-multitouch.flan b/examples/core-input-multitouch.flan index b964188..4f55f08 100644 --- a/examples/core-input-multitouch.flan +++ b/examples/core-input-multitouch.flan @@ -10,7 +10,7 @@ ;;;; assigned a whole struct. Nothing in the repository used one before, so it ;;;; 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 ;;;; 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 @@ -29,7 +29,7 @@ (defconst max-touch-points 10) -(defvar touch-positions [max-touch-points rl/Vector2]) +(defonce touch-positions [max-touch-points rl/Vector2]) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-input-virtual-controls.flan b/examples/core-input-virtual-controls.flan index c4d1829..5cc7a6b 100644 --- a/examples/core-input-virtual-controls.flan +++ b/examples/core-input-virtual-controls.flan @@ -33,7 +33,7 @@ ;;;; ;;;; 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 -;;;; 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") @@ -67,7 +67,7 @@ ;;;; ── The part with no raylib in it ────────────────────────────────── -(defvar player rl/Vector2) +(defonce player rl/Vector2) (defn reset-player [] () (set player (rl/Vector2 {.x (/ (f32 screen-width) 2.0) diff --git a/examples/core-scissor-test.flan b/examples/core-scissor-test.flan index a9f6a2e..a844264 100644 --- a/examples/core-scissor-test.flan +++ b/examples/core-scissor-test.flan @@ -29,8 +29,8 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar scissor rl/Rectangle) -(defvar scissor-mode bool) +(defonce scissor rl/Rectangle) +(defonce scissor-mode bool) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-window-flags.flan b/examples/core-window-flags.flan index 21496b6..7f2fc53 100644 --- a/examples/core-window-flags.flan +++ b/examples/core-window-flags.flan @@ -38,9 +38,9 @@ ;; window somewhere it cannot be typed at, so the only way back is a count. (defconst restore-after 240) -(defvar ball-pos rl/Vector2) -(defvar ball-speed rl/Vector2) -(defvar frames i32) +(defonce ball-pos rl/Vector2) +(defonce ball-speed rl/Vector2) +(defonce frames i32) (defconst ball-radius f32 20.0) diff --git a/examples/core-window-should-close.flan b/examples/core-window-should-close.flan index afcf88d..ddc757a 100644 --- a/examples/core-window-should-close.flan +++ b/examples/core-window-should-close.flan @@ -12,7 +12,7 @@ ;;;; 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. ;;;; 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 ;;;; other example in this directory. ;;;; @@ -25,8 +25,8 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar exit-requested bool) -(defvar exiting bool) +(defonce exit-requested bool) +(defonce exiting bool) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/core-world-screen.flan b/examples/core-world-screen.flan index e753a16..8a2c717 100644 --- a/examples/core-world-screen.flan +++ b/examples/core-world-screen.flan @@ -39,8 +39,8 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar camera rl/Camera3D) -(defvar cube rl/Vector3) +(defonce camera rl/Camera3D) +(defonce cube rl/Vector3) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/models-box-collisions.flan b/examples/models-box-collisions.flan index ce50da2..4910a8c 100644 --- a/examples/models-box-collisions.flan +++ b/examples/models-box-collisions.flan @@ -38,15 +38,15 @@ (defconst screen-width 800) (defconst screen-height 450) -(defvar camera rl/Camera3D) +(defonce camera rl/Camera3D) -(defvar player-position rl/Vector3) -(defvar player-size rl/Vector3) -(defvar player-color rl/Color) +(defonce player-position rl/Vector3) +(defonce player-size rl/Vector3) +(defonce player-color rl/Color) -(defvar enemy-box-pos rl/Vector3) -(defvar enemy-box-size rl/Vector3) -(defvar enemy-sphere-pos rl/Vector3) +(defonce enemy-box-pos rl/Vector3) +(defonce enemy-box-size rl/Vector3) +(defonce enemy-sphere-pos rl/Vector3) (defconst enemy-sphere-size f32 1.5) diff --git a/examples/shapes-basic-shapes.flan b/examples/shapes-basic-shapes.flan index 4ddd6c9..f9763aa 100644 --- a/examples/shapes-basic-shapes.flan +++ b/examples/shapes-basic-shapes.flan @@ -38,7 +38,7 @@ (defconst col-mid 400) (defconst col-right 600) -(defvar rotation f32) +(defonce rotation f32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/shapes-collision-area.flan b/examples/shapes-collision-area.flan index 613b3d7..08c0594 100644 --- a/examples/shapes-collision-area.flan +++ b/examples/shapes-collision-area.flan @@ -28,12 +28,12 @@ ;; not allowed above. (defconst screen-upper-limit 40) -(defvar box-a rl/Rectangle) ; moves by itself, bounces off the sides -(defvar box-b rl/Rectangle) ; follows the mouse -(defvar box-collision rl/Rectangle) ; their overlap, valid only while touching -(defvar box-a-speed-x i32) -(defvar paused bool) -(defvar collision bool) +(defonce box-a rl/Rectangle) ; moves by itself, bounces off the sides +(defonce box-b rl/Rectangle) ; follows the mouse +(defonce box-collision rl/Rectangle) ; their overlap, valid only while touching +(defonce box-a-speed-x i32) +(defonce paused bool) +(defonce collision bool) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/shapes-following-eyes.flan b/examples/shapes-following-eyes.flan index 8da5506..c5884e0 100644 --- a/examples/shapes-following-eyes.flan +++ b/examples/shapes-following-eyes.flan @@ -45,10 +45,10 @@ (defconst sclera-radius f32 80.0) (defconst iris-radius f32 24.0) -(defvar sclera-left rl/Vector2) -(defvar sclera-right rl/Vector2) -(defvar iris-left rl/Vector2) -(defvar iris-right rl/Vector2) +(defonce sclera-left rl/Vector2) +(defonce sclera-right rl/Vector2) +(defonce iris-left rl/Vector2) +(defonce iris-right rl/Vector2) ;; 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 diff --git a/examples/text-codepoints-loading.flan b/examples/text-codepoints-loading.flan index 38e0967..856db04 100644 --- a/examples/text-codepoints-loading.flan +++ b/examples/text-codepoints-loading.flan @@ -90,8 +90,8 @@ ;; ceiling on: this is the number that decides how big the font texture gets. (defconst max-codepoints 128) -(defvar unique-codepoints [max-codepoints i32]) -(defvar unique-count i32) +(defonce unique-codepoints [max-codepoints i32]) +(defonce unique-count i32) ;; Is `cp` already in the first `n` of the table? (defn seen? [cp i32 n i32] bool @@ -174,15 +174,15 @@ (do (rl/get-codepoint-previous b off (addr size)) (if (< (- off size) 0) 0 (- off size)))))) -(defvar font rl/Font) -;; Whether the TTF was there, asked once. A `defvar` and not the call itself +(defonce font rl/Font) +;; 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 ;; question whose answer cannot change while the program runs is exactly what ;; the per-frame rule in docs/PORTING.md is about. -(defvar font-present bool) -(defvar show-font-atlas bool) -(defvar cursor i32) -(defvar codepoint-count i32) +(defonce font-present bool) +(defonce show-font-atlas bool) +(defonce cursor i32) +(defonce codepoint-count i32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/text-input-box.flan b/examples/text-input-box.flan index ec9337d..d5d3508 100644 --- a/examples/text-input-box.flan +++ b/examples/text-input-box.flan @@ -52,12 +52,12 @@ (defconst max-input-chars 9) ;; No +1: see the header comment. There is no NUL to leave room for. -(defvar name [9 u8]) -(defvar letter-count i32) +(defonce name [9 u8]) +(defonce letter-count i32) -(defvar text-box rl/Rectangle) -(defvar mouse-on-text bool) -(defvar frames-counter i32) +(defonce text-box rl/Rectangle) +(defonce mouse-on-text bool) +(defonce frames-counter i32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/text-writing-anim.flan b/examples/text-writing-anim.flan index ec3aa46..40fb396 100644 --- a/examples/text-writing-anim.flan +++ b/examples/text-writing-anim.flan @@ -40,7 +40,7 @@ (defconst message "This sample illustrates a text writing\nanimation effect! Check it out! ;)") -(defvar frames-counter i32) +(defonce frames-counter i32) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/textures-fog-of-war.flan b/examples/textures-fog-of-war.flan index 15d1059..f5e5e5c 100644 --- a/examples/textures-fog-of-war.flan +++ b/examples/textures-fog-of-war.flan @@ -58,18 +58,18 @@ (defconst tiles-y 15) ;; 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 ;; (mostly black). The three-way state is why this is a byte per tile and not ;; a bit. -(defvar tile-fog [375 u8]) +(defonce tile-fog [375 u8]) -(defvar player-position rl/Vector2) -(defvar player-tile-x i32) -(defvar player-tile-y i32) +(defonce player-position rl/Vector2) +(defonce player-tile-x i32) +(defonce player-tile-y i32) -(defvar fog-of-war rl/RenderTexture2D) +(defonce fog-of-war rl/RenderTexture2D) (defn main [] () (rl/init-window screen-width screen-height diff --git a/examples/textures-image-generation.flan b/examples/textures-image-generation.flan index bf6e541..78d3873 100644 --- a/examples/textures-image-generation.flan +++ b/examples/textures-image-generation.flan @@ -37,8 +37,8 @@ ;; same generator with a different `direction`. (defconst num-textures 9) -(defvar textures [9 rl/Texture2D]) -(defvar current-texture i32) +(defonce textures [9 rl/Texture2D]) +(defonce current-texture i32) ;; 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 diff --git a/examples/textures-image-processing.flan b/examples/textures-image-processing.flan index 22b4841..9104fd8 100644 --- a/examples/textures-image-processing.flan +++ b/examples/textures-image-processing.flan @@ -80,10 +80,10 @@ (defconst proc-flip-vertical 7) (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. -(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. (defconst source-width 200) @@ -127,11 +127,11 @@ (= which proc-flip-vertical) (rl/image-flip-vertical img) (= which proc-flip-horizontal) (rl/image-flip-horizontal img))) -(defvar texture rl/Texture2D) -(defvar im-origin rl/Image) -(defvar im-copy rl/Image) -(defvar current-process i32) -(defvar mouse-hover-rec i32) +(defonce texture rl/Texture2D) +(defonce im-origin rl/Image) +(defonce im-copy rl/Image) +(defonce current-process i32) +(defonce mouse-hover-rec i32) ;; 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 diff --git a/examples/textures-mouse-painting.flan b/examples/textures-mouse-painting.flan index 8ea0292..d55553f 100644 --- a/examples/textures-mouse-painting.flan +++ b/examples/textures-mouse-painting.flan @@ -45,21 +45,21 @@ ;; 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 ;; another entry. -(defvar colors [23 rl/Color]) -(defvar colors-recs [23 rl/Rectangle]) +(defonce colors [23 rl/Color]) +(defonce colors-recs [23 rl/Rectangle]) -(defvar color-selected i32) -(defvar color-selected-prev i32) -(defvar color-mouse-hover i32) -(defvar brush-size f32) -(defvar mouse-was-pressed bool) +(defonce color-selected i32) +(defonce color-selected-prev i32) +(defonce color-mouse-hover i32) +(defonce brush-size f32) +(defonce mouse-was-pressed bool) -(defvar btn-save-rec rl/Rectangle) -(defvar btn-save-mouse-hover bool) -(defvar show-save-message bool) -(defvar save-message-counter i32) +(defonce btn-save-rec rl/Rectangle) +(defonce btn-save-mouse-hover bool) +(defonce show-save-message bool) +(defonce save-message-counter i32) -(defvar target rl/RenderTexture2D) +(defonce target rl/RenderTexture2D) (defn main [] () (rl/init-window screen-width screen-height diff --git a/lib/ast.ml b/lib/ast.ml index 3105f57..9fcea8d 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -241,8 +241,16 @@ and decl_kind = (* Inline name/value pairs, as everywhere else. The members are what a keyword at a call site resolves against. *) | Defenum of string * (string * int64) list - (* value is optional: ZII. `uninit` opts out and is recorded as Uninit. *) - | Defvar of string * texpr option * init + (* value is optional: ZII. `uninit` opts out and is recorded as Uninit. + 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 (* ── The dyn side's classes and generic functions ────────────────── 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 } -(* [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 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] @@ -303,6 +311,11 @@ and variant = { vname : string; vfields : field list; vloc : Loc.t } these. *) 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 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 @@ -333,7 +346,7 @@ let method_name (m : methd) = m.mgen ^ "@" ^ dispatch_text m.mkey let declared_name (d : decl) = match d.d with | Defenum (n, _) | Defalias (n, _) | Defstruct (n, _) | Defdata (n, _) - | Defunion (n, _) | Defvar (n, _, _) | Defconst (n, _, _) + | Defunion (n, _) | Defvar (n, _, _, _) | Defconst (n, _, _) | Defclass (n, _) -> Some n | Declare (fn, _) | DeclareC (fn, _) | Defn fn | Defgeneric fn | Defmulti fn -> Some fn.name @@ -452,12 +465,12 @@ let mark_pause ~line ~col (ds : decl list) : decl list option = | Defmethod m -> { 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 } } - | Defvar (n, t, Init e) -> { d with d = Defvar (n, t, Init (walk e)) } - (* The value reading of an undecided [defvar] is walked too: if it is the + | Defvar (n, t, Init e, k) -> { d with d = Defvar (n, t, Init (walk e), k) } + (* 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 reading wins the expression is dropped whole and the mark with it. *) - | Defvar (n, t, Ambiguous e) -> - { d with d = Defvar (n, t, Ambiguous (walk e)) } + | Defvar (n, t, Ambiguous e, k) -> + { d with d = Defvar (n, t, Ambiguous (walk e), k) } | Defconst (n, t, e) -> { d with d = Defconst (n, t, walk e) } | _ -> d in diff --git a/lib/check.ml b/lib/check.ml index 3f44c1c..adf6a99 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -833,7 +833,7 @@ let map_type ?(preds = []) loc (k : Types.t) (v : Types.t) = they are all the same position: something zeroes it. 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 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 @@ -986,7 +986,7 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = is no longer a typo, it is a guess. *) and near_miss env ?(also = []) n = (* [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. *) let candidates = also @@ -1256,24 +1256,24 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list = | _ -> d) 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 - "dispatch the if it's a type do the right thing". [(defvar current-color - i32)] is the zeroed static it has always been, and [(defvar score 0)] is a - dyn global holding 0 — the same thing [(defvar score dyn 0)] spells out, + "dispatch the if it's a type do the right thing". [(defonce current-color + i32)] is the zeroed static it has always been, and [(defonce score 0)] is a + dyn global holding 0 — the same thing [(defonce score dyn 0)] spells out, lowered by the same path and not by a second one. [Parse] settled every shape a shape can settle and handed the rest over 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 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. 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], - [(defvar v (Vec i32))] is a zeroed [Vec], and a wrong type argument inside + programs meaning today's thing: [(defonce p Point)] is a zeroed [Point], + [(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 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 @@ -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 one-line answer into four lines of unrelated reading. The paragraph is for 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 | Some dname -> 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 \ - 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 ...}))" - n dname gname dname gname dname n + n dname form gname dname gname dname n | None -> ()); (match foreign_spelling n with | Some m -> @@ -1330,15 +1330,15 @@ let defvar_neither env loc gname n ~values ~cases = | None -> "" in 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 \ - type — (defvar %s i64) — and a value there declares a dyn global holding \ - it — (defvar %s 0). Nothing named %s is declared as either%s" - n gname gname n hint + type — (%s %s i64) — and a value there declares a dyn global holding \ + it — (%s %s 0). Nothing named %s is declared as either%s" + n form form gname form gname n hint (* 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 - 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 question, not this one's. *) (* 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 (fun (d : Ast.decl) -> 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, _) -> Some fn.Ast.name | _ -> 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.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 below, [check_global], either backend — has a third case to know about. The - dyn reading is rewritten into exactly [(defvar x dyn )], which is the + dyn reading is rewritten into exactly [(defonce x dyn )], which is the 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. *) -(* 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 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 code the writer was not asking for. 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 them. *) 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 reading to carry. So the element check runs on both, and it is the only 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 | Some (v, 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 \ - 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 \ 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" - v v gname + (%s %s dyn ...) — for a dyn global holding the vector you wrote" + v form v form gname | None -> () in + let word = function Ast.Once -> "defonce" | Ast.Every -> "def" in List.map (fun (d : Ast.decl) -> match d.Ast.d with - | Ast.Defvar (n, Some t, Ast.Zeroed) -> brackets n t; d - | Ast.Defvar (n, Some t, Ast.Ambiguous e) -> + | Ast.Defvar (n, Some t, Ast.Zeroed, k) -> brackets ~form:(word k) n t; d + | Ast.Defvar (n, Some t, Ast.Ambiguous e, k) -> + let form = word k in if defvar_reads_as_type env t then begin - brackets n t; - { d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed) } + brackets ~form n t; + { d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed, k) } end else begin (match t.Ast.t with | Ast.Tname s when not (List.mem s (Lazy.force values)) -> - defvar_neither env t.Ast.tloc n s ~values:(Lazy.force values) - ~cases:(Lazy.force cases) + defvar_neither env t.Ast.tloc ~form n s + ~values:(Lazy.force values) ~cases:(Lazy.force cases) | _ -> ()); 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 | _ -> d) 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 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 - 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 [push] is what adopts the context — so a guard only at the construction 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 \ 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: (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 \ 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 \ @@ -2163,7 +2165,7 @@ let box loc (e : Tast.expr) : Tast.expr = both, and they share [flan_dyn_view_flat]. *) (* The element check runs before the lifetime one in all three arms, and 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 is refused too, so the wrong order hands the programmer advice that 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 (* Implicit widening, and this single arm is the whole of its surface. [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 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)) (* 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 - 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 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 @@ -3430,7 +3432,7 @@ and var ctx ?(qualified = false) loc ~want name = write %s.%s" name dname dname c.Tast.vname | None -> (* 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 read: there is no second binding of [double] for it to have meant 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 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. **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 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 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 @@ -5392,7 +5394,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t = Loc.failk "check/set-constant" loc ~notes "%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 \ - defvar if it has to change" name + defonce if it has to change" name | Some (ty, false) -> Tast.Pglobal name, ty | None -> captured ctx loc name; unknown_name ~setting:true ctx loc 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 the explicit instantiation syntax the generics section rules out: nothing 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 out. *) | "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 [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 - 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. *) | "map-new" -> 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 | None -> 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 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 same thing here so the answer does not depend on which side of 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 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 - 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. 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 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 - 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) = match v.Ast.e with | 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 \ 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 \ - a defvar — write (defvar %s ...) with the same brackets" + a defonce — write (defonce %s ...) with the same brackets" n n gname | _ -> defconst_type_shaped env gname i) items @@ -9144,7 +9146,7 @@ let collect env (decls : Ast.decl list) = 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 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. *) (* The qualifier is reserved on this side too. [(defn builtin/len ...)] 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 on the order the file was written 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. *) let decls = settle_defvars env decls in List.iter @@ -9487,10 +9489,12 @@ let collect env (decls : Ast.decl list) = Hashtbl.replace env.generics fn.Ast.name fn; Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret) end - | Ast.Defvar (n, t, _) -> + | Ast.Defvar (n, t, _, k) -> let ty = match t with | 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 Hashtbl.replace env.globals n (ty, false); 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 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 - 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. 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 "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 \ - 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" 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 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. @@ -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) = if zero_only ty then 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 \ 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) @@ -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 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 written by exactly the store that writes one anywhere else — the encoder was 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 "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 \ - 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" 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 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 - 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 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 \ 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 \ - 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" dname case dname (match Hashtbl.find_opt env.datas dname with @@ -9932,9 +9936,9 @@ let const_defconst_init env loc n (v : Tast.expr) = | Some _ -> fail loc "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 \ - 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 \ arithmetic over literals and other constants, which is folded here" 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 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 - 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 restart-case with its own signal inside it, and that is the shape the author 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 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 - 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, 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. @@ -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 ctx () = invented_ctx env Types.Unit in match d.Ast.d with - | Ast.Defvar (n, _, init) -> + | Ast.Defvar (n, _, init, kind) -> let ty, _ = Hashtbl.find env.globals n in no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty; container_global_init d.Ast.dloc n ty init; + (* A [def]'s initialiser is lifted into [global/] 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 = 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.Uninit -> (* 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 -> let c = ctx () 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 [Init] during [collect], and this pass runs over the list that pass handed back. One arriving here is a driver that checked a global without collecting first. *) | Ast.Ambiguous _ -> 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 - 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) -> let ty, _ = Hashtbl.find env.globals n in 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 membership is the question "is this value in the program's shape?" *) Some { Tast.gname = n; gty = ty; ginit; gconst = true; - gfolded = Hashtbl.mem env.consts n } + gfolded = Hashtbl.mem env.consts n; grerun = false } | _ -> None (* 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 ─────────────────────────────── *) (* 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 - i64 (+ a 10))] written above [(defvar a i64 (+ 1 2))] read a zero and + the wrong one as soon as one of them is computed from another: [(defonce b + 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 by what they need, which is what Odin does (src/checker.cpp, [calculate_global_init_order]) and for the same reason — the alternative is @@ -10193,7 +10213,7 @@ let check_main env decls = edge. 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 text would order that pair by luck. Odin's graph is transitive for the same 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 refused by [hidden_dyn] before this number matters. *) (* 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 honest product made the test [n > desc_offsets_max] false, the declaration 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 (* 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 functions. *) List.iter (fun (g : Tast.global) -> Tast.walk look g.Tast.ginit) p.Tast.globals; diff --git a/lib/dev.ml b/lib/dev.ml index 480de94..ac7a9e9 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -2,7 +2,7 @@ beside it, and a socket an editor talks to. 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 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 @@ -2621,7 +2621,7 @@ let reg_listing t ~verb ~note = 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 - 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 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 [flan_program_main] from the top, the startup function that runs the 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 - 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 + is decided by the form that defined the global. A [defonce] is CL's defvar, + so its initialiser runs only if the variable is not already initialised — + [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 for it. diff --git a/lib/emit.ml b/lib/emit.ml index d25ff2d..f79c09a 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -62,7 +62,7 @@ let sname n = "%" ^ quoted n have no cells and call the symbol directly. *) 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 [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 @@ -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: [Check] lifts a computed initialiser into a function and [emit_startup] 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: 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. *) @@ -3500,7 +3500,7 @@ let rec const m (e : Tast.expr) = | _ -> "{ " ^ String.concat ", " inner ^ " }") | Tast.Some_ 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 constant ones — and a computed [defconst] is refused by [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 [defconst]'s initialiser is always one [const] can write, because the 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 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 second time, this call included. - What that must mean is decided by the defining form, not by the daemon. A - [defvar] is Common Lisp's [defvar]: its initialiser runs only if the + What that must mean is decided by the defining form, not by the daemon — + 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 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 - 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 image on one backend and [.init-data]'s stores on the other, and a re-run reaches neither. @@ -3582,19 +3586,18 @@ let startup_sym = fname ".init-globals" 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 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. - So each computed initialiser guards itself with a flag of its own. Per - global and not per startup function, because the rule belongs to the form: - a global whose initialiser is added by a later build, or a future form that - *does* recompute, decides its own case without the other globals' having to - agree. + So each [defonce]'s computed initialiser guards itself with a flag of its + own, and a [def]'s takes none. Per global and not per startup function, + because the rule belongs to the form: a [def] beside a [defonce] decides + its own case without the other globals' having to agree. 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 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 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 terminator and so cannot appear in any symbol a program can write — the same trick [destructure~N] uses. A [.]-separated name would not do: [.] is an - ordinary symbol constituent, so [(defvar .init-once.x ...)] beside a + 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 the assembler. *) 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 about any of the three. [flags] is empty in a release build. *) 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/] 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 = List.filter (fun (g : Tast.global) -> not (Tast.const_init g.Tast.ginit)) globals in - let flags = - if not m.dev then [] - else - List.map - (fun (g : Tast.global) -> - { Tast.gname = init_flag g.Tast.gname; gty = Types.Bool; - ginit = - { Tast.e = Tast.Bool false; ty = Types.Bool; - loc = g.Tast.ginit.Tast.loc }; - gconst = false; gfolded = false }) - computed + (* One guard flag per [defonce] with something to run, and none for a + [def]: the flag is exactly what makes an initialiser run once, and a + [def]'s runs every time — that unguarded store is the whole difference + between the two forms. *) + let flag_for (g : Tast.global) = + if m.dev && not g.Tast.grerun then + Some + { Tast.gname = init_flag g.Tast.gname; gty = Types.Bool; + ginit = + { Tast.e = Tast.Bool false; ty = Types.Bool; + loc = g.Tast.ginit.Tast.loc }; + gconst = false; gfolded = false; grerun = false } + else None 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 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 @@ -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) flags; let body = - List.map2 - (fun (g : Tast.global) (flag : Tast.global option) -> + List.map + (fun ((g : Tast.global), (flag : Tast.global option)) -> let loc = g.Tast.ginit.Tast.loc in let store = { 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.Do [ store; mark ]; ty = Types.Unit; loc }); ty = Types.Unit; loc }) - computed - (if flags = [] then List.map (fun _ -> None) computed - else List.map (fun f -> Some f) flags) + flagged in (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 [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 - 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 from [main] now. What survives is the weaker and sufficient version: 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" (cellptr f.Tast.name))) siblings; + (* A lifted initialiser handed in as a target — [global/] 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 Buffer.add_string m.out "\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. *) (* Its declared initial value travels with it, as a constant the 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. A *computed* initialiser sends a null instead, and the allocation diff --git a/lib/load.ml b/lib/load.ml index 9c81481..67e5dca 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -448,11 +448,11 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl = able to name the type. *) | Ast.Defunion (n, 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, (match init with | 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 — the type half by [rename_texpr] above, the value half 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. *) | Ast.Ambiguous v -> Ast.Ambiguous (rename_expr owned alias [] v) - | other -> other)) + | other -> other), + k) | Ast.Defn fn -> 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 @@ -848,9 +849,9 @@ let decl_uses acc (d : Ast.decl) = | Ast.Declare (f, _) | Ast.DeclareC (f, _) -> List.iter field f.Ast.params; Option.iter (texpr_uses acc) f.Ast.ret - | Ast.Defvar (_, t, init) -> + | Ast.Defvar (_, t, init, _) -> 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 is a declaration dropped from the module. *) (match init with diff --git a/lib/macro.ml b/lib/macro.ml index b67826c..889895c 100644 --- a/lib/macro.ml +++ b/lib/macro.ml @@ -140,7 +140,7 @@ let building = ref false calling a dropped one is as unbuildable as the dropped one itself. 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. 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 diff --git a/lib/mangle.ml b/lib/mangle.ml index fa4be4d..81390aa 100644 --- a/lib/mangle.ml +++ b/lib/mangle.ml @@ -25,7 +25,7 @@ let prefix = "flan." (* 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] and [.init-data] start with a dot no reader token can produce. *) let sym n = prefix ^ n diff --git a/lib/parse.ml b/lib/parse.ml index 1c74b23..05b0bdd 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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 output rather than a dependency. Building a declaration as a value is what a macro is for. *) - | Sym ("defmacro" | "defn" | "defvar" | "defconst" | "defstruct" | "defdata" - | "defunion" | "defclass" | "defgeneric" | "defmulti" | "defmethod" - | "defenum" | "defalias" | "import" as name) -> + | Sym ("defmacro" | "defn" | "def" | "defonce" | "defconst" | "defstruct" + | "defdata" | "defunion" | "defclass" | "defgeneric" | "defmulti" + | "defmethod" | "defenum" | "defalias" | "import" as name) -> fail f "%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 \ @@ -1180,18 +1180,20 @@ and pattern (f : Form.t) : Ast.pattern = Ast.Pctor (ctor, List.map sym binds) | _ -> fail f "expected a pattern, found %s" (Form.to_string f) -(* ── The third element of a defvar ───────────────────────────────────── - [(defvar x i32)] declares a zeroed static and [(defvar score 0)] declares 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 - then it's dyn", and "dispatch the if it's a type do the right thing". +(* ── The third element of a defonce or a def ─────────────────────────── + [(defonce x i32)] declares a zeroed static and [(defonce score 0)] declares + a dyn global holding 0, and which one a form is is decided by whether the + third element is a type. [def] takes exactly the same spellings — the two + 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 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 [(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 - 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. 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 \ 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 | [ n; t ] -> 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"; _ } ] -> - mk (Ast.Defvar (sym n, Some (texpr t), Ast.Uninit)) - | [ n; t; v ] -> mk (Ast.Defvar (sym n, Some (texpr t), Ast.Init (expr v))) + 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), kind)) | _ -> fail f - "defvar is (defvar name Type value?) or (defvar name value) — a \ - third element that is not a type is the value of a dyn global") + "%s is (%s name Type value?) or (%s name value) — a third element \ + 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) -> (match args with diff --git a/lib/prelude.ml b/lib/prelude.ml index ea7678a..163fdd3 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -200,7 +200,7 @@ let source = {flan| ;; 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 ;; 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] () (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 ;; 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. -(defvar gensym-n i64 0) +(defonce gensym-n i64 0) (defn gensym [] Form (set gensym-n (+ gensym-n 1)) diff --git a/lib/reach.ml b/lib/reach.ml index caf5ac0..f7803d4 100644 --- a/lib/reach.ml +++ b/lib/reach.ml @@ -21,7 +21,7 @@ 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 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. Dev builds are not pruned at all. A REPL redefines a function that the diff --git a/lib/session.ml b/lib/session.ml index 63b618d..9f54f0f 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -290,7 +290,7 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc with (* 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 - 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 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 @@ -779,6 +779,30 @@ let eval ?(origin = "") ?pause t src : change = program.Tast.fns) names 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/] — [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 = List.concat_map (fun n -> @@ -797,7 +821,7 @@ let eval ?(origin = "") ?pause t src : change = in let fns = List.sort_uniq String.compare - (declared_fns @ from_generics @ new_instances) + (declared_fns @ def_inits @ from_generics @ new_instances) in (* 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 @@ -1831,7 +1855,7 @@ let write_slot ?(origin = "") t ~frame ~(fn : Tast.fn) ~slot ~path (* ── The globals a stopped stack reaches ───────────────────────────── *) (* 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. Almost the same thunk as [render_locals] with a different root, and the diff --git a/lib/tast.ml b/lib/tast.ml index 6682af8..dccb51b 100644 --- a/lib/tast.ml +++ b/lib/tast.ml @@ -321,6 +321,10 @@ type global = { ginit : expr; gconst : 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 diff --git a/lib/types.ml b/lib/types.ml index 919ea6b..20f026b 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -51,7 +51,7 @@ type t = | Fn of t list * t (* (Fn [T ...] R) *) | Var of string (* a type variable — milestone 5 *) (* [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 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 @@ -103,7 +103,7 @@ let fkind_of_name = function 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 - 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 [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 diff --git a/spec-memory.md b/spec-memory.md index f79d5df..d869b2b 100644 --- a/spec-memory.md +++ b/spec-memory.md @@ -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 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)`, 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 @@ -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: ``` -(defvar the-data (Vec u8)) +(defonce the-data (Vec u8)) (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: ``` -(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 (at a i ...) v) ; fixed array, slice, or Vec element (set (deref p) v) ; whole-object store through a pointer diff --git a/spike/generics/measure.ml b/spike/generics/measure.ml index 229c395..9c1b212 100644 --- a/spike/generics/measure.ml +++ b/spike/generics/measure.ml @@ -45,7 +45,7 @@ let generic_src n = \ (set j (- j 1))))\n\ \ (set i (+ i 1)))))\n\n"; 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; Buffer.add_string b "\n(defn main [] ()\n"; for i = 0 to n - 1 do @@ -76,7 +76,7 @@ let mono_src n = \ (set j (- j 1))))\n\ \ (set i (+ i 1)))))\n\n" 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; Buffer.add_string b "(defn main [] ()\n"; for i = 0 to n - 1 do diff --git a/spike/generics/prelude-shapes.flan b/spike/generics/prelude-shapes.flan index b17b777..d0a5ac6 100644 --- a/spike/generics/prelude-shapes.flan +++ b/spike/generics/prelude-shapes.flan @@ -29,8 +29,8 @@ (set i (+ i 1)) (set j (- j 1))))) -(defvar ns [5 i32]) -(defvar fs [5 f32]) +(defonce ns [5 i32]) +(defonce fs [5 f32]) (defn main [] () (let [xs (slice ns 0 5) diff --git a/spike/js/p1-int-semantics.flan b/spike/js/p1-int-semantics.flan index 1bd155e..44320ed 100644 --- a/spike/js/p1-int-semantics.flan +++ b/spike/js/p1-int-semantics.flan @@ -10,34 +10,34 @@ ;;;; arith.flan gives: a literal operand is exactly what a constant folder ;;;; removes, and a folded program does not contain the code being tested. -(defvar i8hi i8 127) -(defvar i16hi i16 32767) -(defvar i32hi i32 2147483647) -(defvar u8hi u8 255) -(defvar u16hi u16 65535) -(defvar u32hi u32 4294967295) -(defvar i64hi i64 9223372036854775807) -(defvar u64zero u64 0) +(defonce i8hi i8 127) +(defonce i16hi i16 32767) +(defonce i32hi i32 2147483647) +(defonce u8hi u8 255) +(defonce u16hi u16 65535) +(defonce u32hi u32 4294967295) +(defonce i64hi i64 9223372036854775807) +(defonce u64zero u64 0) -(defvar one8 i8 1) -(defvar one16 i16 1) -(defvar one32 i32 1) -(defvar oneu8 u8 1) -(defvar oneu16 u16 1) -(defvar oneu32 u32 1) -(defvar one64 i64 1) -(defvar oneu64 u64 1) +(defonce one8 i8 1) +(defonce one16 i16 1) +(defonce one32 i32 1) +(defonce oneu8 u8 1) +(defonce oneu16 u16 1) +(defonce oneu32 u32 1) +(defonce one64 i64 1) +(defonce oneu64 u64 1) -(defvar big32 i32 123456789) -(defvar big64 i64 1234567890123) -(defvar three i32 3) -(defvar three64 i64 3) -(defvar seven i32 7) -(defvar negsev i32 -7) -(defvar shift i32 33) -(defvar shift8 i8 9) -(defvar f32one f32 1.0) -(defvar f32big f32 16777217.0) +(defonce big32 i32 123456789) +(defonce big64 i64 1234567890123) +(defonce three i32 3) +(defonce three64 i64 3) +(defonce seven i32 7) +(defonce negsev i32 -7) +(defonce shift i32 33) +(defonce shift8 i8 9) +(defonce f32one f32 1.0) +(defonce f32big f32 16777217.0) (defn main [] i32 ;; ── Wrapping at every width ────────────────────────────────────── diff --git a/spike/js/p2-value-copies.flan b/spike/js/p2-value-copies.flan index 8d6eb62..e1eda2b 100644 --- a/spike/js/p2-value-copies.flan +++ b/spike/js/p2-value-copies.flan @@ -18,7 +18,7 @@ (defstruct Point [x i32 y 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. (defn bump [p Point] Point diff --git a/spike/x86/bench/b2-bounds.flan b/spike/x86/bench/b2-bounds.flan index 62330cd..3df3841 100644 --- a/spike/x86/bench/b2-bounds.flan +++ b/spike/x86/bench/b2-bounds.flan @@ -7,7 +7,7 @@ ;;;; the LLVM side says what the check costs when a compiler is allowed to ;;;; hoist it out of the loop. -(defvar xs [1024 i32]) +(defonce xs [1024 i32]) (defn main [] i32 (dotimes [i 1024] diff --git a/spike/x86/p10-defer-transfer.flan b/spike/x86/p10-defer-transfer.flan index d7b295b..9bb51c0 100644 --- a/spike/x86/p10-defer-transfer.flan +++ b/spike/x86/p10-defer-transfer.flan @@ -25,7 +25,7 @@ (defstruct Blip [n i32]) -(defvar log i64) +(defonce log i64) (defn deep [n i32] i32 (signal (Blip {.n n})) diff --git a/spike/x86/p11-shift-edges.flan b/spike/x86/p11-shift-edges.flan index 41dae5e..7701a97 100644 --- a/spike/x86/p11-shift-edges.flan +++ b/spike/x86/p11-shift-edges.flan @@ -14,32 +14,32 @@ ;;;; 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. -(defvar i8min i8 -128) -(defvar i16min i16 -32768) -(defvar i32min i32 -2147483648) -(defvar i64min i64 -9223372036854775808) -(defvar u8hi u8 255) -(defvar u16hi u16 65535) -(defvar u32hi u32 4294967295) -(defvar one8 i8 1) -(defvar one16 i16 1) -(defvar one32 i32 1) -(defvar oneu8 u8 1) -(defvar oneu16 u16 1) -(defvar oneu32 u32 1) -(defvar one64 i64 1) +(defonce i8min i8 -128) +(defonce i16min i16 -32768) +(defonce i32min i32 -2147483648) +(defonce i64min i64 -9223372036854775808) +(defonce u8hi u8 255) +(defonce u16hi u16 65535) +(defonce u32hi u32 4294967295) +(defonce one8 i8 1) +(defonce one16 i16 1) +(defonce one32 i32 1) +(defonce oneu8 u8 1) +(defonce oneu16 u16 1) +(defonce oneu32 u32 1) +(defonce one64 i64 1) -(defvar c8 i8 8) -(defvar c9 i8 9) -(defvar cu8 u8 8) -(defvar c16 i16 16) -(defvar cu16 u16 17) -(defvar c32 i32 32) -(defvar c33 i32 33) -(defvar cu32 u32 32) -(defvar cu33 u32 33) -(defvar c64 i64 64) -(defvar c65 i64 65) +(defonce c8 i8 8) +(defonce c9 i8 9) +(defonce cu8 u8 8) +(defonce c16 i16 16) +(defonce cu16 u16 17) +(defonce c32 i32 32) +(defonce c33 i32 33) +(defonce cu32 u32 32) +(defonce cu33 u32 33) +(defonce c64 i64 64) +(defonce c65 i64 65) (defn main [] i32 ;; ── The count exactly at the width: masks to 0, so nothing moves ── diff --git a/spike/x86/p12-handler-value.flan b/spike/x86/p12-handler-value.flan index e5b73c1..690cae0 100644 --- a/spike/x86/p12-handler-value.flan +++ b/spike/x86/p12-handler-value.flan @@ -23,7 +23,7 @@ ;;;; condition corpus is written in. (defstruct Oops [id i32]) -(defvar trace i64) +(defonce trace i64) ;;; 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 diff --git a/spike/x86/p5-core.flan b/spike/x86/p5-core.flan index e540f5a..913634c 100644 --- a/spike/x86/p5-core.flan +++ b/spike/x86/p5-core.flan @@ -2,7 +2,7 @@ ;; continue, the bitwise family, unsigned arithmetic and shifts, and the ;; conversions in both directions. -(defvar counter i64 0) +(defonce counter i64 0) (defconst limit i32 6) diff --git a/spike/x86/p6-transfer.flan b/spike/x86/p6-transfer.flan index d34c3eb..c635d87 100644 --- a/spike/x86/p6-transfer.flan +++ b/spike/x86/p6-transfer.flan @@ -23,7 +23,7 @@ (defstruct Blip [n i32]) -(defvar log i64) +(defonce log i64) ;;; Two frames down, with a defer between, so the transfer crosses a function ;;; boundary and a transfer exit that has work to do. diff --git a/spike/x86/p7-slice-from-ptr.flan b/spike/x86/p7-slice-from-ptr.flan index 78889bb..0152641 100644 --- a/spike/x86/p7-slice-from-ptr.flan +++ b/spike/x86/p7-slice-from-ptr.flan @@ -14,7 +14,7 @@ ;;;; 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. -(defvar a [4 i32]) +(defonce a [4 i32]) (defn promised [n i32] i32 ;; n is a parameter, so the checker has no literal to look at. diff --git a/spike/x86/p9-dead-defers.flan b/spike/x86/p9-dead-defers.flan index 643f7c3..3116318 100644 --- a/spike/x86/p9-dead-defers.flan +++ b/spike/x86/p9-dead-defers.flan @@ -1,4 +1,4 @@ -(defvar log i64) +(defonce log i64) (defn quiet [n i32] i32 (defer (set log (+ log 1))) diff --git a/test/programs/agent-auto.flan b/test/programs/agent-auto.flan index 6ab6374..ef624d4 100644 --- a/test/programs/agent-auto.flan +++ b/test/programs/agent-auto.flan @@ -11,7 +11,7 @@ ;;;; case that has to keep working. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) (defn tick [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/agent-nostart.flan b/test/programs/agent-nostart.flan index f1a5543..7dd48bb 100644 --- a/test/programs/agent-nostart.flan +++ b/test/programs/agent-nostart.flan @@ -14,7 +14,7 @@ ;;;; constructor either. See flan_agent.c's [auto_start]. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) (defn tick [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/agent-queue.flan b/test/programs/agent-queue.flan index 6d98ec6..d205563 100644 --- a/test/programs/agent-queue.flan +++ b/test/programs/agent-queue.flan @@ -13,7 +13,7 @@ ;;; nothing for a shim to do. (declare stdin-byte [] i32 "getchar") -(defvar ticks i64) +(defonce ticks i64) (defn tick [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/agent.flan b/test/programs/agent.flan index 8eb4246..0ed99ce 100644 --- a/test/programs/agent.flan +++ b/test/programs/agent.flan @@ -13,7 +13,7 @@ ;;;; vendor/agent/agent.flan. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) (defn tick [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/allocators.flan b/test/programs/allocators.flan index 5ac0dc8..e205eec 100644 --- a/test/programs/allocators.flan +++ b/test/programs/allocators.flan @@ -10,7 +10,7 @@ ;; 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 ;; (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 ;;; with-allocator body sees the rebinding without anything being passed. diff --git a/test/programs/arena-region.flan b/test/programs/arena-region.flan index 57538f5..2947b21 100644 --- a/test/programs/arena-region.flan +++ b/test/programs/arena-region.flan @@ -17,7 +17,7 @@ ;;;; 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. -(defvar frame Allocator) +(defonce frame Allocator) (defalias Row (Vec i32)) diff --git a/test/programs/arena-value.flan b/test/programs/arena-value.flan index 49a5b37..2948e1d 100644 --- a/test/programs/arena-value.flan +++ b/test/programs/arena-value.flan @@ -14,7 +14,7 @@ ;;;; 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. -(defvar frame Allocator) +(defonce frame Allocator) (defdata Value [(Nil []) diff --git a/test/programs/arith-condition.flan b/test/programs/arith-condition.flan index 9f501a0..62ba138 100644 --- a/test/programs/arith-condition.flan +++ b/test/programs/arith-condition.flan @@ -44,22 +44,22 @@ ;;;; is allowed to elide — a literal divisor that is neither 0 nor -1, and ;;;; unsigned division, which has no overflow case at all. -(defvar frames i64) -(defvar skipped i64) -(defvar cleaned i64) -(defvar op i32) -(defvar lhs i64) -(defvar rhs i64) +(defonce frames i64) +(defonce skipped i64) +(defonce cleaned i64) +(defonce op i32) +(defonce lhs i64) +(defonce rhs i64) ;;; 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 ;;; to use a global. -(defvar zero i64) -(defvar neg1 i64 -1) -(defvar big i64 9223372036854775807) -(defvar huge f64 1e300) -(defvar small f64 -1e300) -(defvar uz u32) +(defonce zero i64) +(defonce neg1 i64 -1) +(defonce big i64 9223372036854775807) +(defonce huge f64 1e300) +(defonce small f64 -1e300) +(defonce uz u32) (defn show [name string n i64] () (print name) (print " ") (print n) (println "")) diff --git a/test/programs/arith.flan b/test/programs/arith.flan index 3f1fab5..71b747c 100644 --- a/test/programs/arith.flan +++ b/test/programs/arith.flan @@ -14,21 +14,21 @@ ;;;; allowed to elide, and folding these away would leave the test asserting on ;;;; a program that does not contain the check. -(defvar zero i64) -(defvar neg1 i64 -1) -(defvar big i64 9223372036854775807) -(defvar ten i64 10) -(defvar uz u32) -(defvar huge f64 1e300) +(defonce zero i64) +(defonce neg1 i64 -1) +(defonce big i64 9223372036854775807) +(defonce ten i64 10) +(defonce uz u32) +(defonce huge f64 1e300) ;; 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 ;; 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 ;; double on the other. Both routes are supposed to give the same answer and ;; the survey is what says so. -(defvar i32big i32 2147483647) -(defvar m1-32 i32 -1) -(defvar wide f32 1e30) +(defonce i32big i32 2147483647) +(defonce m1-32 i32 -1) +(defonce wide f32 1e30) (defn main [args [string]] i32 (let [n (i32 (bytes->i64 (bytes-view (at args 1)))) diff --git a/test/programs/array-ctor.flan b/test/programs/array-ctor.flan index 9010b3c..375d3b5 100644 --- a/test/programs/array-ctor.flan +++ b/test/programs/array-ctor.flan @@ -7,7 +7,7 @@ (defstruct V2 [x f32 y f32]) (defconst n 3) -(defvar points [3 V2]) +(defonce points [3 V2]) (defn sumx [ps [3 V2]] i32 (let [t (f32 0.0)] diff --git a/test/programs/array-fill.flan b/test/programs/array-fill.flan index 4be1fe9..98c4ba7 100644 --- a/test/programs/array-fill.flan +++ b/test/programs/array-fill.flan @@ -3,7 +3,7 @@ ;;;; 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 ;;;; 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. ;;;; ;;;; The dimensions are in brackets and are the same compile-time lengths the @@ -13,8 +13,8 @@ (defconst cols 4) ;; The line from the note. A typed declaration with a computed initialiser, -;; which is the startup-lifted path a defvar already had. -(defvar grid [rows [cols u8]] (array-fill [rows cols] 255)) +;; which is the startup-lifted path a defonce already had. +(defonce grid [rows [cols u8]] (array-fill [rows cols] 255)) ;; One index per dimension, i32 each, and the return type is the element type. (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 ;; 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. -(defvar ticks i32) +(defonce ticks i32) (defn tick [r i32 c i32] i32 (set ticks (+ ticks 1)) @@ -34,7 +34,7 @@ (defstruct Cell [row i32 col i32]) ;; Counts its own calls, for the evaluated-once line below. -(defvar calls i32) +(defonce calls i32) (defn bump [] i32 (set calls (+ calls 1)) @@ -75,7 +75,7 @@ (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 - ;; 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. (print (i32 (at grid 0 0))) (print " ") (print (i32 (at grid 2 3))) (println "") ; 255 255 diff --git a/test/programs/bounds-condition.flan b/test/programs/bounds-condition.flan index da621b2..99a6c1f 100644 --- a/test/programs/bounds-condition.flan +++ b/test/programs/bounds-condition.flan @@ -44,17 +44,17 @@ ;;;; runtime rather than in emitted IR, so those two are plumbed ;;;; 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 — ;;; check.ml refuses a capture by name and says to use a global — so ;;; everything this program counts lives up here. -(defvar frames i64) -(defvar skipped i64) -(defvar cleaned i64) -(defvar low i64) -(defvar high i64) -(defvar length i64) +(defonce frames i64) +(defonce skipped i64) +(defonce cleaned i64) +(defonce low i64) +(defonce high i64) +(defonce length i64) ;;; Two frames deep, with a defer on the way, so the transfer has something to ;;; cross and something to run on its way out. diff --git a/test/programs/bounds.flan b/test/programs/bounds.flan index ad63f0e..d86cfa3 100644 --- a/test/programs/bounds.flan +++ b/test/programs/bounds.flan @@ -13,7 +13,7 @@ ;;;; 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 ;;;; (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 (let [n (i32 (bytes->i64 (bytes-view (at args 1)))) diff --git a/test/programs/cleanup.flan b/test/programs/cleanup.flan index 5e1f60d..8323f0c 100644 --- a/test/programs/cleanup.flan +++ b/test/programs/cleanup.flan @@ -7,9 +7,9 @@ (defstruct Missing [id i32]) (defstruct Other [id i32]) -(defvar log i64) -(defvar order i64) -(defvar seen i64) +(defonce log i64) +(defonce order i64) +(defonce seen i64) (defn note [n i64] () (set order (+ (* order 10) n))) diff --git a/test/programs/conditions.flan b/test/programs/conditions.flan index a42a29e..3beb3f8 100644 --- a/test/programs/conditions.flan +++ b/test/programs/conditions.flan @@ -8,8 +8,8 @@ (defstruct AssetMissing [id i32]) (defstruct Corrupt [id i32]) -(defvar seen i64) -(defvar other i64) +(defonce seen i64) +(defonce other i64) ;;; Signals twice and keeps going both times — that is the whole of §1. (defn load-all [] () diff --git a/test/programs/defer-let.flan b/test/programs/defer-let.flan index 420bd13..1066600 100644 --- a/test/programs/defer-let.flan +++ b/test/programs/defer-let.flan @@ -9,7 +9,7 @@ ;;;; ;;;; 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)))) diff --git a/test/programs/defvar-dyn.flan b/test/programs/defonce-dyn.flan similarity index 85% rename from test/programs/defvar-dyn.flan rename to test/programs/defonce-dyn.flan index e4bf5f3..1ba855a 100644 --- a/test/programs/defvar-dyn.flan +++ b/test/programs/defonce-dyn.flan @@ -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 ;;;; always been, and anything else is a dyn global initialised from that @@ -7,7 +7,7 @@ ;;;; one of its declarations got. ;;;; ;;;; There is no new lowering under any of it. The dyn half is exactly what -;;;; [(defvar x dyn )] already compiled to — the initialiser lifted into +;;;; [(defonce x dyn )] already compiled to — the initialiser lifted into ;;;; 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 ;;;; 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 ── ;; 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. -(defvar current-color i32) -(defvar grid [2 [3 u32]]) -(defvar origin Point) -(defvar bytes (Vec u8)) +(defonce current-color i32) +(defonce grid [2 [3 u32]]) +(defonce origin Point) +(defonce bytes (Vec u8)) ;; ── The value reading, which is the new spelling ───────────────────── ;; 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. -(defvar score 0) -(defvar label "start") -(defvar config {:level 1 :name "one"}) -(defvar tally seeded) +(defonce score 0) +(defonce label "start") +(defonce config {:level 1 :name "one"}) +(defonce tally seeded) ;; A call, which is the shape the author kept writing: the file is read once, ;; at startup, into a global that outlives main. (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* -;; 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. -(defvar seeded i64 7) +(defonce seeded i64 7) (defn main [] () ;; The statics, untouched: zero, zero, zero, and an empty Vec that is a real diff --git a/test/programs/destructure.flan b/test/programs/destructure.flan index ed6bc90..66225da 100644 --- a/test/programs/destructure.flan +++ b/test/programs/destructure.flan @@ -16,7 +16,7 @@ (defstruct Point [x i32 y i32]) (defstruct Line [a Point b Point]) -(defvar calls i32) +(defonce calls i32) (defn make-point [] Point (set calls (+ calls 1)) diff --git a/test/programs/dev-break-bounds.flan b/test/programs/dev-break-bounds.flan index 80cf1bf..050ce6b 100644 --- a/test/programs/dev-break-bounds.flan +++ b/test/programs/dev-break-bounds.flan @@ -20,8 +20,8 @@ ;;;; bounds failure now lands somewhere a session can be recovered from. (import agent "vendor:agent") -(defvar grid [4 i32]) -(defvar skipped i64) +(defonce grid [4 i32]) +(defonce skipped i64) ;;; One frame deep under the restart-case, so the transfer has something to ;;; cross and the backtrace has something to show. @@ -33,7 +33,7 @@ (do (touch i) (println "frame done")) (continue [] (set skipped (+ skipped 1))))) -(defvar ticks i64) +(defonce ticks i64) (defn main [] i32 (agent/start "/tmp/flan-dev-break-bounds-fallback.sock") diff --git a/test/programs/dev-break.flan b/test/programs/dev-break.flan index 1c37c89..24a353c 100644 --- a/test/programs/dev-break.flan +++ b/test/programs/dev-break.flan @@ -18,7 +18,7 @@ (use-placeholder [] -1) (retry [] 7))) -(defvar ticks i64) +(defonce ticks i64) (defn step [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/dev-chatty.flan b/test/programs/dev-chatty.flan index d82764c..10efc5b 100644 --- a/test/programs/dev-chatty.flan +++ b/test/programs/dev-chatty.flan @@ -17,7 +17,7 @@ ;;; 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. -(defvar frames i64) +(defonce frames i64) (defn chatter [] i64 ;; Sixty-four lines of sixty-three characters and a newline: 4096 bytes a diff --git a/test/programs/dev-classes.flan b/test/programs/dev-classes.flan index 459aaa9..35d7d4a 100644 --- a/test/programs/dev-classes.flan +++ b/test/programs/dev-classes.flan @@ -36,7 +36,7 @@ (defmethod area point [p] (* (get p :x) (get p :y))) -(defvar instances dyn) +(defonce instances dyn) (defn main [] i32 (agent/start "/tmp/flan-dev-classes-fallback.sock") diff --git a/test/programs/dev-dyn-global.flan b/test/programs/dev-dyn-global.flan index f129547..bae1948 100644 --- a/test/programs/dev-dyn-global.flan +++ b/test/programs/dev-dyn-global.flan @@ -16,7 +16,7 @@ ;;;; and everything this fixture is for happens after it. (import agent "vendor:agent") -(defvar config dyn) +(defonce config dyn) (defn main [] i32 (agent/start "/tmp/flan-dev-dyn-global-fallback.sock") diff --git a/test/programs/dev-globals.flan b/test/programs/dev-globals.flan index e4bfb97..10da4d6 100644 --- a/test/programs/dev-globals.flan +++ b/test/programs/dev-globals.flan @@ -2,7 +2,7 @@ ;;;; ;;;; 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 -;;;; 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. ;;;; ;;;; The globals are declared in an order the answer must *not* come back in. @@ -16,10 +16,10 @@ (defstruct Boom [why i32]) -(defvar label string) -(defvar grid [4 i32]) -(defvar pressure i64) -(defvar untouched i64 99) +(defonce label string) +(defonce grid [4 i32]) +(defonce pressure i64) +(defonce untouched i64 99) ;; The inner frame. It writes two globals and then errors with nothing ;; handling the condition, so the program stops here with [main] under it. diff --git a/test/programs/dev-inspect.flan b/test/programs/dev-inspect.flan index 748971a..81a65af 100644 --- a/test/programs/dev-inspect.flan +++ b/test/programs/dev-inspect.flan @@ -24,7 +24,7 @@ ;; 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. -(defvar mark i64) +(defonce mark i64) ;; 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 @@ -41,7 +41,7 @@ s (Shape.Rect {.w 3 .h 6})] (deeper))) -(defvar ticks i64) +(defonce ticks i64) (defn main [] i32 (set mark 99) diff --git a/test/programs/dev-lateagent.flan b/test/programs/dev-lateagent.flan index a483245..ba99fa2 100644 --- a/test/programs/dev-lateagent.flan +++ b/test/programs/dev-lateagent.flan @@ -20,7 +20,7 @@ ;;;; a real daemon as well. (import agent "vendor:agent") -(defvar frames i64) +(defonce frames i64) (defn step [] i64 7) diff --git a/test/programs/dev-locals.flan b/test/programs/dev-locals.flan index 6457e40..8af737a 100644 --- a/test/programs/dev-locals.flan +++ b/test/programs/dev-locals.flan @@ -39,7 +39,7 @@ (let [after (i64 99)] after)) (carry-on [] 5))))) -(defvar ticks i64) +(defonce ticks i64) (defn main [] i32 (agent/start "/tmp/flan-dev-locals-fallback.sock") diff --git a/test/programs/dev-loop.flan b/test/programs/dev-loop.flan index 7fbf933..ff5481e 100644 --- a/test/programs/dev-loop.flan +++ b/test/programs/dev-loop.flan @@ -9,7 +9,7 @@ ;;;; landing there is the zero-argument form working end to end. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) ;;; 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 diff --git a/test/programs/dev-macro.flan b/test/programs/dev-macro.flan index 20fb6db..a5f6ab7 100644 --- a/test/programs/dev-macro.flan +++ b/test/programs/dev-macro.flan @@ -16,7 +16,7 @@ ;;;; running beside it. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) (defn parity [n i64] string (let [out "even"] diff --git a/test/programs/dev-parknote.flan b/test/programs/dev-parknote.flan index b139048..a13686e 100644 --- a/test/programs/dev-parknote.flan +++ b/test/programs/dev-parknote.flan @@ -10,7 +10,7 @@ ;;;; back to a fresh park in one op. (import agent "vendor:agent") -(defvar runs i64) +(defonce runs i64) (defn step [] i64 7) diff --git a/test/programs/dev-pause.flan b/test/programs/dev-pause.flan index 901836f..393b910 100644 --- a/test/programs/dev-pause.flan +++ b/test/programs/dev-pause.flan @@ -10,7 +10,7 @@ ;;;; tail with nothing in front of it. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) ;;; 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 diff --git a/test/programs/dev-ptr.flan b/test/programs/dev-ptr.flan index c6783e9..55d7c75 100644 --- a/test/programs/dev-ptr.flan +++ b/test/programs/dev-ptr.flan @@ -34,8 +34,8 @@ ;;; 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 ;;; them the way a person at the break loop would. -(defvar live-addr i64) -(defvar dead-addr i64) +(defonce live-addr i64) +(defonce dead-addr i64) (defn deeper [] i64 (restart-case @@ -54,7 +54,7 @@ (set dead-addr (ptr-num dead)) (deeper)))) -(defvar ticks i64) +(defonce ticks i64) (defn main [] i32 (agent/start "/tmp/flan-dev-ptr-fallback.sock") diff --git a/test/programs/dev-repl.flan b/test/programs/dev-repl.flan index e9fa843..f829e37 100644 --- a/test/programs/dev-repl.flan +++ b/test/programs/dev-repl.flan @@ -11,7 +11,7 @@ ;;; because the break loop matches on a class the *host* was compiled with. (defstruct Missing [id i32]) -(defvar ticks i64) +(defonce ticks i64) (defn step [] i64 (set ticks (+ ticks 1)) ticks) diff --git a/test/programs/dev-rerun.flan b/test/programs/dev-rerun.flan index f1ff710..2960550 100644 --- a/test/programs/dev-rerun.flan +++ b/test/programs/dev-rerun.flan @@ -1,7 +1,8 @@ ;;;; What a re-run does to a global, which is decided by the form that ;;;; defined it and not by the daemon. ;;;; -;;;; A [defvar] is Common Lisp's [defvar]: its initialiser runs only if the +;;;; 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 ;;;; 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] @@ -12,7 +13,7 @@ ;;;; 2026-09-20 there is no other kind: a defconst's initialiser has to be a ;;;; compile-time constant, refused in the checker so that both backends ;;;; refuse the same program. Before that the LLVM backend refused a computed -;;;; one while x86 guarded it at startup like a defvar. +;;;; 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 ;;;; 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. (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. -(defvar zeroed i64) +(defonce zeroed i64) ;; A computed dyn global: the map is built by a function, rooted before the ;; startup function runs, and mutated by every run. Its contents have to @@ -36,7 +37,7 @@ ;; has to survive collection either way. (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 ;; 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 ;; reason, and if the new spelling had grown a startup path of its own this is ;; the line that would count 1, 1, 1, 1. -(defvar tally 0) +(defonce tally 0) ;; A typed array with a computed initialiser: (array-fill ...) is an ;; expression, so it is lifted into the startup function and guarded there @@ -52,7 +53,7 @@ ;; entry into main — this would count 251, 251, 251, 251 instead of climbing, ;; which is [counter]'s own failure in the one shape that only an array can ;; have. -(defvar grid [2 [3 u8]] (array-fill [2 3] 250)) +(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 ;; to be spelled [.init-once.] — a name a program can write, since [.] @@ -61,7 +62,20 @@ ;; now; before that the dev build died at the assembler with the symbol ;; defined twice, and the flag's Bool retyped this i64 on the way. It stays ;; unprinted on purpose — the expected output is what it was. -(defvar .init-once.counter i64 7) +(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 (agent/start "/tmp/flan-dev-rerun-fallback.sock") @@ -71,6 +85,8 @@ (put state :runs (+ (get state :runs) 1)) (set tally (+ tally 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 "zeroed ") (print zeroed) (println "") (print "runs ") (print (get state :runs)) (println "") @@ -80,6 +96,8 @@ ;; on every run, and 0 if the initialiser had been skipped outright. (print "grid-far ") (print (i32 (at grid 1 2))) (println "") (print "base ") (print base) (println "") + (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 ;; any watchdog — dev-macro.flan's clock, for its reason. (dotimes [i 100] diff --git a/test/programs/dev-robust.flan b/test/programs/dev-robust.flan index 2f1f321..625e0be 100644 --- a/test/programs/dev-robust.flan +++ b/test/programs/dev-robust.flan @@ -14,7 +14,7 @@ ;;;; leave a process of this behind for the rest of the afternoon. (import agent "vendor:agent") -(defvar ticks i64) +(defonce ticks i64) (defn step [] i64 (set ticks (+ ticks 1)) diff --git a/test/programs/dev-trap-null-alloc.flan b/test/programs/dev-trap-null-alloc.flan index 05d5231..4d8fdc9 100644 --- a/test/programs/dev-trap-null-alloc.flan +++ b/test/programs/dev-trap-null-alloc.flan @@ -1,7 +1,7 @@ ;;;; The same claim as dev-trap-free-all.flan over the other allocator trap, ;;;; 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" ;;;; 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 @@ -10,7 +10,7 @@ ;;;; can fix the program in and a daemon that is gone. (import agent "vendor:agent") -(defvar nowhere Allocator) +(defonce nowhere Allocator) (defn main [] i32 (agent/start "/tmp/flan-dev-trap-null-alloc-fallback.sock") diff --git a/test/programs/dev-watch.flan b/test/programs/dev-watch.flan index 5283a5d..b92ec5d 100644 --- a/test/programs/dev-watch.flan +++ b/test/programs/dev-watch.flan @@ -20,7 +20,7 @@ ;; 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") -(defvar ticks i64) +(defonce ticks i64) (defn loop-cells [] i32 (let [i 0] diff --git a/test/programs/dyn-boundary.flan b/test/programs/dyn-boundary.flan index 9556c0e..e787df5 100644 --- a/test/programs/dyn-boundary.flan +++ b/test/programs/dyn-boundary.flan @@ -10,10 +10,10 @@ ;;;; be unboxed at are the ones that do: a parameter, a return type, and a ;;;; global's declared type. All three are here. -(defvar seven i64 7) -(defvar boxed dyn 21) +(defonce seven i64 7) +(defonce boxed dyn 21) ;; 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 (+ d 100)) diff --git a/test/programs/dyn-global.flan b/test/programs/dyn-global.flan index e16f16d..26d609c 100644 --- a/test/programs/dyn-global.flan +++ b/test/programs/dyn-global.flan @@ -7,8 +7,8 @@ ;;;; the computed globals already use, which is the point: a dyn global is a ;;;; computed global and needed no new mechanism. -(defvar counter dyn 0) -(defvar label dyn "start") +(defonce counter dyn 0) +(defonce label dyn "start") (defn bump [] () (set counter (+ counter 1))) diff --git a/test/programs/dyn-map.flan b/test/programs/dyn-map.flan index e41b920..671e187 100644 --- a/test/programs/dyn-map.flan +++ b/test/programs/dyn-map.flan @@ -10,7 +10,7 @@ ;; A dyn global: rooted once at startup, so what main stores in it survives ;; every collection the churn loop below causes. -(defvar config dyn) +(defonce config dyn) (defn main [] i32 ;; The literal, and what it prints as. diff --git a/test/programs/dyn-struct.flan b/test/programs/dyn-struct.flan index 9906d20..026e64d 100644 --- a/test/programs/dyn-struct.flan +++ b/test/programs/dyn-struct.flan @@ -53,11 +53,11 @@ ;;; A global holding dyn words, which main roots before a line of the program ;;; runs and never pops. Zero until its field is set, and a zero word is not a ;;; value the collector follows. -(defvar registry Row) +(defonce registry Row) ;;; What the handler saw, read back after the handler had allocated. -(defvar echoed dyn) -(defvar stalls i64) +(defonce echoed dyn) +(defonce stalls i64) ;;; The collector's own counters, so that "nothing leaks" is a fact this ;;; program states rather than one the absence of a crash implies. diff --git a/test/programs/dyn-view.flan b/test/programs/dyn-view.flan index 9845f03..5dbaf91 100644 --- a/test/programs/dyn-view.flan +++ b/test/programs/dyn-view.flan @@ -42,17 +42,17 @@ (defn as-dyn [d dyn] dyn d) -(defvar v (Vec i64) (vec-new i64)) -(defvar a [4 i64]) -(defvar a2 [3 f64]) -(defvar bv (Vec bool) (vec-new bool)) +(defonce v (Vec i64) (vec-new i64)) +(defonce a [4 i64]) +(defonce a2 [3 f64]) +(defonce bv (Vec bool) (vec-new bool)) ;; A global ARRAY of Vecs. An element of this is permanent — it sits inside ;; the global's own storage at a fixed offset — and a view over it is the ;; acceptance half of the [At] arm's guard. The refusal half is the same ;; program with [[(Vec i64)]] (a global SLICE) instead, which holds only ;; ptr+len and so says nothing about where the Vecs live; test_flan.ml ;; carries that pair, because a refusal cannot run. -(defvar rows [2 (Vec i64)]) +(defonce rows [2 (Vec i64)]) (defn main [args [string]] i32 (let [n (i32 (bytes->i64 (bytes-view (at args 1))))] diff --git a/test/programs/edn-read.flan b/test/programs/edn-read.flan index 8e945e0..590f555 100644 --- a/test/programs/edn-read.flan +++ b/test/programs/edn-read.flan @@ -81,9 +81,9 @@ ;; texture path printed here has to be the one show-tileset printed, which is ;; what says read-file read the file and not merely something — and that ;; freeing the buffer inside the call took none of the document with it. The -;; defvar is a dyn global, rooted once at startup, so the document lives past +;; defonce is a dyn global, rooted once at startup, so the document lives past ;; the frame that read it. -(defvar game-data dyn) +(defonce game-data dyn) (defn by-path [] () (set game-data (edn/read-file "programs/assets/edn/tileset.edn")) @@ -97,7 +97,7 @@ ;; ;; The map check is the assertion: the answer is a real document rather than ;; the nil a swallowed error would have to become, so the restart was taken. -(defvar saw-file-error i64) +(defonce saw-file-error i64) (defn by-missing-path [] () (handler-bind diff --git a/test/programs/exhausted.flan b/test/programs/exhausted.flan index 34688d9..dbb0794 100644 --- a/test/programs/exhausted.flan +++ b/test/programs/exhausted.flan @@ -20,11 +20,11 @@ ;; Globals, because a handler cannot see the locals of the function that ;; established it: check.ml's `captured` refuses one by name and says to use a ;; global. That refusal is the accumulation pattern, and it is not built. -(defvar tight Allocator) -(defvar failures i64) -(defvar last-bytes i64) -(defvar last-align i64) -(defvar same-allocator bool) +(defonce tight Allocator) +(defonce failures i64) +(defonce last-bytes i64) +(defonce last-align i64) +(defonce same-allocator bool) (defn main [] i32 ;; The general-purpose tier, with a ceiling on it. 32 bytes is four i32 and diff --git a/test/programs/files.flan b/test/programs/files.flan index 094dbf9..7a1028c 100644 --- a/test/programs/files.flan +++ b/test/programs/files.flan @@ -15,9 +15,9 @@ ;; Handlers cannot see the locals of the function that established them, so the ;; observations are globals, as in slurp.flan. -(defvar seen i64) -(defvar last-reason i32) -(defvar last-op i32) +(defonce seen i64) +(defonce last-reason i32) +(defonce last-op i32) (defn main [] i32 ;; ── The questions ───────────────────────────────────────────────── diff --git a/test/programs/fill.flan b/test/programs/fill.flan index 2597f5b..935ef03 100644 --- a/test/programs/fill.flan +++ b/test/programs/fill.flan @@ -21,8 +21,8 @@ ;;;; A computed global initialiser: a fill is never a constant the linker can ;;;; write, so this one goes through the startup function on both backends. -(defvar gfill [4 u8] (filled 0x41)) -(defvar gsent [5 u8]) +(defonce gfill [4 u8] (filled 0x41)) +(defonce gsent [5 u8]) (defn bytes4 [b [4 u8]] () (dotimes [i 4] (print (at b i)) (print " "))) diff --git a/test/programs/fn-values.flan b/test/programs/fn-values.flan index 7875c5b..bd5241f 100644 --- a/test/programs/fn-values.flan +++ b/test/programs/fn-values.flan @@ -47,7 +47,7 @@ (when (> x 100) (signal (TooBig {.n x}))) x) -(defvar seen i32) +(defonce seen i32) ;; A handler-bind and an fn literal in *one* function, which is the case that ;; would catch the two lifted-function name sequences sharing a counter: both diff --git a/test/programs/frame-rollback.flan b/test/programs/frame-rollback.flan index c25d3e7..2d3ffe6 100644 --- a/test/programs/frame-rollback.flan +++ b/test/programs/frame-rollback.flan @@ -63,26 +63,26 @@ ;;; back grids *and* a copy of the state object, and in Flan each is one `set`. (defstruct World [placed i32 brush i32]) -(defvar grid [8 i32]) -(defvar world World) +(defonce grid [8 i32]) +(defonce world World) ;;; The snapshot. Same types, same declarations; there is nothing else to it. -(defvar grid-backup [8 i32]) -(defvar world-backup World) +(defonce grid-backup [8 i32]) +(defonce world-backup World) ;;; Evidence, and it has to live *outside* the snapshot or restore rolls back ;;; the proof along with the state. `writes` counts the mutations a frame made ;;; before it failed, `tails` counts the defers that ran on the way out. -(defvar frames i64) -(defvar skipped i64) -(defvar writes i64) -(defvar tails i64) +(defonce frames i64) +(defonce skipped i64) +(defonce writes i64) +(defonce tails i64) ;;; 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 the ;;; condition's numbers land up here too. -(defvar low i64) -(defvar length i64) +(defonce low i64) +(defonce length i64) ;;; ── snapshot and restore ────────────────────────────────────────────── ;;; The whole of it. A fixed array copies on assignment and so does a struct diff --git a/test/programs/global-init.flan b/test/programs/global-init.flan index 791f0e9..b45371c 100644 --- a/test/programs/global-init.flan +++ b/test/programs/global-init.flan @@ -9,7 +9,7 @@ ;;;; What each half of this file is asserting: ;;;; ;;;; - an arena allocated at startup and used from main, which is the form -;;;; the author kept writing: (defvar frame Allocator (arena-new N)). +;;;; the author kept writing: (defonce frame Allocator (arena-new N)). ;;;; - the order. `derived` is written above the global it reads, so the ;;;; declaration order is the wrong one and the sort is what makes it 30. ;;;; - a dependency that runs through a call rather than through the text of @@ -25,28 +25,38 @@ (defdata Shape [Nothing (Circle [r i32]) (Square [side i32])]) ;; Written above `base`, and it reads it. -(defvar derived i64 (* base 10)) -(defvar base i64 (+ 1 2)) +(defonce derived i64 (* base 10)) +(defonce base i64 (+ 1 2)) (defn twice-base [] i64 (* base 2)) ;; Names no global; the function it calls does. -(defvar via-fn i64 (+ (twice-base) 1)) +(defonce via-fn i64 (+ (twice-base) 1)) ;; The author's arena, and the Vec it is meant to hold. -(defvar frame Allocator (arena-new 262144)) +(defonce frame Allocator (arena-new 262144)) ;; Control flow, each shape in its own global. (defn maybe [] (Option i32) (Some 3)) -(defvar matched i32 (match (maybe) (Some x) x None 0)) -(defvar branched i64 (if (> base 2) (let [k (+ base 1)] (* k 2)) 0)) -(defvar counted i64 (let [t (i64 0)] (while (< t 4) (set t (+ t 1))) t)) +(defonce matched i32 (match (maybe) (Some x) x None 0)) +(defonce branched i64 (if (> base 2) (let [k (+ base 1)] (* k 2)) 0)) +(defonce counted i64 (let [t (i64 0)] (while (< t 4) (set t (+ t 1))) t)) ;; A data type case: a store at startup, where a constant would have needed a ;; byte-level encoder for the payload blob. -(defvar shape Shape (Shape.Circle {.r 7})) +(defonce shape Shape (Shape.Circle {.r 7})) ;; And a container whose real value only exists behind an allocator. -(defvar names (Vec i64) (vec-new i64)) +(defonce names (Vec i64) (vec-new i64)) + +;; [def], the re-run form, in a plain run — where there is no re-run, so each +;; is simply its initialiser's value. What these four pin is that every +;; spelling lowers and starts identically on both backends, with the +;; initialiser lifted into [global/] whatever it is (a def's always is, +;; zero and literal included) rather than written into the image. +(def d-zero [2 u32]) +(def d-typed i64 (+ base 2)) +(def d-const i64 5) +(def d-dyn 6) (defn main [] i32 (println derived) @@ -72,4 +82,8 @@ (push v 7) (println (at v 0)))) (free-all frame) + (println (i32 (at d-zero 0))) + (println d-typed) + (println d-const) + (println d-dyn) 0) diff --git a/test/programs/handler-case.flan b/test/programs/handler-case.flan index b469b99..852efa4 100644 --- a/test/programs/handler-case.flan +++ b/test/programs/handler-case.flan @@ -24,8 +24,8 @@ (defstruct Corrupt [id i32]) (defstruct Late [id i32]) -(defvar log i64) -(defvar frame Allocator) +(defonce log i64) +(defonce frame Allocator) (defn note [n i64] () (set log (+ (* log 10) n))) diff --git a/test/programs/init-conditions.flan b/test/programs/init-conditions.flan index ee56bd2..d89827a 100644 --- a/test/programs/init-conditions.flan +++ b/test/programs/init-conditions.flan @@ -45,12 +45,12 @@ (defstruct Missing [id i32]) (defstruct Late [id i32]) -(defvar log i64) +(defonce log i64) ;;; A counter of its own, because two computed globals that both wrote [log] ;;; would be asserting the order the initialiser sort happened to pick between ;;; two that do not depend on each other — and that order is not this file's ;;; subject. -(defvar late i64) +(defonce late i64) (defn note [n i64] () (set log (+ (* log 10) n))) @@ -97,20 +97,20 @@ ;;; handler-bind inside it — which is slurp's own shape and the reason the ;;; checker lets a condition form stand here at all. -(defvar fired i32 +(defonce fired i32 (handler-case (both 4) [(Missing [c] (+ 100 (.id c)))])) -(defvar quiet i32 +(defonce quiet i32 (handler-case (+ 1 40) [(Missing [c] -1)])) -(defvar bound i32 +(defonce bound i32 (handler-bind [(Late [c] (set late 5))] (do (signal (Late {.id 1})) 7))) -(defvar restarted i32 +(defonce restarted i32 (restart-case (handler-bind [(Missing [c] (invoke-restart 'use-zero 9))] (raise 3)) @@ -119,7 +119,7 @@ ;;; The author's own form, which is what sent anyone looking: a dyn global read ;;; out of a file that is not there, with the FileError answered by nil. The ;;; path is never present in a build directory, so the condition always fires. -(defvar game-data dyn +(defonce game-data dyn (handler-case (edn/read-file "no-such-file-here.edn") [(FileError [c] nil)])) diff --git a/test/programs/int-float.flan b/test/programs/int-float.flan index 6aeb8d2..20a9042 100644 --- a/test/programs/int-float.flan +++ b/test/programs/int-float.flan @@ -16,9 +16,9 @@ ;;; if (Vec i32) had been written. (defalias Row (Vec int)) -;;; A zeroed static, from the three-element defvar whose third element is read +;;; A zeroed static, from the three-element defonce whose third element is read ;;; as a type and not as a value. -(defvar total int) +(defonce total int) ;;; Both spellings in one signature, to make the point that they are the same ;;; two types and not a parallel pair. diff --git a/test/programs/int-generic.flan b/test/programs/int-generic.flan index 03ceb3d..a5d8600 100644 --- a/test/programs/int-generic.flan +++ b/test/programs/int-generic.flan @@ -12,8 +12,8 @@ ;;;; order (FIX.org 2026-09-20), so both orders print the same number from ;;;; the same copy. -(defvar i32min i32 -2147483648) -(defvar i64min i64 -9223372036854775808) +(defonce i32min i32 -2147483648) +(defonce i64min i64 -9223372036854775808) ;; The low n bits, which needs a shift, a bit-and and the literal 1 — every ;; one of them admitted by integer? and none by anything weaker. diff --git a/test/programs/into.flan b/test/programs/into.flan index 3374f21..50a42d2 100644 --- a/test/programs/into.flan +++ b/test/programs/into.flan @@ -15,7 +15,7 @@ ;;;; which is move-only, and it is still alive and freeable afterwards. ;;;; 4. A source that is a call is evaluated once, not once per element. -(defvar pulls i32 0) +(defonce pulls i32 0) (defn double [x i32] i32 (set pulls (+ pulls 1)) @@ -25,7 +25,7 @@ (set pulls (+ pulls 1)) (= (% x 2) 0)) -(defvar builds i32 0) +(defonce builds i32 0) ;; A source that is a call. It must be made once, however many elements come ;; out of it. It borrows rather than allocating, which is the shape a call in diff --git a/test/programs/json.flan b/test/programs/json.flan index 0f662a9..220dcf5 100644 --- a/test/programs/json.flan +++ b/test/programs/json.flan @@ -33,7 +33,7 @@ (import json "vendor:json") -(defvar frame Allocator) +(defonce frame Allocator) ;; ── A dump of the token stream ────────────────────────────────────── ;; diff --git a/test/programs/machine.flan b/test/programs/machine.flan index c089a2c..a4c2be2 100644 --- a/test/programs/machine.flan +++ b/test/programs/machine.flan @@ -2,8 +2,8 @@ ;;;; milestone-2 surface calc-me does not reach. (defconst rows 3) (defconst cols 4) -(defvar grid [rows [cols i32]]) -(defvar total i32) +(defonce grid [rows [cols i32]]) +(defonce total i32) (defconst pal [4 u32] [10 20 30 40]) (defstruct P [x i32 y i32]) diff --git a/test/programs/map-exhausted.flan b/test/programs/map-exhausted.flan index 92ed688..8d2e226 100644 --- a/test/programs/map-exhausted.flan +++ b/test/programs/map-exhausted.flan @@ -12,10 +12,10 @@ ;; Globals, because a handler cannot see the locals of the function that ;; established it. -(defvar tight Allocator) -(defvar failures i64) -(defvar last-bytes i64) -(defvar same-allocator bool) +(defonce tight Allocator) +(defonce failures i64) +(defonce last-bytes i64) +(defonce same-allocator bool) (defn main [] i32 (set tight (heap-allocator)) diff --git a/test/programs/math2.flan b/test/programs/math2.flan index 56c2f48..9ba800a 100644 --- a/test/programs/math2.flan +++ b/test/programs/math2.flan @@ -20,7 +20,7 @@ ;;; clamp must evaluate each of its three arguments exactly once. A macro that ;;; repeated one — say (if (< x lo) lo (if (> x hi) hi x)), which names x twice ;;; — would read identically and call this twice. -(defvar calls i32 0) +(defonce calls i32 0) (defn tick [x i32] i32 (set calls (+ calls 1)) diff --git a/test/programs/math3.flan b/test/programs/math3.flan index 3e81f29..0822ea5 100644 --- a/test/programs/math3.flan +++ b/test/programs/math3.flan @@ -21,16 +21,16 @@ ;;; The operands of the (% ...) block at the end. See the note there for why ;;; they are globals. -(defvar rem-a f64 7.5) -(defvar rem-na f64 -7.5) -(defvar rem-b f64 2.0) -(defvar rem-nb f64 -2.0) -(defvar rem-z f64 0.0) -(defvar rem-huge f64 1e300) -(defvar rem-a32 f32 7.5) -(defvar rem-na32 f32 -7.5) -(defvar rem-b32 f32 2.0) -(defvar rem-z32 f32 0.0) +(defonce rem-a f64 7.5) +(defonce rem-na f64 -7.5) +(defonce rem-b f64 2.0) +(defonce rem-nb f64 -2.0) +(defonce rem-z f64 0.0) +(defonce rem-huge f64 1e300) +(defonce rem-a32 f32 7.5) +(defonce rem-na32 f32 -7.5) +(defonce rem-b32 f32 2.0) +(defonce rem-z32 f32 0.0) (defn main [] i32 ;; The f32 half. tan, the three inverses, the three logarithms and exp. diff --git a/test/programs/nil-option.flan b/test/programs/nil-option.flan index db89e9e..d5a830e 100644 --- a/test/programs/nil-option.flan +++ b/test/programs/nil-option.flan @@ -13,7 +13,7 @@ ;;;; refused at compile time instead -- see test_flan.ml and ;;;; test_acceptance.ml's "nil at a bare T, compile time" row for that half. -(defvar absent (Option i64) nil) +(defonce absent (Option i64) nil) (defn opt-of [flag bool] (Option i64) (if flag (Some 7) nil)) diff --git a/test/programs/nth-gone.flan b/test/programs/nth-gone.flan index e338553..0527a1c 100644 --- a/test/programs/nth-gone.flan +++ b/test/programs/nth-gone.flan @@ -5,7 +5,7 @@ ;;;; worse than one name, so nth is gone and this pins the removal: it must ;;;; fail as an unknown name, not quietly resolve to at again. -(defvar a [4 i32]) +(defonce a [4 i32]) (defn main [] i32 (nth a 0)) diff --git a/test/programs/pkg-data.flan b/test/programs/pkg-data.flan index 4bc0bce..98ae3dd 100644 --- a/test/programs/pkg-data.flan +++ b/test/programs/pkg-data.flan @@ -11,7 +11,7 @@ (import tree "pkgs/tree") -(defvar frame Allocator) +(defonce frame Allocator) ;; The type in a signature, and a bare pattern over a value the package made. (defn describe [t tree/Node] string diff --git a/test/programs/pkgs/shadowed/shadowed.flan b/test/programs/pkgs/shadowed/shadowed.flan index 1e67901..2595e45 100644 --- a/test/programs/pkgs/shadowed/shadowed.flan +++ b/test/programs/pkgs/shadowed/shadowed.flan @@ -13,6 +13,6 @@ ;;; qualified: a global's initialiser, which runs at startup and is checked ;;; with no owner at all. The importer below defines a len of its own; this ;;; one is the builtin's and this global is 4. -(defvar size i32 (len "abcd")) +(defonce size i32 (len "abcd")) (defn stored-size [] i32 size) diff --git a/test/programs/prelude-macros.flan b/test/programs/prelude-macros.flan index d4ec1dc..aa5961b 100644 --- a/test/programs/prelude-macros.flan +++ b/test/programs/prelude-macros.flan @@ -9,7 +9,7 @@ (defstruct Counter [hits i32 misses i32]) -(defvar dyn-count dyn 5) +(defonce dyn-count dyn 5) (defn show [label string n i32] () (print label) diff --git a/test/programs/printers.flan b/test/programs/printers.flan index 1dcda6e..5423c68 100644 --- a/test/programs/printers.flan +++ b/test/programs/printers.flan @@ -12,11 +12,11 @@ (defstruct Blob [id i32 name string pos V tags [3 i32]]) (defenum Colour [red 0 green 1 blue 2]) -(defvar ticks i64) -(defvar big u64) -(defvar b Blob) -(defvar arr [4 i32]) -(defvar col Colour) +(defonce ticks i64) +(defonce big u64) +(defonce b Blob) +(defonce arr [4 i32]) +(defonce col Colour) ;;; Read by nothing on purpose: it is here to be *evaluated*, so that C-x C-e ;;; is tested against a constant living in the program's memory and not only diff --git a/test/programs/println.flan b/test/programs/println.flan index 71b2cc8..8a27c7f 100644 --- a/test/programs/println.flan +++ b/test/programs/println.flan @@ -23,16 +23,16 @@ (defstruct D2 [d D3]) (defstruct D1 [d D2]) -(defvar b Blob) -(defvar col Colour) -(defvar big u64) -(defvar small u64) -(defvar arr [4 i32]) -(defvar wide [10 i32]) -(defvar deep D1) -(defvar n i32) +(defonce b Blob) +(defonce col Colour) +(defonce big u64) +(defonce small u64) +(defonce arr [4 i32]) +(defonce wide [10 i32]) +(defonce deep D1) +(defonce n i32) (defstruct Long [s string]) -(defvar long-one Long) +(defonce long-one Long) (defn nothing [] () ) diff --git a/test/programs/raylib-audio.flan b/test/programs/raylib-audio.flan index 91c41f2..ac60569 100644 --- a/test/programs/raylib-audio.flan +++ b/test/programs/raylib-audio.flan @@ -62,7 +62,7 @@ ;; different from its neighbours, both signs present, so a decode that lost ;; the byte order or the sign is a wrong number rather than merely a ;; different one. -(defvar pcm [16 u8]) +(defonce pcm [16 u8]) (defn load-pcm [] () (set (at pcm 0) 0x00) (set (at pcm 1) 0x00) ; 0 diff --git a/test/programs/raylib-codepoints.flan b/test/programs/raylib-codepoints.flan index 6211b16..74aaab4 100644 --- a/test/programs/raylib-codepoints.flan +++ b/test/programs/raylib-codepoints.flan @@ -41,10 +41,10 @@ (defconst max-walk 128) -(defvar forward [max-walk i32]) -(defvar forward-n i32) -(defvar backward [max-walk i32]) -(defvar backward-n i32) +(defonce forward [max-walk i32]) +(defonce forward-n i32) +(defonce backward [max-walk i32]) +(defonce backward-n i32) (defn yes-no [b bool] string (if b "yes" "no")) diff --git a/test/programs/raylib-font.flan b/test/programs/raylib-font.flan index 4ead0dd..0a8c8a6 100644 --- a/test/programs/raylib-font.flan +++ b/test/programs/raylib-font.flan @@ -57,7 +57,7 @@ ;; The atlas rectangles, one per glyph. x and width differ per glyph so that ;; a fallback to the wrong index is a wrong number; height is the same 10 for ;; all three because raylib never reads it here. -(defvar glyph-recs [3 rl/Rectangle]) +(defonce glyph-recs [3 rl/Rectangle]) ;; The glyphs themselves. `image` is raylib's own pixels for the glyph and is ;; left zeroed — it is present so the four ints in front of it are at the @@ -65,7 +65,7 @@ ;; checked: moving `image` to the front of the defstruct shifts the four ints ;; by 24 bytes, and the glyph search then finds nothing — every index reads 0 ;; and glyph C answers with A's numbers. -(defvar glyphs [3 rl/GlyphInfo]) +(defonce glyphs [3 rl/GlyphInfo]) (defn build-glyphs [] () (set (at glyph-recs 0) (rl/Rectangle {.x 0.0 .y 0.0 .width 5.0 .height 10.0})) diff --git a/test/programs/reach-walk.flan b/test/programs/reach-walk.flan index 3561167..d358ea7 100644 --- a/test/programs/reach-walk.flan +++ b/test/programs/reach-walk.flan @@ -16,8 +16,8 @@ ;;;; Nothing here is about the values; the values are how the test notices that ;;;; the program was built and ran at all. -(defvar cells [4 i32]) -(defvar slot i32) +(defonce cells [4 i32]) +(defonce slot i32) (defstruct Nope [id i32]) diff --git a/test/programs/registry.flan b/test/programs/registry.flan index 63921dd..ae27e29 100644 --- a/test/programs/registry.flan +++ b/test/programs/registry.flan @@ -17,7 +17,7 @@ (declare-c reg-live [p (Ptr i32)] i32 "flan_dev_reg_live") (declare-c reg-count [live i32] i64 "flan_dev_reg_count") -(defvar frame Allocator) +(defonce frame Allocator) (defn main [] i32 ;; Armed by a constructor in a dev build and never in a release one. diff --git a/test/programs/reload-agg-v2.flan b/test/programs/reload-agg-v2.flan index 75a85de..ea6b205 100644 --- a/test/programs/reload-agg-v2.flan +++ b/test/programs/reload-agg-v2.flan @@ -26,7 +26,7 @@ (defstruct Duo [x f64 y f64]) (defstruct Mix [n i64 z f64]) -(defvar counter i64) +(defonce counter i64) (defn weigh-pair [p Pair] i64 (+ (.a p) (* 30 (.b p)))) diff --git a/test/programs/reload-agg.flan b/test/programs/reload-agg.flan index 3ee1ff0..5e993a4 100644 --- a/test/programs/reload-agg.flan +++ b/test/programs/reload-agg.flan @@ -44,7 +44,7 @@ ;;; The state that has to survive a reload, written by a loaded module rather ;;; than by the host: a redefinition declares it external, so the store lands ;;; on the host's copy and not on a private one. -(defvar counter i64) +(defonce counter i64) ;;; The other direction. These are never redefined, so a module reaches each ;;; one through its cell and hands it an aggregate — module to host, where the diff --git a/test/programs/reload-generic.flan b/test/programs/reload-generic.flan index 4a8e083..fe66719 100644 --- a/test/programs/reload-generic.flan +++ b/test/programs/reload-generic.flan @@ -8,7 +8,7 @@ ;;;; call site whose element type is *not* used anywhere else, so that a ;;;; redefinition can reach a copy the process was never built with. -(defvar counter i64) +(defonce counter i64) (defn put-at [xs [$t] i i32 v $t] () (set (at xs i) v)) diff --git a/test/programs/reload-v2.flan b/test/programs/reload-v2.flan index ad80dee..6d3f4af 100644 --- a/test/programs/reload-v2.flan +++ b/test/programs/reload-v2.flan @@ -14,8 +14,8 @@ ;;;; defining it, so this body is dead text and the call has to land on the ;;;; host's [* x 2]; with the two bodies identical nothing at run time would ;;;; notice a module that grew its own copy. -(defvar counter i64) -(defvar tally (Vec u8)) +(defonce counter i64) +(defonce tally (Vec u8)) (defn helper [x i64] i64 (* x 3)) diff --git a/test/programs/reload-v3.flan b/test/programs/reload-v3.flan index 78665cb..e92c620 100644 --- a/test/programs/reload-v3.flan +++ b/test/programs/reload-v3.flan @@ -1,4 +1,4 @@ -;;;; v3 introduces names the host was never built with: a defvar [extra] and a +;;;; v3 introduces names the host was never built with: a defonce [extra] and a ;;;; defn [added]. There is no symbol in the running process to bind either to ;;;; and ELF cannot grow one, so both are keyed by string through ;;;; runtime/flan_dev.c and resolved once when the module is installed. @@ -7,14 +7,14 @@ ;;;; being a list of forms rather than one function: C-c C-k on a file that ;;;; adds a var and uses it has to work in one load, or the intermediate state ;;;; is a module referring to storage that does not exist yet. -(defvar counter i64) -(defvar extra i64) +(defonce counter i64) +(defonce extra i64) ;;; And a run-time-new one of move-only type: no symbol to bind to, so it goes ;;; through the by-name registry like [extra], with its declared initial value ;;; travelling along as a constant. A zeroed Vec is an empty Vec, so that ;;; constant is a zeroinitializer and the allocation the registry makes is a ;;; usable Vec rather than a placeholder. -(defvar fresh (Vec u8)) +(defonce fresh (Vec u8)) (defn helper [x i64] i64 (* x 2)) diff --git a/test/programs/reload-v4.flan b/test/programs/reload-v4.flan index a6b5dd1..9e4754a 100644 --- a/test/programs/reload-v4.flan +++ b/test/programs/reload-v4.flan @@ -7,8 +7,8 @@ ;;;; modules found by name. Had v3 cached the address of the function instead ;;;; of the address of its cell, everything else would still pass and this ;;;; would silently keep running v3's [added]. -(defvar counter i64) -(defvar extra i64) +(defonce counter i64) +(defonce extra i64) (defn helper [x i64] i64 (* x 2)) diff --git a/test/programs/reload-v5.flan b/test/programs/reload-v5.flan index b114017..0b4d327 100644 --- a/test/programs/reload-v5.flan +++ b/test/programs/reload-v5.flan @@ -12,8 +12,8 @@ ;;;; ;;;; Loaded on top of v3, and never alongside v4: the process it aborts is the ;;;; whole of the test. -(defvar counter i64) -(defvar extra i32) +(defonce counter i64) +(defonce extra i32) (defn helper [x i64] i64 (* x 2)) diff --git a/test/programs/reload-v6.flan b/test/programs/reload-v6.flan index 4065cc8..835df20 100644 --- a/test/programs/reload-v6.flan +++ b/test/programs/reload-v6.flan @@ -1,11 +1,11 @@ ;;;; A run-time-new global with a value of its own, which is the half of the -;;;; new-name path that a zeroed defvar cannot measure: [flan_dev_global] +;;;; new-name path that a zeroed defonce cannot measure: [flan_dev_global] ;;;; allocates with calloc, so a global declared zero looks right whether or ;;;; not its initial value travelled with the module. [tuning] is 42, and the ;;;; transcript is the number the host prints, so an image that never arrived ;;;; prints 4 rather than 88. -(defvar counter i64) -(defvar tuning i64 42) +(defonce counter i64) +(defonce tuning i64 42) (defn helper [x i64] i64 (* x 2)) diff --git a/test/programs/reload.flan b/test/programs/reload.flan index 5693798..6e7579f 100644 --- a/test/programs/reload.flan +++ b/test/programs/reload.flan @@ -10,7 +10,7 @@ ;;;; [bump] runs when the host calls [outer] then the cell is doing its job. ;;;; The string is not decoration either — a redefinition module has to carry ;;;; its own constants, and a one-function module usually has none. -(defvar counter i64) +(defonce counter i64) ;;; A move-only global, which is allowed because reading one is always a borrow ;;; (spec-memory.md, "Globals of move-only type"). It is here for the reload @@ -19,13 +19,13 @@ ;;; its own zeroed header -- that would drop the storage the running process is ;;; still using, which is the failure a plain i64 counter cannot exhibit ;;; because a re-zeroed i64 merely looks wrong. -(defvar tally (Vec u8)) +(defonce tally (Vec u8)) ;;; Unused, and that is the point: the session's compatibility rules only get a ;;; chance to speak about a change the *checker* accepts, and retyping a var ;;; something else reads is an ordinary type error long before it is a layout ;;; question. -(defvar spare i64) +(defonce spare i64) ;;; Also unused, and for the same reason: a defconst's value and an enum ;;; member are folded into every call site, so a session has to refuse changing diff --git a/test/programs/restarts.flan b/test/programs/restarts.flan index 3a93acd..0270f96 100644 --- a/test/programs/restarts.flan +++ b/test/programs/restarts.flan @@ -11,7 +11,7 @@ ;;;; on its reason. (defstruct AssetMissing [id i32]) -(defvar log i64) +(defonce log i64) ;;; The signalling end. Two frames below the restart-case, so the transfer has ;;; something to cross. diff --git a/test/programs/shadow-builtin.flan b/test/programs/shadow-builtin.flan index 9f9e316..226f5db 100644 --- a/test/programs/shadow-builtin.flan +++ b/test/programs/shadow-builtin.flan @@ -15,7 +15,7 @@ ;;;; - 99: an operator is a builtin like any other and shadows like one. ;;;; - 999: this file's own len, which is what len means in this file. ;;;; - 4 again, and it is the one that needed the work: the package's global -;;;; initialiser (defvar size i32 (len "abcd")) is checked with no +;;;; initialiser (defonce size i32 (len "abcd")) is checked with no ;;;; enclosing function at all, so there is no qualified name on it to say ;;;; it belongs to a package. The file it was written in says so instead. @@ -33,7 +33,7 @@ ;;; And a builtin the imported package uses in a *global initialiser*, which ;;; is the one place there is no enclosing function to carry a package's -;;; qualified name. The package's (defvar size i32 (len "abcd")) is 4; this +;;; qualified name. The package's (defonce size i32 (len "abcd")) is 4; this ;;; definition answers 999 and is reached only here. (defn len [s string] i32 999) diff --git a/test/programs/shadow-pkg.flan b/test/programs/shadow-pkg.flan index 7d49b58..5ce45b6 100644 --- a/test/programs/shadow-pkg.flan +++ b/test/programs/shadow-pkg.flan @@ -6,7 +6,7 @@ ;;;; different variable, which is why this needs a fixture and not a refusal. (defconst limit 5) -(defvar sink i32) +(defonce sink i32) ;;; The expression case: the body's [limit] is the let's, not the constant. (defn shadowed [] i32 diff --git a/test/programs/slice-from-ptr.flan b/test/programs/slice-from-ptr.flan index a16e9c0..92225d4 100644 --- a/test/programs/slice-from-ptr.flan +++ b/test/programs/slice-from-ptr.flan @@ -19,7 +19,7 @@ ;;;; (free (slice-from-ptr ...)) — a slice owns nothing, so free refuses it by ;;;; the rule it already had. -(defvar a [5 i32]) +(defonce a [5 i32]) (defn total [s [i32]] i32 (let [acc 0] diff --git a/test/programs/slices.flan b/test/programs/slices.flan index c2ce561..7924767 100644 --- a/test/programs/slices.flan +++ b/test/programs/slices.flan @@ -11,9 +11,9 @@ ;;;; must be untouched. That last one is the property that dies silently if a ;;;; slice parameter ever starts being copied. -(defvar xs [7 i32]) -(defvar ys [5 i32]) -(defvar zs [8 i32]) +(defonce xs [7 i32]) +(defonce ys [5 i32]) +(defonce zs [8 i32]) (defn show [s [i32]] () (dotimes [i (len s)] diff --git a/test/programs/slurp.flan b/test/programs/slurp.flan index 077085e..1182a6b 100644 --- a/test/programs/slurp.flan +++ b/test/programs/slurp.flan @@ -20,10 +20,10 @@ ;; Handlers cannot see the locals of the function that established them, so the ;; observations are globals — the same shape exhausted.flan uses. -(defvar seen i64) -(defvar last-reason i32) -(defvar last-op i32) -(defvar last-path string) +(defonce seen i64) +(defonce last-reason i32) +(defonce last-op i32) +(defonce last-path string) (defn main [] i32 ;; ── The happy path ──────────────────────────────────────────────── diff --git a/test/programs/unions.flan b/test/programs/unions.flan index 1c49454..d3c6233 100644 --- a/test/programs/unions.flan +++ b/test/programs/unions.flan @@ -26,8 +26,8 @@ ;; Zeroed and uninit, side by side. A data type refuses uninit because its tag ;; steers a match; this one has no tag to steer anything, so both are legal ;; and the zeroed one is all-bytes-zero. -(defvar zeroed Bits) -(defvar scratch Bits uninit) +(defonce zeroed Bits) +(defonce scratch Bits uninit) (defn as-float [b Bits] f32 (.f b)) diff --git a/test/programs/utf8.flan b/test/programs/utf8.flan index 85096d7..51e08cc 100644 --- a/test/programs/utf8.flan +++ b/test/programs/utf8.flan @@ -36,7 +36,7 @@ (defconst emoji [4 u8] [0xf0 0x9f 0x98 0x80]) ; U+1F600 (defconst bad-tail [3 u8] [0x61 0xff 0x62]) ; "a", junk, "b" -(defvar scratch [4 u8]) +(defonce scratch [4 u8]) ;; Each array is sliced at the point of use rather than through a ;; (defn whole [a [4 u8]] [u8] (slice a 0 4)) helper. That helper is a diff --git a/test/programs/values.flan b/test/programs/values.flan index 1c09e93..63ccabd 100644 --- a/test/programs/values.flan +++ b/test/programs/values.flan @@ -2,7 +2,7 @@ ;;;; most likely to be silently wrong in a backend: a struct or a fixed array ;;;; copies on assignment, a slice copies only its view. (defstruct P [x i32]) -(defvar arr [3 i32]) +(defonce arr [3 i32]) (defn main [] i32 (let [a (P {.x 1})] diff --git a/test/programs/vec-global.flan b/test/programs/vec-global.flan index 4647a21..202a28f 100644 --- a/test/programs/vec-global.flan +++ b/test/programs/vec-global.flan @@ -9,8 +9,8 @@ ;;;; loaded by whoever loads it. What this program pins is that the loading ;;;; happens once and survives: [entry] is called twice, the way a re-entered ;;;; main would be, and the second call finds the data the first one left. -(defvar the-data (Vec u8)) -(defvar counts (Map u8 i64)) +(defonce the-data (Vec u8)) +(defonce counts (Map u8 i64)) ;; Reading it here is a borrow. So is reading it in [total] below, which is the ;; case the old rule could not express: two functions holding the same global diff --git a/test/programs/widening.flan b/test/programs/widening.flan index 6b99021..7817787 100644 --- a/test/programs/widening.flan +++ b/test/programs/widening.flan @@ -23,18 +23,18 @@ ;;;; is built at the wanted width by the literal rule and would never reach a ;;;; cast at all. -(defvar w-i8neg i8 -5) -(defvar w-i8pos i8 127) -(defvar w-i16neg i16 -300) -(defvar w-i32neg i32 -2000000000) -(defvar w-i32one i32 1) -(defvar w-i32all i32 -1) -(defvar w-u8max u8 255) -(defvar w-u16max u16 65535) -(defvar w-u32big u32 4000000000) -(defvar w-u32max u32 4294967295) -(defvar w-i64big i64 5000000000) -(defvar w-f32half f32 0.5) +(defonce w-i8neg i8 -5) +(defonce w-i8pos i8 127) +(defonce w-i16neg i16 -300) +(defonce w-i32neg i32 -2000000000) +(defonce w-i32one i32 1) +(defonce w-i32all i32 -1) +(defonce w-u8max u8 255) +(defonce w-u16max u16 65535) +(defonce w-u32big u32 4000000000) +(defonce w-u32max u32 4294967295) +(defonce w-i64big i64 5000000000) +(defonce w-f32half f32 0.5) ;; Widening at a parameter. Each of these is a plain typed function and the ;; call sites below hand it a narrower type with no cast written anywhere. diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index e5f1465..097eae9 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -376,7 +376,12 @@ let () = author's arena, allocated at startup and used from main). -O0 as well as -O2 because the whole thing is a call and a store before main: an optimiser that inlines it away and one that does not have to agree. *) - (let global_init_out = "30\n3\n7\n3\n8\n4\n7\n2\n22\n7\n" in + (* The last four lines are the [def]s, one per spelling: the zeroed + array, the computed, the constant and the dyn. In a plain run a def is + its initialiser's value like anything else — what the rows pin is that + both backends start all four the same way, through the lifted + [global/] a def always gets. *) + (let global_init_out = "30\n3\n7\n3\n8\n4\n7\n2\n22\n7\n0\n5\n5\n6\n" in outputs "computed global initialisers" "programs/global-init.flan" global_init_out; outputs ~opt:"-O0" "computed global initialisers, -O0" @@ -852,7 +857,7 @@ let () = (* Conditions in a global initialiser, and the defer that has not registered when a transfer starts. The two halves are one program because the second was found underneath a report about the first: a - handler-case around (edn/read-file ...) in a defvar segfaulted, and + handler-case around (edn/read-file ...) in a defonce segfaulted, and what segfaulted was read-file's own (defer (free src)) running on the way out of a frame where slurp had transferred before src was written. @@ -2639,7 +2644,7 @@ let () = like any other name; 999 is this program's len. The last 4 is the one that was a bug. It is the package's global - initialiser, (defvar size i32 (len "abcd")), which is the one place a + initialiser, (defonce size i32 (len "abcd")), which is the one place a call sits inside no function and so carries no package-qualified name — the importer's len reached into it and made it 999. The shadow is decided by the file the definition was written in, and a file is @@ -4112,7 +4117,7 @@ level "1" outputs ~x86:true "dyn: a global, --x86" "programs/dyn-global.flan" dyn_global_out; - (* The three-element defvar, both readings in one program. The first line + (* The three-element defonce, both readings in one program. The first line is the four zeroed statics, the next two are the dyn globals as their initialisers left them, and the last two are what the run stored — read back after twenty thousand allocations, so an unrooted dyn global @@ -4122,18 +4127,18 @@ level "1" half lowers to the startup function and the root push, and those are two backends' worth of code emitted from one shared decision. There is nothing backend-specific in the *rule* — it is settled in [Check], and - what reaches either emitter is the [(defvar x dyn )] that + what reaches either emitter is the [(defonce x dyn )] that already existed — which is precisely what these rows are here to keep true. *) - let defvar_dyn_out = "0 0 0 0\n0 start one 7\n3 4\n5 done 2\n3 9\n" in - outputs "defvar: a type third element and a value third element" - "programs/defvar-dyn.flan" defvar_dyn_out; + let defonce_dyn_out = "0 0 0 0\n0 start one 7\n3 4\n5 done 2\n3 9\n" in + outputs "defonce: a type third element and a value third element" + "programs/defonce-dyn.flan" defonce_dyn_out; outputs ~opt:"-O0" - "defvar: a type third element and a value third element, -O0" - "programs/defvar-dyn.flan" defvar_dyn_out; + "defonce: a type third element and a value third element, -O0" + "programs/defonce-dyn.flan" defonce_dyn_out; outputs ~x86:true - "defvar: a type third element and a value third element, --x86" - "programs/defvar-dyn.flan" defvar_dyn_out; + "defonce: a type third element and a value third element, --x86" + "programs/defonce-dyn.flan" defonce_dyn_out; (* The boundary, both directions, and then the claim that is wrong. The first four lines are the conversions; the trap is the fifth, and the @@ -4679,13 +4684,13 @@ level "1" incr failures; Printf.printf "FAIL %s\n said: %S\n" name m end); - (* A defvar holding one is accepted, and that is the same rule seen from + (* A defonce holding one is accepted, and that is the same rule seen from the other side: its initialiser is computed, so it is lifted into a function that runs at startup and the case is written by an ordinary store. Nothing has to be encoded into anything. *) (let name = "a global initialised with a data type case" in let src = - "(defdata U [A (B [x i32])])\n(defvar g U (U.B {.x 1}))\n\ + "(defdata U [A (B [x i32])])\n(defonce g U (U.B {.x 1}))\n\ (defn main [] i32 (match g A 0 (B x) x))" in match @@ -4699,7 +4704,7 @@ level "1" (* And a computed initialiser on a defconst is refused by name, because a constant is what the linker writes and there is nowhere for it to run. The checker, so that the x86 backend refuses it too — it used to run one - through its startup function like a defvar. What folds is still not the + through its startup function like a defonce. What folds is still not the checker's opinion: the accepted set is [Tast.const_init]'s, plus the integer arithmetic [collect] had already folded before this ran. *) (let name = "a defconst with a computed initialiser" in @@ -4724,7 +4729,7 @@ level "1" past every comparison in a match into the block LLVM is entitled to assume cannot be reached. *) refuses_src "uninit on a data type global" - "(defdata U [A B])\n(defvar g U uninit)\n(defn main [] i32 0)" + "(defdata U [A B])\n(defonce g U uninit)\n(defn main [] i32 0)" "its tag steers every match"; (* A data type's fields belong to a case, so .field is not a read anyone can do without having read the tag first. match is how one is opened. *) @@ -4739,7 +4744,7 @@ level "1" (Check.program (Parse.program (Reader.read_all ~file:"" - "(defdata U [A (B [x i32])])\n(defvar g U)\n\ + "(defdata U [A (B [x i32])])\n(defonce g U)\n\ (defn main [] i32 (match g A 0 (B x) x))"))) with | _ -> () @@ -5468,8 +5473,8 @@ level "1" the way out. Each would be a verifier rejection without a location. *) let src = "(defstruct Missing [id i32])\n\ - (defvar seen i64)\n\ - (defvar arr [4 i32])\n\ + (defonce seen i64)\n\ + (defonce arr [4 i32])\n\ (defn pick [xs [i32] i i32] i32 (at xs i))\n\ (defn fetch [n i32] i32\n\ \ (restart-case\n\ diff --git a/test/test_agent.ml b/test/test_agent.ml index c6d5a66..2919760 100644 --- a/test/test_agent.ml +++ b/test/test_agent.ml @@ -93,7 +93,7 @@ let () = in let so1 = build_module - "(defvar acc i64) (defn tick [] i64 (set acc (+ acc 1000)) acc)" + "(defonce acc i64) (defn tick [] i64 (set acc (+ acc 1000)) acc)" "tick1.so" in let so2 = diff --git a/test/test_dev.ml b/test/test_dev.ml index b91d6f4..edebe5c 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -512,7 +512,7 @@ let () = kept the first. *) let r = request c - "(:op \"eval\" :code \"(defvar extra i64) (defn step [] i64 (set extra (+ extra 5)) extra)\" :file \"/tmp/buf.flan\")" + "(:op \"eval\" :code \"(defonce extra i64) (defn step [] i64 (set extra (+ extra 5)) extra)\" :file \"/tmp/buf.flan\")" in if status r <> "ok" then fail "adding a var: %s" @@ -532,7 +532,7 @@ let () = (* A change the running process cannot be told, refused with the reason rather than delivered. *) - let r = request c "(:op \"eval\" :code \"(defvar ticks i32)\" :file \"/tmp/buf.flan\")" in + let r = request c "(:op \"eval\" :code \"(defonce ticks i32)\" :file \"/tmp/buf.flan\")" in if status r <> "error" || not (match Wire.string_field r "message" with @@ -771,7 +771,7 @@ let () = comes back rendered. The program has stopped reaching frame boundaries by now, so this only checks that the types that have no printer say so rather than guessing — the live path is test_repl. *) - let r = request c "(:op \"eval-expr\" :code \"(defvar x i64)\" :file \"/tmp/buf.flan\")" in + let r = request c "(:op \"eval-expr\" :code \"(defonce x i64)\" :file \"/tmp/buf.flan\")" in if status r <> "error" then fail "a declaration was accepted as an expression"; ignore (request c "(:op \"close\")"); Unix.close c; @@ -4911,16 +4911,16 @@ let () = if status r <> "ok" then fail "x86 C-x C-e on a literal: %s" (said r) else if value r <> "\"hi\"" then fail "x86 C-x C-e on a literal answered %S" (value r); - (* A defvar the host has no storage for, with a value of its own, and a + (* A defonce the host has no storage for, with a value of its own, and a defn the host has no cell for: both go through flan_dev.c's registry into slots this backend fills at install time. The expression after them reads one and calls the other, so the answer is what says the lookups resolved rather than that the module merely loaded. *) let r = request c - "(:op \"eval\" :code \"(defvar fresh i64 41)\" :file \"/tmp/x86buf.flan\")" + "(:op \"eval\" :code \"(defonce fresh i64 41)\" :file \"/tmp/x86buf.flan\")" in - if status r <> "ok" then fail "x86 new defvar: %s" (said r); + if status r <> "ok" then fail "x86 new defonce: %s" (said r); let r = request c "(:op \"eval\" :code \"(defn twice [x i64] i64 (* x 2))\" :file \"/tmp/x86buf.flan\")" @@ -5467,7 +5467,7 @@ let () = (* ── What a re-run does to a global ───────────────────────────────── *) - (* The rule is the defining form's: a [defvar] is Common Lisp's, so its + (* The rule is the defining form's: a [defonce] is Common Lisp's, so its initialiser runs only if the variable is not already initialised and its value survives a re-run. A zeroed one always did — .bss is untouched by a second entry into [main] — and a computed one did not, because the @@ -5529,7 +5529,7 @@ let () = (In_channel.with_open_bin rout In_channel.input_all) done; let want = - [ (* A computed [defvar], which is the whole bug: 41 on the first run + [ (* A computed [defonce], which is the whole bug: 41 on the first run and one more on each of the three after it. *) "counter 44"; (* An uncomputed one, which survived before this and still does. *) @@ -5552,7 +5552,14 @@ let () = from "the initialiser never ran": 250, not 0. *) "grid-far 250"; (* And a [defconst], which no run can have changed. *) - "base 40" ] + "base 40"; + (* A [def], CL's defparameter: the initialiser runs on every + re-run, unguarded, so where [tally] climbed to 4 this is 4 on + every one of the four runs — 3 repainted, then incremented. *) + "c 4"; + (* The typed-array [def]: the fill repaints the same storage, so + the element every run increments reads 8 every run. *) + "hue 8" ] in List.iter (fun s -> @@ -5560,6 +5567,15 @@ let () = fail "after three re-runs the program never printed %S: %S" s (Buffer.contents output)) want; + (* The other half of each [def] claim: a run that ever saw the last + run's increment would have printed the next number, so its absence + is what says the repaint happened at all. *) + List.iter + (fun s -> + if printed s then + fail "a def survived a re-run it must not survive: %S in %S" s + (Buffer.contents output)) + [ "c 5"; "hue 9" ]; (* Read back rather than only printed, because the two can differ: a printed line is what the run computed, and this is what the global holds now. *) @@ -5572,6 +5588,43 @@ let () = | v -> fail "counter reads back as %S after three re-runs" (Option.value ~default:(status r) v)); + (* The headline: the author edits a [def]'s initialiser and C-c C-c's + it. The evaluation republishes the lifted [global/c] through its + cell — [Session]'s [def_inits] — and the *next re-run* runs the + edited initialiser: 9 repainted, then incremented, so 10. A + [defonce] beside it keeps its value through the same re-run, which + is the pair the two forms exist to be. *) + let r = + request c + "(:op \"eval\" :code \"(def c 9)\" :file \"programs/dev-rerun.flan\")" + in + if status r <> "ok" then fail "re-evaluating (def c 9): %s" (said r); + let r = request c "(:op \"rerun\")" in + if status r <> "ok" then fail "the rerun after the edit: %s" (said r); + if not (await ~ms:20000 parked) then + fail "the re-run after the edit never parked (%S)" + (In_channel.with_open_bin rout In_channel.input_all); + if not (printed "c 10") then + fail "the edited initialiser never took: no %S in %S" "c 10" + (Buffer.contents output); + (* And the defonce beside it: a fifth run, still climbing — the edit + and the extra re-run must not have reset it. *) + if not (printed "counter 45") then + fail "the defonce beside the edited def lost its value: %S" + (Buffer.contents output); + (* Read back rather than only printed, [counter]'s reason. A dyn + renders through the program's printer — its reply carries the text + in [:output] and an empty [:value] — so the cast is what turns the + answer into a value the reply can hold. *) + (let r = + request c + "(:op \"eval-expr\" :code \"(i64 c)\" :file \"programs/dev-rerun.flan\")" + in + match Wire.string_field r "value" with + | Some "10" -> () + | v -> + fail "c reads back as %S after the edit and re-run" + (Option.value ~default:(status r) v)); ignore (request c "(:op \"close\")"); (try Unix.close c with Unix.Unix_error _ -> ()); (try ignore (Unix.waitpid [] rpid) with Unix.Unix_error _ -> ()) diff --git a/test/test_flan.ml b/test/test_flan.ml index 6013f35..db9e11a 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -476,24 +476,56 @@ let () = (match (parse_decl "(defn f [x i32] ())").d with | Defn { ret = Some { t = Tname "Unit"; _ }; fbody = []; _ } -> () | _ -> check "defn returning () with no body" false); - (match (parse_decl "(defvar grid [4 u32])").d with - | Defvar ("grid", Some _, Zeroed) -> () - | _ -> check "defvar is ZII" false); - (match (parse_decl "(defvar buf [4 u8] uninit)").d with - | Defvar (_, _, Uninit) -> () | _ -> check "defvar uninit opts out" false); - (* A three-element defvar whose third element cannot be a type is settled + (match (parse_decl "(defonce grid [4 u32])").d with + | Defvar ("grid", Some _, Zeroed, Once) -> () + | _ -> check "defonce is ZII" false); + (match (parse_decl "(defonce buf [4 u8] uninit)").d with + | Defvar (_, _, Uninit, Once) -> () + | _ -> check "defonce uninit opts out" false); + (* A three-element defonce whose third element cannot be a type is settled here, by its shape, and comes out as the dyn global it means. *) - (match (parse_decl "(defvar score 0)").d with - | Defvar ("score", Some { t = Tname "dyn"; _ }, Init _) -> () + (match (parse_decl "(defonce score 0)").d with + | Defvar ("score", Some { t = Tname "dyn"; _ }, Init _, Once) -> () | _ -> check "a literal third element parses as a dyn initialiser" false); (* A bare symbol could be either and parse does not know any names, so both readings are carried out of here for [Check] to pick between. *) - (match (parse_decl "(defvar total foo)").d with - | Defvar ("total", Some { t = Tname "foo"; _ }, Ambiguous _) -> () + (match (parse_decl "(defonce total foo)").d with + | Defvar ("total", Some { t = Tname "foo"; _ }, Ambiguous _, Once) -> () | _ -> check "a symbol third element parses undecided" false); - (match (parse_decl "(defvar v (Vec i32))").d with - | Defvar ("v", Some { t = Tapp ("Vec", _); _ }, Ambiguous _) -> () + (match (parse_decl "(defonce v (Vec i32))").d with + | Defvar ("v", Some { t = Tapp ("Vec", _); _ }, Ambiguous _, Once) -> () | _ -> check "a parenthesised third element parses undecided" false); + (* [def] takes exactly the spellings [defonce] takes — the same parse arm + reads both — and differs in the one field that says what a re-run does. + One row per spelling, each asserting the [Every]. *) + (match (parse_decl "(def grid [4 u32])").d with + | Defvar ("grid", Some _, Zeroed, Every) -> () + | _ -> check "def is ZII" false); + (match (parse_decl "(def buf [4 u8] uninit)").d with + | Defvar (_, _, Uninit, Every) -> () + | _ -> check "def uninit opts out" false); + (match (parse_decl "(def score 0)").d with + | Defvar ("score", Some { t = Tname "dyn"; _ }, Init _, Every) -> () + | _ -> check "a literal third element of def is a dyn initialiser" false); + (match (parse_decl "(def total foo)").d with + | Defvar ("total", Some { t = Tname "foo"; _ }, Ambiguous _, Every) -> () + | _ -> check "a symbol third element of def parses undecided" false); + (match (parse_decl "(def counter i64 (start))").d with + | Defvar ("counter", Some { t = Tname "i64"; _ }, Init _, Every) -> () + | _ -> check "a typed def with an initialiser" false); + (* The old name, refused with the migration in the message: what it is + called now, why the name, and both new spellings — each of which + compiles as written. *) + parse_rejects "the old defvar spelling names defonce" + "(defvar counter i64 7)" + ~needle:"defvar is now called defonce — the name says what it does: it \ + initialises once and keeps its value across re-runs. Write \ + (defonce counter i64 7), or (def counter i64 7) if the value \ + should follow the source on every re-run"; + (match read "(defvar counter i64 7)" |> Parse.program with + | _ -> check "the old defvar spelling has a kind" false + | exception Loc.Error { Loc.kind; _ } -> + check "the old defvar spelling has a kind" (kind = "parse/defvar-renamed")); (match (parse_decl "(import rl \"vendor:raylib\")").d with | Import ("rl", "vendor:raylib") -> () | _ -> check "import" false); @@ -830,7 +862,7 @@ let probe_env = lazy (snd (Check.program_with_env [])) It used to be the type of [(defconst probe )], which stopped working on 2026-09-20: a defconst's initialiser has to be a compile-time constant now and most of the probes below are calls — (cast ...), (len ...), a - comparison. A defvar would not do either, since only the defconst form + comparison. A defonce would not do either, since only the defconst form takes no type. This asks [check] the question the wrapper was only ever a way of asking. *) let infers name src expected = @@ -887,7 +919,7 @@ let rejects_check name ?needle src = name n msg | _ -> ()) -(* Which reading a three-element [defvar] got, pinned by what the global came +(* Which reading a three-element [defonce] got, pinned by what the global came out as rather than by what compiled: the two readings differ in the type and in whether anything runs at startup, and a test that only asked "does this check" would pass on either one. [zeroed] is the static reading — the @@ -919,6 +951,49 @@ let defvar_reading name src gname ~ty ~zeroed = Printf.printf "FAIL %s\n src: %s\n error: %s: %s\n" name src (Loc.to_string loc) msg +(* What a [def] came out as: the type, the [grerun] that makes the startup + store unguarded, and the lifted initialiser. The last is a property + [defonce] has only when its initialiser is computed and [def] has always — + zero and literal included — because it is what a re-evaluation swaps: the + host's startup calls [global/] through its cell, so the lifted function + is the one place an edited initialiser can land. An [uninit] def is the + documented exception and is not asked this. *) +let def_reading name src gname ~ty = + match checked src with + | p -> + (match List.find_opt (fun (g : Tast.global) -> g.gname = gname) p.globals with + | Some g -> + let got = Types.to_string g.gty in + let lifted = + match g.Tast.ginit.Tast.e with + | Tast.Call (f, []) -> f = "global/" ^ gname + | _ -> false + in + if got <> ty then begin + incr failures; + Printf.printf "FAIL %s: the type is %s, wanted %s\n src: %s\n" + name got ty src + end; + if not g.Tast.grerun then begin + incr failures; + Printf.printf "FAIL %s: not marked for re-run\n src: %s\n" + name src + end; + if not lifted then begin + incr failures; + Printf.printf + "FAIL %s: the initialiser is not the lifted global/%s\n src: %s\n" + name gname src + end + | None -> + incr failures; + Printf.printf "FAIL %s: no global named %s\n src: %s\n" + name gname src) + | exception Loc.Error { Loc.dloc = loc; dmsg = msg; _ } -> + incr failures; + Printf.printf "FAIL %s\n src: %s\n error: %s: %s\n" + name src (Loc.to_string loc) msg + let () = (* ── Literal defaulting and inference ──────────────────────────── *) infers "int defaults to i32" "42" "i32"; @@ -941,7 +1016,7 @@ let () = infers "array of an array" "(array 2 [3 u8])" "[2 [3 u8]]"; (* (array-fill [r c] v): the same type at any rank, with the element type taken from the fill value. Unlike [array] above this one is a value and - not a zero, which is what lets it be a defvar's initialiser — see + not a zero, which is what lets it be a defonce's initialiser — see programs/array-fill.flan for what it puts in the elements. *) infers "array-fill, rank 1" "(array-fill [5] 7)" "[5 i32]"; infers "array-fill, rank 2" "(array-fill [2 3] 0.5)" "[2 [3 f64]]"; @@ -978,48 +1053,48 @@ let () = [programs/widening.flan] is the other half and asserts the bits; these assert which programs exist. *) accepts "same signedness widens" - "(defvar a i32) (defn g [x i64] ()) (defn f [] () (g a))"; + "(defonce a i32) (defn g [x i64] ()) (defn f [] () (g a))"; accepts "unsigned widens into a wider signed" - "(defvar a u32) (defn g [x i64] ()) (defn f [] () (g a))"; + "(defonce a u32) (defn g [x i64] ()) (defn f [] () (g a))"; accepts "u8 widens into i16" - "(defvar a u8) (defn g [x i16] ()) (defn f [] () (g a))"; + "(defonce a u8) (defn g [x i16] ()) (defn f [] () (g a))"; accepts "f32 widens into f64" - "(defvar a f32) (defn g [x f64] ()) (defn f [] () (g a))"; + "(defonce a f32) (defn g [x f64] ()) (defn f [] () (g a))"; (* Narrowing is the thing that did not change, and the message has to say narrowing rather than "these are different types" — it also names the direction that needs nothing, because that is the half a reader coming from the old rule will not expect. *) rejects_check "narrowing is still refused, and says so" - "(defvar a i64) (defn g [x i32] ()) (defn f [] () (g a))" + "(defonce a i64) (defn g [x i32] ()) (defn f [] () (g a))" ~needle:"i64 into i32 can lose"; rejects_check "and says the other direction is free" - "(defvar a i64) (defn g [x i32] ()) (defn f [] () (g a))" + "(defonce a i64) (defn g [x i32] ()) (defn f [] () (g a))" ~needle:"i32 widens into i64 by itself"; rejects_check "float narrowing is refused too" - "(defvar a f64) (defn g [x f32] ()) (defn f [] () (g a))" + "(defonce a f64) (defn g [x f32] ()) (defn f [] () (g a))" ~needle:"f64 into f32 can lose"; (* Equal width across signedness: each holds values the other cannot, so there is no direction at all and the message says that instead. *) rejects_check "signed does not reach the same-width unsigned" - "(defvar a i32) (defn g [x u32] ()) (defn f [] () (g a))" + "(defonce a i32) (defn g [x u32] ()) (defn f [] () (g a))" ~needle:"neither widens into the other"; rejects_check "and a signed value never reaches an unsigned, wider or not" - "(defvar a i32) (defn g [x u64] ()) (defn f [] () (g a))" + "(defonce a i32) (defn g [x u64] ()) (defn f [] () (g a))" ~needle:"neither widens into the other"; (* Int into float, exact only. This is where the rule is tighter than Odin's, which admits any integer into any float; i64 has values no f64 holds, so it is out, and the cast is written. *) accepts "i32 reaches f64 exactly" - "(defvar a i32) (defn g [x f64] ()) (defn f [] () (g a))"; + "(defonce a i32) (defn g [x f64] ()) (defn f [] () (g a))"; accepts "u32 reaches f64 exactly" - "(defvar a u32) (defn g [x f64] ()) (defn f [] () (g a))"; + "(defonce a u32) (defn g [x f64] ()) (defn f [] () (g a))"; accepts "i16 reaches f32 exactly" - "(defvar a i16) (defn g [x f32] ()) (defn f [] () (g a))"; + "(defonce a i16) (defn g [x f32] ()) (defn f [] () (g a))"; rejects_check "i64 does not reach f64 — above 2^53 it would round" - "(defvar a i64) (defn g [x f64] ()) (defn f [] () (g a))" + "(defonce a i64) (defn g [x f64] ()) (defn f [] () (g a))" ~needle:"(f64 x)"; rejects_check "i32 does not reach f32 — above 2^24 it would round" - "(defvar a i32) (defn g [x f32] ()) (defn f [] () (g a))" + "(defonce a i32) (defn g [x f32] ()) (defn f [] () (g a))" ~needle:"(f32 x)"; (* Containers are invariant: widening rewrites a value with a cast, and there is no value to rewrite in a slice that does not own its bytes. *) @@ -1030,13 +1105,13 @@ let () = (* The binary join. The wider operand decides, in either written order, and an equal-width cross-signed pair still has nothing to decide on. *) accepts "the wider operand decides, wider written first" - "(defvar a i64) (defvar b i32) (defn f [] i64 (+ a b))"; + "(defonce a i64) (defonce b i32) (defn f [] i64 (+ a b))"; accepts "and decides when it is written second" - "(defvar a i64) (defvar b i32) (defn f [] i64 (+ b a))"; + "(defonce a i64) (defonce b i32) (defn f [] i64 (+ b a))"; accepts "min and max join the same way" - "(defvar a i8) (defvar b i16) (defn f [] i16 (max a b))"; + "(defonce a i8) (defonce b i16) (defn f [] i16 (max a b))"; rejects_check "i32 and u32 have no join" - "(defvar a i32) (defvar b u32) (defn f [] i32 (+ a b))" + "(defonce a i32) (defonce b u32) (defn f [] i32 (+ a b))" ~needle:"neither widens into the other"; (* The literal rule is untouched, which is what keeps a u64 constant's arithmetic at u64 rather than defaulting the 1 to an i32. *) @@ -1054,15 +1129,15 @@ let () = width *from*. Both spellings: the literal written as the operand, and the literal buried in one. *) rejects_check "a literal that does not fit is still refused" - "(defvar m u8) (defn f [] u8 (+ m 300))" ~needle:"300 does not fit in u8"; + "(defonce m u8) (defn f [] u8 (+ m 300))" ~needle:"300 does not fit in u8"; rejects_check "and is refused inside an operand too" - "(defvar m u8) (defn f [] u8 (+ m (+ 300 1)))" + "(defonce m u8) (defn f [] u8 (+ m (+ 300 1)))" ~needle:"300 does not fit in u8"; rejects_check "a float literal still cannot stand where an int is wanted" - "(defvar n i32) (defn f [] i32 (+ n 1.5))" + "(defonce n i32) (defn f [] i32 (+ n 1.5))" ~needle:"found the float literal 1.5"; accepts "a literal that does fit still takes the operand's type" - "(defvar m u8) (defn f [] u8 (+ m 200))"; + "(defonce m u8) (defn f [] u8 (+ m 200))"; (* The join reconsiders a refused operand, and a reconsidered pass must leave nothing behind. [scoped] cannot see to that — it puts the scope back on @@ -1078,30 +1153,30 @@ let () = test, so a regression shows up as the message coming back rather than as a silent accept. *) accepts "an abandoned trial inside handler-bind does not leave its frames up" - "(defvar n i32) (defvar w i64) \ + "(defonce n i32) (defonce w i64) \ (defn f [] i32 (println (+ n (handler-bind [] w))) (return 0))"; accepts "nor does one inside a loop leave the loop up" - "(defvar n i32) (defvar w i64) \ + "(defonce n i32) (defonce w i64) \ (defn f [] i32 (println (+ n (loop [i 0] w))) (defer (println 1)) 0)"; rejects_check "and a break outside every loop still says so plainly" - "(defvar n i32) (defvar w i64) \ + "(defonce n i32) (defonce w i64) \ (defn f [] i32 (println (+ n (loop [i 0] w))) (break) 0)" ~needle:"break is only allowed inside a loop"; rejects_check "an abandoned trial leaves no binding behind" - "(defvar n i32) (defvar w i64) \ + "(defonce n i32) (defonce w i64) \ (defn f [] i32 (println (+ n (let [q w] q))) (println q) 0)" ~needle:"unknown name q"; accepts "and does not shadow the binding it was nested in" - "(defvar n i32) (defvar w i64) \ + "(defonce n i32) (defonce w i64) \ (defn f [] i32 (let [t n] (println (+ n (let [t w] t))) (println t)) 0)"; (* Shifts are the carve-out: the value's type decides and the count widens to it, never the reverse, because the result's width and the poison check both belong to the value. *) accepts "a narrower count widens to the value" - "(defvar v i64) (defvar n u8) (defn f [] i64 (<< v n))"; + "(defonce v i64) (defonce n u8) (defn f [] i64 (<< v n))"; rejects_check "a wider count does not drag the value up with it" - "(defvar v u8) (defvar n i32) (defn f [] u8 (<< v n))" + "(defonce v u8) (defonce n i32) (defn f [] u8 (<< v n))" ~needle:"expected u8"; (* ── Bidirectional flow ────────────────────────────────────────── *) @@ -1237,48 +1312,48 @@ let () = representation this milestone does not give a view — and so does any container whose element is outside the three the view can hold. *) accepts "a typed Vec boxed into dyn is a view, not a refusal" - "(defvar v (Vec i64) (vec-new i64))\n\ + "(defonce v (Vec i64) (vec-new i64))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take v))"; accepts "a slice boxed into dyn is a view" - "(defvar xs [3 i64])\n\ + "(defonce xs [3 i64])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (slice xs 0 3)))"; accepts "a fixed array boxed into dyn is a view" - "(defvar a [4 i64])\n\ + "(defonce a [4 i64])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take a))"; accepts "a bool Vec's view" - "(defvar v (Vec bool) (vec-new bool))\n\ + "(defonce v (Vec bool) (vec-new bool))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take v))"; accepts "an f64 Vec's view" - "(defvar v (Vec f64) (vec-new f64))\n\ + "(defonce v (Vec f64) (vec-new f64))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take v))"; (* The element restriction is still refused, and by name: a string element would need a dyn string's own boxing, whose payload is a pointer into the collector's heap, planted where nothing will ever trace it. *) rejects_check "a Vec of strings does not view into dyn yet" - "(defvar v (Vec string) (vec-new string))\n\ + "(defonce v (Vec string) (vec-new string))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take v))" ~needle:"does not cross into dyn yet"; rejects_check "an i32 element is not one of the view's three" - "(defvar v (Vec i32) (vec-new i32))\n\ + "(defonce v (Vec i32) (vec-new i32))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take v))" ~needle:"does not cross into dyn yet"; (* A typed (Map K V) is unrelated to item 3 and keeps its own refusal. *) rejects_check "a typed Map still refuses into dyn" - "(defvar m (Map i64 i64) (map-new i64 i64))\n\ + "(defonce m (Map i64 i64) (map-new i64 i64))\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take m))" ~needle:"does not cross into dyn yet"; (* Which of the two refusals wins when both apply. A LOCAL (Vec string) fails the lifetime guard and the element check both, and the element one has to be the one that speaks: the lifetime message names - (defvar g ...) as the spelling that works, and for a string element + (defonce g ...) as the spelling that works, and for a string element the global spelling is refused too, so the other order would hand back advice that fails when taken. *) rejects_check "a local Vec of strings gets the element refusal, not the \ @@ -1307,11 +1382,11 @@ let () = rebound to a local first loses the trace back to it and is refused — conservative rather than wrong, and the message says what does work. *) accepts "a slice cut from a global inline is still permanent" - "(defvar xs [3 i64])\n\ + "(defonce xs [3 i64])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (slice xs 0 3)))"; rejects_check "a slice rebound to a local loses the trace and is refused" - "(defvar xs [3 i64])\n\ + "(defonce xs [3 i64])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (let [s (slice xs 0 3)] (take s)))" ~needle:"does not cross into dyn as a view here"; @@ -1324,11 +1399,11 @@ let () = indexed and nothing else deciding. Before that guard the refusal row compiled and segfaulted with no diagnostic at all. *) accepts "an element of a global array is permanent" - "(defvar rows [2 (Vec i64)])\n\ + "(defonce rows [2 (Vec i64)])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (at rows 0)))"; rejects_check "an element of a global slice is not permanent" - "(defvar sv [(Vec i64)])\n\ + "(defonce sv [(Vec i64)])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (at sv 0)))" ~needle:"does not cross into dyn as a view here"; @@ -1339,11 +1414,11 @@ let () = not — including the second, which the one-level guard accepted and which then printed a dead frame's contents with exit 0. *) accepts "an element of a global array of arrays is permanent" - "(defvar rows [2 [3 (Vec i64)]])\n\ + "(defonce rows [2 [3 (Vec i64)]])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (at rows 0 1)))"; rejects_check "an element reached through a slice level is not permanent" - "(defvar g [2 [[3 i64]]])\n\ + "(defonce g [2 [[3 i64]]])\n\ (defn take [d dyn] i32 1)\n\ (defn main [] i32 (take (at g 0 1)))" ~needle:"does not cross into dyn as a view here"; @@ -1389,7 +1464,7 @@ let () = (defn main [] i32 (let [a (array 4 S)] (set (.x (at a 0)) 7) 0))"; (* A global, rooted before the startup function runs and never popped. *) accepts "a global struct with a dyn field" - "(defstruct S [x dyn])\n(defvar s S)\n(defn main [] i32 0)"; + "(defstruct S [x dyn])\n(defonce s S)\n(defn main [] i32 0)"; (* What the descriptor still cannot reach, each by name. A typed container owns storage of a length nothing static knows, so the dyn words of one are not a list of offsets — that is the M2 queue's item 3, and it is a @@ -1425,12 +1500,12 @@ let () = cap must not be handed. *) rejects_check "a negative array length" "(defstruct S [x dyn])\n\ - (defvar neg [-1 S])\n\ + (defonce neg [-1 S])\n\ (defn main [] i32 0)" ~needle:"the most this compiler will write out"; rejects_check "an array length that overflows the flattened count" "(defstruct S [x dyn])\n\ - (defvar big [4611686018427387904 S])\n\ + (defonce big [4611686018427387904 S])\n\ (defn main [] i32 0)" ~needle:"the most this compiler will write out"; (* A pointer is not storage. A vector of pointers to structs that hold dyn @@ -1658,7 +1733,7 @@ let () = "(defn h [o (Option i64)] i32 (match o (Some _) 1 None 0))\n\ (defn main [] i32 (h nil))"; accepts "nil becomes None at a global's declared type" - "(defvar ov (Option i64) nil)\n\ + "(defonce ov (Option i64) nil)\n\ (defn main [] i32 (match ov (Some _) 1 None 0))"; (* The other direction: None crossing into dyn is nil, and a program can compare the result the same way it compares any other nil. *) @@ -1818,14 +1893,14 @@ let () = rather than a trap later; everything else is the emitted bounds check's job. A [defconst] is a global in the typed IR, not a folded constant, so it deliberately stays a runtime trap. *) - let arr = "(defvar a [3 i32]) " in + let arr = "(defonce a [3 i32]) " in accepts "last valid index" (arr ^ "(defn f [] i32 (at a 2))"); rejects_check "index past the end" (arr ^ "(defn f [] i32 (at a 3))") ~needle:"out of bounds for length 3"; rejects_check "negative index" (arr ^ "(defn f [] i32 (at a -1))") ~needle:"is negative"; rejects_check "index past the end of an inner dimension" - "(defvar g [2 [4 i32]]) (defn f [] i32 (at g 1 4))" + "(defonce g [2 [4 i32]]) (defn f [] i32 (at g 1 4))" ~needle:"out of bounds for length 4"; accepts "a variable index is checked at runtime, not here" (arr ^ "(defn f [i i32] i32 (at a i))"); @@ -2076,7 +2151,7 @@ let () = "(defconst k 1) (defn f [] () (set k 2))" ~needle:"k 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 defvar if it has to change"; + Declare it with defonce if it has to change"; accepts "addr of a local gives a pointer" (cursor ^ "(defn g [c (Ptr Cursor)] i32 (.pos c)) \ (defn f [s [u8]] i32 (let [c (Cursor {.src s})] (g (addr c))))"); @@ -2189,7 +2264,7 @@ let () = "nothing named shape is in scope either. A field is reached through \ an accessor, (.Crcle shape), not with a dot") | None -> check "a lowercase dotted name is refused" false); - (* [(Pair i32)] in a defvar falls down the value fork now that the third + (* [(Pair i32)] in a defonce falls down the value fork now that the third element takes either reading, and the generics answer the type fork gave it has to be reachable from here too. *) (* A capitalised head with arguments is a *type* given type arguments, and @@ -2197,7 +2272,7 @@ let () = string with no room for parameters. The sentence says which half, since generic functions are here and pointing at them is the useful part. *) rejects_check "a capitalised call with arguments is a generic type" - "(defvar x (Pair i32)) (defn f [] i32 0)" + "(defonce x (Pair i32)) (defn f [] i32 0)" ~needle:"is a generic type, which is not there yet"; accepts "and the generic function it points at is" "(defn pair-fst [a $t b $u] $t (do b a))\n\ @@ -2364,7 +2439,7 @@ let () = (defmethod area point [p] dyn (get p :x))\n\ (defn main [] i32 0)" ~needle:"dyn"; - (* A class and a function are one namespace, as a defn and a defvar are: + (* A class and a function are one namespace, as a defn and a defonce are: the constructor is a defn, so the collision is the ordinary one. *) rejects_check "a class collides with a function of the same name" "(defclass point [x y])\n(defn point [] i32 0)\n(defn main [] i32 0)" @@ -2446,14 +2521,14 @@ let () = as it always was, and for a reason the startup path does not touch: a constant is not an assignable place, so nothing could ever load it. *) accepts "a global Vec with a computed initialiser" - "(defvar g (Vec u8) (slurp \"game-data.edn\")) (defn f [] ())"; + "(defonce g (Vec u8) (slurp \"game-data.edn\")) (defn f [] ())"; rejects_check "a move-only global as a defconst" "(defconst g (Vec u8) (slurp \"game-data.edn\")) (defn f [] ())" - ~needle:"a defvar and not a defconst"; + ~needle:"a defonce and not a defconst"; (* uninit is the one initialiser a container still refuses, and it is a different rule: a garbage block pointer is not a garbage number. *) rejects_check "a global Vec declared uninit" - "(defvar g (Vec u8) uninit) (defn f [] ())" + "(defonce g (Vec u8) uninit) (defn f [] ())" ~needle:"steers every read of it"; (* ── What may be filled with raw bytes ───────────────────────────── @@ -2558,94 +2633,136 @@ let () = ~needle:"does not fit in u32"; (* A fill is never a value the linker can write into the image, so a defconst of one is refused by the constant rule rather than by anything - of this feature's own. A defvar is fine: its initialiser runs at + of this feature's own. A defonce is fine: its initialiser runs at startup, which programs/fill.flan pins. *) rejects_check "a defconst cannot be filled" "(defconst g [4 u8] (filled 0xFF)) (defn f [] ())" ~needle:"defconst"; - (* ── The third element of a defvar ───────────────────────────────── + (* ── The third element of a defonce ───────────────────────────────── 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 the expression at startup. The four rows below are the four spellings, each pinned with its meaning and not merely with the fact that it compiles. *) defvar_reading "a primitive third element stays a zeroed static" - "(defvar current-color i32) (defn f [] i32 current-color)" + "(defonce current-color i32) (defn f [] i32 current-color)" "current-color" ~ty:"i32" ~zeroed:true; defvar_reading "a bracketed type stays a zeroed static array" - "(defvar grid [2 [3 u32]]) (defn f [] u32 (at grid 0 0))" + "(defonce grid [2 [3 u32]]) (defn f [] u32 (at grid 0 0))" "grid" ~ty:"[2 [3 u32]]" ~zeroed:true; (* The edge the rule turns on: [Point] is a type, so the type reading wins and this is the zeroed struct it was before the rule existed. A value named [Point] cannot exist to compete with it — [collect] refuses one name declared twice, across every declaration kind there is. *) defvar_reading "a struct's name stays a zeroed static struct" - "(defstruct Point [x i32 y i32]) (defvar p Point) (defn f [] i32 (.x p))" + "(defstruct Point [x i32 y i32]) (defonce p Point) (defn f [] i32 (.x p))" "p" ~ty:"Point" ~zeroed:true; rejects_check "a type's name and a value's name cannot collide" - "(defstruct Point [x i32 y i32]) (defvar Point i32 1) (defn f [] ())" + "(defstruct Point [x i32 y i32]) (defonce Point i32 1) (defn f [] ())" ~needle:"defined twice"; (* A parenthesised type is still a type, so this is the zeroed Vec it was — which is also why a malformed one stays a type error rather than turning into a call to something named Vec. *) defvar_reading "a parenthesised type stays a zeroed static" - "(defvar v (Vec i32)) (defn f [] i32 (len v))" + "(defonce v (Vec i32)) (defn f [] i32 (len v))" "v" ~ty:"(Vec i32)" ~zeroed:true; rejects_check "a malformed parenthesised type stays a type error" - "(defvar v (Vec i32 i32)) (defn f [] ())" + "(defonce v (Vec i32 i32)) (defn f [] ())" ~needle:"(Vec T) takes exactly one type"; (* And the new spelling, which is the explicit dyn form with the keyword left out. *) defvar_reading "a literal third element is a dyn global holding it" - "(defvar score 0) (defn f [] () (set score (+ score 1)))" + "(defonce score 0) (defn f [] () (set score (+ score 1)))" "score" ~ty:"dyn" ~zeroed:false; defvar_reading "a call as the third element is a dyn global" - "(defn load [] dyn {:n 1}) (defvar game-data (load)) (defn f [] dyn game-data)" + "(defn load [] dyn {:n 1}) (defonce game-data (load)) (defn f [] dyn game-data)" "game-data" ~ty:"dyn" ~zeroed:false; (* A bare symbol naming a value, which is the shape only a name can settle: [seed] is not a type, so this is a dyn global initialised from it. *) defvar_reading "a value's name as the third element is a dyn global" - "(defvar seed i64 3) (defvar score seed) (defn f [] dyn score)" + "(defonce seed i64 3) (defonce score seed) (defn f [] dyn score)" "score" ~ty:"dyn" ~zeroed:false; (* The explicit spellings are untouched by all of it. *) defvar_reading "the explicit dyn form with a value is unchanged" - "(defvar score dyn 0) (defn f [] dyn score)" + "(defonce score dyn 0) (defn f [] dyn score)" "score" ~ty:"dyn" ~zeroed:false; defvar_reading "the explicit dyn form with no value is unchanged" - "(defvar config dyn) (defn f [] dyn config)" + "(defonce config dyn) (defn f [] dyn config)" "config" ~ty:"dyn" ~zeroed:true; + (* [def] through the same third-element rule, one row per spelling. Each + asserts what makes it a def: [grerun], and the initialiser lifted into + [global/] whatever it is — the zero and the literal included, which + is what lets a re-evaluated form swap it through the cell. A defonce + keeps those constants inline; the [defvar_reading] rows above pin that + side (a zeroed one's ginit *is* the zero). *) + def_reading "a def with a type is the zeroed static, repainted" + "(def grid [2 u32]) (defn f [] i32 (i32 (at grid 0)))" + "grid" ~ty:"[2 u32]"; + def_reading "a typed def with a constant initialiser still lifts it" + "(def speed i64 3) (defn f [] i64 speed)" + "speed" ~ty:"i64"; + def_reading "a computed typed def" + "(defn start [] i64 40) (def counter i64 (start)) (defn f [] i64 counter)" + "counter" ~ty:"i64"; + def_reading "a literal third element of a def is a dyn global" + "(def score 0) (defn f [] () (set score (+ score 1)))" + "score" ~ty:"dyn"; + (* The defonce beside it stays unmarked: the pair is the whole feature. *) + (match checked "(defonce keep i64 3) (def fresh i64 4) (defn f [] i64 keep)" with + | p -> + let g n = List.find (fun (g : Tast.global) -> g.Tast.gname = n) p.globals in + check "a defonce is not marked for re-run" (not (g "keep").Tast.grerun); + check "and the def beside it is" (g "fresh").Tast.grerun + | exception Loc.Error _ -> + check "a defonce and a def can stand together" false); + (* Lisp-1, same as every other pair of declarations: [collect]'s claimed + table spans def too. *) + rejects_check "a def and a defn cannot share a name" + "(def step 0) (defn step [] i64 1)" ~needle:"step is defined twice"; + rejects_check "a def and a defonce cannot share a name" + "(defonce total i64) (def total 0) (defn f [] ())" + ~needle:"total is defined twice"; + (* The teaching paragraph speaks the form's own name when the form is a + def. *) + rejects_check "a symbol that is neither, under def, says def" + "(def total foo) (defn f [] ())" + ~needle: + "foo is neither a type nor a value, and the third element of a def \ + has to be one or the other: a type there declares a zeroed global of \ + that type — (def total i64) — and a value there declares a dyn \ + global holding it — (def total 0)"; (* The symbol that is neither, which is the one position the rule made ambiguous: before it there was a single reading and "unknown type" was the whole story, and a message that still said only that would send a reader looking for the wrong mistake. Both readings, both spellings, and the near miss over the value names too. *) rejects_check "a symbol that is neither a type nor a value" - "(defvar total foo) (defn f [] ())" + "(defonce total foo) (defn f [] ())" ~needle: - "foo is neither a type nor a value, and the third element of a defvar \ + "foo is neither a type nor a value, and the third element of a defonce \ has to be one or the other: a type there declares a zeroed global of \ - that type — (defvar total i64) — and a value there declares a dyn \ - global holding it — (defvar total 0). Nothing named foo is declared \ + that type — (defonce total i64) — and a value there declares a dyn \ + global holding it — (defonce total 0). Nothing named foo is declared \ as either"; rejects_check "the near miss is over the value names as well as the types" - "(defvar score i64 1) (defvar total scor) (defn f [] ())" + "(defonce score i64 1) (defonce total scor) (defn f [] ())" ~needle:"Nothing named scor is declared as either — did you mean score?"; (* Three things that know which of the two readings was meant, and get in ahead of the paragraph rather than being buried under it. A paragraph about a fork the reader is not standing at is worse than a line. *) rejects_check "a plain type typo keeps the short answer" - "(defvar total i33) (defn f [] ())" + "(defonce total i33) (defn f [] ())" ~needle:"unknown type i33 — did you mean i32?"; (* [int] used to be this row. It resolves now — it is a builtin alias for [i32] — so the spelling that still teaches has to be one of the ones the exception did not cover. *) rejects_check "and another language's spelling is answered by name" - "(defvar total long) (defn f [] ())" + "(defonce total long) (defn f [] ())" ~needle:"unknown type long — Flan spells it i64"; rejects_check "a data case is not a type, and says what is" - "(defdata Shape [(Circle [r f64])]) (defvar g Circle) (defn f [] ())" + "(defdata Shape [(Circle [r f64])]) (defonce g Circle) (defn f [] ())" ~needle:"Circle is a case of the data type Shape, and a case is not a \ - type of its own — the global's type is the data type: (defvar g \ + type of its own — the global's type is the data type: (defonce g \ Shape). Assign the case you want, as (set g (Shape.Circle \ {.field value ...}))"; (* A bracket form never reaches that fork — the parser gives it the type @@ -2654,25 +2771,25 @@ let () = readings at the element that decided it, and the dyn spelling is the one that works. *) rejects_check "a bracket type whose element names a value says both readings" - "(defvar a i64 1) (defvar b i64 2) (defvar g [a b]) (defn f [] ())" + "(defonce a i64 1) (defonce b i64 2) (defonce g [a b]) (defn f [] ())" ~needle:"b names a value, not a type, and the brackets around it were \ read as a type"; rejects_check "and names the dyn spelling that does work" - "(defvar a i64 1) (defvar b i64 2) (defvar g [a b]) (defn f [] ())" - ~needle:"put dyn in front of the same brackets — (defvar g dyn ...)"; + "(defonce a i64 1) (defonce b i64 2) (defonce g [a b]) (defn f [] ())" + ~needle:"put dyn in front of the same brackets — (defonce g dyn ...)"; accepts "which is a real form" - "(defvar a i64 1) (defvar b i64 2) (defvar g dyn [a b]) (defn f [] ())"; + "(defonce a i64 1) (defonce b i64 2) (defonce g dyn [a b]) (defn f [] ())"; (* defconst's two-element form has no type slot, so a type written in one was read as a name in an array literal and reported as unknown. It is unambiguous evidence: a type and a value cannot share a name here. *) - rejects_check "a type in a two-element defconst names defvar" + rejects_check "a type in a two-element defconst names defonce" "(defconst rows 4) (defconst cols 4) (defconst grid [rows [cols u8]]) \ (defn f [] ())" ~needle:"u8 is a type, and this is a value: a two-element defconst has no \ type slot"; - accepts "and the defvar it names is the form that works" - "(defconst rows 4) (defconst cols 4) (defvar grid [rows [cols u8]]) \ + accepts "and the defonce it names is the form that works" + "(defconst rows 4) (defconst cols 4) (defonce grid [rows [cols u8]]) \ (defn f [] ())"; accepts "an ordinary array constant is untouched" "(defconst xs [1 2 3])"; @@ -2687,15 +2804,15 @@ let () = (* ── (array-fill ...) and (array-gen ...) as initialisers ────────── DISCUSS.org's "need a value-producing array constructor" wanted - [(defvar grid (array-fill [rows cols] 255))] — the grid filled as part of + [(defonce grid (array-fill [rows cols] 255))] — the grid filled as part of its declaration rather than in a mutation step after it. What falls out of the rules already settled, and it is not a carve-out either way: The four-element spelling is the one that works. It is a typed global with - a computed initialiser, which is the startup-lifted path a defvar already + a computed initialiser, which is the startup-lifted path a defonce already had, and the value it stores is an ordinary fixed array. - The three-element spelling does not mean this, and could not. A defvar + The three-element spelling does not mean this, and could not. A defonce whose third element is not a type is a *dyn* global by the 2026-09-20 rule, and a typed fixed array crosses into dyn only as a view of storage that outlives the view. A freshly built array is a temporary, so the view @@ -2705,14 +2822,14 @@ let () = neither was written for this form. *) defvar_reading "a typed array-fill global is computed, not zeroed" "(defconst rows 2) (defconst cols 3)\n\ - (defvar grid [rows [cols u8]] (array-fill [rows cols] 255))\n\ + (defonce grid [rows [cols u8]] (array-fill [rows cols] 255))\n\ (defn f [] u8 (at grid 0 0))" "grid" ~ty:"[2 [3 u8]]" ~zeroed:false; - rejects_check "a three-element array-fill defvar is the dyn reading" - "(defvar xs (array-fill [3] (i64 1))) (defn f [] ())" + rejects_check "a three-element array-fill defonce is the dyn reading" + "(defonce xs (array-fill [3] (i64 1))) (defn f [] ())" ~needle:"does not cross into dyn as a view here"; rejects_check "and its element type is asked about first" - "(defvar grid (array-fill [2 3] 255)) (defn f [] ())" + "(defonce grid (array-fill [2 3] 255)) (defn f [] ())" ~needle:"does not cross into dyn yet"; (* A defconst is not a second path to it: its value is what the linker writes into the image, and a fill is a loop. *) @@ -2723,10 +2840,10 @@ let () = (* The element type the annotation asks for is the one the fill value is checked against, so the disagreement is reported at the value. *) rejects_check "the annotation and the fill value must agree" - "(defvar g [2 [3 u8]] (array-fill [2 3] (f32 1.0))) (defn f [] ())" + "(defonce g [2 [3 u8]] (array-fill [2 3] (f32 1.0))) (defn f [] ())" ~needle:"expected u8, found f32"; rejects_check "the annotation's shape has to be the fill's shape" - "(defvar g [2 u8] (array-fill [3] (u8 1))) (defn f [] ())" + "(defonce g [2 u8] (array-fill [3] (u8 1))) (defn f [] ())" ~needle:"expected [2 u8], found [3 u8]"; (* A dimension is the same compile-time length [n T] takes, and a local is not one. The refusal is [array_len]'s own, which is what "the same rule" @@ -2747,7 +2864,7 @@ let () = rank, and its arguments are indices. *) accepts "array-gen takes a named function" "(defn cell [r i32 c i32] i32 (+ (* r 100) c))\n\ - (defvar grid [2 [3 i32]] (array-gen [2 3] cell))\n\ + (defonce grid [2 [3 i32]] (array-gen [2 3] cell))\n\ (defn f [] i32 (at grid 1 2))"; rejects_check "array-gen's second element is a function" "(defn f [] i32 (let [a (array-gen [3] 7)] 0))" @@ -2778,14 +2895,14 @@ let () = "(array-gen [2 3] (fn [i j] (+ (* i 100) j)))" "[2 [3 i32]]"; infers "an inline generator's element type is read off its body" "(array-gen [3] (fn [i] (i64 i)))" "[3 i64]"; - accepts "an annotated defvar takes an inline generator" - "(defvar grid [2 [3 u8]] (array-gen [2 3] (fn [i j] (u8 (+ i j)))))\n\ + accepts "an annotated defonce takes an inline generator" + "(defonce grid [2 [3 u8]] (array-gen [2 3] (fn [i j] (u8 (+ i j)))))\n\ (defn f [] i32 (i32 (at grid 1 2)))"; (* The annotated element type is the want the body is checked against, so a disagreement is reported at the generator's answer, in the ordinary expected/found words — not as a whole-array mismatch a line up. *) rejects_check "an inline generator's body has to answer the element type" - "(defvar grid [2 [3 u8]] (array-gen [2 3] (fn [i j] 1.5)))\n\ + "(defonce grid [2 [3 u8]] (array-gen [2 3] (fn [i j] 1.5)))\n\ (defn f [] i32 0)" ~needle:"expected u8, found f64"; rejects_check "an inline generator takes one argument per dimension too" @@ -2797,43 +2914,43 @@ let () = The order they run in is the compiler's to choose, so a global written above the one it reads is fine... *) accepts "a global initialised from another, written above it" - "(defvar b i64 (+ a 10)) (defvar a i64 (+ 1 2)) (defn f [] i64 b)"; + "(defonce b i64 (+ a 10)) (defonce a i64 (+ 1 2)) (defn f [] i64 b)"; (* ...and a ring is refused with every name in it, because there is no answer: whichever one started first would read the other's zero. *) rejects_check "two globals that initialise each other" "(defn fa [] i64 b) (defn fb [] i64 a)\n\ - (defvar a i64 (fa)) (defvar b i64 (fb))\n(defn f [] i64 (+ a b))" + (defonce a i64 (fa)) (defonce b i64 (fb))\n(defn f [] i64 (+ a b))" ~needle:"initialise each other"; rejects_check "a global initialised from itself" - "(defn fa [] i64 a) (defvar a i64 (fa)) (defn f [] i64 a)" + "(defn fa [] i64 a) (defonce a i64 (fa)) (defn f [] i64 a)" ~needle:"initialised from itself"; (* The dependency is through the call, not only through what the initialiser names: [fa] reads [b] and nothing in [a]'s text mentions it. *) accepts "a global that reads another through a function it calls" - "(defn fa [] i64 (+ b 1)) (defvar a i64 (fa)) (defvar b i64 (+ 1 2))\n\ + "(defn fa [] i64 (+ b 1)) (defonce a i64 (fa)) (defonce b i64 (+ 1 2))\n\ (defn f [] i64 a)"; (* Nothing outside an initialiser can establish a handler or a restart, so an unanswered signal is a no-op and an unanswered invoke-restart fails at the invoke site. Both are refused by name. *) rejects_check "a signal in a global initialiser" "(defstruct Oops [id i32])\n\ - (defvar w i64 (do (signal (Oops {.id 1})) 1))\n(defn f [] i64 w)" + (defonce w i64 (do (signal (Oops {.id 1})) 1))\n(defn f [] i64 w)" ~needle:"with no handler-bind or restart-case around it"; rejects_check "an invoke-restart in a global initialiser" - "(defvar w i64 (do (invoke-restart 'retry) 1))\n(defn f [] i64 w)" + "(defonce w i64 (do (invoke-restart 'retry) 1))\n(defn f [] i64 w)" ~needle:"an initialiser runs at startup"; (* And what is *inside* one runs like any other code: the frames a restart-case pushes it also pops, before the initialiser returns. This is [slurp]'s shape, which is why a global loaded from a file works at all. *) accepts "a restart-case inside a global initialiser" "(defstruct Oops [id i32])\n\ - (defvar w i64 (restart-case (do (signal (Oops {.id 1})) 7) (use-zero [] 0)))\n\ + (defonce w i64 (restart-case (do (signal (Oops {.id 1})) 7) (use-zero [] 0)))\n\ (defn f [] i64 w)"; (* The borrows, which are what is left once ownership is off the table: a global Vec is read, mutated in place, viewed and copied, and the copy is the one thing something else may own. *) accepts "a global Vec is borrowed, mutated and cloned" - "(defvar g (Vec u8)) \ + "(defonce g (Vec u8)) \ (defn f [] () (set g (vec-new u8)) (push g 1) (set (at g 0) 2) \ (println (len (as-slice g))) (let [c (clone g)] (free c)))"; rejects_check "try is milestone 6" "(defn f [] i32 (try 1))" @@ -3022,11 +3139,11 @@ let () = used to pass the checker — the tables are per-kind — and be caught by LLVM as a redefinition of an emitted symbol, or not caught at all. *) rejects_check "a global defined twice" - "(defvar x i32 1) (defvar x i32 2)" ~needle:"defined twice"; + "(defonce x i32 1) (defonce x i32 2)" ~needle:"defined twice"; rejects_check "a constant shadowing a variable" - "(defconst c 1) (defvar c i32 2)" ~needle:"defined twice"; + "(defconst c 1) (defonce c i32 2)" ~needle:"defined twice"; rejects_check "a function and a global" - "(defn item [] i32 1) (defvar item i32 2)" ~needle:"defined twice"; + "(defn item [] i32 1) (defonce item i32 2)" ~needle:"defined twice"; rejects_check "a struct and an alias" "(defstruct P [x i32]) (defalias P i32)" ~needle:"defined twice"; rejects_check "an enum and a struct" @@ -3057,10 +3174,10 @@ let () = "(defn f [] i64 (let [v (vec-new int) m (map-new int float)] 0))"; accepts "int as a struct field, and float beside it" "(defstruct P [x int y float]) (defn f [] int (let [p (P {.x 1 .y 2.0})] (.x p)))"; - (* The three-element defvar, whose third element is read as a type: a + (* The three-element defonce, whose third element is read as a type: a zeroed static and not a dyn global holding a value called [int]. *) - accepts "a defvar whose type is int" "(defvar g int) (defn f [] int g)"; - accepts "and one whose type is float" "(defvar g float) (defn f [] float g)"; + accepts "a defonce whose type is int" "(defonce g int) (defn f [] int g)"; + accepts "and one whose type is float" "(defonce g float) (defn f [] float g)"; accepts "a user alias over int" "(defalias Row (Vec int)) (defn f [] i64 0)"; (* Identity, stated where identity is the only thing that could make it pass: the two spellings meet as one type with no conversion between @@ -3085,9 +3202,9 @@ let () = and accepted at i64. Kept pointing at identity by pinning both directions: the one that widens, and the one that still cannot. *) accepts "int mixes with i64 exactly as i32 does" - "(defvar a int) (defvar b i64) (defn f [] i64 (+ a b))"; + "(defonce a int) (defonce b i64) (defn f [] i64 (+ a b))"; rejects_check "and refuses the narrowing exactly as i32 does" - "(defvar a int) (defvar b i64) (defn f [] int (+ a b))" + "(defonce a int) (defonce b i64) (defn f [] int (+ a b))" ~needle:"expected i32, found i64"; (* A program that declared the alias itself — which this one's author did, before it was builtin. True as written, it is the no-op it says it is; @@ -3124,9 +3241,9 @@ let () = accepts "a shift by the widest count in range" "(defn f [] i32 (<< 1 31))"; (* An index converts from a narrower integer and never from a wider one. *) - accepts "a u32 index" "(defvar a [4 u32]) (defn f [] u32 (let [i 2] (at a (u32 i))))"; + accepts "a u32 index" "(defonce a [4 u32]) (defn f [] u32 (let [i 2] (at a (u32 i))))"; rejects_check "an i64 index" - "(defvar a [4 u32]) (defn f [] u32 (let [i 2] (at a (i64 i))))" + "(defonce a [4 u32]) (defn f [] u32 (let [i 2] (at a (i64 i))))" ~needle:"is wider"; (* An aggregate cannot cross to C — the shim's job, in C, per target. *) @@ -3166,22 +3283,22 @@ let () = rejects_check "array literal is not a struct" "(defstruct C [pos i32]) (defn f [] C [1 2])" ~needle:"expected C"; rejects_check "wrong element count" - "(defvar xs [2 i32] [1 2 3])" ~needle:"expected 2 elements"; + "(defonce xs [2 i32] [1 2 3])" ~needle:"expected 2 elements"; (* Top-level names are order-independent (plan.org, Modules) — including constants used as array lengths and constants defined in terms of each other. *) accepts "a constant declared after its use as a length" - "(defvar grid [rows i32]) (defconst rows 8)"; + "(defonce grid [rows i32]) (defconst rows 8)"; accepts "constants defined out of order" "(defconst a (+ b 1)) (defconst b 1)"; - (* A typed [defvar] here and not an untyped [defconst], which it was until - 2026-09-20: a computed initialiser belongs to a defvar now, and only the + (* A typed [defonce] here and not an untyped [defconst], which it was until + 2026-09-20: a computed initialiser belongs to a defonce now, and only the defconst form takes no type. The order-independence being pinned is the same one either way — [g] is resolved from a declaration further down the file. *) accepts "a global initialised from a later function" - "(defvar k u8 (g)) (defn g [] u8 1)"; + "(defonce k u8 (g)) (defn g [] u8 1)"; rejects_check "a genuinely unknown constant still reports itself" "(defconst a (+ nope 1))" ~needle:"unknown name nope"; @@ -3200,10 +3317,10 @@ let () = ~needle:"a constant's value must be a compile-time constant"; rejects_check "a computed defconst names the way through" "(defn seed [] i64 7) (defconst c i64 (seed))" - ~needle:"Write (defvar c ...)"; + ~needle:"Write (defonce c ...)"; (* The search for a case goes down through the aggregates, because a case inside a struct literal is the same value the image cannot hold and the - general message's advice — make it a defvar, or write a literal — is not + general message's advice — make it a defonce, or write a literal — is not followable for one. [Emit.const] recursed for the same reason. *) rejects_check "a data type case nested in a constant struct" "(defdata U [A (B [x i32])]) (defstruct S [u U]) \ @@ -3229,7 +3346,7 @@ let () = (* ── Conditions, spec-conditions.md §1 and §2 ──────────────────── *) accepts "handler-bind over a struct condition" - "(defstruct C [id i32]) (defvar n i64)\n\ + "(defstruct C [id i32]) (defonce n i64)\n\ (defn f [] () (handler-bind [(C [c] (set n 1))] (signal (C {.id 2}))))"; (* Matching is by type and there is no hierarchy, so a condition has to be a struct — an integer would have nothing to match against. *) @@ -3730,17 +3847,17 @@ let () = ~needle:"a union is not a map key"; (* A constant is what the linker writes into the image and a union member is a store, so a defconst is refused rather than coming back from the emitter - as "this one is computed". A defvar is not refused any more: its computed + as "this one is computed". A defonce is not refused any more: its computed initialiser is lifted into a function that runs at startup, and the member is written by the same store that writes one in a body. *) accepts "a global initialised with a union member" - "(defunion U [i i32])\n(defvar g U (U {.i 1}))\n(defn f [] i32 0)"; + "(defunion U [i i32])\n(defonce g U (U {.i 1}))\n(defn f [] i32 0)"; rejects_check "a constant initialised with a union member" "(defunion U [i i32])\n(defconst c U (U {.i 1}))\n(defn f [] i32 0)" ~needle:"cannot be written into a constant"; (* The all-bytes-zero value is a constant and goes through, which is what makes (U {}) and a declaration with no value the same thing. *) - (match checked "(defunion U [i i32])\n(defvar g U (U {}))\n\ + (match checked "(defunion U [i i32])\n(defonce g U (U {}))\n\ (defn f [] i32 (.i g))" with | _ -> check "a global zeroed through a literal is allowed" true | exception Loc.Error { Loc.dmsg = msg; _ } -> @@ -3749,7 +3866,7 @@ let () = (* uninit is refused on a data type because its tag steers a match into a block LLVM may treat as unreachable. An untagged union steers nothing, so the argument does not carry over and the answer is different. *) - (match checked "(defunion U [i i32 f f32])\n(defvar g U uninit)\n\ + (match checked "(defunion U [i i32 f f32])\n(defonce g U uninit)\n\ (defn f [] i32 (.i g))" with | _ -> check "uninit on a union is allowed" true | exception Loc.Error { Loc.dmsg = msg; _ } -> @@ -4495,13 +4612,13 @@ let () = (Check.program (Parse.program (read src))).Tast.fns in let with_handler = - "(defstruct Boom [n i32]) (defvar hit i32) \ + "(defstruct Boom [n i32]) (defonce hit i32) \ (defn u [f (Fn [i32] i32)] i32 (f 1)) \ (defn m [] i32 \ (handler-bind [(Boom [c] (set hit (.n c)))] (u (fn [x] x))) 0)" in let without_handler = - "(defstruct Boom [n i32]) (defvar hit i32) \ + "(defstruct Boom [n i32]) (defonce hit i32) \ (defn u [f (Fn [i32] i32)] i32 (f 1)) \ (defn m [] i32 (u (fn [x] x)) 0)" in @@ -5093,7 +5210,7 @@ let () = rejects_check "dyn does not instantiate an integer?-bounded variable" ~needle:"dyn does not answer integer?" "(defn bump [x $t] $t {:where (integer? $t)} (+ x 1))\n\ - (defvar d dyn 5)\n\ + (defonce d dyn 5)\n\ (defn main [] () (println (bump d)))"; (* A float literal inside an integer?-bounded body is refused at the definition, in the bound's own words: there is no instantiation at which @@ -5235,12 +5352,12 @@ let () = rejects_check "a type variable is not instantiated at dyn" ~needle:"is not instantiated at dyn" "(defn idf [x $t] $t x)\n\ - (defvar d dyn 5)\n\ + (defonce d dyn 5)\n\ (defn main [] () (println (idf d)))"; rejects_check "and the refusal names the dyn side rather than only saying no" ~needle:"defmethod" "(defn idf [x $t] $t x)\n\ - (defvar d dyn 5)\n\ + (defonce d dyn 5)\n\ (defn main [] () (println (idf d)))"; (* Nor at a type that merely *reaches* a dyn, which is the shape that used to walk furthest before failing: (Option dyn) is the case the collector @@ -5257,7 +5374,7 @@ let () = rejects_check "a bounded variable is still refused by its bound" ~needle:"numeric?" "(defn twice [x $t] $t {:where (numeric? $t)} (+ x x))\n\ - (defvar d dyn 5)\n\ + (defonce d dyn 5)\n\ (defn main [] () (println (twice d)))"; (* ── Mixed widths at one type variable join at the wider type ─────── @@ -5305,18 +5422,18 @@ let () = rejects_check "u64 and i64 meet at no type" ~needle:"the two meet at no type" "(defn eq2? [a $t b $t] bool {:where (equal? $t)} (= a b))\n\ - (defvar u u64 3)\n(defvar i i64 3)\n\ + (defonce u u64 3)\n(defonce i i64 3)\n\ (defn main [] () (println (eq2? u i)))"; (* And a later, wider argument settles a pair that had no join of its own: u32 and i32 meet nowhere, but all three meet at the i64 that arrives third — in either order, which is what the deferred re-ask is for. *) accepts "a later argument settles a joinless pair" "(defn tri [a $t b $t c $t] $t {:where (numeric? $t)} (+ a (+ b c)))\n\ - (defvar x3 u32 1)\n(defvar y3 i32 2)\n(defvar z3 i64 3)\n\ + (defonce x3 u32 1)\n(defonce y3 i32 2)\n(defonce z3 i64 3)\n\ (defn main [] () (println (tri x3 y3 z3)))"; accepts "and the same trio in the other order" "(defn tri [a $t b $t c $t] $t {:where (numeric? $t)} (+ a (+ b c)))\n\ - (defvar x3 u32 1)\n(defvar y3 i32 2)\n(defvar z3 i64 3)\n\ + (defonce x3 u32 1)\n(defonce y3 i32 2)\n(defonce z3 i64 3)\n\ (defn main [] () (println (tri z3 y3 x3)))"; (* A variable the signature also reaches through a container is bound exactly — a slice's elements cannot be rewritten to a wider width — so @@ -5552,8 +5669,8 @@ let () = way through, and the negatives between them are the point: a typed [vec-new] takes no block, and neither does an immediate. *) memory "the collected heap, and what does not touch it" - "(defvar wide i64 999999999999999)\n\ - (defvar small i32 7)\n\ + "(defonce wide i64 999999999999999)\n\ + (defonce small i32 7)\n\ (defn take [x] () (print x))\n\ (defn main [] ()\n\ \ (let [tv (vec-new i32)\n\ @@ -5593,7 +5710,7 @@ let () = which is why the classifier reads the capacity argument rather than the symbol alone. *) memory "an allocator the program named" - "(defvar gv (Vec i64) (vec-new i64))\n\ + "(defonce gv (Vec i64) (vec-new i64))\n\ (defn take [x] () (print x))\n\ (defn arith [a b] () (take (+ a b)))\n\ (defn main [] ()\n\ diff --git a/test/test_reload.ml b/test/test_reload.ml index 9aceb84..b2053bc 100644 --- a/test/test_reload.ml +++ b/test/test_reload.ml @@ -218,7 +218,7 @@ let () = So an --x86 host gets --x86 modules and the two never meet. All four modules, and the same expected string as the LLVM path above: - v3 introduces a defvar and a defn the host was never built with, and v4 + v3 introduces a defonce and a defn the host was never built with, and v4 redefines the one v3 introduced. Neither has a symbol anywhere, so both go through flan_dev.c's by-name registry into a slot the module defines and [flan_reload_install] fills -- [X86.Lslot], which is the GOT path @@ -257,7 +257,7 @@ let () = if xcode <> 0 || xtext <> want then fail "x86 reload\n got: %S (exit %d)\n wanted: %S" xtext xcode want; - (* [extra] is a defvar the host has no storage for, so its declared value + (* [extra] is a defonce the host has no storage for, so its declared value has to travel with it: [flan_dev_global] copies the module's image onto the allocation the first time the name is interned and ignores it every time after. [extra] is declared zero here, which calloc would also give, diff --git a/test/test_repl.ml b/test/test_repl.ml index de6bba7..e5c1510 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -142,10 +142,10 @@ let () = | None -> fail "%s was accepted" name in (* A declaration is refused by name. It used to come back as "unknown - name defvar", which is why the reason asserted here was empty; now + name defonce", which is why the reason asserted here was empty; now that an expression expands, a macro can produce one, and the head says what it is wherever it appears. *) - refuses "a declaration" "(defvar nope i64)" "top-level declaration"; + refuses "a declaration" "(defonce nope i64)" "top-level declaration"; refuses "a declaration inside an expression" "(do 1 (defn f [] i32 1))" "top-level declaration"; refuses "an unknown name" "no-such-name" "unknown name"; diff --git a/test/test_sanitize.ml b/test/test_sanitize.ml index 033ec54..b43cd72 100644 --- a/test/test_sanitize.ml +++ b/test/test_sanitize.ml @@ -409,7 +409,7 @@ let () = Emit still puts every define in the sanitize_address attribute \ group, without which -fsanitize=address covers the runtime's C \ and nothing else." - "(defvar arr [4 i32])\n\ + "(defonce arr [4 i32])\n\ (defn main [] i32\n\ \ (set (at arr 0) 1)\n\ \ (let [i 5] (print (at arr i)) (println \"\"))\n\ diff --git a/test/test_session.ml b/test/test_session.ml index 8274806..1539b99 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -61,7 +61,7 @@ let () = (* The storage exists and has a shape: reusing it reads at the wrong offsets, and replacing it discards the state the reload exists to preserve. *) refuses "a retyped global" - "(defvar spare i32)" + "(defonce spare i32)" "changes type"; (* Values of the type are already in the running program's memory. *) (* A defconst the *checker* consumed is in the shape of the program — an @@ -249,7 +249,7 @@ let () = is accepted and has nothing to send. Building a module for it would report success for a change that cannot have taken effect, and would cost the program a reload it did not need. *) - (match Session.eval t "(defvar counter i64)" with + (match Session.eval t "(defonce counter i64)" with | c -> if c.Session.installs then fail "an empty change claimed to install" | exception Loc.Error { Loc.dmsg = m; _ } -> fail "redeclaring a var unchanged: %s" m); @@ -274,7 +274,7 @@ let () = (* A new global carries its declared initial value, copied once when the storage is allocated and never again — calloc alone would make it zero. *) - let c = Session.eval t "(defvar started i64 42) (defn read-started [] i64 started)" in + let c = Session.eval t "(defonce started i64 42) (defn read-started [] i64 started)" in if not (has c.Session.ir "@\".init.") then fail "a new global's initialiser was dropped"; if not c.Session.installs then fail "adding a global had nothing to install"; @@ -286,14 +286,14 @@ let () = and the absence of a second image is what proves the initialiser did not travel as code. Re-running one is exactly what would wipe the state a reload exists to preserve. *) - let c = Session.eval t "(defvar computed i64 (+ 20 2)) (defn read-computed [] i64 computed)" in + let c = Session.eval t "(defonce computed i64 (+ 20 2)) (defn read-computed [] i64 computed)" in if not (has c.Session.ir "to i64), ptr null)") then fail "a new computed global did not start zeroed"; if not c.Session.installs then fail "adding a computed global had nothing to install"; (* Names the process was never built with go through the registry instead of binding to a symbol, and adding one is allowed where retyping one is not. *) - let c = Session.eval t "(defvar fresh i64) (defn use-fresh [] i64 (set fresh 3) fresh)" in + let c = Session.eval t "(defonce fresh i64) (defn use-fresh [] i64 (set fresh 3) fresh)" in if not (List.mem "fresh" c.Session.names && List.mem "use-fresh" c.Session.fns) then fail "adding a var and a function reported %s" (String.concat " " c.Session.names); if not (has c.Session.ir "call ptr @flan_dev_global") then @@ -600,7 +600,7 @@ let () = | exception Loc.Error { Loc.dmsg = m; _ } -> if not (has m "top-level declaration") then fail "a declaration as an expression said %S" m); - (match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(do 1 (defvar g i64))" with + (match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(do 1 (defonce g i64))" with | _ -> fail "a nested declaration was accepted as an expression" | exception Loc.Error { Loc.dmsg = m; _ } -> if not (has m "top-level declaration") then diff --git a/test/test_valgrind.ml b/test/test_valgrind.ml index c36556c..8631e71 100644 --- a/test/test_valgrind.ml +++ b/test/test_valgrind.ml @@ -386,7 +386,7 @@ let padded_key = out by the architecture guard — all three of which are silent by construction, which is why the effect is asserted and not the constant. *) let arena_reuse = - "(defvar frame Allocator)\n\ + "(defonce frame Allocator)\n\ (defn main [] i32\n\ \ (set frame (arena-new 65536))\n\ \ (with-allocator frame\n\ diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index f365d08..027ef15 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -789,7 +789,7 @@ ;; read, and raylib's own answer to a zero-length buffer is an image with a ;; null buffer — which is what a zeroed one already is. image-valid? reports ;; false for it either way, so a caller that checks sees the same thing. -(defvar no-image Image) +(defonce no-image Image) (defn load-image-from-memory [file-type string data [u8]] Image (if (= (len data) 0) @@ -1467,10 +1467,10 @@ ;; raylib's own convention is that a NULL pointer with a count of 0 means the ;; default ASCII set, and the wrapper keeps it — but Flan has no null pointer ;; literal, so the null comes from the one place the language does hand out -;; zeroed bytes: a `defvar` with no initialiser is BSS (plan.org, zero +;; zeroed bytes: a `defonce` with no initialiser is BSS (plan.org, zero ;; values), and a zeroed (Ptr i32) is exactly a null one. It is never written ;; to and never read through; raylib only ever compares it against NULL. -(defvar default-codepoints (Ptr i32)) +(defonce default-codepoints (Ptr i32)) (declare-c load-font-ex-raw [path string font-size i32 codepoints (Ptr i32) count i32] Font diff --git a/web/examples/arith.flan b/web/examples/arith.flan index 401e5d8..ea13484 100644 --- a/web/examples/arith.flan +++ b/web/examples/arith.flan @@ -1,7 +1,7 @@ ;; Three integer operations have no right answer. Each used to be a bare SIGFPE ;; or an undefined value; each signals ArithError now. The divisor goes through ;; a global so that constant folding cannot answer it before the backend does. -(defvar zero i32 0) +(defonce zero i32 0) (defn main [] () (println "before") diff --git a/web/examples/arrays.flan b/web/examples/arrays.flan index 1529497..e31eb14 100644 --- a/web/examples/arrays.flan +++ b/web/examples/arrays.flan @@ -5,7 +5,7 @@ (defconst palette [4 u32] [0xE6B800FF 0x3B6E8CFF 0xA83232FF 0xCC6B1FFF]) ;; No initialiser means all-bytes-zero, so this is BSS and costs nothing. -(defvar grid [rows [cols i32]]) +(defonce grid [rows [cols i32]]) (defn main [] () (set (at grid 1 2) 7) diff --git a/web/examples/cast.flan b/web/examples/cast.flan index 61901c5..127ad12 100644 --- a/web/examples/cast.flan +++ b/web/examples/cast.flan @@ -1,7 +1,7 @@ ;; A float-to-integer cast whose value does not fit. The condition it violated ;; is reported as the range the destination type can hold, which is the same ;; shape BoundsError uses for a slice: the violated condition, written out. -(defvar big f64 1e30) +(defonce big f64 1e30) (defn main [] () (println (i32 big))) diff --git a/web/examples/conds.flan b/web/examples/conds.flan index e50c255..29f01ba 100644 --- a/web/examples/conds.flan +++ b/web/examples/conds.flan @@ -1,6 +1,6 @@ (defstruct AssetMissing [id i32]) -(defvar seen i64) +(defonce seen i64) (defn load-all [] () (signal (AssetMissing {.id 1})) ; () — the caller carries on diff --git a/web/examples/globals.flan b/web/examples/globals.flan index ea7583b..73d7452 100644 --- a/web/examples/globals.flan +++ b/web/examples/globals.flan @@ -1,9 +1,9 @@ (defconst cell-size 5) ; a compile-time constant (defconst gravity f32 0.05) ; with its type named -(defvar current-color i32) ; zeroed storage +(defonce current-color i32) ; zeroed storage (defconst rows 3) (defconst cols 4) -(defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes +(defonce grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes (defn main [] () (print cell-size) (println "") diff --git a/web/examples/places.flan b/web/examples/places.flan index fbe4c17..69efa3d 100644 --- a/web/examples/places.flan +++ b/web/examples/places.flan @@ -1,14 +1,14 @@ (defstruct Enemy [hp i32 name string]) -(defvar spawned i32) +(defonce spawned i32) (defconst room-size 4) -(defvar room [room-size i32]) +(defonce room [room-size i32]) ;; `set` takes a fixed list of forms, not an extensible setf. (defn main [] () (let [e (Enemy {.hp 10 .name "slime"}) p (addr e)] - (set spawned (+ spawned 1)) ; a local or a defvar + (set spawned (+ spawned 1)) ; a local or a defonce (set (.hp e) 7) ; a struct field (set (.hp p) 8) ; through a (Ptr Enemy) — derefs one level (set (at room 2) 5) ; a fixed array or slice element diff --git a/web/examples/restart.flan b/web/examples/restart.flan index 24d746c..e987fe6 100644 --- a/web/examples/restart.flan +++ b/web/examples/restart.flan @@ -1,6 +1,6 @@ (defstruct AssetMissing [id i32]) -(defvar cleanups i64) +(defonce cleanups i64) (defn load [n i32] i32 (signal (AssetMissing {.id n})) diff --git a/web/index.html b/web/index.html index 45b8673..526f41b 100644 --- a/web/index.html +++ b/web/index.html @@ -392,15 +392,15 @@ heap is involved.

(defstruct Enemy [hp i32  name string])
 
-(defvar spawned i32)
+(defonce spawned i32)
 (defconst room-size 4)
-(defvar room [room-size i32])
+(defonce room [room-size i32])
 
 ;; `set` takes a fixed list of forms, not an extensible setf.
 (defn main [] ()
   (let [e (Enemy {.hp 10 .name "slime"})
         p (addr e)]
-    (set spawned (+ spawned 1))    ; a local or a defvar
+    (set spawned (+ spawned 1))    ; a local or a defonce
     (set (.hp e) 7)                ; a struct field
     (set (.hp p) 8)                ; through a (Ptr Enemy) — derefs one level
     (set (at room 2) 5)            ; a fixed array or slice element
@@ -458,7 +458,7 @@ the type), and a float-to-integer cast whose value does not fit. All three now s
 
;; Three integer operations have no right answer. Each used to be a bare SIGFPE
 ;; or an undefined value; each signals ArithError now. The divisor goes through
 ;; a global so that constant folding cannot answer it before the backend does.
-(defvar zero i32 0)
+(defonce zero i32 0)
 
 (defn main [] ()
   (println "before")
@@ -478,7 +478,7 @@ holds [-2147483648 2147483647]
;; A float-to-integer cast whose value does not fit. The condition it violated
 ;; is reported as the range the destination type can hold, which is the same
 ;; shape BoundsError uses for a slice: the violated condition, written out.
-(defvar big f64 1e30)
+(defonce big f64 1e30)
 
 (defn main [] ()
   (println (i32 big)))
@@ -647,10 +647,10 @@ functions need no forward declaration. Globals come in two kinds:

(defconst cell-size 5)                 ; a compile-time constant
 (defconst gravity f32 0.05)            ; with its type named
-(defvar current-color i32)             ; zeroed storage
+(defonce current-color i32)             ; zeroed storage
 (defconst rows 3)
 (defconst cols 4)
-(defvar grid [rows [cols u32]])        ; BSS, rows*cols*4 bytes
+(defonce grid [rows [cols u32]])        ; BSS, rows*cols*4 bytes
 
 (defn main [] ()
   (print cell-size) (println "")
@@ -835,7 +835,7 @@ It is a place: (set (at grid r c) v) and (addr (at grid r c))
 (defconst palette [4 u32] [0xE6B800FF 0x3B6E8CFF 0xA83232FF 0xCC6B1FFF])
 
 ;; No initialiser means all-bytes-zero, so this is BSS and costs nothing.
-(defvar grid [rows [cols i32]])
+(defonce grid [rows [cols i32]])
 
 (defn main [] ()
   (set (at grid 1 2) 7)
@@ -1223,7 +1223,7 @@ normally leaves the signaller to carry on — the accumulation case:

(defstruct AssetMissing [id i32])
 
-(defvar seen i64)
+(defonce seen i64)
 
 (defn load-all [] ()
   (signal (AssetMissing {.id 1}))     ; () — the caller carries on
@@ -1248,7 +1248,7 @@ first, before the clause body starts.

(defstruct AssetMissing [id i32])
 
-(defvar cleanups i64)
+(defonce cleanups i64)
 
 (defn load [n i32] i32
   (signal (AssetMissing {.id n}))
@@ -1685,7 +1685,7 @@ a silent mismatch against memory the process has already laid out:

-

A defvar's initial value is deliberately not on that list: +

A defonce's initial value is deliberately not on that list: its storage holds state the program moved past long ago. And a defconst the checker never consumed can be changed, so a colour table can be tuned live while an array length stays refused. A dev build emits those as mutable globals, so LLVM cannot @@ -1796,7 +1796,7 @@ raw address rather than at an expression. M-x flan-allocations and by type, and the same walk with the dead left out.

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 arrive in the same load, or the +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.

C-x C-e compiles the expression before point and runs it inside the @@ -2137,7 +2137,7 @@ disagree with the first.