Macros: source that writes source
the system macros, seeing an expansion, and macro libraries of your own
Table of Contents
A macro is a name with the macro mark < at its end; u_se<, which
imports a library, is the one met first. A call is written like a
dyadic function with a string on each side, "left" n_ame< "right",
and before the program is checked the macro replaces the call with
source it writes from the two texts. Whatever a macro writes is then
read, type-checked and run like code you typed. Every block below is
run by xetal through ob-xetal, and its result recorded under it.
The system macros
The system macros are always there; no import is needed. They are
written in X_eTaL like any macro library, in lib/System.xtlm (built
into xetal and loaded before every file), each with a comment saying
what it does and an example. A side of a call that takes nothing is
written @: @ l_ine< @. The first three choose and repeat code.
i_f<: one of two expressions
"c" i_f< "a; b" is the value of a when c holds, else of b. The
condition is evaluated when the program runs, and only the expression
chosen is evaluated, so the 1 / 0 below never is:
x ← -3 "x > 0" i̲f< "1; -1" 10 × "x > 0" i̲f< "1; -1" "x < 0" i̲f< "0; 1 / 0" ⍝ typed: ⍝ x := -3 ⍝ "x > 0" i_f< "1; -1" ⍝ 10 * "x > 0" i_f< "1; -1" ⍝ "x < 0" i_f< "0; 1 / 0"
-1 -10 0.0
The macro writes a guard in a lambda of no argument, applied at once;
inside an expression it is parenthesized. xetal expand shows the
program after the macros have written their source:
$ xetal expand -e '"x > 0" i_f< "1; -1"'
{ @ -> (x > 0) ? 1; -1 } @
$ xetal expand -e '10 * "x > 0" i_f< "1; -1"'
10 * ({ @ -> (x > 0) ? 1; -1 } @)
Both expressions are checked and must have one type, which is why the
third line gives 0.0: 1 / 0 is a Float, so the 0 is one too.
u_nless<: statements run unless a condition holds
"c" u_nless< "b" runs the statements b unless c holds. It is
there for what b does (printing, here), so its own value is @
(Unit, the value that stands for nothing), shown like any other value
of a statement:
n ← 4 "n = 0" u̲nless< "p_rint! 100 / n" ⍝ typed: ⍝ n := 4 ⍝ "n = 0" u_nless< "p_rint! 100 / n"
25.0 @
It writes { @ -> (n = 0) ? @; p_rint! 100 / n; @ } @.
e_ach<: a statement per word
"w1 w2" e_ach< "template" writes one copy of the template for each
word on its left, with $w in it replaced by the word. A family of
definitions is the use a function cannot serve, since a function
cannot make names:
"m_ax m_in" e̲ach< "u:$w/ := { '$w r_/ _r }" ᵘm̲ax/ 3 1 4 1 5 ᵘm̲in/ 3 1 4 1 5 ⍝ typed: ⍝ "m_ax m_in" e_ach< "u:$w/ := { '$w r_/ _r }" ⍝ u:m_ax/ 3 1 4 1 5 ⍝ u:m_in/ 3 1 4 1 5
5 1
It wrote u:m_ax/ := { 'm_ax r_/ _r } and u:m_in/ := { 'm_in r_/ _r },
one per line: e_ach< writes statements, so it stands as a statement
of its own.
f_ormat<: text with values in it
@ f_ormat< "text {expr}" puts the value of each {expr}, any
expression, into the text, as the function f_ormat writes it (a Bool
as 0 or 1); {{ and }} are braces (Rust's format!):
v ← 3 1 2 @ f̲ormat< "v = {v}, its sum {'+ r_/ v}, sorted? {v m_atch s_ort v}, {{braces}}" ⍝ typed: ⍝ v := 3 1 2 ⍝ @ f_ormat< "v = {v}, its sum {'+ r_/ v}, sorted? {v m_atch s_ort v}, {{braces}}"
v = 3 1 2, its sum 6, sorted? 0, {braces}
It writes "v = " c_at (f_ormat (v)) c_at .... An unclosed {, an
empty {} or a lone } is an error before the program runs, and a type
error inside a hole is reported where it was written in the string.
d_bg< and a_ssert<: looking and checking
@ d_bg< "expr" gives the value of expr after writing it, as written,
with its value and its place, on standard error (Rust's dbg!).
"cond" a_ssert< "message" (or @ for no message) reports a
condition that does not hold, as written, and the program goes on;
unlike Rust's assert!, it never stops the program. Standard error
shows:
$ xetal eval -e 'x := 6 y := 1 + @ d_bg< "x * 2" y' [-e:2] x * 2 = 12 13 $ xetal eval -e 'x := 6 "x > 10" a_ssert< "x is big"' assertion failed: x > 10 (x is big) [-e:2] @
p_anic<: stopping
@ p_anic< "message {expr}" stops the program with error[panic] and
the message, formatted as f_ormat< does, at the call. It stands
where any value may, an Int or a text:
$ xetal eval -e 'u:s_ize := { k -> k > 3 ? @ p_anic< "bad grid size {k}"; k * 2 }
u:s_ize 2
u:s_ize 5'
4
error[panic]: bad grid size 5 at 26..55
What the compiler knows
Some macros give what only the compiler knows, through hooks no ordinary function can call:
| Call | Gives |
|---|---|
@ l_ine< @ |
the line the call is written on |
@ f_ile< @ |
the file it is written in (-e for command-line text) |
@ i_nclude< "data.txt" |
the text of a file beside it, as a string (Rust's include_str!) |
@ c_fg< "web" |
1 when the fact holds: the platform, cli or web, or a flag set with xetal --cfg NAME |
"code" e_rror< "message" |
a compile error at the call (Rust's compile_error!) |
@ c̲fg< "cli" @ c̲fg< "web" ⍝ typed: ⍝ @ c_fg< "cli" ⍝ @ c_fg< "web"
1 0
A macro of yours can write e_rror< into its text to refuse a call:
the error is reported at your call, with your code and message.
Macros of your own
A macro library is a file ending in .xtlm. It defines its macros as
m:n_ame<, each an ordinary function of the two texts, left and right,
that gives the text replacing the call. The standard library Macros
(lib/Macros.xtlm) is a small example:
⍝# A small macro library, an example of macros of your own (MC10). A ⍝# macro is an ordinary function of the source text written on each ⍝# side of its call; the text it gives replaces the call, then is read ⍝# as code. Import it with an alias of your own, "x:" u_se< "Macros", ⍝# and see what a program becomes with xetal expand FILE. ⍝## Statements and definitions ⍝# Run the statements on the right when the condition on the left ⍝# holds; the value is @ either way (the other way round from the ⍝# system macro u_nless<). ⍝# >> "x:" u_se< "Macros" ⍝# >> "1 < 2" x:w_hen< "p_rint! 7" ⍝# 7 ⍝# @ ᵐw̲hen< ← { cond body → "{ @ -> (n_ot " c̲at cond c̲at ") ? @; " c̲at body c̲at "; @ } @" } ⍝# Define the function named on the left (in u:) as the lambda body on ⍝# the right, its name written as text. ⍝# >> "x:" u_se< "Macros" ⍝# >> "s_quare" x:d_ef< "_r * _r" ⍝# >> u:s_quare 7 ⍝# 49 ᵐd̲ef< ← { name body → "u:" c̲at name c̲at " := { " c̲at body c̲at " }" } ⍝## Checks ⍝# The line "ok: label" when the condition on the left holds, else ⍝# "FAIL: label (cond)", the condition shown as it was written, which a ⍝# function, given only the condition's value, cannot do. ⍝# >> "x:" u_se< "Macros" ⍝# >> "5 = 2 * 2" x:c_heck< "doubling" ⍝# FAIL: doubling (5 = 2 * 2) ᵐc̲heck< ← { cond label → "{ @ -> (" c̲at cond c̲at ") ? \"ok: " c̲at label c̲at "\"; \"FAIL: " c̲at label c̲at " (" c̲at cond c̲at ")\" } @" } ⍝## Names a macro binds ⍝# Twice the expression on the right: the macro binds a local t (to 2) ⍝# around your expression, and hygiene renames it (to g1:t, say), so a ⍝# t of your own is still yours. ⍝# >> "x:" u_se< "Macros" ⍝# >> t := 5 ⍝# >> @ x:t_wice< "t + 1" ⍝# 12 ᵐt̲wice< ← { @ e → "{ @ -> t := 2; t * (" c̲at e c̲at ") } @" } ⍝# The text on the right with it bound to the value of the expression ⍝# on the left (an anaphoric macro: it is meant for your text, so it is ⍝# declared below and hygiene leaves it as written). ⍝# binds: it ⍝# >> "x:" u_se< "Macros" ⍝# >> "3 + 4" x:w_ith< "it * it" ⍝# 49 ᵐw̲ith< ← { e body → "{ @ -> it := (" c̲at e c̲at "); " c̲at body c̲at " } @" } ⍝ typed: ⍝ m:w_hen< := { cond body -> "{ @ -> (n_ot " c_at cond c_at ") ? @; " c_at body c_at "; @ } @" } ⍝ m:d_ef< := { name body -> "u:" c_at name c_at " := { " c_at body c_at " }" } ⍝ m:c_heck< := { cond label -> "{ @ -> (" c_at cond c_at ") ? \"ok: " c_at label c_at "\"; \"FAIL: " c_at label c_at " (" c_at cond c_at ")\" } @" } ⍝ m:t_wice< := { @ e -> "{ @ -> t := 2; t * (" c_at e c_at ") } @" } ⍝ m:w_ith< := { e body -> "{ @ -> it := (" c_at e c_at "); " c_at body c_at " } @" }
It is imported like any library, under a prefix of your choosing:
"m:" u_se< "Macros" finds Macros.xtlm (and Macros.xtl, had there
been one in the same directory: a library and its macro library share
one prefix). Its macros are called with that prefix:
ᵐ⁼u̲se< "Macros" "s_quare" ᵐd̲ef< "_r * _r" ᵘs̲quare 7 "4 = 2 + 2" ᵐc̲heck< "addition" "5 = 2 * 2" ᵐc̲heck< "doubling" ⍝ typed: ⍝ "m:" u_se< "Macros" ⍝ "s_quare" m:d_ef< "_r * _r" ⍝ u:s_quare 7 ⍝ "4 = 2 + 2" m:c_heck< "addition" ⍝ "5 = 2 * 2" m:c_heck< "doubling"
49 ok: addition FAIL: doubling (5 = 2 * 2)
m:d_ef< made a function from a name given as text. m:c_heck< shows
its condition as it was written, which a function, given only the
condition's value, could not: it wrote
{ @ -> (5 = 2 * 2) ? "ok: doubling"; "FAIL: doubling (5 = 2 * 2)" } @.
A macro runs when the program is expanded, before it is checked: the macro library is loaded, its function is applied to the two texts, and the text it gives replaces the call. That text may call macros again; expansion stops with an error when macros nest more than 32 deep.
Mistakes it catches
A macro takes a string, or @ for none, on each side:
x i_f< "1; 2" error[bad-macro-call]: i_f< takes a string, or @ for none, on each side: "left" i_f< "right"
A macro a macro library does not define is an error listing those it does:
"x:" u_se< "Macros" "a" x:n_ope< "b" error[not-exported]: x:n_ope< is not defined by that macro library; it defines w_hen<, d_ef<, c_heck<
Macros are defined only in macro libraries, as m: names ending in <:
u:f_< := { a b -> a }
error[misdefined-macro]: macros are defined in macro libraries (.xtlm files), as m:n_ame<
And an error in code written inside a macro's argument is reported
where it was written, inside the string. The whole program of this
document is demos/macros.xtl; xetal expand demos/macros.xtl shows
it after expansion.