The CLI and the tests load, check and link a program through one module in the library

This commit is contained in:
Joseph Ferano 2026-09-25 07:30:55 +07:00
parent 54ec52fde6
commit 7d75cc0a5d
4 changed files with 84 additions and 48 deletions

View File

@ -2007,11 +2007,13 @@ Sixty mutations, nineteen of which left the whole suite green; all nineteen are
closed, each re-planted and watched fail against the new test. What is open is closed, each re-planted and watched fail against the new test. What is open is
that the pass has not been run again, so nineteen is the old number. that the pass has not been run again, so nineteen is the old number.
** TODO bin/main.ml spells the compile pipeline out by hand ** DONE bin/main.ml spells the compile pipeline out by hand
Two places each do a load, a check and a link feeding the builder, where the tests CLOSED: [2026-09-25]
go through one helper. The CLI loads through its own path and checks with a =lib/front.ml= holds the load, the check and the link. =flan build=, =run=,
different entry point, so this is not a matter of calling the test module from the =emit= (both backends), =check= and =shim= go through it with =~all:true= (every error,
binary — closing it means the pipeline moving into the library. =Loc.Errors=); =Test_support.checked= and =linked= go through it without (the
first error, =Loc.Error=). The =--no-gc= and =--warn-memory= passes stay in the
CLI as a hook that sees the program before =Reach= prunes it.
** DONE Build.executable returns only its output path ** DONE Build.executable returns only its output path
CLOSED: [2026-09-25] CLOSED: [2026-09-25]

View File

