Merge: the dev daemon compiles with its own backend unless asked not to

This commit is contained in:
Joseph Ferano 2026-09-19 04:47:30 +07:00
commit 69c033e946
10 changed files with 403 additions and 54 deletions

11
NEXT.md
View File

@ -6,6 +6,17 @@
## The core thesis is done
**`flan dev` is the x86 backend now, with `--llvm` to leave it.** The flag that used to turn it on is the flag
that says you meant it; every other command is LLVM by default and unmoved, which is also what keeps
`lib/x86.ml`'s convention licensed — a dev build compiled entirely by it, a release build entirely by LLVM.
`--debug` picks LLVM on its own, because a redefinition module from this backend carries no line table.
What the flip costs, and it is more than the refusals: **there is no shadow stack here**, so a session built by
it stops on an error and cannot say where — no backtrace, no locals, no globals. The agent used to answer that
with "this program was not built with --dev", which is false of an x86 dev host; `Dev.ask` now rewrites it into
a sentence naming `--llvm`. `C-u C-c C-a` (the IR view) is gone for the same reason and refuses by name too.
Both are documented in `emacs/MANUAL.md`. Frame pushes in `X86.program` would close the larger of the two.
`flan dev --x86` works. The hand-written backend now serves the editor, which is the only thing it was ever
for, and it is roughly twice as fast as the LLVM path:

View File

