From ac7ee912e719b8116f50ab5139fcf5274cba23b8 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Thu, 17 Sep 2026 21:44:40 +0700 Subject: [PATCH] A wrong main signature points at the main that is wrong check_main raised against Loc.unknown, so both of its refusals opened with :0:0. env.locs is the table of where each type was declared and a function is not in it, so the location comes from the declaration list the caller already holds. A main that arrived without a defn keeps the unknown span rather than being given an invented one. --- lib/check.ml | 28 ++++++++++++++++++++++++---- test/test_flan.ml | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index 13c9203..8a86a79 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -6272,7 +6272,27 @@ let check_global env (d : Ast.decl) : Tast.global option = (* The entry point, plan.org: (defn main [args [string]] i32), with both the parameter and the return type optional. *) -let check_main env = +(* Where [main] was written. [env.locs] is the table of where each *type* was + declared — [collect] fills it for structs, data types, unions and enums and + for nothing else — so a function's own location is not in it and the + [find_opt] idiom the rest of this file uses does not apply here. The + declaration list does have it, and the caller is holding the list anyway. + + Without this both refusals below opened with :0:0, which tells a + reader that a rule exists and not where they broke it, and gives + [next-error] nothing to jump to. A [main] that arrived some other way — a + [declare], say — still has no [defn] to point at, so that case keeps the + unknown span rather than inventing one. *) +let main_loc (decls : Ast.decl list) = + let is_main (d : Ast.decl) = + match d.Ast.d with Ast.Defn fn -> fn.Ast.name = "main" | _ -> false + in + match List.find_opt is_main decls with + | Some { Ast.d = Ast.Defn fn; _ } -> fn.Ast.nloc + | _ -> Loc.unknown + +let check_main env decls = + let at = main_loc decls in match Hashtbl.find_opt env.fns "main" with | None -> () (* a library, or a file being checked on its own *) | Some (params, ret) -> @@ -6283,12 +6303,12 @@ let check_main env = | _ -> false in if not ok_params then - fail Loc.unknown + fail at "main takes no parameters or one [string], not (%s)" (String.concat " " (List.map Types.to_string params)); if not (Types.equal ret Types.Unit || Types.equal ret (Types.Int Types.I32)) then - fail Loc.unknown "main returns i32 or nothing, not %s" + fail at "main returns i32 or nothing, not %s" (Types.to_string ret) (* The environment as well as the program. A session needs it to check an @@ -6321,7 +6341,7 @@ let build_program ~keep_going (decls : Ast.decl list) : Tast.program * env = check_finite env; check_union_members env; let s = Loc.sink ~on:keep_going in - ignore (Loc.caught s (fun () -> check_main env)); + ignore (Loc.caught s (fun () -> check_main env decls)); (* Every generic body, checked once with its variables left abstract, and the result thrown away. This is the pass plan.org's rule needs and Odin has no equivalent of: Odin checks a polymorphic body only per diff --git a/test/test_flan.ml b/test/test_flan.ml index 4091203..9d13727 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -945,6 +945,27 @@ let () = ~needle:"main takes no parameters"; rejects_check "main returning the wrong type" "(defn main [] bool true)" ~needle:"main returns i32"; + (* And both of them point at the [main] that is wrong. They used to open with + :0:0 — the checker's rule about the entry point is the one place + that had a name and no span, because [env.locs] records where a *type* was + declared and a function is not in it. The span is the whole difference + between a message you can act on and a message you have to go looking for, + so it is pinned here rather than left to the reader of a report. *) + let main_at name src = + match checked src with + | _ -> + incr failures; + Printf.printf "FAIL %s: expected a type error\n" name + | exception Loc.Error { Loc.dloc; _ } -> + let got = Loc.to_string dloc in + if got <> ":1:7" then begin + incr failures; + Printf.printf "FAIL %s\n wanted: %s\n got: %s\n" + name ":1:7" got + end + in + main_at "a wrong main parameter points at main" "(defn main [n i32] ())"; + main_at "a wrong main return type points at main" "(defn main [] bool true)"; (* ── Unconstrained operators, and everything past milestone 2 ──── *) rejects_check "no built-in = on strings"