The review's fixes: a suggestion that does not compile, and a confident wrong guess
F1 was the blocker and it was the worst kind of fault this pass can have: the condition message told the reader to write (not= x 0), and not= does not exist — the operator is !=. Applying the compiler's own advice got 'unknown function not= — did you mean not?'. Both branches say != now, and all three — the named form, the float zero, and the unnamed one — were checked by compiling the sentence the compiler prints. F5: a typo of a declared capitalised name got the generics lecture. (Piont 1 2) with Point declared was told that a capitalised name given type arguments is milestone 5 work, which is a confident answer about a feature nobody was reaching for. The did-you-mean runs first and, for a capitalised head only, asks the type tables as well; the generics sentence is left for a head that resembles nothing. F2: flan_dyn_cast_kind had the site live and passed NULL on the trapping path — the one entry point on this side that had a location and threw it away. The acceptance row now pins the prefix it prints. F3: the case-typo row used (data ...), which is not a top-level form, so it refused as an unknown top-level form and the needle 'unknown' matched that rather than the rule. Rewritten with defdata, and as a pair: a capitalised head gets no accessor advice, a lowercase one does. Both halves were checked to fail when perturbed. F4: an end-to-end pin for the headline. programs/dyn-trap-site.flan is compiled, run, and its stderr read for the file:line:col in front of the sentence, on both backends and at -O0. Proven live: three failures when the expected line is wrong. F8: usize and size_t stay off the foreign-spelling list, and the comment now says why — the honest answer is pointer-width, which is u64 here and u32 on wasm32, and a tree that builds both cannot name one of them. F10 pins the fourth dot shape. F6 moves the not-reached reasons out of the commit bodies and into FIX.org, where they can be read without git.
This commit is contained in:
parent
16f90a7ef5
commit
78d9a0f051
63
FIX.org
63
FIX.org
@ -1598,3 +1598,66 @@ because the code is the same.
|
||||
Not fixed here, deliberately: the fix is to catch ~Closed~ in that poll and
|
||||
read it as the program having ended, which is a claim about what those rows
|
||||
mean and belongs to whoever owns them. Flagged rather than patched.
|
||||
|
||||
* The diagnostics pass, 2026-09-20
|
||||
|
||||
Worked from ~docs/DIAGNOSTICS-AUDIT.md~, which is tracked as of this lane's
|
||||
first commit. Graded against the contract the audit sets out: show the code
|
||||
with the caret, say what was understood, say what conflicts, name the fix.
|
||||
|
||||
** Reached
|
||||
Worst-20 ranks 1, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||
and the runtime half of 2. All four defvar follow-ups. Two from the author's
|
||||
dogfooding notes in DISCUSS.org: the foreign-spelling list, which answers
|
||||
~int~ with ~i32~ instead of a lecture about type variables, and the
|
||||
two-element ~defconst~ whose bracketed type read as an array literal.
|
||||
|
||||
** Not reached, each with the reason
|
||||
|
||||
*** Rank 8: ~unhandled Boom~ has no location
|
||||
Not a copy of the dyn-trap work, and the difference is the calling
|
||||
convention. ~flan_error~ takes five integer arguments — the type id, the
|
||||
condition, the channel, and the name as ptr+len — which is rdi through r8.
|
||||
A ~(loc, loclen)~ pair makes seven, past x86-64's six argument registers, so
|
||||
~lib/x86.ml~ would need stack-argument passing at a call site whose own
|
||||
comment two hundred lines up says "the channel lands in r9 and the register
|
||||
file is exactly full". The dyn entry points took the pair without any of
|
||||
that because none of them was near the limit.
|
||||
|
||||
The rest of rank 8 — the condition's field values, and the handlers that
|
||||
were in scope — is separate work again and has no ABI question in it.
|
||||
|
||||
The audit's gap 4 names a dev-side half of this: ~flan_trap_hook~ hands
|
||||
control to a session that is in-process with the compiler and *can* read the
|
||||
source, so a real caret at runtime belongs in ~lib/dev.ml~. That file is
|
||||
another lane's and the audit already wrote it up as a hand-off.
|
||||
|
||||
*** Rank 20: the fn-literal arity message
|
||||
Re-read and judged already satisfying. The audit asks it to name the
|
||||
parameter list it was measured against; it prints the whole ~(Fn [T ...] R)~,
|
||||
which is that list. Left alone rather than churned.
|
||||
|
||||
*** ~trap_oom~ in flan_dyn.c
|
||||
The other three trap printers took the location pair. This one is reached
|
||||
from ~gc_alloc~, which has no site to be given: every allocation path in the
|
||||
file would have had to carry one for a sentence that is about the host
|
||||
refusing memory rather than about the program. ~trap_range~ has the pair and
|
||||
every caller passes NULL, so giving ~at~, ~set-at~ and ~push~ a site later is
|
||||
a call-site change and not another round of signature churn.
|
||||
|
||||
*** The audit's structural gaps 3, 5 and 6
|
||||
Printing the stable ~kind~ at the end of the first line, the
|
||||
"understood / conflicted" clause order as a writing rule, and non-cascading
|
||||
multiple errors through ~Loc.sink~. Each needs a decision from the author
|
||||
rather than work, which is what the audit says about them too.
|
||||
|
||||
** Two behaviour changes, not only wording
|
||||
~(defn idx [v i] dyn v)~ was *refused* and now compiles as two dyn
|
||||
parameters. The rule is the digits: this language sizes its machine types in
|
||||
the name, so a typo keeps them — ~f65~, ~i33~ — and a parameter called ~i~ or
|
||||
~n~ has none. ~pair_params~'s own comment says that is what the feature is.
|
||||
|
||||
A ~defn~ whose name is a builtin's is still not refused. The builtin still
|
||||
wins every call and the definition is still unreachable; what changed is that
|
||||
the arity message says so and notes the definition. Refusing the shadowing is
|
||||
a language decision and was left to the author.
|
||||
|
||||
58
lib/check.ml
58
lib/check.ml
@ -260,7 +260,11 @@ let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
|
||||
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. *)
|
||||
this one's. Nor [usize] and [size_t]: the honest answer is "as wide as a
|
||||
pointer on this target", which is [u64] on x86-64 and [u32] on wasm32, and
|
||||
a message that named one of them would be wrong half the time on a tree
|
||||
that builds both. A name goes on this list when the answer does not depend
|
||||
on anything. *)
|
||||
let foreign_spelling = function
|
||||
| "int" | "integer" -> Some "i32"
|
||||
| "uint" | "unsigned" -> Some "u32"
|
||||
@ -3915,7 +3919,7 @@ and check_truthy ctx c =
|
||||
language. It is said only of the typed side, which is where they are.
|
||||
|
||||
The comparison is spelled with the condition's own name where there is
|
||||
one, because [(not= x 0)] is a thing to type and [(not= … 0)] is not.
|
||||
one, because [(!= x 0)] is a thing to type and [(!= … 0)] is not.
|
||||
Anything more complicated than a name gets the operator and no
|
||||
template: a reconstructed expression would be a guess at code the
|
||||
reader can see for themselves. *)
|
||||
@ -3930,8 +3934,8 @@ and check_truthy ctx c =
|
||||
match c0.Tast.ty with Types.Int _ | Types.Float _ -> true | _ -> false
|
||||
in
|
||||
match c.Ast.e, comparable with
|
||||
| Ast.Var n, true -> Printf.sprintf " — test it, as (not= %s %s)" n zero
|
||||
| _, true -> Printf.sprintf " — test it against %s with not=" zero
|
||||
| Ast.Var n, true -> Printf.sprintf " — test it, as (!= %s %s)" n zero
|
||||
| _, true -> Printf.sprintf " — test it against %s with !=" zero
|
||||
| _ -> ""
|
||||
in
|
||||
Loc.failk "check/condition-not-bool" loc
|
||||
@ -6881,26 +6885,42 @@ and named_call ctx ~want loc name args =
|
||||
else if String.contains name '/' then
|
||||
unimplemented loc
|
||||
(Printf.sprintf "the call %s into an imported package" name) 4
|
||||
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
|
||||
(* The did-you-mean comes first, and for a capitalised head it is asked
|
||||
of the *type* tables as well: [(Piont 1 2)] with [Point] declared is
|
||||
a typo, and the generics sentence below would be a confident answer
|
||||
about a feature nobody was reaching for. Only a capitalised head
|
||||
consults the types — a lowercase name written where a value goes
|
||||
was not a mistyped struct, which is [value_candidates]' whole
|
||||
point. *)
|
||||
let capitalised =
|
||||
name <> "" && name.[0] = Char.uppercase_ascii name.[0]
|
||||
&& name.[0] <> Char.lowercase_ascii name.[0]
|
||||
in
|
||||
let guess =
|
||||
match nearest (!builtin_names @ value_candidates ctx) name with
|
||||
| Some _ as m -> m
|
||||
| None -> if capitalised then near_miss ctx.env name else None
|
||||
in
|
||||
match guess 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
|
||||
| None ->
|
||||
if args <> [] && capitalised then
|
||||
(* [(defvar p (Pair i32))]. A capitalised head with arguments and
|
||||
no near miss anywhere 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 Loc.failk "check/unknown-function" loc "unknown function %s" name
|
||||
|
||||
(* ── A call to a generic function ───────────────────────────────────────
|
||||
The whole of instantiation, and it is at the call site because the call
|
||||
|
||||
@ -1330,7 +1330,10 @@ int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
|
||||
: sizeof name - 1;
|
||||
memcpy(name, target, n);
|
||||
name[n] = '\0';
|
||||
trap1(NULL, 0, TYPE_TRAP, name, "a number was wanted", v);
|
||||
/* This one has a site: the cast's own, which the emitter already hands
|
||||
* over for the cross-kind warning below. It was the first entry point on
|
||||
* this side to take a location and it was not passing it on. */
|
||||
trap1(loc, loc_len, TYPE_TRAP, name, "a number was wanted", v);
|
||||
}
|
||||
int32_t is_float = tag == FLAN_DYN_TAG_FLOAT ? 1 : 0;
|
||||
if (is_float != (want_float ? 1 : 0) && site_first_time(loc, loc_len)) {
|
||||
|
||||
28
test/programs/dyn-trap-site.flan
Normal file
28
test/programs/dyn-trap-site.flan
Normal file
@ -0,0 +1,28 @@
|
||||
;;;; A dyn arithmetic trap says where it happened.
|
||||
;;;;
|
||||
;;;; In a dynamic-first language the dyn traps ARE the type errors, and until
|
||||
;;;; the diagnostics pass they printed with no file, no line and no column:
|
||||
;;;;
|
||||
;;;; dyn +: int and text, and it takes two numbers — (+ 3 "hi")
|
||||
;;;;
|
||||
;;;; flan_rt.c's bounds and arithmetic traps have taken an emitter-threaded
|
||||
;;;; (loc, loclen) pair since they were written, so the ABI precedent was
|
||||
;;;; already there; the five arithmetic and four ordering entry points in
|
||||
;;;; flan_dyn.c simply were never given one. They take it now, and the trap
|
||||
;;;; prints it as the GNU "file:line:col: " prefix, which is what makes
|
||||
;;;; next-error walk to a dyn failure the way it walks to a bounds failure.
|
||||
;;;;
|
||||
;;;; This program exists for the prefix and for nothing else. The line prints
|
||||
;;;; first so that the test can tell "the program ran and then trapped" from
|
||||
;;;; "the program did not start", and the operation is inside a defn so that
|
||||
;;;; the site reported is the operator's own and not the call's — which is the
|
||||
;;;; distinction that matters: the + is what failed, and the + is what the
|
||||
;;;; caret should be under.
|
||||
|
||||
(defn add [x y] dyn
|
||||
(+ x y))
|
||||
|
||||
(defn main [] ()
|
||||
(print "before\n")
|
||||
(print (add 3 "hi"))
|
||||
(print "unreachable\n"))
|
||||
@ -3788,6 +3788,45 @@ level "1"
|
||||
some_nil ~opt:"-O0" ();
|
||||
some_nil ~x86:true ();
|
||||
|
||||
(* The dyn trap's own location, end to end: compiled, run, and read off
|
||||
stderr. In a dynamic-first language these traps are the type errors,
|
||||
and they printed with no file and no line at all. The site is a string
|
||||
literal the emitter hands over exactly as it hands [flan_dyn_cast_kind]
|
||||
its own, so this is asserted on both backends and at -O0: the argument
|
||||
is an ordinary one and neither backend treats it specially, which is
|
||||
the claim being pinned.
|
||||
|
||||
23:3 is the (+ x y) inside [add], not the (add 3 "hi") that called it,
|
||||
and that is the point of the site being the operator's: the + is what
|
||||
failed. If the file is edited above line 23 this number moves. *)
|
||||
let trap_site_out = "before\n" in
|
||||
let trap_site ?opt ?x86 () =
|
||||
let exe = compile ?opt ?x86 "programs/dyn-trap-site.flan" in
|
||||
let code, text = run exe None in
|
||||
let name =
|
||||
"dyn: an arithmetic trap says where"
|
||||
^ (match opt with Some o -> ", " ^ o | None -> "")
|
||||
^ (match x86 with Some true -> ", --x86" | _ -> "")
|
||||
in
|
||||
if code <> 134
|
||||
|| not (contains text trap_site_out)
|
||||
|| not (contains text
|
||||
"dyn-trap-site.flan:23:3: dyn +: int and text, and it \
|
||||
takes two numbers — (+ 3 \"hi\")")
|
||||
|| contains text "unreachable"
|
||||
then begin
|
||||
incr failures;
|
||||
Printf.printf
|
||||
"FAIL %s\n got: %S (exit %d)\n wanted: %S then the trap \
|
||||
with its site (exit 134)\n"
|
||||
name text code trap_site_out
|
||||
end;
|
||||
(try Sys.remove exe with Sys_error _ -> ())
|
||||
in
|
||||
trap_site ();
|
||||
trap_site ~opt:"-O0" ();
|
||||
trap_site ~x86:true ();
|
||||
|
||||
(* A numeric cast opening a dyn box — FIX.org 2026-09-20.
|
||||
programs/dyn-cast.flan is one program because the three behaviours are
|
||||
one story told in order: the same-kind casts print, the cross-kind ones
|
||||
@ -3845,8 +3884,14 @@ level "1"
|
||||
|| not (contains text "found a dyn holding an int, and converted it to f64")
|
||||
|| not (contains text "found a dyn holding a float, and converted it to i64")
|
||||
|| not (contains text "found a dyn holding an int, and converted it to f32")
|
||||
(* The non-numeric box, in the runtime's own words. *)
|
||||
|| not (contains text "bool, and a number was wanted")
|
||||
(* The non-numeric box, in the runtime's own words — and with the
|
||||
site in front of them. [flan_dyn_cast_kind] has taken the cast's
|
||||
location since the warning below needed one, and was the one entry
|
||||
point on the dyn side that had a location and threw it away on the
|
||||
trapping path. 71:10 is the (i64 (as-dyn true)) at the end of
|
||||
the program; if that file is edited above it, this number moves. *)
|
||||
|| not (contains text "dyn-cast.flan:71:10: dyn i64: bool, and a \
|
||||
number was wanted")
|
||||
then begin
|
||||
incr failures;
|
||||
Printf.printf
|
||||
|
||||
@ -1746,11 +1746,39 @@ let () =
|
||||
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] (set (.x p) 2) (.x p)))";
|
||||
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";
|
||||
(* The fourth shape: nothing is bound under the head either, so the message
|
||||
claims nothing about what q is — only that the dot is not the operator
|
||||
the writer took it for. *)
|
||||
rejects_check "and an unbound head claims nothing about it"
|
||||
"(defn f [] i32 q.x)"
|
||||
~needle:"unknown name q.x — nothing named q is in scope either. A field \
|
||||
is reached through an accessor, (.x q), not with a dot";
|
||||
(* 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";
|
||||
(* Both sides of the rule, because only the pair says what it is. A
|
||||
capitalised head is a real spelling here — Shape.Circle — so a typo in
|
||||
one is a mistyped case and gets none of the accessor advice; the same
|
||||
text with a lowercase head does. The earlier spelling of this row used
|
||||
(data ...), which is not a top-level form at all, so it refused as an
|
||||
unknown top-level form and the needle "unknown" matched that instead of
|
||||
anything this rule does. *)
|
||||
(match (try ignore (checked "(defdata Shape [(Circle [r f64])]) \
|
||||
(defn f [] Shape Shape.Crcle)"); None
|
||||
with Loc.Error d -> Some d) with
|
||||
| Some d ->
|
||||
check "a capitalised dotted name gets no accessor advice"
|
||||
(contains d.Loc.dmsg "unknown name Shape.Crcle"
|
||||
&& not (contains d.Loc.dmsg "accessor"))
|
||||
| None -> check "a mistyped case is refused" false);
|
||||
(match (try ignore (checked "(defdata Shape [(Circle [r f64])]) \
|
||||
(defn f [] Shape shape.Crcle)"); None
|
||||
with Loc.Error d -> Some d) with
|
||||
| Some d ->
|
||||
check "and a lowercase one does"
|
||||
(contains d.Loc.dmsg
|
||||
"nothing named shape is in scope either. A field is reached through \
|
||||
an accessor, (.Crcle shape), not with a dot")
|
||||
| None -> check "a lowercase dotted name is refused" false);
|
||||
(* [(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. *)
|
||||
@ -3790,10 +3818,10 @@ let () =
|
||||
is right, and with the condition's own name where it has one. *)
|
||||
rejects_check "a non-bool condition states the rule"
|
||||
"(defn f [] i32 (let [x 1] (if x 1 0)))"
|
||||
~needle:"a condition is a bool or a dyn, and this is i32 — test it, as (not= x 0)";
|
||||
~needle:"a condition is a bool or a dyn, and this is i32 — test it, as (!= x 0)";
|
||||
rejects_check "and offers no template for a form it cannot name"
|
||||
"(defn f [] i32 (if (+ 1 2) 1 0))"
|
||||
~needle:"this is i32 — test it against 0 with not=";
|
||||
~needle:"this is i32 — test it against 0 with !=";
|
||||
rejects_check "and offers no comparison at all for a type that has none"
|
||||
"(defstruct P [x i32]) (defn f [] i32 (let [p (P {.x 1})] (if p 1 0)))"
|
||||
~needle:"a condition is a bool or a dyn, and this is P";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user