A call through a failed local is not reported and a miscounted call's arguments are, eval-in-frame answers slotless frames and ended stops, flan dev links its own agent, and the daemon buffer colours only file:line:col diagnostics

This commit is contained in:
Joseph Ferano 2026-09-25 16:47:46 +07:00
parent f32f88413e
commit 37938db7c2
9 changed files with 284 additions and 149 deletions

View File

@ -2015,6 +2015,11 @@ because =locals= and the inspector are asked by it. The innermost frame is
shown even when it is the prelude's, unless the stop is =(pause)=, because it shown even when it is the prelude's, unless the stop is =(pause)=, because it
is where the program stopped. Rules out renumbering the visible frames. is where the program stopped. Rules out renumbering the visible frames.
** DONE C-c C-c reports one error, not every error in the form
CLOSED: [2026-09-25]
Every error at any depth: a refused subexpression stands as a Never that fits
any want, and what it causes is left unsaid. Rules out stopping at a statement boundary.
** DONE There is no stepper ** DONE There is no stepper
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
C-c C-s instruments a defn with a step point before each body form; no step C-c C-s instruments a defn with a step point before each body form; no step

View File

@ -1195,6 +1195,7 @@ Use `C-c C-g` if you need frames.
| `C-u C-c C-c` | ...and stop at the form point is inside (`C-u C-u`: on entry) | | `C-u C-c C-c` | ...and stop at the form point is inside (`C-u C-u`: on entry) |
| `C-M-x` | the same as `C-c C-c`, on the binding SLIME and CIDER use | | `C-M-x` | the same as `C-c C-c`, on the binding SLIME and CIDER use |
| `C-c C-k` | load the whole buffer, as one module; what does not compile is listed | | `C-c C-k` | load the whole buffer, as one module; what does not compile is listed |
| `C-c C-s` | install the defn at point to stop before each form of its body |
| `C-x C-e` | the form before point, evaluated — or installed, if it is a declaration | | `C-x C-e` | the form before point, evaluated — or installed, if it is a declaration |
| `C-u C-x C-e` | ...and stop at it instead of showing its value | | `C-u C-x C-e` | ...and stop at it instead of showing its value |
| `C-c C-z` | connect (finds `.flan-dev.sock` upward) | | `C-c C-z` | connect (finds `.flan-dev.sock` upward) |

View File

@ -428,6 +428,15 @@ is switched on here, first, or the rules would never be drawn."
(font-lock-mode 1) (font-lock-mode 1)
;; A log, not source: a quote the program printed opens no string. ;; A log, not source: a quote the program printed opens no string.
(setq-local font-lock-keywords-only t) (setq-local font-lock-keywords-only t)
;; Only the compiler's own shape, `file:line:col:', is a diagnostic here.
;; compile.el's other rules are for a build log: one of them draws any
;; line starting `word:' as a program name, which is every `score: 10'
;; the program prints.
(setq-local compilation-mode-font-lock-keywords nil)
(setq-local compilation-error-regexp-alist
'(("^\\([^ \t\n:][^\t\n:]*\\):\\([0-9]+\\):\\([0-9]+\\): \
\\(?:\\(warning\\)\\|\\(note\\|info\\)\\)?"
1 2 3 (4 . 5))))
(compilation-minor-mode 1) (compilation-minor-mode 1)
(flan--navigable-notes)) (flan--navigable-notes))

View File

