cond is a special form again because the prelude relies on it, and until is a prelude macro because only programs use it
This commit is contained in:
parent
60ef7b2e06
commit
08545c757f
10
TODO.org
10
TODO.org
@ -96,14 +96,10 @@ not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk".
|
||||
sees the whole file and can. The fix is collecting from the expanded forms,
|
||||
which =Load.program= does not hand back.
|
||||
|
||||
** DONE until and cond are prelude macros; when and dotimes stay special forms
|
||||
** DONE A form the prelude relies on is built in; a form only programs use is a macro
|
||||
CLOSED: [2026-09-25]
|
||||
=cond= builds its chain through the prelude function =cond-chain= and refuses
|
||||
through =compile-error=; =until= keeps its label. Prelude functions that call
|
||||
=cond= drop out of the macro module, so =into-wrap= is written with =if=. The
|
||||
macro module now holds only the macros the forms reach. =when= and =dotimes= stay:
|
||||
the prelude's form-building functions use them. docs/BUILT.md, "cond and until
|
||||
followed".
|
||||
=cond=, =when= and =dotimes= are special forms in parse.ml; =inc=, =++=, =into=,
|
||||
=unless=, =until= and =comment= are prelude macros.
|
||||
|
||||
** DONE Macros are imported from a package
|
||||
The old refusal claimed collecting a package's macros needed a second import
|
||||
|
||||
@ -3592,27 +3592,13 @@ reason `-linkall` is not optional. Say plainly what that coverage is not: nothin
|
||||
before this landed, so `macro-unless.flan` is a test written after the feature. The corpus written before it is
|
||||
`sand.flan` and `web/examples/control.flan`, and both compile unchanged.
|
||||
|
||||
### `cond` and `until` followed, and `when` and `dotimes` stay
|
||||
### The line between a special form and a macro
|
||||
|
||||
`cond` and `until` are prelude macros now. `until` peels an optional leading label and answers
|
||||
`(while :label (not test) body ...)`; `cond` hands its clauses to the prelude function `cond-chain`, which builds the
|
||||
same nested `if` the parser used to, with `(do)` as the innermost else. A function and not a self-call, because
|
||||
`(cond)` is refused while the empty tail of a longer `cond` is not, and a recursive expansion sees both as `(cond)`.
|
||||
Their refusals are `compile-error` calls: the caret is the whole call rather than the bad clause, since a node the
|
||||
macro builds takes the call's location.
|
||||
|
||||
`cond` was not free to move, whatever the old note said: six prelude functions used it. A prelude function that calls
|
||||
a macro is dropped from the build of the macro module (`Macro.reduce`), which is harmless for `sign-f32`, the rune
|
||||
decoders and `format-f64` — no macro calls them — and fatal for `into-wrap`, which the `into` macro calls. So
|
||||
`into-wrap` and `cond-chain` are written with `if`. `when` and `dotimes` stay special forms: the prelude uses them in
|
||||
dozens of functions, the form-building functions among them, and every one would drop out of the module that runs
|
||||
macros.
|
||||
|
||||
Moving `cond` also made nearly every file and package need a macro module, and that exposed the rule that a macro
|
||||
body may call no function of its own file. `vendor/edn` parses its own files calling `cond` and never `defedn`, yet
|
||||
`defedn`'s body calls the package's `refuse`, so compiling every macro the package declares failed. `Macro.loaded_for`
|
||||
now compiles only the macros the forms reach: a name anywhere outside a `defmacro` (a quasiquoted call arrives as a
|
||||
string, and counts), a head called inside a `defmacro` body, and whatever the reached macros' bodies name in turn.
|
||||
A form the prelude itself relies on is built into the parser: `cond`, `when` and `dotimes`. A form only programs use
|
||||
is a prelude macro: `inc`, `++`, `into`, `unless`, `until` and `comment`. The reason is `Macro.reduce`: a prelude
|
||||
function that calls a macro is left out of the module that runs macros, so a form the prelude's own functions use
|
||||
cannot be a macro without taking those functions away from every macro body. `until` peels an optional leading label
|
||||
and answers `(while :label (not test) body ...)`.
|
||||
|
||||
## `break` and `continue`, and the rule that replaced a blanket refusal
|
||||
|
||||
|
||||
@ -146,9 +146,9 @@ of \=`, ~ and ~@, and the sigils are not symbols for a keyword rule to reach.
|
||||
recognises only in order to refuse them, and drawing those as keywords would
|
||||
advertise four forms that cannot be used.
|
||||
|
||||
`cond' and `until' are macros in the prelude and not heads of `Parse.form'.
|
||||
They stay here because they are control flow a reader takes for `if' and
|
||||
`while', which is what this face says.")
|
||||
`until' is a macro in the prelude and not a head of `Parse.form'. It stays
|
||||
here because it is control flow a reader takes for `while', which is what this
|
||||
face says.")
|
||||
|
||||
(defconst flan--builtins
|
||||
'(;; arithmetic, comparison, bits
|
||||
|
||||
@ -405,9 +405,8 @@
|
||||
("(handler-case (go) [(E [c] 1)])" "handler-case"
|
||||
font-lock-keyword-face "handler-case")
|
||||
;; Special forms the old list had never heard of.
|
||||
;; cond and until are prelude macros, drawn as the control
|
||||
;; flow they are.
|
||||
("(cond (= a 1) 2)" "cond" font-lock-keyword-face "cond")
|
||||
;; until is a prelude macro, drawn as the control flow it is.
|
||||
("(until (> i 3) (go))" "until" font-lock-keyword-face "until")
|
||||
("(and a b)" "and" font-lock-keyword-face "and")
|
||||
("(handler-bind [(E [c] 1)] (go))" "handler-bind"
|
||||
|
||||
59
lib/macro.ml
59
lib/macro.ml
@ -549,62 +549,11 @@ let loaded_for (forms : Form.t list) : loaded option =
|
||||
List.filter (fun (n, _) -> not (List.mem_assoc n mine)) imported
|
||||
in
|
||||
let mine = imported @ mine in
|
||||
(* Only the macros these forms can reach. A macro body may call prelude
|
||||
functions and other macros and nothing else, so a file or package whose
|
||||
macro calls one of its own functions cannot have that macro compiled —
|
||||
and it need not be, when nothing here calls it. That is the common case
|
||||
for a package parsing its own files: they call [cond], a prelude macro,
|
||||
and never the macros the package exports.
|
||||
|
||||
Reached means named anywhere: as a symbol, or as the string a
|
||||
quasiquoted name desugars to, since a macro that answers a call to
|
||||
another needs that one in the module too. Over-reaching only compiles a
|
||||
macro that would have been compiled anyway. *)
|
||||
let mine =
|
||||
let rec atoms (f : Form.t) acc =
|
||||
match f.Form.v with
|
||||
| Form.Sym n | Form.Str n -> n :: acc
|
||||
| Form.List xs | Form.Vec xs | Form.Map xs ->
|
||||
List.fold_left (fun a x -> atoms x a) acc xs
|
||||
| _ -> acc
|
||||
in
|
||||
(* The heads a form calls. A macro body that calls another macro for
|
||||
real is expanded by the same walk as everything else, whether or not
|
||||
the macro it belongs to is ever called, so what it calls is reached. *)
|
||||
let rec heads (f : Form.t) acc =
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym h; _ } :: rest) ->
|
||||
List.fold_left (fun a x -> heads x a) (h :: acc) rest
|
||||
| Form.List xs | Form.Vec xs | Form.Map xs ->
|
||||
List.fold_left (fun a x -> heads x a) acc xs
|
||||
| _ -> acc
|
||||
in
|
||||
let want = Hashtbl.create 16 in
|
||||
List.iter
|
||||
(fun f ->
|
||||
let names = if macro_name f = None then atoms f [] else heads f [] in
|
||||
List.iter (fun n -> Hashtbl.replace want n ()) names)
|
||||
forms;
|
||||
let changed = ref true in
|
||||
let taken = Hashtbl.create 16 in
|
||||
while !changed do
|
||||
changed := false;
|
||||
List.iter
|
||||
(fun (n, f) ->
|
||||
if Hashtbl.mem want n && not (Hashtbl.mem taken n) then begin
|
||||
Hashtbl.replace taken n ();
|
||||
changed := true;
|
||||
List.iter (fun a -> Hashtbl.replace want a ()) (atoms f [])
|
||||
end)
|
||||
mine
|
||||
done;
|
||||
List.filter (fun (n, _) -> Hashtbl.mem taken n) mine
|
||||
in
|
||||
let all = prelude @ List.map fst mine in
|
||||
(* A file that calls no macro costs one scan and no module. Few do, since
|
||||
cond is a macro; for a file with no macros of its own the module is the
|
||||
prelude's alone, one cached .so shared by every such file, so the usual
|
||||
cost is a stat and a dlopen. *)
|
||||
(* The common case by a wide margin, and the reason a build that uses 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
|
||||
for the prelude's macros and pay a clang driver to answer nothing. *)
|
||||
if all = [] || not (List.exists (names_macro all) forms) then None
|
||||
else begin
|
||||
let extra = rounds ~prelude mine in
|
||||
|
||||
20
lib/parse.ml
20
lib/parse.ml
@ -361,10 +361,9 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
||||
| [ c; t; e ] -> mk (Ast.If (expr c, expr t, Some (expr e)))
|
||||
| _ -> fail f "if is (if test then) or (if test then else)")
|
||||
|
||||
(* Sugar, desugared here. [cond] and [until] are prelude macros; [when] and
|
||||
[dotimes] stay special forms because the prelude uses them in the
|
||||
functions the macro module is built from, and a function calling a macro
|
||||
cannot be compiled into that module. *)
|
||||
(* Sugar, desugared here. [cond], [when] and [dotimes] are special forms
|
||||
because the prelude relies on them; a form only programs use, such as
|
||||
[until] or [unless], is a prelude macro. *)
|
||||
(* An empty body is allowed, and becomes the same [Ast.Do []] that [(do)]
|
||||
already means. There was never a reason for the restriction: [(when test)]
|
||||
is a guard whose consequent has not been written yet, which is a state a
|
||||
@ -377,6 +376,8 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
||||
mk (Ast.If (expr c, { Ast.e = Ast.Do (body_of body); loc = f.loc }, None))
|
||||
| _ -> fail f "when is (when test body ...)")
|
||||
|
||||
| Sym "cond" -> cond f args
|
||||
|
||||
(* Short-circuiting, so they cannot be ordinary calls. *)
|
||||
| Sym "and" -> shortcircuit f args ~is_and:true
|
||||
| Sym "or" -> shortcircuit f args ~is_and:false
|
||||
@ -1075,6 +1076,17 @@ and struct_fields f (items : Form.t list) : (string * Ast.expr) list =
|
||||
in
|
||||
ignore f; go items
|
||||
|
||||
and cond f (args : Form.t list) : Ast.expr =
|
||||
let rec go = function
|
||||
| [] -> { Ast.e = Ast.Do []; loc = f.loc } (* no clause matched: Unit *)
|
||||
| { v = Kw "else"; _ } :: body :: _ -> expr body
|
||||
| test :: body :: rest ->
|
||||
{ Ast.e = Ast.If (expr test, expr body, Some (go rest)); loc = f.loc }
|
||||
| [ odd ] -> Loc.fail odd.loc "cond clause %s has no body"
|
||||
(Form.to_string odd)
|
||||
in
|
||||
if args = [] then Loc.fail f.loc "cond needs at least one clause" else go args
|
||||
|
||||
(* Every test here is an [if]'s condition, so a dyn operand is truthy-tested
|
||||
(check.ml's check_truthy) exactly the way a bare [if]'s is, for both
|
||||
[and] and [or]. The *answer* is the operand that decided the form, which
|
||||
|
||||
@ -2185,64 +2185,8 @@ let source = {flan|
|
||||
`(unless-takes-a-test)
|
||||
`(if (not ~(at args 0)) (do ~@(form-rest args 1)))))
|
||||
|
||||
;; ── cond and until ────────────────────────────────────────────────────
|
||||
;; ── until ─────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; (cond a 1 b 2 :else 3) is (if a 1 (if b 2 3)). With no :else the innermost
|
||||
;; else is (do), so a cond that matches nothing answers (). A test of :else
|
||||
;; ends the chain and anything written after its body is not read.
|
||||
;;
|
||||
;; The chain is built by a function and not by the macro calling itself,
|
||||
;; because (cond) with no clauses is refused while the empty tail of a longer
|
||||
;; cond is not, and a recursive expansion cannot tell the two apart. The
|
||||
;; function uses if and never cond: `cond` expands through it, so it is
|
||||
;; compiled into the module that expands macros, which a function calling a
|
||||
;; macro cannot be.
|
||||
;;
|
||||
;; A prelude function that does call cond is dropped from that module, and so
|
||||
;; cannot be called from a macro body: sign-f32, the rune decoders and encoders,
|
||||
;; and format-f64 (which calls clamp). None is a thing a macro reaches for.
|
||||
(defn cond-no-body [c Form] Form
|
||||
(let [v (vec-new u8)]
|
||||
(match c
|
||||
(Form.Sym s)
|
||||
(do (append (addr v) (bytes-view "cond clause "))
|
||||
(append (addr v) (bytes-view s))
|
||||
(append (addr v) (bytes-view " has no body")))
|
||||
(Form.Kw k)
|
||||
(do (append (addr v) (bytes-view "cond clause :"))
|
||||
(append (addr v) (bytes-view k))
|
||||
(append (addr v) (bytes-view " has no body")))
|
||||
_ (append (addr v) (bytes-view "the last cond clause has no body")))
|
||||
(append (addr v)
|
||||
(bytes-view ": every test is followed by the value it chooses"))
|
||||
`(compile-error ~(Form.Str {.s (string (slice v))}))))
|
||||
|
||||
(defn cond-chain [cs [Form]] Form
|
||||
(let [n (length cs)
|
||||
end n
|
||||
acc `(do)
|
||||
i 0]
|
||||
(while (< i n)
|
||||
(if (= (+ i 1) n)
|
||||
(return (cond-no-body (at cs i)))
|
||||
(if (match (at cs i)
|
||||
(Form.Kw k) (bytes=? (bytes-view k) (bytes-view "else"))
|
||||
_ false)
|
||||
(do (set end i)
|
||||
(set acc (at cs (+ i 1)))
|
||||
(break))
|
||||
(set i (+ i 2)))))
|
||||
(let [j end]
|
||||
(while (> j 0)
|
||||
(set j (- j 2))
|
||||
(set acc `(if ~(at cs j) ~(at cs (+ j 1)) ~acc)))
|
||||
acc)))
|
||||
|
||||
(defmacro cond [& args]
|
||||
(if (= (length args) 0)
|
||||
`(compile-error "cond needs at least one clause: (cond test body ...)")
|
||||
(cond-chain args)))
|
||||
|
||||
;; (until test body ...) is (while (not test) body ...), and a label written
|
||||
;; first stays first: (until :outer test body ...).
|
||||
(defmacro until [& args]
|
||||
@ -2394,14 +2338,10 @@ let source = {flan|
|
||||
`(into-transform-is-map-or-filter-of-one-function ~t)
|
||||
(let [head (at items 0)
|
||||
f (at items 1)]
|
||||
;; if and not cond: cond is a macro, and the `into` macro calls
|
||||
;; this function, so it has to be compiled into the module that
|
||||
;; expands macros — which a function calling a macro cannot be.
|
||||
(if (form-sym? head "map")
|
||||
(recur (- k 1) `(let [~x (~f ~x)] ~body))
|
||||
(if (form-sym? head "filter")
|
||||
(recur (- k 1) `(when (~f ~x) ~body))
|
||||
`(into-transform-is-map-or-filter ~t)))))))))
|
||||
(cond
|
||||
(form-sym? head "map") (recur (- k 1) `(let [~x (~f ~x)] ~body))
|
||||
(form-sym? head "filter") (recur (- k 1) `(when (~f ~x) ~body))
|
||||
:else `(into-transform-is-map-or-filter ~t))))))))
|
||||
|
||||
;; The items of a list form, and the empty slice for anything else — a
|
||||
;; non-list transform falls into the arity complaint above rather than needing
|
||||
|
||||
@ -783,6 +783,7 @@ let () =
|
||||
(* ── Malformed syntax is caught with a location ────────────────── *)
|
||||
parse_rejects "odd let bindings" "(let [a])";
|
||||
parse_rejects "odd field pairs" "(defstruct S [a])";
|
||||
parse_rejects "cond without body" "(cond a)";
|
||||
parse_rejects "unknown top form" "(nope x)";
|
||||
parse_rejects "break takes only a label" "(defn f [] () (break 1))"
|
||||
~needle:"break is (break) or (break :label)";
|
||||
@ -1285,24 +1286,10 @@ let () =
|
||||
"(defn f [x] () (when x 0))";
|
||||
accepts "dyn cond accepts a non-bool dyn condition"
|
||||
"(defn f [x] dyn (cond x 1 :else 2))";
|
||||
(* cond and until are prelude macros, and their refusals are compile-error
|
||||
calls in the expansion: the caret is on the call and the sentence is the
|
||||
macro's. *)
|
||||
rejects_check "cond with no clauses" "(defn f [] () (cond))"
|
||||
~needle:"cond needs at least one clause";
|
||||
rejects_check "a cond test with no body" "(defn f [a bool] () (cond a))"
|
||||
~needle:"cond clause a has no body";
|
||||
rejects_check "a cond test that is not a name, with no body"
|
||||
"(defn f [a bool] () (cond a 1 (= 1 2)))"
|
||||
~needle:"the last cond clause has no body";
|
||||
rejects_check "a cond :else with no body" "(defn f [a bool] () (cond a 1 :else))"
|
||||
~needle:"cond clause :else has no body";
|
||||
(* until is a prelude macro, and its refusal is a compile-error call in the
|
||||
expansion. *)
|
||||
rejects_check "until with no test" "(defn f [] () (until :outer))"
|
||||
~needle:"until is (until test body ...)";
|
||||
accepts "anything after cond's :else body is not read"
|
||||
"(defn f [a bool] i32 (cond a 1 :else 2 junk))";
|
||||
accepts "a cond that matches nothing answers ()"
|
||||
"(defn f [a bool] () (cond a (println \"a\")))";
|
||||
accepts "until keeps its label for break"
|
||||
"(defn f [] () (let [i 0] (until :outer (> i 3) (set i (+ i 1)) (break :outer))))";
|
||||
accepts "dyn and accepts a non-bool dyn operand"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user