15 — Macros and Decorators
Chapter goal: attach reusable behaviour to functions with decorators, and use Aura’s compile-time macros. Examples:
../../examples/macros.aura,../../examples/compile_time_macros.aura.
Decorators
A decorator is @name or @name(args) above a def (or a class). Decorators on a field are rejected (E320). Member modifiers and decorators may appear in any order.
@staticmethod
public def max(a, b) -> int { return a }
class Rect {
public @property
def area() -> int { return 4 }
public @classmethod
def create(cls) { return cls() }
}
The class-member decorators are @property, @staticmethod and @classmethod (chapter 07). A decorator on a plain function can be any callable, including one you write:
def trace(f) {
def wrapper(x) {
print(f"calling with {x}")
return f(x)
}
return wrapper
}
@trace
def square(x) -> int { return x * x }
def main() {
print(square(4))
}
calling with 4
16
Built-in runtime macros
Aura ships a small prelude of decorators. The prelude is injected automatically only when a macro is used.
@debug
def multiply(a, b) -> int {
return a * b
}
@timeit
def sum_to(n) -> int {
let mut total = 0
for i in range(n) { total += i }
return total
}
@memoize
def fib(n) -> int {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
@cache(maxsize=256)
def expensive(n) -> int { return n * n }
def main() {
print(multiply(6, 7)) // DEBUG: enter/exit lines, then 42
print(sum_to(100)) // a timing line, then 4950
print(fib(20)) // 6765
print(expensive(12)) // 144
}
| Decorator | Effect |
|---|---|
@debug | prints the call’s arguments and result |
@timeit | prints how long the call took |
@memoize | unbounded result cache |
@cache(maxsize=n) | bounded LRU cache |
These are runtime decorators: they wrap the emitted Python function.
Compile-time macros
A second tier expands before any Python is emitted. The macro receives its operands as quoted AST and returns replacement AST, so nothing survives to runtime unless the expansion chooses to emit it. Import them from macros:
import macros
def main() {
assert_eq(2 + 2, 4) // evaluate both once, then assert
assert_ne("a", "b")
static_assert(true) // checked while compiling
let mut a = 1
let mut b = 2
swap(a, b) // hygienic temporary; a,b change
print(a, b) // 2 1
print(identity(41) + 1) // identity disappears; prints 42
print(stringify(42)) // folds the literal at compile time
print(debug_value(a)) // prints "a = 2", yields the value
}
| Macro | Expansion |
|---|---|
assert_eq(a, b) | evaluate both once, assert equality |
assert_ne(a, b) | evaluate both once, assert inequality |
static_assert(x) | checked at compile time; a non-literal is an error |
identity(x) | expands to x |
stringify(x) | folds a literal to a string at compile time |
swap(a, b) | exchange two lvalues via a hygienic temporary |
debug_value(x) | print x = <value> once, then yield the value |
A macro that introduces bindings uses hygienic names, so it can never capture a local at the call site.
Runtime vs compile-time
| Runtime decorators | Compile-time macros | |
|---|---|---|
| When they run | when the function is called | while transpiling |
| Form | @decorator on a def | call-like expressions in the body |
| Examples | @debug, @timeit, @memoize, @cache | assert_eq, swap, stringify |
| Use when | you want a wrapper around a call | you want the code rewritten before emit |
Decorator order
When several decorators are stacked they apply bottom-up: the one closest to the def wraps first, the outermost runs first. Order changes behaviour. Using a local trace wrapper:
def trace(f) {
def wrapper(x) {
print(f"calling with {x}")
return f(x)
}
return wrapper
}
@memoize
@trace
def fib(n) -> int {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
def main() {
print(fib(5))
}
calling with 5
calling with 4
calling with 3
calling with 2
calling with 1
calling with 0
5
With @memoize outermost, the cache is checked before the trace, so each value of n is traced exactly once. Swap the two lines and the trace runs on every call, because recursion reaches the traced function before the cache.
What you learned
- Decorators attach to
def/class (never a field);@property,@staticmethod,@classmethod, and custom decorators. - Built-in runtime decorators
@debug,@timeit,@memoize,@cache. - Compile-time macros from
macros, includingassert_eq,swap,stringify,static_assert.