Review follow-ups: a new def's image, the keyword that cannot change, and the sweep
Three defects, all from lifting every def initialiser, none of which the suite caught: A def typed fresh into a live session came up zero and stayed zero. The image flan_dev_global copies on the allocation is the only value a new global ever gets — the host's .init-globals never calls its initialiser — and both backends chose that image with Tast.const_init, which a def's lifted Call fails by construction. Emit.initial_image reads the constant back out of the lifted body; the x86 twin had the same bug. Changing a global between def and defonce was silently ineffective: the guard lives in the startup function compiled into the host, which a reload cannot republish. Session.compatible refuses both directions and says to restart; editing the value stays allowed. And global/<n> no longer leaks into the signature refusal when a def is retyped — the global loop names the same fact in words a reader can act on. flan check prints def, defonce or defconst off grerun; (defvar) with no arguments names the shapes rather than offering (defonce ); the docs, plan.org, runtime comments and valgrind.supp are swept; BUILT.md states the release-build cost and the uninit caveat.
This commit is contained in:
parent
801c70bd0d
commit
a64bee6d96
33
FIX.org
33
FIX.org
@ -4409,6 +4409,39 @@ 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
|
in programs/global-init.flan's last four lines (all def spellings start
|
||||||
identically on both backends and at both -O0 and -O2).
|
identically on both backends and at both -O0 and -O2).
|
||||||
|
|
||||||
|
** Review follow-ups
|
||||||
|
Three real defects, all from the always-lift, all found by review rather than
|
||||||
|
by the suite:
|
||||||
|
|
||||||
|
- *A def typed fresh into a live session came up zero and stayed zero.* The
|
||||||
|
image [flan_dev_global] copies on the allocation is the only value a
|
||||||
|
brand-new global ever gets — the host's [.init-globals] was compiled when
|
||||||
|
the process started and never calls the new name's initialiser — and both
|
||||||
|
backends decided that image with [Tast.const_init g.ginit], which a def's
|
||||||
|
lifted [Call] fails by construction. [(def n i64 42)] therefore came up 0
|
||||||
|
where [(defonce n i64 42)] came up 42, permanently, for that process. Now
|
||||||
|
[Emit.initial_image] reads the constant back out of the lifted body and
|
||||||
|
both backends ask it; the x86 twin had the same bug and the same fix.
|
||||||
|
Pinned in test_session.ml beside the defonce row it is compared against.
|
||||||
|
- *Changing the keyword on an existing global was silently ineffective.*
|
||||||
|
Which form declared it is not in the storage, it is in the startup
|
||||||
|
function's guard, compiled into the host. A redefinition republishes the
|
||||||
|
initialiser and cannot republish that, so defonce→def kept the guard and
|
||||||
|
never re-ran, and def→defonce kept re-running. [Session.compatible]
|
||||||
|
refuses both ways now and says to restart. Editing the *value* is the
|
||||||
|
workflow and stays allowed, which is the row beside it.
|
||||||
|
- *[global/<n>] leaked into a user-facing refusal.* Retyping a def hit the
|
||||||
|
function-signature arm first, which answered about [global/paint] — a name
|
||||||
|
nothing in the source mentions. The lifted initialiser is skipped there
|
||||||
|
now; the global loop below says the same fact in the words a reader can
|
||||||
|
act on.
|
||||||
|
|
||||||
|
Also covered, having been reasoned rather than exercised: a def whose type
|
||||||
|
changes between re-runs (the "changes type" refusal), and a def initialiser
|
||||||
|
that reads another global at run time rather than only in the static
|
||||||
|
ordering analysis (an ordinary republish; the read is the running program's
|
||||||
|
storage).
|
||||||
|
|
||||||
** Red on this branch, for the merger
|
** Red on this branch, for the merger
|
||||||
sand.flan spells ~defvar~ at lines 15, 16, 24, 25, 26, 115 and 116 and was
|
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
|
not touched — same situation as the raylib keywords above. The three
|
||||||
|
|||||||
10
bin/main.ml
10
bin/main.ml
@ -351,7 +351,13 @@ let () =
|
|||||||
List.iter
|
List.iter
|
||||||
(fun (g : Flan.Tast.global) ->
|
(fun (g : Flan.Tast.global) ->
|
||||||
Printf.printf "%s %s %s\n"
|
Printf.printf "%s %s %s\n"
|
||||||
(if g.gconst then "defconst" else "defvar")
|
(* The three defining forms, told apart the way the compiler
|
||||||
|
tells them apart: [gconst] is the image, and [grerun] is
|
||||||
|
what a re-run does to the storage. A listing that called
|
||||||
|
both mutable forms one name could not answer the question
|
||||||
|
someone runs [flan check] on a dev file to ask. *)
|
||||||
|
(if g.gconst then "defconst"
|
||||||
|
else if g.grerun then "def" else "defonce")
|
||||||
g.gname (Flan.Types.to_string g.gty))
|
g.gname (Flan.Types.to_string g.gty))
|
||||||
p.globals;
|
p.globals;
|
||||||
List.iter
|
List.iter
|
||||||
@ -739,7 +745,7 @@ let () =
|
|||||||
p ~out))
|
p ~out))
|
||||||
(* The daemon an editor talks to: one session, the program it belongs to
|
(* The daemon an editor talks to: one session, the program it belongs to
|
||||||
running beside it, and a socket. Unlike [flan reload] the session persists,
|
running beside it, and a socket. Unlike [flan reload] the session persists,
|
||||||
so a defvar added by one evaluation is part of what the next one is checked
|
so a defonce added by one evaluation is part of what the next one is checked
|
||||||
against — and it owns the build, which is what makes its layout rules
|
against — and it owns the build, which is what makes its layout rules
|
||||||
describe the process that is actually running. *)
|
describe the process that is actually running. *)
|
||||||
| _ :: "dev" :: path :: rest ->
|
| _ :: "dev" :: path :: rest ->
|
||||||
|
|||||||
@ -6207,7 +6207,31 @@ place — same storage, no reallocation, every reference sees the new bytes. And
|
|||||||
startup calls the initialiser through its function cell, so a re-evaluated `def` swaps the cell (`Session`'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
|
`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.
|
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.
|
|
||||||
|
Two consequences of the always-lift that are worth stating, because both were bugs first. A global the *process was
|
||||||
|
never built with* — a `def` typed fresh into a live session — gets its initial value from the image
|
||||||
|
`flan_dev_global` copies on the allocation, and nothing else: the host's `.init-globals` was compiled when the process
|
||||||
|
started and never calls the new name's initialiser, at this run or any re-run. Asking `Tast.const_init` about a def's
|
||||||
|
`ginit` there answers no every time, because the lift made it a call, so a new `(def n i64 42)` came up zero and
|
||||||
|
stayed zero while the `defonce` beside it came up 42. `Emit.initial_image` reads the constant back out of the lifted
|
||||||
|
body instead, and both backends ask it. And *changing the keyword* on an existing global is refused: which form
|
||||||
|
declared it lives in the startup function's guard, which was compiled into the host, so a reload can replace the
|
||||||
|
initialiser but not how often it is called — swapping `def` for `defonce` would load cleanly and go on doing what the
|
||||||
|
old keyword said. `Session.compatible` names it and says to restart. Editing the *value* is the workflow and stays
|
||||||
|
allowed.
|
||||||
|
|
||||||
|
**`uninit` is the one spelling where `def` does not do what its name promises.** `(def buf [8 u8] uninit)` keeps its
|
||||||
|
bytes across a re-run, because there is nothing to run: the initialiser that would repaint it is the absence of one.
|
||||||
|
That is deliberate — repainting would mean storing garbage on purpose, and the whole point of `uninit` is that nobody
|
||||||
|
has said what the bytes are — but it is the one place where reading the keyword does not tell you the answer. Write
|
||||||
|
`(def buf [8 u8])` if the re-run should zero it.
|
||||||
|
|
||||||
|
**What a `def` costs a release build.** A `defonce` with a constant initialiser is written into the image and starts
|
||||||
|
for free; a `def` with the same initialiser is not, because its initialiser is lifted into `global/<n>` whether or not
|
||||||
|
a linker could have written it. So the storage leaves `.data` for `.bss`, and startup gains one call and one store for
|
||||||
|
it — once per program, not per frame, and no guard flag either way in a release build. The x86 backend tracks LLVM
|
||||||
|
`-O0`, so that call is definitely emitted there rather than being something an optimiser might fold. A `defonce` is
|
||||||
|
unchanged in a release build, byte for byte, which is what keeps the cost something you opt into by choosing the form.
|
||||||
|
|
||||||
One shared plan feeds both backends, so x86 and LLVM cannot disagree; `programs/global-init.flan`'s last four lines pin
|
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
|
every `def` spelling's startup on both, at `-O0` and `-O2`, and `programs/dev-rerun.flan` pins the live loop — a
|
||||||
|
|||||||
@ -57,7 +57,7 @@ runtime has only preformatted loc strings, no access to source text).
|
|||||||
| 12 | `unknown type i — did you mean i8? A parameter with no type is dyn, so this would otherwise be read as a second parameter called i` | check.ml:850 | `(defn idx [v i] dyn …)` | A | B | C | C | Locates and explains well, but the did-you-mean fires on a *lowercase* name the user plainly meant as a parameter, so the suggestion is a false accusation. Suppress `near_miss` when the name is lowercase and in a parameter vector; lead with the dyn-parameter reading instead. |
|
| 12 | `unknown type i — did you mean i8? A parameter with no type is dyn, so this would otherwise be read as a second parameter called i` | check.ml:850 | `(defn idx [v i] dyn …)` | A | B | C | C | Locates and explains well, but the did-you-mean fires on a *lowercase* name the user plainly meant as a parameter, so the suggestion is a false accusation. Suppress `near_miss` when the name is lowercase and in a parameter vector; lead with the dyn-parameter reading instead. |
|
||||||
| 13 | `expected a type, found 1. This is the return type, which every defn states -- a function that returns nothing writes ()` | parse.ml:1156 | `(defn f [x i32] (+ x 1))` | C | B | B | C | Says the fix, which is good. Two warts: the caret lands on the `1` deep inside the body rather than on the position where the return type belongs; and the literal `--` where the house uses `—` everywhere else. |
|
| 13 | `expected a type, found 1. This is the return type, which every defn states -- a function that returns nothing writes ()` | parse.ml:1156 | `(defn f [x i32] (+ x 1))` | C | B | B | C | Says the fix, which is good. Two warts: the caret lands on the `1` deep inside the body rather than on the position where the return type belongs; and the literal `--` where the house uses `—` everywhere else. |
|
||||||
| 14 | `f64 is not a struct, so it has no fields` | check.ml:4028 | `(match s (Circle c) (.r c))` — single-field case binds the payload directly | B | C | D | A | The user wrote what looks like a destructuring pattern and got a type fact. Say what the pattern bound (`c` is the payload, an `f64`) and that the field is already in hand. |
|
| 14 | `f64 is not a struct, so it has no fields` | check.ml:4028 | `(match s (Circle c) (.r c))` — single-field case binds the payload directly | B | C | D | A | The user wrote what looks like a destructuring pattern and got a type fact. Say what the pattern bound (`c` is the payload, an `f64`) and that the field is already in hand. |
|
||||||
| 15 | `% is a constant` | check.ml:4043 | `(defconst k 3)` + `(set k 4)` | A | C | D | A | Four words. Needs a `Loc.note` at the `defconst` and the named fix (`defvar`). `no_container_defconst` (7447) already shows how the house writes this well — imitate it. |
|
| 15 | `% is a constant` | check.ml:4043 | `(defconst k 3)` + `(set k 4)` | A | C | D | A | Four words. Needs a `Loc.note` at the `defconst` and the named fix (`defonce`). `no_container_defconst` (7447) already shows how the house writes this well — imitate it. |
|
||||||
| 16 | `% is a parameter, and parameters are not assignable places (spec-memory.md) — bind a local with let` | check.ml:4037 | `(defn f [x i32] i32 (set x 1) x)` | A | B | B | C | Names the fix. Register wart: a diagnostic should not cite a spec filename at the user; put the rule in words and drop `(spec-memory.md)`. Same for `(plan.org, Types)` at 4609/4614 and `(see plan.org)` at 423. |
|
| 16 | `% is a parameter, and parameters are not assignable places (spec-memory.md) — bind a local with let` | check.ml:4037 | `(defn f [x i32] i32 (set x 1) x)` | A | B | B | C | Names the fix. Register wart: a diagnostic should not cite a spec filename at the user; put the rule in words and drop `(spec-memory.md)`. Same for `(plan.org, Types)` at 4609/4614 and `(see plan.org)` at 423. |
|
||||||
| 17 | `% is not implemented yet — milestone %d (see plan.org)` | check.ml:423 (`unimplemented`), used widely | `(Result i32 string)` in a type position | B | B | D | D | Sends the user to a planning document. Say what is missing in one clause and what to write in the meantime, or nothing. |
|
| 17 | `% is not implemented yet — milestone %d (see plan.org)` | check.ml:423 (`unimplemented`), used widely | `(Result i32 string)` in a type position | B | B | D | D | Sends the user to a planning document. Say what is missing in one clause and what to write in the meantime, or nothing. |
|
||||||
| 18 | `% takes numbers, found string` | check.ml:4219 (`binary`) | `(+ "a" "b")` | C | B | C | A | Caret covers the whole form rather than the offending operand — the exact "whole form vs operand" regression class this repo already fixed once in `check_truthy`. The operand's `Tast.eloc` is right there. Also: no mention of `str-cat`/the concatenation route, which is what the user wanted. |
|
| 18 | `% takes numbers, found string` | check.ml:4219 (`binary`) | `(+ "a" "b")` | C | B | C | A | Caret covers the whole form rather than the offending operand — the exact "whole form vs operand" regression class this repo already fixed once in `check_truthy`. The operand's `Tast.eloc` is right there. Also: no mention of `str-cat`/the concatenation route, which is what the user wanted. |
|
||||||
@ -110,11 +110,11 @@ Elm-class.
|
|||||||
and `no_container_defconst` (7447). No secondary span, but the prose does the
|
and `no_container_defconst` (7447). No secondary span, but the prose does the
|
||||||
whole contract: what was understood ("the constant `n` is computed"), what it
|
whole contract: what was understood ("the constant `n` is computed"), what it
|
||||||
conflicts with ("a defconst is what the linker writes into the image and has
|
conflicts with ("a defconst is what the linker writes into the image and has
|
||||||
nowhere to run"), and two named ways out (`defvar`, or a folded literal). The
|
nowhere to run"), and two named ways out (`defonce`, or a folded literal). The
|
||||||
best *prose-only* message in the tree.
|
best *prose-only* message in the tree.
|
||||||
|
|
||||||
**4. The dyn view-lifetime refusal.** `view_not_permanent` (1570). Explains
|
**4. The dyn view-lifetime refusal.** `view_not_permanent` (1570). Explains
|
||||||
the rule, gives the one shape that does work (`defvar g …`), and enumerates
|
the rule, gives the one shape that does work (`defonce g …`), and enumerates
|
||||||
what is refused. Borderline long — it is the closest thing in the tree to the
|
what is refused. Borderline long — it is the closest thing in the tree to the
|
||||||
banned essay register, and a fix pass should cut it by a third rather than
|
banned essay register, and a fix pass should cut it by a third rather than
|
||||||
lengthen anything toward it.
|
lengthen anything toward it.
|
||||||
|
|||||||
@ -74,7 +74,7 @@ Three more the same way, none of them blockers, all one line each:
|
|||||||
| Missing | Used at | Verdict |
|
| Missing | Used at | Verdict |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ImageFromImage` | `sprite_atlas.clj` `auto-select-tiles`; `tilemap-blob.lisp` `tile-subimages` | Annoyance — see §3 |
|
| `ImageFromImage` | `sprite_atlas.clj` `auto-select-tiles`; `tilemap-blob.lisp` `tile-subimages` | Annoyance — see §3 |
|
||||||
| `IsWindowReady` | `engine.clj` `run-game!`, `engine.lisp` `run-game` — the "window already open" guard | Annoyance; a `defvar bool` does the same thing |
|
| `IsWindowReady` | `engine.clj` `run-game!`, `engine.lisp` `run-game` — the "window already open" guard | Annoyance; a `defonce bool` does the same thing |
|
||||||
| `SetTextureFilter` | declared in `rl.clj`, called nowhere | Not needed. Point is raylib's default |
|
| `SetTextureFilter` | declared in `rl.clj`, called nowhere | Not needed. Point is raylib's default |
|
||||||
| `UpdateTexture` | declared in `rl.clj`, called nowhere | Not needed |
|
| `UpdateTexture` | declared in `rl.clj`, called nowhere | Not needed |
|
||||||
| `SetClipboardText` | `sprite_atlas.clj`, in a `comment` block only | Not needed |
|
| `SetClipboardText` | `sprite_atlas.clj`, in a `comment` block only | Not needed |
|
||||||
@ -245,7 +245,7 @@ Ordered by how much of the game it touches.
|
|||||||
(`engine.clj`'s `texture-cache` atom, `engine.lisp`'s `*texture-cache*` hash table), and
|
(`engine.clj`'s `texture-cache` atom, `engine.lisp`'s `*texture-cache*` hash table), and
|
||||||
`engine.lisp` keeps `*buffers*` there too.
|
`engine.lisp` keeps `*buffers*` there too.
|
||||||
|
|
||||||
`sand.flan`'s idiom — everything in `defvar` fixed arrays, the loop functions taking no
|
`sand.flan`'s idiom — everything in `defonce` fixed arrays, the loop functions taking no
|
||||||
state — works because nothing it holds is move-only. This game's state is not like that:
|
state — works because nothing it holds is move-only. This game's state is not like that:
|
||||||
`tilesets`, `src-rects`, `dst-rects` and the bitmask table all want a `Vec` or a `Map`.
|
`tilesets`, `src-rects`, `dst-rects` and the bitmask table all want a `Vec` or a `Map`.
|
||||||
|
|
||||||
@ -302,7 +302,7 @@ piece that needs to touch the state, and the game can supply `snapshot`/`restore
|
|||||||
more callbacks.
|
more callbacks.
|
||||||
|
|
||||||
> **Settled by the author, 2026-09-13: fixed arrays with counts.** The first branch below. So the
|
> **Settled by the author, 2026-09-13: fixed arrays with counts.** The first branch below. So the
|
||||||
> conditional verdict in this section is now unconditional: the state fits in `defvar` globals, the
|
> conditional verdict in this section is now unconditional: the state fits in `defonce` globals, the
|
||||||
> engine takes only `(Fn [] ())` callbacks and never names a state type, generics stays off this
|
> engine takes only `(Fn [] ())` callbacks and never names a state type, generics stays off this
|
||||||
> game's critical path, and `drop` is not needed for this version. Nothing in the language had to be
|
> game's critical path, and `drop` is not needed for this version. Nothing in the language had to be
|
||||||
> built for it — fixed arrays, counts and slices all already work. What is left is writing the game.
|
> built for it — fixed arrays, counts and slices all already work. What is left is writing the game.
|
||||||
@ -313,7 +313,7 @@ more callbacks.
|
|||||||
**This verdict is conditional, and the condition is one decision.** Both it and the
|
**This verdict is conditional, and the condition is one decision.** Both it and the
|
||||||
globals workaround above hang on the same pivot, so it is worth stating once, flatly:
|
globals workaround above hang on the same pivot, so it is worth stating once, flatly:
|
||||||
*nothing in this game's state needs to be move-only.* Make the inner collections fixed
|
*nothing in this game's state needs to be move-only.* Make the inner collections fixed
|
||||||
arrays with counts, and then the state fits in `defvar` globals, the engine takes only
|
arrays with counts, and then the state fits in `defonce` globals, the engine takes only
|
||||||
`(Fn [] Unit)` callbacks and never names a state type, and generics stays off the critical
|
`(Fn [] Unit)` callbacks and never names a state type, and generics stays off the critical
|
||||||
path. Keep `Vec`s in a let-bound `Game` struct passed as `(Ptr Game)` instead, and
|
path. Keep `Vec`s in a let-bound `Game` struct passed as `(Ptr Game)` instead, and
|
||||||
`run-game` names `Game`, two programs with two state types need two engines, and generics
|
`run-game` names `Game`, two programs with two state types need two engines, and generics
|
||||||
@ -844,7 +844,7 @@ is the C's `char *ptr` and compiles to nothing: a `string` and a `[u8]` are the
|
|||||||
words. Every forward-reading `const char *` entry point is reachable that way.
|
words. Every forward-reading `const char *` entry point is reachable that way.
|
||||||
|
|
||||||
**`load-font-ex` needed nothing.** It takes a `[i32]` of codepoints, takes the
|
**`load-font-ex` needed nothing.** It takes a `[i32]` of codepoints, takes the
|
||||||
pointer-and-count apart itself, and uses a zeroed `defvar` as the null pointer that means
|
pointer-and-count apart itself, and uses a zeroed `defonce` as the null pointer that means
|
||||||
"the default ASCII set". It was written for this call before anything called it, and the
|
"the default ASCII set". It was written for this call before anything called it, and the
|
||||||
call fit it exactly.
|
call fit it exactly.
|
||||||
|
|
||||||
|
|||||||
@ -215,7 +215,7 @@ still alive, and a dyn in it has to stay rooted across a transfer the root stack
|
|||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(defstruct S [x dyn])
|
(defstruct S [x dyn])
|
||||||
(defvar boxes (Vec S))
|
(defonce boxes (Vec S))
|
||||||
(defn stash [] () (push boxes (S {.x (vec-new dyn)})))
|
(defn stash [] () (push boxes (S {.x (vec-new dyn)})))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -362,7 +362,7 @@ A parameter vector is a flat list of pairs. Probed:
|
|||||||
|
|
||||||
So an odd count is a parse error and an even count is a silently different program. There is no count to
|
So an odd count is a parse error and an even count is a silently different program. There is no count to
|
||||||
disambiguate by, which is precisely the finding NEXT.md item 4 records for `let`: *"`let` is a flat list of
|
disambiguate by, which is precisely the finding NEXT.md item 4 records for `let`: *"`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
|
pairs, so it 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."*
|
arguments to a form, and there is no such boundary between one pair and the next."*
|
||||||
|
|
||||||
And the meaning that would have to be given to `[row col]` is exactly the class of change this parser was
|
And the meaning that would have to be given to `[row col]` is exactly the class of change this parser was
|
||||||
@ -398,7 +398,7 @@ The cheap first step below is the one that needs **no** grammar change at all.
|
|||||||
It is worth taking seriously because everything expensive above evaporates:
|
It is worth taking seriously because everything expensive above evaporates:
|
||||||
|
|
||||||
- **One grammar decision instead of two, and it needs a marker.** Omitting the return slot cannot be decided by
|
- **One grammar decision instead of two, and it needs a marker.** Omitting the return slot cannot be decided by
|
||||||
count, and the temptation to say otherwise should be resisted: `defvar` and `defconst` disambiguate by count
|
count, and the temptation to say otherwise should be resisted: `defonce` and `defconst` disambiguate by count
|
||||||
because they have a *fixed maximum arity* — `[n t v]` is three slots and there is no fourth. A `defn` body is
|
because they have a *fixed maximum arity* — `[n t v]` is three slots and there is no fourth. A `defn` body is
|
||||||
an unbounded list of forms, so `(defn f [] (a) (b))` is `ret=(a), body=(b)` or `infer-ret, body=(a) (b)` and
|
an unbounded list of forms, so `(defn f [] (a) (b))` is `ret=(a), body=(b)` or `infer-ret, body=(a) (b)` and
|
||||||
nothing in the shape decides which. That is the same structural fact NEXT.md states about `let` — *"there is
|
nothing in the shape decides which. That is the same structural fact NEXT.md states about `let` — *"there is
|
||||||
|
|||||||
64
lib/emit.ml
64
lib/emit.ml
@ -3701,6 +3701,45 @@ let emit_startup m ?(hidden = false) (globals : Tast.global list) =
|
|||||||
floc = (List.hd computed).Tast.ginit.Tast.loc };
|
floc = (List.hd computed).Tast.ginit.Tast.loc };
|
||||||
true
|
true
|
||||||
|
|
||||||
|
(* ── What a name the process has never had starts with ─────────────────
|
||||||
|
A global a redefinition module introduces is allocated by
|
||||||
|
[flan_dev_global], which copies an image on the allocation and ignores it
|
||||||
|
for ever after. That image is the only chance the value gets: the host's
|
||||||
|
[.init-globals] was fixed when the process was built, so nothing in it
|
||||||
|
calls the new global's initialiser, now or at any re-run.
|
||||||
|
|
||||||
|
For most globals the image is the initialiser itself, when the linker
|
||||||
|
could have written it. A *computed* one has none and the allocation keeps
|
||||||
|
calloc's zeroes — the reload rule, not a gap in it: an initialiser runs at
|
||||||
|
startup, once, a reload does not run initialisers, and "edit the code,
|
||||||
|
keep the sand" is the whole demo.
|
||||||
|
|
||||||
|
A [def] needs the second arm, and it is the whole reason this is a
|
||||||
|
function rather than a call to [Tast.const_init]. Every def's initialiser
|
||||||
|
is lifted into [global/<n>] so that a re-evaluation can swap it through
|
||||||
|
the function cell, which means a def's own [ginit] is a [Call] and never a
|
||||||
|
constant — and a brand-new [(def n i64 42)] typed into a live session
|
||||||
|
would come up zero and stay zero for the life of the process, where the
|
||||||
|
[defonce] beside it correctly comes up 42. So the constant is read back
|
||||||
|
out of the lifted body, which is where it went.
|
||||||
|
|
||||||
|
Both backends ask this, because a rule only one of them follows is not a
|
||||||
|
rule. *)
|
||||||
|
let initial_image (p : Tast.program) (g : Tast.global) : Tast.expr option =
|
||||||
|
if Tast.const_init g.Tast.ginit then Some g.Tast.ginit
|
||||||
|
else
|
||||||
|
(* Only a lifted body that is exactly one expression, which is what a
|
||||||
|
constant initialiser lifts to. Anything else — a [let], a defer
|
||||||
|
counter ahead of the value — is computed by definition and has no
|
||||||
|
image, so the conservative shape is also the correct one. *)
|
||||||
|
match
|
||||||
|
List.find_opt
|
||||||
|
(fun (f : Tast.fn) -> String.equal f.Tast.name ("global/" ^ g.Tast.gname))
|
||||||
|
p.Tast.fns
|
||||||
|
with
|
||||||
|
| Some { Tast.body = [ v ]; _ } when Tast.const_init v -> Some v
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
(* ── Program ───────────────────────────────────────────────────────── *)
|
(* ── Program ───────────────────────────────────────────────────────── *)
|
||||||
|
|
||||||
let header = {|; Generated by flan. The layout is C's: no object headers anywhere,
|
let header = {|; Generated by flan. The layout is C's: no object headers anywhere,
|
||||||
@ -4575,28 +4614,21 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
|
|||||||
would have to agree with LLVM's on every target. *)
|
would have to agree with LLVM's on every target. *)
|
||||||
(* Its declared initial value travels with it, as a constant the
|
(* Its declared initial value travels with it, as a constant the
|
||||||
runtime copies on the allocation and ignores afterwards. Without
|
runtime copies on the allocation and ignores afterwards. Without
|
||||||
this a new (defonce n i64 42) or a new defconst would silently be
|
this a new (defonce n i64 42), a new (def n i64 42) or a new
|
||||||
zero — calloc is only the right answer for ZII.
|
defconst would silently be zero — calloc is only the right answer
|
||||||
|
for ZII. [initial_image] is what decides, and it is shared with
|
||||||
A *computed* initialiser sends a null instead, and the allocation
|
the x86 backend: see the note above it for the def's case and for
|
||||||
keeps calloc's zeroes. That is the reload rule and not a gap in
|
why a computed initialiser sends a null instead. *)
|
||||||
this one: a global's initialiser runs at startup, once, and a
|
|
||||||
reload does not run initialisers — sand's grid is a global and
|
|
||||||
"edit the code, keep the sand" is the whole demo. A name the
|
|
||||||
program is meeting for the first time has no startup to have
|
|
||||||
missed, so it starts as ZII and the function that loads it loads
|
|
||||||
it. Both backends answer the same way, which is the only answer
|
|
||||||
that makes it a rule. *)
|
|
||||||
let init =
|
let init =
|
||||||
if not (Tast.const_init g.Tast.ginit) then "null"
|
match initial_image p g with
|
||||||
else begin
|
| None -> "null"
|
||||||
|
| Some v ->
|
||||||
let init = Printf.sprintf "@\".init.%d\"" m.nstr in
|
let init = Printf.sprintf "@\".init.%d\"" m.nstr in
|
||||||
m.nstr <- m.nstr + 1;
|
m.nstr <- m.nstr + 1;
|
||||||
Buffer.add_string m.strs
|
Buffer.add_string m.strs
|
||||||
(Printf.sprintf "%s = private constant %s %s\n" init
|
(Printf.sprintf "%s = private constant %s %s\n" init
|
||||||
(ll g.Tast.gty) (const m g.Tast.ginit));
|
(ll g.Tast.gty) (const m v));
|
||||||
init
|
init
|
||||||
end
|
|
||||||
in
|
in
|
||||||
Buffer.add_string b
|
Buffer.add_string b
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
|
|||||||
29
lib/parse.ml
29
lib/parse.ml
@ -1660,14 +1660,27 @@ let rec decl (f : Form.t) : Ast.decl =
|
|||||||
through to "unknown function": every program written before the rename
|
through to "unknown function": every program written before the rename
|
||||||
spells it, and the message is the migration. *)
|
spells it, and the message is the migration. *)
|
||||||
| List ({ v = Sym "defvar"; _ } :: args) ->
|
| List ({ v = Sym "defvar"; _ } :: args) ->
|
||||||
let rest =
|
(* The rest of the form is echoed back inside the two spellings, so the
|
||||||
String.concat " " (List.map Form.to_string args)
|
answer is a line that can be pasted. A form with nothing after the
|
||||||
in
|
keyword has nothing to paste, and echoing it would offer
|
||||||
Loc.failk "parse/defvar-renamed" f.loc
|
[(defonce )] as the fix for [(defvar )] — a malformed old form
|
||||||
"defvar is now called defonce — the name says what it does: it \
|
answered with a malformed new one. The names alone then, which is what
|
||||||
initialises once and keeps its value across re-runs. Write (defonce \
|
there is to say about a form that named nothing. *)
|
||||||
%s), or (def %s) if the value should follow the source on every re-run"
|
(match args with
|
||||||
rest rest
|
| [] ->
|
||||||
|
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. It is \
|
||||||
|
(defonce name Type value?), or (def name Type value?) if the value \
|
||||||
|
should follow the source on every re-run"
|
||||||
|
| _ ->
|
||||||
|
let rest = String.concat " " (List.map Form.to_string args) in
|
||||||
|
Loc.failk "parse/defvar-renamed" f.loc
|
||||||
|
"defvar is now called defonce — the name says what it does: it \
|
||||||
|
initialises once and keeps its value across re-runs. Write (defonce \
|
||||||
|
%s), or (def %s) if the value should follow the source on every \
|
||||||
|
re-run"
|
||||||
|
rest rest)
|
||||||
|
|
||||||
| List ({ v = Sym "defconst"; _ } :: args) ->
|
| List ({ v = Sym "defconst"; _ } :: args) ->
|
||||||
(match args with
|
(match args with
|
||||||
|
|||||||
@ -239,7 +239,19 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
|
|||||||
callers and makes the refusal itself, naming the ones in the way.
|
callers and makes the refusal itself, naming the ones in the way.
|
||||||
Nothing else sets it, and a name that is not in it is refused here
|
Nothing else sets it, and a name that is not in it is refused here
|
||||||
as it always was. *)
|
as it always was. *)
|
||||||
if not same && not (List.mem f.Tast.name relaxed) then
|
(* A lifted initialiser, [global/<n>], is not a function anybody
|
||||||
|
wrote: its signature is its global's type, so the only way it can
|
||||||
|
change is the global being retyped — and the loop below says that
|
||||||
|
in the words a reader can act on, naming the global and its two
|
||||||
|
types. Left in, this arm gets there first and answers a question
|
||||||
|
about [global/paint] that nothing in the source mentions. So the
|
||||||
|
fact is refused exactly once, by the pass that can name it. *)
|
||||||
|
let lifted_init =
|
||||||
|
match f.Tast.fparent with
|
||||||
|
| Some parent -> String.equal f.Tast.name ("global/" ^ parent)
|
||||||
|
| None -> false
|
||||||
|
in
|
||||||
|
if not same && not lifted_init && not (List.mem f.Tast.name relaxed) then
|
||||||
(* ── When the name is not one the programmer wrote ──────────────
|
(* ── When the name is not one the programmer wrote ──────────────
|
||||||
A generic's instantiations are named [sort-i32], [sort-f32]
|
A generic's instantiations are named [sort-i32], [sort-f32]
|
||||||
and so on, and the mangling carries only the *type variables*
|
and so on, and the mangling carries only the *type variables*
|
||||||
@ -318,6 +330,30 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
|
|||||||
"%s changes type, from %s to %s; the running program already laid \
|
"%s changes type, from %s to %s; the running program already laid \
|
||||||
that storage out. Restart to change it."
|
that storage out. Restart to change it."
|
||||||
g.Tast.gname (Types.to_string h.Tast.gty) (Types.to_string g.Tast.gty)
|
g.Tast.gname (Types.to_string h.Tast.gty) (Types.to_string g.Tast.gty)
|
||||||
|
(* Which of the two mutable forms declared it is not in the storage,
|
||||||
|
it is in the *startup function*: [Emit.startup_plan] wrote the
|
||||||
|
[.init~once.] guard around a defonce's store and left a def's bare,
|
||||||
|
and that function was compiled into the host when the process was
|
||||||
|
built. A redefinition republishes the initialiser and cannot
|
||||||
|
republish the thing that decides how often it is called, so
|
||||||
|
swapping the keyword would load cleanly and then do exactly what
|
||||||
|
the old keyword said — a def that never re-runs, or a defonce that
|
||||||
|
keeps being overwritten — with nothing anywhere saying so. That is
|
||||||
|
the silent-wrongness class the house rule is about, so it is a
|
||||||
|
refusal with the reason. A defconst on either side is the arm
|
||||||
|
above's business and never reaches here: the type check catches a
|
||||||
|
retype, and a constant's own arm catches the rest. *)
|
||||||
|
| Some h
|
||||||
|
when (not h.Tast.gconst) && (not g.Tast.gconst)
|
||||||
|
&& h.Tast.grerun <> g.Tast.grerun ->
|
||||||
|
let word b = if b then "def" else "defonce" in
|
||||||
|
fail loc
|
||||||
|
"%s changes from %s to %s. The running program decides when an \
|
||||||
|
initialiser runs in its startup function, which was compiled \
|
||||||
|
when it started — a reload can replace the initialiser but not \
|
||||||
|
that. Restart to change it, or keep %s and edit the value."
|
||||||
|
g.Tast.gname (word h.Tast.grerun) (word g.Tast.grerun)
|
||||||
|
(word h.Tast.grerun)
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
new_.Tast.globals;
|
new_.Tast.globals;
|
||||||
List.iter
|
List.iter
|
||||||
|
|||||||
17
lib/x86.ml
17
lib/x86.ml
@ -4825,18 +4825,25 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
|||||||
and the allocation keeps calloc's zeroes. It runs at startup, and this
|
and the allocation keeps calloc's zeroes. It runs at startup, and this
|
||||||
module is loaded rather than started -- there is no startup here to have
|
module is loaded rather than started -- there is no startup here to have
|
||||||
missed. [emit.ml] answers a new computed global the same way and for the
|
missed. [emit.ml] answers a new computed global the same way and for the
|
||||||
same reason; the value it would compute is not what a reload is for. *)
|
same reason; the value it would compute is not what a reload is for.
|
||||||
|
|
||||||
|
Which value that is, when there is one, is [Emit.initial_image]'s answer
|
||||||
|
and not this file's. A [def] is the case that needs the shared answer: its
|
||||||
|
initialiser is lifted into [global/<n>] so a re-evaluation can swap it, so
|
||||||
|
its own [ginit] is a call and never a constant, and asking [const_init]
|
||||||
|
here would give a new [(def n i64 42)] calloc's zero for the life of the
|
||||||
|
process while the [defonce] beside it came up 42. *)
|
||||||
let images =
|
let images =
|
||||||
List.map
|
List.map
|
||||||
(fun (g : Tast.global) ->
|
(fun (g : Tast.global) ->
|
||||||
let size, align = Emit.lay md g.Tast.gty in
|
let size, align = Emit.lay md g.Tast.gty in
|
||||||
let l =
|
let l =
|
||||||
if not (Tast.const_init g.Tast.ginit) then None
|
match Emit.initial_image p g with
|
||||||
else begin
|
| None -> None
|
||||||
|
| Some v ->
|
||||||
let l = rodata_label f in
|
let l = rodata_label f in
|
||||||
scoped f (fun () -> lower f g.Tast.ginit (Lg (l, 0)));
|
scoped f (fun () -> lower f v (Lg (l, 0)));
|
||||||
Some l
|
Some l
|
||||||
end
|
|
||||||
in
|
in
|
||||||
(g, l, max 1 size, max 1 align))
|
(g, l, max 1 size, max 1 align))
|
||||||
new_globals
|
new_globals
|
||||||
|
|||||||
6
plan.org
6
plan.org
@ -122,7 +122,7 @@ world.
|
|||||||
startup, by a function ~main~ calls before a line of the program's own code —
|
startup, by a function ~main~ calls before a line of the program's own code —
|
||||||
Odin's ~__$startup_runtime~ shape rather than a constructor, so the runtime is
|
Odin's ~__$startup_runtime~ shape rather than a constructor, so the runtime is
|
||||||
up and the order is the compiler's to choose. The computed ones are sorted by
|
up and the order is the compiler's to choose. The computed ones are sorted by
|
||||||
what they read, so ~(defvar b i64 (+ a 1))~ works above ~a~; a cycle between two
|
what they read, so ~(defonce b i64 (+ a 1))~ works above ~a~; a cycle between two
|
||||||
of them is a compile error naming both. A transfer out of an initialiser —
|
of them is a compile error naming both. A transfer out of an initialiser —
|
||||||
~signal~, ~restart-case~ — is refused: nothing has established a handler that
|
~signal~, ~restart-case~ — is refused: nothing has established a handler that
|
||||||
early. A reload never re-runs an initialiser, which is what keeps the live
|
early. A reload never re-runs an initialiser, which is what keeps the live
|
||||||
@ -131,7 +131,7 @@ world.
|
|||||||
zeroed*, as in Odin — the same rule as a declaration with no initialiser, so
|
zeroed*, as in Odin — the same rule as a declaration with no initialiser, so
|
||||||
~(Cursor {.src src})~ is complete and means ~pos~ is 0.
|
~(Cursor {.src src})~ is complete and means ~pos~ is 0.
|
||||||
- *Zero is initialisation (ZII), with an opt-out.* No initialiser means
|
- *Zero is initialisation (ZII), with an opt-out.* No initialiser means
|
||||||
all-bytes-zero. ~(defvar buf [65536 u8] uninit)~ skips it, exactly as Odin's
|
all-bytes-zero. ~(defonce buf [65536 u8] uninit)~ skips it, exactly as Odin's
|
||||||
~---~ does, for a large buffer that is about to be overwritten. ~uninit~ is
|
~---~ does, for a large buffer that is about to be overwritten. ~uninit~ is
|
||||||
greppable and rare by design; reading an ~uninit~ value before writing it is
|
greppable and rare by design; reading an ~uninit~ value before writing it is
|
||||||
undefined, and dev builds poison the memory so the bug is loud.
|
undefined, and dev builds poison the memory so the bug is loud.
|
||||||
@ -900,7 +900,7 @@ marked.
|
|||||||
*struct* layout with live values is rejected; a managed class layout has an
|
*struct* layout with live values is rejected; a managed class layout has an
|
||||||
explicit frame-boundary migration path, as described above. Still open: a
|
explicit frame-boundary migration path, as described above. Still open: a
|
||||||
function pointer already handed to C, a captured environment, and redefining
|
function pointer already handed to C, a captured environment, and redefining
|
||||||
a ~defvar~. Each needs an answer of the form "rejected", "accepted with a
|
a ~defonce~. Each needs an answer of the form "rejected", "accepted with a
|
||||||
migration", or "accepted and the old code keeps running".
|
migration", or "accepted and the old code keeps running".
|
||||||
7. Does the interpreter survive milestone 3, or is the compiled path the only
|
7. Does the interpreter survive milestone 3, or is the compiled path the only
|
||||||
backend? /Settled: the compiled path is the only one, and there is no
|
backend? /Settled: the compiled path is the only one, and there is no
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* A redefinition module reaches the host's functions and globals through
|
* A redefinition module reaches the host's functions and globals through
|
||||||
* symbols the host already exports: a cell for each function, the storage for
|
* symbols the host already exports: a cell for each function, the storage for
|
||||||
* each global. That covers everything the program was *built* with. It does
|
* each global. That covers everything the program was *built* with. It does
|
||||||
* not cover a name the module introduces — a defn or a defvar typed into the
|
* not cover a name the module introduces — a defn or a defonce typed into the
|
||||||
* REPL after the process started — because there is no symbol in the host to
|
* REPL after the process started — because there is no symbol in the host to
|
||||||
* bind to and ELF cannot grow one.
|
* bind to and ELF cannot grow one.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -321,7 +321,7 @@ int64_t flan_dyn_need_i64(flan_dyn v) {
|
|||||||
double flan_dyn_need_f64(flan_dyn v) {
|
double flan_dyn_need_f64(flan_dyn v) {
|
||||||
cell *c = as(v);
|
cell *c = as(v);
|
||||||
/* An i64 satisfies an f64 slot, because a dyn integer literal is an i64 by
|
/* An i64 satisfies an f64 slot, because a dyn integer literal is an i64 by
|
||||||
* the header's rule and (defvar x f64 (f 1)) would otherwise be unwritable
|
* the header's rule and (defonce x f64 (f 1)) would otherwise be unwritable
|
||||||
* for any f returning dyn. The reverse is not true: f64 to i64 loses. */
|
* for any f returning dyn. The reverse is not true: f64 to i64 loses. */
|
||||||
if (c->tag == T_I64) return (double)c->u.i;
|
if (c->tag == T_I64) return (double)c->u.i;
|
||||||
if (c->tag != T_F64) dyn_trap("DynExpectedF64", "this value was required to be an f64 and is not");
|
if (c->tag != T_F64) dyn_trap("DynExpectedF64", "this value was required to be an f64 and is not");
|
||||||
|
|||||||
@ -1403,7 +1403,7 @@ _Noreturn void flan_null_alloc_fail(const uint8_t *loc, int64_t loclen);
|
|||||||
* no-op: "I released the region" and "I leaked the region" must not be the
|
* no-op: "I released the region" and "I leaked the region" must not be the
|
||||||
* same program text. */
|
* same program text. */
|
||||||
void flan_alloc_free_all(flan_allocator *a, const uint8_t *loc, int64_t loclen) {
|
void flan_alloc_free_all(flan_allocator *a, const uint8_t *loc, int64_t loclen) {
|
||||||
/* A null allocator is a zeroed [defvar] nobody assigned yet. Silently doing
|
/* A null allocator is a zeroed [defonce] nobody assigned yet. Silently doing
|
||||||
* nothing would make "I released the region" and "I never made one" the same
|
* nothing would make "I released the region" and "I never made one" the same
|
||||||
* program text, which is the thing this trap exists to prevent. */
|
* program text, which is the thing this trap exists to prevent. */
|
||||||
if (!a) flan_null_alloc_fail(loc, loclen);
|
if (!a) flan_null_alloc_fail(loc, loclen);
|
||||||
@ -1599,7 +1599,7 @@ static void flan_vec_check(flan_vec *v, const uint8_t *loc, int64_t loclen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A zeroed Vec — a struct field nobody assigned, or a (defvar xs (Vec i32)) —
|
/* A zeroed Vec — a struct field nobody assigned, or a (defonce xs (Vec i32)) —
|
||||||
* has a null allocator, and the first operation that needs storage adopts the
|
* has a null allocator, and the first operation that needs storage adopts the
|
||||||
* context allocator. That is Odin's behaviour, and the alternative was to
|
* context allocator. That is Odin's behaviour, and the alternative was to
|
||||||
* refuse a Vec-typed struct field outright until step 5. Shipping the null
|
* refuse a Vec-typed struct field outright until step 5. Shipping the null
|
||||||
@ -1615,7 +1615,7 @@ static flan_allocator *flan_vec_adopt(flan_vec *v) {
|
|||||||
/* The region requirement asked of a container rather than of a named
|
/* The region requirement asked of a container rather than of a named
|
||||||
* allocator, which is the form every *growth* site needs. A Vec that was built
|
* allocator, which is the form every *growth* site needs. A Vec that was built
|
||||||
* by (vec-new) has its allocator already and answers from it; one that was
|
* by (vec-new) has its allocator already and answers from it; one that was
|
||||||
* zeroed — a data type case's field left out of a literal, a (defvar xs (Vec
|
* zeroed — a data type case's field left out of a literal, a (defonce xs (Vec
|
||||||
* Value)) that a global starts as — has none yet, and the allocator it is
|
* Value)) that a global starts as — has none yet, and the allocator it is
|
||||||
* about to adopt is the context. Asking the context in that case is not a
|
* about to adopt is the context. Asking the context in that case is not a
|
||||||
* guess: [flan_vec_adopt], three lines up, is the code that will take it, and
|
* guess: [flan_vec_adopt], three lines up, is the code that will take it, and
|
||||||
@ -2472,7 +2472,7 @@ int8_t flan_map_init(flan_map *m, flan_allocator *a, int64_t ksize,
|
|||||||
m->alloc = a;
|
m->alloc = a;
|
||||||
m->epoch = (int64_t)a->epoch;
|
m->epoch = (int64_t)a->epoch;
|
||||||
/* No block until something is put in it: an empty map that is never written
|
/* No block until something is put in it: an empty map that is never written
|
||||||
* costs nothing, which is what makes a (defvar m (Map string i32)) free. */
|
* costs nothing, which is what makes a (defonce m (Map string i32)) free. */
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
;;;; printing unchanged, where the old (bytes s) either showed the write
|
;;;; printing unchanged, where the old (bytes s) either showed the write
|
||||||
;;;; through or trapped, depending on where the string's storage was.
|
;;;; through or trapped, depending on where the string's storage was.
|
||||||
|
|
||||||
(defvar frame Allocator)
|
(defonce frame Allocator)
|
||||||
|
|
||||||
(defn main [] i32
|
(defn main [] i32
|
||||||
;; 1. The copy is writable and independent. Under the old reinterpret this
|
;; 1. The copy is writable and independent. Under the old reinterpret this
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
;;;; ones selected at check time and the dyn ones handed to the runtime, so
|
;;;; ones selected at check time and the dyn ones handed to the runtime, so
|
||||||
;;;; the interleaving on one line is what proves the two sinks share a buffer.
|
;;;; the interleaving on one line is what proves the two sinks share a buffer.
|
||||||
|
|
||||||
(defvar boxed dyn 21)
|
(defonce boxed dyn 21)
|
||||||
(defvar dlabel dyn "mid")
|
(defonce dlabel dyn "mid")
|
||||||
|
|
||||||
(defn main [] ()
|
(defn main [] ()
|
||||||
;; println at every arity
|
;; println at every arity
|
||||||
|
|||||||
@ -526,6 +526,13 @@ let () =
|
|||||||
| _ -> check "the old defvar spelling has a kind" false
|
| _ -> check "the old defvar spelling has a kind" false
|
||||||
| exception Loc.Error { Loc.kind; _ } ->
|
| exception Loc.Error { Loc.kind; _ } ->
|
||||||
check "the old defvar spelling has a kind" (kind = "parse/defvar-renamed"));
|
check "the old defvar spelling has a kind" (kind = "parse/defvar-renamed"));
|
||||||
|
(* A form with nothing after the keyword has nothing to echo, and the
|
||||||
|
answer must not be "(defonce )" — a malformed old form getting a
|
||||||
|
malformed new one as its fix. *)
|
||||||
|
parse_rejects "the old spelling with no arguments names the shapes"
|
||||||
|
"(defvar)"
|
||||||
|
~needle:"It is (defonce name Type value?), or (def name Type value?) if \
|
||||||
|
the value should follow the source on every re-run";
|
||||||
(match (parse_decl "(import rl \"vendor:raylib\")").d with
|
(match (parse_decl "(import rl \"vendor:raylib\")").d with
|
||||||
| Import ("rl", "vendor:raylib") -> () | _ -> check "import" false);
|
| Import ("rl", "vendor:raylib") -> () | _ -> check "import" false);
|
||||||
|
|
||||||
|
|||||||
@ -291,6 +291,79 @@ let () =
|
|||||||
fail "a new computed global did not start zeroed";
|
fail "a new computed global did not start zeroed";
|
||||||
if not c.Session.installs then fail "adding a computed global had nothing to install";
|
if not c.Session.installs then fail "adding a computed global had nothing to install";
|
||||||
|
|
||||||
|
(* And the same claim for the form that makes it hard. Every [def]
|
||||||
|
initialiser is lifted into [global/<n>] so a re-evaluation can swap it
|
||||||
|
through the function cell, so a def's own [ginit] is a call and never a
|
||||||
|
constant — and asking [Tast.const_init] about it would hand a brand-new
|
||||||
|
(def n i64 42) calloc's zero for the life of the process, with no
|
||||||
|
startup in the host to ever put 42 there. [Emit.initial_image] reads the
|
||||||
|
constant back out of the lifted body, which is where it went. The
|
||||||
|
defonce two rows above is what this is being compared against. *)
|
||||||
|
let c = Session.eval t "(def started-def i64 42) (defn read-sd [] i64 started-def)" in
|
||||||
|
if not (has c.Session.ir "@\".init.") then
|
||||||
|
fail "a new def's initial value was dropped";
|
||||||
|
if has c.Session.ir "@\"flan.started-def\"), i64 ptrtoint (ptr getelementptr (i64, ptr null, i32 1) to i64), ptr null)"
|
||||||
|
then fail "a new def was allocated with a null image";
|
||||||
|
(* The def's own half of the computed rule, which answers the same way the
|
||||||
|
defonce's does: nothing to write, so the storage is ZII. *)
|
||||||
|
let c =
|
||||||
|
Session.eval t
|
||||||
|
"(defn seed-def [] i64 22) (def computed-def i64 (seed-def)) \
|
||||||
|
(defn read-cd [] i64 computed-def)"
|
||||||
|
in
|
||||||
|
if not (has c.Session.ir "to i64), ptr null)") then
|
||||||
|
fail "a new computed def did not start zeroed";
|
||||||
|
|
||||||
|
(* Which of the two mutable forms declared a global is in the *startup
|
||||||
|
function*, compiled when the process started — the guard a defonce has
|
||||||
|
and a def does not. A reload republishes the initialiser and cannot
|
||||||
|
republish that, so swapping the keyword would load cleanly and then keep
|
||||||
|
doing what the old keyword said. Refused, both ways.
|
||||||
|
|
||||||
|
The host's own globals, both of them read by nothing, so the checker has
|
||||||
|
no complaint and the session is the only thing that can refuse: [spare]
|
||||||
|
is a defonce and [paint] is a def. *)
|
||||||
|
(match Session.eval t "(def spare i64 0)" with
|
||||||
|
| _ -> fail "defonce became def without a word"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
if not (has m "spare changes from defonce to def") then
|
||||||
|
fail "the defonce-to-def refusal says: %s" m;
|
||||||
|
if not (has m "Restart to change it") then
|
||||||
|
fail "the form-change refusal does not say what to do: %s" m);
|
||||||
|
(match Session.eval t "(defonce paint i64 7)" with
|
||||||
|
| _ -> fail "def became defonce without a word"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
if not (has m "paint changes from def to defonce") then
|
||||||
|
fail "the def-to-defonce refusal says: %s" m);
|
||||||
|
(* Editing the *value* of a def is the workflow and stays allowed: same
|
||||||
|
form, same type, a new initialiser, and the lifted [global/paint]
|
||||||
|
republished through its cell so the next re-run stores the edited value.
|
||||||
|
This is the one the whole form exists for. *)
|
||||||
|
(match Session.eval t "(def paint i64 9)" with
|
||||||
|
| c ->
|
||||||
|
if not (List.mem "global/paint" c.Session.fns) then
|
||||||
|
fail "editing a def's value did not republish its initialiser: %s"
|
||||||
|
(String.concat " " c.Session.fns)
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "editing a def's value was refused: %s" m);
|
||||||
|
(* A def is storage like any other, so retyping one is the same refusal a
|
||||||
|
defonce gets — reasoned when the form landed, exercised here. *)
|
||||||
|
(match Session.eval t "(def paint i32 9)" with
|
||||||
|
| _ -> fail "a def was retyped without a word"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
if not (has m "paint changes type") then
|
||||||
|
fail "retyping a def says: %s" m);
|
||||||
|
(* A def initialiser that reads another global at run time, which is a
|
||||||
|
different claim from the static ordering the checker sorts on: the
|
||||||
|
lifted function loads [counter] when it runs, so re-evaluating it is an
|
||||||
|
ordinary republish and the read is the running program's storage. *)
|
||||||
|
(match Session.eval t "(def paint i64 (+ counter 1))" with
|
||||||
|
| c ->
|
||||||
|
if not (List.mem "global/paint" c.Session.fns) then
|
||||||
|
fail "a def initialiser reading a global was not republished"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "a def initialiser reading a global was refused: %s" m);
|
||||||
|
|
||||||
(* Names the process was never built with go through the registry instead of
|
(* Names the process was never built with go through the registry instead of
|
||||||
binding to a symbol, and adding one is allowed where retyping one is not. *)
|
binding to a symbol, and adding one is allowed where retyping one is not. *)
|
||||||
let c = Session.eval t "(defonce fresh i64) (defn use-fresh [] i64 (set fresh 3) fresh)" in
|
let c = Session.eval t "(defonce fresh i64) (defn use-fresh [] i64 (set fresh 3) fresh)" in
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
# sees a frontend attribute, and cannot tell Emit's output from clang's.
|
# sees a frontend attribute, and cannot tell Emit's output from clang's.
|
||||||
# This is the whole reason it was reachable here when MSan was not.
|
# This is the whole reason it was reachable here when MSan was not.
|
||||||
#
|
#
|
||||||
# 3. `zeroed` storage. A zeroed defvar is in .bss, which memcheck treats as
|
# 3. `zeroed` storage. A zeroed defonce is in .bss, which memcheck treats as
|
||||||
# defined because it is — the kernel supplies zeroes. No complaint, and
|
# defined because it is — the kernel supplies zeroes. No complaint, and
|
||||||
# correctly so.
|
# correctly so.
|
||||||
#
|
#
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user