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.

Literate documents · Live demo · Repository