← 12. Loop control · Contents · 14. Recoverable errors →
13. Routines and calls
13.1 Scope
This chapter defines routine declarations as callable interfaces, invocation, argument binding, results, return, routine completion, recursive calls, and source-level activation behaviour. Chapters 4, 5, and 8 define declaration order, forwards, names, headers, parameters, and local declarations. Chapters 6 and 7 define value copying, aggregate aliases, and lifetime.
Nucleus has one routine family. A routine declares no result or one result type. It has no overload, nested declaration, multiple-result form, implicit result variable, routine-name assignment, routine value, indirect call, or callback type.
13.2 Routine syntax
The routine fragment is:
routine-header ::= "sub" NAME routine-signature-tail
routine-signature-tail
::= "(" [ formal-parameter
{ "," formal-parameter } ] ")"
[ "as" type ] [ "fails" ]
formal-parameter ::= NAME "as" type
forward-routine ::= "forward" routine-header NEWLINE
routine-definition ::= "sub" NAME routine-definition-tail
routine-definition-tail
::= routine-signature-tail NEWLINE routine-body
| NEWLINE routine-body
routine-body ::= { local-declaration }
statement-sequence "end" NEWLINE
routine-invocation ::= NAME argument-list
argument-list ::= "(" [ expression { "," expression } ] ")"
return-statement ::= "return" [ expression ]Chapter 8 remains authoritative for declaration placement and the local-declaration prefix. The fragments here complete their call and result meaning. Parentheses are required in every complete header and invocation, including a routine with no parameters or arguments. The abbreviated header is available only to the body that completes an earlier forward.
An omitted result type declares a result-free routine. A written type declares one result of that exact scalar or aggregate type. The optional fails effect is defined by Chapter 14. The header has no separate procedure/function keyword and no result-name declaration.
13.3 Visible signatures and invocation
A routine invocation begins with a visible routine name whose complete signature has already been checked. An earlier forward declaration supplies that signature when the definition appears later. The compiler does not infer a signature from arguments or defer checking until another pass.
The invocation must supply exactly one argument for each formal parameter, in declaration order. Nucleus has no optional, named, variadic, grouped, or default arguments. An infallible result-free routine may be used only as the complete call statement from Chapter 10. An infallible result-bearing routine may be used as an expression or as a call statement that discards the result. Chapter 14 restricts every failable call to a position with one explicit failure consumer.
A call expression takes its static result type directly from the signature. A scalar result is a scalar value. An aggregate result is a transient typed alias and may take the field or index suffixes admitted by Chapter 9. It must then be consumed under Section 13.6; a routine name without its argument list is invalid in every expression and statement context.
13.4 Argument evaluation and compatibility
Arguments are evaluated from left to right. Each scalar argument is evaluated and converted if permitted, and its resulting value is retained before evaluation of the next argument. Each aggregate argument evaluates its storage path, including field selection and checked indexing, and establishes the alias value supplied to the parameter.
If argument evaluation traps, no later argument is evaluated and the routine body does not begin. Effects from earlier arguments remain observable.
A scalar argument must have the exact parameter type, be an exact integer value that fits it, or use one of the value-preserving implicit conversions: u8 to u16, u8 to i16, or i8 to i16. Every other integer conversion requires the explicit destination-type form u8(...), u16(...), i8(...), or i16(...), which traps when the mathematical source value lies outside the destination range. Boolean and integer arguments do not convert between each other.
An argument for a concrete aggregate parameter must be an aggregate storage path or transient alias with exact referent-type identity. An argument for string[] may instead have any concrete bounded-string capacity, be another string[] parameter, or be a string literal. A literal argument creates the distinct anonymous object defined in Section 6.8 and transfers its ordinary address-and-capacity carrier. An argument for T[] may be any complete concrete T[N] storage path or transient alias, or another T[] parameter; its element type must match exactly. In every case the call transfers an alias rather than copying the object. An open binding also retains the actual capacity or count used by its postfix operations. Scalar-leaf mutation through the parameter is visible through every other path to the same storage.
Nucleus has no parameter modes, implicit read-only aggregate parameter, write permission, copy-in/copy-out aggregate parameter, or hidden source-level pointer conversion.
13.5 Activation semantics
A successful call begins one logical activation after all arguments have been evaluated. The activation contains that invocation's copied scalar parameters, aggregate-parameter bindings, and scalar locals. Activation-local initialization follows Section 8.12 before the first statement begins.
Each simultaneously active invocation has distinct activation state. Calling another routine does not change the caller's scalar parameters, scalar locals, or aggregate-parameter bindings. The callee may change program-lifetime storage that it can name or reach through an aggregate argument, and those mutations remain visible to the caller.
The caller resumes after the invocation when the callee returns normally. For an expression call, the result is transferred before evaluation continues in the containing expression. For a call statement, any result is discarded after transfer.
13.6 return and results
A result-free routine uses bare return, or reaches its closing end. Every return expression is invalid in a result-free routine, including an expression that is a failable invocation. A failable result-free call must consume failure as its own statement before a later successful return.
A result-bearing routine uses return expression. Bare return is invalid. The expression is evaluated once before the activation ends and must be compatible with the declared result type. It cannot be a failable invocation: failure must be propagated or handled by an earlier statement, and return represents success only.
A scalar result follows the scalar destination rules: exact type, fitting exact integer value, or an admitted value-preserving implicit conversion. Every other integer conversion must be written explicitly and checked. The caller receives a copied scalar value.
An aggregate result must be an aggregate storage path or transient aggregate-alias result with exact referent-type identity. The storage path is rooted in a visible program variable, aggregate constant, or aggregate parameter. The caller receives a transient alias to the same existing program-lifetime object, not a copy. Section 7.9 establishes the lifetime of every admitted aggregate result without another result check.
The caller may consume that transient alias only by discarding it as a complete call statement, passing it directly to a compatible aggregate parameter, forwarding it as an aggregate return, applying an immediate field or index suffix, or using it as an exact-type aggregate-assignment source. It cannot be retained in a source variable. To retain the returned value, the caller assigns the call result into a program object or caller-supplied aggregate destination, causing the copy defined by Section 7.8.
If evaluating a later argument or suffix performs another call, the transient carrier remains valid until its containing operation consumes it. This does not create a source-visible pointer or extend the result beyond the operation.
return may appear anywhere in a routine statement sequence, including inside a conditional or loop. It ends the current activation immediately after transferring the result, if any. It does not execute later statements in the routine.
13.7 Value-routine completion
A value routine is invalid when its closing end is reachable without executing return expression. Nucleus does not supply an implicit value, result variable, or default return.
The static rule uses a bounded structured fallthrough summary:
return expressiondoes not fall through;- assignment and call statements fall through;
- an
ifdoes not fall through only when it has anelseand every clause body does not fall through; - an
ifwithoutelsemay fall through; and - a
selectfollows the fallthrough rule in Section 11.7; and - a
whilewhose condition folds to the Boolean constanttruedoes not fall through when no syntacticexittargets that loop; every otherwhileand everyforis treated as able to finish.
A statement sequence can reach its end when control can pass through every statement on a path. Once a statement on a path does not fall through, later statements on that path do not restore fallthrough.
The non-fallthrough loop rule recognizes any condition that the ordinary expression folder proves to be the Boolean constant true. Parentheses, not false, true and true, a named Boolean constant, and another folded Boolean expression therefore qualify. A dynamic condition and a condition folding to false do not. Any syntactic exit whose nearest loop is that while makes it conservatively fallthrough-capable, even when the exit follows a non-fallthrough statement and cannot execute. An exit whose nearest loop is a nested while or for does not count against the outer loop.
13.8 Forward definitions and recursion
A forward declaration contains the routine's complete and sole signature, including its parameter names. Its later body begins with sub NAME and a logical newline. That name must resolve to exactly one incomplete forward under Chapters 4, 5, and 8. The stored parameter names bind the body; no parameter, result, or fails clause is repeated. The forward declaration and body definition denote one routine.
The body does not repeat the signature, so there is no second signature to compare. A streaming compiler must retain the forward's parameter names and signature until it compiles the body.
After its complete signature has been checked, a routine may call itself directly. Mutually recursive routines require an earlier forward signature for every routine called before its definition. Recursive calls use the ordinary argument, activation, result, and lifetime rules; Nucleus has no separate recursive syntax.
Recursion is part of Nucleus 0.1. Standard language mode must not reinterpret or reject recursive source within the implementation's documented compile-time capacities.
13.9 Activation capacity
Runtime activation capacity is implementation-defined. An implementation may bound the number of simultaneously active routine invocations, the storage consumed by their activation state, or both. It must publish every bound and provide at least the capacity needed by every complete accepted program in Chapter 18 under its stated inputs. Before beginning a call that would exceed a published bound, the program performs the activation-capacity trap specified by Chapter 15; it must not overwrite a live activation, alias one activation's locals with another, or continue with partial parameter binding.
The trap point is after argument evaluation and before the new activation begins. Effects from evaluated arguments remain observable, while the callee performs no local initialization or body statement.
This runtime limit does not create a non-recursive language profile. A compiler accepts recursive call graphs subject to its ordinary compile-time capacities; active depth is a runtime property.
13.10 Cleanup
Nucleus routines have no destructors, finally, defer, exception unwinding, variable-sized local allocation, or other source-level scope-exit action. A return therefore performs no hidden source cleanup before transferring control.
13.11 Invalid calls and capacity limits
The compiler must diagnose an unavailable or non-routine callee, a missing argument list, wrong arity, an incompatible scalar argument or result, an aggregate argument or result with the wrong referent type, a result-free call used as a value, the wrong return form, a value routine whose end is reachable, an abbreviated body without one incomplete forward, and a duplicate or missing forward completion.
An implementation may bound parameters, arguments, active expression-call nesting, retained signatures, fallthrough-summary depth, and compile-time call-graph metadata. It must publish each limit and issue a capacity diagnostic before dropping an argument, corrupting a signature, losing a result, merging live state, or changing a call target. Runtime activation capacity follows Section 13.9 rather than this compile-time capacity rule.
13.12 Examples
A result-free routine and a value routine use the same declaration family:
sub display(value as u8)
return
end
sub maximum(left as u16, right as u16) as u16
if left >= right
return left
else
return right
end
endBoth paths through maximum return a compatible value. The result may be used directly:
largest = maximum(first, second)An aggregate result preserves alias identity:
sub entryAt(index as u8) as Entry
return entries[index]
end
sub update(items as Entry[8], index as u8)
items[index].value = entryAt(index).value
endentryAt returns an alias to program-lifetime storage. The call itself copies no Entry; an aggregate assignment using that result copies into its destination.
To retain the complete returned value, the caller provides destination storage:
sub retain(index as u8, destination as Entry)
destination = entryAt(index)
enddestination remains bound to the caller's object. The assignment materializes the transient result without declaring an aggregate local.
A length-polymorphic routine receives a complete array rather than a slice:
sub sum(data as u16[]) as u16
var total as u16 = 0
var i as u16
for i = 0 until data.length
total = total + data[i]
end
return total
endThe same routine accepts any admitted concrete u16[N] argument. data.length is the caller's retained u16 element count, and every index checks against that count. The counter is declared before the loop; a Nucleus for header never declares it.
Mutation uses the same complete-object view:
sub fill(data as u8[], value as u8)
var i as u16
for i = 0 until data.length
data[i] = value
end
end
const tooLong = 5
sub copy(source as u8[], destination as u8[]) fails
var i as u16
if source.length > destination.length
fail tooLong
end
for i = 0 until source.length
destination[i] = source[i]
end
endcopy checks the destination before its first write. The two parameters may have different concrete lengths because compatibility is determined by their exact element type, not by an equal array bound.
Direct and mutual recursion use ordinary signatures:
forward sub odd(value as u16) as boolean
sub even(value as u16) as boolean
if value = 0
return true
end
return odd(value - 1)
end
sub odd
if value = 0
return false
end
return even(value - 1)
endThese forms are invalid:
sub missing(value as u8) as u8
if value = 0
return 1
end
end // value path reaches end
sub procedure()
return 1 // result-free routine
end
sub value() as u8
return // value routine requires an expression
end