diff --git a/FIX.org b/FIX.org index 58df760..c81fd8f 100644 --- a/FIX.org +++ b/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. diff --git a/lib/check.ml b/lib/check.ml index 3d0b36c..10dd51d 100644 --- a/lib/check.ml +++ b/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 - match nearest (!builtin_names @ value_candidates ctx) name with + (* 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 diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 9268c5b..062a821 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -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)) { diff --git a/test/programs/dyn-trap-site.flan b/test/programs/dyn-trap-site.flan new file mode 100644 index 0000000..726b0ae --- /dev/null +++ b/test/programs/dyn-trap-site.flan @@ -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")) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index bcc7366..0a96d0e 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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 diff --git a/test/test_flan.ml b/test/test_flan.ml index a95d824..2398da8 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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";