The defvar follow-ups, the spellings other languages use, and two register warts

The four the defvar review left behind, plus the two the author's dogfooding
notes name.

A three-element defvar that is neither a type nor a value gets a paragraph
about the fork it stands at, and the paragraph is right for the name that
genuinely could have been either. Three names cannot: a data case, which is
a third thing with its own spelling; a name another language uses for a type
this one has; and a plain type typo, where a confident one-edit suggestion
was turning a line into four. Each answers first now.

A bracket form never reaches that fork at all — the parser gives it the type
reading outright — so a value name inside one landed in [resolve_name] and
came back as a lecture about generic code. Both readings at the element that
decided it, and the dyn spelling it offers is checked to be a real form.

[int] is two edits from [i32] and so outside the one-edit net, correctly:
two edits is a guess. But the name is not a guess, it is what four other
languages call the default integer, so a short list answers it by name.
Nothing goes on that list without one honest answer — [char] and [void] are
off it, and the comment says why.

A parameter called [i] is not a mistyped [i8]. The machine types size
themselves in the name, so a typo keeps the digits and a parameter name has
none; that is the rule that stopped (defn idx [v i] dyn ...) being refused.

A type written in a two-element defconst was reported as an unknown name,
because that form has no type slot and the brackets read as an array
literal. A type name inside one is unambiguous — a type and a value cannot
share a name here — so it says what happened and names defvar.

Two register warts alongside: no message cites a repo filename at the reader
any more (plan.org in three, spec-memory.md in one).
This commit is contained in:
Joseph Ferano 2026-09-20 17:55:59 +07:00
parent db2f6c69b9
commit 8add0093ba
2 changed files with 231 additions and 16 deletions

View File

