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:
parent
db2f6c69b9
commit
8add0093ba
186
lib/check.ml
186
lib/check.ml
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user