A file with a broken form still starts a session, a package's macros expand bare from its file, a delivery a running program never polls for says so with a fix that compiles, and aborting an expression stopped in the park abandons it and keeps the session

This commit is contained in:
Joseph Ferano 2026-09-25 13:06:36 +07:00
parent b74002be02
commit f87d596bdc
9 changed files with 230 additions and 43 deletions

View File

@ -285,8 +285,10 @@ SLIME and CIDER. It is sent as **one** module rather than as a form at a time.
That matters: a `defonce` and the function that uses it have to arrive together, That matters: a `defonce` and the function that uses it have to arrive together,
or the function refers to storage that does not exist yet. or the function refers to storage that does not exist yet.
A form that does not compile is left out, and so is any form that uses it; the A form that does not compile is left out, and the rest are installed. A name
rest are installed. Each form left out is marked in the buffer and listed in that was already in the program keeps its earlier definition, and the forms that
use it are compiled against that one; a form that uses a name defined nowhere
else is left out too. Each form left out is marked in the buffer and listed in
`*flan-diagnostics*`, and the echo area counts them. When nothing compiles, `*flan-diagnostics*`, and the echo area counts them. When nothing compiles,
nothing is installed and the first error is reported as `C-c C-c` reports one. nothing is installed and the first error is reported as `C-c C-c` reports one.
@ -1184,7 +1186,6 @@ 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-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

