An error says which macro it is really about

The provenance rides on the location, not on the form, because the location
is the thing that already travels: Expand.unmarshal stamps the call site onto
every node a macro answers with, and that stamp goes on through the AST and
the typed IR untouched. Tagging it there means an error raised anywhere
downstream can name the macro with no field added to Form, to Ast or to Tast.

Outermost wins. The macro the author wrote is the one worth naming, not
whatever it expanded into on the way down.

The honest limit, since it would otherwise read as a claim: a macro's
expansion has no source of its own to point at, so the note lands on the call
site along with the error. What it buys is the reader knowing the code being
refused is not the code they wrote.
This commit is contained in:
Joseph Ferano 2026-09-13 07:58:25 +07:00
parent 2efed1630f
commit e8aeb89282
2 changed files with 35 additions and 3 deletions

View File

@ -20,11 +20,28 @@ type t = {
col : int; (* 1-based *)
eline : int; (* 1-based, exclusive end *)
ecol : int;
(* The macro whose expansion produced whatever is at this position, if one
did. It rides on the location rather than on the form because the location
is the thing that already travels: [Expand.unmarshal] stamps the call
site onto every node a macro answers with, and that stamp goes on through
the AST and the typed IR untouched. Tagging it here means an error raised
anywhere downstream can say which macro it is really about, with no field
added to Form, to Ast or to Tast. *)
macro : string option;
}
let make file line col = { file; line; col; eline = line; ecol = col }
let make file line col =
{ file; line; col; eline = line; ecol = col; macro = None }
let unknown = make "<unknown>" 0 0
(** Tag a location as coming out of [name]'s expansion, unless it already names
a macro. Already-tagged wins because the tag is applied outermost-last: the
macro the author actually wrote is the one worth naming, not whatever it
expanded into on the way. *)
let from_macro name (t : t) =
match t.macro with None -> { t with macro = Some name } | Some _ -> t
(** [upto start stop] is [start] widened to end where [stop] begins. A [stop]
that is not after [start], or is in another file, leaves it alone: a span
that runs backwards would draw nonsense. *)
@ -124,6 +141,14 @@ let sort_notes (d : diag) =
let note ?(sev = Info) loc msg = { nmsg = msg; nloc = loc; nsev = sev }
let diag ?(kind = "error") ?(notes = []) ?expansion loc msg =
(* The location already knows whether it came out of a macro, so an error
does not have to be raised anywhere special to say so. An explicit
[expansion] still wins, for the one caller that knows better. *)
let expansion =
match expansion with
| Some _ as e -> e
| None -> (match loc.macro with Some m -> Some (m, loc) | None -> None)
in
sort_notes { kind; dloc = loc; dmsg = msg; notes; expansion }
let raise_diag d = raise (Error (sort_notes d))

View File

@ -176,7 +176,12 @@ let rec expand_form (l : loaded) (f : Form.t) : Form.t =
match f.Form.v with
| Form.List ({ Form.v = Form.Sym n; _ } :: args) when List.mem_assoc n l.fns ->
let args = List.map (expand_form l) args in
settle l n loc (Expand.call ~loc (List.assoc n l.fns) args) fuel
(* The call site, tagged with the macro it is a call to. [Expand.unmarshal]
stamps this onto every node the macro answers with, so from here down
every form it produced knows where it came from and an error on one of
them can say so. *)
let from = Loc.from_macro n loc in
settle l n loc (Expand.call ~loc:from (List.assoc n l.fns) args) fuel
| Form.List xs -> Form.make (Form.List (List.map (expand_form l) xs)) loc
| Form.Vec xs -> Form.make (Form.Vec (List.map (expand_form l) xs)) loc
| Form.Map xs -> Form.make (Form.Map (List.map (expand_form l) xs)) loc
@ -193,7 +198,9 @@ and settle l first loc (f : Form.t) left =
first fuel
else begin
let args = List.map (expand_form l) args in
settle l first loc (Expand.call ~loc (List.assoc m l.fns) args) (left - 1)
let from = Loc.from_macro m loc in
settle l first loc (Expand.call ~loc:from (List.assoc m l.fns) args)
(left - 1)
end
(* Settled at the head. The rest of it may still hold macro calls — a cond
expands to an if whose else-branch is another cond so the ordinary walk