Functions, callables, and control flow
Silk functions have locally readable contracts, evaluate calls in a fixed order, and use explicit
control transfers. Named functions are first-class callable values. if, while, and match
provide structured selection and repetition without truthiness, implicit exception flow, or hidden
ownership transfers.
Effect construction, execution, and channels are defined by effects and execution and Effect contracts. This page describes the ordinary function and control-flow behavior shared by eager and effect bodies. Moves, borrows, captures, and cleanup are defined by ownership and borrowing.
Terminology
- A named function is a module-level
fnoreffect fndeclaration. - A function item is the callable value denoted by naming a function without calling it.
- A callable is a value that may be invoked with an ordered list of arguments.
- A callable's invocation mode is shared reusable
fn, exclusive reusablemut fn, or consumingonce fnaccess to its captured environment. - A section is a callable produced by supplying a trailing suffix of a named function's arguments while leaving one or more leading parameters unsupplied.
- A control transfer is
return,break,continue, typed failure propagation, or another operation that exits the current region without ordinary fallthrough. - A reachable path is a possible route through the body that has not already returned, failed, diverged, broken, or continued.
- A guard is the optional boolean expression between a match pattern and
=>. - Coverage is the set of possible nominal alternatives handled by a match's arms.
- Narrowing gives a value a more precise type within one proven branch without changing its declared type outside that branch.
Function declarations, calls, and returns
FUNC-001 — Every named function has a locally readable contract
Status: Confirmed
Every parameter declares its type. Omitting a result annotation declares (), not an inferred
result. For an effect function, the declared success, failure, and requirement channels describe
the Effect produced by calling it.
An ordinary or Effect function may prefix an owned parameter name with mut. This creates mutable local
storage for the transferred value without changing the callable's parameter type or identity.
Borrowed parameters use & or &mut and do not accept the binding-level mut prefix. Service and
interface operations describe contracts rather than local storage, so their parameters do not
accept it either.
struct LoadError {}
service Store {
effect fn load(id: i32) -> i32 ! LoadError ? &Store
}
fn notify(message: string) {
}
effect fn load(id: i32) -> i32 ! LoadError ? &Store {
return run Store.load(id)
}notify has result type (). Calling load has result
Effect<i32 ! LoadError ? &Store>. The compiler checks bodies against those declarations without
using later callers to enlarge or replace them.
Boundary: A named function cannot omit a result annotation and later return another type. An effect function cannot originate a failure or requirement omitted from its declared channels. Generic parameters may make parts of the contract abstract, but they remain explicit declaration parameters rather than caller-driven rewriting of the signature.
Diagnostics: Unknown parameter or result types receive their ordinary type diagnostic. A non-unit return from an omitted result contract receives the return mismatch described by RETURN-001. Undeclared Effect failures and requirements report the channel diagnostics defined by EFF-009 and EFF-011.
Evidence: effect-function contracts, omitted unit results, omitted channels.
FUNC-002 — Ordinary functions execute eagerly and effect functions construct lazily
Status: Confirmed
Calling an ordinary fn executes its body immediately. Calling an effect fn captures its supplied
arguments and constructs one Effect whose complete body executes only when run.
fn increment(value: i32) -> i32 {
return value + 1
}
effect fn incrementLater(value: i32) -> i32 {
return value + 1
}increment(41) is 42. incrementLater(41) is Effect<i32>; run incrementLater(41) is 42.
Boundary: The effect fn spelling changes execution timing, not the meaning of return or the
declared success type. Returning an Effect from an ordinary function does not make that function
lazy: eager setup still runs before the returned Effect is constructed.
Diagnostics: Calling either declaration is valid. Using Effect<A> where A is required must
report the type mismatch defined under EFF-002. Ignoring the constructed Effect as a statement
reports SEM0087 under STMT-001.
Evidence: Effect construction, eager setup.
CALL-001 — A call evaluates each argument once from left to right
Status: Confirmed
A call evaluates its callable expression once, then evaluates supplied arguments once in source order from left to right. Only after the arguments complete are their values bound to parameters and the target body entered.
fn first() -> i32 { return 1 }
fn second() -> i32 { return 2 }
fn combine(left: i32, right: i32) -> i32 { return left * 10 + right }
pub fn main() -> i32 {
return combine(first(), second())
}first() completes before second(), and both complete before combine begins. The result is
12.
Boundary: If evaluating an earlier argument traps, propagates a typed failure, or otherwise transfers control, later arguments do not begin. Argument evaluation order never changes to match parameter ownership, optimizer preference, or target ABI order.
A pipeline has its own related order: it evaluates its completed left expression before evaluating the callable expression on the right, as defined by PIPE-001.
Diagnostics: Evaluation order is behavior rather than a validity condition. Each invalid argument receives its own expression, ownership, failure, or requirement diagnostic at that argument. A control transfer prevents execution of later arguments but does not suppress independent compile-time diagnostics in their source.
Evidence: evaluation trace specification, callable application specification.
CALL-002 — Calls satisfy the declared positional parameter contract
Status: Confirmed
A full invocation supplies one compatible argument for every positional parameter. Arguments bind to parameters by ordinal; names at the call site do not reorder them. Silk performs no implicit numeric conversion, truthiness conversion, or ownership-mode conversion to make an argument fit.
fn subtract(left: i32, right: i32) -> i32 {
return left - right
}
pub fn main() -> i32 {
return subtract(10, 3)
}The result is 7, with 10 bound to left and 3 to right.
Supplying a nonempty trailing suffix may construct a section instead of invoking the body under CALLABLE-002. Supplying too many arguments is always invalid. Supplying no arguments to a function that declares parameters neither invokes it nor creates a redundant section; name the function item directly.
Boundary: subtract(10, true) is invalid rather than converting true. Passing an affine
binding to an owned parameter requires explicit move; passing a fixed array to a slice parameter
requires an explicit borrow. Parameter mut affects only the callee's local storage and never
weakens either transfer rule.
Diagnostics: Too many arguments or another non-section arity mismatch reports SEM0007.
Incompatible argument types report SEM0012 at the argument. Applying a non-callable reports
SEM0075. A redundant empty call of a unary function reports SEM0078; invalid ownership uses the
corresponding OWN diagnostic.
Evidence: call semantic diagnostics, callable specification, parameter ownership.
RETURN-001 — return exits with a value compatible with the declared result
Status: Confirmed
return expression evaluates the expression once and exits the current function or Effect body
with that value. The value must be compatible with the declaration's result or success type. Silk
does not infer a different result from the returned expression and does not insert numeric or Effect
conversions.
fn absolute(value: i32) -> i32 {
if value < 0 {
return -value
}
return value
}Every reachable path through a non-unit body must reach a compatible return or another terminal
operation such as a typed failure or divergence. return without an expression is equivalent to
return () and is valid only for a unit result. A unit body may fall through its closing brace,
which produces ().
fn notify() {
return
}
fn alsoNotify() {
}Boundary: Falling through a body declared to return i32 is invalid. Returning
Effect<i32> from a body declared to produce i32, or returning i32 from a body declared to
produce Effect<i32>, is a type mismatch; neither direction runs, wraps, or flattens automatically.
Diagnostics: Reachable non-unit fallthrough reports SEM0130 at the closing boundary. An
incompatible returned expression reports SEM0129 at that expression with the declared and actual
types. More specific union or representation diagnostics may explain those specialized joins.
Effect-specific examples are recorded under EFF-002.
The contract is semantic: a trailing return is unnecessary when no reachable path can fall through.
Evidence: function syntax, Effect return semantics, unit fallthrough tests.
RETURN-002 — An Effect block derives its contract from every reachable terminal
Status: Confirmed
The success and failure types of effect { ... } come from every reachable return and fail in
the deferred block. Equal return types remain that type; distinct joinable ordinary value types form
their canonical union; never contributes no success member. Source order never makes the last
written terminal override earlier branches.
fn later(flag: bool) -> Effect<bool | i32> {
return effect {
if flag {
return true
}
return 42
}
}The block has success type bool | i32, not i32. A fail contributes its precise ordinary value
type to the failure row, including a value-kind generic type parameter. Terminals nested in an
unsafe { ... } statement count exactly like terminals at the block's top level.
Boundary: A surrounding expected Effect type does not discard a block terminal or coerce its
value. If the example were returned as Effect<i32>, the inferred Effect<bool | i32> would be
incompatible. A pair of return types with no legal finite representation cannot form a block result.
Capture analysis follows the same complete block traversal. A binding used only as the argument of
Enum.value is still captured when the Effect is constructed and read when it runs.
Diagnostics: An inferred union incompatible with the surrounding expected Effect reports the
ordinary union or type mismatch at that boundary. Return types with no legal join report SEM0163
at the offending terminal and identify the contributing types. A failure left unhandled at run
reports SEM0066; generic failure values are not dropped from that check.
Evidence: effect-block terminal specification, canonical join implementation, effect-block typing tests.
Callable values and pipelines
CALLABLE-001 — Naming a function produces a first-class callable value
Status: Confirmed
A resolved named function may be passed, returned, or bound without being invoked. A plain named function has no captured environment and supports shared reusable invocation.
fn increment(value: i32) -> i32 { return value + 1 }
fn apply(transform: fn(i32) -> i32, value: i32) -> i32 {
return transform(value)
}
pub fn main() -> i32 {
return apply(increment, 41)
}The result is 42. Naming increment does not call it and does not require empty parentheses.
Boundary: A function item must satisfy the expected parameter, result, and invocation-mode
contract. Two functions with the same visible callable signature may retain distinct concrete
identities for specialization; source cannot erase those identities merely by naming the structural
callable type. Safety is also part of that contract: an unsafe fn(A) -> B value still requires an
unsafe acknowledgement when its complete invocation occurs. A safe callable may satisfy an unsafe
callable parameter, but an unsafe callable cannot satisfy a safe one. Partial application preserves
the qualifier until the final invocation; constructing the section itself does not acknowledge the
eventual call.
Diagnostics: Applying a non-callable reports SEM0075. An incompatible callable parameter,
result, or mode reports SEM0076. A context that would erase a required concrete callable identity
reports SEM0080 or the more specific represented-storage diagnostic.
Evidence: callable specification, indirect-call tests, unsafe callable contracts.
CALLABLE-002 — Supplying a trailing argument suffix constructs a section
Status: Confirmed
Calling an N-parameter named function with K arguments, where 0 < K < N, binds those arguments
to the trailing K parameters and returns a callable awaiting the leading N - K parameters.
fn combine(a: i32, b: i32, c: i32) -> i32 {
return a + b + c
}
fn staged() -> i32 {
let withThree = combine(3)
let withTwoAndThree = withThree(2)
return withTwoAndThree(1)
}combine(3)(2)(1) invokes combine(1, 2, 3) and produces 6. Every stage captures one contiguous
trailing suffix; sections do not leave holes, reorder parameters, or bind a leading parameter while
omitting a later one.
Boundary: Supplying all parameters invokes the function. Supplying none denotes no application; use the function name as a callable value. Supplying more than the remaining arity is invalid.
Diagnostics: Too many arguments report the ordinary arity diagnostic. A partially applied callable used where its eventual result is required reports a type mismatch naming the remaining callable contract. Capture ownership errors occur when the section is constructed.
The compiler carries every remaining leading parameter and captured trailing argument through
semantic facts, HIR, MIR, and each execution engine. combine(3)(2)(1) therefore preserves both
source evaluation order and the final positional call combine(1, 2, 3).
Evidence: captured callable rule.
CALLABLE-003 — Invocation mode describes access to the callable environment
Status: Confirmed
Callable contracts distinguish three modes:
| Contract | Environment access | Reuse |
|---|---|---|
fn(A) -> B | shared | repeatable |
mut fn(A) -> B | exclusive | repeatable in sequence |
once fn(A) -> B | consuming | at most once |
Shared callable access may satisfy an exclusive or consuming parameter, and exclusive access may satisfy a consuming parameter. The reverse substitutions are invalid because they promise more reuse than the supplied callable supports.
Invocation mode applies to the hidden environment, independently from the ownership modes of newly supplied arguments. A shared callable may still accept an owned argument; a consuming callable may accept a Copy argument while consuming one capture.
Boundary: A callable that moves an affine capture during invocation is once even if its
visible argument and result types are Copy. A callable that mutates an exclusive capture is mut
even when callers invoke it sequentially.
Diagnostics: An incompatible callable contract reports SEM0076. Invoking a callable without
the required shared, exclusive, or consuming access reports SEM0077; represented stored values may
use ownership diagnostic OWN0014 for the same access violation.
Evidence: callable ownership, callable specification.
PIPE-001 — A pipeline invokes one unary callable after evaluating its left value
Status: Confirmed
value |> operation evaluates the completed left expression exactly once, then evaluates the right
expression as a unary callable and invokes it with the left value. Pipelines associate left to
right.
fn add(left: i32, right: i32) -> i32 { return left + right }
fn multiply(left: i32, right: i32) -> i32 { return left * right }
pub fn main() -> i32 {
return 2 |> add(3) |> multiply(4)
}The expression groups as (2 |> add(3)) |> multiply(4) and produces 20.
The pipeline does not insert an argument into syntax. add(3) is first an ordinary section waiting
for left; the pipeline then invokes that callable with 2.
The left expression may be an explicit & or &mut borrow when the callable expects a borrowed
view. If the invoked function returns that view under the one-source return contract, the result
retains the same source provenance and loan lifetime as the equivalent direct call. The same rule
applies when the exact source is a supplied argument or a trailing capture of a known section;
opaque callable values do not invent a source.
Boundary: The right side may be a function item, section, binding, grouped expression, or any other compatible unary callable. It does not perform method lookup, open a namespace, import a name, or change the callable's ownership contract.
Diagnostics: A non-callable right expression reports SEM0075. A callable with incompatible
arity, parameter type, result use, or invocation mode reports the corresponding callable or
argument diagnostic. Invalid transfer or borrowing of the left value reports its ordinary ownership
diagnostic.
Evidence: operator pipeline specification, pipeline elaboration tests, pipeline ownership.
Conditionals, loops, and transfers
IF-001 — if selects one statement branch using a boolean condition
Status: Confirmed
An if statement evaluates its condition exactly once. The condition must have type bool; Silk
has no truthiness conversion. A true condition executes the first arm, while a false condition
executes the else arm when present and otherwise continues after the statement.
fn choose(flag: bool) -> i32 {
if flag {
return 1
}
return 2
}Only the selected arm executes. Chained else if uses the same rule in source order.
Boundary: Bootstrap if is a statement, not a value-producing expression. A branch communicates
a value by returning it, binding or mutating an outer place under ordinary ownership rules, or by
continuing to a later expression. Code needing a value selected from exhaustive alternatives may use
match.
Pattern-conditioned if let Pattern = expression { ... } tests and destructures a value while
introducing bindings only in the selected body. It remains distinct from ordinary boolean if and
does not add implicit matching to a boolean condition. See
PATT-007.
An if condition cannot be an integer, nominal value, Effect, or other implicitly converted value.
An Effect returning bool may be executed conditionally either inside the selected statement arm or
in the right operand of && or ||. In both forms its failures, requirements, and ownership follow
the ordinary enclosing execution contract.
Diagnostics: A non-boolean condition reports SEM0011 at the condition and identifies its actual
type. Invalid if use in expression position receives a parser diagnostic. Each arm retains its own
type, ownership, failure, and requirement diagnostics even though only one arm executes at runtime.
Evidence: conditional syntax, conditional semantic facts, short-circuit boundary.
LOOP-001 — while is a boolean pre-test loop
Status: Confirmed
while condition { body } evaluates condition before every possible iteration. It enters the body
only when the result is true. A false initial result executes the body zero times.
fn countToThree() -> i32 {
let mut count = 0
while count < 3 {
count = count + 1
}
return count
}The condition is evaluated four times, the body three times, and the result is 3. Body fallthrough
or continue begins the next condition evaluation; break continues after the loop.
Boundary: The condition must be bool; there is no integer or optional-value truthiness. The
bootstrap language has no for, unconditional loop, labeled loop, or value-producing break.
Equivalent iteration is expressed with while and explicit mutable state.
Every path that repeats must restore a compatible ownership state for outer bindings. Iteration locals are new lexical owners on each iteration and clean before the next one begins.
Diagnostics: A non-boolean condition reports SEM0011. A repeating path with incompatible owner
liveness reports OWN0005 and identifies the loop and affected owner. Other invalid reads, writes,
moves, and borrows receive their ordinary diagnostics.
Evidence: mutable loop specification, loop ownership, mutable-loop tests.
TRANSFER-001 — break and continue target the innermost loop
Status: Confirmed
continue ends the current iteration and begins the innermost loop's next condition evaluation.
break exits the innermost loop and continues with the following statement. Neither form carries a
value.
fn stopAtThree() -> i32 {
let mut index = 0
while true {
if index == 3 {
break
}
index = index + 1
continue
}
return index
}Every live owner created inside an exited arm or iteration is cleaned before the transfer reaches its target. The transfer does not bypass borrow ending or structured cleanup.
Boundary: break and continue are invalid outside a loop. Bootstrap has no labels for
targeting an outer loop directly and no break expression form. return exits the function or
Effect body rather than merely exiting a loop.
Diagnostics: A loop transfer outside any loop reports SEM0038. Supplying a value or label
receives a parser diagnostic. Ownership and cleanup conflicts at a transfer use their ordinary
OWN diagnostics.
Evidence: mutable loop specification, loop cleanup specification, mutable-loop tests.
Exhaustive matching
MATCH-001 — A match states how it accesses its scrutinee
Status: Confirmed
A match evaluates its scrutinee exactly once. match value is available only when the scrutinee is
Copy. match move value consumes one complete owner. match &value borrows it shared, and
match &mut value borrows one mutable place exclusively.
struct Token { kind: i32 }
struct End {}
fn inspect(event: Token | End) -> i32 {
return match &event {
Token { kind } => kind
End {} => 0
}
}The shared match leaves event owned by the function. Pattern bindings inherit the selected access
mode; a consuming match transfers complete selected payload ownership into its arm.
Boundary: A bare affine match is invalid because it would hide whether the operation copies, borrows, or consumes. A shared or exclusive pattern binding cannot escape its arm or be placed in owned storage. An exclusive match requires a mutable place.
Diagnostics: A bare affine match reports OWN0003. Exclusive access to an immutable root
reports OWN0007. An invalid borrowed scrutinee place reports OWN0009; escaping a borrowed pattern
binding reports OWN0006.
Evidence: match ownership, exhaustive matching specification.
MATCH-002 — Nominal patterns are complete or explicitly omit fields
Status: Confirmed
A nominal pattern names one possible nominal member and either names every field exactly once or
uses .. to acknowledge omitted fields. Fields may bind under their own names, bind under another
local name with field: local, or contain a nested nominal pattern.
struct Span {
start: i32
end: i32
}
struct Token {
kind: i32
span: Span
}
fn start(token: Token) -> i32 {
return match &token {
Token { span: Span { start: offset, .. }, .. } => offset
}
}Pattern bindings are flat, arm-local declarations. They do not shadow an existing declaration in
the same visible scope. In a consuming arm, omitted affine fields remain that arm's cleanup
obligations; .. does not leak or forget them.
Boundary: Omitting a field without .., naming one field twice, naming a field absent from the
member, or introducing a conflicting binding makes the arm invalid. A whole-member pattern such as
Token token binds the complete payload and therefore needs no per-field list or ...
Diagnostics: A missing field reports SEM0046 and suggests naming it or using ... A duplicate
field reports SEM0047; a binding conflict reports SEM0048. Unknown members or fields receive
their specific match or field diagnostic while other supplied pattern facts remain available.
Evidence: exhaustive matching specification, matching tests, whole-member tests.
MATCH-003 — Match coverage is exhaustive and guards do not prove coverage
Status: Confirmed
Arms are tested in source order. An unguarded member arm covers its member. A guarded arm handles
that member only when its guard evaluates to true, so it removes nothing from the remaining
coverage set. _ covers every remaining member and makes every following arm unreachable.
struct Token { kind: i32 }
struct End {}
fn classify(event: Token | End) -> i32 {
return match &event {
Token { kind } if kind > 0 => kind
Token { .. } => 0
End {} => -1
}
}The second Token arm remains necessary because the first arm's guard may be false. Every guard
must have type bool and may inspect its provisional pattern bindings without consuming them.
A scalar enum begins with its complete declared member set. An unguarded qualified member pattern
such as Status.Ready covers that exact canonical member; a guarded occurrence does not remove it.
Enum patterns bind no payload, and _ covers every remaining member just as it does for a
structural union.
Boundary: A match missing any member is invalid. A duplicate unguarded member, an arm after _,
or another arm made impossible by earlier coverage is unreachable. A guarded arm alone never makes
a member exhaustive.
Diagnostics: An incomplete structural-union match reports SEM0044 and lists the uncovered
members. An unreachable arm reports SEM0043. Scalar enums use the more specific coverage codes:
SEM0158 for missing members, SEM0159 for a duplicate unguarded member, and SEM0160 for an arm
after _. A non-boolean guard reports SEM0045. Consuming a provisional guard binding reports
OWN0008 because later arms may still need the unchanged payload.
Evidence: exhaustive matching specification, coverage tests.
MATCH-004 — Matching narrows only inside the selected arm
Status: Confirmed
Inside a member arm, bindings and projections use that precise member type. The original scrutinee's declared union type does not change outside the arm. A borrowed match may therefore inspect a narrowed member and later continue using the unchanged union owner.
struct Token { kind: i32 }
struct End {}
fn inspect(event: Token | End) -> i32 {
let result = match &event {
Token { kind } => kind
End {} => 0
}
return result
}Within the first arm, kind comes from a precise Token. Outside the match, event remains
Token | End.
A scalar enum member pattern selects one value but introduces no member subtype or backing-integer narrowing. The scrutinee and every use of it remain the enum's nominal type inside and outside the arm.
Boundary: Match narrowing does not introduce general subtyping, mutate a binding's declared type, expose a union's numeric runtime tag, or carry a borrowed member binding outside its arm.
Diagnostics: A structural pattern member absent from the scrutinee reports SEM0042. A scalar
enum pattern from another enum reports SEM0161; an integer literal pattern against an enum reports
SEM0162. Using a member-only field without branch proof receives the ordinary field/type
diagnostic. Escaping a borrowed narrowed binding reports OWN0006.
Evidence: exhaustive matching specification, matching tests.
MATCH-005 — A match joins the results of reachable arms
Status: Confirmed
The type of a match expression is computed from reachable arm results. Equal types remain that
type. Distinct ordinary value types form one normalized structural union. An arm of type never
contributes no result member.
struct Left { value: i32 }
struct Right { value: i32 }
fn preserve(input: Left | Right) -> Left | Right {
return match move input {
Left left => move left
Right right => move right
}
}The result is the normalized union Left | Right, independent of arm order.
Boundary: Result joining does not convert any arm result or erase its ownership and lifetime properties. If a result type is unavailable or cannot legally be stored in the resulting union, the match result is unavailable.
Diagnostics: An invalid reachable result union reports SEM0049 and lists the contributing
types and the precise unavailable member. An unreachable arm contributes neither a result type nor
a second result mismatch. Ownership transfers from result expressions remain governed by their
arm's access mode.
PATT-015–019 define exact whole-value bindings for non-nominal union members; nominal patterns keep the rules in MATCH-002 and all forms share the contextual rules in patterns and destructuring.
Evidence: exhaustive matching specification, match result tests.