flan dev starts on a file with no main and links the agent into every program it builds, C-c C-k loads a file keeping what compiles, and an expression sent from a package's file resolves in that package
This commit is contained in:
parent
d2867a4267
commit
11e1ff10a7
23
TODO.org
23
TODO.org
@ -1693,10 +1693,9 @@ nothing sweeps other sessions' directories.
|
|||||||
CLOSED: [2026-09-20]
|
CLOSED: [2026-09-20]
|
||||||
The zero-argument form takes the daemon's socket where there is one and an
|
The zero-argument form takes the daemon's socket where there is one and an
|
||||||
announced path where there is not, bound by a constructor before =main=. =Reach=
|
announced path where there is not, bound by a constructor before =main=. =Reach=
|
||||||
prunes a package nothing calls into, so the constructor reaches only a program
|
prunes a package nothing calls into in a release build. =flan dev= links the
|
||||||
that polls or waits. Full invisibility — a dev build linking the agent whether or
|
agent's C into every program it builds whether or not the source imports it; a
|
||||||
not the source says so — needs the package force-linked and is a decision about
|
release build links it only when the program calls into it.
|
||||||
what =--dev= means.
|
|
||||||
|
|
||||||
** NEXT FLAN_AGENT_SOCKET in a shell's environment steals the socket
|
** NEXT FLAN_AGENT_SOCKET in a shell's environment steals the socket
|
||||||
Decided 2026-09-25: narrow the gate. The daemon also exports its own pid, and the constructor binds the socket only when that pid is the program's parent (or the program itself, in a merged build).
|
Decided 2026-09-25: narrow the gate. The daemon also exports its own pid, and the constructor binds the socket only when that pid is the program's parent (or the program itself, in a merged build).
|
||||||
@ -2091,12 +2090,6 @@ motion states in that mode; every other key, including what =special-mode-map=
|
|||||||
binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly,
|
binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly,
|
||||||
diagnostics, lower) have the same exposure and are not changed.
|
diagnostics, lower) have the same exposure and are not changed.
|
||||||
|
|
||||||
** NEXT C-x C-e in a package's file resolves names in that package
|
|
||||||
Decided 2026-09-25: CIDER's rule — an evaluation sent from a file resolves names
|
|
||||||
as code written in that file would, so =(integrate 1.0)= in =physics/step.flan=
|
|
||||||
reaches the package's own functions, =defn-= included. Today it answers
|
|
||||||
"unknown function", resolving as the program's main file.
|
|
||||||
|
|
||||||
** NEXT Evil takes the keys in the other Flan buffers
|
** NEXT Evil takes the keys in the other Flan buffers
|
||||||
The inspect, watch, doc, disassembly, diagnostics and lower buffers get the break
|
The inspect, watch, doc, disassembly, diagnostics and lower buffers get the break
|
||||||
buffer's treatment: the keys each binds itself go to Evil's normal and motion
|
buffer's treatment: the keys each binds itself go to Evil's normal and motion
|
||||||
@ -2174,13 +2167,9 @@ daemon asks for the sink off (=lib/loc.ml:185=) and gets one exception, so a
|
|||||||
function with three bad expressions takes three round trips. The sink is
|
function with three bad expressions takes three round trips. The sink is
|
||||||
per-phase; making it per-form would need a resync point inside a body.
|
per-phase; making it per-form would need a resync point inside a body.
|
||||||
|
|
||||||
** NEXT A session should start before a program compiles
|
** DONE A session should start before a program compiles
|
||||||
Decided 2026-09-25: SBCL/nREPL order: an empty host session, then a load-file op. Re-running with no =main= says there is none; a half-loaded file keeps what compiled and lists the errors. =C-c C-k= is load-file and the inspector moves to =C-c M-i=, CIDER's key.
|
CLOSED: [2026-09-25]
|
||||||
=flan dev= builds the program first, so a =main= that does not compile gives no
|
A file with no =main= starts on a stub =main= that returns and parks; =load-file= (=C-c C-k=, already its key — the inspector stays on =C-c C-i=) keeps what compiles and lists the rest. Rules out =flan dev= with no file at all, and =--two-process= on a file with no =main=.
|
||||||
session. Want SBCL/nREPL order: empty image, then load a file into it. Needs a
|
|
||||||
host built with no user program, and a load-file op — the reload machinery
|
|
||||||
already builds a file as a module. Open: what =flan-rerun= does with no =main=,
|
|
||||||
and a half-loaded file. =C-c C-k= is taken by the inspector.
|
|
||||||
|
|
||||||
** NEXT The daemon buffer is navigable but not coloured
|
** NEXT The daemon buffer is navigable but not coloured
|
||||||
Decided 2026-09-25: errors, warnings and notes take compilation-mode's faces, and the program's own output takes a face of its own so it reads apart from the compiler's.
|
Decided 2026-09-25: errors, warnings and notes take compilation-mode's faces, and the program's own output takes a face of its own so it reads apart from the compiler's.
|
||||||
|
|||||||
@ -90,6 +90,20 @@ stops the program too — but only one this Emacs started. A daemon you launched
|
|||||||
in a terminal is not Emacs' to kill, and it will say so rather than do something
|
in a terminal is not Emacs' to kill, and it will say so rather than do something
|
||||||
surprising.
|
surprising.
|
||||||
|
|
||||||
|
### A file with no `main`
|
||||||
|
|
||||||
|
A file of functions with no `main` — a scratch file, a library being written —
|
||||||
|
starts a session too. Nothing runs, and the session is there to take
|
||||||
|
definitions and expressions: `C-x C-e` on `(fib 10)` answers, and `C-c C-k`
|
||||||
|
loads more into it. `M-x flan-rerun` says there is no `main` until one has been
|
||||||
|
loaded.
|
||||||
|
|
||||||
|
A form in such a file that does not compile is left out of the start and printed
|
||||||
|
in `*flan*`, and the session starts with the rest.
|
||||||
|
|
||||||
|
The program does not have to import the agent. `flan dev` links it into every
|
||||||
|
program it builds, and a release build is unaffected.
|
||||||
|
|
||||||
### Which backend the session uses, and what it costs
|
### Which backend the session uses, and what it costs
|
||||||
|
|
||||||
`flan dev` compiles the session with the hand-written x86-64 backend. That is
|
`flan dev` compiles the session with the hand-written x86-64 backend. That is
|
||||||
@ -195,6 +209,11 @@ The form before point can also be a declaration — a `defonce` typed at the top
|
|||||||
a file — in which case `C-x C-e` installs it rather than refusing it, and says
|
a file — in which case `C-x C-e` installs it rather than refusing it, and says
|
||||||
which names changed instead of printing a value.
|
which names changed instead of printing a value.
|
||||||
|
|
||||||
|
**Names mean what they mean in the file.** An expression sent from a package's
|
||||||
|
file resolves as code written in that file would: `(integrate 1.0)` in
|
||||||
|
`physics/step.flan` reaches `physics/integrate`, a `defn-` included. The REPL is
|
||||||
|
not a file, so there the program's own names apply.
|
||||||
|
|
||||||
### `C-u C-c C-c` — stop there
|
### `C-u C-c C-c` — stop there
|
||||||
|
|
||||||
The same key with a prefix argument **marks a form as a breakpoint**. `C-u C-c
|
The same key with a prefix argument **marks a form as a breakpoint**. `C-u C-c
|
||||||
@ -261,12 +280,18 @@ drawn on the call — it does not hang.
|
|||||||
|
|
||||||
### `C-c C-k` — the whole buffer
|
### `C-c C-k` — the whole buffer
|
||||||
|
|
||||||
The whole buffer, sent as **one** module rather than as a form at a time. That
|
The whole buffer is loaded into the running program, as `C-c C-k` loads a file in
|
||||||
matters: a `defonce` and the function that uses it have to arrive together, or the
|
SLIME and CIDER. It is sent as **one** module rather than as a form at a time.
|
||||||
function refers to storage that does not exist yet.
|
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.
|
||||||
|
|
||||||
Use this when you have changed several things at once, or when you have added a
|
A form that does not compile is left out, and so is any form that uses it; the
|
||||||
new global.
|
rest are installed. Each form left out is marked in the buffer and listed in
|
||||||
|
`*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.
|
||||||
|
|
||||||
|
Use this when you have changed several things at once, when you have added a new
|
||||||
|
global, or to load a file into a session started on another.
|
||||||
|
|
||||||
### When it lands
|
### When it lands
|
||||||
|
|
||||||
@ -1155,7 +1180,8 @@ Use `C-c C-g` if you need frames.
|
|||||||
| `C-c C-c` | the top-level form at point: a declaration installed, anything else evaluated |
|
| `C-c C-c` | the top-level form at point: a declaration installed, anything else evaluated |
|
||||||
| `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` | the whole buffer, as one module |
|
| `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) |
|
||||||
|
|||||||
@ -1618,15 +1618,17 @@ quitting the program — which is the point of it."
|
|||||||
(set-marker flan--diagnostics-memory-start nil)
|
(set-marker flan--diagnostics-memory-start nil)
|
||||||
(setq flan--diagnostics-memory-start nil))))
|
(setq flan--diagnostics-memory-start nil))))
|
||||||
|
|
||||||
(defun flan--show-error (loc msg)
|
(defun flan--show-error (loc msg &optional keep)
|
||||||
"Mark MSG at LOC, if LOC names a file some buffer is visiting.
|
"Mark MSG at LOC, if LOC names a file some buffer is visiting.
|
||||||
|
The buffer's other error marks are taken down first unless KEEP is non-nil.
|
||||||
Returns non-nil when it put an overlay somewhere."
|
Returns non-nil when it put an overlay somewhere."
|
||||||
(let ((parts (flan--parse-loc loc)))
|
(let ((parts (flan--parse-loc loc)))
|
||||||
(when parts
|
(when parts
|
||||||
(let ((buf (flan--buffer-visiting (nth 0 parts))))
|
(let ((buf (flan--buffer-visiting (nth 0 parts))))
|
||||||
(when buf
|
(when buf
|
||||||
(with-current-buffer buf
|
(with-current-buffer buf
|
||||||
(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
|
||||||
@ -1651,7 +1653,8 @@ Returns non-nil when it put an overlay somewhere."
|
|||||||
;; Point goes there too, but only in the buffer being looked at:
|
;; Point goes there too, but only in the buffer being looked at:
|
||||||
;; moving point in a buffer nobody is showing is a surprise the
|
;; moving point in a buffer nobody is showing is a surprise the
|
||||||
;; next time it is visited.
|
;; next time it is visited.
|
||||||
(when (eq buf (current-buffer)) (goto-char beg))
|
(when (and (not keep) (eq buf (current-buffer)))
|
||||||
|
(goto-char beg))
|
||||||
t)))))))
|
t)))))))
|
||||||
|
|
||||||
;;; Inline results
|
;;; Inline results
|
||||||
@ -2795,18 +2798,55 @@ declaration for it to live in."
|
|||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun flan-eval-buffer ()
|
(defun flan-eval-buffer ()
|
||||||
"Recompile every top-level form in this buffer and install them together.
|
"Load this buffer into the running program, as `C-c C-k' does in SLIME and CIDER.
|
||||||
One module, not one per form: a var and the function that uses it have to
|
Every top-level form is compiled and installed together, as one module: a var
|
||||||
arrive in the same load or the first refers to storage that does not exist."
|
and the function that uses it have to arrive in the same load or the first
|
||||||
|
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
|
||||||
|
rest are installed. Each one left out is marked in the buffer and listed in
|
||||||
|
`flan-diagnostics-buffer'. When nothing compiles, nothing is installed and
|
||||||
|
the command signals, as `C-c C-c' does."
|
||||||
(interactive)
|
(interactive)
|
||||||
(flan--eval (buffer-substring-no-properties (point-min) (point-max))
|
(let* ((reply (flan--request
|
||||||
(buffer-name))
|
(list :op "load-file" :file (or buffer-file-name "<buffer>")
|
||||||
;; `flan--eval' signals on a rejection, so reaching here means every
|
:code (buffer-substring-no-properties
|
||||||
;; declaration in the buffer was just replaced by an unmarked one — and
|
(point-min) (point-max)))))
|
||||||
;; therefore that every mark in it is gone. Done here rather than by passing
|
(errors (plist-get reply :errors)))
|
||||||
;; bounds, because those are also what gets flashed and pulsing a whole
|
(if (equal (plist-get reply :status) "ok")
|
||||||
;; buffer is not feedback, it is a flicker.
|
(progn
|
||||||
(flan-clear-pause))
|
(flan--report reply (buffer-name))
|
||||||
|
;; Every declaration that installed was replaced by an unmarked one,
|
||||||
|
;; so every mark in the buffer is gone. Done here rather than by
|
||||||
|
;; passing bounds, because those are also what gets flashed and
|
||||||
|
;; pulsing a whole buffer is not feedback, it is a flicker.
|
||||||
|
(flan-clear-pause)
|
||||||
|
(when errors
|
||||||
|
(flan--report-load-errors errors)
|
||||||
|
(message "flan: %s loaded; %d form%s did not compile, listed in %s"
|
||||||
|
(buffer-name) (length errors)
|
||||||
|
(if (= (length errors) 1) "" "s")
|
||||||
|
flan-diagnostics-buffer)))
|
||||||
|
;; `flan--report' records, marks and signals the first; the others are
|
||||||
|
;; recorded and marked beside it before the signal leaves.
|
||||||
|
(condition-case err
|
||||||
|
(flan--report reply (buffer-name))
|
||||||
|
(user-error
|
||||||
|
(flan--report-load-errors (cdr errors) t)
|
||||||
|
(signal (car err) (cdr err)))))
|
||||||
|
reply))
|
||||||
|
|
||||||
|
(defun flan--report-load-errors (errors &optional keep)
|
||||||
|
"Record and mark each of ERRORS, a load reply's `:errors' list.
|
||||||
|
The marks from earlier in the same load are kept, and so are the marks
|
||||||
|
already in the buffer when KEEP is non-nil."
|
||||||
|
(let ((first (not keep)))
|
||||||
|
(dolist (e errors)
|
||||||
|
(let ((loc (plist-get e :loc))
|
||||||
|
(msg (plist-get e :message)))
|
||||||
|
(ignore-errors (flan--record-diagnostic loc msg))
|
||||||
|
(ignore-errors (flan--show-error loc msg (not first)))
|
||||||
|
(setq first nil)))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun flan-eval-last-sexp (&optional arg)
|
(defun flan-eval-last-sexp (&optional arg)
|
||||||
|
|||||||
@ -2302,6 +2302,67 @@ already rely on it — so nothing here is a stand-in for the real thing."
|
|||||||
(flan-quit)
|
(flan-quit)
|
||||||
(ignore-errors (delete-file socket5)))
|
(ignore-errors (delete-file socket5)))
|
||||||
|
|
||||||
|
;; ── A file with no main ───────────────────────────────────────────────
|
||||||
|
;;
|
||||||
|
;; SBCL's order: the session comes up on a file that has nothing to run,
|
||||||
|
;; and `C-c C-k' loads into it. A load keeps what compiles and marks what
|
||||||
|
;; does not; a re-run says there is no main.
|
||||||
|
(let ((socket6 (concat socket "-nomain"))
|
||||||
|
(scratch (expand-file-name "nomain.flan" (file-name-directory file)))
|
||||||
|
(value (lambda (code)
|
||||||
|
(plist-get (flan--request
|
||||||
|
(list :op "eval-expr" :code code :file "<test>"))
|
||||||
|
:value))))
|
||||||
|
(with-temp-file scratch
|
||||||
|
(insert "(defn fib [n i64] i64\n (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))\n"))
|
||||||
|
(ignore-errors (delete-file socket6))
|
||||||
|
(flan scratch socket6)
|
||||||
|
(test-flan--check "a daemon starts on a file with no main"
|
||||||
|
(process-live-p flan--connection))
|
||||||
|
(test-flan--check "and an expression reaches the file's functions"
|
||||||
|
(equal (funcall value "(fib 10)") "55"))
|
||||||
|
(with-current-buffer (find-file-noselect scratch)
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert "\n(defn biggest [xs [$t]] $t\n {:where (ordered? $t)}\n"
|
||||||
|
" (let [m (at xs 0)]\n (dotimes [i (length xs)]\n"
|
||||||
|
" (set m (max m (at xs i))))\n m))\n\n"
|
||||||
|
"(defn twice [n i64] i64 (* 2 n))\n")
|
||||||
|
(flan-eval-buffer)
|
||||||
|
(test-flan--check "C-c C-k loads a generic into it"
|
||||||
|
(equal (funcall value "(let [ns [3 9 2]] (biggest (slice ns 0 3)))")
|
||||||
|
"9"))
|
||||||
|
(test-flan--check "and a plain function"
|
||||||
|
(equal (funcall value "(twice 21)") "42"))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert "(defn good [] i64 (bad))\n\n(defn bad [] i64 \"x\")\n\n"
|
||||||
|
"(defn fine [] i64 42)\n")
|
||||||
|
(let ((said (test-flan--said (flan-eval-buffer))))
|
||||||
|
(test-flan--check "a load with errors installs the rest and counts what it left out"
|
||||||
|
(and said (string-match-p "2 forms did not compile" said))))
|
||||||
|
(test-flan--check "and marks each form it left out"
|
||||||
|
(= 2 (length (flan--error-overlays))))
|
||||||
|
(test-flan--check "what compiled is in the program"
|
||||||
|
(equal (funcall value "(fine)") "42"))
|
||||||
|
(test-flan--check "and what used a form that did not is not"
|
||||||
|
(equal (plist-get (flan--request
|
||||||
|
(list :op "eval-expr" :code "(good)"
|
||||||
|
:file "<test>"))
|
||||||
|
:status)
|
||||||
|
"error"))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert "(defn bad [] i64 \"x\")\n")
|
||||||
|
(test-flan--check "a load where nothing compiles is refused"
|
||||||
|
(condition-case nil (progn (flan-eval-buffer) nil)
|
||||||
|
(user-error t)))
|
||||||
|
(set-buffer-modified-p nil))
|
||||||
|
(test-flan--check "a re-run with no main says there is none"
|
||||||
|
(condition-case err (progn (flan-rerun) nil)
|
||||||
|
(user-error
|
||||||
|
(string-match-p "has no main" (error-message-string err)))))
|
||||||
|
(flan-quit)
|
||||||
|
(ignore-errors (delete-file socket6))
|
||||||
|
(ignore-errors (delete-file scratch)))
|
||||||
|
|
||||||
(if (zerop test-flan--failures)
|
(if (zerop test-flan--failures)
|
||||||
(message "flan.el: all tests passed")
|
(message "flan.el: all tests passed")
|
||||||
(message "\n%d failure(s)" test-flan--failures)
|
(message "\n%d failure(s)" test-flan--failures)
|
||||||
|
|||||||
229
lib/dev.ml
229
lib/dev.ml
@ -122,27 +122,10 @@ let await ?(ms = 5000) f =
|
|||||||
in
|
in
|
||||||
go ms
|
go ms
|
||||||
|
|
||||||
(* What a program needs so that code from the editor can reach it. Spelled once
|
(* Every program [flan dev] builds has the agent linked — see [with_agent] — so
|
||||||
because three replies give it: a delivery, an evaluation and the daemon's
|
a program that cannot be reached is one whose socket went away, not one that
|
||||||
own warning. Both lines compile as written. *)
|
never had an agent. *)
|
||||||
let agent_howto =
|
let unreachable _t e = "cannot reach the program: " ^ Unix.error_message e
|
||||||
"Import the agent with (import agent \"vendor:agent\") and call \
|
|
||||||
(agent/poll) once in each pass of the program's main loop, then start \
|
|
||||||
flan dev again."
|
|
||||||
|
|
||||||
let no_agent =
|
|
||||||
"the program has no agent, so nothing in it can receive code from the \
|
|
||||||
editor. " ^ agent_howto
|
|
||||||
|
|
||||||
(* Whether anything in this session can hand code to the program. A merged
|
|
||||||
build with no agent linked has no agent to call and never binds a socket,
|
|
||||||
and a connect to one answers "No such file or directory" about a path the
|
|
||||||
reader never chose; that is the case this names. *)
|
|
||||||
let agentless t = t.child = None && not (Agent.present ())
|
|
||||||
|
|
||||||
let unreachable t e =
|
|
||||||
if agentless t then no_agent
|
|
||||||
else "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.
|
||||||
|
|
||||||
@ -183,7 +166,7 @@ let agent_check t =
|
|||||||
t.agent_watch <- None;
|
t.agent_watch <- None;
|
||||||
Printf.eprintf
|
Printf.eprintf
|
||||||
"flan dev: nothing is listening on %s, so code from the editor cannot \
|
"flan dev: nothing is listening on %s, so code from the editor cannot \
|
||||||
reach the program. %s\n%!" t.agent agent_howto
|
reach the program.\n%!" t.agent
|
||||||
end
|
end
|
||||||
|
|
||||||
(* ── Asking the agent ──────────────────────────────────────────────── *)
|
(* ── Asking the agent ──────────────────────────────────────────────── *)
|
||||||
@ -826,8 +809,8 @@ let refusal ~parked reply =
|
|||||||
[max_socket_path], which [session_dir] refuses before building — and there
|
[max_socket_path], which [session_dir] refuses before building — and there
|
||||||
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. A program with no agent linked has nothing to
|
cause, so there is none. *)
|
||||||
queue a module on, and its delivery is refused with [no_agent]. *)
|
|
||||||
let install_note t ~parked =
|
let install_note t ~parked =
|
||||||
if parked then begin
|
if parked then begin
|
||||||
let first = not t.park_noted in
|
let first = not t.park_noted in
|
||||||
@ -882,7 +865,7 @@ let stale_field (ss : Session.stale list) =
|
|||||||
(if x.Session.running then " :running t" else ""))
|
(if x.Session.running then " :running t" else ""))
|
||||||
ss) ]
|
ss) ]
|
||||||
|
|
||||||
let eval t ~code ~origin ~pause =
|
let eval ?forms ?base ?(extra = []) t ~code ~origin ~pause =
|
||||||
let now = liveness t in
|
let now = liveness t in
|
||||||
let parked_now = now = Parked in
|
let parked_now = now = Parked in
|
||||||
(* A park that is over takes its note with it: the long sentence below is
|
(* A park that is over takes its note with it: the long sentence below is
|
||||||
@ -905,15 +888,19 @@ let eval t ~code ~origin ~pause =
|
|||||||
let refused msg = Session.restore t.session before; error msg in
|
let refused msg = Session.restore t.session before; error msg in
|
||||||
if now = Gone then error gone
|
if now = Gone then error gone
|
||||||
else
|
else
|
||||||
match Session.eval ~origin ?pause ~running:(not parked_now) t.session code with
|
match
|
||||||
|
Session.eval ~origin ?base ?forms ?pause ~running:(not parked_now)
|
||||||
|
t.session code
|
||||||
|
with
|
||||||
| c when not c.Session.installs ->
|
| c when not c.Session.installs ->
|
||||||
(* Accepted into the session and nothing to send: a declaration the
|
(* Accepted into the session and nothing to send: a declaration the
|
||||||
program already has, with no body and no new storage. Saying "ok" and
|
program already has, with no body and no new storage. Saying "ok" and
|
||||||
shipping an empty module would report success for a change that cannot
|
shipping an empty module would report success for a change that cannot
|
||||||
have taken effect. *)
|
have taken effect. *)
|
||||||
ok
|
ok
|
||||||
[ ":names " ^ Wire.strings c.Session.names; ":fns ()";
|
([ ":names " ^ Wire.strings c.Session.names; ":fns ()";
|
||||||
":note " ^ Wire.quote "nothing to install" ]
|
":note " ^ Wire.quote "nothing to install" ]
|
||||||
|
@ extra)
|
||||||
| c ->
|
| c ->
|
||||||
(* Everything from here to the delivery is inside the restore, and by
|
(* Everything from here to the delivery is inside the restore, and by
|
||||||
exception type as well as by arm. The three named below are the ones
|
exception type as well as by arm. The three named below are the ones
|
||||||
@ -965,14 +952,13 @@ let eval t ~code ~origin ~pause =
|
|||||||
| Some (l, c) ->
|
| Some (l, c) ->
|
||||||
[ ":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
|
||||||
|
@ 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, _, _) ->
|
||||||
refused
|
refused
|
||||||
(if agentless t then no_agent
|
("cannot reach the program on " ^ t.agent ^ ": "
|
||||||
else
|
^ Unix.error_message e))
|
||||||
"cannot reach the program on " ^ t.agent ^ ": "
|
|
||||||
^ Unix.error_message e))
|
|
||||||
| exception Failure m -> refused m)
|
| exception Failure m -> refused m)
|
||||||
with e when not !accepted -> Session.restore t.session before; raise e)
|
with e when not !accepted -> Session.restore t.session before; raise e)
|
||||||
(* Nothing to put back: the check itself raised, so [Session.eval] never
|
(* Nothing to put back: the check itself raised, so [Session.eval] never
|
||||||
@ -984,6 +970,60 @@ let eval t ~code ~origin ~pause =
|
|||||||
Session.restore t.session before;
|
Session.restore t.session before;
|
||||||
error ~loc:(Loc.to_string l) msg
|
error ~loc:(Loc.to_string l) msg
|
||||||
|
|
||||||
|
(* The refusals a load answered beside what it installed, one plist each. *)
|
||||||
|
let errors_field (ds : Loc.diag list) =
|
||||||
|
match ds with
|
||||||
|
| [] -> []
|
||||||
|
| ds ->
|
||||||
|
[ ":errors "
|
||||||
|
^ Wire.list
|
||||||
|
(List.map
|
||||||
|
(fun (d : Loc.diag) ->
|
||||||
|
Printf.sprintf "(:loc %s :message %s)"
|
||||||
|
(Wire.quote (Loc.to_string d.Loc.dloc))
|
||||||
|
(Wire.quote d.Loc.dmsg))
|
||||||
|
ds) ]
|
||||||
|
|
||||||
|
(* C-c C-k: a whole file into the running session, SBCL's [load]. [eval] with
|
||||||
|
one difference — a form that does not compile is left out and listed
|
||||||
|
rather than refusing the rest, so a file with one broken function still
|
||||||
|
defines the others ([Session.pruned] finds which). The survivors go through
|
||||||
|
[eval] as one module, as the whole buffer always has. When nothing
|
||||||
|
survives the reply is a refusal, with the first error where every other
|
||||||
|
refusal puts it. *)
|
||||||
|
let load_file t ~code ~origin =
|
||||||
|
match Reader.read_all ~file:origin code with
|
||||||
|
| exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } ->
|
||||||
|
error ~loc:(Loc.to_string l) msg
|
||||||
|
| forms ->
|
||||||
|
(* The imports of a file on disk are its own, wherever the session's file
|
||||||
|
is. *)
|
||||||
|
let base = if Sys.file_exists origin then Some origin else None in
|
||||||
|
let running = liveness t <> Parked in
|
||||||
|
let check forms =
|
||||||
|
let before = Session.held t.session in
|
||||||
|
Fun.protect
|
||||||
|
~finally:(fun () -> Session.restore t.session before)
|
||||||
|
(fun () ->
|
||||||
|
ignore (Session.eval ~origin ?base ~forms ~running t.session code))
|
||||||
|
in
|
||||||
|
(match Session.pruned check forms with
|
||||||
|
| exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } ->
|
||||||
|
error ~loc:(Loc.to_string l) msg
|
||||||
|
| exception Loc.Errors ({ Loc.dloc = l; dmsg = msg; _ } :: _ as ds) ->
|
||||||
|
let e = error ~loc:(Loc.to_string l) msg in
|
||||||
|
String.sub e 0 (String.length e - 1)
|
||||||
|
^ " " ^ String.concat " " (errors_field ds) ^ ")"
|
||||||
|
| (), kept, errs ->
|
||||||
|
if errs <> [] && kept = [] then
|
||||||
|
let d = List.hd errs in
|
||||||
|
let e = error ~loc:(Loc.to_string d.Loc.dloc) d.Loc.dmsg in
|
||||||
|
String.sub e 0 (String.length e - 1)
|
||||||
|
^ " " ^ String.concat " " (errors_field errs) ^ ")"
|
||||||
|
else
|
||||||
|
eval ~forms:kept ?base ~extra:(errors_field errs) t ~code ~origin
|
||||||
|
~pause:None)
|
||||||
|
|
||||||
(* Redefining a name installs a body; evaluating an expression has no name to
|
(* Redefining a name installs a body; evaluating an expression has no name to
|
||||||
install into, so the module carries a thunk the agent runs once. The value
|
install into, so the module carries a thunk the agent runs once. The value
|
||||||
comes back through the runtime rather than through this reply, because the
|
comes back through the runtime rather than through this reply, because the
|
||||||
@ -1023,7 +1063,6 @@ let eval t ~code ~origin ~pause =
|
|||||||
let eval_expr t ~code ~origin ~pause =
|
let eval_expr t ~code ~origin ~pause =
|
||||||
match liveness t with
|
match liveness t with
|
||||||
| Gone -> error gone
|
| Gone -> error gone
|
||||||
| (Live | Parked) when agentless t -> error no_agent
|
|
||||||
| Live | Parked ->
|
| Live | Parked ->
|
||||||
(* The same rollback [eval] takes, for the same reason and a smaller
|
(* The same rollback [eval] takes, for the same reason and a smaller
|
||||||
cargo. A thunk is not a declaration and never joins the session, but the
|
cargo. A thunk is not a declaration and never joins the session, but the
|
||||||
@ -3315,6 +3354,17 @@ let abort t =
|
|||||||
let rerun t =
|
let rerun t =
|
||||||
match liveness t with
|
match liveness t with
|
||||||
| Gone -> error gone
|
| Gone -> error gone
|
||||||
|
(* A file started with no [main] runs a stub that returns at once; running
|
||||||
|
it again would answer "running main again" about nothing. *)
|
||||||
|
| (Live | Parked) when not (Session.has_main t.session) ->
|
||||||
|
error
|
||||||
|
(Printf.sprintf
|
||||||
|
"%s has no main, so there is nothing to run. A program starts at a \
|
||||||
|
function named main; add one and load the file with C-c C-k, for \
|
||||||
|
example:\n\n\
|
||||||
|
\ (defn main [] i32\n\
|
||||||
|
\ 0)"
|
||||||
|
t.session.Session.file)
|
||||||
| Live | Parked ->
|
| Live | Parked ->
|
||||||
(* Read before the request and not after it. Taking a re-run is what ends
|
(* Read before the request and not after it. Taking a re-run is what ends
|
||||||
the park — the C stores the new state as it accepts — so by the time
|
the park — the C stores the new state as it accepts — so by the time
|
||||||
@ -4008,7 +4058,21 @@ let handle t req =
|
|||||||
in
|
in
|
||||||
macroexpand t ~code ~origin ~all
|
macroexpand t ~code ~origin ~all
|
||||||
| None -> error "macroexpand needs :code")
|
| None -> error "macroexpand needs :code")
|
||||||
|
(* A whole file, [:code] being its text as the editor holds it — which need
|
||||||
|
not be what is saved — and [:file] where it lives. Without [:code] the
|
||||||
|
file is read from disk. *)
|
||||||
|
| Some "load-file" ->
|
||||||
|
(match Wire.string_field req "file" with
|
||||||
|
| None -> error "load-file needs :file"
|
||||||
|
| Some origin ->
|
||||||
|
(match Wire.string_field req "code" with
|
||||||
|
| Some code -> load_file t ~code ~origin
|
||||||
|
| None ->
|
||||||
|
(match read_file origin with
|
||||||
|
| code -> load_file t ~code ~origin
|
||||||
|
| 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
|
||||||
@ -4539,9 +4603,10 @@ let make_session_dir ~file dir =
|
|||||||
dev %s"
|
dev %s"
|
||||||
dir (Unix.error_message e) (Filename.quote file))
|
dir (Unix.error_message e) (Filename.quote file))
|
||||||
|
|
||||||
(* A session over a file with no [main] has nothing to run, and the build
|
(* The two-process daemon's program is a child process, and a child whose
|
||||||
would find that out at the link — as a missing symbol, or as the merged
|
[main] returns at once is a dead child rather than a parked one — so the
|
||||||
build's rename finding nothing to rename. *)
|
stub [Session.create_dev] gives a file with no [main] is no use to it. The
|
||||||
|
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
|
||||||
(List.exists (fun (f : Tast.fn) -> f.Tast.name = "main")
|
(List.exists (fun (f : Tast.fn) -> f.Tast.name = "main")
|
||||||
@ -4549,12 +4614,49 @@ let need_main ~file (session : Session.t) =
|
|||||||
then
|
then
|
||||||
failwith
|
failwith
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
"%s has no main, so flan dev has nothing to run. A program starts at \
|
"%s has no main, and flan dev --two-process runs the program as a \
|
||||||
a function named main, for example:\n\n\
|
separate process, which needs one. Start flan dev without \
|
||||||
|
--two-process, or add a function named main, for example:\n\n\
|
||||||
\ (defn main [] i32\n\
|
\ (defn main [] i32\n\
|
||||||
\ 0)"
|
\ 0)"
|
||||||
file)
|
file)
|
||||||
|
|
||||||
|
(* 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
|
||||||
|
file that never mentions the agent can still be reached from the editor. A
|
||||||
|
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
|
||||||
|
affected. *)
|
||||||
|
let with_agent ~dir csrcs lflags =
|
||||||
|
let csrcs =
|
||||||
|
if List.exists (fun c -> Filename.basename c = "flan_agent.c") csrcs then
|
||||||
|
csrcs
|
||||||
|
else begin
|
||||||
|
let c = Filename.concat dir "flan_agent.c" in
|
||||||
|
write_file c Runtime_src.agent_source;
|
||||||
|
csrcs @ [ c ]
|
||||||
|
end
|
||||||
|
in
|
||||||
|
let lflags =
|
||||||
|
lflags
|
||||||
|
@ List.filter (fun f -> not (List.mem f lflags)) [ "-lpthread"; "-ldl" ]
|
||||||
|
in
|
||||||
|
(csrcs, lflags)
|
||||||
|
|
||||||
|
(* What [flan dev] says about the forms of a file with no [main] that did not
|
||||||
|
compile. The session starts without them; C-c C-k loads the file again once
|
||||||
|
they are fixed. *)
|
||||||
|
let report_dropped ~file = function
|
||||||
|
| [] -> ()
|
||||||
|
| ds ->
|
||||||
|
Printf.eprintf "%s\n%!" (Loc.report_all ds);
|
||||||
|
Printf.eprintf
|
||||||
|
"flan dev: %s has %d form%s that did not compile; the session starts \
|
||||||
|
without %s\n%!"
|
||||||
|
file (List.length ds)
|
||||||
|
(if List.length ds = 1 then "" else "s")
|
||||||
|
(if List.length ds = 1 then "it" else "them")
|
||||||
|
|
||||||
(* [debug] is off by default, which keeps [flan dev] exactly what it was: a
|
(* [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
|
-O2 host and -O2 modules. It is opt-in rather than always-on because a debug
|
||||||
build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg
|
build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg
|
||||||
@ -4574,6 +4676,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
let session, l = Session.create ~debug ~x86 ~file () in
|
let session, l = Session.create ~debug ~x86 ~file () in
|
||||||
need_main ~file:given session;
|
need_main ~file:given session;
|
||||||
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 exe = Filename.concat dir "program" in
|
let exe = Filename.concat dir "program" in
|
||||||
(* [keep] so the host's own IR survives the build. It is the text [llc] was
|
(* [keep] so the host's own IR survives the build. It is the text [llc] was
|
||||||
actually given, not a second emission of it, which is the difference
|
actually given, not a second emission of it, which is the difference
|
||||||
@ -4590,7 +4693,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
Build.executable
|
Build.executable
|
||||||
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
~opts:{ Build.default with Build.dev = true; Build.keep = true;
|
||||||
Build.debug; Build.x86 }
|
Build.debug; Build.x86 }
|
||||||
~csrcs:l.Load.csrcs ~lflags:l.Load.lflags session.Session.host ~out:exe
|
~csrcs ~lflags session.Session.host ~out:exe
|
||||||
in
|
in
|
||||||
(* Host and modules are chosen together, which is the whole licence: an
|
(* Host and modules are chosen together, which is the whole licence: an
|
||||||
[--x86] host gets [--x86] modules because one flag set both, and the
|
[--x86] host gets [--x86] modules because one flag set both, and the
|
||||||
@ -4642,7 +4745,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
failwith
|
failwith
|
||||||
("the program did not open its agent socket at " ^ agent
|
("the program did not open its agent socket at " ^ agent
|
||||||
^ ". Under --two-process every edit reaches the program through that \
|
^ ". Under --two-process every edit reaches the program through that \
|
||||||
socket. " ^ agent_howto)
|
socket.")
|
||||||
end;
|
end;
|
||||||
|
|
||||||
let t =
|
let t =
|
||||||
@ -4723,10 +4826,9 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
the process starts rather than because anybody asked — and the startup below
|
the process starts rather than because anybody asked — and the startup below
|
||||||
is still the one place that decides, in three lines of C.
|
is still the one place that decides, in three lines of C.
|
||||||
|
|
||||||
What is genuinely open is the *session*: [Session.create ~file] is the only
|
The session starts from a file with no [main] as well: [Session.create_dev]
|
||||||
entry there is, so a REPL that starts empty and accumulates as files are
|
gives it a stub that returns at once, so the process comes up, parks, and
|
||||||
loaded needs [Session] to have a second constructor. It already accumulates;
|
accumulates what the editor loads into it. And [rerun] runs [main] and nothing else,
|
||||||
it just cannot start from nothing. And [rerun] runs [main] and nothing else,
|
|
||||||
deliberately — running a *named* function is what [eval-expr] already is,
|
deliberately — running a *named* function is what [eval-expr] already is,
|
||||||
and the two should not grow into one verb with a mode. *)
|
and the two should not grow into one verb with a mode. *)
|
||||||
|
|
||||||
@ -4971,11 +5073,26 @@ static void flan_merged_park(void) {
|
|||||||
program_state = PROGRAM_PARKED;
|
program_state = PROGRAM_PARKED;
|
||||||
pthread_mutex_unlock(&program_lock);
|
pthread_mutex_unlock(&program_lock);
|
||||||
fflush(NULL);
|
fflush(NULL);
|
||||||
fprintf(stderr,
|
/* A file with no main is started on a stub that returns at once, and its
|
||||||
"flan dev: the program finished with %d; the process is parked and "
|
* first park is the session coming up rather than a program finishing. */
|
||||||
"its globals are as it left them — M-x flan-rerun runs it again\n",
|
{
|
||||||
(int)program_status);
|
static int first_park = 1;
|
||||||
|
const char *no_main = getenv("FLAN_DEV_NO_MAIN");
|
||||||
|
if (first_park && no_main != NULL && no_main[0] == '1')
|
||||||
|
fprintf(stderr,
|
||||||
|
"flan dev: the file has no main, so nothing runs until one is "
|
||||||
|
"loaded; the session is up and takes definitions and "
|
||||||
|
"expressions\n");
|
||||||
|
else
|
||||||
|
fprintf(stderr,
|
||||||
|
"flan dev: the program finished with %d; the process is parked "
|
||||||
|
"and its globals are as it left them — M-x flan-rerun runs it "
|
||||||
|
"again\n",
|
||||||
|
(int)program_status);
|
||||||
|
first_park = 0;
|
||||||
|
}
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
|
|
||||||
pthread_mutex_lock(&program_lock);
|
pthread_mutex_lock(&program_lock);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
while (!program_asked && !program_poll)
|
while (!program_asked && !program_poll)
|
||||||
@ -5472,7 +5589,7 @@ let merged_setup () =
|
|||||||
marshalling a [Session.t] through a file, which buys nothing: the source
|
marshalling a [Session.t] through a file, which buys nothing: the source
|
||||||
cannot have changed between the two, because the build that produced
|
cannot have changed between the two, because the build that produced
|
||||||
this binary is the one that exec'd it. *)
|
this binary is the one that exec'd it. *)
|
||||||
let session, _ = Session.create ~debug ~x86 ~file () in
|
let session, _, _ = Session.create_dev ~debug ~x86 ~file () in
|
||||||
(* The program's output has to reach an editor exactly as it did when the
|
(* The program's output has to reach an editor exactly as it did when the
|
||||||
daemon held the other end of a pipe. Same pipe, one process: fd 1 is
|
daemon held the other end of a pipe. Same pipe, one process: fd 1 is
|
||||||
replaced before the program starts, and the accept loop drains it —
|
replaced before the program starts, and the accept loop drains it —
|
||||||
@ -5576,9 +5693,10 @@ let start_merged ?(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;
|
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 exe = Filename.concat dir "program" in
|
let exe = Filename.concat dir "program" in
|
||||||
(* The host's IR goes straight to its final home rather than being written
|
(* The host's IR goes straight to its final home rather than being written
|
||||||
into the build's working directory and moved: the merged link is spelled
|
into the build's working directory and moved: the merged link is spelled
|
||||||
@ -5589,8 +5707,13 @@ let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () =
|
|||||||
ignore
|
ignore
|
||||||
(merged_executable
|
(merged_executable
|
||||||
~opts:{ Build.default with Build.dev = true; Build.debug; Build.x86 }
|
~opts:{ Build.default with Build.dev = true; Build.debug; Build.x86 }
|
||||||
~csrcs:l.Load.csrcs ~lflags:l.Load.lflags ~pnames:[]
|
~csrcs ~lflags ~pnames:[]
|
||||||
session.Session.host ~out:exe ~ll:host_ll);
|
session.Session.host ~out:exe ~ll:host_ll);
|
||||||
|
(* Read by the park, so the first one says the session is waiting rather
|
||||||
|
than that a program finished. *)
|
||||||
|
Unix.putenv "FLAN_DEV_NO_MAIN"
|
||||||
|
(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
|
||||||
|
|||||||
7
lib/dune
7
lib/dune
@ -39,7 +39,8 @@
|
|||||||
%{workspace_root}/runtime/flan_rt.c
|
%{workspace_root}/runtime/flan_rt.c
|
||||||
%{workspace_root}/runtime/flan_dev.c
|
%{workspace_root}/runtime/flan_dev.c
|
||||||
%{workspace_root}/runtime/flan_dyn.c
|
%{workspace_root}/runtime/flan_dyn.c
|
||||||
%{workspace_root}/runtime/flan_dyn.h)
|
%{workspace_root}/runtime/flan_dyn.h
|
||||||
|
%{workspace_root}/vendor/agent/flan_agent.c)
|
||||||
(action
|
(action
|
||||||
(with-stdout-to
|
(with-stdout-to
|
||||||
runtime_src.ml
|
runtime_src.ml
|
||||||
@ -58,4 +59,8 @@
|
|||||||
; compiled against the old one.
|
; compiled against the old one.
|
||||||
(echo "|c}\n\nlet dyn_header = {c|\n")
|
(echo "|c}\n\nlet dyn_header = {c|\n")
|
||||||
(cat %{workspace_root}/runtime/flan_dyn.h)
|
(cat %{workspace_root}/runtime/flan_dyn.h)
|
||||||
|
; And the agent's C, which flan dev links into every program it builds
|
||||||
|
; whether or not the source imports the package. See [Dev.with_agent].
|
||||||
|
(echo "|c}\n\nlet agent_source = {c|\n")
|
||||||
|
(cat %{workspace_root}/vendor/agent/flan_agent.c)
|
||||||
(echo "|c}\n")))))
|
(echo "|c}\n")))))
|
||||||
|
|||||||
113
lib/session.ml
113
lib/session.ml
@ -252,8 +252,7 @@ let with_expansion_macros (forms : Form.t list) (load : unit -> 'a) : 'a * Form.
|
|||||||
Parse.expansion_macros := [];
|
Parse.expansion_macros := [];
|
||||||
(r, Load.macro_union (own_macros forms) defined)
|
(r, Load.macro_union (own_macros forms) defined)
|
||||||
|
|
||||||
let create ?(debug = false) ?(x86 = false) ~file () =
|
let of_forms ~debug ~x86 ~file forms =
|
||||||
let forms = Reader.read_file file in
|
|
||||||
let l, mine = with_expansion_macros forms (fun () -> Load.program ~file forms) in
|
let l, mine = with_expansion_macros forms (fun () -> Load.program ~file forms) in
|
||||||
let p, env = Check.program_with_env l.Load.decls in
|
let p, env = Check.program_with_env l.Load.decls in
|
||||||
({ file; decls = l.Load.decls; program = p; env; host = p; pkgs = l.Load.pkgs;
|
({ file; decls = l.Load.decls; program = p; env; host = p; pkgs = l.Load.pkgs;
|
||||||
@ -264,6 +263,92 @@ let create ?(debug = false) ?(x86 = false) ~file () =
|
|||||||
thunks = 0; debug; x86;
|
thunks = 0; debug; x86;
|
||||||
built = record_built env p p.Tast.fns SM.empty; live = SM.empty }, l)
|
built = record_built env p p.Tast.fns SM.empty; live = SM.empty }, l)
|
||||||
|
|
||||||
|
let create ?(debug = false) ?(x86 = false) ~file () =
|
||||||
|
of_forms ~debug ~x86 ~file (Reader.read_file file)
|
||||||
|
|
||||||
|
(* ── A file loaded a form at a time, keeping what compiles ─────────── *)
|
||||||
|
|
||||||
|
(* The form a diagnostic is about: the last one in [forms] that starts at or
|
||||||
|
before the position it was reported at — the call, for an error inside a
|
||||||
|
macro's expansion. [None] when the position is in no file these forms came
|
||||||
|
from, which is an error nothing here can drop a form to avoid. *)
|
||||||
|
let blame (forms : Form.t list) (d : Loc.diag) : Form.t option =
|
||||||
|
let at = Loc.call_site d.Loc.dloc in
|
||||||
|
List.fold_left
|
||||||
|
(fun acc (f : Form.t) ->
|
||||||
|
if String.equal f.Form.loc.Loc.file at.Loc.file
|
||||||
|
&& Loc.before f.Form.loc at <= 0
|
||||||
|
then Some f
|
||||||
|
else acc)
|
||||||
|
None forms
|
||||||
|
|
||||||
|
(* SBCL's [load] and CIDER's load-file: a file whose third form does not
|
||||||
|
compile still defines the other two. [attempt] is run over [forms]; each
|
||||||
|
refusal drops the form it is about and runs it again over the rest, so a
|
||||||
|
form that only failed because it called one that was dropped is dropped
|
||||||
|
with its own error on the next round. Each round drops at least one form,
|
||||||
|
so this ends. Answers what [attempt] returned, the forms it was given, and
|
||||||
|
every error in the order found. An error no form can be blamed for is
|
||||||
|
raised as it came. *)
|
||||||
|
let pruned (attempt : Form.t list -> 'a) (forms : Form.t list) :
|
||||||
|
'a * Form.t list * Loc.diag list =
|
||||||
|
let rec go forms errs =
|
||||||
|
let drop ds e =
|
||||||
|
let bad = List.map (blame forms) ds in
|
||||||
|
if List.exists Option.is_none bad then raise e
|
||||||
|
else
|
||||||
|
let bad = List.filter_map Fun.id bad in
|
||||||
|
go (List.filter (fun f -> not (List.memq f bad)) forms)
|
||||||
|
(List.rev_append ds errs)
|
||||||
|
in
|
||||||
|
match attempt forms with
|
||||||
|
| r -> (r, forms, List.rev errs)
|
||||||
|
| exception (Loc.Error d as e) -> drop [ d ] e
|
||||||
|
| exception (Loc.Errors ds as e) -> drop ds e
|
||||||
|
in
|
||||||
|
go forms []
|
||||||
|
|
||||||
|
(* Where [flan dev] writes the [main] a file without one is given. Not a path:
|
||||||
|
nothing reads it back, and a location here is how the session tells that
|
||||||
|
[main] apart from one somebody wrote. *)
|
||||||
|
let stub_file = "<flan dev>"
|
||||||
|
|
||||||
|
let stub_main () = Reader.read_all ~file:stub_file "(defn main [] i32 0)"
|
||||||
|
|
||||||
|
let declares_main (forms : Form.t list) =
|
||||||
|
List.exists
|
||||||
|
(fun (f : Form.t) ->
|
||||||
|
match f.Form.v with
|
||||||
|
| Form.List
|
||||||
|
({ Form.v = Form.Sym "defn"; _ } :: { Form.v = Form.Sym "main"; _ } :: _)
|
||||||
|
-> true
|
||||||
|
| _ -> false)
|
||||||
|
forms
|
||||||
|
|
||||||
|
(* Whether the [main] this session would run is one somebody wrote. *)
|
||||||
|
let has_main t =
|
||||||
|
List.exists
|
||||||
|
(fun (d : Ast.decl) ->
|
||||||
|
Ast.declared_name d = Some "main"
|
||||||
|
&& not (String.equal d.Ast.dloc.Loc.file stub_file))
|
||||||
|
t.decls
|
||||||
|
|
||||||
|
(* The session [flan dev] starts. A file with a [main] is [create]'s. A file
|
||||||
|
without one is the empty image of SBCL's order: a [main] that returns at
|
||||||
|
once is added so there is a process to park, and the file's forms are
|
||||||
|
loaded into it as [pruned] loads them — what compiles is in the host, and
|
||||||
|
what does not is answered beside the session rather than instead of it. A
|
||||||
|
[main] loaded later replaces the stub like any other redefinition. *)
|
||||||
|
let create_dev ?(debug = false) ?(x86 = false) ~file () =
|
||||||
|
let forms = Reader.read_file file in
|
||||||
|
if declares_main forms then
|
||||||
|
let t, l = create ~debug ~x86 ~file () in
|
||||||
|
(t, l, [])
|
||||||
|
else
|
||||||
|
let build forms = of_forms ~debug ~x86 ~file (forms @ stub_main ()) in
|
||||||
|
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
|
||||||
calls its own package's functions has to find them. [Load.program] hands
|
calls its own package's functions has to find them. [Load.program] hands
|
||||||
@ -669,8 +754,15 @@ let restore t h =
|
|||||||
newest one and no older activation is left running. *)
|
newest one and no older activation is left running. *)
|
||||||
let rerun t = t.live <- SM.empty
|
let rerun t = t.live <- SM.empty
|
||||||
|
|
||||||
let eval ?(origin = "<eval>") ?pause ?(running = true) t src : change =
|
(* [forms], when given, are [src] already read — [pruned] runs this over a
|
||||||
let forms = Reader.read_all ~file:origin src in
|
file a form fewer each round and has no text for the subset. [base] is the
|
||||||
|
file an [(import ...)] in them is resolved against, the session's own when
|
||||||
|
absent: a file loaded from another directory names its packages from
|
||||||
|
there. *)
|
||||||
|
let eval ?(origin = "<eval>") ?base ?forms ?pause ?(running = true) t src : change =
|
||||||
|
let forms =
|
||||||
|
match forms with Some f -> f | None -> Reader.read_all ~file:origin src
|
||||||
|
in
|
||||||
(* 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;
|
||||||
@ -683,7 +775,8 @@ let eval ?(origin = "<eval>") ?pause ?(running = true) t src : change =
|
|||||||
let macros = ref t.macros in
|
let macros = ref t.macros in
|
||||||
let incoming =
|
let incoming =
|
||||||
let l, mine =
|
let l, mine =
|
||||||
with_expansion_macros forms (fun () -> Load.program ~file:t.file forms)
|
with_expansion_macros forms (fun () ->
|
||||||
|
Load.program ~file:(Option.value base ~default:t.file) forms)
|
||||||
in
|
in
|
||||||
(* An evaluated import *adds* to the session's set, so a macro brought in
|
(* An evaluated import *adds* to the session's set, so a macro brought in
|
||||||
by C-c C-k is there for the C-c C-c after it. A union and not an
|
by C-c C-k is there for the C-c C-c after it. A union and not an
|
||||||
@ -2236,6 +2329,16 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
|||||||
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) t.macros (fun () -> Parse.expr form) in
|
||||||
|
(* 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
|
||||||
|
reaches [physics/integrate]. The qualification [eval] gives a declaration
|
||||||
|
from the same buffer. A [defn-] is reachable too, because the location
|
||||||
|
[private_ref] compares is the buffer's own path. *)
|
||||||
|
let parsed =
|
||||||
|
match package_of t origin with
|
||||||
|
| None -> parsed
|
||||||
|
| Some p -> Load.rename_expr p.Load.owns p.Load.alias [] parsed
|
||||||
|
in
|
||||||
(* Wrapped before the checker, so the call is checked like any other and a
|
(* Wrapped before the checker, so the call is checked like any other and a
|
||||||
prelude that stopped offering [pause] would be an ordinary unknown name
|
prelude that stopped offering [pause] would be an ordinary unknown name
|
||||||
rather than a thunk that silently did not stop. The [Do] takes the
|
rather than a thunk that silently did not stop. The [Do] takes the
|
||||||
|
|||||||
8
test/programs/dev-load-errors.flan
Normal file
8
test/programs/dev-load-errors.flan
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
;;;; A file loaded into a session with two forms that do not compile: [bad]
|
||||||
|
;;;; returns a string where it says i64, and [good] calls [bad], so it goes
|
||||||
|
;;;; when [bad] does. [fine] is the form that has to arrive anyway.
|
||||||
|
(defn good [] i64 (bad))
|
||||||
|
|
||||||
|
(defn bad [] i64 "x")
|
||||||
|
|
||||||
|
(defn fine [] i64 42)
|
||||||
9
test/programs/dev-load-generic.flan
Normal file
9
test/programs/dev-load-generic.flan
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
;;;; A file loaded into a session with no main: a generic and a plain function.
|
||||||
|
(defn biggest [xs [$t]] $t
|
||||||
|
{:where (ordered? $t)}
|
||||||
|
(let [m (at xs 0)]
|
||||||
|
(dotimes [i (length xs)]
|
||||||
|
(set m (max m (at xs i))))
|
||||||
|
m))
|
||||||
|
|
||||||
|
(defn twice [n i64] i64 (* 2 n))
|
||||||
@ -1,19 +1,9 @@
|
|||||||
;;;; A program that has not been told about the agent and is still running.
|
;;;; A program that never imports the agent and is still running. flan dev
|
||||||
|
;;;; links the agent anyway, so a delivery is queued rather than refused; it
|
||||||
|
;;;; installs at a frame boundary the program never reaches, since nothing in
|
||||||
|
;;;; it calls (agent/poll).
|
||||||
;;;;
|
;;;;
|
||||||
;;;; dev-noagent.flan is the other half of this pair and stops short of it: its
|
;;;; No (import agent ...) anywhere, and a loop that outlasts the test.
|
||||||
;;;; main returns, so a moment later it is parked, and a redefinition sent to a
|
|
||||||
;;;; parked program is answered by the parking path. This one keeps running,
|
|
||||||
;;;; which is the state nothing had pinned: there is no agent in the process to
|
|
||||||
;;;; hand a module to and no socket to fall back on, so the daemon refuses the
|
|
||||||
;;;; delivery.
|
|
||||||
;;;;
|
|
||||||
;;;; That is the honest answer for it: the reply says the program has no agent
|
|
||||||
;;;; and how to add one. A program that *links* the agent has its socket bound
|
|
||||||
;;;; by the package's constructor before main, and the one way that bind fails
|
|
||||||
;;;; — a socket path too long for a unix socket — is refused when flan dev
|
|
||||||
;;;; starts.
|
|
||||||
;;;;
|
|
||||||
;;;; So: no (import agent ...) anywhere, and a loop that outlasts the test.
|
|
||||||
(defn step [] i64 7)
|
(defn step [] i64 7)
|
||||||
|
|
||||||
(defn main [] i32
|
(defn main [] i32
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
;;;; A program [flan dev] can host that never calls (agent/start ...), which
|
;;;; A program that never imports the agent. flan dev links the agent's C into
|
||||||
;;;; is the one condition the merged session and the two-process daemon answer
|
;;;; every program it builds, so a definition and an expression sent while it
|
||||||
;;;; differently: [two_process] kills the child and fails, [merged_serve]
|
;;;; is parked both land. It prints and returns: a Flan main that returns under
|
||||||
;;;; warns and serves anyway. See test_dev.ml's last block.
|
;;;; flan dev parks instead of ending the process (flan_merged_exit).
|
||||||
;;;;
|
|
||||||
;;;; No (import agent ...) at all, because the point is a program that has not
|
|
||||||
;;;; been told about the agent rather than one that forgot a call. It prints
|
|
||||||
;;;; and returns: a Flan main that returns under [flan dev] parks instead of
|
|
||||||
;;;; ending the process (flan_merged_exit), so the session outlives it and the
|
|
||||||
;;;; accept loop keeps answering.
|
|
||||||
(defn step [] i64 7)
|
(defn step [] i64 7)
|
||||||
|
|
||||||
(defn main [] i32
|
(defn main [] i32
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
;;;; A file with no main: flan dev has nothing to run, and says so before building.
|
;;;; A file with no main. flan dev starts a session on it anyway, with a stub
|
||||||
|
;;;; main that returns at once, and the file's functions are in the host.
|
||||||
|
;;;; flan dev --two-process refuses it, since its program is a child process.
|
||||||
(defn helper [] i64 1)
|
(defn helper [] i64 1)
|
||||||
|
|||||||
319
test/test_dev.ml
319
test/test_dev.ml
@ -4885,55 +4885,20 @@ let () =
|
|||||||
(try ignore (Unix.waitpid [ Unix.WNOHANG ] opid) with Unix.Unix_error _ -> ());
|
(try ignore (Unix.waitpid [ Unix.WNOHANG ] opid) with Unix.Unix_error _ -> ());
|
||||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ osock; oout ];
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ osock; oout ];
|
||||||
|
|
||||||
(* A program that never calls [agent/start], which is the one condition on
|
(* A program whose source never mentions the agent. [flan dev] links the
|
||||||
which the two shapes of [flan dev] deliberately disagree. [two_process]
|
agent's C into every program it builds ([Dev.with_agent]), and the
|
||||||
kills its child and [failwith]s: the program is a separate process, the
|
constructor binds the socket before main, so the file is reachable from
|
||||||
daemon owns it, and a daemon with nothing to deliver to is useless.
|
the editor as it stands: dev-noagent.flan's main prints and returns,
|
||||||
[merged_serve] prints a warning and serves anyway, because the thing it
|
and a definition and an expression sent to the park both land.
|
||||||
would have to kill is itself — an editor connected to it still deserves
|
|
||||||
[describe], [defs] and the program's output, and only a *delivery*
|
|
||||||
needs the agent.
|
|
||||||
|
|
||||||
That second policy was held up by nothing at all. Nothing in the suite
|
[--llvm] here, on a daemon that was going to stand up anyway: one
|
||||||
reached lib/dev.ml's warning branch, and the shape of the mistake it
|
merged LLVM daemon stays in the suite now that a flagless one is x86,
|
||||||
guards against is a small one: copying the daemon's answer back into
|
and the host listing proves the opt-in reached the build. *)
|
||||||
the merged path is the obvious tidy-up, and it would turn every program
|
|
||||||
without an agent into a session that dies at startup, silently, because
|
|
||||||
no test would have noticed.
|
|
||||||
|
|
||||||
So what is asserted is the policy and not the sentence: the session
|
|
||||||
answers. The warning text is checked second, as the evidence that this
|
|
||||||
is the branch that produced it and not some other path that happened to
|
|
||||||
work.
|
|
||||||
|
|
||||||
WHEN it answers is asserted too, and that is the newer half. The wait
|
|
||||||
for the agent socket used to sit in front of [accept_loop], so this
|
|
||||||
[describe] could not arrive until the ten seconds had run out — the
|
|
||||||
block's cost, and every real session's first keystroke. The wait is a
|
|
||||||
deadline the session passes now ([Dev.agent_check]), so the reply comes
|
|
||||||
back immediately and the sentence is said later, by the accept loop,
|
|
||||||
once the deadline is behind it. The second half is what still costs ten
|
|
||||||
seconds here: a warning about a program that is never going to start an
|
|
||||||
agent cannot honestly be said before waiting for one.
|
|
||||||
|
|
||||||
[describe] and not a cheaper op on purpose: it is what
|
|
||||||
[emacs/flan.el] sends straight after [flan--open] (flan.el:646) and
|
|
||||||
what its poll sends after that, so this is the stall a person would
|
|
||||||
actually have felt. *)
|
|
||||||
let nsock = tmp "noagent.sock" and nlog = tmp "noagent.log" in
|
let nsock = tmp "noagent.sock" and nlog = tmp "noagent.log" in
|
||||||
(try Sys.remove nsock with Sys_error _ -> ());
|
(try Sys.remove nsock with Sys_error _ -> ());
|
||||||
(* Its own stderr, unlike every other daemon here: the warning is the
|
|
||||||
evidence and it is written there. *)
|
|
||||||
let nfd =
|
let nfd =
|
||||||
Unix.openfile nlog [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
Unix.openfile nlog [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||||
in
|
in
|
||||||
(* [--llvm] here, on a daemon that was going to stand up anyway: the
|
|
||||||
opt-in is the other half of the default checked at the top of this file,
|
|
||||||
and proving it costs the same [stat] on the same directory. It also
|
|
||||||
keeps one merged LLVM daemon in the suite now that a flagless one is an
|
|
||||||
x86 one -- this test is about a program with no [(agent/start ...)],
|
|
||||||
which is a claim about the daemon and not about a backend, so it is the
|
|
||||||
cheapest place for both. *)
|
|
||||||
let npid =
|
let npid =
|
||||||
Unix.create_process flan
|
Unix.create_process flan
|
||||||
[| flan; "dev"; "programs/dev-noagent.flan"; "-s"; nsock; "--llvm" |]
|
[| flan; "dev"; "programs/dev-noagent.flan"; "-s"; nsock; "--llvm" |]
|
||||||
@ -4955,78 +4920,44 @@ let () =
|
|||||||
if Sys.file_exists (nhost "s") then
|
if Sys.file_exists (nhost "s") then
|
||||||
fail "flan dev --llvm left an x86 listing at %s" (nhost "s");
|
fail "flan dev --llvm left an x86 listing at %s" (nhost "s");
|
||||||
let nc = connect nsock in
|
let nc = connect nsock in
|
||||||
(* The exception arm is not defensive: a session that adopted the
|
let parked () =
|
||||||
daemon's policy would exit here, and the connection would come back
|
match Wire.field (request nc "(:op \"describe\")") "parked" with
|
||||||
ECONNRESET rather than with a status. Reported by name because an
|
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||||
uncaught [Unix_error] out of a test binary says nothing about which
|
| _ -> false
|
||||||
test. *)
|
in
|
||||||
let nt0 = Unix.gettimeofday () in
|
if not (await parked) then fail "the agentless program never parked"
|
||||||
(match Wire.parse (Wire.send nc "(:op \"describe\")"; Wire.recv nc) with
|
else begin
|
||||||
| r when status r = "ok" -> ()
|
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
||||||
| r ->
|
let r =
|
||||||
fail "a program without (agent/start ...) was not served: describe: %s"
|
request nc
|
||||||
(status r)
|
"(:op \"eval\" :code \"(defn step [] i64 8)\" :file \
|
||||||
| exception e ->
|
\"programs/dev-noagent.flan\")"
|
||||||
fail
|
in
|
||||||
"a program without (agent/start ...) ended the session instead of \
|
if status r <> "ok" then
|
||||||
drawing a warning: %s" (Printexc.to_string e));
|
fail "a definition for a program that never imports the agent: %s"
|
||||||
let ndt = Unix.gettimeofday () -. nt0 in
|
(said r);
|
||||||
(* Two seconds, against a stall that was ten and a reply that is a
|
let r =
|
||||||
fraction of one. The threshold is loose on purpose: what is being
|
request nc
|
||||||
held is "the session does not wait for the program's socket before
|
"(:op \"eval-expr\" :code \"(step)\" :file \
|
||||||
answering", and a number close to the real cost would fail on a
|
\"programs/dev-noagent.flan\")"
|
||||||
loaded machine for a reason that has nothing to do with the wait. *)
|
in
|
||||||
if ndt > 2. then
|
if Wire.string_field r "value" <> Some "8" then
|
||||||
fail
|
fail "an expression for a program that never imports the agent: %s %s"
|
||||||
"the first editor request waited %.1fs on a program without \
|
(status r) (said r)
|
||||||
(agent/start ...); the accept loop is gated on the agent again"
|
end;
|
||||||
ndt;
|
(try
|
||||||
(* Dropped rather than closed with [(:op "close")], and the difference
|
ignore (Wire.send nc "(:op \"close\")");
|
||||||
is the rest of this row: [close] ends the session, the process
|
ignore (Wire.recv nc)
|
||||||
[_exit]s, and the deadline below would be waited out by nobody. A
|
with _ -> ());
|
||||||
dropped connection leaves the accept loop cycling, which is where
|
|
||||||
the sentence is said from. *)
|
|
||||||
(try Unix.close nc with Unix.Unix_error _ -> ())
|
(try Unix.close nc with Unix.Unix_error _ -> ())
|
||||||
end;
|
end;
|
||||||
(* And the sentence, which arrives after the deadline rather than before
|
|
||||||
the loop. Awaited with the daemon still alive — the accept loop is what
|
|
||||||
says it, so killing first would be testing that a dead process does not
|
|
||||||
print. Generous against the ten-second deadline for the reason the
|
|
||||||
threshold above is loose. *)
|
|
||||||
let nlog_says () =
|
|
||||||
contains_sub
|
|
||||||
(try In_channel.with_open_bin nlog In_channel.input_all
|
|
||||||
with Sys_error _ -> "")
|
|
||||||
"(import agent \"vendor:agent\")"
|
|
||||||
in
|
|
||||||
ignore (await ~ms:30000 nlog_says);
|
|
||||||
(try Unix.kill npid Sys.sigkill with Unix.Unix_error _ -> ());
|
(try Unix.kill npid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||||
(try ignore (Unix.waitpid [] npid) with Unix.Unix_error _ -> ());
|
(try ignore (Unix.waitpid [] npid) with Unix.Unix_error _ -> ());
|
||||||
if not (nlog_says ()) then
|
|
||||||
fail
|
|
||||||
"a program with no agent drew no warning from flan dev:\n%s"
|
|
||||||
(try In_channel.with_open_bin nlog In_channel.input_all
|
|
||||||
with Sys_error _ -> "");
|
|
||||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
[ nsock; nlog ];
|
[ nsock; nlog ];
|
||||||
|
|
||||||
(* ── ...and what a delivery to one is told ────────────────────────
|
(* And one still running, which never calls [(agent/poll)]: a delivery is
|
||||||
|
queued rather than refused, because the agent is there to take it. *)
|
||||||
The row above is about the daemon's own stderr. This one is about the
|
|
||||||
reply an editor gets for a redefinition, and it is here because that
|
|
||||||
reply was never pinned and this lane changed which of two it is.
|
|
||||||
|
|
||||||
A program that does not link the agent at all has no agent in this
|
|
||||||
process to call and no socket to fall back to, so the delivery is
|
|
||||||
REFUSED, and the reply says the program has no agent and how to give it
|
|
||||||
one — both for a redefinition and for an expression. "Queued" would
|
|
||||||
have promised a poll that has nothing to drain, and "connect: No such
|
|
||||||
file or directory" names a path the reader never chose.
|
|
||||||
|
|
||||||
It needs the program to be running, which is why it is not folded into
|
|
||||||
the row above: dev-noagent.flan's main returns, so it parks within the
|
|
||||||
first moment and a delivery to it is answered by the parking path
|
|
||||||
instead. This fixture loops. *)
|
|
||||||
let gsock = tmp "noagent-running.sock" and glog = tmp "noagent-running.log" in
|
let gsock = tmp "noagent-running.sock" and glog = tmp "noagent-running.log" in
|
||||||
(try Sys.remove gsock with Sys_error _ -> ());
|
(try Sys.remove gsock with Sys_error _ -> ());
|
||||||
let gfd =
|
let gfd =
|
||||||
@ -5049,46 +4980,9 @@ 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 msg = Option.value ~default:"" (Wire.string_field r "message") in
|
if status r <> "ok" then
|
||||||
(* Refused, and the reason is the program's rather than the compiler's:
|
fail "a delivery to a running program that never imports the agent: %s"
|
||||||
the module built, and what is missing is an agent to hand it to. The
|
(Option.value ~default:"" (Wire.string_field r "message"));
|
||||||
fix it names is the import. *)
|
|
||||||
let names_the_fix m =
|
|
||||||
contains_sub m "has no agent"
|
|
||||||
&& contains_sub m "(import agent \"vendor:agent\")"
|
|
||||||
&& contains_sub m "(agent/poll)"
|
|
||||||
in
|
|
||||||
if status r = "ok" then
|
|
||||||
fail
|
|
||||||
"a redefinition for a program with no agent in it was answered \
|
|
||||||
ok%s — nothing can install it"
|
|
||||||
(match Wire.string_field r "note" with
|
|
||||||
| Some n -> Printf.sprintf " (note: %S)" n
|
|
||||||
| None -> "")
|
|
||||||
else if not (names_the_fix msg) then
|
|
||||||
fail "a delivery to a running agentless program was refused with: %S"
|
|
||||||
msg;
|
|
||||||
(* And an expression, which used to be answered with the connect's own
|
|
||||||
errno. *)
|
|
||||||
let r =
|
|
||||||
request gc
|
|
||||||
"(:op \"eval-expr\" :code \"(step)\" :file \
|
|
||||||
\"programs/dev-noagent-running.flan\")"
|
|
||||||
in
|
|
||||||
let msg = Option.value ~default:"" (Wire.string_field r "message") in
|
|
||||||
if status r = "ok" || not (names_the_fix msg) then
|
|
||||||
fail "an expression for a running agentless program was answered %s: %S"
|
|
||||||
(status r) msg;
|
|
||||||
(* And the session is still there afterwards, which is the rest of the
|
|
||||||
claim: a refusal is a reply, not the end. *)
|
|
||||||
(match request gc "(:op \"describe\")" with
|
|
||||||
| r when status r = "ok" -> ()
|
|
||||||
| r ->
|
|
||||||
fail "the session did not survive an unreachable delivery: %s"
|
|
||||||
(status r)
|
|
||||||
| exception e ->
|
|
||||||
fail "the session ended on an unreachable delivery: %s"
|
|
||||||
(Printexc.to_string e));
|
|
||||||
(try
|
(try
|
||||||
ignore (Wire.send gc "(:op \"close\")");
|
ignore (Wire.send gc "(:op \"close\")");
|
||||||
ignore (Wire.recv gc)
|
ignore (Wire.recv gc)
|
||||||
@ -5154,8 +5048,11 @@ let () =
|
|||||||
refused_at_start "a TMPDIR that does not exist" ~tmpdir:missing
|
refused_at_start "a TMPDIR that does not exist" ~tmpdir:missing
|
||||||
~prog:"programs/dev-lateagent.flan" ~mode
|
~prog:"programs/dev-lateagent.flan" ~mode
|
||||||
[ missing ^ " does not exist"; "TMPDIR=/tmp flan dev" ];
|
[ missing ^ " does not exist"; "TMPDIR=/tmp flan dev" ];
|
||||||
refused_at_start "a program with no main" ~tmpdir:here ~prog:nomain
|
(* Only the two-process daemon: its program is a child, and a child
|
||||||
~mode [ "has no main"; "(defn main [] i32" ])
|
whose main returns is gone. One process starts on the file. *)
|
||||||
|
if mode <> [||] then
|
||||||
|
refused_at_start "a program with no main" ~tmpdir:here ~prog:nomain
|
||||||
|
~mode [ "has no main"; "(defn main [] i32"; "without --two-process" ])
|
||||||
[ [||]; [| "--two-process" |] ];
|
[ [||]; [| "--two-process" |] ];
|
||||||
(try Unix.rmdir deep with Unix.Unix_error _ -> ());
|
(try Unix.rmdir deep with Unix.Unix_error _ -> ());
|
||||||
|
|
||||||
@ -7264,10 +7161,9 @@ let () =
|
|||||||
let lc = connect lsock in
|
let lc = connect lsock in
|
||||||
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
||||||
(* The program sleeps for three seconds before [agent/start], so this is
|
(* The program sleeps for three seconds before [agent/start], so this is
|
||||||
asked inside the delay. Two seconds is the threshold for the reason
|
asked inside the delay. Two seconds is the threshold because the reply
|
||||||
the agentless row gives: the reply is a fraction of one and the bug
|
is a fraction of one and the bug was ten, so anything in between is a
|
||||||
was ten, so anything in between is a loaded machine rather than a
|
loaded machine rather than a regression. *)
|
||||||
regression. *)
|
|
||||||
let lt0 = Unix.gettimeofday () in
|
let lt0 = Unix.gettimeofday () in
|
||||||
let r = request lc "(:op \"describe\")" in
|
let r = request lc "(:op \"describe\")" in
|
||||||
let ldt = Unix.gettimeofday () -. lt0 in
|
let ldt = Unix.gettimeofday () -. lt0 in
|
||||||
@ -7450,6 +7346,121 @@ let () =
|
|||||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
[ psock; pout ];
|
[ psock; pout ];
|
||||||
|
|
||||||
|
(* ── A session on a file with no main ─────────────────────────────
|
||||||
|
SBCL's order: the session comes up with nothing to run, and a file is
|
||||||
|
loaded into it. A load that has forms which do not compile installs the
|
||||||
|
rest and lists them; a re-run says there is no main and how to add one. *)
|
||||||
|
let msock = tmp "nomain.sock" and mout = tmp "nomain.out" in
|
||||||
|
(try Sys.remove msock with Sys_error _ -> ());
|
||||||
|
let mfd =
|
||||||
|
Unix.openfile mout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||||
|
in
|
||||||
|
let mpid =
|
||||||
|
Unix.create_process flan
|
||||||
|
[| flan; "dev"; "programs/dev-nomain.flan"; "-s"; msock |]
|
||||||
|
Unix.stdin mfd mfd
|
||||||
|
in
|
||||||
|
Unix.close mfd;
|
||||||
|
if not (listening ~pid:mpid msock) then begin
|
||||||
|
fail "a daemon on a file with no main %s (%S)" !listen_why
|
||||||
|
(In_channel.with_open_bin mout In_channel.input_all);
|
||||||
|
(try Unix.kill mpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
else begin
|
||||||
|
let mc = connect msock in
|
||||||
|
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
||||||
|
let value code =
|
||||||
|
let r =
|
||||||
|
request mc
|
||||||
|
(Printf.sprintf "(:op \"eval-expr\" :code %s :file \"<test>\")"
|
||||||
|
(Wire.quote code))
|
||||||
|
in
|
||||||
|
match Wire.string_field r "value" with
|
||||||
|
| Some v -> v
|
||||||
|
| None -> "refused: " ^ said r
|
||||||
|
in
|
||||||
|
let load file = request mc (Printf.sprintf "(:op \"load-file\" :file %S)" file) in
|
||||||
|
let errors r =
|
||||||
|
match Wire.field r "errors" with
|
||||||
|
| Some { Form.v = Form.List l; _ } ->
|
||||||
|
List.map
|
||||||
|
(fun e -> Option.value ~default:"" (Wire.string_field e "message"))
|
||||||
|
l
|
||||||
|
| _ -> []
|
||||||
|
in
|
||||||
|
(match value "(helper)" with
|
||||||
|
| "1" -> ()
|
||||||
|
| v -> fail "the no-main file's own function: %s" v);
|
||||||
|
let r = load "programs/dev-load-generic.flan" in
|
||||||
|
if status r <> "ok" then fail "loading a generic and a function: %s" (said r)
|
||||||
|
else begin
|
||||||
|
(match value "(let [ns [3 9 2]] (biggest (slice ns 0 3)))" with
|
||||||
|
| "9" -> ()
|
||||||
|
| v -> fail "a loaded generic, called: %s" v);
|
||||||
|
match value "(twice 21)" with
|
||||||
|
| "42" -> ()
|
||||||
|
| v -> fail "a loaded function, called: %s" v
|
||||||
|
end;
|
||||||
|
let r = load "programs/dev-load-errors.flan" in
|
||||||
|
if status r <> "ok" then
|
||||||
|
fail "a load with two bad forms installed nothing: %s" (said r)
|
||||||
|
else begin
|
||||||
|
(match errors r with
|
||||||
|
| [ a; b ] ->
|
||||||
|
if not (contains_sub a "expected i64" && contains_sub b "unknown function bad")
|
||||||
|
then fail "a load listed %S and %S" a b
|
||||||
|
| es -> fail "a load with two bad forms listed %d" (List.length es));
|
||||||
|
(match value "(fine)" with
|
||||||
|
| "42" -> ()
|
||||||
|
| v -> fail "the form that compiled beside two that did not: %s" v);
|
||||||
|
if not (contains_sub (value "(good)") "refused") then
|
||||||
|
fail "a form that called one left out was installed"
|
||||||
|
end;
|
||||||
|
(* Nothing compiles: a refusal, with the error where every refusal
|
||||||
|
puts it and the list beside it. *)
|
||||||
|
let r =
|
||||||
|
request mc
|
||||||
|
"(:op \"load-file\" :file \"programs/dev-load-errors.flan\" :code \
|
||||||
|
\"(defn bad [] i64 \\\"x\\\")\")"
|
||||||
|
in
|
||||||
|
if status r <> "error" || errors r = [] || Wire.string_field r "loc" = None
|
||||||
|
then fail "a load where nothing compiles: %s %s" (status r) (said r);
|
||||||
|
let r = request mc "(:op \"rerun\")" in
|
||||||
|
if status r <> "error"
|
||||||
|
|| not (contains_sub (said r) "has no main")
|
||||||
|
|| not (contains_sub (said r) "(defn main [] i32")
|
||||||
|
then fail "a re-run with no main: %s %s" (status r) (said r);
|
||||||
|
(* A main loaded into it is the one a re-run runs. *)
|
||||||
|
let r =
|
||||||
|
request mc
|
||||||
|
"(:op \"eval\" :code \"(defn main [] i32 (println \\\"from main\\\") 0)\" \
|
||||||
|
:file \"programs/dev-nomain.flan\")"
|
||||||
|
in
|
||||||
|
if status r <> "ok" then fail "loading a main: %s" (said r)
|
||||||
|
else begin
|
||||||
|
let before = Buffer.length output in
|
||||||
|
let r = request mc "(:op \"rerun\")" in
|
||||||
|
if status r <> "ok" then fail "a re-run after a main was loaded: %s" (said r)
|
||||||
|
else if
|
||||||
|
not
|
||||||
|
(await (fun () ->
|
||||||
|
ignore (request mc "(:op \"describe\")");
|
||||||
|
contains_sub
|
||||||
|
(Buffer.sub output before (Buffer.length output - before))
|
||||||
|
"from main"))
|
||||||
|
then fail "the loaded main did not run"
|
||||||
|
end;
|
||||||
|
(try
|
||||||
|
ignore (Wire.send mc "(:op \"close\")");
|
||||||
|
ignore (Wire.recv mc)
|
||||||
|
with _ -> ());
|
||||||
|
(try Unix.close mc with Unix.Unix_error _ -> ())
|
||||||
|
end;
|
||||||
|
(try Unix.kill mpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||||
|
(try ignore (Unix.waitpid [] mpid) with Unix.Unix_error _ -> ());
|
||||||
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
|
[ msock; mout ];
|
||||||
|
|
||||||
(* ── A class redefined under its own instances ──────────────────
|
(* ── A class redefined under its own instances ──────────────────
|
||||||
CLHS 4.3.6's update protocol, end to end, with a real editor at one
|
CLHS 4.3.6's update protocol, end to end, with a real editor at one
|
||||||
end and the running program's own heap at the other. "A method added
|
end and the running program's own heap at the other. "A method added
|
||||||
|
|||||||
@ -1619,4 +1619,88 @@ let () =
|
|||||||
| _ ->
|
| _ ->
|
||||||
fail "two slots that strip to one name did not keep their raw spellings"));
|
fail "two slots that strip to one name did not keep their raw spellings"));
|
||||||
|
|
||||||
|
(* ── A file with no main ───────────────────────────────────────────
|
||||||
|
[create_dev] gives it a [main] that returns, so there is a process to
|
||||||
|
start, and [has_main] tells that stub from one somebody wrote. *)
|
||||||
|
(match Session.create_dev ~file:"programs/dev-nomain.flan" () with
|
||||||
|
| t, _, [] ->
|
||||||
|
if Session.has_main t then fail "the stub main counted as the file's own";
|
||||||
|
if not
|
||||||
|
(List.exists (fun (f : Tast.fn) -> f.Tast.name = "main")
|
||||||
|
t.Session.host.Tast.fns)
|
||||||
|
then fail "a file with no main was given no main to start";
|
||||||
|
(* A [main] loaded later is the file's own. *)
|
||||||
|
(match Session.eval ~origin:"programs/dev-nomain.flan" t
|
||||||
|
"(defn main [] i32 3)" with
|
||||||
|
| _ ->
|
||||||
|
if not (Session.has_main t) then
|
||||||
|
fail "a main loaded into the session did not replace the stub"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "loading a main into a session started without one: %s" m)
|
||||||
|
| _, _, _ :: _ -> fail "dev-nomain.flan had forms that did not compile"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "a session over a file with no main: %s" m);
|
||||||
|
(match Session.create_dev ~file:"programs/dev-parknote.flan" () with
|
||||||
|
| t, _, _ ->
|
||||||
|
if not (Session.has_main t) then fail "a file's own main was not its own"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } -> fail "dev-parknote.flan: %s" m);
|
||||||
|
|
||||||
|
(* A file with no main whose forms do not all compile starts without them,
|
||||||
|
and names them. [good] goes because it calls [bad]. *)
|
||||||
|
(match Session.create_dev ~file:"programs/dev-load-errors.flan" () with
|
||||||
|
| t, _, errs ->
|
||||||
|
let fns = List.map (fun (f : Tast.fn) -> f.Tast.name) t.Session.host.Tast.fns in
|
||||||
|
if not (List.mem "fine" fns) then fail "the form that compiled was left out";
|
||||||
|
if List.mem "good" fns || List.mem "bad" fns then
|
||||||
|
fail "a form that did not compile is in the host";
|
||||||
|
if List.length errs <> 2 then
|
||||||
|
fail "a start with two bad forms named %d" (List.length errs)
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "a no-main file with a bad form refused the session: %s" m);
|
||||||
|
|
||||||
|
(* [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. *)
|
||||||
|
(let t, _ = Session.create ~file:"programs/dev-parknote.flan" () in
|
||||||
|
let src =
|
||||||
|
In_channel.with_open_bin "programs/dev-load-errors.flan" In_channel.input_all
|
||||||
|
in
|
||||||
|
let origin = "programs/dev-load-errors.flan" in
|
||||||
|
let forms = Reader.read_all ~file:origin src in
|
||||||
|
let check forms =
|
||||||
|
let h = Session.held t in
|
||||||
|
Fun.protect ~finally:(fun () -> Session.restore t h)
|
||||||
|
(fun () -> ignore (Session.eval ~origin ~forms t src))
|
||||||
|
in
|
||||||
|
match Session.pruned check forms with
|
||||||
|
| (), kept, errs ->
|
||||||
|
if List.length kept <> 1 then fail "load kept %d forms, not 1" (List.length kept);
|
||||||
|
(match errs with
|
||||||
|
| [ a; b ] ->
|
||||||
|
if not (has a.Loc.dmsg "expected i64") then
|
||||||
|
fail "the first error a load found was %S" a.Loc.dmsg;
|
||||||
|
if not (has b.Loc.dmsg "unknown function bad") then
|
||||||
|
fail "the form that used a dropped one went for %S" b.Loc.dmsg
|
||||||
|
| _ -> fail "load found %d errors, not 2" (List.length errs))
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } -> fail "pruned raised: %s" m);
|
||||||
|
|
||||||
|
(* ── An expression from a package's file ─────────────────────────────
|
||||||
|
CIDER's rule: it resolves as the file would, so the package's own names
|
||||||
|
reach it bare, a [defn-] included. From the program's file the same bare
|
||||||
|
name is not the package's. *)
|
||||||
|
(let tq, _ = Session.create ~file:"programs/pkg-private.flan" () in
|
||||||
|
(match
|
||||||
|
Session.eval_expr ~origin:"programs/pkgs/secret/secret.flan" tq
|
||||||
|
"(+ (combine 1 2) (mix 3 4))"
|
||||||
|
with
|
||||||
|
| c ->
|
||||||
|
if not (has c.Session.ir "secret") then
|
||||||
|
fail "an expression from a package's file did not reach the package"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "an expression from a package's file: %s" m);
|
||||||
|
match
|
||||||
|
Session.eval_expr ~origin:"programs/pkg-private.flan" tq "(combine 1 2)"
|
||||||
|
with
|
||||||
|
| _ -> fail "a package's bare name resolved from the program's own file"
|
||||||
|
| exception Loc.Error _ -> ());
|
||||||
|
|
||||||
Test_support.report ~label:"session" ()
|
Test_support.report ~label:"session" ()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user