@ -1540,6 +1540,30 @@ would be overwritten. Look again and re-do the edit")
(and (eq (lookup-key flan-cnr-mode-map "s") #'flan-cnr-step) (and (eq (lookup-key flan-cnr-mode-map "s") #'flan-cnr-step)
(eq (lookup-key flan-cnr-mode-map "c") #'flan-cnr-continue)))) (eq (lookup-key flan-cnr-mode-map "c") #'flan-cnr-continue))))
;; Every key flan-mode binds has a row in the manual's key reference.
(let ((text (with-temp-buffer
(insert-file-contents
(expand-file-name "MANUAL.md"
(file-name-directory (locate-library "flan-mode"))))
(buffer-string)))
(missing nil))
(map-keymap
(lambda (k d)
(when (and (eq k ?\C-c) (keymapp d))
(map-keymap
(lambda (k2 d2)
(when (commandp d2)
(let ((desc (replace-regexp-in-string
"RET" "C-m"
(replace-regexp-in-string
"TAB" "C-i" (key-description (vector k k2))))))
(unless (string-match-p (regexp-quote (format "| `%s` |" desc)) text)
(push desc missing)))))
d)))
flan-mode-map)
(test-flan--check (format "every C-c key has a row in the manual (missing %s)" missing)
(null missing)))
;; C-c C-s sends the defn at point for stepping and marks it. ;; C-c C-s sends the defn at point for stepping and marks it.
(let ((sent nil)) (let ((sent nil))
(with-temp-buffer (with-temp-buffer

View File

@ -545,7 +545,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
"/tmp/a.flan:3:1: warning: w\n" "/tmp/a.flan:3:1: warning: w\n"
"/tmp/a.flan:4:1: note: n\n") "/tmp/a.flan:4:1: note: n\n")
(flan--daemon-buffer-setup) (flan--daemon-buffer-setup)
(flan--append-output "said hi\n") (flan--append-output "said hi\nscore: 10\n")
(font-lock-ensure) (font-lock-ensure)
(let ((face-on (lambda (text) (let ((face-on (lambda (text)
(goto-char (point-min)) (goto-char (point-min))
@ -559,6 +559,12 @@ already rely on it — so nothing here is a stand-in for the real thing."
(test-flan--check (test-flan--check
"the program's output takes its own face" "the program's output takes its own face"
(eq (funcall face-on "said") 'flan-output-face)) (eq (funcall face-on "said") 'flan-output-face))
(test-flan--check
"a line of output shaped `word:' is not drawn as a program name"
(progn (goto-char (point-min)) (search-forward "score")
(and (null (get-text-property (match-beginning 0) 'face))
(eq (get-text-property (match-beginning 0) 'font-lock-face)
'flan-output-face))))
(test-flan--check (test-flan--check
"the daemon's own line is left plain" "the daemon's own line is left plain"
(null (funcall face-on "flan dev:"))))) (null (funcall face-on "flan dev:")))))

View File

@ -4252,7 +4252,17 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
if is_poison r then env.poison <- env.poison + 1; if is_poison r then env.poison <- env.poison + 1;
r r
| exception Loc.Error d -> | exception Loc.Error d ->
if not (caused ()) then record_recovered env d; if not (caused ()) then begin
record_recovered env d;
(* A call refused as a whole — the wrong number of arguments, say —
never checked its arguments, and a mistake inside one is still a
mistake. They are checked on their own, with no expectation, so
only what no expectation could change is kept: a name that is not
there. *)
match e.Ast.e with
| Ast.Call (_, args) -> recheck_args ctx args
| _ -> ()
end;
env.poison <- env.poison + 1; env.poison <- env.poison + 1;
poison e.Ast.loc poison e.Ast.loc
(* A checker arm that was never written for a [Never] operand may fail (* A checker arm that was never written for a [Never] operand may fail
@ -4263,6 +4273,20 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
poison e.Ast.loc poison e.Ast.loc
end end
and recheck_args ctx (args : Ast.expr list) =
let env = ctx.env in
let before = env.recovered in
List.iter (fun a -> ignore (check ctx a)) args;
let rec fresh l = if l == before then [] else match l with [] -> [] | d :: r -> d :: fresh r in
let kept =
List.filter
(fun (d : Loc.diag) ->
String.starts_with ~prefix:"check/unknown-" d.Loc.kind
|| String.equal d.Loc.kind "check/private")
(fresh env.recovered)
in
env.recovered <- kept @ before
and check_plain ctx ?want (e : Ast.expr) : Tast.expr = and check_plain ctx ?want (e : Ast.expr) : Tast.expr =
let place = ctx.place_ok in let place = ctx.place_ok in
ctx.place_ok <- false; ctx.place_ok <- false;
@ -10861,6 +10885,16 @@ and ordinary_call ctx ~want loc name args =
lifted body captures it by value and then calls the copy. [peek_outer] lifted body captures it by value and then calls the copy. [peek_outer]
rather than [capture] in the guard, because a guard must not take a copy rather than [capture] in the guard, because a guard must not take a copy
on its way to deciding what a form means. *) on its way to deciding what a form means. *)
(* A local bound to a refused initialiser's stand-in, called: the refusal
is already reported, so the call stands in too, its arguments still
checked. *)
| _ when ctx.env.recovering
&& (match lookup ctx name with
| Some b -> Types.equal b.bty Types.Never
| None -> false) ->
List.iter (fun a -> ignore (check ctx a)) args;
ctx.env.poison <- ctx.env.poison + 1;
poison loc
| _ when (match lookup ctx name with | _ when (match lookup ctx name with
| Some b -> callable_ty b.bty | Some b -> callable_ty b.bty
| None -> | None ->

View File

@ -1246,6 +1246,18 @@ let eval_expr_at t ~code ~origin ~pause ~at =
TODO.org, "Whose break it is, which no counter answers" has it. *) TODO.org, "Whose break it is, which no counter answers" has it. *)
let entered = state t in let entered = state t in
let entered_gen = stop_gen t in let entered_gen = stop_gen t in
(* In a frame the thunk is addressed to one stop, and the agent drops it,
and counts the drop, if that stop has ended by the time it is
claimed. Read before the build, which is when that usually happens. *)
let refused_before = if at = None then None else refusals t in
let dropped () =
match refused_before with
| None -> None
| Some (before, _) ->
(match refusals t with
| Some (now, why) when now > before -> Some why
| _ -> None)
in
t.n <- t.n + 1; t.n <- t.n + 1;
let out = Filename.concat t.dir (Printf.sprintf "e%d.so" t.n) in let out = Filename.concat t.dir (Printf.sprintf "e%d.so" t.n) in
(* A generic called at a new type makes a copy that is defined in this (* A generic called at a new type makes a copy that is defined in this
@ -1417,6 +1429,9 @@ let eval_expr_at t ~code ~origin ~pause ~at =
| Some answer -> | Some answer ->
(match value () with Some v -> `Value v | None -> answer) (match value () with Some v -> `Value v | None -> answer)
| None -> | None ->
match dropped () with
| Some why -> `Dropped why
| None ->
if ms <= 0 then `Timeout if ms <= 0 then `Timeout
else begin else begin
ignore (Unix.select [] [] [] 0.005); ignore (Unix.select [] [] [] 0.005);
@ -1481,6 +1496,7 @@ let eval_expr_at t ~code ~origin ~pause ~at =
left over, which is the shape it was always about: a program left over, which is the shape it was always about: a program
that is running, is not parked, and produced nothing in five that is running, is not parked, and produced nothing in five
seconds. *) seconds. *)
| `Dropped why -> error why
| `Timeout -> | `Timeout ->
if liveness t = Parked then if liveness t = Parked then
error error
@ -2425,7 +2441,7 @@ let stopped_frame t ~frame ~what : (string * Tast.fn, string) result =
that stopped frame's locals — see [Session.in_frame]. The frame is checked that stopped frame's locals — see [Session.in_frame]. The frame is checked
the way [locals] and [inspect] check it, and the thunk is delivered at this the way [locals] and [inspect] check it, and the thunk is delivered at this
stop only. *) stop only. *)
let eval_expr ?frame t ~code ~origin ~pause = let eval_expr ?frame ?at_stop t ~code ~origin ~pause =
match frame with match frame with
| None -> eval_expr_at t ~code ~origin ~pause ~at:None | None -> eval_expr_at t ~code ~origin ~pause ~at:None
| Some index -> | Some index ->
@ -2438,7 +2454,17 @@ let eval_expr ?frame t ~code ~origin ~pause =
"the program resumed while this was being asked; there is no frame \ "the program resumed while this was being asked; there is no frame \
to evaluate in any more" to evaluate in any more"
| Some gen -> | Some gen ->
(match bound_slots t ~frame:index with (* A frame with no slots has no locals to bind, and the program
has no table to answer for it: the expression sees globals. *)
let bound =
if Array.length fn.Tast.slots = 0 then Ok []
else bound_slots t ~frame:index
in
(* [at_stop] is the stop the editor drew the frame at. It is not
checked here: the agent refuses a thunk addressed to a stop that
is over, and says so. *)
let gen = Option.value ~default:gen at_stop in
(match bound with
| Error m -> | Error m ->
error ("the program refused to say which slots are bound: " ^ m) error ("the program refused to say which slots are bound: " ^ m)
| Ok bound -> | Ok bound ->
@ -4444,7 +4470,8 @@ and handle_op t req =
| Some { Form.v = Form.Sym "nil"; _ } | None -> false | Some { Form.v = Form.Sym "nil"; _ } | None -> false
| Some _ -> true | Some _ -> true
in in
eval_expr ?frame:(Wire.int_field req "frame") t ~code ~origin ~pause eval_expr ?frame:(Wire.int_field req "frame")
?at_stop:(Wire.int_field req "at-stop") t ~code ~origin ~pause
| None -> error "eval-expr needs :code") | None -> error "eval-expr needs :code")
(* [:all], absent or [nil] being false and anything else true — the spelling (* [:all], absent or [nil] being false and anything else true — the spelling
[:pause], [:on] and [:reset] already use. One step is the default because [:pause], [:on] and [:reset] already use. One step is the default because
@ -5229,19 +5256,21 @@ let need_main ~file (session : Session.t) =
(* The agent's C, in every program [flan dev] builds, whether or not the source (* The agent's C, in every program [flan dev] builds, whether or not the source
imports the package: its constructor binds the socket before [main], so a imports the package: its constructor binds the socket before [main], so a
file that never mentions the agent can still be reached from the editor. A file that never mentions the agent can still be reached from the editor.
program that imports it already has it, and is left alone — two copies
would collide at the link. A release build is not built here and is not Always this compiler's own copy, in place of any the program vendors. The
affected. *) agent is the daemon's other half — the stack it snapshots and the verbs it
answers are what this file reads — and a program outside this repository
carries whatever copy of vendor/agent it was given, however old. An old one
builds and answers, and then puts every frame at its function's own line,
because it predates the call-site record, so the stepper never moves. One
copy and not two: two would collide at the link. A release build is not
built here. *)
let with_agent ~dir csrcs lflags = let with_agent ~dir csrcs lflags =
let c = Filename.concat dir "flan_agent.c" in
write_file c Runtime_src.agent_source;
let csrcs = let csrcs =
if List.exists (fun c -> Filename.basename c = "flan_agent.c") csrcs then List.filter (fun x -> Filename.basename x <> "flan_agent.c") csrcs @ [ c ]
csrcs
else begin
let c = Filename.concat dir "flan_agent.c" in
write_file c Runtime_src.agent_source;
csrcs @ [ c ]
end
in in
let lflags = let lflags =
lflags lflags

View File

@ -127,6 +127,111 @@ let status r =
let contains_sub = Test_support.contains let contains_sub = Test_support.contains
(* The stepper, against a running program whose [step] is dev-pause.flan's
and dev-repl.flan's: sent with [:step t], a call stops before each form of
its body — the (set ...), then after [next] the [ticks] it answers.
[continue] runs the rest of the call and the next call steps again, and a
plain evaluation takes it out. Run in a daemon of each backend that another
block already started, so it costs no build of its own. *)
let stepper_checks ~what ask =
let stopped r =
match Wire.field r "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true
| _ -> false
in
let body = "(defn step [] i64 (set ticks (+ ticks 1)) ticks)" in
let col sub =
let n = String.length sub in
let rec find i =
if String.equal (String.sub body i n) sub then i + 1 else find (i + 1)
in
find 0
in
(* Where the stepped frame is: the frame of [step], whose location is
the step point's, which is the form about to run. *)
let at () =
match Wire.field (ask "(:op \"backtrace\")") "frames" with
| Some { Form.v = Form.List l; _ } ->
List.find_map
(fun (f : Form.t) ->
match f.Form.v with
| Form.List ({ Form.v = Form.Str "step"; _ }
:: { Form.v = Form.Str loc; _ } :: _) -> Some loc
| _ -> None)
l
| _ -> None
in
let stops_at sub =
let want = Printf.sprintf ":1:%d" (col sub) in
await (fun () ->
stopped (ask "(:op \"describe\")")
&& (match at () with Some l -> contains_sub l want | None -> false))
in
let r =
ask
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/step.flan\" :step t)"
(Wire.quote body))
in
if status r <> "ok" then
fail "%sinstrumenting for the stepper: %s" what
(Option.value ~default:"" (Wire.string_field r "message"))
else begin
if Wire.field r "step" = None then
fail "%san instrumented defn did not echo :step" what;
if not (stops_at "(set ticks") then
fail "%sthe stepper did not stop before the first form (at %s)" what
(Option.value ~default:"<none>" (at ()))
else begin
(match Wire.string_field (ask "(:op \"describe\")") "condition" with
| Some "StepPoint" -> ()
| c -> fail "%sa step stopped on %s" what (Option.value ~default:"<none>" c));
(* The stepper's own local is not one of the frame's. *)
let fr = ask "(:op \"locals\" :frame 1)" in
(match Wire.string_field fr "frame" with
| Some "step" ->
(match Wire.field fr "locals" with
| Some { Form.v = Form.List []; _ } | None -> ()
| _ -> fail "%sthe stepper's flag is listed as a local" what)
| f -> fail "%sframe 1 at a step is %s" what (Option.value ~default:"<none>" f));
let r = ask "(:op \"restart\" :name \"next\")" in
if status r <> "ok" then
fail "%snext at a step: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
if not (stops_at "ticks)") then
fail "%snext did not stop before the second form (at %s)" what
(Option.value ~default:"<none>" (at ()));
let r = ask "(:op \"restart\" :name \"continue\")" in
if status r <> "ok" then
fail "%scontinue at a step: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
(* The next call, 5ms on, steps again from the top. *)
if not (stops_at "(set ticks") then
fail "%sthe next call did not step again" what;
let r =
ask
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/step.flan\")"
(Wire.quote body))
in
if status r <> "ok" then
fail "%sinstalling the plain defn: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
ignore (ask "(:op \"restart\" :name \"continue\")");
if not (await (fun () -> not (stopped (ask "(:op \"describe\")")))) then
fail "%sthe program did not resume from the last step" what;
let deadline = Unix.gettimeofday () +. 0.5 in
let rec run_on () =
if Unix.gettimeofday () > deadline then ()
else if stopped (ask "(:op \"describe\")") then
fail "%sthe plain defn still steps" what
else begin
ignore (Unix.select [] [] [] 0.01);
run_on ()
end
in
run_on ()
end
end
(* Eval-in-frame against dev-locals.flan's [look], stopped at its (error ...): (* Eval-in-frame against dev-locals.flan's [look], stopped at its (error ...):
the expression sees that frame's locals, the inner of two [label]s wins, a the expression sees that frame's locals, the inner of two [label]s wins, a
[set] writes the frame's own storage, and a local not bound yet is refused [set] writes the frame's own storage, and a local not bound yet is refused
@ -168,9 +273,19 @@ let eval_in_frame_checks ~backend ask =
| Error m when contains_sub m "after is not bound yet" -> () | Error m when contains_sub m "after is not bound yet" -> ()
| Error m -> fail "%s eval-in-frame of an unbound local said %s" backend m | Error m -> fail "%s eval-in-frame of an unbound local said %s" backend m
| Ok v -> fail "%s eval-in-frame read an unbound local as %s" backend v); | Ok v -> fail "%s eval-in-frame read an unbound local as %s" backend v);
match value "(+ n \"x\")" with (match value "(+ n \"x\")" with
| Error _ -> () | Error _ -> ()
| Ok v -> fail "%s eval-in-frame accepted a type error: %s" backend v | Ok v -> fail "%s eval-in-frame accepted a type error: %s" backend v);
(* Addressed to a stop that is over: the agent drops it, and the reply says
why at once rather than timing out. *)
let t0 = Unix.gettimeofday () in
let r = ask "(:op \"eval-expr\" :frame 0 :at-stop 999999 :code \"n\")" in
let m = Option.value ~default:"" (Wire.string_field r "message") in
if status r = "ok" then fail "%s eval-in-frame ran at a stop that is over" backend
else if not (contains_sub m "resumed" || contains_sub m "stopped again") then
fail "%s eval-in-frame at a stop that is over said %s" backend m
else if Unix.gettimeofday () -. t0 > 4.0 then
fail "%s eval-in-frame at a stop that is over waited out the clock" backend
(* ── The one verb whose reply races the process it ends ─────────────── *) (* ── The one verb whose reply races the process it ends ─────────────── *)
@ -239,7 +354,21 @@ let () =
explain and is quoted as it stands. *) explain and is quoted as it stands. *)
let other = Dev.refusal ~parked:true "err flan.abi.x86: the module is x86" in let other = Dev.refusal ~parked:true "err flan.abi.x86: the module is x86" in
if not (contains_sub other "flan.abi.x86") then if not (contains_sub other "flan.abi.x86") then
fail "a parked program's other refusals were rewritten too: %S" other fail "a parked program's other refusals were rewritten too: %S" other;
(* The agent is always this compiler's, never the copy a program vendors:
an old copy answers every frame at its function's own line. *)
let dir = tmp "agent-dir" in
(try Unix.mkdir dir 0o700 with Unix.Unix_error _ -> ());
let csrcs, _ =
Dev.with_agent ~dir [ "/far/vendor/agent/flan_agent.c"; "/far/x.c" ] []
in
let own = Filename.concat dir "flan_agent.c" in
if csrcs <> [ "/far/x.c"; own ] then
fail "a vendored agent was linked in place of the compiler's: %s"
(String.concat " " csrcs)
else if In_channel.with_open_bin own In_channel.input_all
<> Runtime_src.agent_source then
fail "the agent linked is not the compiler's own"
(* A daemon's death is reported by the signal's name. [WSIGNALED] carries (* A daemon's death is reported by the signal's name. [WSIGNALED] carries
OCaml's own numbering, in which SIGTERM is -11, and a SIGTERM printed as OCaml's own numbering, in which SIGTERM is -11, and a SIGTERM printed as
@ -4418,6 +4547,7 @@ let () =
end end
else begin else begin
let c = connect sigsock in let c = connect sigsock in
stepper_checks ~what:"llvm " (request c);
let stopped r = let stopped r =
match Wire.field r "stopped" with match Wire.field r "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true | Some { Form.v = Form.Sym "t"; _ } -> true
@ -4882,6 +5012,13 @@ let () =
if not (List.exists (String.equal "continue") names) then if not (List.exists (String.equal "continue") names) then
fail "a break at (pause) offers %s, wanted continue among them" fail "a break at (pause) offers %s, wanted continue among them"
(String.concat ", " names); (String.concat ", " names);
(* [step] has no slots at all, so there is nothing to bind and no
table for the program to answer from: an expression evaluated
in its frame sees the globals. Frame 0 is [pause]'s own. *)
(let r = ask "(:op \"eval-expr\" :frame 1 :code \"(+ ticks 0)\")" in
if status r <> "ok" then
fail "eval-in-frame of a frame with no slots: %s"
(Option.value ~default:(status r) (Wire.string_field r "message")));
(* The second claim, and the one this block exists for. A plain (* The second claim, and the one this block exists for. A plain
re-evaluation of the same form replaces the stored declaration re-evaluation of the same form replaces the stored declaration
@ -4927,6 +5064,7 @@ let () =
end end
end end
end; end;
stepper_checks ~what:"x86 " ask;
(* The other way a thunk reaches a [(pause)], and the one no flag asks (* The other way a thunk reaches a [(pause)], and the one no flag asks
for: an ordinary [C-x C-e] over an expression that calls a body for: an ordinary [C-x C-e] over an expression that calls a body
@ -8799,135 +8937,6 @@ let () =
hook_block ~llvm:false; hook_block ~llvm:false;
hook_block ~llvm:true; hook_block ~llvm:true;
(* ── The stepper ─────────────────────────────────────────────────
[dev-pause.flan] calls [step] every 5ms. Sent with [:step t], a call
stops before each form of its body: first the (set ...), then, after
[next], the [ticks] it answers. [continue] runs the rest of the call,
and the next call steps again. A plain evaluation takes it out. Under
both backends, each with its own daemon. *)
let stepper ~llvm =
let what = if llvm then "llvm " else "x86 " in
let ssock = tmp (what ^ "step.sock") and sout = tmp (what ^ "step.out") in
(try Sys.remove ssock with Sys_error _ -> ());
let sfd = Unix.openfile sout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let spid =
Unix.create_process flan
(Array.append
[| flan; "dev"; "programs/dev-pause.flan"; "-s"; ssock |]
(if llvm then [| "--llvm" |] else [||]))
Unix.stdin sfd Unix.stderr
in
Unix.close sfd;
if not (listening ~pid:spid ssock) then
fail "%sstepper daemon %s" what !listen_why
else begin
let c = connect ssock in
let ask = request c in
let stopped r =
match Wire.field r "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true
| _ -> false
in
let body = "(defn step [] i64 (set ticks (+ ticks 1)) ticks)" in
let col sub =
let n = String.length sub in
let rec find i =
if String.equal (String.sub body i n) sub then i + 1 else find (i + 1)
in
find 0
in
(* Where the stepped frame is: the frame of [step], whose location is
the step point's, which is the form about to run. *)
let at () =
match Wire.field (ask "(:op \"backtrace\")") "frames" with
| Some { Form.v = Form.List l; _ } ->
List.find_map
(fun (f : Form.t) ->
match f.Form.v with
| Form.List ({ Form.v = Form.Str "step"; _ }
:: { Form.v = Form.Str loc; _ } :: _) -> Some loc
| _ -> None)
l
| _ -> None
in
let stops_at sub =
let want = Printf.sprintf ":1:%d" (col sub) in
await (fun () ->
stopped (ask "(:op \"describe\")")
&& (match at () with Some l -> contains_sub l want | None -> false))
in
let r =
ask
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/step.flan\" :step t)"
(Wire.quote body))
in
if status r <> "ok" then
fail "%sinstrumenting for the stepper: %s" what
(Option.value ~default:"" (Wire.string_field r "message"))
else begin
if Wire.field r "step" = None then
fail "%san instrumented defn did not echo :step" what;
if not (stops_at "(set ticks") then
fail "%sthe stepper did not stop before the first form (at %s)" what
(Option.value ~default:"<none>" (at ()))
else begin
(match Wire.string_field (ask "(:op \"describe\")") "condition" with
| Some "StepPoint" -> ()
| c -> fail "%sa step stopped on %s" what (Option.value ~default:"<none>" c));
(* The stepper's own local is not one of the frame's. *)
let fr = ask "(:op \"locals\" :frame 1)" in
(match Wire.string_field fr "frame" with
| Some "step" ->
(match Wire.field fr "locals" with
| Some { Form.v = Form.List []; _ } | None -> ()
| _ -> fail "%sthe stepper's flag is listed as a local" what)
| f -> fail "%sframe 1 at a step is %s" what (Option.value ~default:"<none>" f));
let r = ask "(:op \"restart\" :name \"next\")" in
if status r <> "ok" then
fail "%snext at a step: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
if not (stops_at "ticks)") then
fail "%snext did not stop before the second form (at %s)" what
(Option.value ~default:"<none>" (at ()));
let r = ask "(:op \"restart\" :name \"continue\")" in
if status r <> "ok" then
fail "%scontinue at a step: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
(* The next call, 5ms on, steps again from the top. *)
if not (stops_at "(set ticks") then
fail "%sthe next call did not step again" what;
let r =
ask
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/step.flan\")"
(Wire.quote body))
in
if status r <> "ok" then
fail "%sinstalling the plain defn: %s" what
(Option.value ~default:"" (Wire.string_field r "message"));
ignore (ask "(:op \"restart\" :name \"continue\")");
if not (await (fun () -> not (stopped (ask "(:op \"describe\")")))) then
fail "%sthe program did not resume from the last step" what;
let deadline = Unix.gettimeofday () +. 0.5 in
let rec run_on () =
if Unix.gettimeofday () > deadline then ()
else if stopped (ask "(:op \"describe\")") then
fail "%sthe plain defn still steps" what
else begin
ignore (Unix.select [] [] [] 0.01);
run_on ()
end
in
run_on ()
end
end;
(try Unix.close c with Unix.Unix_error _ -> ())
end;
(try Unix.kill spid Sys.sigkill with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] spid) with Unix.Unix_error _ -> ());
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ ssock; sout ]
in
stepper ~llvm:false;
stepper ~llvm:true;
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ sock; out; bsock; bout ]; [ sock; out; bsock; bout ];

View File

@ -1795,6 +1795,24 @@ let () =
in in
if List.length caused <> 1 || not (has (msgs caused) "nope") then if List.length caused <> 1 || not (has (msgs caused) "nope") then
fail "a failure's consequences were reported: %s" (msgs caused); fail "a failure's consequences were reported: %s" (msgs caused);
(* A local bound to a refused initialiser and then called is the same
consequence: no "unknown function p". *)
let called = errors "(defn called [] i64 (let [p (nope 1)] (p 3)))" in
if List.length called <> 1 then
fail "calling a local bound to a failure was reported: %s" (msgs called);
(* A call refused for its argument count still has its arguments checked. *)
let arity =
errors "(defn arity [] i64 (bump (nope2) 7 8))"
in
if List.length arity <> 2 || not (has (msgs arity) "nope2") then
fail "an error inside a miscounted call was not reported: %s" (msgs arity);
(* And the whole-file path: fn-no-type.flan has one mistake, reported once. *)
(match Front.checked ~all:true "programs/fn-no-type.flan" with
| _ -> fail "fn-no-type.flan checked"
| exception Loc.Error _ -> ()
| exception Loc.Errors ds ->
if List.length ds <> 1 then
fail "fn-no-type.flan gave %d errors: %s" (List.length ds) (msgs ds));
(* One error is the [Loc.Error] every caller of one form expects. *) (* One error is the [Loc.Error] every caller of one form expects. *)
let t, _ = Session.create ~file:"programs/reload.flan" () in let t, _ = Session.create ~file:"programs/reload.flan" () in
(match Session.eval t "(defn one [] i64 (nope 1))" with (match Session.eval t "(defn one [] i64 (nope 1))" with