@ -228,6 +228,35 @@ let one_edit a b =
value goes was not a mistyped struct. *)
let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
(* What the last language called it. [int] is two edits from [i32] and so is
outside [one_edit]'s net, which is right two edits is a guess but the
name is not a guess at all: it is what C, Java, Go and Python spell the
default integer, and somebody writing it here has not mistyped anything,
they have not yet learned that this language sizes its integers in the
name. Without this list [int] falls through to the lowercase arm of
[resolve_name] and is reported as generic code over a type variable, which
is a sentence about a feature the reader was not reaching for.
Short on purpose, and only names with one honest answer. [char] is not
here: C's is a byte, Java's is a UTF-16 unit and Rust's is a scalar value,
and this language has [u8] and rune functions, so there is nothing to
translate it to in three words. Nor [void]: it is a return type and the
answer there is the shape [()], which is [parse]'s message to give and not
this one's. *)
let foreign_spelling = function
| "int" | "integer" -> Some "i32"
| "uint" | "unsigned" -> Some "u32"
| "long" -> Some "i64"
| "ulong" -> Some "u64"
| "short" -> Some "i16"
| "ushort" -> Some "u16"
| "byte" -> Some "u8"
| "float" -> Some "f32"
| "double" -> Some "f64"
| "boolean" -> Some "bool"
| "str" -> Some "string"
| _ -> None
(* The builtin names, for the did-you-mean at a call — [prinltn] is a typo for
[println], and [println] is not in any table the checker keeps, it is an arm
of the call dispatch. The full [builtins] table is a long way below this
@ -469,7 +498,10 @@ let branch ctx f =
(* ── Type resolution ───────────────────────────────────────────────── *)
let unimplemented loc what milestone =
fail loc "%s is not implemented yet — milestone %d (see plan.org)"
(* No repo filename in a message. Somebody meeting this wants to know that
the thing is not there yet and roughly how far off it is; where the
schedule is written down is the compiler's business, not theirs. *)
fail loc "%s is not implemented yet — it is milestone %d work"
what milestone
(* ── where predicates ──────────────────────────────────────────────────
@ -801,6 +833,9 @@ and resolve_name env ~seen loc n =
(* A typo in a primitive is lowercase too, and the type-variable rule
below would otherwise report [f65] as unimplemented generics and send
you to plan.org instead of to the character you mistyped. *)
| _ when foreign_spelling n <> None ->
Loc.failk "check/unknown-type" loc "unknown type %s — Flan spells it %s"
n (Option.get (foreign_spelling n))
| _ when near_miss env n <> None ->
Loc.failk "check/unknown-type" loc "unknown type %s — did you mean %s?" n
(Option.get (near_miss env n))
@ -879,7 +914,22 @@ let is_type_name env n =
that it was meant to be one. That case is the feature working as specified,
and it is the residual the parent owns. *)
let dyn_param_or_typo env n loc =
match near_miss env n with
(* [(defn idx [v i] dyn ...)] is two dyn parameters, and [i] is one edit
from [i8], so the did-you-mean used to accuse a perfectly ordinary
parameter name of being a mistyped type. What separates the two is the
digits: this language sizes its machine types in the name, so a typo in
one keeps them [f65] for [f64], [i33] for [i32] while [i], [v], [n]
and [x] carry none and are what parameters are actually called. A name
with no digit, one edit from a type that has one, is a parameter; the
suggestion is dropped and the dyn reading stands, which is the reading
the writer meant. *)
let has_digit s = String.exists (fun c -> c >= '0' && c <= '9') s in
let suggestion =
match near_miss env n with
| Some m when has_digit m && not (has_digit n) -> None
| m -> m
in
match suggestion with
| Some m ->
Loc.failk "check/unknown-type" loc
"unknown type %s — did you mean %s? A parameter with no type is dyn, so \
@ -983,7 +1033,33 @@ let defvar_reads_as_type env (t : Ast.texpr) =
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 =
(* Both readings, and the paragraph that explains them — but only when both
readings really are open. Three things get in ahead of it, because each one
knows which of the two the writer meant and the paragraph would bury that
under a lecture about a fork they are not standing at:
a case name, which is a third thing entirely and has its own spelling; a
name another language spells for a type this one has under a different
name; and a plain type typo, where a confident one-edit suggestion turns a
one-line answer into four lines of unrelated reading. The paragraph is for
the name that genuinely could have been either and is neither. *)
let defvar_neither env loc gname n ~values ~cases =
(match List.assoc_opt n cases with
| Some dname ->
Loc.failk "check/defvar-case-not-type" loc
"%s is a case of the data type %s, and a case is not a type of its \
own the global's type is the data type: (defvar %s %s). Assign the \
case you want, as (set %s (%s.%s {.field value ...}))"
n dname gname dname gname dname n
| None -> ());
(match foreign_spelling n with
| Some m ->
Loc.failk "check/unknown-type" loc "unknown type %s — Flan spells it %s" n m
| None -> ());
(match near_miss env n with
| Some m ->
Loc.failk "check/unknown-type" loc "unknown type %s — did you mean %s?" n m
| None -> ());
let hint =
match near_miss env ~also:values n with
| Some m -> Printf.sprintf " — did you mean %s?" m
@ -1001,6 +1077,21 @@ let defvar_neither env loc gname n ~values =
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. *)
(* Case name -> the data type it belongs to, read off the declarations rather
than out of [env.cases]: this runs inside [collect], which has registered
the data type *names* by here but not resolved their cases, so the table
would be empty. Last writer wins, exactly as [env.cases] does, and for the
same reason this is only ever asked "what is this a case of", and two
data types may share a case name. *)
let case_owners (decls : Ast.decl list) =
List.concat_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defdata (dn, vs) ->
List.map (fun (v : Ast.variant) -> (v.Ast.vname, dn)) vs
| _ -> [])
decls
let value_names env (decls : Ast.decl list) =
let declared =
List.filter_map
@ -1022,18 +1113,62 @@ let value_names env (decls : Ast.decl list) =
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. *)
(* A bracket form whose element names a value. [(defvar g [a b])] parses as a
type and stays one type wins wherever there is a type reading, which is
the rule so the element had to name an element type, and [b] names a
defvar. Left alone this reaches [resolve_name], where a lowercase name that
is no type is a type variable, and the answer is a paragraph about generic
code the writer was not asking for.
Both readings, and both spellings, at the element that decided it. The dyn
spelling is the one that actually works: [(defvar g dyn [a b])] is a dyn
global holding a vector, which is what the brackets meant to whoever wrote
them. *)
let rec bracket_value_element env values (t : Ast.texpr) =
let elem (e : Ast.texpr) =
match e.Ast.t with
| Ast.Tname n when (not (is_type_name env n)) && List.mem n values ->
Some (n, e.Ast.tloc)
| _ -> bracket_value_element env values e
in
match t.Ast.t with
| Ast.Tslice e -> elem e
| Ast.Tarray (_, e) -> elem e
| _ -> None
let settle_defvars env (decls : Ast.decl list) : Ast.decl list =
let values = lazy (value_names env decls) in
let cases = lazy (case_owners decls) in
(* A bracket form never reaches the fork below: [Parse.defvar3] gives it
[Zeroed] outright, because a bracket that parses as a type has no second
reading to carry. So the element check runs on both, and it is the only
thing the [Zeroed] arm does. *)
let brackets gname (t : Ast.texpr) =
match bracket_value_element env (Lazy.force values) t with
| Some (v, vloc) ->
Loc.failk "check/defvar-bracket-element-is-a-value" vloc
"%s names a value, not a type, and the brackets around it were read \
as a type a defvar's third element is a type wherever there is a \
type reading, so %s had to be the element type. Write a type there \
for a zeroed global, or put dyn in front of the same brackets \
(defvar %s dyn ...) for a dyn global holding the vector you wrote"
v v gname
| None -> ()
in
List.map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defvar (n, Some t, Ast.Zeroed) -> brackets n t; d
| Ast.Defvar (n, Some t, Ast.Ambiguous e) ->
if defvar_reads_as_type env t then
if defvar_reads_as_type env t then begin
brackets n t;
{ d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed) }
end
else begin
(match t.Ast.t with
| Ast.Tname s when not (List.mem s (Lazy.force values)) ->
defvar_neither env t.Ast.tloc n s ~values:(Lazy.force values)
~cases:(Lazy.force cases)
| _ -> ());
let dyn = { Ast.t = Ast.Tname "dyn"; tloc = t.Ast.tloc } in
{ d with Ast.d = Ast.Defvar (n, Some dyn, Ast.Init e) }
@ -4337,8 +4472,8 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
| Some b ->
if not b.assignable then
fail loc
"%s is a parameter, and parameters are not assignable places \
(spec-memory.md) bind a local with let" name;
"%s is a parameter, and a parameter is not a place you can assign \
to bind a local with let" name;
Tast.Plocal b.slot, b.bty
| None ->
match Hashtbl.find_opt ctx.env.globals name with
@ -4918,12 +5053,12 @@ and named_call ctx ~want loc name args =
(match name with
| "=" | "!=" ->
fail loc
"%s compares machine numbers, enums and strings; %s has no \
built-in equality (plan.org, Types)" name (Types.to_string a.Tast.ty)
"%s compares machine numbers, enums and strings, and %s is none of \
those" name (Types.to_string a.Tast.ty)
| _ ->
fail loc
"%s orders machine numbers and enums; %s has no built-in ordering \
(plan.org, Types)" name (Types.to_string a.Tast.ty));
"%s orders machine numbers and enums, and %s is neither" name
(Types.to_string a.Tast.ty));
prim p Types.Bool [ a; b ]
end
| "not" ->
@ -7191,6 +7326,33 @@ let rec const_int env (e : Ast.expr) : int64 option =
(const_int env x) (y :: rest)
| _ -> None
(* [(defconst grid [rows [cols u8]])]. A two-element defconst has no type slot
the second form is always a value so the brackets were read as an array
*literal* and [u8] as a name in it, and the refusal that came out was
"unknown name u8", which sends the reader to look for a missing definition
of something the language has had all along.
A type name inside an array literal is unambiguous evidence, because a type
and a value cannot share a name: [collect]'s claimed table is over every
declaration kind there is. So finding one means the whole form was meant as
a type, and the form that takes one is [defvar]. *)
let rec defconst_type_shaped env gname (v : Ast.expr) =
match v.Ast.e with
| Ast.Arr items ->
List.iter
(fun (i : Ast.expr) ->
match i.Ast.e with
| Ast.Var n when is_type_name env n ->
Loc.failk "check/defconst-is-a-type" i.Ast.loc
"%s is a type, and this is a value: a two-element defconst has no \
type slot, so the brackets around it were read as an array \
literal and %s as a name in it. A global declared by its type is \
a defvar write (defvar %s ...) with the same brackets"
n n gname
| _ -> defconst_type_shaped env gname i)
items
| _ -> ()
let collect env (decls : Ast.decl list) =
(* One pass over every declaration kind before any of the others, because
the tables below are per-kind structs, data types, aliases, enums, functions
@ -7493,7 +7655,9 @@ let collect env (decls : Ast.decl list) =
Hashtbl.replace env.globals n (ty, false)
| Ast.Defconst (n, Some t, _) ->
Hashtbl.replace env.globals n (resolve env t, true)
| Ast.Defconst (n, None, v) -> untyped := (n, v) :: !untyped
| Ast.Defconst (n, None, v) ->
defconst_type_shaped env n v;
untyped := (n, v) :: !untyped
(* [Classes.expand] ran at the top of [build_program] and left none of
these behind, the way [Shim.expand] leaves no [declare-c] behind. A
driver that assembled a declaration list and skipped that pass would

View File

@ -1661,7 +1661,7 @@ let () =
accepts "a local is assignable"
"(defn f [] i32 (let [x 1] (set x 2) x))";
rejects_check "a parameter is not assignable"
"(defn f [x i32] () (set x 2))" ~needle:"parameters are not assignable";
"(defn f [x i32] () (set x 2))" ~needle:"a parameter is not a place you can assign to";
rejects_check "a constant is not assignable"
"(defconst k 1) (defn f [] () (set k 2))" ~needle:"is a constant";
accepts "addr of a local gives a pointer"
@ -1924,13 +1924,13 @@ let () =
accepts "typed = on strings" "(defn f [] bool (= \"a\" \"b\"))";
accepts "typed != on strings" "(defn f [] bool (!= \"a\" \"b\"))";
rejects_check "no built-in < on strings"
"(defn f [] bool (< \"a\" \"b\"))" ~needle:"no built-in ordering";
"(defn f [] bool (< \"a\" \"b\"))" ~needle:"orders machine numbers and enums";
rejects_check "no built-in <= on strings"
"(defn f [] bool (<= \"a\" \"b\"))" ~needle:"no built-in ordering";
"(defn f [] bool (<= \"a\" \"b\"))" ~needle:"orders machine numbers and enums";
rejects_check "no built-in > on strings"
"(defn f [] bool (> \"a\" \"b\"))" ~needle:"no built-in ordering";
"(defn f [] bool (> \"a\" \"b\"))" ~needle:"orders machine numbers and enums";
rejects_check "no built-in >= on strings"
"(defn f [] bool (>= \"a\" \"b\"))" ~needle:"no built-in ordering";
"(defn f [] bool (>= \"a\" \"b\"))" ~needle:"orders machine numbers and enums";
(* (Vec T) is built. What is still refused is the arity: one element type,
and a near-miss there would otherwise resolve to a type variable and come
back as generics. *)
@ -2069,6 +2069,57 @@ let () =
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?";
(* Three things that know which of the two readings was meant, and get in
ahead of the paragraph rather than being buried under it. A paragraph
about a fork the reader is not standing at is worse than a line. *)
rejects_check "a plain type typo keeps the short answer"
"(defvar total i33) (defn f [] ())"
~needle:"unknown type i33 — did you mean i32?";
rejects_check "and another language's spelling is answered by name"
"(defvar total int) (defn f [] ())"
~needle:"unknown type int — Flan spells it i32";
rejects_check "a data case is not a type, and says what is"
"(defdata Shape [(Circle [r f64])]) (defvar g Circle) (defn f [] ())"
~needle:"Circle is a case of the data type Shape, and a case is not a \
type of its own the global's type is the data type: (defvar g \
Shape). Assign the case you want, as (set g (Shape.Circle \
{.field value ...}))";
(* A bracket form never reaches that fork — the parser gives it the type
reading outright so a value name inside one used to land in
[resolve_name] and come back as a lecture about generic code. Both
readings at the element that decided it, and the dyn spelling is the one
that works. *)
rejects_check "a bracket type whose element names a value says both readings"
"(defvar a i64 1) (defvar b i64 2) (defvar g [a b]) (defn f [] ())"
~needle:"b names a value, not a type, and the brackets around it were \
read as a type";
rejects_check "and names the dyn spelling that does work"
"(defvar a i64 1) (defvar b i64 2) (defvar g [a b]) (defn f [] ())"
~needle:"put dyn in front of the same brackets — (defvar g dyn ...)";
accepts "which is a real form"
"(defvar a i64 1) (defvar b i64 2) (defvar g dyn [a b]) (defn f [] ())";
(* defconst's two-element form has no type slot, so a type written in one
was read as a name in an array literal and reported as unknown. It is
unambiguous evidence: a type and a value cannot share a name here. *)
rejects_check "a type in a two-element defconst names defvar"
"(defconst rows 4) (defconst cols 4) (defconst grid [rows [cols u8]]) \
(defn f [] ())"
~needle:"u8 is a type, and this is a value: a two-element defconst has no \
type slot";
accepts "and the defvar it names is the form that works"
"(defconst rows 4) (defconst cols 4) (defvar grid [rows [cols u8]]) \
(defn f [] ())";
accepts "an ordinary array constant is untouched" "(defconst xs [1 2 3])";
(* A parameter name is not a mistyped type. This language sizes its machine
types in the name, so a typo in one keeps the digits and a parameter
called [i] or [n] has none which is the whole of the rule that stopped
[(defn idx [v i] dyn ...)] being refused. *)
accepts "a short parameter name is not a mistyped type"
"(defn idx [v i] dyn v)";
rejects_check "but a mistyped machine type still is"
"(defn g [x f65] f64 x)" ~needle:"unknown type f65 — did you mean f64?";
(* ── Computed global initialisers ──────────────────────────────────
The order they run in is the compiler's to choose, so a global written