def re-runs its initialiser, and defvar is renamed defonce
The trio the author decided on 2026-09-20 is now all built: def is CL's defparameter — its initialiser runs on every daemon re-run, unguarded, so an edited initialiser repaints the same storage on C-c C-c plus re-run — defonce (Clojure's name for CL's defvar, per the author) initialises once behind the .init~once. flag, and defconst stays the image. One parse arm reads both forms; the difference is Ast.reinit, carried to Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and Check.check_global lifts every def initialiser — zero and literal included — into global/<n>, so the host's startup reaches it through the function cell and a re-evaluated def swaps it (Session's def_inits; Emit.redefinition declares the cell for a non-sibling target). The old defvar spelling is refused with the rename and both compiling spellings, and every program, test, doc and editor list is swept — except sand.flan, the author's live WIP, whose seven defvar lines are flagged in FIX.org and keep its three dependent tests red on this branch.
This commit is contained in:
parent
9bb01b77df
commit
a4c6b996ff
73
FIX.org
73
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/<n>] — zero and literal included, where a defonce keeps constants
|
||||
inline in the image. The host's startup calls the initialiser through its
|
||||
function cell, so when the author edits the form and C-c C-c's it,
|
||||
[Session]'s [def_inits] hands [global/<n>] to the redefinition, the cell
|
||||
swaps, and the next re-run stores the *edited* value into the same storage
|
||||
— a native ~[4 u32]~ def repaints in place, so every reference sees the new
|
||||
values. A constant left inline would have baked the stale value into the
|
||||
host's startup body for ever. [Emit.redefinition] declares the cell for a
|
||||
non-sibling target (a lifted initialiser's [fparent] is its global); the
|
||||
x86 side already reached host cells through the GOT.
|
||||
|
||||
~uninit~ is the one exception on both forms: nothing to run, nothing to lift,
|
||||
nothing repaints.
|
||||
|
||||
Pinned in test/programs/dev-rerun.flan (a ~(def c 3)~ printing 4, 4, 4, 4
|
||||
beside a ~tally~ climbing 1..4; a ~(def hues [4 u32] ...)~ element printing 8
|
||||
every run; then ~(def c 9)~ evaluated and the next re-run printing 10 while
|
||||
the defonce beside it climbs on), in test_flan.ml (parse shapes for every
|
||||
spelling of both forms, [grerun] on the pair, the lifted-initialiser claim,
|
||||
def/defn and def/defonce collisions, the defvar teaching error verbatim), and
|
||||
in programs/global-init.flan's last four lines (all def spellings start
|
||||
identically on both backends and at both -O0 and -O2).
|
||||
|
||||
** Red on this branch, for the merger
|
||||
sand.flan spells ~defvar~ at lines 15, 16, 24, 25, 26, 115 and 116 and was
|
||||
not touched — same situation as the raylib keywords above. The three
|
||||
sand-dependent tests (test_flan's parse pin, test_session's create,
|
||||
test_acceptance's "a package's main is not visible") are red here and turn
|
||||
green when those seven lines say ~defonce~ (or ~def~, where the author wants
|
||||
the initialiser to follow the source — ~colors~ was the motivating one).
|
||||
|
||||
20
NEXT.md
20
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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <program.flan>` holds one `Session`, builds the program, launches it, and listens on `.flan-dev.sock` beside
|
||||
the source. What it adds over `flan reload` is that the session *persists* — a `defvar` added by one evaluation is part
|
||||
the source. What it adds over `flan reload` is that the session *persists* — a `defonce` added by one evaluation is part
|
||||
of what the next one is checked against — and that it **owns the build**, which is what makes its layout rules describe
|
||||
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/<n>`, constants and zeros included, where a `defonce` keeps constants inline: the host's
|
||||
startup calls the initialiser through its function cell, so a re-evaluated `def` swaps the cell (`Session`'s
|
||||
`def_inits`, plus `Emit.redefinition` declaring the cell for a target whose `fparent` is a global) and the next re-run
|
||||
stores the edited value. A constant left inline would have baked the stale number into the host's startup for ever.
|
||||
`uninit` opts out on both forms — nothing to run, nothing repaints.
|
||||
|
||||
One shared plan feeds both backends, so x86 and LLVM cannot disagree; `programs/global-init.flan`'s last four lines pin
|
||||
every `def` spelling's startup on both, at `-O0` and `-O2`, and `programs/dev-rerun.flan` pins the live loop — a
|
||||
`(def c 3)` printing 4 on every run beside a defonce that climbs, and the edited `(def c 9)` printing 10 after the next
|
||||
re-run.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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 "\\_<spark\\_>" 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 "\\_<ticks\\_>" 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 ")"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ─────────────────
|
||||
;;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
29
lib/ast.ml
29
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
|
||||
|
||||
174
lib/check.ml
174
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 <expr>)], which is the
|
||||
dyn reading is rewritten into exactly [(defonce x dyn <expr>)], which is the
|
||||
whole of "it lowers to the same thing": the startup lifting, the re-run
|
||||
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/<n>] whatever it is — a
|
||||
zero, a literal, a computed expression — where a [defonce]'s is lifted
|
||||
only when it is computed. The lifting is what makes the form's promise
|
||||
reachable: the host's startup function calls the initialiser through
|
||||
its function cell, so a re-evaluated [def] swaps the cell and the next
|
||||
re-run stores the *edited* value. A constant left inline would be
|
||||
baked into the host's startup body, and every re-run would paint the
|
||||
stale value back. [uninit] is the one exception on both forms: there
|
||||
is nothing to run, so there is nothing to lift. *)
|
||||
let lift_always = (match kind with Ast.Once -> false | Ast.Every -> true) in
|
||||
let ginit =
|
||||
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;
|
||||
|
||||
11
lib/dev.ml
11
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.
|
||||
|
||||
|
||||
93
lib/emit.ml
93
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/<n>] call — so every [def]
|
||||
except an [uninit] one is in here, which is what makes its store run
|
||||
on each re-run and its initialiser reachable through the function
|
||||
cell a re-evaluation swaps. *)
|
||||
let computed =
|
||||
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/<n>] when a [def]
|
||||
is re-evaluated — is not a sibling: its [fparent] is the global it
|
||||
initialises, not a function in [fns]. Its cell is the host's like any
|
||||
other (the host declares one per function, lifted ones included), so
|
||||
the publish store below needs the declaration the sibling loop above
|
||||
could not write. *)
|
||||
List.iter
|
||||
(fun (f : Tast.fn) ->
|
||||
if f.Tast.fparent <> None && known f.Tast.name
|
||||
&& not (transient f.Tast.name) then
|
||||
Buffer.add_string m.out
|
||||
(Printf.sprintf "%s = external global ptr\n" (cellname f.Tast.name)))
|
||||
targets;
|
||||
if new_fns <> [] || new_globals <> [] then
|
||||
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
|
||||
|
||||
11
lib/load.ml
11
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
53
lib/parse.ml
53
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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = "<eval>") ?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/<n>] — [Check.check_global]
|
||||
lifts every [def] initialiser, constants included, for exactly this —
|
||||
and the host's startup function calls it through its cell, so
|
||||
republishing that one function is the whole delivery. Only when the
|
||||
host already has the cell: a [def] the process has never seen gets its
|
||||
storage from [flan_dev_global] like any new global, and there is no
|
||||
startup call to swap. *)
|
||||
let def_inits =
|
||||
List.concat_map
|
||||
(fun (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Defvar (n, _, _, Ast.Every) ->
|
||||
let g = "global/" ^ n in
|
||||
if known t g
|
||||
&& List.exists
|
||||
(fun (f : Tast.fn) -> String.equal f.Tast.name g)
|
||||
program.Tast.fns
|
||||
then [ g ]
|
||||
else []
|
||||
| _ -> [])
|
||||
incoming
|
||||
in
|
||||
let from_generics =
|
||||
List.concat_map
|
||||
(fun n ->
|
||||
@ -797,7 +821,7 @@ let eval ?(origin = "<eval>") ?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 = "<set>") 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ──────────────────────────────────────
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
|
||||
(defstruct Blip [n i32])
|
||||
|
||||
(defvar log i64)
|
||||
(defonce log i64)
|
||||
|
||||
(defn deep [n i32] i32
|
||||
(signal (Blip {.n n}))
|
||||
|
||||
@ -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 ──
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
(defvar log i64)
|
||||
(defonce log i64)
|
||||
|
||||
(defn quiet [n i32] i32
|
||||
(defer (set log (+ log 1)))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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 [])
|
||||
|
||||
@ -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 ""))
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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 [] ()
|
||||
|
||||
@ -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))))
|
||||
|
||||
|
||||
@ -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 <expr>)] already compiled to — the initialiser lifted into
|
||||
;;;; [(defonce x dyn <expr>)] already compiled to — the initialiser lifted into
|
||||
;;;; the startup function that main calls after the runtime is up, and a
|
||||
;;;; 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
|
||||
@ -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))
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
(use-placeholder [] -1)
|
||||
(retry [] 7)))
|
||||
|
||||
(defvar ticks i64)
|
||||
(defonce ticks i64)
|
||||
|
||||
(defn step [] i64
|
||||
(set ticks (+ ticks 1))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
;;;; a real daemon as well.
|
||||
(import agent "vendor:agent")
|
||||
|
||||
(defvar frames i64)
|
||||
(defonce frames i64)
|
||||
|
||||
(defn step [] i64 7)
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.<name>] — a name a program can write, since [.]
|
||||
@ -61,7 +62,20 @@
|
||||
;; now; before that the dev build died at the assembler with the symbol
|
||||
;; 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]
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user