The's refusal of a dyn says what the boundary does at that type, elements that cannot become a dyn are refused against each other, two literal arms meet at the wider type, and the renamed prelude function and a second startup warning stay out of sight

This commit is contained in:
Joseph Ferano 2026-09-25 12:49:02 +07:00
parent aedd50c646
commit 35f455acb3
6 changed files with 189 additions and 14 deletions

View File

@ -362,6 +362,7 @@ let () =
p.globals; p.globals;
List.iter List.iter
(fun (f : Flan.Tast.fn) -> (fun (f : Flan.Tast.fn) ->
if not (Flan.Check.internal_name f.name) then
Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name
(String.concat " " (String.concat " "
(List.map Flan.Types.to_string f.params)) (List.map Flan.Types.to_string f.params))

View File

@ -5338,6 +5338,17 @@ and check_if ctx ?(tail = false) ?want loc c t e =
no value on the missing side. `when` desugars to this. *) no value on the missing side. `when` desugars to this. *)
let t = branch ctx (fun () -> in_tail (fun () -> check ctx t)) in let t = branch ctx (fun () -> in_tail (fun () -> check ctx t)) in
expect ctx loc ~want (mk loc Types.Unit (Tast.If (c, t, unit_at loc))) expect ctx loc ~want (mk loc Types.Unit (Tast.If (c, t, unit_at loc)))
(* Two literal arms meet at the wider of their own types, as two literal
elements of an array do: [(if c 1 2.5)] is an f64. *)
| Some e
when want = None && lone_literal t && lone_literal e
&& (match literal_join ctx t e with
| Some j -> not (Types.equal j (Types.Int Types.I32))
| None -> false) ->
let want = literal_join ctx t e in
let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in
let e = branch ctx (fun () -> in_tail (fun () -> check ctx ?want e)) in
mk loc t.Tast.ty (Tast.If (c, t, e))
| Some e when want = None && lone_literal t && not (lone_literal e) | Some e when want = None && lone_literal t && not (lone_literal e)
&& not (and_sentinel e) -> && not (and_sentinel e) ->
(* A literal has no type of its own until something asks, so with no (* A literal has no type of its own until something asks, so with no
@ -5392,6 +5403,18 @@ and check_if ctx ?(tail = false) ?want loc c t e =
in in
mk loc ty (Tast.If (c, t, e)) mk loc ty (Tast.If (c, t, e))
(* The type two literals meet at, each at its own type — a wide integer at
u64, which is the only type that holds one. *)
and literal_join ctx (a : Ast.expr) (b : Ast.expr) =
let own (x : Ast.expr) =
match x.Ast.e with
| Ast.UInt _ -> Some (Types.Int Types.U64)
| _ -> probe ctx x.Ast.loc (fun () -> (check ctx x).Tast.ty)
in
match own a, own b with
| Some x, Some y -> Types.join x y
| _ -> None
(* Whether a name would reach a callee if it were called — a global function, a (* Whether a name would reach a callee if it were called — a global function, a
generic, or a local holding a function value. The three sources [named_call] generic, or a local holding a function value. The three sources [named_call]
itself consults, in its own order; builtins are deliberately not among them, itself consults, in its own order; builtins are deliberately not among them,
@ -5725,8 +5748,12 @@ and check_arr ctx ~want loc items =
expect ctx loc ~want expect ctx loc ~want
(check_arr ctx ~want:(Some (Types.Array (n, t))) loc items) (check_arr ctx ~want:(Some (Types.Array (n, t))) loc items)
| None -> | None ->
expect ctx loc ~want (match
(dyn_vec ctx loc (map_lr (fun i -> check ctx ~want:Types.Dyn i) items))) trial ctx (fun () ->
dyn_vec ctx loc (map_lr (fun i -> check ctx ~want:Types.Dyn i) items))
with
| Ok v -> expect ctx loc ~want v
| Error d -> mixed_refusal ctx items d))
| _ -> | _ ->
let items = map_lr (fun i -> check ctx ?want:elem_want i) items in let items = map_lr (fun i -> check ctx ?want:elem_want i) items in
let n = Int64.of_int (List.length items) in let n = Int64.of_int (List.length items) in
@ -5831,6 +5858,44 @@ and arr_elem_type ctx (items : Ast.expr list) : Types.t option =
(fun found c -> match found with Some _ -> found | None -> settle c) (fun found c -> match found with Some _ -> found | None -> settle c)
None candidates None candidates
(* Elements that do not agree and cannot all become a dyn either: a struct
beside a number, a type variable beside a literal. The dyn vector's refusal
would be about dyn, which the program never mentioned, so the elements are
refused against each other instead — the first one's type is what the rest
are checked at, and the refusal points back at it. [d] is the answer if
that finds nothing. *)
and mixed_refusal : 'a. ctx -> Ast.expr list -> Loc.diag -> 'a =
fun ctx items d ->
match items with
| [] -> raise (Loc.Error d)
| first :: rest ->
let first = check ctx first in
let want = match first.Tast.ty with Types.Never -> None | t -> Some t in
List.iter
(fun (i : Ast.expr) ->
match check ctx ?want i with
| v ->
(match want with
| Some t when not (Types.fits ~expected:t ~actual:v.Tast.ty) ->
fail i.Ast.loc "this array's elements are %s, but this one is %s"
(Types.to_string t) (Types.to_string v.Tast.ty)
| _ -> ())
| exception Loc.Error e when e.Loc.dloc = i.Ast.loc && want <> None ->
raise
(Loc.Error
{ e with
Loc.notes =
e.Loc.notes
@ [ Loc.note first.Tast.loc
(Printf.sprintf
"this array's first element is %s, so every \
element is"
(match first.Tast.ty with
| Types.Var v -> "$" ^ v
| t -> Types.to_string t)) ] }))
rest;
raise (Loc.Error d)
(* A dyn vector built where it stands from elements already checked at dyn: (* A dyn vector built where it stands from elements already checked at dyn:
the runtime's own vec, pushed to in order. *) the runtime's own vec, pushed to in order. *)
and dyn_vec ctx loc (items : Tast.expr list) = and dyn_vec ctx loc (items : Tast.expr list) =
@ -5986,10 +6051,29 @@ and check_the ctx ~want loc (t : Ast.texpr) (v : Ast.expr) =
| "" -> Printf.sprintf "convert it with the %s cast instead" tn | "" -> Printf.sprintf "convert it with the %s cast instead" tn
| s -> Printf.sprintf "write (%s %s) to convert it" tn s) | s -> Printf.sprintf "write (%s %s) to convert it" tn s)
else else
fail v.Ast.loc (* What a dyn does at this type is the boundary's own answer, asked of
"the checks a value as %s and does not convert one, and this is a dyn \ it rather than restated: some types take one where a value is passed,
— a dyn becomes a %s where a %s is passed, returned or stored" returned or stored, and the rest do not take one at all. *)
tn tn tn let crosses =
probe ctx loc (fun () ->
ignore (expect ctx v.Ast.loc ~want:(Some ty) (check ctx v)))
in
match crosses with
| Some () ->
fail v.Ast.loc
"the checks a value as %s and does not convert one, and this is a \
dyn — a dyn becomes a %s where a %s is passed, returned or stored"
tn tn tn
| None ->
(match check ctx ~want:ty v with
| _ ->
fail v.Ast.loc
"the checks a value as %s and does not convert one, and this is \
a dyn" tn
| exception Loc.Error d ->
fail v.Ast.loc
"the checks a value as %s and does not convert one, and this is \
a dyn — %s" tn d.Loc.dmsg)
end; end;
let r = let r =
match ty, v.Ast.e with match ty, v.Ast.e with
@ -6230,6 +6314,26 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
let literal_arm ((a : Ast.arm), _, _) = let literal_arm ((a : Ast.arm), _, _) =
match List.rev a.Ast.body with last :: _ -> lone_literal last | [] -> false match List.rev a.Ast.body with last :: _ -> lone_literal last | [] -> false
in in
(* Every arm a literal: they meet at the wider of their own types, as an
[if]'s two do. *)
(if !want = None && resolved <> [] && List.for_all literal_arm resolved then
let lasts =
List.map (fun ((a : Ast.arm), _, _) -> List.hd (List.rev a.Ast.body))
resolved
in
match lasts with
| first :: rest ->
let j =
List.fold_left
(fun acc x ->
Option.bind acc (fun a ->
Option.bind (literal_join ctx first x) (Types.join a)))
(literal_join ctx first first) rest
in
(match j with
| Some t when not (Types.equal t (Types.Int Types.I32)) -> want := Some t
| _ -> ())
| [] -> ());
let order = let order =
let idx = List.mapi (fun i r -> (i, r)) resolved in let idx = List.mapi (fun i r -> (i, r)) resolved in
if !want <> None then idx if !want <> None then idx
@ -7724,8 +7828,12 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| Types.F64 -> Float.max_float | Types.F64 -> Float.max_float
in in
mk loc ty (Tast.Float ((if max then m else -.m), k)) mk loc ty (Tast.Float ((if max then m else -.m), k))
| Types.Var _ -> | Types.Var v ->
unconstrained ctx.env loc name ~needs:"numeric?" ty; if not (declares ctx.env.tvpreds v "numeric?") then
Loc.failk "check/unconstrained-type-variable" a.Ast.loc
"%s is a limit of a numeric type, and nothing declares $%s \
numeric — write {:where (numeric? $%s)} at the head of the body"
name v v;
int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
| _ -> | _ ->
fail a.Ast.loc fail a.Ast.loc
@ -12820,6 +12928,20 @@ let escape_check (fn : Tast.fn) =
twice. *) twice. *)
let prelude_alias = "prelude~" let prelude_alias = "prelude~"
(* Off for a check whose warnings were already printed for the same source:
the dev program re-creating the session its launcher built and warned for. *)
let print_warnings = ref true
(* A name the renaming above made, which nobody wrote: left out of every
listing a person reads, and shown as whose it is where a frame has to be. *)
let internal_name n = String.starts_with ~prefix:(prelude_alias ^ "/") n
let shown_name n =
if internal_name n then
let p = String.length prelude_alias + 1 in
"the prelude's " ^ String.sub n p (String.length n - p)
else n
let shadow_prelude (prelude : Ast.decl list) (decls : Ast.decl list) = let shadow_prelude (prelude : Ast.decl list) (decls : Ast.decl list) =
let fn_name (d : Ast.decl) = let fn_name (d : Ast.decl) =
match d.Ast.d with match d.Ast.d with
@ -12935,6 +13057,7 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
reload, which is where a defn is most likely to be written. Printed in reload, which is where a defn is most likely to be written. Printed in
the shape [Loc] gives an error, so a checker in an editor parses it the the shape [Loc] gives an error, so a checker in an editor parses it the
same way. *) same way. *)
if !print_warnings then
List.iter List.iter
(fun (d : Loc.diag) -> (fun (d : Loc.diag) ->
prerr_endline prerr_endline

View File

@ -1448,7 +1448,10 @@ let describe t =
ok ok
[ ":fns " [ ":fns "
^ Wire.strings ^ Wire.strings
(List.map (fun (f : Tast.fn) -> f.Tast.name) (List.filter_map
(fun (f : Tast.fn) ->
if Check.internal_name f.Tast.name then None
else Some f.Tast.name)
t.session.Session.program.Tast.fns); t.session.Session.program.Tast.fns);
":globals " ":globals "
^ Wire.strings ^ Wire.strings
@ -1560,6 +1563,7 @@ let defs t =
Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc); Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc);
None None
| None when List.mem f.Tast.name class_names -> None | None when List.mem f.Tast.name class_names -> None
| None when Check.internal_name f.Tast.name -> None
| None -> | None ->
Some Some
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f) (entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
@ -2007,7 +2011,7 @@ let backtrace_op t =
(List.map (List.map
(fun (name, loc, mine, nslots, _sig, _rsig) -> (fun (name, loc, mine, nslots, _sig, _rsig) ->
Wire.list Wire.list
[ Wire.quote name; Wire.quote loc; [ Wire.quote (Check.shown_name name); Wire.quote loc;
Wire.quote (if mine then "program" else "eval"); Wire.quote (if mine then "program" else "eval");
string_of_int nslots ]) string_of_int nslots ])
frames); frames);
@ -5811,7 +5815,10 @@ let merged_setup () =
marshalling a [Session.t] through a file, which buys nothing: the source marshalling a [Session.t] through a file, which buys nothing: the source
cannot have changed between the two, because the build that produced cannot have changed between the two, because the build that produced
this binary is the one that exec'd it. *) this binary is the one that exec'd it. *)
(* Its warnings were printed by the launcher over the same source. *)
Check.print_warnings := false;
let session, _ = Session.create ~debug ~x86 ~file () in let session, _ = Session.create ~debug ~x86 ~file () in
Check.print_warnings := true;
(* The program's output has to reach an editor exactly as it did when the (* The program's output has to reach an editor exactly as it did when the
daemon held the other end of a pipe. Same pipe, one process: fd 1 is daemon held the other end of a pipe. Same pipe, one process: fd 1 is
replaced before the program starts, and the accept loop drains it — replaced before the program starts, and the accept loop drains it —

View File

@ -186,6 +186,10 @@ let stale_sites ?(live = SM.empty) ?(running = false) built (p : Tast.program) :
m acc m acc
in in
from ~kept:true live (from ~kept:false built []) from ~kept:true live (from ~kept:false built [])
(* A caller or callee the prelude-shadowing rename made is not the
program's, and there is nothing in the program to recompile for it. *)
|> List.filter (fun s ->
not (Check.internal_name s.caller || Check.internal_name s.target))
|> List.sort (fun a b -> |> List.sort (fun a b ->
match String.compare a.at.Loc.file b.at.Loc.file with match String.compare a.at.Loc.file b.at.Loc.file with
| 0 -> Loc.before a.at b.at | 0 -> Loc.before a.at b.at

View File

@ -6637,6 +6637,16 @@ level "1"
cli_case "--debug and an explicit -O are refused together" cli_case "--debug and an explicit -O are refused together"
"build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2 "build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2
~says:[ "--debug"; "-O2"; "Drop one of the two" ]; ~says:[ "--debug"; "-O2"; "Drop one of the two" ];
(* The name a shadowed prelude function is moved to is nobody's to read. *)
(let code, text = cli "check programs/shadow-prelude.flan" in
if code <> 0 || contains text "prelude~"
|| not (contains text "defn floor-f32")
then begin
incr failures;
Printf.printf
"FAIL check's listing leaves out the renamed prelude function\n\
\ got: %S (exit %d)\n" text code
end);
(* A file with no main is refused by name before the link, which would (* A file with no main is refused by name before the link, which would
otherwise report an undefined reference from crt1.o. *) otherwise report an undefined reference from crt1.o. *)
let nomain = Filename.concat scratch "no-main.flan" in let nomain = Filename.concat scratch "no-main.flan" in

View File

@ -6528,6 +6528,24 @@ let () =
infers "the names the element type of a mixed literal" "(the [dyn] [1 2.5])" "[2 dyn]"; infers "the names the element type of a mixed literal" "(the [dyn] [1 2.5])" "[2 dyn]";
infers "the with a slice type gives the literal's array type" infers "the with a slice type gives the literal's array type"
"(the [f32] [1 2.5])" "[2 f32]"; "(the [f32] [1 2.5])" "[2 f32]";
(match checked "(defstruct P [x i32]) (defn main [] i32 (let [a [(P 1) 2]] 0))" with
| _ -> check "a struct beside a number is refused" false
| exception Loc.Error d ->
check "elements that cannot become a dyn are refused against the first"
(contains d.Loc.dmsg "expected P, found the integer literal 2"
&& List.exists
(fun (n : Loc.note) ->
contains n.Loc.nmsg "this array's first element is P")
d.Loc.notes));
(match checked "(defn g [x $t] i32 (let [a [x 1]] 0))" with
| _ -> check "a type variable beside a literal is refused" false
| exception Loc.Error d ->
check "a type variable beside a literal names the bound and spells $t"
(contains d.Loc.dmsg "{:where (numeric? $t)}"
&& List.exists
(fun (n : Loc.note) ->
contains n.Loc.nmsg "this array's first element is $t")
d.Loc.notes));
rejects_check "every element needing a type names the first's refusal" rejects_check "every element needing a type names the first's refusal"
"(defn main [] i32 (let [a [None None]] 0))" "(defn main [] i32 (let [a [None None]] 0))"
~needle:"what None is an Option of"; ~needle:"what None is an Option of";
@ -6535,8 +6553,16 @@ let () =
(* ── (max-of T) and (min-of T) ──────────────────────────────────── *) (* ── (max-of T) and (min-of T) ──────────────────────────────────── *)
infers "max-of carries its type" "(max-of u16)" "u16"; infers "max-of carries its type" "(max-of u16)" "u16";
infers "min-of at a float" "(min-of f32)" "f32"; infers "min-of at a float" "(min-of f32)" "f32";
rejects_check "max-of at an unbounded type variable names the bound" (match checked "(defn f [x $t] $t (max-of $t))" with
"(defn f [x $t] $t (max-of $t))" ~needle:"{:where (numeric? $t)}"; | _ -> check "max-of at an unbounded type variable is refused" false
| exception Loc.Error d ->
check "max-of at an unbounded type variable names the bound and only it"
(contains d.Loc.dmsg "write {:where (numeric? $t)}"
&& not (contains d.Loc.dmsg "Fn")));
infers "two literal if arms meet at the wider" "(if true 1 2.5)" "f64";
infers "two integer if arms stay i32" "(if true 1 2)" "i32";
infers "two literal match arms meet at the wider"
"(match (Some 1) (Some v) 1 None 2.5)" "f64";
accepts "max-of at a type variable the bound admits" accepts "max-of at a type variable the bound admits"
"(defn f [x $t] $t {:where (integer? $t)} (max-of t))"; "(defn f [x $t] $t {:where (integer? $t)} (max-of t))";
rejects_check "max-of at a type that is not a number names the bound" rejects_check "max-of at a type that is not a number names the bound"
@ -6553,9 +6579,13 @@ let () =
rejects_check "the refuses a dyn and names the cast" rejects_check "the refuses a dyn and names the cast"
"(defn f [x dyn] i32 (the i32 x))" ~needle:"write (i32 x) to convert it"; "(defn f [x dyn] i32 (the i32 x))" ~needle:"write (i32 x) to convert it";
accepts "the cast that refusal names compiles" "(defn f [x dyn] i32 (i32 x))"; accepts "the cast that refusal names compiles" "(defn f [x dyn] i32 (i32 x))";
rejects_check "the refuses a dyn at a type that has no cast" rejects_check "the refuses a dyn at a type a dyn does not become"
"(defn f [x dyn] string (the string x))" "(defn f [x dyn] string (the string x))"
~needle:"a dyn becomes a string where a string is passed"; ~needle:"dyn — string does not cross into a written type yet";
rejects_check "the refuses a dyn at bool, which a dyn becomes where passed"
"(defn f [x dyn] bool (the bool x))"
~needle:"a dyn becomes a bool where a bool is passed";
accepts "the bool a dyn becomes where it is returned" "(defn f [x dyn] bool x)";
accepts "the at an Option takes nil" "(defn f [] (Option i32) (the (Option i32) nil))"; accepts "the at an Option takes nil" "(defn f [] (Option i32) (the (Option i32) nil))";
parse_rejects "the takes a type and a value" "(defn f [] i32 (the i32))" parse_rejects "the takes a type and a value" "(defn f [] i32 (the i32))"
~needle:"the is (the TYPE value)"; ~needle:"the is (the TYPE value)";