Skip to content

Functions

Functions group reusable logic. They can use parameters and local values, but they do not read top-level variables.

def add(left: num, right: num) -> num:
total = left + right
return total
print(add(2, 3))

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.

Intentional failure — NLK2001 top-level capture
x = 1
def bad() -> num:
return x

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.

Intentional failure — NLK2006 branch return
def bad(flag: bool) -> num:
if flag:
return 1
return 0

Direct 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.

def pair(value: num) -> tuple[num, num]:
return value, value + 1
left, right = pair(4)
print(left)
print(right)