Macros
June 23, 2026 ยท View on GitHub
Macros can reduce repetitive code, and can help improve the readability. Some use cases are:
forloops to iterate over entries and / or indexes,when(ternary expressions),assert,- (debug) logging,
- sorting,
- filter and map (list comprehension),
- serialization,
- language integrated query,
- compile-time reflection.
Using Macros
Macros are used like regular functions. However, macros do not always evaluate all parameters: Macros are replaced at compile time with the macro implementation, and parameters are only evaluated when needed. Macros have access to the source code of the arguments.
For Loops
There are built-in macros range and until to iterate
from 0 or from some other value up to, but excluding, an upper bound.
The following will print 0, 1, 2, 3, and 4:
for i := until(5)
println('i: ' i)
for j := range(0, 5)
println('j: ' j)
When (Ternary Expression)
The macro when(condition, ifTrue, ifFalse) evaluates ifTrue and ifFalse only when needed.
It is used like the ternary expression condition ? ifTrue : ifFalse in other languages.
import org.bau.Utils
when
number := 10
group : when(number >= 10, 'large', 'small')
println(group)
Assert
The assert macro verifies that expression is true,
and stops execution if not, printing the line number, module (if any),
source code of the expression, and variable values (sometimes called "power assert"):
import org.bau.Assert
assert
age := -10
assert(age >= 0)
> assertion failed at line 423: age >= 0
> for values "age","-10"
Debug Logging
The debug macro prints the expression value, source code, and variable values (if any).
import org.bau.Debug
debug
x := -10
y := debug(x * x)
> debug line 2: x * x == 100 for "x","-10"
Language Integrated Query
Macros can be used to generate SQL queries, and access databases in a type-safe way.
import org.bau.db.Sqlite3
Sqlite
Record
text
printText
import org.bau.Int
import org.bau.String
type User
id int
name text
fun main()
db : Sqlite3.open('demo.db')
if db == null
return
db.dropTable(User)
db.createTable(User)
db.insert(newUser(0, 'This'))
db.insert(newUser(1, 'Is'))
db.insert(newUser(2, 'Better'))
list : db.from(User).where(it.id > 0).orderBy(it.name).select()
for i := until(list.size)
u : list.get(i)
break u == null
printText('id: ' u.id '; name: ' u.name)
# help function for inserting
fun newUser(id int, name text) User
u : User(name)
u.id = id
return u
# helper function for de-serialization
fun convertRecordToUser(r Record) User
return User(text())
Implementing For Loop Functions
Custom for loop functions can be implemented.
The variable _ marks the result, and return _ is replaced with the loop body.
A continue statement jumps to the statement just after the return.
One might one to iterate over the elements of an array or list.
Sometimes it is useful to have the index and the value,
or (for a hash table) the key and the value.
For this purpose, a pair type can be used:
fun main()
array : int[5]
for i := until(array.len)
array[i] = i * 10
for e := elements(array)
println('element ' e)
for p := pairs(array)
println('index-value-pairs #' p.i ' = ' p.value)
fun for elements(array T[]) T
if array.len
i := 0..array.len
loop
_ : array[i]
return _
break i + 1 >= array.len
i += 1
type pair(T)
i int
value T
fun newPair(value T, index int) pair(T)
result : pair(T)
result.value = value
result.i = index
return result
fun for pairs(array T[]) pair(T)
if array.len
i := 0..array.len
loop
_ : newPair(array[i], i)
return _
break i + 1 >= array.len
i += 1
Implementing Macros
Macros are implemented like regular functions, by adding macro.
Generic types are supported. Example:
fun macro when(condition int, a T, b T) const T
if condition
return a
else
return b
However, unlike regular functions, macros are expanded when used.
Because of that, the parameters a and b are only evaluated when needed.
In a macro, additional fields are available for each parameter:
modulecontains the module of the source code (if any)linecontains the line number in this modulesourcecontains the source code of the expressionastcontains the AST (abstract syntax tree)valuescontains the variable names and values of the expression
Example:
import org.bau.Env
fun macro assert(x int)
if not x
source : x.source
line : x.line
values : x.values
module : x.module
println('assertion failed at line ' line ': ' source)
if values.len
println('for values ' values)
if module.len
println('in module ' module)
Env.exit(1)
The it Variable
When calling a macro, a variable named it is available, if the macro defines this variable.
This is useful to eg. iterate over elements.
As an example, if a function or operation should be applied to each element of an array:
fun macro int[].mapInt(value int) int[]
result : int[this.len]
i := 0
while i < this.len
it : this[i]
result[i] = value
i += 1
return result
array : int[10]
for i := until(10)
array[i] = i
squares : array.mapInt(it * it)
for i := until(10)
println(i ': ' squares[i])
The type of the it variable can be specified in the parameter
if it is different from the type of the variable (value for the case above):
import org.bau.Math
fun macro int[].mapIntToFloat(value(int) float) float[]
result : float[this.len]
i := 0
while i < this.len
it : this[i]
result[i] = value
i += 1
return result
array : int[10]
for i := until(10)
array[i] = i
squares : array.mapIntToFloat(Math.sqrt(it))
for i := until(10)
println(i ': ' squares[i])