Functions
Functions group reusable logic. They can use parameters and local values, but they do not read top-level variables.
Syntax
Section titled “Syntax”def add(left: num, right: num) -> num: total = left + right return total
print(add(2, 3))Semantics
Section titled “Semantics”Functions are introduced with def, a name, parameters, an optional return type after ->, and an
indented body. Parameters may have default values after any optional type annotation.
Parameter and return annotations are runtime contracts. Function calls evaluate their arguments and bind them to the function’s parameters.
Calls may use positional arguments, named arguments, or both. Positional arguments must come before named arguments. Named arguments use the parameter names from the function definition and may be reordered.
def scale(value: num, factor: num = 2, offset: num = 0) -> num: return value * factor + offset
print(scale(4))print(scale(4, offset=1))print(scale(factor=3, value=4))Default values are evaluated when the function is called. A default may read earlier parameters, but required parameters cannot follow parameters with defaults.
Functions can call functions defined later in the file. Top-level calls can only call functions that have already appeared earlier in source order.
Function call statements discard the return value.
def tick(value: num) -> num: return value + 1
tick(1)print is an effectful procedure. A function that calls print, directly or through another
function, is also effectful. Nilakan infers this property from the call graph. Effectful functions
must be called as statements; they cannot supply an operand, argument, return value, guard, or
assigned value. The language does not yet have surface syntax that marks this distinction. See
Open Work.
Functions cannot read top-level variables.
x = 1
def bad() -> num: return xConstraints And Errors
Section titled “Constraints And Errors”Function names and parameter names cannot use reserved built-in names such as print, len, or
dict. Parameters must be unique.
Too many arguments, missing required arguments, duplicate named arguments, unexpected named arguments, and positional arguments after named arguments are errors.
Every function must end with a final return. A return followed by another statement is rejected.
return inside if branches is not supported.
def bad(flag: bool) -> num: if flag: return 1 return 0Direct and mutual recursion are allowed. Recursive functions must use an input that reaches a base case:
def finish_after(steps: num) -> str: return "done" if steps <= 0 else finish_after(steps - 1)
print(finish_after(3))Mutual recursion follows the same rule:
def first(steps: num) -> str: return "first" if steps == 0 else second(steps - 1)
def second(steps: num) -> str: return "second" if steps == 0 else first(steps - 1)
print(first(2))There is no proof that recursive calls terminate. Non-terminating recursion eventually produces
CallDepthExceededError/NLK5005; the default maximum call depth is 100.
Function calls reset source prime depth, but they share the run’s runtime loop-depth counter. A
function that enters a commit loop while its caller’s loop is active adds another level and can
trigger LoopDepthExceededError/NLK5002. Recursive frames therefore consume call depth and dynamic
loop depth separately while sharing one global round budget. See
Limits And Errors.
Function definitions inside executable branches are not supported.
Examples
Section titled “Examples”def pair(value: num) -> tuple[num, num]: return value, value + 1
left, right = pair(4)print(left)print(right)