The dot habit, and a did-you-mean over values
Two of the audit's four cheapest structural wins, and they share a raise point. [p.x] is how C, Go and Odin spell field access, and it arrived here as the symbol [p.x] and left as 'unknown name p.x' — true, and no use to anyone. The head is looked up now, so the refusal can say what [p] actually is, and the struct's declaration comes along as a note. A capitalised head is left alone: [Shape.Circle] is a real spelling and a typo in one is a mistyped case. [near_miss] was written, tested and wired to the type tables alone, so a mistyped value name got the bare refusal. [one_edit] is hoisted out of it so the value side matches on the same rule rather than a second one that would drift, and the candidate list at a value position is the scope, the globals and the functions — plus, at a call, the builtin names, which live in no table the checker keeps and reach the raise through a forward reference. Two repairs alongside: the data-type message's format string carried eleven stray spaces from a wrapped line, and (Pair i32) in a defvar had lost the type fork's 'generics are milestone 5' answer when it started falling down the value fork.
This commit is contained in:
parent
6d82221978
commit
2b5d793d7a
172
lib/check.ml
172
lib/check.ml
@ -187,6 +187,45 @@ let declared_note env name =
|
|||||||
in
|
in
|
||||||
[ Loc.note at what ]
|
[ Loc.note at what ]
|
||||||
|
|
||||||
|
(* One edit apart: 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. Hoisted out of [near_miss] so that the did-you-mean over *values* —
|
||||||
|
function names, globals, locals — matches on exactly the same rule the one
|
||||||
|
over types has always matched on, rather than on a second one that would
|
||||||
|
drift. *)
|
||||||
|
let one_edit a b =
|
||||||
|
let la = String.length a and lb = String.length b in
|
||||||
|
if abs (la - lb) > 1 then false
|
||||||
|
else begin
|
||||||
|
(* Walk both until they diverge, then require the tails to match with the
|
||||||
|
single edit applied. *)
|
||||||
|
let i = ref 0 in
|
||||||
|
while !i < la && !i < lb && a.[!i] = b.[!i] do incr i done;
|
||||||
|
let ta s k = String.sub s k (String.length s - k) in
|
||||||
|
if la = lb then
|
||||||
|
!i < la
|
||||||
|
&& (ta a (!i + 1) = ta b (!i + 1)
|
||||||
|
(* stirng/string: two neighbours swapped. *)
|
||||||
|
|| (!i + 1 < la && a.[!i] = b.[!i + 1] && a.[!i + 1] = b.[!i]
|
||||||
|
&& ta a (!i + 2) = ta b (!i + 2)))
|
||||||
|
else if la < lb then ta a !i = ta b (!i + 1)
|
||||||
|
else ta a (!i + 1) = ta b !i
|
||||||
|
end
|
||||||
|
|
||||||
|
(* The same question asked of a candidate list the caller assembles, which for
|
||||||
|
a value position is the function table, the globals and whatever is in
|
||||||
|
scope — and nothing from the type tables, because a name written where a
|
||||||
|
value goes was not a mistyped struct. *)
|
||||||
|
let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
|
||||||
|
|
||||||
|
(* 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
|
||||||
|
point and carries a signature and a sentence per entry for eldoc; a forward
|
||||||
|
reference to its names is cheaper than moving it or writing the list twice
|
||||||
|
and letting the two drift. Filled once, immediately after that table. *)
|
||||||
|
let builtin_names : string list ref = ref []
|
||||||
|
|
||||||
(* What a [break] or a [continue] may be talking about, innermost first.
|
(* What a [break] or a [continue] may be talking about, innermost first.
|
||||||
|
|
||||||
[Lloop] is a loop it is lexically inside, carrying its label if it was given
|
[Lloop] is a loop it is lexically inside, carrying its label if it was given
|
||||||
@ -668,25 +707,6 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
|
|||||||
deletion or a transposition of neighbours. Bounded at one, because two edits
|
deletion or a transposition of neighbours. Bounded at one, because two edits
|
||||||
is no longer a typo, it is a guess. *)
|
is no longer a typo, it is a guess. *)
|
||||||
and near_miss env ?(also = []) 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
|
|
||||||
else begin
|
|
||||||
(* Walk both until they diverge, then require the tails to match with the
|
|
||||||
single edit applied. *)
|
|
||||||
let i = ref 0 in
|
|
||||||
while !i < la && !i < lb && a.[!i] = b.[!i] do incr i done;
|
|
||||||
let ta s k = String.sub s k (String.length s - k) in
|
|
||||||
if la = lb then
|
|
||||||
!i < la
|
|
||||||
&& (ta a (!i + 1) = ta b (!i + 1)
|
|
||||||
(* stirng/string: two neighbours swapped. *)
|
|
||||||
|| (!i + 1 < la && a.[!i] = b.[!i + 1] && a.[!i + 1] = b.[!i]
|
|
||||||
&& ta a (!i + 2) = ta b (!i + 2)))
|
|
||||||
else if la < lb then ta a !i = ta b (!i + 1)
|
|
||||||
else ta a (!i + 1) = ta b !i
|
|
||||||
end
|
|
||||||
in
|
|
||||||
(* [also] widens the candidate list past the types, and exactly one caller
|
(* [also] widens the candidate list past the types, and exactly one caller
|
||||||
passes it: the defvar whose third element has to be a type *or* a value,
|
passes it: the 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. *)
|
whose suggestion is worth nothing if it can only ever name a type. *)
|
||||||
@ -2840,8 +2860,7 @@ and var ctx loc ~want name =
|
|||||||
and pass that" name;
|
and pass that" name;
|
||||||
expect ctx loc ~want
|
expect ctx loc ~want
|
||||||
(mk loc (Types.Fn (params, ret)) (Tast.FnAddr (Tast.Fnval name)))
|
(mk loc (Types.Fn (params, ret)) (Tast.FnAddr (Tast.Fnval name)))
|
||||||
| None -> captured ctx loc name;
|
| None -> captured ctx loc name; unknown_name ctx loc name)
|
||||||
Loc.failk "check/unknown-name" loc "unknown name %s" name)
|
|
||||||
|
|
||||||
(* What remains of spec-memory.md's ownership section after the repeals of
|
(* What remains of spec-memory.md's ownership section after the repeals of
|
||||||
2026-09-18 is the allocator's side alone: the region rule decides where a
|
2026-09-18 is the allocator's side alone: the region rule decides where a
|
||||||
@ -4110,6 +4129,85 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
|
|||||||
question for the layout and not for this — so [.x] is one path and not two,
|
question for the layout and not for this — so [.x] is one path and not two,
|
||||||
and a union member is read with the accessor everything else is read with.
|
and a union member is read with the accessor everything else is read with.
|
||||||
That is the whole of what makes punning ordinary code. *)
|
That is the whole of what makes punning ordinary code. *)
|
||||||
|
(* Every name a value could be standing under here: what is in scope, the
|
||||||
|
globals, the functions — generic ones included, since a call to one is
|
||||||
|
written exactly like a call to any other. No type names: a symbol written
|
||||||
|
where a value goes was not a mistyped struct, and offering one would send
|
||||||
|
the reader to the wrong file. *)
|
||||||
|
and value_candidates ctx =
|
||||||
|
List.map fst ctx.scope
|
||||||
|
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.globals []
|
||||||
|
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.fns []
|
||||||
|
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.gsigs []
|
||||||
|
|
||||||
|
(* The name nothing answers to, refused with whatever this position can still
|
||||||
|
tell the reader.
|
||||||
|
|
||||||
|
Two readings get in ahead of the bare refusal. The first is the dot: [p.x]
|
||||||
|
is how C, Go and Odin spell field access and it is the habit everyone
|
||||||
|
arrives with, so a symbol with a dot in it and a lowercase head is almost
|
||||||
|
never a name — it is an accessor written the way the last language wrote
|
||||||
|
it. The head is looked up, so the sentence can say what [p] actually is
|
||||||
|
rather than guess, and the struct's declaration comes along as a note when
|
||||||
|
there is one. Capitalised heads are left alone: [Shape.Circle] is a real
|
||||||
|
spelling in this language and a typo in one is a mistyped case, not a
|
||||||
|
dot-infix habit.
|
||||||
|
|
||||||
|
The second is the near miss, over values only — see [value_candidates]. *)
|
||||||
|
and unknown_name : 'a. ctx -> Loc.t -> string -> 'a =
|
||||||
|
fun ctx loc name ->
|
||||||
|
let dot = String.index_opt name '.' in
|
||||||
|
let head, field =
|
||||||
|
match dot with
|
||||||
|
| Some i when i > 0 && i + 1 < String.length name ->
|
||||||
|
String.sub name 0 i, String.sub name (i + 1) (String.length name - i - 1)
|
||||||
|
| _ -> "", ""
|
||||||
|
in
|
||||||
|
let lower = head <> "" && head.[0] = Char.lowercase_ascii head.[0]
|
||||||
|
&& head.[0] <> Char.uppercase_ascii head.[0] in
|
||||||
|
if lower then begin
|
||||||
|
let ty =
|
||||||
|
match lookup ctx head with
|
||||||
|
| Some b -> Some b.bty
|
||||||
|
| None -> Option.map fst (Hashtbl.find_opt ctx.env.globals head)
|
||||||
|
in
|
||||||
|
let sname =
|
||||||
|
match ty with
|
||||||
|
| Some (Types.Named n) when fields_named ctx.env n <> None -> Some n
|
||||||
|
| Some (Types.Ptr (Types.Named n)) when fields_named ctx.env n <> None -> Some n
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
match sname, ty with
|
||||||
|
| Some sn, _ ->
|
||||||
|
let s = Option.get (fields_named ctx.env sn) in
|
||||||
|
let notes = declared_note ctx.env sn in
|
||||||
|
if Tast.field_index s field <> None then
|
||||||
|
Loc.failk "check/dot-access" loc ~notes
|
||||||
|
"unknown name %s — a field is read with an accessor, so write (.%s %s)"
|
||||||
|
name field head
|
||||||
|
else
|
||||||
|
Loc.failk "check/dot-access" loc ~notes
|
||||||
|
"unknown name %s — a field is read with an accessor, (.%s %s), and \
|
||||||
|
%s has no field %s"
|
||||||
|
name field head sn field
|
||||||
|
| None, Some t ->
|
||||||
|
Loc.failk "check/dot-access" loc
|
||||||
|
"unknown name %s — a dot is part of the name here, not field access. \
|
||||||
|
Fields are read with an accessor, (.%s %s), and %s is %s, which has \
|
||||||
|
no fields"
|
||||||
|
name field head head (Types.to_string t)
|
||||||
|
| None, None ->
|
||||||
|
Loc.failk "check/unknown-name" loc
|
||||||
|
"unknown name %s — nothing named %s is in scope either. A field is \
|
||||||
|
read with an accessor, (.%s %s), not with a dot"
|
||||||
|
name head field head
|
||||||
|
end
|
||||||
|
else
|
||||||
|
match nearest (value_candidates ctx) name with
|
||||||
|
| Some m ->
|
||||||
|
Loc.failk "check/unknown-name" loc "unknown name %s — did you mean %s?" name m
|
||||||
|
| None -> Loc.failk "check/unknown-name" loc "unknown name %s" name
|
||||||
|
|
||||||
and fields_named env n : Tast.structure option =
|
and fields_named env n : Tast.structure option =
|
||||||
match Hashtbl.find_opt env.structs n with
|
match Hashtbl.find_opt env.structs n with
|
||||||
| Some s -> Some s
|
| Some s -> Some s
|
||||||
@ -4154,8 +4252,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
|
|||||||
match Hashtbl.find_opt ctx.env.globals name with
|
match Hashtbl.find_opt ctx.env.globals name with
|
||||||
| Some (_, true) -> fail loc "%s is a constant" name
|
| Some (_, true) -> fail loc "%s is a constant" name
|
||||||
| Some (ty, false) -> Tast.Pglobal name, ty
|
| Some (ty, false) -> Tast.Pglobal name, ty
|
||||||
| None -> captured ctx loc name;
|
| None -> captured ctx loc name; unknown_name ctx loc name)
|
||||||
Loc.failk "check/unknown-name" loc "unknown name %s" name)
|
|
||||||
| Ast.Pfield (target, name) ->
|
| Ast.Pfield (target, name) ->
|
||||||
let target, sname = struct_target ctx target in
|
let target, sname = struct_target ctx target in
|
||||||
let s = Option.get (fields_named ctx.env sname) in
|
let s = Option.get (fields_named ctx.env sname) in
|
||||||
@ -6380,7 +6477,8 @@ and named_call ctx ~want loc name args =
|
|||||||
| None ->
|
| None ->
|
||||||
if Hashtbl.mem ctx.env.datas name then
|
if Hashtbl.mem ctx.env.datas name then
|
||||||
fail loc
|
fail loc
|
||||||
"%s is a data type — a data type value names the case too, as (%s.%s {.field value ...})"
|
"%s is a data type — a data type value names the case too, as \
|
||||||
|
(%s.%s {.field value ...})"
|
||||||
name name (first_case_name ctx.env name)
|
name name (first_case_name ctx.env name)
|
||||||
else if Hashtbl.mem ctx.env.cases name then
|
else if Hashtbl.mem ctx.env.cases name then
|
||||||
(* [(U.C)] and [(C)]: a case written as a call. Both are how someone
|
(* [(U.C)] and [(C)]: a case written as a call. Both are how someone
|
||||||
@ -6397,7 +6495,26 @@ and named_call ctx ~want loc name args =
|
|||||||
else if String.contains name '/' then
|
else if String.contains name '/' then
|
||||||
unimplemented loc
|
unimplemented loc
|
||||||
(Printf.sprintf "the call %s into an imported package" name) 4
|
(Printf.sprintf "the call %s into an imported package" name) 4
|
||||||
else Loc.failk "check/unknown-function" loc "unknown function %s" name
|
else if args <> []
|
||||||
|
&& name <> "" && name.[0] = Char.uppercase_ascii name.[0]
|
||||||
|
&& name.[0] <> Char.lowercase_ascii name.[0]
|
||||||
|
then
|
||||||
|
(* [(defvar p (Pair i32))]. A capitalised head with arguments is
|
||||||
|
somebody reaching for a parameterised type, which is what the type
|
||||||
|
resolver says about [(Pair i32)] when the same text lands in a type
|
||||||
|
position. Before defvar took either reading, that is the message
|
||||||
|
this text got; it says the same thing here so the answer does not
|
||||||
|
depend on which side of the fork the form fell down. *)
|
||||||
|
Loc.failk "check/unknown-function" loc
|
||||||
|
"unknown function %s. A capitalised name is a type, and a type given \
|
||||||
|
type arguments — (%s ...) — is generic code, which is milestone 5"
|
||||||
|
name name
|
||||||
|
else
|
||||||
|
match nearest (!builtin_names @ value_candidates ctx) name with
|
||||||
|
| Some m ->
|
||||||
|
Loc.failk "check/unknown-function" loc
|
||||||
|
"unknown function %s — did you mean %s?" name m
|
||||||
|
| None -> Loc.failk "check/unknown-function" loc "unknown function %s" name
|
||||||
|
|
||||||
(* ── A call to a generic function ───────────────────────────────────────
|
(* ── A call to a generic function ───────────────────────────────────────
|
||||||
The whole of instantiation, and it is at the call site because the call
|
The whole of instantiation, and it is at the call site because the call
|
||||||
@ -6943,6 +7060,11 @@ let builtins : (string * string * string) list =
|
|||||||
context/allocator.")
|
context/allocator.")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
(* The forward reference declared beside [nearest], filled the moment the table
|
||||||
|
it names exists. Nothing reads it before a call is checked, and no call is
|
||||||
|
checked before this module is loaded. *)
|
||||||
|
let () = builtin_names := List.map (fun (n, _, _) -> n) builtins
|
||||||
|
|
||||||
(* ── Declarations: pass 1, collect ─────────────────────────────────── *)
|
(* ── Declarations: pass 1, collect ─────────────────────────────────── *)
|
||||||
|
|
||||||
(* Constant folding, only over integers and only for defconst — enough for an
|
(* Constant folding, only over integers and only for defconst — enough for an
|
||||||
|
|||||||
@ -1555,12 +1555,14 @@ let () =
|
|||||||
check "the x86 backend roots its dyn values"
|
check "the x86 backend roots its dyn values"
|
||||||
(contains dyn_asm "flan_dyn_root_push"
|
(contains dyn_asm "flan_dyn_root_push"
|
||||||
&& contains dyn_asm "flan_dyn_root_pop");
|
&& contains dyn_asm "flan_dyn_root_pop");
|
||||||
(* The site travels with the operands, on this backend as on the other. A
|
(* The site travels with the operands, on this backend as on the other: a
|
||||||
dyn arithmetic trap is the type error of a dynamic program and it used to
|
dyn arithmetic trap is the type error of a dynamic program, and it used
|
||||||
print with no file and no line; the string literal below is what the
|
to print with no file and no line. This backend writes a string constant
|
||||||
runtime prints as a GNU prefix in front of the sentence. *)
|
as [.byte] hex rather than as text, so the needle is the encoding of the
|
||||||
|
":1:21" that ends the site of the [(+ x y)] above — the path in front of
|
||||||
|
it is the test runner's temporary directory and is not pinnable. *)
|
||||||
check "the x86 backend hands the dyn operators their site"
|
check "the x86 backend hands the dyn operators their site"
|
||||||
(contains dyn_asm "<test>:1:21");
|
(contains dyn_asm "0x3a,0x31,0x3a,0x32,0x31");
|
||||||
(* And a program with no dyn in it emits not one byte of any of it, which is
|
(* And a program with no dyn in it emits not one byte of any of it, which is
|
||||||
what lets the sweep's other MATCHes stand as a regression check on this
|
what lets the sweep's other MATCHes stand as a regression check on this
|
||||||
lane rather than being re-measured by it. *)
|
lane rather than being re-measured by it. *)
|
||||||
@ -1708,6 +1710,45 @@ let () =
|
|||||||
rejects_check "unknown name" "(defn f [] i32 nope)" ~needle:"unknown name";
|
rejects_check "unknown name" "(defn f [] i32 nope)" ~needle:"unknown name";
|
||||||
rejects_check "unknown function" "(defn f [] i32 (nope 1))"
|
rejects_check "unknown function" "(defn f [] i32 (nope 1))"
|
||||||
~needle:"unknown function";
|
~needle:"unknown function";
|
||||||
|
|
||||||
|
(* ── Did-you-mean, and the dot habit ───────────────────────────────
|
||||||
|
[near_miss] was written, tested and wired to the type tables alone, so a
|
||||||
|
mistyped *value* got the bare refusal. The candidate list at a value
|
||||||
|
position is the scope, the globals and the functions — and, at a call,
|
||||||
|
the builtin names, which live in no table the checker keeps. No type
|
||||||
|
names on either list: a symbol written where a value goes was not a
|
||||||
|
mistyped struct. *)
|
||||||
|
rejects_check "a mistyped local is a near miss"
|
||||||
|
"(defn f [] i32 (let [total 1] totl))" ~needle:"did you mean total?";
|
||||||
|
rejects_check "a mistyped defn is a near miss"
|
||||||
|
"(defn helper [x i32] i32 x) (defn f [] i32 (helpr 1))"
|
||||||
|
~needle:"unknown function helpr — did you mean helper?";
|
||||||
|
rejects_check "a mistyped builtin is a near miss"
|
||||||
|
"(defn f [] () (prinltn \"hi\"))"
|
||||||
|
~needle:"unknown function prinltn — did you mean println?";
|
||||||
|
(* [p.x] is the habit from C, Go and Odin, and the checker can see exactly
|
||||||
|
what the head is, so the refusal names the accessor rather than reporting
|
||||||
|
a name nobody wrote. The declaration comes along as a note, which is
|
||||||
|
[declared_note]'s shape. *)
|
||||||
|
rejects_check "dot-infix field access names the accessor"
|
||||||
|
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] p.x))"
|
||||||
|
~needle:"a field is read with an accessor, so write (.x p)";
|
||||||
|
rejects_check "and says so when the field is not there either"
|
||||||
|
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] p.z))"
|
||||||
|
~needle:"(.z p), and P has no field z";
|
||||||
|
rejects_check "a dotted head that is not a struct says what it is"
|
||||||
|
"(defn f [] i32 (let [n 1] n.x))" ~needle:"n is i32, which has no fields";
|
||||||
|
(* A capitalised head keeps the case spelling it always had: [Shape.Circle]
|
||||||
|
is real here, so a typo in one is not the dot habit. *)
|
||||||
|
rejects_check "a capitalised dotted name is still a case"
|
||||||
|
"(data Shape (Circle [r f64])) (defn f [] Shape Shape.Crcle)"
|
||||||
|
~needle:"unknown";
|
||||||
|
(* [(Pair i32)] in a defvar falls down the value fork now that the third
|
||||||
|
element takes either reading, and the generics answer the type fork gave
|
||||||
|
it has to be reachable from here too. *)
|
||||||
|
rejects_check "a capitalised call with arguments is generics"
|
||||||
|
"(defvar x (Pair i32)) (defn f [] i32 0)"
|
||||||
|
~needle:"is generic code, which is milestone 5";
|
||||||
rejects_check "defined twice" "(defn f [] ()) (defn f [] ())"
|
rejects_check "defined twice" "(defn f [] ()) (defn f [] ())"
|
||||||
~needle:"defined twice";
|
~needle:"defined twice";
|
||||||
accepts "main with no parameters and no return" "(defn main [] ())";
|
accepts "main with no parameters and no return" "(defn main [] ())";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user