From d6217830ffb982301f7cfcfdf3b908bd871c26be Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 11:50:23 +0700 Subject: [PATCH 1/2] A defn whose body stands in its return slot is told it has no return type, a misspelled type constructor is a near miss, and not=, ==, &&, || and their kin name the Flan builtin that computes the same thing --- lib/check.ml | 84 +++++++++++++++++++++++++++++++++++++++++++++++ test/test_flan.ml | 49 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/lib/check.ml b/lib/check.ml index 4d6112fc..d6e43213 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -407,6 +407,37 @@ let spell_arg stand_for (a : Ast.expr) = | Ast.UInt (_, s) -> s | _ -> stand_for +(* Operators other languages spell differently, each mapped to the Flan + builtin that computes the same thing. Only exact equivalents: [mod] is left + out because Clojure's is floored and [%] is not. *) +let operator_aliases = + [ ("not=", ("!=", "Not-equal")); ("=/=", ("!=", "Not-equal")); + ("/=", ("!=", "Not-equal")); ("<>", ("!=", "Not-equal")); + ("==", ("=", "Equality")); ("===", ("=", "Equality")); + ("&&", ("and", "Logical and")); ("||", ("or", "Logical or")); + ("!", ("not", "Logical not")) ] + +(* The fix, as the sentence that ends the refusal. The reader's call is + written back out under the Flan name only when every argument can be + spelled and the count is one the builtin takes, so a suggestion printed as + code compiles once pasted. An argument that cannot be spelled leaves the + call as it was, with only the name to change; a count the builtin does not + take gets the builtin's shape. *) +let alias_fix flan (args : Ast.expr list) = + let spelled = List.map (spell_arg "") args in + let n = List.length args in + let arity_ok = + match flan with + | "not" -> n = 1 + | "and" | "or" -> n >= 1 + | _ -> n >= 2 + in + if not arity_ok then + Printf.sprintf "It is called as %s" + (if flan = "not" then "(not x)" else "(" ^ flan ^ " x y)") + else if List.mem "" spelled then Printf.sprintf "Write %s in its place" flan + else Printf.sprintf "Write (%s %s)" flan (String.concat " " spelled) + (* 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 @@ -1212,6 +1243,21 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = [Types.t] and therefore to the layout calculator, both backends, [Render] and the DWARF path. docs/SPIKE-GENERICS.md, question 4, prices it and leaves it out. *) + (* A head that is not a type at all but one edit from one is the typo + [(Vect i32)], and the generics sentence would answer a question + nobody asked. *) + let constructors = [ "Ptr"; "Option"; "Vec"; "Map" ] in + (match + if Hashtbl.mem env.aliases name || Hashtbl.mem env.structs name + || Hashtbl.mem env.datas name || Hashtbl.mem env.unions name + || Hashtbl.mem env.enums name + then None + else near_miss env ~also:constructors name + with + | Some m when List.mem m constructors -> + Loc.failk "check/unknown-type" loc + "unknown type %s — did you mean %s?" name m + | _ -> ()); fail loc "%s takes no type arguments. A generic function is written with $t \ in its parameter vector; a generic type is not there yet" @@ -1411,6 +1457,34 @@ let is_type_name env n = slot holding one is a type however few of them there are. *) || (n <> "" && n.[0] = '$') +(* A defn written without its return type puts the body's first form in the + slot, and [(dotimes [i n] ...)] parses as a type application. Every type + application's head is a constructor, and a constructor is capitalised, so a + lowercase head there — or the name of a function — is a body form and not a + malformed type. A capitalised head that is neither stays with [resolve], + whose unknown-type and no-type-arguments sentences are the right ones for + [(Vect i32)] and [(Pair i32)]. *) +let missing_return_type env (fn : Ast.fn) = + match fn.Ast.ret with + | Some { Ast.t = Ast.Tapp (head, _); tloc } -> + let lowercase = + head <> "" && not (head.[0] >= 'A' && head.[0] <= 'Z') + in + let constructor = + List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ] + in + if (not constructor) && (not (is_type_name env head)) + && (lowercase || Hashtbl.mem env.fns head + || List.mem head !builtin_names) + then + Loc.failk "check/return-type-missing" tloc + "%s has no return type: (%s ...) stands where the return type goes, \ + and %s is not a type. The return type is written between the \ + parameter vector and the body, and a function that returns nothing \ + writes () there" + fn.Ast.name head head + | _ -> () + (* Before a bare symbol is allowed to become an unannotated parameter, the two ways it is more likely to be a type that went wrong. @@ -9370,6 +9444,15 @@ and ordinary_call ctx ~want loc name args = name dname dname c.Tast.vname dname c.Tast.vname else if Hashtbl.mem ctx.env.structs name then positional_struct ctx ~want loc name args + else if List.mem_assoc name operator_aliases then + (* Asked before the package test, because [/=] and [=/=] have a slash + in them and are not package calls. The did-you-mean cannot reach + these: [not=] is one edit from [not], which is the wrong answer, + and [&&] is no edit at all from [and]. *) + let flan, what = List.assoc name operator_aliases in + Loc.failk "check/unknown-function" loc + "there is no %s. %s is %s. %s" name what flan + (alias_fix flan args) else if String.contains name '/' then unimplemented loc (Printf.sprintf "the call %s into an imported package" name) 4 @@ -10995,6 +11078,7 @@ let collect env (decls : Ast.decl list) = let fields = List.map field ms in Hashtbl.replace env.unions n { Tast.sname = n; fields } | Ast.Defn fn -> + missing_return_type env fn; (* A signature that introduces a type variable is a *pattern*, not a signature: it goes in [gsigs] and the function goes nowhere near [fns], because nothing can be called at [t]. Every call site turns diff --git a/test/test_flan.ml b/test/test_flan.ml index c2c11843..8af379c2 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2446,6 +2446,55 @@ let () = reach past anything. Four claims: the call is refused, the refusal says what to write instead, the name binds in every position, and a defn under it earns no shadowing warning. *) + (* ── Operators spelled as other languages spell them ─────────────── *) + rejects_check "not= names !=" + "(defn f [a i32 b i32] bool (not= a b))" + ~needle:"there is no not=. Not-equal is !=. Write (!= a b)"; + accepts "and the call it writes compiles" + "(defn f [a i32 b i32] bool (!= a b))"; + rejects_check "not= over calls says which name to change" + "(defn f [a i32 b i32] bool (not= (+ a 1) b))" + ~needle:"Write != in its place"; + rejects_check "/= is not a package call" + "(defn f [a i32 b i32] bool (/= a b))" ~needle:"Write (!= a b)"; + rejects_check "=/= is not a package call" + "(defn f [a i32 b i32] bool (=/= a b))" ~needle:"Write (!= a b)"; + rejects_check "== names =" + "(defn f [a i32 b i32] bool (== a b))" ~needle:"Write (= a b)"; + rejects_check "&& names and" + "(defn f [a bool b bool] bool (&& a b))" ~needle:"Write (and a b)"; + accepts "and that call compiles" "(defn f [a bool b bool] bool (and a b))"; + rejects_check "|| names or" + "(defn f [a bool b bool] bool (|| a b))" ~needle:"Write (or a b)"; + rejects_check "! names not" + "(defn f [a bool] bool (! a))" ~needle:"Write (not a)"; + rejects_check "a ! at an arity not does not take gets not's shape" + "(defn f [a bool b bool] bool (! a b))" ~needle:"called as (not x)"; + accepts "a program's own not= is its own" + "(defn not= [a i32 b i32] bool (!= a b)) \ + (defn f [a i32 b i32] bool (not= a b))"; + + (* ── A defn with no return type ─────────────────────────────────── + The body's first form lands in the return slot and parses as a type + application; the refusal names the missing return type rather than the + form's head. *) + rejects_check "a body form in the return slot is a missing return type" + "(defn f [s string] (dotimes [i (length s)] (println i)))" + ~needle:"f has no return type: (dotimes ...) stands where the return type goes"; + rejects_check "and says how a function returning nothing is written" + "(defn f [a i32 b i32] (+ a b))" + ~needle:"a function that returns nothing writes () there"; + accepts "the fix compiles" + "(defn f [s string] () (dotimes [i (length s)] (println i)))"; + accepts "a type application in the slot is still a type" + "(defn f [] (Option i32) None)"; + rejects_check "a misspelled constructor is a near miss" + "(defn f [] (Optoin i32) None)" ~needle:"unknown type Optoin — did you mean Option?"; + rejects_check "an unknown capitalised head keeps the generics sentence" + "(defn f [] (Pair i32) 0)" ~needle:"Pair takes no type arguments"; + rejects_check "a misspelled plain return type is still an unknown type" + "(defn f [] i3 0)" ~needle:"unknown type i3"; + rejects_check "len is not a builtin" "(defn f [s [i32]] i32 (len s))" ~needle:"there is no len"; rejects_check "and the refusal writes the call out" From f6291235619ceb0c9044863508ee0eecd776c2f5 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 11:56:56 +0700 Subject: [PATCH 2/2] A lowercase vec, ptr, option or map over types in a return slot is a near miss for its constructor, and a bare && is written back as the (and) that compiles --- lib/check.ml | 25 ++++++++++++++++++++++--- test/test_flan.ml | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index d6e43213..9ae65a8d 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -429,14 +429,14 @@ let alias_fix flan (args : Ast.expr list) = let arity_ok = match flan with | "not" -> n = 1 - | "and" | "or" -> n >= 1 + | "and" | "or" -> true | _ -> n >= 2 in if not arity_ok then Printf.sprintf "It is called as %s" (if flan = "not" then "(not x)" else "(" ^ flan ^ " x y)") else if List.mem "" spelled then Printf.sprintf "Write %s in its place" flan - else Printf.sprintf "Write (%s %s)" flan (String.concat " " spelled) + else Printf.sprintf "Write (%s)" (String.concat " " (flan :: spelled)) (* What a [break] or a [continue] may be talking about, innermost first. @@ -1466,13 +1466,32 @@ let is_type_name env n = [(Vect i32)] and [(Pair i32)]. *) let missing_return_type env (fn : Ast.fn) = match fn.Ast.ret with - | Some { Ast.t = Ast.Tapp (head, _); tloc } -> + | Some { Ast.t = Ast.Tapp (head, args); tloc } -> let lowercase = head <> "" && not (head.[0] >= 'A' && head.[0] <= 'Z') in let constructor = List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ] in + (* [(vec i32)] is the constructor with the wrong case, not a body form — + but only while every argument is a type, since [(map inc xs)] is a + body form whose head is a function. *) + let args_are_types = + List.for_all + (fun (a : Ast.texpr) -> + match a.Ast.t with Ast.Tname n -> is_type_name env n | _ -> true) + args + in + (match + List.find_opt + (fun c -> String.lowercase_ascii c = String.lowercase_ascii head + && c <> head) + [ "Ptr"; "Option"; "Vec"; "Map" ] + with + | Some c when args_are_types && not (is_type_name env head) -> + Loc.failk "check/unknown-type" tloc + "unknown type %s — did you mean %s?" head c + | _ -> ()); if (not constructor) && (not (is_type_name env head)) && (lowercase || Hashtbl.mem env.fns head || List.mem head !builtin_names) diff --git a/test/test_flan.ml b/test/test_flan.ml index 8af379c2..af208316 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2470,6 +2470,9 @@ let () = "(defn f [a bool] bool (! a))" ~needle:"Write (not a)"; rejects_check "a ! at an arity not does not take gets not's shape" "(defn f [a bool b bool] bool (! a b))" ~needle:"called as (not x)"; + rejects_check "a bare && is written back as the and that compiles" + "(defn f [] bool (&&))" ~needle:"Write (and)"; + accepts "and it does" "(defn f [] bool (and))"; accepts "a program's own not= is its own" "(defn not= [a i32 b i32] bool (!= a b)) \ (defn f [a i32 b i32] bool (not= a b))"; @@ -2490,6 +2493,17 @@ let () = "(defn f [] (Option i32) None)"; rejects_check "a misspelled constructor is a near miss" "(defn f [] (Optoin i32) None)" ~needle:"unknown type Optoin — did you mean Option?"; + rejects_check "a lowercase vec is Vec" + "(defn f [] (vec i32) (vec-new i32))" ~needle:"unknown type vec — did you mean Vec?"; + rejects_check "a lowercase ptr is Ptr" + "(defn f [p (Ptr i32)] (ptr i32) p)" ~needle:"unknown type ptr — did you mean Ptr?"; + rejects_check "a lowercase option is Option" + "(defn f [] (option i32) None)" ~needle:"unknown type option — did you mean Option?"; + rejects_check "a lowercase map is Map" + "(defn f [] (map i32 i32) (map-new i32 i32))" + ~needle:"unknown type map — did you mean Map?"; + rejects_check "a map over values in the slot is a body form" + "(defn f [inc i32 xs i32] (map inc xs))" ~needle:"f has no return type"; rejects_check "an unknown capitalised head keeps the generics sentence" "(defn f [] (Pair i32) 0)" ~needle:"Pair takes no type arguments"; rejects_check "a misspelled plain return type is still an unknown type"