The pause wrap takes the expanded expression's location

C-u C-x C-e was never tried on a macro call. Ast.pause_call takes the
expanded loc, which Loc.from_macro has stamped -- it sets a name and
leaves file, line and column the call site's, so the frame the break
loop reports is the line the reader is looking at. Asserted rather than
argued.

Also: the ring rule stated generally (refused at the parse of whichever
file first has both members in scope, always before a session exists),
and the declaration refusal's sentence made build-neutral, since the arm
fires in an ordinary file parse too.
This commit is contained in:
Joseph Ferano 2026-09-13 17:58:36 +07:00
parent 6bc4726ddd
commit 8ca63a7717
4 changed files with 33 additions and 7 deletions

View File

@ -700,16 +700,19 @@ expression can do.
inside the daemon, and a hang there wedges the editor with the program still on screen. The **spin** — a macro that
expands into a call to itself and does not get smaller — fires on this path, bounded at 200 rounds, and comes back as
a `Loc.Error` that `Dev.eval_expr` already answers as an error reply. The **ring** never reaches this path, and
finding out why was worth the trip: a ring is refused while *the package it lives in* is parsed, because that file's
own bodies name each other, so no importer of a ring can be loaded and no session over one can exist. The refusal is
in front of the path rather than on it, which is the stronger place for it; `test_session` asserts it at
finding out why was worth the trip: a ring is refused at the parse of whichever file first has both of
its members in scope — the package itself when the ring is internal to one, the importer when it is not — and that is
always a file `Load` reads before any session exists. So no program holding a ring can be loaded and no session over
one can be created. The refusal is in front of the path rather than on it, which is the stronger place for it; `test_session` asserts it at
`Session.create` so that moving the check later shows up as a failing test and not as a wedged daemon.
`test/programs/pkg-macro-idle.flan` is the fixture the spin needs — it imports and calls nothing, so the session is
created without expanding anything and the expression is the first thing that ever expands the macro.
Expansion happens **before** the thunk is built, so the three-way `` `Value | `Stopped | `Timeout `` wait in
`Dev.eval_expr` is untouched: a cold macro module costs its ~300ms before that 5-second clock starts, and `before` is
sampled after `Session.eval_expr` has returned. `temps` is not reset on this path, unlike `decl`'s — a declaration is
sampled after `Session.eval_expr` has returned. `C-u C-x C-e` wraps the **expanded** expression, so `Ast.pause_call`
takes a macro-stamped location — and `Loc.from_macro` sets a name and leaves file, line and column the call site's, so
the frame the break loop reports is still the line the reader is looking at. `temps` is not reset on this path, unlike `decl`'s — a declaration is
a fresh top level, an expression is evaluated into a session that has been handing out temporaries all along.
Still missing: a package-private marker for anything other than `main`, which is why `rl/get-color-raw` is callable.

11
NEXT.md
View File

@ -117,6 +117,17 @@ on this path; the ring never can, because a ring is refused while its own packag
expands is the prelude's macros and the imported packages' — not the file's own, which is a session
limit `C-c C-c` shares and which is now pinned rather than fixed.
Two things left behind it, neither this lane's. **A session expands no macro the buffer itself
declares** — `Macro.program` scans the forms it is handed, and an evaluation hands it the one form
that was sent, so `(tenfold 7)` is an unknown name at `C-x C-e` and `(defn x [] i32 (tenfold 7))` is
one at `C-c C-c`. Fixing it means the session keeping its file's own `defmacro`s the way it keeps the
imports', which is a `Session` change and a decision about what a `defmacro` typed at the REPL does.
And **`Dev.eval_expr` catches only `Loc.Error`** around `Session.eval_expr`: the two non-termination
refusals are that, so they answer as replies, but `C-x C-e` can now reach `Build.macro_module` for
the first time, and a macro that fails to *compile* raises `Failure` from the clang driver or
`Loc.Errors` from the checker. Neither is caught, and an unhandled exception there is a dead session.
Pre-existing for `C-c C-c`, which reaches the same builder; `lib/dev.ml` is another lane's file.
Two things found while finishing it. `Session` held the imported macro set but **replaced** it on
every evaluation, and the one form `C-c C-c` sends carries no import — so a package macro worked on
the build and was an unknown name on the first reload. It unions now, and `test_session` drives two

View File

@ -458,9 +458,9 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
| Sym ("defmacro" | "defn" | "defvar" | "defconst" | "defstruct" | "defunion"
| "defenum" | "defalias" | "import" as name) ->
fail f
"%s is a top-level declaration, not an expression. A macro may answer \
with one only where a declaration is expected, so this cannot come back \
from evaluating an expression" name
"%s is a top-level declaration, not an expression. A quasiquoted one is \
a value and a macro may answer with it; an evaluated one is not a thing \
anything can do" name
(* Recognised, deliberately unimplemented. Rejected rather than left to fall
through to Call, where they would parse and mean nothing. *)

View File

@ -323,6 +323,18 @@ let () =
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a prelude macro through C-x C-e: %s" m);
(* And with a pause on it, which is C-u C-x C-e. [Ast.pause_call] takes the
*expanded* expression's location, and expansion stamps every node a macro
answered with [Loc.from_macro] which sets a name and leaves the file,
line and column the call site's, so the frame the break loop reports is
still the line the reader is looking at. Untested until now because every
pause case was an expression no macro touched. *)
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" ~pause:true tm
"(mac/twice 21)" with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "C-u C-x C-e on a macro call: %s" m);
(* Not the file's own macro, and deliberately not: [Macro.program] collects
those by scanning the forms it is handed, and the forms handed to an
evaluation are the one thing that was sent. That limit is the session's