@ -108,17 +108,13 @@ let summarise (d : Flan.Ast.decl) =
(match fn.ret with None -> "Unit" | Some _ -> "explicit") (match fn.ret with None -> "Unit" | Some _ -> "explicit")
(List.length fn.fbody) (List.length fn.fbody)
(* Every path past [parse] goes through [Load]: an import is resolved into the (* Every path past [parse] goes through [Front], and so through [Load]: an
declarations it stands for, and the package's C shim and linker arguments import is resolved into the declarations it stands for, and the package's C
come back with them. *) shim and linker arguments come back with them. Every driver here is the
(* Every driver here is the batch case, which is the one the workflow is: write batch case, which is the one the workflow is: write everything, compile at
everything, compile at the end, work through the list. So every one of them the end, work through the list. So every one of them passes [~all:true] and
asks for the whole list rather than the first thing wrong. *) asks for the whole list rather than the first thing wrong. *)
let load path : Flan.Load.t = let checked path = snd (Flan.Front.checked ~all:true path)
Flan.Load.program ~file:path
~parse:Flan.Parse.program_all (Flan.Reader.read_file path)
let checked path = Flan.Check.program_all (load path).decls
(* What the source called each parameter, per function. The typed IR refers to (* What the source called each parameter, per function. The typed IR refers to
locals by slot index and records no names — [Check] has them in its scope locals by slot index and records no names — [Check] has them in its scope
@ -658,8 +654,7 @@ let () =
List.iter List.iter
(fun path -> (fun path ->
with_errors path (fun () -> with_errors path (fun () ->
load path |> fun l -> checked path
Flan.Check.program_all l.decls
|> Flan.X86.program ~checks ~dev ~debug ~annotate |> Flan.X86.program ~checks ~dev ~debug ~annotate
|> print_string)) |> print_string))
files files
@ -675,9 +670,8 @@ let () =
List.iter List.iter
(fun path -> (fun path ->
with_errors path (fun () -> with_errors path (fun () ->
let l = load path in let l, p = Flan.Front.checked ~all:true path in
let pnames = if debug then param_names l else [] in let pnames = if debug then param_names l else [] in
let p = Flan.Check.program_all l.decls in
(* Between checking and emission, and it hands the very same program (* Between checking and emission, and it hands the very same program
on: the flag is a question asked of what was checked, never a on: the flag is a question asked of what was checked, never a
parameter of what is emitted. *) parameter of what is emitted. *)
@ -724,28 +718,30 @@ let () =
exit 2 exit 2
in in
with_errors path (fun () -> with_errors path (fun () ->
let l = load path in
let p = Flan.Check.program_all l.decls in
(* Before reachability rather than after: a dyn in a function nothing
calls is still a dyn somebody wrote, and a refusal that depended on
what [main] happened to reach would come and go as the program was
edited elsewhere. *)
if List.mem no_gc_flag rest then Flan.Check.no_gc p;
(* Before reachability too, and for the same reason: a push in a function
nothing calls is still a push somebody wrote. *)
if List.mem warn_memory_flag rest then print_memory_warnings ~file:path p;
(* The link follows the program, not the import list: a package nothing (* The link follows the program, not the import list: a package nothing
reachable calls into contributes no C and no linker argument, and its reachable calls into contributes no C and no linker argument, and its
functions are not emitted either. That is what lets one file import functions are not emitted either. That is what lets one file import
raylib and still be buildable for wasm32. *) raylib and still be buildable for wasm32. *)
let p, csrcs, lflags = Flan.Reach.link ~dev l p in let f =
Flan.Front.linked ~all:true ~dev path ~before_link:(fun p ->
(* Before reachability rather than after: a dyn in a function
nothing calls is still a dyn somebody wrote, and a refusal that
depended on what [main] happened to reach would come and go as
the program was edited elsewhere. *)
if List.mem no_gc_flag rest then Flan.Check.no_gc p;
(* Before reachability too, and for the same reason: a push in a
function nothing calls is still a push somebody wrote. *)
if List.mem warn_memory_flag rest then
print_memory_warnings ~file:path p)
in
ignore (Flan.Build.executable ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; dev; debug; sanitize; ~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
target; x86; target; x86;
opt = Option.value opt opt = Option.value opt
~default:Flan.Build.default.Flan.Build.opt } ~default:Flan.Build.default.Flan.Build.opt }
~csrcs ~lflags ~pnames:(if debug then param_names l else []) ~csrcs:f.csrcs ~lflags:f.lflags
p ~out)) ~pnames:(if debug then param_names f.load else [])
f.program ~out))
(* The daemon an editor talks to: one session, the program it belongs to (* The daemon an editor talks to: one session, the program it belongs to
running beside it, and a socket. Unlike [flan reload] the session persists, running beside it, and a socket. Unlike [flan reload] the session persists,
so a defonce added by one evaluation is part of what the next one is checked so a defonce added by one evaluation is part of what the next one is checked
@ -916,16 +912,15 @@ let () =
Filename.concat (Filename.get_temp_dir_name ()) Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-run-%d" (Unix.getpid ())) (Printf.sprintf "flan-run-%d" (Unix.getpid ()))
in in
let l = load path in let f = Flan.Front.linked ~all:true path in
let p = Flan.Check.program_all l.decls in
let p, csrcs, lflags = Flan.Reach.link l p in
ignore (Flan.Build.executable ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; debug; sanitize; ~opts:{ Flan.Build.default with checks; debug; sanitize;
x86; x86;
opt = Option.value opt opt = Option.value opt
~default:Flan.Build.default.Flan.Build.opt } ~default:Flan.Build.default.Flan.Build.opt }
~csrcs ~lflags ~pnames:(if debug then param_names l else []) ~csrcs:f.csrcs ~lflags:f.lflags
p ~out:exe); ~pnames:(if debug then param_names f.load else [])
f.program ~out:exe);
let code = let code =
Sys.command Sys.command
(String.concat " " (List.map Filename.quote (exe :: prog_args))) (String.concat " " (List.map Filename.quote (exe :: prog_args)))

40
lib/front.ml Normal file
View File

@ -0,0 +1,40 @@
(** The front half of a compile: read a file, load it with its imports, check
it, and decide the link. What [flan build], [flan run], [flan emit] and
[flan check] do before a backend is reached, and what the test binaries do
before theirs, so that the two cannot drift apart.
[all] chooses how a refusal is reported. With it, the parser and the
checker keep going and raise [Loc.Errors] with every declaration they
refused, which is what the command-line drivers want: write everything,
compile at the end, work through the list. Without it they stop at the
first and raise [Loc.Error], which is what a test asserting on one message
wants. For a program with no errors the two are the same. *)
let load ?(all = false) path : Load.t =
Load.program ~file:path
~parse:(if all then Parse.program_all else Parse.program)
(Reader.read_file path)
let check ?(all = false) (l : Load.t) : Tast.program =
(if all then Check.program_all else Check.program) l.Load.decls
let checked ?all path =
let l = load ?all path in
(l, check ?all l)
type linked = {
load : Load.t; (* the declarations, as loaded *)
program : Tast.program; (* checked, and pruned to what is reached *)
csrcs : string list; (* the packages' C that the link needs *)
lflags : string list; (* ...and their linker arguments *)
}
(* [before_link] sees the checked program before [Reach.link] prunes it. The
questions asked there — [Check.no_gc], the memory warnings — are about what
was written, and a function nothing calls is still something somebody
wrote. *)
let linked ?(dev = false) ?all ?(before_link = fun _ -> ()) path =
let l, p = checked ?all path in
before_link p;
let program, csrcs, lflags = Reach.link ~dev l p in
{ load = l; program; csrcs; lflags }

View File

@ -193,14 +193,14 @@ let listening ?(ms = 30000) ~pid path =
(* ── The front half of a compile ──────────────────────────────────── *) (* ── The front half of a compile ──────────────────────────────────── *)
(* Read, load and check a program: the same two calls [flan build] makes (* Read, load and check a program, through [Front], which is the path
before it reaches the backend. Through [Load], so a program with an [flan build] takes too. Without [~all], so a refusal raises the first error
(import ...) is buildable here — it brings back the package's declarations rather than the list. Through [Load], so a program with an (import ...) is
as well as the file's own. *) buildable here — it brings back the package's declarations as well as the
let checked path = file's own. *)
Check.program (Load.program ~file:path (Reader.read_file path)).Load.decls let checked path = snd (Front.checked path)
(* And the third call, which is where the binaries below actually differ from (* And the link, which is where the binaries below actually differ from
one another: [Reach.link] decides the link from the checked program — a one another: [Reach.link] decides the link from the checked program — a
package nothing reachable calls into hands over no C and no linker package nothing reachable calls into hands over no C and no linker
argument, and its functions are not emitted — and returns the program argument, and its functions are not emitted — and returns the program
@ -210,7 +210,6 @@ let checked path =
different [Build.opts] (a sanitized build, a -O0 one, an x86 one, a web different [Build.opts] (a sanitized build, a -O0 one, an x86 one, a web
one) and several want the exception rather than the executable, so the one) and several want the exception rather than the executable, so the
options record is the one part that is genuinely theirs. *) options record is the one part that is genuinely theirs. *)
let linked ?(dev = false) path = let linked ?dev path =
let l = Load.program ~file:path (Reader.read_file path) in let f = Front.linked ?dev path in
let p = Check.program l.Load.decls in (f.Front.program, f.Front.csrcs, f.Front.lflags)
Reach.link ~dev l p