A defn with no return type says so, and an operator spelled as another language spells it names Flan's

This commit is contained in:
Joseph Ferano 2026-09-25 11:57:09 +07:00
commit 9bb1bde665
2 changed files with 166 additions and 0 deletions

View File

@ -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" -> 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)" (String.concat " " (flan :: 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,53 @@ 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, 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)
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 +9463,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 +11097,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

View File

@ -2446,6 +2446,69 @@ 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)";
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))";
(* ── 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 "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"
"(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"