diff --git a/bin/main.ml b/bin/main.ml index f4564016..f15d5066 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -736,6 +736,8 @@ let () = if List.mem warn_memory_flag rest then print_memory_warnings ~file:path p) in + Flan.Build.need_main ~file:path ~doing:"flan build has nothing to link" + f.program; ignore (Flan.Build.executable ~opts:{ Flan.Build.default with checks; dev; debug; sanitize; target; x86; @@ -915,6 +917,8 @@ let () = (Printf.sprintf "flan-run-%d" (Unix.getpid ())) in let f = Flan.Front.linked ~all:true path in + Flan.Build.need_main ~file:path ~doing:"flan run has nothing to run" + f.program; ignore (Flan.Build.executable ~opts:{ Flan.Build.default with checks; debug; sanitize; x86; diff --git a/lib/build.ml b/lib/build.ml index 899310d2..70e94449 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -744,6 +744,22 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () = end; obj +(* A program with no [main] builds every function and then fails at the link, + as an undefined reference from the C startup code — a message about crt1.o + for a mistake in the .flan file. Asked here, by the commands that make an + executable, rather than inside [executable], whose callers include hosts + and tests that supply their own [main]. *) +let need_main ~file ~doing (p : Tast.program) = + if not (List.exists (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns) + then + failwith + (Printf.sprintf + "%s has no main, so %s. A program starts at a function named main, \ + for example:\n\n\ + \ (defn main [] i32\n\ + \ 0)" + file doing) + (* [csrcs] and [lflags] come from the imported packages (see [Load]): the C shim a package binds through, and the arguments needed to link the library it binds to. diff --git a/lib/dev.ml b/lib/dev.ml index d83191d7..44fd3fa4 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -4520,17 +4520,8 @@ let make_session_dir ~file dir = would find that out at the link — as a missing symbol, or as the merged build's rename finding nothing to rename. *) let need_main ~file (session : Session.t) = - if not - (List.exists (fun (f : Tast.fn) -> f.Tast.name = "main") - session.Session.host.Tast.fns) - then - failwith - (Printf.sprintf - "%s has no main, so flan dev has nothing to run. A program starts at \ - a function named main, for example:\n\n\ - \ (defn main [] i32\n\ - \ 0)" - file) + Build.need_main ~file ~doing:"flan dev has nothing to run" + session.Session.host (* [debug] is off by default, which keeps [flan dev] exactly what it was: a -O2 host and -O2 modules. It is opt-in rather than always-on because a debug diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 6ced8956..cbb7a993 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -6472,6 +6472,18 @@ level "1" cli_case "--debug and an explicit -O are refused together" "build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2 ~says:[ "--debug"; "-O2"; "Drop one of the two" ]; + (* A file with no main is refused by name before the link, which would + otherwise report an undefined reference from crt1.o. *) + let nomain = Filename.concat scratch "no-main.flan" in + Out_channel.with_open_bin nomain (fun oc -> + output_string oc "(defn f [] i32 0)\n"); + cli_case "build of a file with no main names main" + (Printf.sprintf "build %s -o /dev/null" (Filename.quote nomain)) ~code:1 + ~says:[ "has no main"; "(defn main [] i32" ]; + cli_case "run of a file with no main names main" + (Printf.sprintf "run %s" (Filename.quote nomain)) ~code:1 + ~says:[ "has no main"; "(defn main [] i32" ]; + Sys.remove nomain; (* Every row above that went through the pool has been forked; nothing after this point may look at [failures] until every one of them has