Silk

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 fn or effect fn declaration.
  • 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 reusable mut fn, or consuming once fn access 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:

ContractEnvironment accessReuse
fn(A) -> Bsharedrepeatable
mut fn(A) -> Bexclusiverepeatable in sequence
once fn(A) -> Bconsumingat 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.

On this page