A defvar's third element decides: a type is the static, anything else is dyn
This commit is contained in:
parent
e19dd5be29
commit
9321547822
72
FIX.org
72
FIX.org
@ -1217,3 +1217,75 @@ function dyn_ops.c *calls*, the call is compiled against the header and the
|
||||
symbol has to resolve against flan_dyn.o, so a rename, a removal or a changed
|
||||
argument list is a compile or link error in =dune test=. A function nothing
|
||||
here calls gets neither. Both comments now say that instead.
|
||||
|
||||
* The third element of a defvar decides, 2026-09-20
|
||||
The author, deciding it: "if it's 3 atoms then it's dyn", and "dispatch the if
|
||||
it's a type do the right thing."
|
||||
|
||||
So ~(defvar x <type>)~ is the zeroed static global it has always been and
|
||||
~(defvar x <expr>)~ is a dyn global initialised from that expression at
|
||||
startup. ~(defvar current-color i32)~, ~(defvar grid [rows [cols u32]])~ and
|
||||
~(defvar p Point)~ all keep their meaning to the letter; ~(defvar score 0)~ is
|
||||
a dyn holding 0, and ~(defvar game-data (edn/read-file "x.edn"))~ is what
|
||||
~(defvar game-data dyn (edn/read-file "x.edn"))~ spells out. The four-element
|
||||
forms are untouched, ~(defvar x dyn <expr>)~ among them.
|
||||
|
||||
This is a step in the direction the "dynamic-first dream" names — the author
|
||||
wants dynamic by default, lowering to static where it can — and it is the
|
||||
cheapest one available: the dyn spelling stops needing a keyword, and the
|
||||
static spelling loses nothing. It is the same dispatch the parameter vector
|
||||
already makes ([(defn f [x y] ...)] is one annotated parameter if [y] names a
|
||||
type and two dyn parameters if it does not), now in the one other position
|
||||
where a name could be either.
|
||||
|
||||
** Where it is decided
|
||||
Half in [Parse.defvar3] and half in [Check.settle_defvars], split by what each
|
||||
one can know.
|
||||
|
||||
Parse settles every form a *shape* settles, and that is most of them: [0], a
|
||||
string, a map, [[1 2 3]] and [(f "x")] are not types by any reading, so the
|
||||
global is dyn and the third element is its initialiser; [[4 u32]], [()] and
|
||||
[(Fn [i32] i32)] are types by any reading and keep today's meaning. Note where
|
||||
the bracket falls — [[n T]] stays a fixed array, so no [(defvar rows [4 u32])]
|
||||
changed under this — and that [texpr] is called under a handler, because "does
|
||||
this parse as a type" is a question its refusals answer.
|
||||
|
||||
Two shapes are left, and a name and not a shape decides them: a bare symbol,
|
||||
and [(head arg ...)] with type-shaped arguments. Both readings leave Parse
|
||||
together — the [texpr] in the [Ast.Defvar] and an [Ast.Ambiguous] expression
|
||||
beside it — and [Check.collect] picks, at the point where every type name is
|
||||
registered and just after [pair_decls], which is there for the same reason.
|
||||
The type reading wins wherever there is one, and a built-in constructor is
|
||||
recognised by name rather than by whether [resolve] happened to accept it, so
|
||||
[(Vec i32 i32)] stays a malformed [Vec] instead of becoming a call to
|
||||
something named [Vec].
|
||||
|
||||
An undecided defvar leaves [collect] as a [Zeroed] or as an [Init] at [dyn] —
|
||||
that is, as [(defvar x dyn <expr>)] exactly. Nothing downstream has a third
|
||||
case to learn: the startup lifting, the [.init~once.] re-run guard and the
|
||||
collector root are the ones that form already had, and neither backend was
|
||||
touched.
|
||||
|
||||
** The ambiguous symbol
|
||||
One namespace covers every declaration kind ([collect]'s [claimed] table), so
|
||||
a type and a value cannot share a name and the two readings can never both be
|
||||
live. What the rule *does* create is a symbol that is neither, where the old
|
||||
"unknown type foo" would now send a reader looking for the wrong mistake:
|
||||
|
||||
: foo is neither a type nor a value, and the third element of a defvar has to
|
||||
: be one or the other: a type there declares a zeroed global of that type —
|
||||
: (defvar total i64) — and a value there declares a dyn global holding it —
|
||||
: (defvar total 0). Nothing named foo is declared as either — did you mean fo?
|
||||
|
||||
Both readings, both spellings, and the near miss ranges over the value names
|
||||
as well as the type names — [near_miss] grew an [~also] parameter for it, and
|
||||
this is its only caller.
|
||||
|
||||
** Pinned
|
||||
test_flan.ml holds the four spellings with their meanings (the type and
|
||||
whether anything runs at startup, not merely that they compile), the parse
|
||||
shapes, the collision refusal and the diagnostic verbatim;
|
||||
test/programs/defvar-dyn.flan is both readings in one program, pinned in
|
||||
acceptance at the default, -O0 and --x86; and dev-rerun.flan grew a
|
||||
[(defvar tally 0)] whose line is 4 after three re-runs, which is the claim
|
||||
that the new spelling goes through the old guard.
|
||||
|
||||
15
lib/ast.ml
15
lib/ast.ml
@ -211,7 +211,15 @@ and decl_kind =
|
||||
|
||||
and variant = { vname : string; vfields : field list; vloc : Loc.t }
|
||||
|
||||
and init = Zeroed | Uninit | Init of expr
|
||||
(* [Ambiguous] is the three-element [(defvar x foo)] and [(defvar 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]
|
||||
picks, type first — a known type name or a built-in type constructor is
|
||||
[Zeroed], anything else is [Init] of this expression at [dyn]. Everything
|
||||
whose shape settles it is settled in [Parse] and never becomes one of
|
||||
these. *)
|
||||
and init = Zeroed | Uninit | Init of expr | Ambiguous of expr
|
||||
|
||||
(* 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
|
||||
@ -324,6 +332,11 @@ let mark_pause ~line ~col (ds : decl list) : decl list option =
|
||||
{ d with d = Defn { f with fbody = pause_call d.dloc :: f.fbody } }
|
||||
| Defn f -> { d with d = Defn { 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
|
||||
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)) }
|
||||
| Defconst (n, t, e) -> { d with d = Defconst (n, t, walk e) }
|
||||
| _ -> d
|
||||
in
|
||||
|
||||
117
lib/check.ml
117
lib/check.ml
@ -667,7 +667,7 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
|
||||
(* One edit away from a type that exists — a substitution, an insertion, a
|
||||
deletion or a transposition of neighbours. Bounded at one, because two edits
|
||||
is no longer a typo, it is a guess. *)
|
||||
and near_miss env n =
|
||||
and near_miss env ?(also = []) n =
|
||||
let one_edit a b =
|
||||
let la = String.length a and lb = String.length b in
|
||||
if abs (la - lb) > 1 then false
|
||||
@ -687,8 +687,12 @@ and near_miss env n =
|
||||
else ta a (!i + 1) = ta b !i
|
||||
end
|
||||
in
|
||||
(* [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,
|
||||
whose suggestion is worth nothing if it can only ever name a type. *)
|
||||
let candidates =
|
||||
Types.primitive_names
|
||||
also
|
||||
@ Types.primitive_names
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.aliases []
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.structs []
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.datas []
|
||||
@ -908,6 +912,105 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list =
|
||||
| _ -> d)
|
||||
decls
|
||||
|
||||
(* ── The third element of a defvar, 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,
|
||||
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
|
||||
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
|
||||
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
|
||||
not a call to something called [Vec].
|
||||
|
||||
A type and a value cannot share a name: [collect]'s [claimed] table is over
|
||||
every declaration kind there is, so one name is one declaration and the two
|
||||
readings can never both be live. *)
|
||||
let defvar_reads_as_type env (t : Ast.texpr) =
|
||||
match t.Ast.t with
|
||||
| Ast.Tname n -> is_type_name env n
|
||||
| Ast.Tapp (head, _) ->
|
||||
List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ]
|
||||
(* A slice, a fixed array, a map type or an (Fn ...): [Parse] only carries
|
||||
one of these over when it read as a type and had no value reading, so
|
||||
there is nothing here to decide. *)
|
||||
| _ -> true
|
||||
|
||||
(* The bare symbol that is neither. Before the rule there was one reading and
|
||||
the message was "unknown type"; now the position takes either kind of name,
|
||||
so a message naming only one of them would send a reader looking for the
|
||||
wrong mistake. Both readings, both spellings, and the near miss over the
|
||||
value names as well as the type names. *)
|
||||
let defvar_neither env loc gname n ~values =
|
||||
let hint =
|
||||
match near_miss env ~also:values n with
|
||||
| Some m -> Printf.sprintf " — did you mean %s?" m
|
||||
| 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 \
|
||||
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
|
||||
|
||||
(* 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
|
||||
still undecided belongs on it: what it resolves to is the next pass's
|
||||
question, not this one's. *)
|
||||
let value_names env (decls : Ast.decl list) =
|
||||
let declared =
|
||||
List.filter_map
|
||||
(fun (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Defvar (n, _, _) | Ast.Defconst (n, _, _) -> Some n
|
||||
| Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) ->
|
||||
Some fn.Ast.name
|
||||
| _ -> None)
|
||||
decls
|
||||
in
|
||||
declared
|
||||
@ 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
|
||||
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
|
||||
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. *)
|
||||
let settle_defvars env (decls : Ast.decl list) : Ast.decl list =
|
||||
let values = lazy (value_names env decls) in
|
||||
List.map
|
||||
(fun (d : Ast.decl) ->
|
||||
match d.Ast.d with
|
||||
| Ast.Defvar (n, Some t, Ast.Ambiguous e) ->
|
||||
if defvar_reads_as_type env t then
|
||||
{ d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed) }
|
||||
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)
|
||||
| _ -> ());
|
||||
let dyn = { Ast.t = Ast.Tname "dyn"; tloc = t.Ast.tloc } in
|
||||
{ d with Ast.d = Ast.Defvar (n, Some dyn, Ast.Init e) }
|
||||
end
|
||||
| _ -> d)
|
||||
decls
|
||||
|
||||
(* ── Generics: the four operations monomorphisation needs ───────────────
|
||||
Naming a variable, binding one from an argument, substituting the binding
|
||||
back in, and spelling the result as a symbol. Everything else about the
|
||||
@ -6914,6 +7017,9 @@ 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
|
||||
type or a value by name, and every type name is registered by here. *)
|
||||
let decls = settle_defvars env decls in
|
||||
List.iter
|
||||
(fun (d : Ast.decl) ->
|
||||
let loc = d.Ast.dloc in
|
||||
@ -7629,6 +7735,13 @@ let check_global env (d : Ast.decl) : Tast.global option =
|
||||
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
|
||||
(* [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
|
||||
in
|
||||
Some { Tast.gname = n; gty = ty; ginit; gconst = false; gfolded = false }
|
||||
| Ast.Defconst (n, _, v) ->
|
||||
|
||||
15
lib/load.ml
15
lib/load.ml
@ -399,6 +399,14 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
|
||||
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
|
||||
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
|
||||
the form, which is [Check]'s decision and not this
|
||||
pass's. *)
|
||||
| Ast.Ambiguous v ->
|
||||
Ast.Ambiguous (rename_expr owned alias [] v)
|
||||
| other -> other))
|
||||
| Ast.Defn fn ->
|
||||
let params = List.map (rename_field owned alias) fn.Ast.params in
|
||||
@ -739,7 +747,12 @@ let decl_uses acc (d : Ast.decl) =
|
||||
Option.iter (texpr_uses acc) f.Ast.ret
|
||||
| Ast.Defvar (_, t, init) ->
|
||||
Option.iter (texpr_uses acc) t;
|
||||
(match init with Ast.Init v -> expr_uses acc v | _ -> ())
|
||||
(* Both readings again: an undecided defvar 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
|
||||
| Ast.Init v | Ast.Ambiguous v -> expr_uses acc v
|
||||
| Ast.Zeroed | Ast.Uninit -> ())
|
||||
| Ast.Defconst (_, t, v) ->
|
||||
Option.iter (texpr_uses acc) t; expr_uses acc v
|
||||
|
||||
|
||||
58
lib/parse.ml
58
lib/parse.ml
@ -1014,6 +1014,55 @@ 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".
|
||||
|
||||
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
|
||||
*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
|
||||
a shape: a bare symbol, which is a type name or a value's name, and
|
||||
[(head arg ...)] with every argument type-shaped, which is [(Vec i32)] or a
|
||||
call. Both readings are built and carried — the [texpr] in the [Defvar] and
|
||||
the [Ast.Ambiguous] expression beside it — and [Check.collect] picks the
|
||||
type reading whenever the form is a type. Nothing here resolves a name,
|
||||
because at parse time there are none.
|
||||
|
||||
[texpr] is called under a handler on purpose: "does this parse as a type"
|
||||
is the question, and its refusals are how it answers no. It builds an AST
|
||||
and touches nothing else, so there is nothing to undo when it raises. *)
|
||||
let defvar3 (f : Form.t) : Ast.texpr * Ast.init =
|
||||
let as_type () = match texpr f with t -> Some t | exception Loc.Error _ -> None in
|
||||
let dyn = { Ast.t = Ast.Tname "dyn"; tloc = f.loc } in
|
||||
match f.v with
|
||||
| Sym _ ->
|
||||
(match as_type () with
|
||||
(* [Sym "Unit"] is the one symbol [texpr] refuses outright — unit is
|
||||
spelled [()] — and the refusal is about the spelling of a type, so it
|
||||
stays the error it is rather than becoming a read of a variable
|
||||
nobody can have declared. *)
|
||||
| None -> (texpr f, Ast.Zeroed)
|
||||
| Some t -> (t, Ast.Ambiguous (expr f)))
|
||||
| List ({ v = Sym _; _ } :: _ :: _) ->
|
||||
(match as_type () with
|
||||
| Some ({ Ast.t = Ast.Tapp _; _ } as t) -> (t, Ast.Ambiguous (expr f))
|
||||
(* [(Fn [i32] i32)] and anything else [texpr] reads as a type without
|
||||
going through [Tapp] has no call reading to be confused with. *)
|
||||
| Some t -> (t, Ast.Zeroed)
|
||||
| None -> (dyn, Ast.Init (expr f)))
|
||||
| _ ->
|
||||
(match as_type () with
|
||||
| Some t -> (t, Ast.Zeroed)
|
||||
| None -> (dyn, Ast.Init (expr f)))
|
||||
|
||||
(* ── Declarations ──────────────────────────────────────────────────── *)
|
||||
|
||||
let rec decl (f : Form.t) : Ast.decl =
|
||||
@ -1332,11 +1381,16 @@ let rec decl (f : Form.t) : Ast.decl =
|
||||
|
||||
| List ({ v = Sym "defvar"; _ } :: args) ->
|
||||
(match args with
|
||||
| [ n; t ] -> mk (Ast.Defvar (sym n, Some (texpr t), Ast.Zeroed))
|
||||
| [ n; t ] ->
|
||||
let ty, init = defvar3 t in
|
||||
mk (Ast.Defvar (sym n, Some ty, init))
|
||||
| [ 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)))
|
||||
| _ -> fail f "defvar is (defvar name Type value?)")
|
||||
| _ ->
|
||||
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")
|
||||
|
||||
| List ({ v = Sym "defconst"; _ } :: args) ->
|
||||
(match args with
|
||||
|
||||
75
test/programs/defvar-dyn.flan
Normal file
75
test/programs/defvar-dyn.flan
Normal file
@ -0,0 +1,75 @@
|
||||
;;;; The third element of a defvar, 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
|
||||
;;;; expression at startup. So this file is one program holding both kinds
|
||||
;;;; side by side, and every line it prints is a claim about which reading
|
||||
;;;; 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
|
||||
;;;; 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
|
||||
;;;; learned anything for it.
|
||||
|
||||
(defstruct Point [x i32 y i32])
|
||||
|
||||
;; ── 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))
|
||||
|
||||
;; ── 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)
|
||||
|
||||
;; 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))
|
||||
|
||||
;; 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
|
||||
;; hand, not in the order the file was written.
|
||||
(defvar seeded i64 7)
|
||||
|
||||
(defn main [] ()
|
||||
;; The statics, untouched: zero, zero, zero, and an empty Vec that is a real
|
||||
;; empty Vec rather than a placeholder.
|
||||
(print current-color) (print " ") (print (at grid 1 2)) (print " ")
|
||||
(print (.x origin)) (print " ") (print (len bytes)) (println "")
|
||||
|
||||
;; The dyn globals, as their initialisers left them.
|
||||
(print score) (print " ") (print label) (print " ")
|
||||
(print (get config :name)) (print " ") (print tally) (println "")
|
||||
(print (get game-data :rows)) (print " ") (print (get game-data :cols))
|
||||
(println "")
|
||||
|
||||
;; They are globals and not constants: each is assigned, and the map is
|
||||
;; mutated in place through the same root the startup function filled.
|
||||
(set score (+ score 5))
|
||||
(set label "done")
|
||||
(put config :level 2)
|
||||
(set current-color 3)
|
||||
(set (at grid 1 2) 9)
|
||||
|
||||
;; An allocation loop between filling them and reading them back, so the
|
||||
;; collector runs with the run's own frames on the stack. A dyn global whose
|
||||
;; root was not pushed reads back as a stale word here rather than as what
|
||||
;; was stored.
|
||||
(dotimes [i 20000]
|
||||
(let [junk {:i i :s "forty-seven bytes of text to fatten each row"}]
|
||||
(put config :seen (get junk :i))))
|
||||
|
||||
(print score) (print " ") (print label) (print " ")
|
||||
(print (get config :level)) (println "")
|
||||
(print current-color) (print " ") (print (at grid 1 2)) (println ""))
|
||||
@ -36,6 +36,14 @@
|
||||
|
||||
(defvar 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]
|
||||
;; is, down to the guard flag, because the rule lowers to that form and not to
|
||||
;; 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)
|
||||
|
||||
;; The guard flags the fix adds are the compiler's own globals, and they used
|
||||
;; to be spelled [.init-once.<name>] — a name a program can write, since [.]
|
||||
;; is an ordinary symbol constituent. This one is exactly the old spelling of
|
||||
@ -51,9 +59,11 @@
|
||||
(set zeroed (+ zeroed 2))
|
||||
(set .init-once.counter (+ .init-once.counter 1))
|
||||
(put state :runs (+ (get state :runs) 1))
|
||||
(set tally (+ tally 1))
|
||||
(print "counter ") (print counter) (println "")
|
||||
(print "zeroed ") (print zeroed) (println "")
|
||||
(print "runs ") (print (get state :runs)) (println "")
|
||||
(print "tally ") (print tally) (println "")
|
||||
(print "base ") (print base) (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.
|
||||
|
||||
@ -3625,6 +3625,29 @@ level "1"
|
||||
outputs ~x86:true "dyn: a global, --x86"
|
||||
"programs/dyn-global.flan" dyn_global_out;
|
||||
|
||||
(* The three-element defvar, both readings in one program. The first line
|
||||
is the four zeroed statics, the next two are the dyn globals as their
|
||||
initialisers left them, and the last two are what the run stored —
|
||||
read back after twenty thousand allocations, so an unrooted dyn global
|
||||
comes out stale rather than as what was written.
|
||||
|
||||
Three rows for the same reason [dyn-global.flan] has three: the dyn
|
||||
half lowers to the startup function and the root push, and those are
|
||||
two backends' worth of code emitted from one shared decision. There is
|
||||
nothing backend-specific in the *rule* — it is settled in [Check], and
|
||||
what reaches either emitter is the [(defvar x dyn <expr>)] that
|
||||
already existed — which is precisely what these rows are here to keep
|
||||
true. *)
|
||||
let defvar_dyn_out = "0 0 0 0\n0 start one 7\n3 4\n5 done 2\n3 9\n" in
|
||||
outputs "defvar: a type third element and a value third element"
|
||||
"programs/defvar-dyn.flan" defvar_dyn_out;
|
||||
outputs ~opt:"-O0"
|
||||
"defvar: a type third element and a value third element, -O0"
|
||||
"programs/defvar-dyn.flan" defvar_dyn_out;
|
||||
outputs ~x86:true
|
||||
"defvar: a type third element and a value third element, --x86"
|
||||
"programs/defvar-dyn.flan" defvar_dyn_out;
|
||||
|
||||
(* The boundary, both directions, and then the claim that is wrong. The
|
||||
first four lines are the conversions; the trap is the fifth, and the
|
||||
runtime owns its wording — the compiler could only have said that two
|
||||
|
||||
@ -4742,9 +4742,10 @@ let () =
|
||||
initialiser every *other* time would pass a single re-run.
|
||||
|
||||
[programs/dev-rerun.flan] prints one line per case per run, and the
|
||||
whole assertion is the fourth run's four lines: [counter] computed and
|
||||
whole assertion is the fourth run's five lines: [counter] computed and
|
||||
incremented four times, [zeroed] uncomputed and incremented four times,
|
||||
a computed dyn map whose contents were mutated four times, and a
|
||||
a computed dyn map whose contents were mutated four times, a dyn global
|
||||
written the three-element way and incremented four times, and a
|
||||
[defconst] that no run can have changed. *)
|
||||
let rsock = tmp "rerun.sock" and rout = tmp "rerun.out" in
|
||||
(try Sys.remove rsock with Sys_error _ -> ());
|
||||
@ -4796,6 +4797,11 @@ let () =
|
||||
and so does the mutation, which is the map still being the map the
|
||||
first run built. *)
|
||||
"runs 4";
|
||||
(* The three-element spelling of a computed dyn global, which is the
|
||||
explicit one with the keyword left out: it goes through the same
|
||||
guard because it *is* the same declaration by the time anything
|
||||
downstream sees it. *)
|
||||
"tally 4";
|
||||
(* And a [defconst], which no run can have changed. *)
|
||||
"base 40" ]
|
||||
in
|
||||
|
||||
@ -453,6 +453,19 @@ let () =
|
||||
| _ -> check "defvar is ZII" false);
|
||||
(match (parse_decl "(defvar buf [4 u8] uninit)").d with
|
||||
| Defvar (_, _, Uninit) -> () | _ -> check "defvar uninit opts out" false);
|
||||
(* A three-element defvar whose third element cannot be a type is settled
|
||||
here, by its shape, and comes out as the dyn global it means. *)
|
||||
(match (parse_decl "(defvar score 0)").d with
|
||||
| Defvar ("score", Some { t = Tname "dyn"; _ }, Init _) -> ()
|
||||
| _ -> check "a literal third element parses as a dyn initialiser" false);
|
||||
(* A bare symbol could be either and parse does not know any names, so both
|
||||
readings are carried out of here for [Check] to pick between. *)
|
||||
(match (parse_decl "(defvar total foo)").d with
|
||||
| Defvar ("total", Some { t = Tname "foo"; _ }, Ambiguous _) -> ()
|
||||
| _ -> check "a symbol third element parses undecided" false);
|
||||
(match (parse_decl "(defvar v (Vec i32))").d with
|
||||
| Defvar ("v", Some { t = Tapp ("Vec", _); _ }, Ambiguous _) -> ()
|
||||
| _ -> check "a parenthesised third element parses undecided" false);
|
||||
(match (parse_decl "(import rl \"vendor:raylib\")").d with
|
||||
| Import ("rl", "vendor:raylib") -> () | _ -> check "import" false);
|
||||
|
||||
@ -790,6 +803,38 @@ let rejects_check name ?needle src =
|
||||
name n msg
|
||||
| _ -> ())
|
||||
|
||||
(* Which reading a three-element [defvar] got, pinned by what the global came
|
||||
out as rather than by what compiled: the two readings differ in the type and
|
||||
in whether anything runs at startup, and a test that only asked "does this
|
||||
check" would pass on either one. [zeroed] is the static reading — the
|
||||
all-bytes-zero initialiser the linker writes — and its negation is the dyn
|
||||
one, whose initialiser is an expression [Emit] lifts into the startup
|
||||
function. *)
|
||||
let defvar_reading name src gname ~ty ~zeroed =
|
||||
match checked src with
|
||||
| p ->
|
||||
(match List.find_opt (fun (g : Tast.global) -> g.gname = gname) p.globals with
|
||||
| Some g ->
|
||||
let got = Types.to_string g.gty in
|
||||
let got_zeroed =
|
||||
match g.Tast.ginit.Tast.e with Tast.Zero _ -> true | _ -> false
|
||||
in
|
||||
if got <> ty || got_zeroed <> zeroed then begin
|
||||
incr failures;
|
||||
Printf.printf
|
||||
"FAIL %s\n src: %s\n got: %s, %s\n wanted: %s, %s\n"
|
||||
name src got (if got_zeroed then "zeroed" else "initialised")
|
||||
ty (if zeroed then "zeroed" else "initialised")
|
||||
end
|
||||
| None ->
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s: no global named %s\n src: %s\n"
|
||||
name gname src)
|
||||
| exception Loc.Error { Loc.dloc = loc; dmsg = msg; _ } ->
|
||||
incr failures;
|
||||
Printf.printf "FAIL %s\n src: %s\n error: %s: %s\n"
|
||||
name src (Loc.to_string loc) msg
|
||||
|
||||
let () =
|
||||
(* ── Literal defaulting and inference ──────────────────────────── *)
|
||||
infers "int defaults to i32" "42" "i32";
|
||||
@ -1759,6 +1804,73 @@ let () =
|
||||
"(defvar g (Vec u8) uninit) (defn f [] ())"
|
||||
~needle:"steers every read of it";
|
||||
|
||||
(* ── The third element of a defvar ─────────────────────────────────
|
||||
The rule, 2026-09-20: a type there is the zeroed static global it has
|
||||
always been, and anything else is a dyn global initialised from the
|
||||
expression at startup. The four rows below are the four spellings, each
|
||||
pinned with its meaning and not merely with the fact that it compiles. *)
|
||||
defvar_reading "a primitive third element stays a zeroed static"
|
||||
"(defvar current-color i32) (defn f [] i32 current-color)"
|
||||
"current-color" ~ty:"i32" ~zeroed:true;
|
||||
defvar_reading "a bracketed type stays a zeroed static array"
|
||||
"(defvar grid [2 [3 u32]]) (defn f [] u32 (at grid 0 0))"
|
||||
"grid" ~ty:"[2 [3 u32]]" ~zeroed:true;
|
||||
(* The edge the rule turns on: [Point] is a type, so the type reading wins
|
||||
and this is the zeroed struct it was before the rule existed. A value
|
||||
named [Point] cannot exist to compete with it — [collect] refuses one
|
||||
name declared twice, across every declaration kind there is. *)
|
||||
defvar_reading "a struct's name stays a zeroed static struct"
|
||||
"(defstruct Point [x i32 y i32]) (defvar p Point) (defn f [] i32 (.x p))"
|
||||
"p" ~ty:"Point" ~zeroed:true;
|
||||
rejects_check "a type's name and a value's name cannot collide"
|
||||
"(defstruct Point [x i32 y i32]) (defvar Point i32 1) (defn f [] ())"
|
||||
~needle:"defined twice";
|
||||
(* A parenthesised type is still a type, so this is the zeroed Vec it was —
|
||||
which is also why a malformed one stays a type error rather than turning
|
||||
into a call to something named Vec. *)
|
||||
defvar_reading "a parenthesised type stays a zeroed static"
|
||||
"(defvar v (Vec i32)) (defn f [] i32 (len v))"
|
||||
"v" ~ty:"(Vec i32)" ~zeroed:true;
|
||||
rejects_check "a malformed parenthesised type stays a type error"
|
||||
"(defvar v (Vec i32 i32)) (defn f [] ())"
|
||||
~needle:"(Vec T) takes exactly one type";
|
||||
(* And the new spelling, which is the explicit dyn form with the keyword
|
||||
left out. *)
|
||||
defvar_reading "a literal third element is a dyn global holding it"
|
||||
"(defvar score 0) (defn f [] () (set score (+ score 1)))"
|
||||
"score" ~ty:"dyn" ~zeroed:false;
|
||||
defvar_reading "a call as the third element is a dyn global"
|
||||
"(defn load [] dyn {:n 1}) (defvar game-data (load)) (defn f [] dyn game-data)"
|
||||
"game-data" ~ty:"dyn" ~zeroed:false;
|
||||
(* A bare symbol naming a value, which is the shape only a name can settle:
|
||||
[seed] is not a type, so this is a dyn global initialised from it. *)
|
||||
defvar_reading "a value's name as the third element is a dyn global"
|
||||
"(defvar seed i64 3) (defvar score seed) (defn f [] dyn score)"
|
||||
"score" ~ty:"dyn" ~zeroed:false;
|
||||
(* The explicit spellings are untouched by all of it. *)
|
||||
defvar_reading "the explicit dyn form with a value is unchanged"
|
||||
"(defvar score dyn 0) (defn f [] dyn score)"
|
||||
"score" ~ty:"dyn" ~zeroed:false;
|
||||
defvar_reading "the explicit dyn form with no value is unchanged"
|
||||
"(defvar config dyn) (defn f [] dyn config)"
|
||||
"config" ~ty:"dyn" ~zeroed:true;
|
||||
(* The symbol that is neither, which is the one position the rule made
|
||||
ambiguous: before it there was a single reading and "unknown type" was
|
||||
the whole story, and a message that still said only that would send a
|
||||
reader looking for the wrong mistake. Both readings, both spellings, and
|
||||
the near miss over the value names too. *)
|
||||
rejects_check "a symbol that is neither a type nor a value"
|
||||
"(defvar total foo) (defn f [] ())"
|
||||
~needle:
|
||||
"foo is neither a type nor a value, and the third element of a defvar \
|
||||
has to be one or the other: a type there declares a zeroed global of \
|
||||
that type — (defvar total i64) — and a value there declares a dyn \
|
||||
global holding it — (defvar total 0). Nothing named foo is declared \
|
||||
as either";
|
||||
rejects_check "the near miss is over the value names as well as the types"
|
||||
"(defvar score i64 1) (defvar total scor) (defn f [] ())"
|
||||
~needle:"Nothing named scor is declared as either — did you mean score?";
|
||||
|
||||
(* ── Computed global initialisers ──────────────────────────────────
|
||||
The order they run in is the compiler's to choose, so a global written
|
||||
above the one it reads is fine... *)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user