Skip to content

Nucleus 0.1 Language Specification13

← 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:

text
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 expression does not fall through;
  • assignment and call statements fall through;
  • an if does not fall through only when it has an else and every clause body does not fall through;
  • an if without else may fall through; and
  • a select follows the fallthrough rule in Section 11.7; and
  • a while whose condition folds to the Boolean constant true does not fall through when no syntactic exit targets that loop; every other while and every for is 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:

nucleus
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
end

Both paths through maximum return a compatible value. The result may be used directly:

nucleus
largest = maximum(first, second)

An aggregate result preserves alias identity:

nucleus
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
end

entryAt 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:

nucleus
sub retain(index as u8, destination as Entry)
    destination = entryAt(index)
end

destination 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:

nucleus
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
end

The 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:

nucleus
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
end

copy 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:

nucleus
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)
end

These forms are invalid:

nucleus
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