diff --git a/bin/main.ml b/bin/main.ml index 6ee475e..6612f1a 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -3,7 +3,7 @@ (* Both error channels, in the one place that prints them. A single refusal still exits 1 and still opens with [file:line:col: message]; a driver that got to the end of the file hands over everything it found, sorted, with a - count after it. Nothing here parses the message — the squiggle comes from + count after it. Nothing here parses the message — the squiggle comes from the span and the classification from the kind. *) let with_errors path f = try f () with @@ -42,10 +42,14 @@ let summarise (d : Flan.Ast.decl) = (* Every path past [parse] goes through [Load]: an import is resolved into the declarations it stands for, and the package's C shim and linker arguments come back with them. *) +(* Every driver here is the batch case, which is the one the workflow is: write + everything, compile at the end, work through the list. So every one of them + asks for the whole list rather than the first thing wrong. *) let load path : Flan.Load.t = - Flan.Load.program ~file:path (Flan.Parse.program (Flan.Reader.read_file path)) + Flan.Load.program ~file:path + (Flan.Parse.program ~keep_going:true (Flan.Reader.read_file path)) -let checked path = Flan.Check.program (load path).decls +let checked path = Flan.Check.program ~keep_going:true (load path).decls (* 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 @@ -132,7 +136,7 @@ let () = (fun path -> with_errors path (fun () -> Flan.Reader.read_file path - |> Flan.Parse.program + |> Flan.Parse.program ~keep_going:true |> List.iter (fun d -> print_endline (summarise d)))) files | _ :: "check" :: files when files <> [] -> @@ -286,7 +290,7 @@ let () = with_errors path (fun () -> let l = load path in let pnames = if debug then param_names l else [] in - Flan.Check.program l.decls + Flan.Check.program ~keep_going:true l.decls |> Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize |> print_string)) files @@ -318,7 +322,7 @@ let () = in with_errors path (fun () -> let l = load path in - let p = Flan.Check.program l.decls in + let p = Flan.Check.program ~keep_going:true l.decls in (* The link follows the program, not the import list: a package nothing reachable calls into contributes no C and no linker argument, and its functions are not emitted either. That is what lets one file import @@ -395,7 +399,7 @@ let () = (Printf.sprintf "flan-run-%d" (Unix.getpid ())) in let l = load path in - let p = Flan.Check.program l.decls in + let p = Flan.Check.program ~keep_going:true l.decls in let p, csrcs, lflags = Flan.Reach.link l p in ignore (Flan.Build.executable ~csrcs ~lflags p ~out:exe); let code = diff --git a/lib/check.ml b/lib/check.ml index 9032cc7..38458f3 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -4132,7 +4132,8 @@ let check_main env = expression typed at a REPL against the program the process is running — and it has to be this one rather than anything rebuilt from declarations, because [program] prepends the prelude and no accumulated AST contains it. *) -let program_with_env (decls : Ast.decl list) : Tast.program * env = +let program_with_env ?(keep_going = false) (decls : Ast.decl list) + : Tast.program * env = let env = new_env () in let decls = Parse.program (Prelude.forms ()) @ decls in (* Before anything is collected: every (declare-c ...) becomes an ordinary @@ -4140,18 +4141,34 @@ let program_with_env (decls : Ast.decl list) : Tast.program * env = flattening comes back to be compiled into the build. Nothing below this line knows the form exists. *) let decls, cshim = Shim.expand decls in + (* Pass one, and it stops at the first thing it refuses. That is not + laziness: every name, type and signature in the file comes from here, so a + declaration this pass could not make sense of leaves a hole that pass two + would report once per mention. A wrong signature is one error; the thirty + "unknown name" lines under it are not errors, they are the same one. + + Pass two is where the volume is, and it is where collecting pays. By the + time it runs every signature is sound, so a body that fails to check + cannot make the next body fail — which is what makes a declaration a + resync point that needs no resynchronising. *) collect env decls; check_finite env; - check_main env; - let globals = List.filter_map (check_global env) decls in + let s = Loc.sink ~on:keep_going in + ignore (Loc.caught s (fun () -> check_main env)); + let globals = + List.filter_map + (fun d -> Option.join (Loc.caught s (fun () -> check_global env d))) + decls + in let fns = List.filter_map (fun (d : Ast.decl) -> match d.Ast.d with - | Ast.Defn fn -> Some (check_fn env fn) + | Ast.Defn fn -> Loc.caught s (fun () -> check_fn env fn) | _ -> None) decls in + Loc.finish s; (* The handler clauses lifted out along the way. They are ordinary functions from here down; nothing in the backend knows they were written inside something else. *) @@ -4175,7 +4192,8 @@ let program_with_env (decls : Ast.decl list) : Tast.program * env = globals; externs; fns; cshim }, env) -let program (decls : Ast.decl list) : Tast.program = fst (program_with_env decls) +let program ?keep_going (decls : Ast.decl list) : Tast.program = + fst (program_with_env ?keep_going decls) (* One expression, checked against a program that is already running. The frame is empty — a REPL expression has no parameters and no enclosing diff --git a/lib/loc.ml b/lib/loc.ml index f1a67c4..ebe043e 100644 --- a/lib/loc.ml +++ b/lib/loc.ml @@ -49,12 +49,12 @@ let width (t : t) = let to_string t = Printf.sprintf "%s:%d:%d" t.file t.line t.col -(* ── Diagnostics ────────────────────────────────────────── +(* ── Diagnostics ────────────────────────────────────────── An error is a value rather than a location and a string, and the three parts that make it one each buy something the pair could not express. - [kind] is a stable id — ["reader/unterminated-string"]. It classifies + [kind] is a stable id — ["reader/unterminated-string"]. It classifies without any prose being parsed, so a message may be reworded freely and a test that asserts on *which* error this is keeps holding. It is not a replacement for the message: the messages here already state the reason and @@ -101,7 +101,7 @@ type diag = { This is still the single-error channel, and that is deliberate: the daemon and [Session.eval] evaluate *one* form and have one failure to report, so they keep catching this and take a location and a message out of it with - [summary]. Only the batch drivers — the ones that compile a whole file — + [summary]. Only the batch drivers — the ones that compile a whole file — raise the list below, and nothing interactive has to know it exists. *) exception Error of diag @@ -137,6 +137,43 @@ let fail loc fmt = let failk ?notes ?expansion kind loc fmt = Printf.ksprintf (fun msg -> raise_diag (diag ~kind ?notes ?expansion loc msg)) fmt +(* ── Collecting ───────────────────────────────────────────────────── + + A sink holds what a pass found, so the pass can carry on to the next thing + rather than stop at the first. It is switched on by the caller and not by + the code that raises, which is what keeps the interactive path exactly as it + was: the daemon checks one form and wants one exception, so it asks for a + sink that is off, every [caught] re-raises, and nothing downstream ever sees + a list. + + A sink that is on is finished at a *phase* boundary and nowhere else. That + is the whole of the resynchronisation story and it is deliberately crude: + the hard part of recovery is not recording the error, it is not cascading + afterwards, and a phase run on a foundation the phase before it already + refused reports wreckage. Three real errors beat thirty of which + twenty-seven are consequences of the first. *) + +type sink = { on : bool; mutable found : diag list } + +let sink ~on = { on; found = [] } + +(** Run [f]. With the sink off this is [Some (f ())] and an error propagates as + it always did. With it on, an error is recorded and the answer is [None], + so the caller drops this one item and goes on to the next. *) +let caught s f = + if not s.on then Some (f ()) + else + match f () with + | x -> Some x + | exception Error d -> s.found <- d :: s.found; None + +let any s = s.found <> [] + +(** Raise everything found, in the order it was found, or return if the pass + was clean. *) +let finish s = + match List.rev s.found with [] -> () | ds -> raise (Errors ds) + (* ── Reporting ────────────────────────────────────────────────────── The first line of every entry is exactly [file:line:col: message], which is diff --git a/lib/parse.ml b/lib/parse.ml index 135d54c..21dbacd 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -866,14 +866,25 @@ and variant (f : Form.t) : Ast.variant = unknown name, which is wrong but not silent. *) let expander : (Form.t list -> Form.t list) ref = ref (fun fs -> fs) -let program (forms : Form.t list) : Ast.decl list = +(* [keep_going] asks for every bad declaration in the file rather than the + first. The resync point is a top-level form, and it is the only honest one + here: the reader already found where each declaration ends, so skipping a + bad one costs nothing and cannot lose its place. Inside a declaration there + is no such landmark, so one bad [defn] is one error. + + Off by default, because the daemon parses one form at a time and wants one + exception. *) +let program ?(keep_going = false) (forms : Form.t list) : Ast.decl list = (* Quasiquote first and always, because it is pure and needs nothing loaded: it is what turns a macro body into ordinary code, and the prelude's own macros have to parse in a process that has not built a macro module yet. Then expansion, which may need one. *) let forms = !expander (List.map Expand.quasiquote forms) in temps := 0; - List.map decl forms + let s = Loc.sink ~on:keep_going in + let decls = List.filter_map (fun f -> Loc.caught s (fun () -> decl f)) forms in + Loc.finish s; + decls (* Single-declaration entry point, for tests and the REPL. *) let decl (f : Form.t) : Ast.decl =