@ -124,7 +124,7 @@ Eleven of them, and the four anyone starts with:
flan check <file.flan> type-check a program
flan run <file.flan> [flags] [-- args...] build and run it
flan build <file.flan> [-o out] [flags] build a native executable
flan dev <file.flan> [-s socket] start a live development session
flan dev <file.flan> [-s socket] [--llvm] start a live development session
```
Useful build options include `-O0``-O3`, `--debug`, `--sanitize`,
@ -135,6 +135,15 @@ guessed at. `run` is native-only; cross-built output should be run with an
appropriate WASI runtime or browser. A `.wasm` file is not a tiny native
executable in a trench coat.
`--x86` picks the hand-written backend (lib/x86.ml) instead of LLVM, and
`flan dev` is the one command that takes it unasked: a dev session is what it
was written for, it halves the `C-c C-c` round trip, and nothing it builds
outlives the session. `--llvm` is how to ask for the other one there — for a
program this backend refuses by name, for `--debug`, and for the inspector,
which walks a shadow stack it does not push. Every other command here is LLVM
by default and stays that way; `emacs/MANUAL.md` lists what the dev backend
does not do.
The other seven. Four print a stage of the pipeline, which is how you find out
what the compiler thinks it was given:

View File

@ -4,8 +4,15 @@
still exits 1 and still opens with [file:line:col: message]; a driver that
got to the end of the file hands over everything it found, sorted, with a
count after it. Nothing here parses the message the squiggle comes from
the span and the classification from the kind. *)
let with_errors path f =
the span and the classification from the kind.
[?x86_hint] is the one thing here that is not the same for every command.
A refusal from the dev backend is actionable in a different way depending on
how that backend was chosen: somebody who typed [--x86] already knows they
asked for it, and somebody who typed [flan dev] did not ask for anything.
The second is the common case now that it is the default, so the command
that takes it by default hands over the sentence naming the way out. *)
let with_errors ?x86_hint path f =
try f () with
| Flan.Loc.Error d ->
prerr_endline (Flan.Loc.report d);
@ -21,6 +28,7 @@ let with_errors path f =
"did not compile". *)
| Flan.X86.Unsupported m ->
prerr_endline ("x86: " ^ m);
Option.iter prerr_endline x86_hint;
ignore path;
exit 3
(* The JS dialect's refusal, and the same status for the same reason. It is
@ -152,12 +160,34 @@ let sanitize_flag = "--sanitize"
let two_process_flag = "--two-process"
(* The hand-written x86-64 backend (lib/x86.ml) instead of LLVM. The dev
backend from docs/DISCUSS.md item 15, off by default and named explicitly:
LLVM stays the release path and the default one. It covers a subset of the
IR and refuses the rest by name, so a build that succeeds is one it really
compiled. *)
backend from docs/DISCUSS.md item 15. It covers a subset of the IR and
refuses the rest by name, so a build that succeeds is one it really
compiled.
Where it is the default and where it is not is the whole of the split: it
is the dev backend, so [flan dev] takes it unasked, and every other command
here builds something that outlives the session and stays on LLVM. That is
also what keeps [lib/x86.ml]'s calling convention licensed a dev build is
compiled entirely by it and a release build entirely by LLVM, so the two
never meet in one process. On the commands that are still LLVM by default
this flag is how you ask for the other backend; on [flan dev] it is how you
say you meant it, which is what keeps [--x86 --debug] refused by name
rather than reached by accident. *)
let x86_flag = "--x86"
(* The other direction, and it exists because [flan dev] changed sides: a dev
session is the x86 backend now, and this is how to ask for the one that
compiles every node. The two reasons to want it are the ones the backend
itself names a program it refuses, and [--debug], which it has no line
table for in a redefinition module.
Every other command already builds through LLVM, so there it names the
default and is accepted and ignored. That is deliberate rather than an
oversight in the flag table: a flag that states what a command was going to
do anyway is a true sentence, and refusing it would make the one spelling
mean two different things depending on which subcommand it followed. *)
let llvm_flag = "--llvm"
(* [flan emit --x86] annotates, because it exists to be read. This turns that
off, and the only caller who wants it is the test that assembles the listing
both ways and compares the object's sections byte for byte -- a claim that
@ -167,7 +197,28 @@ let no_annotate_flag = "--no-annotate"
let flags =
[ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag;
x86_flag; no_annotate_flag ]
x86_flag; llvm_flag; no_annotate_flag ]
(* Which backend a command got, from the two flags and the default it would
have taken. One function because there is one rule, and the only thing that
varies is which way it falls with neither flag given: [flan dev] true, every
other command false.
Both flags together is refused rather than resolved. Last-one-wins is what
[-O] does above, but an optimisation level is a dial and this is a fork: the
two backends disagree on every aggregate, and someone who wrote both
spellings does not know which half of their session they were asking about.
A sentence is more use than a coin flip. *)
let backend_x86 ~default args =
let x86 = List.mem x86_flag args and llvm = List.mem llvm_flag args in
if x86 && llvm then begin
prerr_endline
"flan: --x86 and --llvm together — one process is compiled by one \
backend, so this asks a question with no answer. Pick the one you \
meant.";
exit 2
end;
if x86 then true else if llvm then false else default
(* [--target=wasm32-wasi] and [--target=web], the two cross targets. Unlike
the flags above, a target
@ -574,7 +625,11 @@ let () =
let dev = List.mem dev_flag rest in
let debug = List.mem debug_flag rest in
let sanitize = List.mem sanitize_flag rest in
let x86 = List.mem x86_flag rest in
(* LLVM by default, and deliberately not moved when [flan dev] moved: a
release build is the one thing here that outlives the session, and the
argument [lib/x86.ml] rests on is exactly that a dev build is compiled
entirely by it and a release build entirely by LLVM. *)
let x86 = backend_x86 ~default:false rest in
let opt = opt_of rest in
check_opt_against_debug ~debug ~opt;
let target = target_of rest in
@ -629,15 +684,31 @@ let () =
each redefinition module to still be firing after C-c C-c. It implies
-O0 on both, so it is asked for rather than assumed. *)
let debug = List.mem debug_flag rest in
(* One flag for both halves of the session, which is what makes it safe at
all: the host and every module this daemon sends are compiled by the
(* One decision for both halves of the session, which is what makes it safe
at all: the host and every module this daemon sends are compiled by the
same backend, because there is one place that says which. The two
conventions agree on every scalar and disagree on every aggregate, so a
crossed pair is correct until the first redefined function takes or
returns a struct and [flan.abi.x86] refuses that pair at [dlopen] if
this is ever got wrong. Off by default: LLVM stays the default path
here exactly as it is for [flan build]. *)
let x86 = List.mem x86_flag rest in
this is ever got wrong.
x86 by default, and this is the command where that is true. A dev
session is the thing the backend was written for: it turns the round
trip from 62ms to 28ms, and nothing it builds outlives the session, so
the subset it covers is a subset of one editor's afternoon rather than
of a shipped binary. [--llvm] is how to leave it for a program it
refuses by name, and for [--debug].
[--debug] takes [--llvm]'s side on its own, because it is not a
preference: [X86.redefinition] emits no line table, so a [--debug]
session on this backend would set breakpoints that stop firing at the
first C-c C-c. Asking for the debugger is asking for the backend that
has one. Writing [--x86] as well still reaches [Dev.start]'s refusal,
which is the point of leaving it readable here the combination stays
refused by name, it is just no longer somewhere you arrive by typing one
flag. *)
let x86 = backend_x86 ~default:(not debug) rest in
let asked_x86 = List.mem x86_flag rest in
let merged = not (List.mem two_process_flag rest) in
let rest = List.filter (fun a -> not (is_flag a)) rest in
let sock =
@ -646,11 +717,23 @@ let () =
| [] -> Filename.concat (Filename.dirname path) ".flan-dev.sock"
| _ ->
prerr_endline
"usage: flan dev <program.flan> [-s socket] [--debug] [--x86] \
"usage: flan dev <program.flan> [-s socket] [--debug] [--llvm] \
[--two-process]";
exit 2
in
with_errors path (fun () ->
(* Only this command hands one over, and only when it chose the backend
itself. A refusal from a session nobody asked to be on this backend is
one flag away from building, and the flag is the half of the sentence
[X86.unsupported] cannot know to write. *)
let x86_hint =
if x86 && not asked_x86 then
Some
"flan dev: the x86 backend is the default for a dev session because \
it is about twice as fast, and it does not compile every program. \
Start this one with flan dev --llvm."
else None
in
with_errors ?x86_hint path (fun () ->
Flan.Dev.start ~debug ~merged ~x86 ~file:path ~sock ())
(* One redefinition, built the way an editor will ask for it: a session over
@ -665,7 +748,7 @@ let () =
compiled is how the crossed pair was reachable from the CLI at all.
Building an --x86 host with [flan build --x86 --dev] and then reloading
into it now has a spelling that produces a module it can load. *)
let x86 = List.mem x86_flag rest in
let x86 = backend_x86 ~default:false rest in
let rest = List.filter (fun a -> not (is_flag a)) rest in
let out =
match rest with
@ -725,7 +808,8 @@ let () =
[flan build], and an --x86 --dev route through here would have made that
sentence false. *)
let run_flags =
[ no_checks_flag; debug_flag; sanitize_flag; x86_flag ] @ opt_levels
[ no_checks_flag; debug_flag; sanitize_flag; x86_flag; llvm_flag ]
@ opt_levels
in
let build_args, prog_args =
let rec split acc = function
@ -750,7 +834,7 @@ let () =
let checks = not (List.mem no_checks_flag build_args) in
let debug = List.mem debug_flag build_args in
let sanitize = List.mem sanitize_flag build_args in
let x86 = List.mem x86_flag build_args in
let x86 = backend_x86 ~default:false build_args in
let opt = opt_of build_args in
check_opt_against_debug ~debug ~opt;
with_errors path (fun () ->

View File

@ -90,6 +90,61 @@ 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
surprising.
### Which backend the session uses, and what it costs
`flan dev` compiles the session with the hand-written x86-64 backend. That is
the default, and it is the reason `C-c C-c` is fast: about 28ms at the socket
against about 63ms through LLVM, because `as` does in 8ms what `llc` does in
45ms. On a project you are iterating in, that is the difference you feel.
It is not the whole compiler. Three things a session built by it cannot do:
- **No backtrace, no locals, no globals.** The inspector — `C-c C-b`, and
everything the break loop shows you about *where* a stopped program is — walks
a shadow stack of frames, and this backend does not push them. A program built
by it still stops on an unhandled error, still takes a restart, still answers
`C-x C-e` at the break. It just cannot tell you where it stopped.
- **No `C-u C-c C-a`.** That view shows the LLVM IR a body was built from, and
there is none; the listing this backend produced is assembly. Plain `C-c C-a`
disassembles the object and works exactly as before.
- **Some programs it refuses by name.** It covers a subset of the IR and says
so rather than guessing — a `signal` or a `restart-case` inside a global's
initialiser, a `declare` that returns a struct by value, a value crossing the
C boundary in a shape it has no classifier for. A refusal names the form
*and* names `--llvm`, in both places you can meet one: the daemon's own build
log when it starts, and the error overlay when `C-c C-c` hits one
mid-session. It is never a bare "unsupported".
**`--debug` is not on this list**, because it picks LLVM for you. `flan dev
--debug` — the breakpoint and `flan-dape` path — is an LLVM session, since a
redefinition module compiled by the x86 backend carries no line table and a
breakpoint set in one would stop firing at the first `C-c C-c`. Writing
`--x86 --debug` together is still refused, and now says which one to drop.
### Asking for LLVM
Set `flan-daemon-args`. It is a list of strings, spliced into the daemon's
command line after the file, and it is how anything that is not the file or the
socket reaches `flan dev` from Emacs:
```elisp
(setq flan-daemon-args '("--llvm")) ; the inspector, the IR view, every form
(setq flan-daemon-args '("--debug")) ; breakpoints, through flan-dape
```
The first line of `*flan-dev*` is the command that actually ran, so you can
always check what a session was started with.
There is no prefix argument for this and that is deliberate: `C-u M-x flan`
already means "ask me which file", and a second level that meant "ask me which
flags" would be two unrelated questions on one key. A setting is also the
honest shape of the thing — which backend your sessions use is a property of
the project you are working on, not of the keystroke that started this one.
**If you were relying on the old default:** every `flan dev` before this one
was an LLVM session, so a workflow built around `C-c C-b` or `C-u C-c C-a` will
find them refused now. One line in your init puts it back.
---
## The loop
@ -809,6 +864,7 @@ Commands with no key: `M-x flan` (start a program), `M-x flan-quit`
| Variable | Default | What it is |
|---|---|---|
| `flan-command` | `"flan"` | the compiler binary |
| `flan-daemon-args` | `nil` | extra arguments for `flan dev``("--llvm")`, `("--debug")` |
| `flan-socket-name` | `".flan-dev.sock"` | what `C-c C-z` searches for |
| `flan-echo-result` | `t` | report an accepted evaluation in the echo area |
| `flan-inline-result` | `t` | show an expression's value beside the form, not in the echo area |

View File

@ -688,6 +688,15 @@ With no argument, look for `flan-socket-name' up from this buffer."
A name is looked up on `exec-path'; a path is used as given."
:type 'string)
(defcustom flan-daemon-args nil
"Extra arguments for `flan dev', after the file and before `-s'.
A list of strings, each one argument: (\"--llvm\") to compile the session
with LLVM instead of the x86 dev backend, (\"--debug\") for a session
`flan-dape' can set breakpoints in.
These belong to the daemon, not to the program it runs."
:type '(repeat string))
;; `flan-daemon-buffer' belongs to this section and is declared with the
;; other buffer names at the top of the file instead, because the reply reader
;; -- which runs long before any of this -- names it in the message it gives
@ -719,27 +728,37 @@ It builds the program first, which for a cold project is most of this."
(defun flan--start-daemon (file socket)
"Start `flan dev' on FILE listening on SOCKET, and return the process."
(let ((buf (get-buffer-create flan-daemon-buffer))
;; Expanded before `default-directory' moves, so that a command given
;; as a path is the path the user meant and not one relative to the
;; program's directory. A bare name is left alone for `exec-path'.
(cmd (if (file-name-directory flan-command)
(expand-file-name flan-command)
flan-command))
;; The daemon runs where the program is, and so does the program it
;; launches — it inherits this. A game opening "assets/tiles.png"
;; means the project's directory, not whichever buffer Emacs happened
;; to be in when the command was typed. (Imports do not depend on
;; this: lib/load.ml resolves those from the importing file.)
(default-directory (file-name-directory (expand-file-name file))))
(let* ((buf (get-buffer-create flan-daemon-buffer))
;; Expanded before `default-directory' moves, so that a command given
;; as a path is the path the user meant and not one relative to the
;; program's directory. A bare name is left alone for `exec-path'.
(cmd (if (file-name-directory flan-command)
(expand-file-name flan-command)
flan-command))
;; `flan-daemon-args' goes after the file and before `-s', which is
;; where the usage line puts the flags and where `flan dev' reads
;; them from: everything but the path and the socket pair is a flag,
;; wherever it sits. Built once and used twice, because the buffer's
;; first line is what somebody reads to find out what ran and it used
;; to reassemble the command rather than show it — two spellings that
;; could disagree, and with anything else on the line they would have.
(args (append (list cmd "dev" file)
flan-daemon-args
(list "-s" socket)))
;; The daemon runs where the program is, and so does the program it
;; launches — it inherits this. A game opening "assets/tiles.png"
;; means the project's directory, not whichever buffer Emacs happened
;; to be in when the command was typed. (Imports do not depend on
;; this: lib/load.ml resolves those from the importing file.)
(default-directory (file-name-directory (expand-file-name file))))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "%s dev %s -s %s\n\n" cmd file socket)))
(insert (mapconcat #'identity args " ") "\n\n"))
(setq default-directory (file-name-directory (expand-file-name file))))
(make-process
:name "flan-daemon" :buffer buf
:command (list cmd "dev" file "-s" socket)
:command args
;; The daemon writes its ready line and the program's stderr to stderr,
;; and both belong in the same buffer in the order they happened.
:connection-type 'pipe :noquery t

View File

@ -1126,6 +1126,38 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; `flan dev' with no -s puts one beside the program, which for a test
;; program in /tmp is a path shared with every other thing running there.
(setq flan-command flan)
;; `flan-daemon-args' reaches the command line. Checked against the
;; argument list rather than by starting a daemon with a flag on it: the
;; claim is that the setting is spliced in at all, which a build of the
;; whole program would take seconds to say and say no more clearly. The
;; daemon buffer's first line is checked too, because it is a second
;; spelling of the same command and somebody reads it to find out what ran.
(let ((seen nil)
(probe nil))
(setq probe (lambda (&rest args) (setq seen (plist-get args :command)) nil))
(advice-add 'make-process :override probe)
(unwind-protect
(let ((flan-daemon-args '("--llvm" "--debug")))
(flan--start-daemon program "/tmp/does-not-matter.sock")
(test-flan--check "flan-daemon-args reaches the daemon's command line"
(equal seen (list flan "dev" program
"--llvm" "--debug"
"-s" "/tmp/does-not-matter.sock")))
(test-flan--check "and the daemon buffer shows the command that ran"
(with-current-buffer flan-daemon-buffer
(string-match-p
"--llvm --debug"
(buffer-substring-no-properties
(point-min) (point-max)))))
(setq seen nil)
(let ((flan-daemon-args nil))
(flan--start-daemon program "/tmp/does-not-matter.sock"))
(test-flan--check "and an empty setting leaves the old command line"
(equal seen (list flan "dev" program
"-s" "/tmp/does-not-matter.sock"))))
(advice-remove 'make-process probe)))
(let ((socket2 (concat socket "-started-from-emacs")))
(ignore-errors (delete-file socket2))
(test-flan--check "nothing to quit before anything was started"
@ -1377,11 +1409,21 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; Its own daemon, because the first one was disconnected above and a
;; disassembly is a question only a live session can answer: the daemon is
;; the thing that built the module and still has the .ll and the .so.
(let ((socket3 (concat socket "-disasm")))
;;
;; And an LLVM one, asked for through the setting rather than around it:
;; `C-u C-c C-a' shows the IR a body was built from and an x86 session has
;; none, so half of what this section checks only exists on that backend.
;; Going through `flan-daemon-args' means the same run proves the setting
;; reaches a daemon that actually starts, which the stubbed check above
;; deliberately does not.
(let ((socket3 (concat socket "-disasm"))
(flan-daemon-args '("--llvm")))
(ignore-errors (delete-file socket3))
(flan program socket3)
(test-flan--check "a daemon to disassemble against"
(process-live-p flan--connection))
(test-flan--check "and flan-daemon-args put --llvm on its command line"
(member "--llvm" (process-command flan--daemon)))
(when (executable-find "objdump")
(flan-disassemble "step")

View File

@ -217,7 +217,38 @@ let result t =
thread on the frame that erred and waits. The agent is where that shows, and
the session is the only thing holding it so an editor asks here or not at
all. *)
let ask t verb = request t verb
(* One sentence the agent cannot write, rewritten in the one place every verb
passes through.
[vendor/agent/flan_agent.c] answers a backtrace, a locals or a globals
request from a program with no shadow-stack frames with "this program was
not built with --dev". That was true of exactly one thing when it was
written -- a release build, which has no frames because it has no dev
machinery at all -- and it stopped being true when [lib/x86.ml] became the
default for [flan dev]. An x86 dev host has its cells, its globals and its
registry; what it has not got is the frame push, so it stops on an error and
then cannot say where. The agent has no way to tell the two apart: it sees
an empty chain either way, and it is inside the program, which knows nothing
about the backend that compiled it.
The session does know, so it is the one that corrects the sentence. Rewriting
the reply rather than teaching the agent a new environment variable keeps the
claim where the fact is -- and a message that names [--llvm] is the whole of
what the reader needs, where "not built with --dev" sends them to look for a
flag they did not leave off. *)
let mentions hay needle =
let n = String.length hay and m = String.length needle in
let rec go i = i + m <= n && (String.sub hay i m = needle || go (i + 1)) in
m = 0 || go 0
let ask t verb =
let text = request t verb in
if t.session.Session.x86 && mentions text "was not built with --dev" then
"err the x86 dev backend pushes no shadow-stack frames, so a stopped \
program built by it cannot say where it is -- no backtrace, no locals and \
no globals. It is the default for flan dev because it is about twice as \
fast; restart the daemon with flan dev --llvm to inspect frames.\n"
else text
type state =
| Running
@ -2675,7 +2706,21 @@ let disassemble t ~name ~form =
":loc " ^ Wire.quote o.oloc;
":basis " ^ Wire.quote why ]
in
if form = "ir" then
(* There is no LLVM IR in an x86 session, and until now the way that was
reported was a grep that found nothing in a file whose name ends [.s]:
"no define for @flan.step in .../host.s", which reads like a compiler
that lost track of its own output. The view that does exist is the
other one -- [:form "asm"] disassembles the object, and an x86 session
has objects like any other -- so this says which of the two questions
it cannot answer rather than failing at the text of an answer it was
never going to find. *)
if form = "ir" && t.session.Session.x86 then
error
"this session was compiled by the x86 dev backend, so there is no \
LLVM IR to show the listing it produced is assembly. C-c C-a \
disassembles the object, which works here; for the IR view, restart \
the daemon with flan dev --llvm"
else if form = "ir" then
match read_file o.oll with
| text ->
(match ir_of ~ir:text name with
@ -3233,7 +3278,7 @@ let accept_loop ?grace t ls =
deletes it and silently making every reloaded body -O0 would change the
frame time of the one function you are iterating on, in the loop whose whole
point is watching that number. *)
let two_process ?(debug = false) ?(x86 = false) ~file ~sock () =
let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
let t0 = Unix.gettimeofday () in
(* Absolute, because every location this daemon ever reports is derived from
it and an editor is not in this process's working directory. [flan dev
@ -4114,7 +4159,7 @@ let merged_serve () =
(* The merged build is made here and then [exec]'d, so what an editor talks to
is the program itself rather than something that launched it. The launcher
does not survive: there is one process from the first reply onwards. *)
let start_merged ?(debug = false) ?(x86 = false) ~file ~sock () =
let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () =
let t0 = Unix.gettimeofday () in
let file = try Unix.realpath file with Unix.Unix_error _ -> file in
let session, l = Session.create ~debug ~x86 ~file () in
@ -4166,19 +4211,32 @@ let start_merged ?(debug = false) ?(x86 = false) ~file ~sock () =
flan.cmxa beside the binary and it is what every behaviour in this file
was written against, so it stays until the transport it exists to drive is
actually deleted. *)
let start ?(debug = false) ?(merged = true) ?(x86 = false) ~file ~sock () =
let start ?(debug = false) ?(merged = true) ?(x86 = true) ~file ~sock () =
(* x86 unless told otherwise, and the default is here rather than only in
[bin/main.ml] so that there is one answer to "what backend is a dev
session". A library caller that starts a daemon starts the same daemon the
editor does; the CLI passes this explicitly and never leans on it, but a
default that disagreed with the command would be a second answer nobody
had written down. *)
(* [--x86] and [--debug] are refused together here, and only here: [flan
build --x86 --debug] is deliberately allowed, because [X86.program] emits
a hand-written DWARF 4 unit. [X86.redefinition] does not, so a [--debug]
session would build a host with a line table and then send it modules with
none -- a breakpoint on a line in the buffer would fire before the first
C-c C-c and stop firing after it, which is worse than not offering the
combination. Accepting the flag and ignoring it would be worse still. *)
combination. Accepting the flag and ignoring it would be worse still.
Now that x86 is the default, this is reachable only by asking for it:
[bin/main.ml] lets [--debug] alone select LLVM, precisely so that the one
flag a person types to get a debugger does not land on the backend that
has no line table for a redefinition. Writing both is still writing both,
and it is still refused. *)
if x86 && debug then
failwith
"flan dev --x86 --debug: the dev backend emits DWARF for a whole program \
but not yet for a redefinition module, so a breakpoint set on a line \
would stop firing at the first C-c C-c. Use one or the other.";
would stop firing at the first C-c C-c. Drop --x86 and --debug will \
build this session with LLVM, which has both.";
(* The merged daemon used to be refused here for [--x86] and no longer is,
and what made the combination safe is worth stating where the refusal
stood. A merged build is the program and the compiler in one process, and

View File

@ -406,7 +406,16 @@ let redefinition (t : t) ?retains ?call ?(consts = []) program ~fns =
| Some f -> f.Tast.floc
| None -> Loc.unknown
in
fail loc "the x86 dev backend cannot compile this: %s" m
(* The flag is named because the backend is no longer something anybody
asked for: [flan dev] takes it by default, so the person reading this
chose a program and not a code generator. "unsupported" on its own
tells them their form is wrong, which it is not it compiles, on the
other backend, and the whole of the fix is one flag on the daemon.
Naming it here rather than in the editor keeps the sentence with the
refusal it belongs to, and reaches [flan reload] too. *)
fail loc
"the x86 dev backend cannot compile this: %s. Restart the daemon with \
flan dev --llvm, which compiles every form this one refuses" m
(* ── Undoing an acceptance ─────────────────────────────────────────── *)

View File

@ -229,9 +229,18 @@ let () =
how an editor meets it, and because it launches and owns a program of
its own. Its child's stdout is what we read the result off. *)
let flan = "../bin/main.exe" in
(* [--llvm] on this daemon and on the four below it, and it is about what
they test rather than about a preference. Everything they ask for -- a
backtrace, the locals of a stopped frame, the globals a stopped stack
reaches -- is read off the dev shadow stack, and [lib/x86.ml] pushes no
frames onto it. x86 is what [flan dev] takes unasked now, so a flagless
daemon here would answer every one of those with the sentence [Dev.ask]
rewrites: a true statement about the backend and no test of the verb.
The default itself is checked further down, on a daemon that does not
need frames. *)
let pid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-loop.flan"; "-s"; sock |]
[| flan; "dev"; "programs/dev-loop.flan"; "-s"; sock; "--llvm" |]
Unix.stdin fd Unix.stderr
in
Unix.close fd;
@ -821,7 +830,7 @@ let () =
let bfd = Unix.openfile bout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let bpid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-break.flan"; "-s"; bsock |]
[| flan; "dev"; "programs/dev-break.flan"; "-s"; bsock; "--llvm" |]
Unix.stdin bfd Unix.stderr
in
Unix.close bfd;
@ -1490,7 +1499,7 @@ let () =
let lfd = Unix.openfile lout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let lpid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-locals.flan"; "-s"; lsock |]
[| flan; "dev"; "programs/dev-locals.flan"; "-s"; lsock; "--llvm" |]
Unix.stdin lfd Unix.stderr
in
Unix.close lfd;
@ -1671,7 +1680,7 @@ let () =
let ifd = Unix.openfile iout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let ipid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-inspect.flan"; "-s"; isock |]
[| flan; "dev"; "programs/dev-inspect.flan"; "-s"; isock; "--llvm" |]
Unix.stdin ifd Unix.stderr
in
Unix.close ifd;
@ -1881,7 +1890,7 @@ let () =
let pfd = Unix.openfile pout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let ppid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-ptr.flan"; "-s"; psock |]
[| flan; "dev"; "programs/dev-ptr.flan"; "-s"; psock; "--llvm" |]
Unix.stdin pfd Unix.stderr
in
Unix.close pfd;
@ -2184,7 +2193,7 @@ let () =
let gfd = Unix.openfile gout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let gpid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-globals.flan"; "-s"; gsock |]
[| flan; "dev"; "programs/dev-globals.flan"; "-s"; gsock; "--llvm" |]
Unix.stdin gfd Unix.stderr
in
Unix.close gfd;
@ -2407,7 +2416,7 @@ let () =
let dfd = Unix.openfile dout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
let dpid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; dsock |]
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; dsock; "--llvm" |]
Unix.stdin dfd Unix.stderr
in
Unix.close dfd;
@ -2626,7 +2635,12 @@ let () =
A daemon whose [llc] is [false] reproduces it exactly and cheaply: the
host is built by clang and runs, every redefinition checks and then
fails to build, and nothing is ever delivered. *)
fails to build, and nothing is ever delivered.
[--llvm] with it, and the two go together: [llc] is only in the loop at
all on that backend, so a session that defaulted to x86 would assemble
its modules with [as] and deliver them, and the window this reproduces
would never open. *)
let ssock = tmp "stale.sock" and sout = tmp "stale.out" in
(try Sys.remove ssock with Sys_error _ -> ());
let sfd = Unix.openfile sout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
@ -2635,7 +2649,7 @@ let () =
in
let spid =
Unix.create_process_env flan
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; ssock |]
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; ssock; "--llvm" |]
env Unix.stdin sfd Unix.stderr
in
Unix.close sfd;
@ -2721,6 +2735,27 @@ let () =
(try Unix.kill cpid Sys.sigkill with Unix.Unix_error _ -> ())
end
else begin
(* And the default backend, on the first flagless daemon in this file
that does not need the shadow stack. No flag was given above, so what
it built through is what [flan dev] builds through when nobody says:
[start_merged] writes the host's listing beside the binary and names
it [host.s] or [host.ll] by the backend that produced it, so the
extension is the whole of the claim. Here rather than in a daemon of
its own because this one is already standing and the answer costs a
[stat] -- the opt-in half is checked the same way on the [--llvm]
daemon further down. *)
let chost ext =
Filename.concat
(Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-dev-%d" cpid))
("host." ^ ext)
in
if not (Sys.file_exists (chost "s")) then
fail "flan dev with no backend flag did not build through x86: %s is \
not there"
(chost "s");
if Sys.file_exists (chost "ll") then
fail "flan dev with no backend flag left LLVM IR at %s" (chost "ll");
let c = connect csock in
(* Long enough for the program to have run the sixteen frames that fill
the pipe before anything is asked of the daemon. It prints at 4K a
@ -3491,15 +3526,33 @@ let () =
let nfd =
Unix.openfile nlog [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
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 =
Unix.create_process flan
[| flan; "dev"; "programs/dev-noagent.flan"; "-s"; nsock |]
[| flan; "dev"; "programs/dev-noagent.flan"; "-s"; nsock; "--llvm" |]
Unix.stdin nfd nfd
in
Unix.close nfd;
if not (listening ~pid:npid nsock) then
fail "the agentless daemon %s" !listen_why
else begin
let nhost ext =
Filename.concat
(Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-dev-%d" npid))
("host." ^ ext)
in
if not (Sys.file_exists (nhost "ll")) then
fail "flan dev --llvm did not build through LLVM: %s is not there"
(nhost "ll");
if Sys.file_exists (nhost "s") then
fail "flan dev --llvm left an x86 listing at %s" (nhost "s");
let nc = connect nsock in
(* Blocks for the whole of [merged_serve]'s wait, by construction. The
exception arm is not defensive: a session that adopted the daemon's

View File

@ -82,9 +82,17 @@ let () =
let p = "programs/dev-repl.flan" in
try Unix.realpath p with Unix.Unix_error _ -> p
in
(* [--llvm], and it is not incidental: this suite drives the inspector --
backtrace, locals, globals -- and those are frames on the dev shadow
stack, which [lib/x86.ml] does not push. The default backend for [flan
dev] is x86 now, so a flagless daemon here would answer every one of
those with the refusal naming this flag, and the client half of the
break protocol would go untested. The client's own daemon at the end of
the run asks for it through [flan-daemon-args] for the same reason,
which is also where that setting is proved to reach a command line. *)
let pid =
Unix.create_process flan
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; sock |]
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; sock; "--llvm" |]
Unix.stdin fd efd
in
Unix.close fd;