A macro call says what it expands to, and a Form learns to print itself
C-c C-m. One step on the bare key, the fixpoint under C-u: a macro may quasiquote a call to another macro, and Loc.from_macro is outermost-wins, so by the time a full expansion settles the intermediate name is gone. One step is the only thing that can say which macro produced what. The expansion runs against the macros the *session* holds -- the prelude's, its imports', and every defmacro evaluated since it started -- and writes nothing back: a defmacro handed to C-c C-m does not join the session by having been looked at. Both non-termination refusals stay refusals, and only where they are needed. One step makes one call and does not look at the answer, so (s/spin) one- stepped answers with itself; all the way hits the fuel and names the macro, inside Dev.serve's guard, so the daemon replies rather than hanging. Macro's module handling is a Fun.protect now -- a build that raised was a process about to exit, and the daemon is not that process. No printer for a Form existed. Form.to_string is an error-message renderer and is what Macro.key digests, so it is untouched; Form.to_source round-trips floats, strings and bytes through the reader, and Form.pretty decides where the line breaks go and leaves the columns to flan-mode. The answer is a read-only flan-mode buffer shaped like the disassembly one, with cnr's idea in it: m expands the form at point one more step in place. Three inherited keys refuse by name -- an expansion is in no file. The text is sent padded onto its own line and its own column, unlike C-x C-e, so the refusal lands on the call and not at the start of its line.
This commit is contained in:
parent
9fc04193d9
commit
3dd9f61b7d
102
BUILT.md
102
BUILT.md
@ -5505,3 +5505,105 @@ Both editor paths work and they are different wraps — `Parse.expr`'s for `C-x
|
|||||||
plus editing that macro and calling it again; the assertions are on the IR and not on the absence of an
|
plus editing that macro and calling it again; the assertions are on the IR and not on the absence of an
|
||||||
exception, because an expression that did not expand raises while one that expanded to the *wrong* thing does
|
exception, because an expression that did not expand raises while one that expanded to the *wrong* thing does
|
||||||
not.
|
not.
|
||||||
|
|
||||||
|
## `C-c C-m`: what a macro call expands to, and a printer for `Form`
|
||||||
|
|
||||||
|
Macros grew far enough today that the editor had to be able to ask. The argument is one line: **a macro is
|
||||||
|
importable from a package, arriving qualified**, so `(rl/with-drawing …)` is a call whose `defmacro` lives in
|
||||||
|
another directory. There is nothing beside the call site to read — and there would be nothing to read even if the
|
||||||
|
`defmacro` were in the buffer, because a macro answers a `Form` and nobody ever wrote that down.
|
||||||
|
|
||||||
|
### One step on the key, all the way under `C-u`
|
||||||
|
|
||||||
|
Both, because the difference is real here and the second is nearly free once the first exists. A macro may
|
||||||
|
quasiquote a call to another macro — `mac/quad` answers `(mac/twice (mac/twice n))` — so one step and the fixpoint
|
||||||
|
are different text.
|
||||||
|
|
||||||
|
One step is the *bare* key, and the reason is `Loc.from_macro`. It is outermost-wins by design: the macro the
|
||||||
|
author actually wrote is the one worth naming. So by the time a full expansion settles, every node of it is stamped
|
||||||
|
`mac/quad` and `mac/twice` is unnameable. All the way is the answer the compiler acts on; one step is the only way
|
||||||
|
to find out which macro produced what. `C-u` for the other half rather than a second key is the rule `C-c C-a`
|
||||||
|
already follows: it is the same question asked of the same form.
|
||||||
|
|
||||||
|
One step is **outermost-only**, and that is a deliberate divergence from `Macro.expand_form`, which expands a
|
||||||
|
call's arguments before calling it. `(mac/twice (mac/twice 1))` one-stepped here is `(+ (mac/twice 1) (mac/twice
|
||||||
|
1))`; the compiler's own first move is `(mac/twice (+ 1 1))`. Different intermediates, the same fixpoint. Written
|
||||||
|
down because someone will otherwise discover it and file it.
|
||||||
|
|
||||||
|
### The session's macros, not a fresh read
|
||||||
|
|
||||||
|
`Session.macroexpand` reads `t.macros` and writes nothing at all. Re-reading the file would answer with macros the
|
||||||
|
session was never told about, and with whatever is *saved* rather than what is typed — the unsaved-versus-saved
|
||||||
|
skew "A session expands the buffer's own macros" already refused, and it is worse here than anywhere, because an
|
||||||
|
expansion that disagrees with what an evaluation does is a lie about what the code *is*.
|
||||||
|
|
||||||
|
Writing nothing is a property and it is pinned: a `defmacro` handed to `C-c C-m` must not join the session by
|
||||||
|
having been looked at, so `test_session` and `test_repl` both expand one and then require the name to still be
|
||||||
|
unknown. `C-c C-c` is where a declaration goes.
|
||||||
|
|
||||||
|
### The refusals, and the one that must not be reachable
|
||||||
|
|
||||||
|
The verb sits inside the guard the robustness lane put round the whole of `handle` in `Dev.serve` — confirmed
|
||||||
|
rather than assumed, and it is why `Dev.macroexpand` has no `Loc.Error` arm of its own. Both non-termination
|
||||||
|
refusals raise `Loc.Error` and come back as replies with the call site on them.
|
||||||
|
|
||||||
|
**The bound has to be reached, and only where it is needed.** A hang in the daemon wedges the editor with the
|
||||||
|
program still on screen. So: all the way runs `Macro.expand_form`, hits `Macro.fuel` and names the macro; one step
|
||||||
|
makes exactly one call and does not look at what comes back, so `(s/spin)` one-stepped *answers*, with `(s/spin)`
|
||||||
|
and the name `s/spin`. A command that refused to show anybody the thing they were trying to see would be the wrong
|
||||||
|
kind of safe. The ring is refused a level up, while the package holding it is parsed, so no session over one can
|
||||||
|
exist and nothing on this path can reach it.
|
||||||
|
|
||||||
|
`Macro`'s module handling is now a `Fun.protect`. It used to be two statements after the call, which was enough
|
||||||
|
while a build was the only caller — a build that raised was a process about to exit. The daemon is not that
|
||||||
|
process: a macro that does not settle raises, the editor is told, and the next `C-c C-m` does it again, so
|
||||||
|
`Dynload.owned` must not be a list that only grows over a session's lifetime.
|
||||||
|
|
||||||
|
### `Form.to_source` and `Form.pretty`, because no printer existed
|
||||||
|
|
||||||
|
`Form.to_string` is an error-message renderer — one line, no width, `%S` and `%g`. It is also what `Macro.key`
|
||||||
|
digests, so every cached macro module on disk is keyed by what it prints; it is untouched. What an expansion needs
|
||||||
|
is text a *reader reads back*, and three of its spellings are not round trips, all reachable because a macro may
|
||||||
|
build any literal: `%g` prints 1.0 as `1` and truncates at six digits, `%S` is OCaml's escaping where the reader
|
||||||
|
takes exactly six escapes, and `Byte` falls through to `\<char>` where the reader names `\nul` and `\return`.
|
||||||
|
|
||||||
|
`Form.pretty` decides **where the line breaks go and not where the columns do**. Structure is a printer's job;
|
||||||
|
indentation is `flan-mode`'s, which is where this project's rules already live — the buffer runs `indent-region`
|
||||||
|
over what it is shown, so nothing in OCaml has to know that a `let` aligns its bindings under the bracket. A client
|
||||||
|
with no Emacs still gets something readable rather than one very long line.
|
||||||
|
|
||||||
|
### Where the answer goes
|
||||||
|
|
||||||
|
`*flan-macroexpansion*`, a read-only `flan-mode` buffer — what is in it is Flan source and reading it is the whole
|
||||||
|
point, so the font-lock and the indentation are the ones already there. It follows `flan-disassemble`'s shape,
|
||||||
|
header comment lines and all, rather than `flan-cnr`'s: cnr's folding is frame-and-locals machinery with its own
|
||||||
|
text properties and there is nothing here to fold. What *was* taken from cnr is the idea its header names — a key
|
||||||
|
that expands in place. `m` (or `C-c C-m`) takes the form at point one more step and puts the result where it was,
|
||||||
|
which is what makes one-step-by-default usable rather than a thing you press once and lose. `a` goes to the
|
||||||
|
fixpoint, `g` asks again — useful after re-evaluating the `defmacro` — and `q` closes.
|
||||||
|
|
||||||
|
Three keys inherited from `flan-mode-map` are shadowed by name, not unbound, so pressing one says why: `C-c C-c`,
|
||||||
|
`C-c C-k` and `C-x C-e` over an expansion would install a body nobody wrote under a name somebody did.
|
||||||
|
|
||||||
|
**What `Loc.from_macro` means for what is printed** is a header line, because there is nowhere else it could be
|
||||||
|
said. Every node below it carries the *call site's* file, line and column with the macro's name stamped on it —
|
||||||
|
which is how an error inside expanded code points at the call you wrote — and therefore none of it has a location
|
||||||
|
of its own, nothing in it is the text of any file, and there is nothing for `M-.` to jump to.
|
||||||
|
|
||||||
|
### What gets sent, and the padding that is not `C-x C-e`'s
|
||||||
|
|
||||||
|
The region is `C-x C-e`'s, plus one case it has no need of: the form *at* point when point is on its opening
|
||||||
|
delimiter. A macro call is a form you put point on, and `backward-sexp` from an open paren takes the previous
|
||||||
|
sibling, which is never what was meant.
|
||||||
|
|
||||||
|
The text is padded onto its own line **and its own column**, which `flan-dev--text` does not do and says why it
|
||||||
|
does not: a top-level form starts at column 1, so the columns already agreed. A macro call does not — it is
|
||||||
|
written well inside a `defn` — and the refusal this path can get carries a column measured from the start of the
|
||||||
|
snippet, so an unpadded send would draw "did not settle" at the start of the line. Leading newlines and leading
|
||||||
|
spaces are both whitespace the reader skips. `test-flan-dev.el` checks the arithmetic the only way that proves it:
|
||||||
|
it evaluates a spinning `defmacro` into a live session, asks for the fixpoint of a call to it from a known buffer
|
||||||
|
position, and requires the error overlay to start at exactly that position.
|
||||||
|
|
||||||
|
The in-buffer `m` sends its text *un*padded, which is the honest shape there: the expansion is in no file, so
|
||||||
|
there is no line or column for a refusal to be drawn at. The `:file` still goes on the wire, because that is what
|
||||||
|
says which session's macros to expand against.
|
||||||
|
|||||||
@ -108,6 +108,41 @@ hit it as many times as you like; an ordinary `C-c C-c` over the same form (or
|
|||||||
expression instead of printing its value. That one does not stick, because there
|
expression instead of printing its value. That one does not stick, because there
|
||||||
is no definition for it to stick to.
|
is no definition for it to stick to.
|
||||||
|
|
||||||
|
### `C-c C-m` — what a macro call expands to
|
||||||
|
|
||||||
|
Put point on a macro call and press it. `*flan-macroexpansion*` opens with the
|
||||||
|
code that call turns into, as Flan source, indented and coloured like any other.
|
||||||
|
|
||||||
|
**One step by default, `C-u C-c C-m` for all the way.** The two genuinely
|
||||||
|
differ: a macro may produce a call to another macro, so `(mac/quad 3)` is
|
||||||
|
`(mac/twice (mac/twice 3))` after one step and `(+ (+ 3 3) (+ 3 3))` at the end.
|
||||||
|
One step is the default because it is the one that can say *which* macro
|
||||||
|
produced what — the full expansion is tagged with the name you wrote and
|
||||||
|
nothing else.
|
||||||
|
|
||||||
|
Inside the buffer, `m` expands the form at point one more step **in place**,
|
||||||
|
`a` takes it to the end, `g` asks again — useful after you have re-evaluated
|
||||||
|
the `defmacro` — and `q` closes it.
|
||||||
|
|
||||||
|
This matters most for a macro you cannot read beside the call: a package's
|
||||||
|
macros arrive qualified, so `rl/with-drawing` is defined in another directory
|
||||||
|
and there is nothing next to the call site to look at.
|
||||||
|
|
||||||
|
It expands against the macros **this session holds** — the prelude's, the ones
|
||||||
|
its imports brought in, and every `defmacro` you have evaluated since it
|
||||||
|
started — not against a fresh read of the file. So it agrees with what an
|
||||||
|
evaluation does, and it sees the macro you have typed rather than the one you
|
||||||
|
last saved.
|
||||||
|
|
||||||
|
Two things the buffer says out loud. Nothing in it has a location of its own:
|
||||||
|
every node carries the *call site's* file, line and column with the macro's
|
||||||
|
name stamped on it, which is how an error inside expanded code points at the
|
||||||
|
call you wrote. And it is not the text of any file, so `C-c C-c`, `C-c C-k` and
|
||||||
|
`C-x C-e` in that buffer refuse rather than installing code nobody wrote.
|
||||||
|
|
||||||
|
A macro that never settles is refused at a bound and named, with the refusal
|
||||||
|
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, sent as **one** module rather than as a form at a time. That
|
||||||
@ -554,6 +589,7 @@ mode's, so they work in a file you have only opened.
|
|||||||
| `C-c C-d` | what the running program currently defines |
|
| `C-c C-d` | what the running program currently defines |
|
||||||
| `C-c C-v` | help on the name at point |
|
| `C-c C-v` | help on the name at point |
|
||||||
| `C-c C-a` | disassemble a function; `C-u` first for its LLVM IR |
|
| `C-c C-a` | disassemble a function; `C-u` first for its LLVM IR |
|
||||||
|
| `C-c C-m` | what the macro call at point expands to; `C-u` for all the way |
|
||||||
|
|
||||||
Completion, eldoc and `M-.` all read one cached answer rather than asking the
|
Completion, eldoc and `M-.` all read one cached answer rather than asking the
|
||||||
program per keystroke. It refreshes at the two moments the answer can have
|
program per keystroke. It refreshes at the two moments the answer can have
|
||||||
@ -599,6 +635,8 @@ Use `C-c C-g` if you need frames.
|
|||||||
| `C-c C-b` | a stopped program: condition, restarts, stack |
|
| `C-c C-b` | a stopped program: condition, restarts, stack |
|
||||||
| `C-c C-M-b` | the same restarts, as a one-key prompt |
|
| `C-c C-M-b` | the same restarts, as a one-key prompt |
|
||||||
| `C-c C-i` | inspect a value |
|
| `C-c C-i` | inspect a value |
|
||||||
|
| `C-c C-m` | what the macro call at point expands to |
|
||||||
|
| `C-u C-c C-m` | ...all the way, rather than one step |
|
||||||
| `C-c C-a` | disassemble; `C-u` first for LLVM IR |
|
| `C-c C-a` | disassemble; `C-u` first for LLVM IR |
|
||||||
| `C-c C-g` | debug under lldb, through dape |
|
| `C-c C-g` | debug under lldb, through dape |
|
||||||
| `C-c C-d` | what the running program defines |
|
| `C-c C-d` | what the running program defines |
|
||||||
@ -609,8 +647,9 @@ Use `C-c C-g` if you need frames.
|
|||||||
Commands with no key: `M-x flan-dev` (start a program), `M-x flan-dev-quit`
|
Commands with no key: `M-x flan-dev` (start a program), `M-x flan-dev-quit`
|
||||||
(stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`,
|
(stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`,
|
||||||
`M-x flan-watch-ghost-mode` (the same values inline),
|
`M-x flan-watch-ghost-mode` (the same values inline),
|
||||||
`M-x flan-inspect-address` (what is at an address), and `M-x flan-allocations`
|
`M-x flan-inspect-address` (what is at an address), `M-x flan-macroexpand-all`
|
||||||
/ `M-x flan-leaks` (where the memory went, and what is still held).
|
(the `C-u` half of `C-c C-m`, by name), and `M-x flan-allocations` /
|
||||||
|
`M-x flan-leaks` (where the memory went, and what is still held).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,11 @@
|
|||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
(require 'xref)
|
(require 'xref)
|
||||||
(require 'eldoc)
|
(require 'eldoc)
|
||||||
|
;; The macroexpansion buffer is a `flan-mode' buffer — what is in it is Flan
|
||||||
|
;; source and reading it is the point — so the mode has to exist by the time
|
||||||
|
;; this file defines one derived from it. Not a cycle: flan-mode.el autoloads
|
||||||
|
;; the commands here and requires nothing back.
|
||||||
|
(require 'flan-mode)
|
||||||
|
|
||||||
(defgroup flan-dev nil
|
(defgroup flan-dev nil
|
||||||
"Talking to a running Flan program."
|
"Talking to a running Flan program."
|
||||||
@ -1444,6 +1449,28 @@ columns already were, because a top-level form starts at column 1."
|
|||||||
(concat (make-string (1- (line-number-at-pos start)) ?\n)
|
(concat (make-string (1- (line-number-at-pos start)) ?\n)
|
||||||
(buffer-substring-no-properties start end)))
|
(buffer-substring-no-properties start end)))
|
||||||
|
|
||||||
|
(defun flan-dev--text-at (start end)
|
||||||
|
"The buffer text from START to END, on the line AND column it is written at.
|
||||||
|
|
||||||
|
`flan-dev--text' pads lines only, and says why it needs nothing more: a
|
||||||
|
top-level form starts at column 1, so the columns already agreed. A macro
|
||||||
|
call does not. It is written somewhere inside a `defn', and a refusal the
|
||||||
|
daemon reports against it — a macro that never settles is the one that
|
||||||
|
happens — carries a column that would otherwise be measured from the start of
|
||||||
|
the snippet and drawn at the start of the line.
|
||||||
|
|
||||||
|
Leading newlines and leading spaces are both whitespace the reader skips, so
|
||||||
|
padding with each is the whole fix. Byte columns, for the reason
|
||||||
|
`flan-dev--wire-position' gives: the reader walks the source a byte at a time,
|
||||||
|
and a space is one byte, so a byte count is exactly how many to write."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char start)
|
||||||
|
(concat (make-string (1- (line-number-at-pos start)) ?\n)
|
||||||
|
(make-string (- (position-bytes start)
|
||||||
|
(position-bytes (line-beginning-position)))
|
||||||
|
?\s)
|
||||||
|
(buffer-substring-no-properties start end))))
|
||||||
|
|
||||||
(defun flan-dev--defun-bounds ()
|
(defun flan-dev--defun-bounds ()
|
||||||
"Bounds of the top-level form containing or preceding point, as (START . END)."
|
"Bounds of the top-level form containing or preceding point, as (START . END)."
|
||||||
(save-excursion
|
(save-excursion
|
||||||
@ -1657,6 +1684,259 @@ that the IR half is findable by name rather than only by a modifier."
|
|||||||
nil t))))
|
nil t))))
|
||||||
(flan-disassemble name t))
|
(flan-disassemble name t))
|
||||||
|
|
||||||
|
;;; What a macro call expands to
|
||||||
|
|
||||||
|
;; CIDER's `C-c C-m', and the reason it arrives now rather than with macros
|
||||||
|
;; themselves: a macro is importable from a package, so `(rl/with-drawing ...)'
|
||||||
|
;; is a call whose `defmacro' lives in another directory. Until the editor can
|
||||||
|
;; ask, there is nothing to read beside the call site at all — and the answer
|
||||||
|
;; is not in the file either way, because a macro answers a `Form' and nobody
|
||||||
|
;; ever wrote that down.
|
||||||
|
;;
|
||||||
|
;; Two commands off one key, the way `C-c C-a' already does it: `C-c C-m' is
|
||||||
|
;; one step and `C-u C-c C-m' is all the way. One step is the bare key because
|
||||||
|
;; it is the one that can name the macro that ran. A macro may quasiquote a
|
||||||
|
;; call to another macro — `mac/quad' answers `(mac/twice (mac/twice n))' — and
|
||||||
|
;; `Loc.from_macro' is outermost-wins, so by the time a full expansion settles
|
||||||
|
;; every node of it is stamped with the name the author wrote and the
|
||||||
|
;; intermediate ones are gone. All the way is the answer the compiler acts on;
|
||||||
|
;; one step is how you find out which macro produced what.
|
||||||
|
;;
|
||||||
|
;; The buffer is `flan-mode', because what it holds is Flan source and the
|
||||||
|
;; whole point is to read it: the indentation and the font-lock are this
|
||||||
|
;; project's own. Three keys are shadowed for exactly that reason — the text
|
||||||
|
;; in here is *not* the text of any file, it has no locations of its own, and
|
||||||
|
;; `C-c C-c' over it would install a body nobody wrote under a name somebody
|
||||||
|
;; did.
|
||||||
|
;;
|
||||||
|
;; From cnr, the idea rather than the code: a key that expands in place. `C-c
|
||||||
|
;; C-m' inside the buffer takes the form at point one more step and puts the
|
||||||
|
;; result where it was, which is what makes one-step-by-default usable rather
|
||||||
|
;; than a thing you press twice and lose. cnr's own folding is frame-and-locals
|
||||||
|
;; machinery with its own text properties and there is nothing here to fold.
|
||||||
|
|
||||||
|
(defcustom flan-macroexpansion-buffer "*flan-macroexpansion*"
|
||||||
|
"Where `flan-macroexpand' draws."
|
||||||
|
:type 'string)
|
||||||
|
|
||||||
|
(defvar flan-macroexpand-request-function #'flan-dev--request
|
||||||
|
"How the macroexpansion buffer reaches the daemon.
|
||||||
|
One plist in, the reply plist out. A variable for `flan-cnr-request-function''s
|
||||||
|
reason: so the renderer can be driven from a fixture.")
|
||||||
|
|
||||||
|
(defvar-local flan-macroexpand--origin nil
|
||||||
|
"What this buffer is showing, as a plist: :code, :file and :all.
|
||||||
|
`g' re-asks it, so an expansion can be refreshed after the `defmacro' behind
|
||||||
|
it has been re-evaluated.")
|
||||||
|
|
||||||
|
(defun flan-macroexpand--bounds ()
|
||||||
|
"Bounds of the form to expand, as (START . END).
|
||||||
|
|
||||||
|
The form before point, which is the region `C-x C-e' chooses and the one
|
||||||
|
CIDER's own macroexpand uses — and the form *at* point when point sits on its
|
||||||
|
opening delimiter, which `C-x C-e' has no need of and this does. A macro call
|
||||||
|
is a form you put point on; `backward-sexp' from an open paren takes the
|
||||||
|
previous sibling, which is never what was meant."
|
||||||
|
(save-excursion
|
||||||
|
(skip-chars-forward " \t")
|
||||||
|
(if (looking-at-p "[([{]")
|
||||||
|
(cons (point) (save-excursion (forward-sexp) (point)))
|
||||||
|
(let ((end (point)))
|
||||||
|
(backward-sexp)
|
||||||
|
(cons (point) end)))))
|
||||||
|
|
||||||
|
(defun flan-macroexpand--ask (code file all)
|
||||||
|
"Ask the daemon what CODE, written in FILE, expands to.
|
||||||
|
ALL asks for the fixpoint rather than one step. Answers the reply plist, or
|
||||||
|
signals — having drawn the refusal where it happened, which is why the caller
|
||||||
|
sends padded text."
|
||||||
|
(let ((r (funcall flan-macroexpand-request-function
|
||||||
|
(list :op "macroexpand" :code code :file file
|
||||||
|
:all (if all t nil)))))
|
||||||
|
(unless (equal (plist-get r :status) "ok")
|
||||||
|
(let ((loc (plist-get r :loc))
|
||||||
|
(msg (plist-get r :message)))
|
||||||
|
;; The two refusals that reach here are a macro that never settles and
|
||||||
|
;; a ring, and both name a macro at a location — so mark it, exactly as
|
||||||
|
;; a refused evaluation is marked. This must not itself signal: the
|
||||||
|
;; error the caller is owed is the daemon's.
|
||||||
|
(ignore-errors (flan-dev--show-error loc (or msg "refused")))
|
||||||
|
(user-error "flan: %s%s" (or msg "refused")
|
||||||
|
(if loc (format " (%s)" loc) ""))))
|
||||||
|
r))
|
||||||
|
|
||||||
|
(defun flan-macroexpand--insert-code (text)
|
||||||
|
"Insert TEXT as code and let `flan-mode' decide its columns.
|
||||||
|
`Form.pretty' puts the line breaks in — that is structure, and a printer has
|
||||||
|
to choose it — and stops there. Where the columns go is this project's
|
||||||
|
indentation, which lives in `flan-mode' and not in the compiler."
|
||||||
|
(let ((start (point))
|
||||||
|
;; `indent-region' reports its progress, which is noise in the echo
|
||||||
|
;; area over a form and a hundred lines of it in a batch run.
|
||||||
|
(inhibit-message t))
|
||||||
|
(insert text "\n")
|
||||||
|
(indent-region start (point))))
|
||||||
|
|
||||||
|
(defun flan-macroexpand--render (reply code file all)
|
||||||
|
"Draw REPLY, the expansion of CODE from FILE, into the macroexpansion buffer."
|
||||||
|
(with-current-buffer (get-buffer-create flan-macroexpansion-buffer)
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(erase-buffer)
|
||||||
|
(flan-macroexpansion-mode)
|
||||||
|
(setq flan-macroexpand--origin (list :code code :file file :all all))
|
||||||
|
(let ((start (point)))
|
||||||
|
(insert (format "; macroexpansion, %s\n"
|
||||||
|
(if all "all the way" "one step")))
|
||||||
|
(put-text-property start (point) 'face 'font-lock-comment-face))
|
||||||
|
(flan-disassemble--header "of" (string-trim code))
|
||||||
|
(flan-disassemble--header
|
||||||
|
"macro" (or (plist-get reply :macro)
|
||||||
|
"none — the head of this form is not a macro"))
|
||||||
|
;; What `Loc.from_macro' means for what is printed, said here because
|
||||||
|
;; there is nowhere else it could be said. Every node below carries the
|
||||||
|
;; *call site's* file, line and column with the macro's name stamped on
|
||||||
|
;; it, so an error in expanded code points at the call you wrote. None of
|
||||||
|
;; it has a location of its own, nothing in it is the text of any file,
|
||||||
|
;; and there is nothing here for `M-.' to jump to.
|
||||||
|
(flan-disassemble--header
|
||||||
|
"locations"
|
||||||
|
(format "every node below carries %s's own %s, tagged with the macro \
|
||||||
|
that produced it; the text has no locations of its own and is not in any file"
|
||||||
|
(file-name-nondirectory file)
|
||||||
|
(if (plist-get reply :macro) "line and column" "position")))
|
||||||
|
(when (plist-get reply :note)
|
||||||
|
(flan-disassemble--header "note" (plist-get reply :note)))
|
||||||
|
(insert "\n")
|
||||||
|
(flan-macroexpand--insert-code (plist-get reply :text))
|
||||||
|
;; Point on the code rather than on the header. Found by the blank line
|
||||||
|
;; that separates them and not by counting: a header line is filled, so
|
||||||
|
;; a long one is two lines and a count would drift onto it.
|
||||||
|
(goto-char (point-min))
|
||||||
|
(if (re-search-forward "^$" nil t) (forward-line 1) (goto-char (point-min)))))
|
||||||
|
(display-buffer flan-macroexpansion-buffer))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun flan-macroexpand (&optional all)
|
||||||
|
"Show what the macro call before point expands to.
|
||||||
|
|
||||||
|
One step, which is the one that can name the macro that ran. With a prefix
|
||||||
|
argument, or non-nil ALL, expand to the fixpoint instead — a macro may
|
||||||
|
quasiquote a call to another macro, so the two genuinely differ.
|
||||||
|
|
||||||
|
The form is expanded against the macros this *session* holds: the prelude's,
|
||||||
|
the ones its imports brought in, and every `defmacro' evaluated since it
|
||||||
|
started. Not a fresh read of the file, which would answer with what is saved
|
||||||
|
rather than with what is typed."
|
||||||
|
(interactive "P")
|
||||||
|
(let* ((b (flan-macroexpand--bounds))
|
||||||
|
(file (or buffer-file-name "<buffer>"))
|
||||||
|
;; Padded onto its own line *and column*, unlike `C-x C-e', because
|
||||||
|
;; the refusals this path can get name a location inside the snippet
|
||||||
|
;; and a macro call is written well inside a line.
|
||||||
|
(code (flan-dev--text-at (car b) (cdr b)))
|
||||||
|
(r (flan-macroexpand--ask code file all)))
|
||||||
|
(flan-macroexpand--render r (buffer-substring-no-properties (car b) (cdr b))
|
||||||
|
file all)
|
||||||
|
(pulse-momentary-highlight-region (car b) (cdr b))
|
||||||
|
(unless (plist-get r :expanded)
|
||||||
|
(message "flan: %s" (or (plist-get r :note) "nothing expanded")))
|
||||||
|
r))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun flan-macroexpand-all ()
|
||||||
|
"Expand the macro call before point to its fixpoint.
|
||||||
|
`flan-macroexpand' with a prefix argument does the same thing; this exists so
|
||||||
|
the other half is findable by name and not only by a modifier."
|
||||||
|
(interactive)
|
||||||
|
(flan-macroexpand t))
|
||||||
|
|
||||||
|
(defun flan-macroexpand-again (&optional all)
|
||||||
|
"Expand the form at point one more step, in place.
|
||||||
|
|
||||||
|
The buffer holds code with no file behind it, so the expansion replaces the
|
||||||
|
text it came from rather than opening anything. With a prefix argument, or
|
||||||
|
non-nil ALL, take it all the way instead."
|
||||||
|
(interactive "P")
|
||||||
|
(unless flan-macroexpand--origin
|
||||||
|
(user-error "flan: this is not a macroexpansion buffer"))
|
||||||
|
(let* ((b (flan-macroexpand--bounds))
|
||||||
|
(code (buffer-substring-no-properties (car b) (cdr b)))
|
||||||
|
;; Unpadded, and that is the honest shape here: this text is in no
|
||||||
|
;; file, so there is no line or column for a refusal to be drawn at.
|
||||||
|
;; The file still goes on the wire — it is what tells the daemon which
|
||||||
|
;; session's macros to expand against.
|
||||||
|
(r (flan-macroexpand--ask code (plist-get flan-macroexpand--origin :file)
|
||||||
|
all)))
|
||||||
|
(if (not (plist-get r :expanded))
|
||||||
|
(message "flan: %s" (or (plist-get r :note) "nothing expanded"))
|
||||||
|
(let ((inhibit-read-only t)
|
||||||
|
(start (car b)))
|
||||||
|
(delete-region (car b) (cdr b))
|
||||||
|
(goto-char start)
|
||||||
|
;; No trailing newline from `--insert-code': this is a form inside a
|
||||||
|
;; line, not a section.
|
||||||
|
(let ((from (point)))
|
||||||
|
(insert (plist-get r :text))
|
||||||
|
(indent-region from (point))
|
||||||
|
(pulse-momentary-highlight-region from (point)))
|
||||||
|
(goto-char start))
|
||||||
|
(message "flan: %s" (or (plist-get r :macro) "expanded")))))
|
||||||
|
|
||||||
|
(defun flan-macroexpand-all-again ()
|
||||||
|
"Expand the form at point to its fixpoint, in place."
|
||||||
|
(interactive)
|
||||||
|
(flan-macroexpand-again t))
|
||||||
|
|
||||||
|
(defun flan-macroexpand-refresh ()
|
||||||
|
"Ask again for the expansion this buffer is showing.
|
||||||
|
The `defmacro' behind it may have been re-evaluated since, and an expansion
|
||||||
|
drawn from the old body is exactly the staleness this whole feature exists to
|
||||||
|
remove."
|
||||||
|
(interactive)
|
||||||
|
(unless flan-macroexpand--origin
|
||||||
|
(user-error "flan: this is not a macroexpansion buffer"))
|
||||||
|
(let* ((o flan-macroexpand--origin)
|
||||||
|
(r (flan-macroexpand--ask (plist-get o :code) (plist-get o :file)
|
||||||
|
(plist-get o :all))))
|
||||||
|
(flan-macroexpand--render r (plist-get o :code) (plist-get o :file)
|
||||||
|
(plist-get o :all))))
|
||||||
|
|
||||||
|
(defun flan-macroexpand--not-source ()
|
||||||
|
"Refuse to evaluate the contents of the macroexpansion buffer."
|
||||||
|
(interactive)
|
||||||
|
(user-error
|
||||||
|
"flan: this is an expansion, not source — it is in no file, and installing \
|
||||||
|
it would redefine a name with code nobody wrote"))
|
||||||
|
|
||||||
|
(defvar flan-macroexpansion-mode-map
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
;; The three that would send this buffer's text to the daemon as though it
|
||||||
|
;; were a file. Inherited from `flan-mode-map' and shadowed by name rather
|
||||||
|
;; than by unbinding, so pressing one says why instead of doing nothing.
|
||||||
|
(define-key map (kbd "C-c C-c") #'flan-macroexpand--not-source)
|
||||||
|
(define-key map (kbd "C-c C-k") #'flan-macroexpand--not-source)
|
||||||
|
(define-key map (kbd "C-x C-e") #'flan-macroexpand--not-source)
|
||||||
|
(define-key map (kbd "C-c C-m") #'flan-macroexpand-again)
|
||||||
|
;; One-key aliases, as the break buffer has: this is a buffer you read with
|
||||||
|
;; one hand.
|
||||||
|
(define-key map (kbd "m") #'flan-macroexpand-again)
|
||||||
|
(define-key map (kbd "a") #'flan-macroexpand-all-again)
|
||||||
|
(define-key map (kbd "g") #'flan-macroexpand-refresh)
|
||||||
|
(define-key map (kbd "q") #'quit-window)
|
||||||
|
map)
|
||||||
|
"Keymap for `flan-macroexpansion-mode'.")
|
||||||
|
|
||||||
|
(define-derived-mode flan-macroexpansion-mode flan-mode "Flan-Macro"
|
||||||
|
"Mode for the buffer `flan-macroexpand' writes.
|
||||||
|
|
||||||
|
Derived from `flan-mode' because what is in it is Flan source and reading it
|
||||||
|
is the whole point: the indentation and the font-lock are the ones this
|
||||||
|
project already has. Read-only, and the three keys that would send its text
|
||||||
|
back to the daemon say why they will not.
|
||||||
|
|
||||||
|
\\{flan-macroexpansion-mode-map}"
|
||||||
|
(setq buffer-read-only t))
|
||||||
|
|
||||||
;;; What the program is made of, and what it is still holding
|
;;; What the program is made of, and what it is still holding
|
||||||
|
|
||||||
;; Two readings of one table. A dev build records the type at every
|
;; Two readings of one table. A dev build records the type at every
|
||||||
|
|||||||
@ -73,6 +73,11 @@
|
|||||||
(autoload 'flan-dev-restart-program "flan-dev" nil t)
|
(autoload 'flan-dev-restart-program "flan-dev" nil t)
|
||||||
;; Bound below, like the rest, and it was the one missing an autoload.
|
;; Bound below, like the rest, and it was the one missing an autoload.
|
||||||
(autoload 'flan-disassemble "flan-dev" nil t)
|
(autoload 'flan-disassemble "flan-dev" nil t)
|
||||||
|
;; C-c C-m and the half of it that is findable by name rather than by a
|
||||||
|
;; modifier. Real autoloads for the reason stated above: a `declare-function'
|
||||||
|
;; would leave M-x with nothing to load.
|
||||||
|
(autoload 'flan-macroexpand "flan-dev" nil t)
|
||||||
|
(autoload 'flan-macroexpand-all "flan-dev" nil t)
|
||||||
|
|
||||||
(defgroup flan nil
|
(defgroup flan nil
|
||||||
"Editing and evaluating Flan."
|
"Editing and evaluating Flan."
|
||||||
@ -190,6 +195,12 @@ line is off screen."
|
|||||||
;; IR it was built from. C-u for the IR rather than a second key: it is
|
;; IR it was built from. C-u for the IR rather than a second key: it is
|
||||||
;; the same question asked of the same body.
|
;; the same question asked of the same body.
|
||||||
(define-key map (kbd "C-c C-a") #'flan-disassemble)
|
(define-key map (kbd "C-c C-a") #'flan-disassemble)
|
||||||
|
;; What the macro call before point expands to. CIDER's key, and `C-u' for
|
||||||
|
;; the fixpoint rather than a second one — the same question asked of the
|
||||||
|
;; same form, which is the rule `C-c C-a' above already follows. One step
|
||||||
|
;; is the bare key because it is the one that can name the macro that ran:
|
||||||
|
;; a full expansion is stamped with the outermost name only.
|
||||||
|
(define-key map (kbd "C-c C-m") #'flan-macroexpand)
|
||||||
;; The way out when a reload is refused: rebuild, relaunch, reconnect.
|
;; The way out when a reload is refused: rebuild, relaunch, reconnect.
|
||||||
(define-key map (kbd "C-c C-x") #'flan-dev-restart-program)
|
(define-key map (kbd "C-c C-x") #'flan-dev-restart-program)
|
||||||
map)
|
map)
|
||||||
|
|||||||
@ -1115,6 +1115,84 @@ stopped program, which is the case where it should fire."
|
|||||||
(string-match-p "unhandled" text)))
|
(string-match-p "unhandled" text)))
|
||||||
|
|
||||||
|
|
||||||
|
;;; The macroexpansion buffer
|
||||||
|
|
||||||
|
;; The same kind of thing again: a reply in, a buffer out. What the live test
|
||||||
|
;; in test-flan-dev.el cannot easily reach is the shape where *nothing*
|
||||||
|
;; expanded — a head that is not a macro, and a macro whose answer is its own
|
||||||
|
;; call — because both have to be arranged in a program. Here they are a
|
||||||
|
;; plist.
|
||||||
|
|
||||||
|
(message "\nthe macroexpansion buffer")
|
||||||
|
|
||||||
|
(let ((flan-macroexpand-request-function
|
||||||
|
(lambda (_) '(:status "ok" :text "(if (not false)\n (do 1 2))"
|
||||||
|
:flat "(if (not false) (do 1 2))"
|
||||||
|
:expanded t :macro "unless"))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(flan-mode)
|
||||||
|
(insert "(unless false 1 2)")
|
||||||
|
(goto-char (point-min))
|
||||||
|
(flan-macroexpand))
|
||||||
|
(let ((text (with-current-buffer flan-macroexpansion-buffer (buffer-string))))
|
||||||
|
(test-flan--check "the expansion is drawn"
|
||||||
|
(string-match-p (regexp-quote "(if (not false)") text))
|
||||||
|
(test-flan--check "with the macro that produced it named"
|
||||||
|
(string-match-p "macro +unless" text))
|
||||||
|
(test-flan--check "and the form it came from"
|
||||||
|
(string-match-p (regexp-quote "(unless false 1 2)") text))
|
||||||
|
;; The one thing a reader of this buffer has to be told, because there is
|
||||||
|
;; nowhere else it could be said: nothing in it has a location of its own.
|
||||||
|
(test-flan--check "and what the locations in it mean"
|
||||||
|
(string-match-p "no locations of its own" text))
|
||||||
|
(test-flan--check "one step says so, rather than implying a fixpoint"
|
||||||
|
(string-match-p "one step" text))))
|
||||||
|
|
||||||
|
;; A head that is not a macro. The form comes back as itself, and the buffer
|
||||||
|
;; has to say which kind of nothing that is rather than looking like a very
|
||||||
|
;; short expansion.
|
||||||
|
(let ((flan-macroexpand-request-function
|
||||||
|
(lambda (_) '(:status "ok" :text "(+ 1 2)" :flat "(+ 1 2)"
|
||||||
|
:expanded nil
|
||||||
|
:note "the head of this form is not a macro"))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(flan-mode)
|
||||||
|
(insert "(+ 1 2)")
|
||||||
|
(goto-char (point-min))
|
||||||
|
(flan-macroexpand))
|
||||||
|
(let ((text (with-current-buffer flan-macroexpansion-buffer (buffer-string))))
|
||||||
|
(test-flan--check "a head that is not a macro says so"
|
||||||
|
(string-match-p "not a macro" text))
|
||||||
|
(test-flan--check "and the macro line says none rather than being absent"
|
||||||
|
(string-match-p "macro +none" text))))
|
||||||
|
|
||||||
|
;; The refusals reach here as an ordinary error reply, and the command has to
|
||||||
|
;; signal rather than draw an empty buffer: a macro that never settles is the
|
||||||
|
;; one that gets here.
|
||||||
|
(let ((flan-macroexpand-request-function
|
||||||
|
(lambda (_) '(:status "error"
|
||||||
|
:message "expanding s/spin did not settle after 200 rounds"
|
||||||
|
:loc "buf.flan:3:3"))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(flan-mode)
|
||||||
|
(insert "(s/spin)")
|
||||||
|
(goto-char (point-min))
|
||||||
|
(test-flan--check
|
||||||
|
"a refused expansion signals, carrying the daemon's words"
|
||||||
|
(let ((said (test-flan--caught #'flan-macroexpand)))
|
||||||
|
(and said (string-match-p "did not settle" said))))))
|
||||||
|
|
||||||
|
;; And the buffer refuses to be treated as source, which is the one way it
|
||||||
|
;; could do damage: the text in it is in no file and installing it would
|
||||||
|
;; redefine a name with code nobody wrote.
|
||||||
|
(with-current-buffer (get-buffer-create flan-macroexpansion-buffer)
|
||||||
|
(test-flan--check
|
||||||
|
"the expansion buffer refuses C-c C-c, C-c C-k and C-x C-e"
|
||||||
|
(seq-every-p
|
||||||
|
(lambda (k)
|
||||||
|
(eq (key-binding (kbd k)) #'flan-macroexpand--not-source))
|
||||||
|
'("C-c C-c" "C-c C-k" "C-x C-e"))))
|
||||||
|
|
||||||
;; `flan-mode' itself — indentation, which is a function from text to text and
|
;; `flan-mode' itself — indentation, which is a function from text to text and
|
||||||
;; so belongs with the other fixture-driven checks rather than with anything
|
;; so belongs with the other fixture-driven checks rather than with anything
|
||||||
;; that needs a daemon. Loaded rather than run separately because
|
;; that needs a daemon. Loaded rather than run separately because
|
||||||
|
|||||||
@ -947,6 +947,142 @@ is written instead — the real `message' call the real command makes."
|
|||||||
(flan-dev-quit)
|
(flan-dev-quit)
|
||||||
(ignore-errors (delete-file socket4)))
|
(ignore-errors (delete-file socket4)))
|
||||||
|
|
||||||
|
;; ── C-c C-m: what a macro call expands to ─────────────────────────────
|
||||||
|
;;
|
||||||
|
;; test_session.ml drives the expansion itself and is where the language
|
||||||
|
;; cases live. What this adds is the half that is only true in an editor:
|
||||||
|
;; which region the command picks, that the expansion arrives as readable
|
||||||
|
;; source in a buffer, that expanding again in place works, and — the one
|
||||||
|
;; worth a real daemon — that a macro which never settles comes back as a
|
||||||
|
;; refusal drawn on the call, at its own line *and its own column*.
|
||||||
|
;;
|
||||||
|
;; That last one is what `flan-dev--text-at' exists for. `C-x C-e' sends a
|
||||||
|
;; raw substring and `flan-dev--text' pads lines only, because a top-level
|
||||||
|
;; form starts at column 1; a macro call is written well inside a line, and
|
||||||
|
;; a refusal against it would otherwise be drawn at the start of that line.
|
||||||
|
(let ((socket5 (concat socket "-macro")))
|
||||||
|
(ignore-errors (delete-file socket5))
|
||||||
|
(flan-dev program socket5)
|
||||||
|
(test-flan--check "a daemon to expand macros against"
|
||||||
|
(process-live-p flan-dev--connection))
|
||||||
|
(with-current-buffer (get-file-buffer file)
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert "\n(defn expandable [] i32\n (unless false 1 2)\n (clamp 9 0 3))\n")
|
||||||
|
;; One step, on a prelude macro. Point on the opening paren, which is
|
||||||
|
;; where a reader puts it and which `C-x C-e' could not use.
|
||||||
|
(goto-char (point-max))
|
||||||
|
(search-backward "(unless false 1 2)")
|
||||||
|
(flan-macroexpand)
|
||||||
|
(test-flan--check
|
||||||
|
"C-c C-m expands the form point is on"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(string-match-p "(if (not false)" (buffer-string))))
|
||||||
|
(test-flan--check
|
||||||
|
"and says which macro ran"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(string-match-p "macro +unless" (buffer-string))))
|
||||||
|
(test-flan--check
|
||||||
|
"and the buffer is Flan source, not a dump"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(and (derived-mode-p 'flan-mode) buffer-read-only)))
|
||||||
|
;; The three keys that would send an expansion back as though it were a
|
||||||
|
;; file refuse by name rather than doing nothing.
|
||||||
|
(test-flan--check
|
||||||
|
"C-c C-c in the expansion buffer refuses, and says why"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(condition-case e (progn (call-interactively
|
||||||
|
(key-binding (kbd "C-c C-c")))
|
||||||
|
nil)
|
||||||
|
(user-error (string-match-p "not source" (error-message-string e))))))
|
||||||
|
;; All the way, off the same key with a prefix.
|
||||||
|
(goto-char (point-max))
|
||||||
|
(search-backward "(clamp 9 0 3)")
|
||||||
|
(flan-macroexpand t)
|
||||||
|
(test-flan--check
|
||||||
|
"C-u C-c C-m expands all the way"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(and (string-match-p "all the way" (buffer-string))
|
||||||
|
(string-match-p (regexp-quote "(min 3 (max 0 9))")
|
||||||
|
(buffer-string)))))
|
||||||
|
;; A macro that never settles, evaluated into the session and then asked
|
||||||
|
;; about. One step answers — it makes one call and does not look at what
|
||||||
|
;; comes back — and all the way is refused at the bound rather than
|
||||||
|
;; hanging the daemon, which is the failure that would wedge the editor
|
||||||
|
;; with the program still on screen.
|
||||||
|
(flan-dev--request
|
||||||
|
(list :op "eval" :file file
|
||||||
|
:code "(defmacro spinner [args] `(spinner ~@args))"))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert "\n(defn spun [] i32\n (spinner 1))\n")
|
||||||
|
(goto-char (point-max))
|
||||||
|
(search-backward "(spinner 1)")
|
||||||
|
(let ((call (point))
|
||||||
|
(r (flan-macroexpand)))
|
||||||
|
(test-flan--check
|
||||||
|
"one step of a macro that does not settle answers"
|
||||||
|
(and (null (plist-get r :expanded))
|
||||||
|
(equal (plist-get r :macro) "spinner")))
|
||||||
|
(flan-dev-clear-errors)
|
||||||
|
(goto-char call)
|
||||||
|
(let ((said (condition-case e (progn (flan-macroexpand t) nil)
|
||||||
|
(user-error (error-message-string e)))))
|
||||||
|
(test-flan--check
|
||||||
|
"and all the way is refused rather than hanging"
|
||||||
|
(and said (string-match-p "did not settle" said)))
|
||||||
|
;; The whole of `flan-dev--text-at': the refusal's location is the
|
||||||
|
;; call's own line and column, so the overlay lands on the call and
|
||||||
|
;; not at the start of the line it is written on.
|
||||||
|
(let ((ovs (flan-dev--error-overlays)))
|
||||||
|
(test-flan--check
|
||||||
|
"and the refusal is drawn on the call, at its own column"
|
||||||
|
(and (= 1 (length ovs))
|
||||||
|
(= (overlay-start (car ovs)) call)))))
|
||||||
|
(flan-dev-clear-errors))
|
||||||
|
|
||||||
|
;; Expanding again in place, which is what makes one-step-by-default
|
||||||
|
;; usable rather than a thing you press once and lose. Two macros into
|
||||||
|
;; the session, the outer one quasiquoting a call to the inner: that is
|
||||||
|
;; the shape where one step leaves a macro call standing, and it is the
|
||||||
|
;; only shape where expanding in place has anything to do.
|
||||||
|
(flan-dev--request
|
||||||
|
(list :op "eval" :file file
|
||||||
|
:code "(defmacro m-inner [args] `(+ ~(at args 0) 1))"))
|
||||||
|
(flan-dev--request
|
||||||
|
(list :op "eval" :file file
|
||||||
|
:code "(defmacro m-outer [args] `(m-inner ~(at args 0)))"))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert "\n(defn outered [] i32 (m-outer 5))\n")
|
||||||
|
(goto-char (point-max))
|
||||||
|
(search-backward "(m-outer 5)")
|
||||||
|
(flan-macroexpand)
|
||||||
|
(test-flan--check
|
||||||
|
"one step leaves the call to the macro it quasiquoted"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(string-match-p (regexp-quote "(m-inner 5)") (buffer-string))))
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward "(m-inner 5)")
|
||||||
|
(goto-char (match-beginning 0))
|
||||||
|
(flan-macroexpand-again)
|
||||||
|
(test-flan--check
|
||||||
|
"m in the expansion buffer expands the form at point in place"
|
||||||
|
(and (string-match-p (regexp-quote "(+ 5 1)") (buffer-string))
|
||||||
|
(not (string-match-p (regexp-quote "(m-inner 5)")
|
||||||
|
(buffer-string))))))
|
||||||
|
;; And the same call taken all the way in one go, which is the half the
|
||||||
|
;; prefix argument is for: no intermediate at all.
|
||||||
|
(goto-char (point-max))
|
||||||
|
(search-backward "(m-outer 5)")
|
||||||
|
(flan-macroexpand t)
|
||||||
|
(test-flan--check
|
||||||
|
"C-u goes straight to the fixpoint"
|
||||||
|
(with-current-buffer flan-macroexpansion-buffer
|
||||||
|
(and (string-match-p (regexp-quote "(+ 5 1)") (buffer-string))
|
||||||
|
(not (string-match-p (regexp-quote "(m-inner 5)")
|
||||||
|
(buffer-string)))))))
|
||||||
|
(flan-dev-quit)
|
||||||
|
(ignore-errors (delete-file socket5)))
|
||||||
|
|
||||||
(if (zerop test-flan--failures)
|
(if (zerop test-flan--failures)
|
||||||
(message "flan-dev.el: all tests passed")
|
(message "flan-dev.el: all tests passed")
|
||||||
(message "\n%d failure(s)" test-flan--failures)
|
(message "\n%d failure(s)" test-flan--failures)
|
||||||
|
|||||||
65
lib/dev.ml
65
lib/dev.ml
@ -546,6 +546,52 @@ let eval_expr t ~code ~origin ~pause =
|
|||||||
| exception Failure m -> error m)
|
| exception Failure m -> error m)
|
||||||
| exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } -> error ~loc:(Loc.to_string l) msg
|
| exception Loc.Error { Loc.dloc = l; dmsg = msg; _ } -> error ~loc:(Loc.to_string l) msg
|
||||||
|
|
||||||
|
(* What a macro call expands to — [C-c C-m], and the one verb here that never
|
||||||
|
touches the program.
|
||||||
|
|
||||||
|
Deliberately not gated on [alive t]. Every other verb in this file is a
|
||||||
|
question about a running process and says so when there is not one;
|
||||||
|
expansion is a question about the *compiler*, answered out of the macros the
|
||||||
|
session holds, and it is still answerable after the program has exited. That
|
||||||
|
is worth having rather than tidying away: the moment you most want to know
|
||||||
|
what a macro produced is often just after the code it produced crashed.
|
||||||
|
|
||||||
|
No [Loc.Error] arm either, and that is not an omission. The guard round
|
||||||
|
[handle] in [serve] catches everything non-fatal and answers it with
|
||||||
|
[reply_of_exn] — which is where the two non-termination refusals land, with
|
||||||
|
the call site's location on them, rather than in a hang that would leave the
|
||||||
|
editor waiting on a daemon with the program still on screen. *)
|
||||||
|
let macroexpand t ~code ~origin ~all =
|
||||||
|
let x = Session.macroexpand ~origin ~all t.session code in
|
||||||
|
ok
|
||||||
|
([ (* Line breaks in, columns left to the editor: see [Form.pretty]. *)
|
||||||
|
":text " ^ Wire.quote (Form.pretty x.Session.xafter);
|
||||||
|
(* And the same thing on one line, for a client with no indenter and for
|
||||||
|
the echo area. *)
|
||||||
|
":flat " ^ Wire.quote (Form.to_source x.Session.xafter);
|
||||||
|
":source " ^ Wire.quote (Form.to_source x.Session.xbefore);
|
||||||
|
(if all then ":all t" else ":all nil");
|
||||||
|
(if x.Session.xchanged then ":expanded t" else ":expanded nil") ]
|
||||||
|
@ (match x.Session.xmacro with
|
||||||
|
| Some n -> [ ":macro " ^ Wire.quote n ]
|
||||||
|
| None -> [])
|
||||||
|
@
|
||||||
|
(* The two ways of coming back unchanged are different facts and the
|
||||||
|
editor should not have to guess which it has. Only the one where a
|
||||||
|
macro *did* run is a statement about that macro. *)
|
||||||
|
match (x.Session.xchanged, x.Session.xmacro) with
|
||||||
|
| true, _ -> []
|
||||||
|
| false, None ->
|
||||||
|
[ ":note "
|
||||||
|
^ Wire.quote
|
||||||
|
"the head of this form is not a macro this session holds — the \
|
||||||
|
prelude's, an import's, and every defmacro evaluated since it \
|
||||||
|
started are what it can expand" ]
|
||||||
|
| false, Some n ->
|
||||||
|
[ ":note "
|
||||||
|
^ Wire.quote
|
||||||
|
(n ^ " expanded to the call it was given, unchanged") ])
|
||||||
|
|
||||||
let describe t =
|
let describe t =
|
||||||
ok
|
ok
|
||||||
[ ":fns "
|
[ ":fns "
|
||||||
@ -2102,6 +2148,25 @@ let handle t req =
|
|||||||
in
|
in
|
||||||
eval_expr t ~code ~origin ~pause
|
eval_expr t ~code ~origin ~pause
|
||||||
| None -> error "eval-expr needs :code")
|
| None -> error "eval-expr needs :code")
|
||||||
|
(* [:all], absent or [nil] being false and anything else true — the spelling
|
||||||
|
[:pause], [:on] and [:reset] already use. One step is the default because
|
||||||
|
it is the one that can name the macro that ran: a full expansion of a
|
||||||
|
macro that quasiquotes a call to another is stamped with the outermost
|
||||||
|
name only, [Loc.from_macro] being outermost-wins, so the intermediate is
|
||||||
|
unnameable by the time it settles. *)
|
||||||
|
| Some "macroexpand" ->
|
||||||
|
(match Wire.string_field req "code" with
|
||||||
|
| Some code ->
|
||||||
|
let origin =
|
||||||
|
match Wire.string_field req "file" with Some f -> f | None -> "<editor>"
|
||||||
|
in
|
||||||
|
let all =
|
||||||
|
match Wire.field req "all" with
|
||||||
|
| Some { Form.v = Form.Sym "nil"; _ } | None -> false
|
||||||
|
| Some _ -> true
|
||||||
|
in
|
||||||
|
macroexpand t ~code ~origin ~all
|
||||||
|
| None -> error "macroexpand needs :code")
|
||||||
| Some "describe" -> describe t
|
| Some "describe" -> describe t
|
||||||
| Some "defs" -> defs t
|
| Some "defs" -> defs t
|
||||||
| Some "break" -> break t
|
| Some "break" -> break t
|
||||||
|
|||||||
145
lib/form.ml
145
lib/form.ml
@ -39,3 +39,148 @@ let rec to_string f =
|
|||||||
| List l -> "(" ^ seq l ^ ")"
|
| List l -> "(" ^ seq l ^ ")"
|
||||||
| Vec l -> "[" ^ seq l ^ "]"
|
| Vec l -> "[" ^ seq l ^ "]"
|
||||||
| Map l -> "{" ^ seq l ^ "}"
|
| Map l -> "{" ^ seq l ^ "}"
|
||||||
|
|
||||||
|
(* ── Printing a Form back as source ─────────────────────────────────
|
||||||
|
|
||||||
|
[to_string] above is an error-message renderer: one line, no width, and
|
||||||
|
[%S] and [%g] where a reader's own spelling was never needed to say
|
||||||
|
"expected a name, found this". It is also what [Macro.key] digests, so it
|
||||||
|
is left exactly as it is — every cached macro module on disk is keyed by
|
||||||
|
what it prints today.
|
||||||
|
|
||||||
|
What follows is the other job, and it arrived with [C-c C-m]: text a person
|
||||||
|
reads and a reader reads back. An expansion is *only* text. A macro answers
|
||||||
|
a [Form] and nothing in the language ever wrote it down, so unlike every
|
||||||
|
other thing the editor shows there is no file to point at and no source to
|
||||||
|
fall back on — whatever this prints is the whole of what anybody sees.
|
||||||
|
|
||||||
|
Three places where [to_string] is not a round trip, all of them reachable
|
||||||
|
from an expansion because a macro may build any literal at all:
|
||||||
|
|
||||||
|
- [%g] prints 1.0 as "1", which reads back as an [Int], and it truncates at
|
||||||
|
six significant digits. Shortest-round-trip here, then a ".0" when
|
||||||
|
nothing in the text says "float".
|
||||||
|
- [%S] is OCaml's escaping. The reader takes exactly six escapes — newline,
|
||||||
|
tab, return, backslash, quote and nul — and every other byte literally,
|
||||||
|
so the three-digit decimal escape [%S] writes would not read back.
|
||||||
|
- [Byte] falls through to \<char>, which spells 0 and 13 as a NUL and a
|
||||||
|
carriage return sitting in the middle of the source. The reader has names
|
||||||
|
for those and this uses them. *)
|
||||||
|
|
||||||
|
let escape s =
|
||||||
|
let b = Buffer.create (String.length s + 2) in
|
||||||
|
String.iter
|
||||||
|
(fun c ->
|
||||||
|
match c with
|
||||||
|
| '\n' -> Buffer.add_string b "\\n"
|
||||||
|
| '\t' -> Buffer.add_string b "\\t"
|
||||||
|
| '\r' -> Buffer.add_string b "\\r"
|
||||||
|
| '\\' -> Buffer.add_string b "\\\\"
|
||||||
|
| '"' -> Buffer.add_string b "\\\""
|
||||||
|
| '\000' -> Buffer.add_string b "\\0"
|
||||||
|
| c -> Buffer.add_char b c)
|
||||||
|
s;
|
||||||
|
Buffer.contents b
|
||||||
|
|
||||||
|
let float_repr x =
|
||||||
|
(* The shortest of the three precisions that survives [float_of_string] is
|
||||||
|
the one the reader parses back to the same bits. 15 covers almost every
|
||||||
|
literal anyone writes; 17 covers every double there is. *)
|
||||||
|
let rec shortest = function
|
||||||
|
| [] -> Printf.sprintf "%.17g" x
|
||||||
|
| p :: ps ->
|
||||||
|
let s = Printf.sprintf "%.*g" p x in
|
||||||
|
if float_of_string s = x then s else shortest ps
|
||||||
|
in
|
||||||
|
let s = shortest [ 15; 16; 17 ] in
|
||||||
|
(* "1" is an integer to the reader, so a float whose text has no point and
|
||||||
|
no exponent needs one. Guarded on the text and not on the value: nan and
|
||||||
|
the infinities print as words, and "nan.0" is no improvement on a literal
|
||||||
|
no reader accepts either way. *)
|
||||||
|
let plain =
|
||||||
|
s <> ""
|
||||||
|
&& String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-' || c = '+') s
|
||||||
|
in
|
||||||
|
if plain then s ^ ".0" else s
|
||||||
|
|
||||||
|
let byte_repr b =
|
||||||
|
match b with
|
||||||
|
| 32 -> "\\space"
|
||||||
|
| 9 -> "\\tab"
|
||||||
|
| 10 -> "\\newline"
|
||||||
|
| 13 -> "\\return"
|
||||||
|
| 0 -> "\\nul"
|
||||||
|
(* Printable ASCII is written as itself. Anything else has no spelling in
|
||||||
|
the reader at all — [read_byte] takes a name or a single character — so
|
||||||
|
it is written as the decimal the reader would have to grow, rather than
|
||||||
|
as a byte that would corrupt the line it is on. *)
|
||||||
|
| b when b > 32 && b < 127 -> Printf.sprintf "\\%c" (Char.chr b)
|
||||||
|
| b -> Printf.sprintf "\\%d" b
|
||||||
|
|
||||||
|
(** One line, and a reader reads it back. *)
|
||||||
|
let rec to_source f =
|
||||||
|
let seq l = String.concat " " (List.map to_source l) in
|
||||||
|
match f.v with
|
||||||
|
| Sym s -> s
|
||||||
|
| Kw s -> ":" ^ s
|
||||||
|
| Int i -> Int64.to_string i
|
||||||
|
| Float x -> float_repr x
|
||||||
|
| Str s -> "\"" ^ escape s ^ "\""
|
||||||
|
| Byte b -> byte_repr b
|
||||||
|
| List l -> "(" ^ seq l ^ ")"
|
||||||
|
| Vec l -> "[" ^ seq l ^ "]"
|
||||||
|
| Map l -> "{" ^ seq l ^ "}"
|
||||||
|
|
||||||
|
(** The same text with line breaks in it, for a form too wide to read on one.
|
||||||
|
|
||||||
|
Where the breaks go, and deliberately not where the columns do. A list
|
||||||
|
that fits is written flat; one that does not keeps its head on the opening
|
||||||
|
line and puts each remaining element on its own, two columns in. That is
|
||||||
|
the structural half, which a printer has to decide. The indentation is the
|
||||||
|
editor's: [flan-mode] re-indents what it is shown, and that is where this
|
||||||
|
project's indentation rules already live, so nothing here tries to know
|
||||||
|
that a [let] aligns its bindings under the bracket. A client with no Emacs
|
||||||
|
still gets something readable rather than one very long line. *)
|
||||||
|
let pretty ?(width = 72) (f : t) : string =
|
||||||
|
let b = Buffer.create 256 in
|
||||||
|
let rec go col f =
|
||||||
|
let flat = to_source f in
|
||||||
|
if col + String.length flat <= width then Buffer.add_string b flat
|
||||||
|
else
|
||||||
|
let elements ind rest =
|
||||||
|
List.iter
|
||||||
|
(fun e ->
|
||||||
|
Buffer.add_char b '\n';
|
||||||
|
Buffer.add_string b (String.make ind ' ');
|
||||||
|
go ind e)
|
||||||
|
rest
|
||||||
|
in
|
||||||
|
match f.v with
|
||||||
|
(* A call or a special form: the head names what this is, so it stays on
|
||||||
|
the opening line whatever the rest of it costs. *)
|
||||||
|
| List (({ v = Sym _; _ } as h) :: rest) ->
|
||||||
|
Buffer.add_char b '(';
|
||||||
|
Buffer.add_string b (to_source h);
|
||||||
|
elements (col + 2) rest;
|
||||||
|
Buffer.add_char b ')'
|
||||||
|
| List (x :: rest) ->
|
||||||
|
Buffer.add_char b '(';
|
||||||
|
go (col + 1) x;
|
||||||
|
elements (col + 1) rest;
|
||||||
|
Buffer.add_char b ')'
|
||||||
|
| Vec (x :: rest) ->
|
||||||
|
Buffer.add_char b '[';
|
||||||
|
go (col + 1) x;
|
||||||
|
elements (col + 1) rest;
|
||||||
|
Buffer.add_char b ']'
|
||||||
|
| Map (x :: rest) ->
|
||||||
|
Buffer.add_char b '{';
|
||||||
|
go (col + 1) x;
|
||||||
|
elements (col + 1) rest;
|
||||||
|
Buffer.add_char b '}'
|
||||||
|
(* An empty bracket, or a single atom longer than the width. Neither has
|
||||||
|
a break in it to take. *)
|
||||||
|
| _ -> Buffer.add_string b flat
|
||||||
|
in
|
||||||
|
go 0 f;
|
||||||
|
Buffer.contents b
|
||||||
|
|||||||
95
lib/macro.ml
95
lib/macro.ml
@ -291,8 +291,17 @@ let rounds ~(prelude : string list) (pending : (string * Form.t) list)
|
|||||||
a macro would otherwise re-read the whole of it on every parse. *)
|
a macro would otherwise re-read the whole of it on every parse. *)
|
||||||
let prelude_macros = lazy (macros_in (Prelude.forms ()))
|
let prelude_macros = lazy (macros_in (Prelude.forms ()))
|
||||||
|
|
||||||
let program (forms : Form.t list) : Form.t list =
|
(* The module the forms below will be expanded against, or [None] when there is
|
||||||
if !building then forms
|
nothing to expand them with.
|
||||||
|
|
||||||
|
Split out of [program] rather than copied into the editor's path, because
|
||||||
|
everything in it is a decision with a paragraph attached — which names are
|
||||||
|
ambient, which shadow which, and the shortcut that keeps a macro-free build
|
||||||
|
free of a clang driver. Two copies of that would drift, and the second copy
|
||||||
|
is the one a reader would not know to distrust. [program] below is the whole
|
||||||
|
of what used to be here; [expand_step] and [expand_all] are the editor's. *)
|
||||||
|
let loaded_for (forms : Form.t list) : loaded option =
|
||||||
|
if !building then None
|
||||||
else
|
else
|
||||||
let prelude = Lazy.force prelude_macros in
|
let prelude = Lazy.force prelude_macros in
|
||||||
(* The prelude's own macros are dropped from [mine], and the reason is that
|
(* The prelude's own macros are dropped from [mine], and the reason is that
|
||||||
@ -340,14 +349,84 @@ let program (forms : Form.t list) : Form.t list =
|
|||||||
macro pays nothing: a file that calls none costs one scan and no
|
macro pays nothing: a file that calls none costs one scan and no
|
||||||
compiler. Without it every build in the suite would link a macro module
|
compiler. Without it every build in the suite would link a macro module
|
||||||
for the prelude's macros and pay a clang driver to answer nothing. *)
|
for the prelude's macros and pay a clang driver to answer nothing. *)
|
||||||
if all = [] || not (List.exists (names_macro all) forms) then forms
|
if all = [] || not (List.exists (names_macro all) forms) then None
|
||||||
else begin
|
else begin
|
||||||
let extra = rounds ~prelude mine in
|
let extra = rounds ~prelude mine in
|
||||||
let l = compile all (List.map snd extra) in
|
Some (compile all (List.map snd extra))
|
||||||
let out = List.map (expand_form l) forms in
|
|
||||||
Dynload.dl_close l.handle;
|
|
||||||
Dynload.release ();
|
|
||||||
out
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
(* Closing the handle and freeing the marshaller's scratch, on the way out of
|
||||||
|
an expansion however it ends.
|
||||||
|
|
||||||
|
[Fun.protect] and not two statements after the call, which is what this was
|
||||||
|
while a build was the only caller: a build that raised was a compile that
|
||||||
|
failed and a process about to exit, so a leaked handle and a few unfreed
|
||||||
|
argument buffers cost nothing anybody could measure. The daemon is not that
|
||||||
|
process. A macro that does not settle raises out of here, the editor is told
|
||||||
|
and stays connected, and the next [C-c C-m] does it again — so the one thing
|
||||||
|
[Dynload.owned] must not become is a list that only ever grows over a
|
||||||
|
session's lifetime. *)
|
||||||
|
let with_module (l : loaded) (f : unit -> 'a) : 'a =
|
||||||
|
Fun.protect
|
||||||
|
~finally:(fun () ->
|
||||||
|
Dynload.dl_close l.handle;
|
||||||
|
Dynload.release ())
|
||||||
|
f
|
||||||
|
|
||||||
|
let program (forms : Form.t list) : Form.t list =
|
||||||
|
match loaded_for forms with
|
||||||
|
| None -> forms
|
||||||
|
| Some l -> with_module l (fun () -> List.map (expand_form l) forms)
|
||||||
|
|
||||||
let () = Parse.expander := program
|
let () = Parse.expander := program
|
||||||
|
|
||||||
|
(* ── What the editor asks ──────────────────────────────────────────
|
||||||
|
[C-c C-m]. Two questions and not one, because a macro that quasiquotes a
|
||||||
|
call to another macro makes the difference real: [mac/quad] answers
|
||||||
|
[(mac/twice (mac/twice n))], and the fixpoint of that says nothing about
|
||||||
|
which macro produced what.
|
||||||
|
|
||||||
|
Both answer the name at the head when it is a macro, so the editor can say
|
||||||
|
*which* macro it just ran rather than only that something changed. That name
|
||||||
|
is the one thing the printed text cannot carry: [Loc.from_macro] is
|
||||||
|
outermost-wins, so every node of a full expansion is stamped with the macro
|
||||||
|
the author wrote and the intermediate names are gone by the time it settles.
|
||||||
|
|
||||||
|
One step is outermost-only, and that is a deliberate difference from
|
||||||
|
[expand_form], which expands a call's arguments *before* calling it. So
|
||||||
|
[(mac/twice (mac/twice 1))] one-stepped here is [(+ (mac/twice 1)
|
||||||
|
(mac/twice 1))], where the compiler's own first move is [(mac/twice (+ 1
|
||||||
|
1))]. Different intermediates, the same fixpoint. One step is a view of what
|
||||||
|
this macro did; all the way is the answer the compiler acts on. *)
|
||||||
|
|
||||||
|
let head_macro (l : loaded) (f : Form.t) : string option =
|
||||||
|
match f.Form.v with
|
||||||
|
| Form.List ({ Form.v = Form.Sym n; _ } :: _) when List.mem_assoc n l.fns ->
|
||||||
|
Some n
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
(** One round of the outermost call, or the form unchanged when its head does
|
||||||
|
not name a macro. Nothing here needs the fuel [settle] carries: one call is
|
||||||
|
one call, and what it answers is not looked at again. *)
|
||||||
|
let expand_step (f : Form.t) : Form.t * string option =
|
||||||
|
match loaded_for [ f ] with
|
||||||
|
| None -> (f, None)
|
||||||
|
| Some l ->
|
||||||
|
with_module l (fun () ->
|
||||||
|
match f.Form.v with
|
||||||
|
| Form.List ({ Form.v = Form.Sym n; _ } :: args)
|
||||||
|
when List.mem_assoc n l.fns ->
|
||||||
|
( Expand.call ~loc:(Loc.from_macro n f.Form.loc) (List.assoc n l.fns)
|
||||||
|
args,
|
||||||
|
Some n )
|
||||||
|
| _ -> (f, None))
|
||||||
|
|
||||||
|
(** To the fixpoint, through exactly the walk a build goes through — so the
|
||||||
|
text this answers is the text the checker is about to be handed, and the
|
||||||
|
refusals are the build's own. A macro that does not settle raises
|
||||||
|
[Loc.Error] out of [settle] at the bound, and a ring was refused a level up
|
||||||
|
in [rounds] before anything was compiled at all. *)
|
||||||
|
let expand_all (f : Form.t) : Form.t * string option =
|
||||||
|
match loaded_for [ f ] with
|
||||||
|
| None -> (f, None)
|
||||||
|
| Some l -> with_module l (fun () -> (expand_form l f, head_macro l f))
|
||||||
|
|||||||
@ -1248,3 +1248,72 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
|||||||
lost track of what the program contains is worse than one that died. *)
|
lost track of what the program contains is worse than one that died. *)
|
||||||
t.program <- { t.program with Tast.fns = t.program.Tast.fns @ fresh };
|
t.program <- { t.program with Tast.fns = t.program.Tast.fns @ fresh };
|
||||||
{ ir; names = []; fns = []; installs = true }
|
{ ir; names = []; fns = []; installs = true }
|
||||||
|
|
||||||
|
(* ── What a macro call expands to ──────────────────────────────────── *)
|
||||||
|
|
||||||
|
(* [C-c C-m]. The one thing the editor can ask that compiles nothing, sends
|
||||||
|
nothing to the program, and leaves the session exactly as it found it.
|
||||||
|
|
||||||
|
It is a session operation rather than a [Reader] plus a [Macro] call for the
|
||||||
|
reason the whole of this file exists: what a call expands to is decided by
|
||||||
|
*which macros this session holds* — the prelude's, the ones its imports
|
||||||
|
brought in, and the [defmacro]s the buffer has evaluated since it started —
|
||||||
|
and that set lives in [t.macros] and nowhere else. Expanding against a fresh
|
||||||
|
read of the file would answer with macros the session was never told about
|
||||||
|
and with a version of the ones it was told about that is whatever happens to
|
||||||
|
be *saved*. An expansion that disagreed with what an evaluation does would
|
||||||
|
be worse than no expansion at all: a macro decides what the code is.
|
||||||
|
|
||||||
|
Nothing here is assigned. [eval] commits [t.macros] and [eval_expr] bumps
|
||||||
|
[t.thunks] and [t.program]; this reads and writes nothing, so a form that
|
||||||
|
only *looks* like a declaration — a [defmacro] handed to [C-c C-m] — does
|
||||||
|
not join the session by having been looked at. [test_session] pins that. *)
|
||||||
|
|
||||||
|
type expansion = {
|
||||||
|
xbefore : Form.t; (* what was sent, quasiquote already desugared *)
|
||||||
|
xafter : Form.t; (* what it expands to *)
|
||||||
|
xmacro : string option; (* the macro at the head, when the head is one *)
|
||||||
|
(* False when the expansion is the form itself. Reported rather than left for
|
||||||
|
the editor to diff, because the two ways of being unchanged are different
|
||||||
|
facts: a head that names no macro, and a macro whose answer is its own
|
||||||
|
call. Only [xmacro] tells them apart. *)
|
||||||
|
xchanged : bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
(* [all] is the difference between CIDER's two commands, and it is real here
|
||||||
|
rather than inherited: a macro may quasiquote a call to another macro, so
|
||||||
|
[(mac/quad 3)] one-stepped is [(mac/twice (mac/twice 3))] and all the way is
|
||||||
|
[(+ (+ 3 3) (+ 3 3))].
|
||||||
|
|
||||||
|
The refusals both live on the [all] path and both are [Loc.Error], which the
|
||||||
|
daemon answers as a reply: a macro that never settles is stopped by
|
||||||
|
[Macro.fuel] and named, and a ring of macros was refused while the package
|
||||||
|
holding it was parsed, which is before any session over it could exist. One
|
||||||
|
step cannot reach either — it makes exactly one call and does not look at
|
||||||
|
the answer — so [(s/spin)] one-stepped is a fact about the macro and only
|
||||||
|
[C-u] refuses. *)
|
||||||
|
let macroexpand ?(origin = "<eval>") ~(all : bool) t (src : string) : expansion =
|
||||||
|
let form =
|
||||||
|
match Reader.read_all ~file:origin src with
|
||||||
|
| [ f ] -> f
|
||||||
|
| [] -> fail Loc.unknown "nothing to expand"
|
||||||
|
| _ :: f :: _ -> fail f.Form.loc "one form at a time"
|
||||||
|
in
|
||||||
|
(* Desugared first, exactly as [Parse.parse_forms] does it and for its
|
||||||
|
reason: a quasiquote that has not been rewritten still looks like a call
|
||||||
|
to something named [quasiquote], and the [Form.Sym] inside it looks like a
|
||||||
|
head anything could mistake for one. *)
|
||||||
|
let before = Expand.quasiquote form in
|
||||||
|
(* And the session's macros in front of it, as [eval] and [eval_expr] both
|
||||||
|
put them: [Macro.program] reads [Parse.imported_macros] directly. *)
|
||||||
|
Parse.with_imported t.macros @@ fun () ->
|
||||||
|
let after, name =
|
||||||
|
if all then Macro.expand_all before else Macro.expand_step before
|
||||||
|
in
|
||||||
|
{ xbefore = before; xafter = after; xmacro = name;
|
||||||
|
(* Structural, through the printer both halves are shown with, so "changed"
|
||||||
|
means "would print differently" — which is the claim the buffer makes.
|
||||||
|
There is no equality on [Form.t] to use instead: it carries a [Loc.t],
|
||||||
|
and [Loc.from_macro] stamps a name onto every node a macro answers, so
|
||||||
|
even an identity expansion compares unequal. *)
|
||||||
|
xchanged = Form.to_source after <> Form.to_source before }
|
||||||
|
|||||||
@ -232,6 +232,92 @@ let () =
|
|||||||
(Option.value ~default:(status r) (field r "message")));
|
(Option.value ~default:(status r) (field r "message")));
|
||||||
value "a macro defined at the editor" "(thrice 14)" "42";
|
value "a macro defined at the editor" "(thrice 14)" "42";
|
||||||
|
|
||||||
|
(* ── C-c C-m over the socket ────────────────────────────────────
|
||||||
|
[test_session] drives the expansion itself and is where the cases
|
||||||
|
live; this is the same question asked the way a person asks it, and
|
||||||
|
what it adds is the daemon: the reply's fields, the pretty text
|
||||||
|
surviving the framing, and — the part worth a socket — that a request
|
||||||
|
which compiles a macro module and refuses it comes back as a reply
|
||||||
|
rather than as a daemon that stopped answering.
|
||||||
|
|
||||||
|
The macro set here is [printers.flan]'s, so it covers the prelude's
|
||||||
|
and the buffer's own; the imported-package half is [test_session]'s,
|
||||||
|
over [pkg-macro.flan]. *)
|
||||||
|
let expands ?(all = false) name code want =
|
||||||
|
let r =
|
||||||
|
request c
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\" :all %s)"
|
||||||
|
(quote code) (if all then "t" else "nil"))
|
||||||
|
in
|
||||||
|
match field r "flat" with
|
||||||
|
| Some v when v = want -> ()
|
||||||
|
| Some v -> fail "%s\n got: %S\n wanted: %S" name v want
|
||||||
|
| None ->
|
||||||
|
fail "%s: %s" name (Option.value ~default:(status r) (field r "message"))
|
||||||
|
in
|
||||||
|
expands "a prelude macro, expanded" "(clamp 9 0 3)" "(min 3 (max 0 9))";
|
||||||
|
expands "the file's own macro, expanded" "(tenfold 7)" "(* 7 10)";
|
||||||
|
(* And the one typed at the editor a moment ago, which is the session's
|
||||||
|
set rather than the file's: nothing on disk declares [thrice]. *)
|
||||||
|
expands "a macro defined at the editor, expanded" "(thrice 14)" "(* 14 3)";
|
||||||
|
(* A form with no macro in it comes back as itself, and says so rather
|
||||||
|
than echoing and leaving the editor to diff. *)
|
||||||
|
(let r =
|
||||||
|
request c
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\")"
|
||||||
|
(quote "(+ 1 2)"))
|
||||||
|
in
|
||||||
|
(* [:expanded] is a flag and therefore a symbol, not a string — the
|
||||||
|
spelling [:stopped] and [:overflow] already use — so it is read out
|
||||||
|
of the form rather than through [Wire.string_field]. *)
|
||||||
|
let flag k =
|
||||||
|
match Wire.field r k with
|
||||||
|
| Some f -> Form.to_source f
|
||||||
|
| None -> "<none>"
|
||||||
|
in
|
||||||
|
if flag "expanded" <> "nil" then
|
||||||
|
fail "a form that is not a macro call reported :expanded %s"
|
||||||
|
(flag "expanded");
|
||||||
|
if field r "note" = None then
|
||||||
|
fail "a form that is not a macro call gave no reason");
|
||||||
|
(* The name of the macro that ran rides on the reply, because the text
|
||||||
|
cannot carry it: [Loc.from_macro] is outermost-wins. *)
|
||||||
|
(let r =
|
||||||
|
request c
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\")"
|
||||||
|
(quote "(tenfold 7)"))
|
||||||
|
in
|
||||||
|
if field r "macro" <> Some "tenfold" then
|
||||||
|
fail "the reply named %s as the macro that ran"
|
||||||
|
(Option.value ~default:"<none>" (field r "macro")));
|
||||||
|
(* :text is the same expansion with the line breaks in it. Nothing here
|
||||||
|
asserts where they go — that is [Form.pretty]'s and the editor's — only
|
||||||
|
that the field is there and reads back as the same form. *)
|
||||||
|
(let r =
|
||||||
|
request c
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\")"
|
||||||
|
(quote "(tenfold 7)"))
|
||||||
|
in
|
||||||
|
match field r "text" with
|
||||||
|
| Some t when t = "(* 7 10)" -> ()
|
||||||
|
| Some t -> fail ":text was %S" t
|
||||||
|
| None -> fail ":text was missing");
|
||||||
|
(* And the session is untouched by having been asked: a [defmacro] handed
|
||||||
|
to C-c C-m does not join it. C-c C-c is where a declaration goes, and
|
||||||
|
the round trip below is the only way to check the aftermath. *)
|
||||||
|
ignore
|
||||||
|
(request c
|
||||||
|
(Printf.sprintf
|
||||||
|
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\")"
|
||||||
|
(quote "(defmacro looked-at [args] `(* ~(at args 0) 5))")));
|
||||||
|
(let r = evals "(looked-at 3)" in
|
||||||
|
if status r = "ok" then
|
||||||
|
fail "a defmacro joined the session by being macroexpanded");
|
||||||
|
|
||||||
ignore (request c "(:op \"close\")");
|
ignore (request c "(:op \"close\")");
|
||||||
Unix.close c
|
Unix.close c
|
||||||
end;
|
end;
|
||||||
|
|||||||
@ -529,6 +529,139 @@ let () =
|
|||||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
fail "a session that refused a macro could not evaluate afterwards: %s" m);
|
fail "a session that refused a macro could not evaluate afterwards: %s" m);
|
||||||
|
|
||||||
|
(* ── C-c C-m: what a macro call expands to ─────────────────────────
|
||||||
|
|
||||||
|
The verb that compiles nothing and sends nothing. What it has to get right
|
||||||
|
is the set it expands against: whatever *this session* holds, which is the
|
||||||
|
prelude's macros, the ones its imports brought in, and every [defmacro]
|
||||||
|
the buffer has evaluated since it started. A fresh read of the file would
|
||||||
|
answer with a different set and with whatever is saved rather than what is
|
||||||
|
typed, and an expansion that disagreed with an evaluation is the quietest
|
||||||
|
wrongness there is — a macro decides what the code *is*.
|
||||||
|
|
||||||
|
Asserted on the printed text, because the printed text is the whole of
|
||||||
|
what a person is shown. There is no IR to read here and nothing to compare
|
||||||
|
structurally: a [Form] carries a [Loc.t] and [Loc.from_macro] stamps a
|
||||||
|
name onto every node a macro answers, so even an identity expansion is
|
||||||
|
structurally unequal to its input. *)
|
||||||
|
let expands ?(all = false) name src want =
|
||||||
|
match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all tm src with
|
||||||
|
| x ->
|
||||||
|
let got = Form.to_source x.Session.xafter in
|
||||||
|
if got <> want then
|
||||||
|
fail "%s\n got: %S\n wanted: %S" name got want
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } -> fail "%s: %s" name m
|
||||||
|
in
|
||||||
|
(* A prelude macro: ambient, in every session, and the set that worked before
|
||||||
|
any of this existed. *)
|
||||||
|
expands "a prelude macro, one step" "(unless false 1 2)"
|
||||||
|
"(if (not false) (do 1 2))";
|
||||||
|
(* An imported package's, arriving qualified. This is most of the argument
|
||||||
|
for the feature: the definition is in another directory and cannot be read
|
||||||
|
beside the call site any more. *)
|
||||||
|
expands "an imported package macro, one step" "(mac/twice 4)" "(+ 4 4)";
|
||||||
|
(* The buffer's own, which is the set a session had to start holding before
|
||||||
|
an editor could ask anything about it. *)
|
||||||
|
expands "the file's own macro, one step" "(tenfold 7)" "(* 7 10)";
|
||||||
|
(* And the one that makes the two commands different rather than one with a
|
||||||
|
flag: [quad] quasiquotes a call to [twice], so a single step stops with a
|
||||||
|
macro call still in it and the full expansion does not. Both are true and
|
||||||
|
they answer different questions. *)
|
||||||
|
expands "a macro that expands to a call to another macro, one step"
|
||||||
|
"(mac/quad 3)" "(mac/twice (mac/twice 3))";
|
||||||
|
expands ~all:true "a macro that expands to a call to another macro, all the way"
|
||||||
|
"(mac/quad 3)" "(+ (+ 3 3) (+ 3 3))";
|
||||||
|
(* Outermost-only, which is where one step and the compiler's own first move
|
||||||
|
deliberately differ: [expand_form] expands a call's *arguments* before
|
||||||
|
calling it, so the compiler's first move here is [(mac/twice (+ 3 3))].
|
||||||
|
Same fixpoint, different intermediate, and the intermediate is the whole
|
||||||
|
of what one step is for. *)
|
||||||
|
expands "one step does not expand the arguments first"
|
||||||
|
"(mac/twice (mac/twice 3))" "(+ (mac/twice 3) (mac/twice 3))";
|
||||||
|
(* Not a macro call at all. The form comes back as it was, and the answer
|
||||||
|
that matters is [xmacro]: nothing ran. *)
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
|
||||||
|
"(+ 1 2)"
|
||||||
|
with
|
||||||
|
| x ->
|
||||||
|
if x.Session.xchanged || x.Session.xmacro <> None then
|
||||||
|
fail "a form whose head is not a macro reported a macro"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "expanding a form that is not a macro call: %s" m);
|
||||||
|
(* And the name of the macro that ran, which is the one thing the text cannot
|
||||||
|
carry: [Loc.from_macro] is outermost-wins, so a full expansion is stamped
|
||||||
|
with the macro the author wrote and every intermediate name is gone. *)
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
|
||||||
|
"(mac/quad 3)"
|
||||||
|
with
|
||||||
|
| x when x.Session.xmacro = Some "mac/quad" -> ()
|
||||||
|
| x ->
|
||||||
|
fail "expanding (mac/quad 3) named %s"
|
||||||
|
(match x.Session.xmacro with None -> "nothing" | Some n -> n)
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } -> fail "naming the macro: %s" m);
|
||||||
|
|
||||||
|
(* The session is not touched by having been asked. [eval] commits
|
||||||
|
[t.macros], [eval_expr] bumps [t.thunks] and [t.program]; this writes
|
||||||
|
nothing, so a [defmacro] handed to C-c C-m must not join the session by
|
||||||
|
having been looked at. C-c C-c is where a declaration goes.
|
||||||
|
|
||||||
|
The head is not a macro, so nothing expands; the claim is about the
|
||||||
|
*aftermath*, and it is checked the only way it can be — by calling the
|
||||||
|
name and requiring it to still be unknown. *)
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
|
||||||
|
"(defmacro looked-at [args] `(* ~(at args 0) 5))"
|
||||||
|
with
|
||||||
|
| _ -> ()
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "expanding a defmacro: %s" m);
|
||||||
|
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(looked-at 3)" with
|
||||||
|
| _ -> fail "a defmacro joined the session by being macroexpanded"
|
||||||
|
| exception Loc.Error _ -> ());
|
||||||
|
|
||||||
|
(* Non-termination, on this path, in both directions.
|
||||||
|
|
||||||
|
One step makes exactly one call and does not look at what comes back, so
|
||||||
|
[(s/spin)] one-stepped is a fact about the macro and terminates — the
|
||||||
|
bound must be where it is needed and nowhere else, or the command would
|
||||||
|
refuse to show anybody the thing they are trying to see.
|
||||||
|
|
||||||
|
All the way is the path with the fuel on it, and what has to be true is
|
||||||
|
that the bound is *reached* rather than the daemon hanging: a hang here
|
||||||
|
wedges the editor with the program still running and no way to say so.
|
||||||
|
[Loc.Error] out of this call is what [Dev.serve]'s guard answers as a
|
||||||
|
reply. *)
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro-idle.flan" ~all:false ti
|
||||||
|
"(s/spin)"
|
||||||
|
with
|
||||||
|
| x ->
|
||||||
|
if Form.to_source x.Session.xafter <> "(s/spin)" then
|
||||||
|
fail "one step of a macro that does not settle answered %S"
|
||||||
|
(Form.to_source x.Session.xafter)
|
||||||
|
else if x.Session.xchanged then
|
||||||
|
fail "one step of a macro that expands to itself reported a change"
|
||||||
|
else if x.Session.xmacro <> Some "s/spin" then
|
||||||
|
fail "one step of a macro that does not settle did not name it"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "one step of a macro that does not settle was refused: %s" m);
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro-idle.flan" ~all:true ti
|
||||||
|
"(s/spin)"
|
||||||
|
with
|
||||||
|
| _ -> fail "a macro that does not settle was expanded all the way"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
if not (has m "did not settle") then
|
||||||
|
fail "expanding a macro that does not settle all the way said %S" m);
|
||||||
|
(* And the session survives the refusal, as it does after an evaluation that
|
||||||
|
was refused. *)
|
||||||
|
(match Session.macroexpand ~origin:"programs/pkg-macro-idle.flan" ~all:true ti
|
||||||
|
"(mac/twice 21)"
|
||||||
|
with
|
||||||
|
| x ->
|
||||||
|
if Form.to_source x.Session.xafter <> "(+ 21 21)" then
|
||||||
|
fail "after a refused expansion, (mac/twice 21) expanded to %S"
|
||||||
|
(Form.to_source x.Session.xafter)
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "a session that refused an expansion could not expand afterwards: %s" m);
|
||||||
|
|
||||||
(* A form typed into a file that is *imported as a package* has to be
|
(* A form typed into a file that is *imported as a package* has to be
|
||||||
qualified the way the import qualified it, or it splices as a brand-new
|
qualified the way the import qualified it, or it splices as a brand-new
|
||||||
unrelated name: the evaluation reports success and the running program
|
unrelated name: the evaluation reports success and the running program
|
||||||
|
|||||||
@ -1679,6 +1679,7 @@ something surprising.</p>
|
|||||||
<tr><td><kbd>C-c C-b</kbd></td><td>a stopped program: the condition, the restarts, the stack</td></tr>
|
<tr><td><kbd>C-c C-b</kbd></td><td>a stopped program: the condition, the restarts, the stack</td></tr>
|
||||||
<tr><td><kbd>C-c C-M-b</kbd></td><td>the same restarts, as a one-key prompt</td></tr>
|
<tr><td><kbd>C-c C-M-b</kbd></td><td>the same restarts, as a one-key prompt</td></tr>
|
||||||
<tr><td><kbd>C-c C-i</kbd></td><td>inspect a value, navigating into its fields</td></tr>
|
<tr><td><kbd>C-c C-i</kbd></td><td>inspect a value, navigating into its fields</td></tr>
|
||||||
|
<tr><td><kbd>C-c C-m</kbd></td><td>what the macro call at point expands to, one step; <kbd>C-u</kbd> first for all the way</td></tr>
|
||||||
<tr><td><kbd>C-c C-a</kbd></td><td>disassemble a function; <kbd>C-u</kbd> first for its LLVM IR</td></tr>
|
<tr><td><kbd>C-c C-a</kbd></td><td>disassemble a function; <kbd>C-u</kbd> first for its LLVM IR</td></tr>
|
||||||
<tr><td><kbd>C-c C-g</kbd></td><td>debug under lldb, through dape — bound only once <code>flan-dape.el</code> is loaded, so <code>flan-mode</code> works without dape installed</td></tr>
|
<tr><td><kbd>C-c C-g</kbd></td><td>debug under lldb, through dape — bound only once <code>flan-dape.el</code> is loaded, so <code>flan-mode</code> works without dape installed</td></tr>
|
||||||
<tr><td><kbd>C-c C-d</kbd></td><td>what the running program currently defines</td></tr>
|
<tr><td><kbd>C-c C-d</kbd></td><td>what the running program currently defines</td></tr>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user