diff --git a/TODO.org b/TODO.org index b0d30b34..b92a4f8e 100644 --- a/TODO.org +++ b/TODO.org @@ -854,13 +854,6 @@ Already fixed by 3672da2, which blames the arm that is not a compiler temp; the caret is on the last operand and =test/test_flan.ml= asserts its column. Rules out relabelling the else arm, a bool sentinel, and inverting the condition. -** NEXT Signature pairing's cold-rebuild edge -Decided 2026-09-25: the type takes precedence, as today. The warning is at the parameter site: where a name in a parameter vector is read as a program-declared type but could also have been read as a parameter name, the parameter vector gets a warning naming the type and where it is declared. -Whether a parameter vector reads as one annotated parameter or two dyn ones -depends on what type names exist, so adding a type can silently re-pair an -existing signature between compiles. A changed-pairing warning was proposed and -not queued. - ** DONE A typed container crosses into dyn as a view, and only from permanent storage CLOSED: [2026-09-20] The descriptor is pointer, length and element type — a slice plus the piece a diff --git a/lib/check.ml b/lib/check.ml index 252cafba..aee64ee2 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1631,9 +1631,37 @@ let dyn_param_or_typo env n loc = parameters are lowercase" n -let pair_params ?(also = fun _ -> false) env (items : Ast.pitem list) - : Ast.field list = +(* The pairings a parameter vector owes to a type the program declares under + a name that is also a legal parameter name — a lowercase one, since a + capitalised name is refused as a parameter. [(defn f [p point] ...)] is one + parameter while [point] is a type and two dyn ones the moment it is not, + so adding or removing the type re-pairs the signature with no edit to it. + The type still wins; this is the warning at the parameter, filled by + [pair_decls] and printed by [build_program] with the other warnings. *) +let pairing_warnings : Loc.diag list ref = ref [] + +let pair_params ?(also = fun _ -> false) ?(declared = fun _ -> None) env + (items : Ast.pitem list) : Ast.field list = let is_type_name env n = is_type_name env n || also n in + let warn_pairing n t tloc = + let bare = + match String.rindex_opt t '/' with + | Some i -> String.sub t (i + 1) (String.length t - i - 1) + | None -> t + in + match declared t with + | Some (what, (at : Loc.t)) + when bare <> "" && bare.[0] >= 'a' && bare.[0] <= 'z' -> + pairing_warnings := + Loc.diag ~kind:"check/parameter-reads-a-type" tloc + (Printf.sprintf + "[%s %s] is one parameter %s of type %s, the %s declared at %s, \ + and not two dyn parameters. If two were meant, give the second \ + a name no type has" + n t n t what (Loc.to_string at)) + :: !pairing_warnings + | _ -> () + in let dyn loc = { Ast.t = Ast.Tname "dyn"; tloc = loc } in let rec go = function | [] -> [] @@ -1649,6 +1677,7 @@ let pair_params ?(also = fun _ -> false) env (items : Ast.pitem list) | Ast.Pname (n, loc) :: Ast.Ptype t :: rest -> { Ast.fname = n; fty = t; floc = loc } :: go rest | Ast.Pname (n, loc) :: Ast.Pname (t, tloc) :: rest when is_type_name env t -> + warn_pairing n t tloc; { Ast.fname = n; fty = { Ast.t = Ast.Tname t; tloc }; floc = loc } :: go rest (* The slot after this one is not a type, so this one is a parameter with no type written — unless the slot after it only *looks* unlike a type @@ -1778,10 +1807,32 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list = match d.Ast.d with Ast.Defclass (n, _) -> Some n | _ -> None) decls in + (* The types the program declares, with what kind and where, for + [pair_params]'s warning. The prelude's are left out: its names are the + language's, not a declaration the reader made. *) + let types = Hashtbl.create 16 in + List.iter + (fun (d : Ast.decl) -> + let add n what = + if d.Ast.dloc.Loc.file <> Prelude.file then + Hashtbl.replace types n (what, d.Ast.dloc) + in + match d.Ast.d with + | Ast.Defstruct (n, _, _) -> add n "struct" + | Ast.Defenum (n, _) -> add n "enum" + | Ast.Defalias (n, _) -> add n "alias" + | Ast.Defdata (n, _) -> add n "data type" + | Ast.Defunion (n, _) -> add n "union" + | _ -> ()) + decls; + pairing_warnings := []; let fn (f : Ast.fn) = match f.Ast.praw with | None -> f - | Some items -> { f with Ast.params = pair_params env items; praw = None } + | Some items -> + { f with + Ast.params = pair_params ~declared:(Hashtbl.find_opt types) env items; + praw = None } in List.map (fun (d : Ast.decl) -> @@ -14108,6 +14159,12 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) : cannot make the next body fail — which is what makes a declaration a resync point that needs no resynchronising. *) let decls = collect env decls in + if !print_warnings then + List.iter + (fun (d : Loc.diag) -> + prerr_endline + (Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg)) + (List.rev !pairing_warnings); check_finite env; check_union_members env; let s = Loc.sink ~on:keep_going in diff --git a/test/test_flan.ml b/test/test_flan.ml index 131f7ab5..1e20a2e4 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -5814,6 +5814,29 @@ let () = (match checked shadow_src with | _ -> true | exception Loc.Error _ -> false); + (* A parameter vector paired by a lowercase type the program declares reads + as two dyn parameters the day the type goes, so the pairing is warned at, + naming the type and where it is declared. A capitalised type cannot be a + parameter name, so it has nothing to warn about. *) + (match + checked "(defstruct point [x i32])\n(defstruct Vec2 [x i32])\n\ + (defn px [p point] i32 (.x p))\n(defn vx [v Vec2] i32 (.x v))" + with + | _ -> + (match !Check.pairing_warnings with + | [ d ] -> + check "a lowercase declared type in a parameter vector is warned at" + (d.Loc.kind = "check/parameter-reads-a-type" + && d.Loc.dloc.Loc.line = 3 && d.Loc.dloc.Loc.col = 13 + && d.Loc.dmsg + = "[p point] is one parameter p of type point, the struct \ + declared at :1:1, and not two dyn parameters. If two \ + were meant, give the second a name no type has") + | ds -> + check + (Printf.sprintf "one pairing warning, not %d" (List.length ds)) + false) + | exception Loc.Error _ -> check "the paired program checks" false); check "a program that shadows nothing is warned at not at all" (Check.shadowed_builtins (program "(defn f [] i32 1)") = []); (* A prelude function's name is taken over the same way, for the calls in