Functions¶
A function groups a block of statements under a name. Functions are top-level declarations — they cannot be nested inside another function.
Declaration¶
A function is named with the @ sigil and may declare a parameter list and a
return type:
@add($a: u16, $b: u16) -> u16 {
return $a + $b
}
The parameter list is in parentheses; each parameter is
$name: Type. If there are no parameters, the parentheses are optional.The return type follows
->. If omitted, the function returns nothing (void).The body is a block.
A few valid shapes:
@noop { } # no params, no return
@tick() { 1 ->+ $ticks } # empty params, no return
@cpu_mhz() -> u16 { return 16 } # no params, returns u16
@clamp($x: u8, $lo: u8, $hi: u8) -> u8 { ... }
Parameter types may be any type usable as a value: primitives, ptr <space>
<type>, str ram, or a function type fn(...). Note that as a parameter
type, strings must be str ram (see Type system).
The entry point: @main¶
@main is an ordinary function that the compiler treats specially as the
program entry point. It takes no parameters and returns nothing. The compiler
generates the reset vector, stack initialisation, and the jump into @main.
Because a microcontroller program never exits, @main typically ends in an
infinite loop *.
Calling¶
Call a function with its sigil and parenthesised arguments:
ram imut $s: u16 = @add(2, 3)
@uart_println()
Arguments are expressions, evaluated and passed in order. A call to a function
that returns a value is itself an expression; a call to a void function is a
statement.
Calling convention¶
The compiler uses a register-based calling convention:
Arguments occupy register slots descending from
r24: the first argument is inr24(r24:r25for a 16-bit value), the second inr22(:r23), and so on down tor16(r18on the reducedAVRrccore, whose register file isr16–r31).Surplus arguments that do not fit the register slots travel on the hardware stack: the caller pushes them in ascending byte order so they sit just above the return address, the callee reads them with post-increment loads through
Z, and the caller pops them after the call returns.The return value travels in
r24(r24:r25for 16-bit values).Callee-saved registers (values kept live across calls) are
r2–r15on classic cores andr26–r28onAVRrc; everything else is call-clobbered.
You do not manage any of this by hand, but it is useful to know when reading a
VM register trace while debugging (the --trace instruction trace on the
bundled VM shows the register state).
Function pointers¶
A function-pointer type is fn(paramtypes) -> ret (see Type system). Obtain
a function pointer with the address-of operator on a function name, and call
through it with the indirect-call syntax @$var(...):
@double($x: u8) -> u8 { return $x * 2 }
@triple($x: u8) -> u8 { return $x * 3 }
@apply($f: fn(u8) -> u8, $v: u8) -> u8 {
return @$f($v) # indirect call
}
@main {
ram imut $r1: u8 = @apply(&@double, 21) # 42
ram imut $r2: u8 = @apply(&@triple, 14) # 42
ram imut $g: fn(u8) -> u8 = &@double
ram imut $r3: u8 = @$g(21) # 42, direct via variable
}
Lowering, in brief: &@func produces the function’s code address; an indirect
call @$var(...) loads that address and performs an indirect call to it. A
function pointer’s type must match the target function’s parameters and return
type exactly. This mechanism is what lets library routines such as
@font_stream and @font_fold take a
caller-supplied callback.
Recursion and reentrancy¶
Functions may call other functions, including indirectly through pointers. Be
mindful that the AVR has a small SRAM stack; deep or unbounded recursion can
overflow it. Library functions in std are written iteratively for this
reason (for example the math routines avoid recursion to prevent static-local
collisions and stack growth on AVR).