diff --git a/NEXT.md b/NEXT.md index 2c23b6e..2066b4b 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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: diff --git a/README.md b/README.md index d52184c..942b235 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Eleven of them, and the four anyone starts with: flan check type-check a program flan run [flags] [-- args...] build and run it flan build [-o out] [flags] build a native executable -flan dev [-s socket] start a live development session +flan dev [-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: diff --git a/bin/main.ml b/bin/main.ml index 47266d4..032fe0e 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -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 [-s socket] [--debug] [--x86] \ + "usage: flan dev [-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 () -> diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 150670d..1beb307 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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 @@ -778,6 +833,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` | print `C-x C-e`'s value in the echo area | | `flan-names-shown` | `4` | how many names to list before summarising | diff --git a/emacs/flan.el b/emacs/flan.el index aea5491..9f42d23 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -675,6 +675,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 @@ -706,27 +715,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 diff --git a/emacs/test-flan.el b/emacs/test-flan.el index d650ccc..3945d0b 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -874,6 +874,38 @@ is written instead — the real `message' call the real command makes." ;; `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" @@ -1125,11 +1157,21 @@ is written instead — the real `message' call the real command makes." ;; 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") diff --git a/lib/dev.ml b/lib/dev.ml index c0b4913..34dfcda 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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 @@ -2649,7 +2680,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 @@ -3207,7 +3252,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 @@ -4088,7 +4133,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 @@ -4140,19 +4185,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 diff --git a/lib/session.ml b/lib/session.ml index f1c12e8..abff502 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -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 ─────────────────────────────────────────── *) diff --git a/test/test_dev.ml b/test/test_dev.ml index 77dfaf2..7b1b6b6 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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; @@ -781,7 +790,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; @@ -1450,7 +1459,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; @@ -1631,7 +1640,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; @@ -1841,7 +1850,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; @@ -2144,7 +2153,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; @@ -2367,7 +2376,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; @@ -2586,7 +2595,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 @@ -2595,7 +2609,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; @@ -2681,6 +2695,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 @@ -3451,15 +3486,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 diff --git a/test/test_emacs.ml b/test/test_emacs.ml index 0ed08a9..743866a 100644 --- a/test/test_emacs.ml +++ b/test/test_emacs.ml @@ -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;