@ -537,7 +537,12 @@ with it, and a rejected evaluation is a likely moment to *become* stopped."
(unless (eq was now) (unless (eq was now)
(force-mode-line-update t) (force-mode-line-update t)
(when now (when now
(message "flan: the program finished; C-c C-M-x runs it again")))) ;; `:main nil' is a file with no main of its own: nothing ran, and
;; there is nothing to run again until one is loaded.
(if (and (plist-member reply :main) (null (plist-get reply :main)))
(message "flan: the file has no main; C-x C-e and C-c C-k work \
on the session, and C-c C-M-x runs main once one is loaded")
(message "flan: the program finished; C-c C-M-x runs it again")))))
(let ((was flan--stopped) (let ((was flan--stopped)
(now (and (plist-get reply :stopped) (now (and (plist-get reply :stopped)
(or (plist-get reply :condition) "a condition")))) (or (plist-get reply :condition) "a condition"))))
@ -1290,7 +1295,8 @@ one process would be writing the same globals at once."
(defun flan-abort () (defun flan-abort ()
"Let the stopped program die where it stopped. "Let the stopped program die where it stopped.
This ends `flan dev' too: the daemon owns the program's lifetime and has This ends `flan dev' too: the daemon owns the program's lifetime and has
nothing left to serve once it has gone." nothing left to serve once it has gone. An expression that stopped while the
program was parked is abandoned instead, and the session stays."
(interactive) (interactive)
(let ((r (flan--request '(:op "abort")))) (let ((r (flan--request '(:op "abort"))))
(if (equal (plist-get r :status) "ok") (if (equal (plist-get r :status) "ok")
@ -1690,7 +1696,6 @@ Returns non-nil when it put an overlay somewhere."
(when buf (when buf
(with-current-buffer buf (with-current-buffer buf
(unless keep (flan-clear-errors buf)) (unless keep (flan-clear-errors buf))
;; A refusal is not a value, and the two must never be drawn over ;; A refusal is not a value, and the two must never be drawn over
;; one form at once. Ordinarily the command that ran this ;; one form at once. Ordinarily the command that ran this
;; evaluation already cleared the last one through the hook; this ;; evaluation already cleared the last one through the hook; this
@ -2875,8 +2880,10 @@ Every top-level form is compiled and installed together, as one module: a var
and the function that uses it have to arrive in the same load or the first and the function that uses it have to arrive in the same load or the first
refers to storage that does not exist. refers to storage that does not exist.
A form that does not compile is left out, and so is a form that uses it; the A form that does not compile is left out, and the rest are installed. A name
rest are installed. Each one left out is marked in the buffer and listed in already in the program keeps its earlier definition, and forms that use it are
compiled against that one; a form that uses a name defined nowhere else is
left out too. Each one left out is marked in the buffer and listed in
`flan-diagnostics-buffer'. When nothing compiles, nothing is installed and `flan-diagnostics-buffer'. When nothing compiles, nothing is installed and
the command signals, as `C-c C-c' does." the command signals, as `C-c C-c' does."
(interactive) (interactive)

View File

@ -2405,6 +2405,12 @@ already rely on it — so nothing here is a stand-in for the real thing."
(flan scratch socket6) (flan scratch socket6)
(test-flan--check "a daemon starts on a file with no main" (test-flan--check "a daemon starts on a file with no main"
(process-live-p flan--connection)) (process-live-p flan--connection))
(let* ((reply (flan--request '(:op "describe")))
(said (progn (setq flan--parked nil)
(test-flan--said (flan--absorb reply)))))
(test-flan--check "a parked session with no main does not say a program finished"
(and said (string-match-p "has no main" said)
(not (string-match-p "finished" said)))))
(test-flan--check "and an expression reaches the file's functions" (test-flan--check "and an expression reaches the file's functions"
(equal (funcall value "(fib 10)") "55")) (equal (funcall value "(fib 10)") "55"))
(with-current-buffer (find-file-noselect scratch) (with-current-buffer (find-file-noselect scratch)

View File

@ -137,6 +137,14 @@ let await ?(ms = 5000) f =
(* Every program [flan dev] builds has the agent linked — see [with_agent] — so (* Every program [flan dev] builds has the agent linked — see [with_agent] — so
a program that cannot be reached is one whose socket went away, not one that a program that cannot be reached is one whose socket went away, not one that
never had an agent. *) never had an agent. *)
(* What a program does so that code from the editor reaches it while it runs.
Every program [flan dev] builds has the agent's C, but only the program can
decide where its frames end. Both forms compile as written. *)
let polls_for_it =
"Code from the editor runs where the program polls for it: import the \
agent with (import agent \"vendor:agent\") and call (agent/poll) once in \
each pass of the main loop"
let unreachable _t e = "cannot reach the program: " ^ Unix.error_message e let unreachable _t e = "cannot reach the program: " ^ Unix.error_message e
(* Whether the program has bound the socket it receives modules on. (* Whether the program has bound the socket it receives modules on.
@ -734,6 +742,11 @@ let with_break t reply =
let fields = let fields =
fields ^ (if liveness t = Parked then " :parked t" else " :parked nil") fields ^ (if liveness t = Parked then " :parked t" else " :parked nil")
in in
(* Only when there is no [main] of the file's own: a parked session over a
file without one finished nothing, and an editor should not say it did. *)
let fields =
if Session.has_main t.session then fields else fields ^ " :main nil"
in
String.sub reply 0 (String.length reply - 1) ^ fields ^ ")" String.sub reply 0 (String.length reply - 1) ^ fields ^ ")"
let error ?loc msg = let error ?loc msg =
@ -875,6 +888,37 @@ let refusal ~parked reply =
the module is still queued through the in-process call. A note saying the the module is still queued through the in-process call. A note saying the
program "has not called (agent/start ...)" named a cause that was not the program "has not called (agent/start ...)" named a cause that was not the
cause, so there is none. *) cause, so there is none. *)
(* A running program takes a queued module at its next [(agent/poll)], and one
that never polls never takes it. The agent always accepts, so "ok" alone
would report a change that does not land: the ring is watched for a moment
and a module still waiting at the end is said to be waiting, with the fix.
An agent without the [pending] verb (a program vendoring an older one) is
not asked twice. A stopped program polls from its break loop, and a parked
one gets its own note. *)
let unpolled_note t ~parked =
if parked || state t <> Running then []
else begin
let deadline = Unix.gettimeofday () +. 3. in
let rec taken () =
match int_of_string_opt (String.trim (request t "pending")) with
| None | Some 0 -> true
| Some _ ->
if Unix.gettimeofday () >= deadline then false
else begin
drain t;
ignore (Unix.select [] [] [] 0.005);
taken ()
end
| exception Unix.Unix_error _ -> true
in
if taken () then []
else
[ ":note "
^ Wire.quote
("queued, but the running program has not taken it in three \
seconds, so it has not reached a frame boundary. "
^ polls_for_it ^ "; it installs at the first poll") ]
end
let install_note t ~parked = let install_note t ~parked =
if parked then begin if parked then begin
@ -1018,6 +1062,7 @@ let eval ?forms ?base ?(extra = []) t ~code ~origin ~pause =
[ ":pause " ^ Wire.quote (Printf.sprintf "%d:%d" l c) ] [ ":pause " ^ Wire.quote (Printf.sprintf "%d:%d" l c) ]
| None -> []) | None -> [])
@ install_note t ~parked:parked_now @ install_note t ~parked:parked_now
@ unpolled_note t ~parked:parked_now
@ extra) @ extra)
| reply -> refused (refusal ~parked:parked_now reply) | reply -> refused (refusal ~parked:parked_now reply)
| exception Unix.Unix_error (e, _, _) -> | exception Unix.Unix_error (e, _, _) ->
@ -1424,8 +1469,8 @@ let eval_expr t ~code ~origin ~pause =
break buffer, and evaluate this again" break buffer, and evaluate this again"
else else
error error
"the program did not reach a frame boundary; is it calling \ ("the program did not reach a frame boundary in five seconds. "
(agent/poll)?") ^ polls_for_it))
(* A module that was taken is the program's from here on, whatever (* A module that was taken is the program's from here on, whatever
the wait then says: a timeout is a frame boundary not reached the wait then says: a timeout is a frame boundary not reached
yet, not a module refused, so the instances in it stay in the yet, not a module refused, so the instances in it stay in the
@ -2195,8 +2240,8 @@ let run_render_thunk ?(stopped_only = false) ?at_stop t ~tag
it stands now" it stands now"
else else
Error Error
"the program did not reach a frame boundary; is it calling \ ("the program did not reach a frame boundary in five seconds. "
(agent/poll)?" ^ polls_for_it)
in in
if ms <= 0 then gave_up () if ms <= 0 then gave_up ()
else begin else begin
@ -3402,11 +3447,28 @@ let abort t =
parked parked
"the program has already finished, so there is nothing to abort and \ "the program has already finished, so there is nothing to abort and \
nothing that would end by aborting it but this session" nothing that would end by aborting it but this session"
(* The one exception, and it is the same exception the restarts are: a thunk (* A thunk stopped in the break loop on the parked thread. Aborting it is
stopped in the break loop on the parked thread is something to abort, and abandoning the expression, not ending the process: the program had
for a condition with no restart worth taking it is the only thing that already finished, and the break loop offers the thunk's own boundary as a
ends it. What it costs is unchanged and is what the reply has always said restart, so that is what is taken and the session stays parked. A trap
— the process goes, and the session with it. *) offers nothing that can be taken, and there abort still ends the
process. *)
| Parked
when match restarts t with
| Ok (rs, false) -> List.exists (fun (_, f, _) -> f = Boundary) rs
| _ -> false ->
(match restarts t with
| Ok (rs, _) ->
let i, _, _ = List.find (fun (_, f, _) -> f = Boundary) rs in
(match ask t ("restart-at " ^ string_of_int i) with
| reply when accepted reply <> None ->
ok
[ ":note "
^ Wire.quote
"the evaluation is abandoned; the program is still parked, and anything the expression changed before it stopped stays changed" ]
| reply -> error (String.trim reply)
| exception Unix.Unix_error (e, _, _) -> error (unreachable t e))
| Error m -> error m)
| Live | Parked -> | Live | Parked ->
match ask t "abort" with match ask t "abort" with
| reply when String.trim reply = "ok" -> | reply when String.trim reply = "ok" ->
@ -4229,7 +4291,6 @@ let handle t req =
| code -> load_file t ~code ~origin | code -> load_file t ~code ~origin
| exception Sys_error m -> error ("cannot read the file: " ^ m)))) | exception Sys_error m -> error ("cannot read the file: " ^ m))))
| Some "describe" -> describe t | Some "describe" -> describe t
| Some "defs" -> defs t | Some "defs" -> defs t
| Some "break" -> break t | Some "break" -> break t
| Some "condition" -> condition_op t | Some "condition" -> condition_op t
@ -4956,10 +5017,7 @@ let make_session_dir ~file dir =
stub [Session.create_dev] gives a file with no [main] is no use to it. The stub [Session.create_dev] gives a file with no [main] is no use to it. The
one-process daemon takes such a file. *) one-process daemon takes such a file. *)
let need_main ~file (session : Session.t) = let need_main ~file (session : Session.t) =
if not if not (Session.has_main session) then
(List.exists (fun (f : Tast.fn) -> f.Tast.name = "main")
session.Session.host.Tast.fns)
then
failwith failwith
(Printf.sprintf (Printf.sprintf
"%s has no main, and flan dev --two-process runs the program as a \ "%s has no main, and flan dev --two-process runs the program as a \
@ -4991,9 +5049,9 @@ let with_agent ~dir csrcs lflags =
in in
(csrcs, lflags) (csrcs, lflags)
(* What [flan dev] says about the forms of a file with no [main] that did not (* What [flan dev] says about the forms of its file that did not compile. The
compile. The session starts without them; C-c C-k loads the file again once session starts without them; C-c C-k loads the file again once they are
they are fixed. *) fixed. *)
let report_dropped ~file = function let report_dropped ~file = function
| [] -> () | [] -> ()
| ds -> | ds ->
@ -5021,8 +5079,9 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
let dir = session_dir ~file ~sock in let dir = session_dir ~file ~sock in
let given = file in let given = file in
let file = try Unix.realpath file with Unix.Unix_error _ -> file in let file = try Unix.realpath file with Unix.Unix_error _ -> file in
let session, l = Session.create ~debug ~x86 ~file () in let session, l, dropped = Session.create_dev ~debug ~x86 ~file () in
need_main ~file:given session; need_main ~file:given session;
report_dropped ~file:given dropped;
make_session_dir ~file:given dir; make_session_dir ~file:given dir;
let csrcs, lflags = with_agent ~dir l.Load.csrcs l.Load.lflags in let csrcs, lflags = with_agent ~dir l.Load.csrcs l.Load.lflags in
let exe = Filename.concat dir "program" in let exe = Filename.concat dir "program" in
@ -6061,7 +6120,6 @@ let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () =
than that a program finished. *) than that a program finished. *)
Unix.putenv "FLAN_DEV_NO_MAIN" Unix.putenv "FLAN_DEV_NO_MAIN"
(if Session.has_main session then "0" else "1"); (if Session.has_main session then "0" else "1");
let agent = Filename.concat dir "agent.sock" in let agent = Filename.concat dir "agent.sock" in
(* Every one of these is read by the exec'd binary and by nothing else. They (* Every one of these is read by the exec'd binary and by nothing else. They
are set before the exec rather than by the compiler thread afterwards, so are set before the exec rather than by the compiler thread afterwards, so

View File

@ -333,21 +333,19 @@ let has_main t =
&& not (String.equal d.Ast.dloc.Loc.file stub_file)) && not (String.equal d.Ast.dloc.Loc.file stub_file))
t.decls t.decls
(* The session [flan dev] starts. A file with a [main] is [create]'s. A file (* The session [flan dev] starts: the file's forms loaded as [pruned] loads
without one is the empty image of SBCL's order: a [main] that returns at them, so what compiles is in the host and what does not is answered beside
once is added so there is a process to park, and the file's forms are the session rather than instead of it. A file with no [main] — or whose
loaded into it as [pruned] loads them — what compiles is in the host, and [main] is one of the forms left out — is the empty image of SBCL's order: a
what does not is answered beside the session rather than instead of it. A [main] that returns at once is added so there is a process to park, and a
[main] loaded later replaces the stub like any other redefinition. *) [main] loaded later replaces the stub like any other redefinition. *)
let create_dev ?(debug = false) ?(x86 = false) ~file () = let create_dev ?(debug = false) ?(x86 = false) ~file () =
let forms = Reader.read_file file in let build forms =
if declares_main forms then of_forms ~debug ~x86 ~file
let t, l = create ~debug ~x86 ~file () in (if declares_main forms then forms else forms @ stub_main ())
(t, l, []) in
else let (t, l), _, errs = pruned build (Reader.read_file file) in
let build forms = of_forms ~debug ~x86 ~file (forms @ stub_main ()) in (t, l, errs)
let (t, l), _, errs = pruned build forms in
(t, l, errs)
(* What a macro may call, for the same reason [macros] is held: an evaluation (* What a macro may call, for the same reason [macros] is held: an evaluation
parses one form with no import in sight, and a package macro whose body parses one form with no import in sight, and a package macro whose body
@ -420,6 +418,29 @@ let package_of t origin =
(* A name the running process exports. Everything else is looked up by name at (* A name the running process exports. Everything else is looked up by name at
install time — see [Emit.redefinition]'s [known]. *) install time — see [Emit.redefinition]'s [known]. *)
(* The macros a form sent from [origin] can call. From a package's file that
is the package's own under the bare names the file writes, in front of the
rest: the session holds them qualified, as the importer calls them, and a
bare call parsed without these is a call to an unknown function that the
qualification afterwards turns into a call to the macro's own name. *)
let macros_for t origin =
match package_of t origin with
| None -> t.macros
| Some p ->
let pre = p.Load.alias ^ "/" in
let bare =
List.filter_map
(fun (f : Form.t) ->
match f.Form.v with
| Form.List (hd :: ({ Form.v = Form.Sym n; _ } as nf) :: rest)
when String.starts_with ~prefix:pre n ->
let b = String.sub n (String.length pre) (String.length n - String.length pre) in
Some { f with Form.v = Form.List (hd :: { nf with Form.v = Form.Sym b } :: rest) }
| _ -> None)
t.macros
in
Load.macro_union bare t.macros
let known t n = let known t n =
List.exists (fun (f : Tast.fn) -> String.equal f.Tast.name n) t.host.Tast.fns List.exists (fun (f : Tast.fn) -> String.equal f.Tast.name n) t.host.Tast.fns
|| List.exists || List.exists
@ -766,7 +787,7 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(running = true) t src : chan
(* What an annotated listing quotes for this form is what was sent, not what (* What an annotated listing quotes for this form is what was sent, not what
the file on disk said when it was last read. *) the file on disk said when it was last read. *)
Loc.remember ~file:origin src; Loc.remember ~file:origin src;
Parse.with_imported ~decls:(package_decls t) t.macros @@ fun () -> Parse.with_imported ~decls:(package_decls t) (macros_for t origin) @@ fun () ->
(* Through [Load] like any other source, so an evaluated (import ...) means (* Through [Load] like any other source, so an evaluated (import ...) means
what it means in a file. Its expansion is what gets spliced, which is also what it means in a file. Its expansion is what gets spliced, which is also
why the accumulated list is the post-Load one: re-evaluating a file that why the accumulated list is the post-Load one: re-evaluating a file that
@ -2328,7 +2349,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
a cold macro module costs its ~300ms before that clock starts, a cold macro module costs its ~300ms before that clock starts,
and the non-termination refusals raise [Loc.Error] out of this call, which and the non-termination refusals raise [Loc.Error] out of this call, which
the daemon already answers as an error rather than a silence. *) the daemon already answers as an error rather than a silence. *)
let parsed = Parse.with_imported ~decls:(package_decls t) t.macros (fun () -> Parse.expr form) in let parsed = Parse.with_imported ~decls:(package_decls t) (macros_for t origin) (fun () -> Parse.expr form) in
(* CIDER's rule: an expression sent from a package's file means what it (* CIDER's rule: an expression sent from a package's file means what it
would mean written in that file, so [(integrate 1.0)] in physics/step.flan would mean written in that file, so [(integrate 1.0)] in physics/step.flan
reaches [physics/integrate]. The qualification [eval] gives a declaration reaches [physics/integrate]. The qualification [eval] gives a declaration
@ -2482,7 +2503,7 @@ let macroexpand ?(origin = "<eval>") ~(all : bool) t (src : string) : expansion
let before = Expand.quasiquote form in let before = Expand.quasiquote form in
(* And the session's macros in front of it, as [eval] and [eval_expr] both (* And the session's macros in front of it, as [eval] and [eval_expr] both
put them: [Macro.program] reads [Parse.imported_macros] directly. *) put them: [Macro.program] reads [Parse.imported_macros] directly. *)
Parse.with_imported ~decls:(package_decls t) t.macros @@ fun () -> Parse.with_imported ~decls:(package_decls t) (macros_for t origin) @@ fun () ->
let after, name = let after, name =
if all then Macro.expand_all before else Macro.expand_step before if all then Macro.expand_all before else Macro.expand_step before
in in

View File

@ -0,0 +1,7 @@
;;;; A file with a main and one form that does not compile: flan dev starts
;;;; with the rest.
(defn fine [] i64 42)
(defn bad [] i64 "x")
(defn main [] i32 0)

View File

@ -5219,8 +5219,28 @@ let () =
"(:op \"eval\" :code \"(defn step [] i64 9)\" :file \ "(:op \"eval\" :code \"(defn step [] i64 9)\" :file \
\"programs/dev-noagent-running.flan\")" \"programs/dev-noagent-running.flan\")"
in in
let fix m =
contains_sub m "(import agent \"vendor:agent\")"
&& contains_sub m "(agent/poll)"
in
if status r <> "ok" then if status r <> "ok" then
fail "a delivery to a running program that never imports the agent: %s" fail "a delivery to a running program that never imports the agent: %s"
(Option.value ~default:"" (Wire.string_field r "message"))
else if not (fix (Option.value ~default:"" (Wire.string_field r "note")))
then
fail "a delivery the running program never takes did not say so: %s"
(Option.value ~default:"(no note)" (Wire.string_field r "note"));
(* And an expression, which waits for a frame boundary the program never
reaches, and names the same fix. *)
let r =
request gc
"(:op \"eval-expr\" :code \"(step)\" :file \
\"programs/dev-noagent-running.flan\")"
in
if status r <> "error"
|| not (fix (Option.value ~default:"" (Wire.string_field r "message")))
then
fail "an expression for a program that never polls: %s %s" (status r)
(Option.value ~default:"" (Wire.string_field r "message")); (Option.value ~default:"" (Wire.string_field r "message"));
(try (try
ignore (Wire.send gc "(:op \"close\")"); ignore (Wire.send gc "(:op \"close\")");
@ -7669,6 +7689,35 @@ let () =
|| not (contains_sub (said r) "has no main") || not (contains_sub (said r) "has no main")
|| not (contains_sub (said r) "(defn main [] i32") || not (contains_sub (said r) "(defn main [] i32")
then fail "a re-run with no main: %s %s" (status r) (said r); then fail "a re-run with no main: %s %s" (status r) (said r);
(* The replies say there is no main of the file's own, so an editor
does not report a program that finished. *)
(match Wire.field (request mc "(:op \"describe\")") "main" with
| Some { Form.v = Form.Sym "nil"; _ } -> ()
| _ -> fail "a session with no main did not say so on its replies");
(* An expression that stops in the park, aborted: the expression is
abandoned and the session stays. *)
let r =
request mc
"(:op \"eval-expr\" :code \"(twice 1)\" :pause t :file \"<test>\")"
in
ignore r;
if not
(await (fun () ->
match Wire.field (request mc "(:op \"describe\")") "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true
| _ -> false))
then fail "a paused expression in the park did not stop"
else begin
match request mc "(:op \"abort\")" with
| r when status r <> "ok" -> fail "aborting a parked expression: %s" (said r)
| _ ->
(match value "(twice 2)" with
| "4" -> ()
| v -> fail "the session after aborting a parked expression: %s" v
| exception e ->
fail "aborting a parked expression ended the session: %s"
(Printexc.to_string e))
end;
(* A main loaded into it is the one a re-run runs. *) (* A main loaded into it is the one a re-run runs. *)
let r = let r =
request mc request mc

View File

@ -1658,6 +1658,19 @@ let () =
| exception Loc.Error { Loc.dmsg = m; _ } -> | exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a no-main file with a bad form refused the session: %s" m); fail "a no-main file with a bad form refused the session: %s" m);
(* A file with a main of its own and a form that does not compile starts
the same way, with its own main. *)
(match Session.create_dev ~file:"programs/dev-main-broken.flan" () with
| t, _, errs ->
if not (Session.has_main t) then fail "the file's own main was left out";
let fns = List.map (fun (f : Tast.fn) -> f.Tast.name) t.Session.host.Tast.fns in
if not (List.mem "fine" fns) || List.mem "bad" fns then
fail "a file with a main started with the wrong forms";
if List.length errs <> 1 then
fail "a start with one bad form named %d" (List.length errs)
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a file with a main and a bad form refused the session: %s" m);
(* [pruned] on its own, as the daemon's load-file runs it: each round drops (* [pruned] on its own, as the daemon's load-file runs it: each round drops
the form the error is in, and one it could not blame is raised. *) the form the error is in, and one it could not blame is raised. *)
(let t, _ = Session.create ~file:"programs/dev-parknote.flan" () in (let t, _ = Session.create ~file:"programs/dev-parknote.flan" () in
@ -1697,6 +1710,21 @@ let () =
fail "an expression from a package's file did not reach the package" fail "an expression from a package's file did not reach the package"
| exception Loc.Error { Loc.dmsg = m; _ } -> | exception Loc.Error { Loc.dmsg = m; _ } ->
fail "an expression from a package's file: %s" m); fail "an expression from a package's file: %s" m);
(* The package's own macro, bare, from its file: an expression and a
declaration both expand it as the file would. *)
(match
Session.eval_expr ~origin:"programs/pkgs/secret/secret.flan" tq "(mixed 1 1)"
with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a package's macro, bare, from its file: %s" m);
(match
Session.eval ~origin:"programs/pkgs/secret/secret.flan" tq
"(defn via-mixed [] i32 (mixed 1 2))"
with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a package's macro, bare, in a declaration from its file: %s" m);
match match
Session.eval_expr ~origin:"programs/pkg-private.flan" tq "(combine 1 2)" Session.eval_expr ~origin:"programs/pkg-private.flan" tq "(combine 1 2)"
with with

View File

@ -1195,6 +1195,16 @@ static void handle_line(char *line, sink *o) {
* running too — "running" is an answer, not a refusal — because this is * running too — "running" is an answer, not a refusal — because this is
* the one question an editor asks without knowing the state already, and * the one question an editor asks without knowing the state already, and
* refusing it would leave nothing to poll. */ * refusing it would leave nothing to poll. */
/* How many modules are queued and not yet taken by a poll. The daemon asks
* after a delivery to a running program, so that one which never polls is
* told so rather than answered "ok" for a change that never lands. */
if (strcmp(line, "pending") == 0) {
char b[32];
unsigned h = atomic_load(&head), t = atomic_load(&tail);
snprintf(b, sizeof b, "%u\n", h - t);
reply(o, b);
return;
}
if (strcmp(line, "status") == 0) { if (strcmp(line, "status") == 0) {
if ((atomic_load(&depth) > 0)) { if ((atomic_load(&depth) > 0)) {
reply(o, "stopped "); reply(o, "stopped ");