diff --git a/BUILT.md b/BUILT.md index 5fecf56..9ce0d18 100644 --- a/BUILT.md +++ b/BUILT.md @@ -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. diff --git a/NEXT.md b/NEXT.md index 183122e..86b91a5 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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 diff --git a/lib/parse.ml b/lib/parse.ml index 24c0ca8..e2518e7 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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. *) diff --git a/test/test_session.ml b/test/test_session.ml index 458b851..486a526 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -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