Control Flow
Functions
Functions are declared with func.
func scan_words(text: &str): WordStats {
...
}
Names usually do not define intrinsic language behavior. A function named init, new, or drop is ordinary. A selected executable module's function named main is the executable entry point. drop is not reserved; top-level drop declarations and explicit drop statements are contextual source forms.
Parameters are written as name: Type. Mutable parameter bindings are not supported. Parameter binding and ownership rules are specified in Ownership, Borrowing, and Drop.
Return checking:
- A
voidfunction may use barereturnor reach the end of the function body. - A
voidfunction may usereturn expressionwhenexpressionhas typevoid. The expression is evaluated before the function completes and does not become a return value. - A non-fallible, non-optional function returning a non-
voidtype must produce a value through the function body result or explicitreturnon every reachable normal path, unless the path terminates withnever. - A value-producing function body result or explicit
return expressionis checked against the complete declared result type by recursive outcome injection. This is one rule forT?,T!,T?!, and(T!)?; these forms do not have independent return-conversion rules. - A fallible function
T!must produce a success value through the function body result or explicitreturn, return anerrorfailure value, or terminate withneveron every reachable path. - A fallible
void!function may use barereturnor reach the end of the function body for success;return error_valuereturns failure. - A fallible
void!function may return avoidexpression as payloadless success. Success is constructed only after the expression completes normally. - An optional function
T?must produce a present value through the function body result or explicitreturn,return none, or terminate withneveron every reachable path. - A fallible optional function
T?!must produce a present success value through the function body result or explicitreturn,return noneas success absence, return anerrorfailure value, or terminate withneveron every reachable path. - Optional result types with
voidas their eventual payload are invalid. Barereturnand end-of-body completion therefore never construct a hidden present-voidoptional state. func main(): i32!andfunc main(): usize!follow the same source-level return checking rules as functions returning those fallible types; success returnsi32orusize, and failure returns anerrorvalue.func main(): void!follows the same source-level return checking rules as a function returningvoid!; success returns no value, and failure returns anerrorvalue.func main(): void,func main(): i32, andfunc main(): usizefollow the same return checking rules as functions with those return types.
Return value ownership, move, borrow, and view rules are specified in Ownership, Borrowing, and Drop.
Function Calls and Arguments
Calls use positional arguments only.
func copy(allocator: &+Allocator, source: &str): String! {
...
}
let text = String.copy(&+allocator, "hello")?
Rules:
- Top-level function, construction function, method, and primitive calls use positional arguments.
- Argument expressions are matched to parameters by position.
- Fixed arguments must satisfy every ordinary parameter. A final argument-pack parameter accepts zero or more remaining values and spreads.
- Each argument must type-check against the corresponding parameter type under the normal contextual typing, ownership, move, copy, and borrow rules.
- A callable's own generic type arguments are always inferred from its receiver, arguments, contextual closures, and expected result type. They are not written at the call site.
- Function call arguments are evaluated left to right in the order written.
- Method receiver expressions are evaluated before method arguments.
- Method arguments are then evaluated left to right in the order written.
- Parameter names are not part of call syntax.
- Named arguments and default parameters are not supported.
- Top-level functions, construction functions, and methods cannot be overloaded by type, arity, or return type.
- A duplicate callable name in the same namespace is a compile error.
- Parameter and argument lists accept one trailing comma regardless of physical layout under Comma-Delimited Lists.
Examples:
pub func copy(
allocator: &+Allocator,
source: &str,
): String! {
...
}
let text = String.copy(
"hello",
)
Invalid:
String.copy(source: "hello") // named argument
func open(path: &str = "input.txt"): File! {
...
}
func open(path: &str): File! {
...
}
func open(path: &str, mode: OpenMode): File! {
...
}
Use a configuration struct when an API has many boolean or optional choices. Typed argument packs and call-site spread are specified in Argument Packs, Literal Definitions, and Sequence Spread.
pub struct OpenOptions {
pub read: bool
pub write: bool
pub create: bool
}
let file = File.open_with(path, OpenOptions {
read: true,
write: false,
create: false,
})?
Body Results and Control Expressions
Braced bodies have a unified result form.
{
stmt1
stmt2
result
}
The last expression in a body is the body result. A short body may be written on one line.
{ expr }
Rules:
- A body contains zero or more statements followed by an optional result expression.
- The result expression is written without
return. - An expression used as a non-final statement must have type
voidornever. A non-final expression of any value-producing type is an error even when its value would be immediately droppable. let _ = expressionexplicitly evaluates and discards a value-producing expression. It is a statement, not a binding and not a body result.- Bindings introduced by earlier statements in the same body are in scope for the result expression.
- A body without a result expression has type
voidunless all reachable paths terminate withreturn,break,continue, ornever. voidrecords normal completion without a produced value. It is not a value that can initialize a binding, parameter, field, element, or generic substitution.- A body whose reachable paths all terminate has type
never. - Function, method, drop,
if,if is,match, loop, andcatchbodies use this same body form. - A loop body is still checked as a body, but the enclosing loop is a statement and has no consumer
for a body result. Its reachable body result must therefore have type
voidornever. Explicitly discard any other final expression withlet _ = expressioninside the body. - A function or method body result is a return value for the declared return type.
- Explicit
returnremains valid when an early exit is clearer or required. - A
voidfunction may have no body result and may reach the end of the body. - A non-
voidfunction must either have a body result assignable to the declared return type or guarantee an explicit return orneveron every reachable path.
if, if is, and match can be used as expressions.
func max(a: i32, b: i32): i32 {
if a > b {
a
} else {
b
}
}
Rules:
if condition { ... }may be used as a statement. Withoutelse, its value type isvoid.if condition { ... } else { ... }may be used as an expression when the branch body result types are compatible.- The
ifcondition expression must have typebool. - Only the selected
ifbranch is evaluated. if enum_expr is Enum.variant { ... }follows the same statement/expression rules as ordinaryif.- At the outer delimiter depth of an
if,if is,while,match, orforheader, the first{begins the control body. Parenthesize the complete header expression when it contains a struct literal, closure, recovery block, or nested control expression at that level, as specified by Primary Expressions. - Payload names introduced by
if expr is Enum.variant(payload)are visible only inside the then body. - Pattern target ownership and payload binding types are defined by
Enums and Variant Construction. In
particular,
&Enumbinds&Payload,&+Enumbinds&+Payload, andmove placebinds owned payload values while consuming the place. if expr is Enum.variant(_)checks a one-payload variant without introducing a binding. Enum patterns have exact positional arity, so every field of a multi-payload variant needs its own identifier or_slot.else if ...is syntax for anelsebody whose result is anotherifexpression.match enum_expr { ... }andmatch enum_expr { ... _ { ... } }may be used as statements or expressions.- A
matchexpression without_must cover all variants to avoid avoidmissing-branch type. matcharm body result types must be compatible when thematchvalue is used.- A
match_fallback arm matches every remaining variant and must be the last arm. _remains valid after all current variants were explicitly covered. Its body is checked and contributes to result-type compatibility even though no current tag selects it.- A
matchcannot repeat an explicit enum variant arm. Since payload slots only bind or ignore fields, changing their names or_positions does not make a repeated variant reachable. - A
neverbranch is compatible with the other branch result type. - Only the selected
matcharm body is evaluated. for name in start..<end { ... }is a statement.return valueexplicitly returns a value from the current function, method, or closure body.return error_valueexplicitly selects the fallible layer in the current callable's declared result type. Recursive outcome injection preserves any enclosing optional layer.return noneexplicitly selects the optional layer in the current callable's declared result type. Recursive outcome injection preserves any enclosing fallible layer.
Examples:
let value = if condition {
a
} else {
b
}
return match error {
AppError.open_failed(path) { 1 }
_ { 0 }
}
Unsupported forms:
- The ternary conditional operator
condition ? then_value : else_valueis not Nocter syntax. Useif. - The pattern conditional expression
enum_expr ?{ ... }is not Nocter syntax. Usematch.
Statement separation:
- Semicolons are not statement terminators.
- One statement per line is the normal style.
- A newline separates statements where the grammar can end a statement.
- A single newline does not separate statements when the next line starts with a continuation leader
such as
., a binary-only operator,as,catch, orotherwise. - A token that can begin an expression, including unary
-, never acts as a continuation leader. When subtraction spans lines, keep-at the end of the first line rather than the beginning of the second line. - A blank or comment-only intervening line ends leading-token continuation.
- A closing brace
}ends the current block or arm. - Multi-line expressions are allowed only where the expression syntax clearly continues, such as inside calls, literals, or parenthesized expressions.
- The complete continuation-leader set and lexical source text rules are specified in Lexical Grammar.
Evaluation Order and Temporaries
Expression evaluation is left-to-right.
Rules:
- Function call arguments are evaluated left-to-right.
- Method call receiver expressions are evaluated before method arguments.
- For evaluated method arguments, evaluation remains left-to-right.
- Struct literal field initializer expressions are evaluated left-to-right in the order written in the literal, regardless of declaration order.
- Assignment evaluates the complete right-hand side first, then resolves the target place exactly once before replacing it. The detailed assignment rules are specified in Bindings and Initialization.
- Compound assignment evaluates its complete right-hand side first, then resolves its target place exactly once, reads the old value, performs the checked operation, and stores the result.
- Operators and expressions with conditional evaluation, such as
&&,||,otherwise,if, andmatch, evaluate only the needed operand, branch, or arm. - When an operand or branch is evaluated, its subexpressions still follow the normal left-to-right rule.
- Temporaries are dropped at the end of the current statement in reverse creation order unless a narrower control-header scope below applies or ownership is moved into a longer-lived owner.
- An ordinary
ifboolean condition is one control-header temporary scope. After itsboolresult is computed, all remaining condition temporaries are dropped in reverse creation order before the selected body begins. - Each evaluation of a
whileboolean condition is a new control-header temporary scope. Its remaining temporaries are dropped after that iteration'sboolis computed and before either entering the body or leaving the loop. - Condition cleanup also runs before propagation or another early exit leaves the control header.
A condition temporary therefore never remains live through an ordinary
iforwhilebody. if expr is Patternandmatch exprdo not use the boolean-condition rule for their pattern target. An owned pattern target and payload projections use the pattern-operation lifetime defined in Enums and Variant Construction.- A value produced for
let _ = expressionis consumed by that discard statement. Its active owned content is dropped after expression evaluation and before earlier temporaries from the same statement are dropped. - Longer-lived owners include local bindings, owned parameters, constructed aggregate values, assigned target places, and returned values.
- Blocks,
ifbodies,matcharms, and loop bodies create scopes. - Initialized local values are dropped at scope end in reverse declaration order.
- Maybe initialized local values use compiler-generated conditional drop at scope end.
- Partially initialized structs drop their remaining initialized fields in reverse declaration order. A named field that is maybe initialized after control-flow merge uses conditional drop. Such a partial struct never owns a type-level drop body.
- Postfix
?,return,break, andcontinuefirst drop temporaries already created by the current statement, then run the required normal or conditional drops for scopes they leave. - Borrows and borrow-like views derived from temporaries cannot escape the statement.
- Temporary lifetime extension is not supported.
Examples:
let result = make_a().combine(make_b())
Evaluation order:
- 1.
make_a() - method receiver preparation for
.combine make_b()- method call
- statement-end temporary drops
This is invalid:
let view = &String.copy("abc") as &str
String.copy(...) produces a temporary owned String. The explicit conversion borrows from that temporary. The temporary would be dropped at the end of the statement, so the &str cannot be stored in view.
Write this instead:
var text = String.copy("abc")
let view = &text as &str
Condition temporaries end before the selected body:
if Guard.acquire(&+state).ready() {
inspect(&state)
}
The temporary Guard and its loan are dropped after ready() produces bool. Bind the guard before the if when the body must retain it.
A method receiver borrow lasts only for the call unless the method returns a value whose type carries a borrow-like lifetime tracked by the compiler.
file.write_text("hello")?
The call above creates a temporary readwrite borrow of file for the duration of the call and ends that borrow after the call.
Fallible temporary receivers must make each fallible step explicit:
(File.open(path)?).write_text("hello")?
If File.open(path) fails, no File temporary exists. If write_text fails, the temporary File produced by File.open(path) is dropped before the failure propagates. If write_text succeeds, the temporary File is dropped at the end of the statement.
Loops
Loop forms are while, loop, range for, collection for, break, and continue.
var i: usize = 0
while i < bytes.len() {
let byte = bytes[i]
if byte == 0 {
break
}
i += 1
}
loop {
poll_once()
if should_stop() {
break
}
}
loop {
let item = iter.next() otherwise { break }
consume(item)
}
Rules:
while condition { ... }requiresconditionto have typebool.- Every
whilecondition is evaluated before its corresponding iteration. Condition temporaries are dropped before the body starts under Evaluation Order and Temporaries. while let,while var,if let, andif varare not Nocter syntax.- Optional values do not have dedicated loop syntax; use
otherwise { break }orotherwise { continue }inside an ordinary loop when absence controls iteration. loop { ... }is an infinite loop unless exited bybreak,return, or another terminating control flow.for name in start..<end { ... }loops over a half-open integer range.inis a reserved keyword used by theforheader...<is the half-open range token in rangeforheader syntax.startandendare evaluated once, left-to-right, before the loop begins.startandendmust have the same integer type after literal contextual typing.- The loop variable has the same type as
startandend. - The loop variable is an immutable binding scoped to the loop body.
- If
start >= end, the loop body runs zero times. - The range step is always
+1. breakexits the innermost loop in the current function, method, or closure body.continueskips to the next iteration of the innermost loop in the current function, method, or closure body.- A nested closure is a callable control-flow boundary. Its
breakandcontinuecannot target a loop outside that closure, even when the closure expression is written lexically inside the loop. break valueis not supported.- Loops are statements and do not produce values.
- The reachable result expression of a
while,loop, rangefor, or collectionforbody must have typevoidornever. A value-producing final expression is not implicitly discarded on each iteration; uselet _ = expression. - Exiting a loop runs the normal scope-end
dropbehavior for values whose scopes end. breakandcontinuerun the same cleanup for scopes they leave.
Collection iteration is protocol-driven and has an explicit ownership mode:
for item in &values {
inspect(item)
}
for item in move values {
consume(move item)
}
for item in &+values {
update(item)
}
&expressionselects the source type's readonly expansion operator.&+expressionselects its readwrite expansion operator and holds an exclusive source loan.move placefirst uses the source directly when its type implements the trusted iterator contract. Otherwise the form selects the source type's owned expansion operator. A type that has both a direct iterator implementation and an owned expansion uses direct iteration; this priority is fixed and does not form an overload set.- The
movein a collection-loop source is the ordinary move expression, not a separate capability marker. Its operand must be an existing move-only local, parameter, or eligible named struct field.for item in move make_values()is invalid. Bind a newly produced collection first, then iterate withmove binding. - A bare expression is accepted only when its type already implements the trusted iterator
contract, and ordinary ownership rules still apply. A new iterator temporary may be used
directly. A copyable iterator place is copied. An existing move-only iterator place requires
move place;for item in iteratornever performs an implicit move. - A collection value without
&,&+, ormoveis rejected rather than guessed. - The source expression is evaluated once. Its iterator is owned by the loop and advanced through a validated declaration identity, not a method-name search.
- Each successful step initializes one immutable loop binding. Absence ends the loop without initializing or dropping an element.
continuedrops an unconsumed current element before advancing.break,return, propagation, and normal completion drop live element state and then the iterator exactly once.- A readonly yielded borrow retains its source loan through the borrow's last use. A consuming yielded value owns exactly one transferred drop obligation.
- A readwrite yielded borrow holds one exclusive element loan. It must end before the iterator is advanced again.
Deferred:
- implicit choice between readonly and consuming iteration for a bare collection value
- reverse iteration and custom step syntax
- asynchronous iteration and iterator adapters that require closures
The compiler must not lower collection iteration into calls selected by the spellings iter, into_iter, or next. Expansion operators and the trusted Iterator declaration are selected by declaration identity. See Expansion Operators.
Use range for with indexing:
for i in 0..<bytes.len() {
let byte = bytes[i]
consume(byte)
}
Never and Reachability
never represents a computation that does not return normally.
never is not an ordinary value-carrying type. It is the type of control flow that terminates the current path instead of producing a value.
Typical uses:
trap(): never- a standard-library termination operation from the compiler-checked
std/processcontract - an infinite event loop that has no reachable
break - an explicit unreachable-code marker in the standard library
trap is the primitive boundary for non-recoverable program defects. The compiler may also generate traps for checked operations such as out-of-bounds indexing or invalid arithmetic.
Standard-library termination operations are not compiler primitives.
panic is not a language feature. Nocter has no stack-unwinding mechanism.
Example:
use std/process
func require_path(path: &str?): &str {
let value = path otherwise { process.abort() }
return value
}
Rules:
- A function declared as returning
nevermust not complete normally. - A
neverfunction body must terminate all reachable paths with anothernevercall, a non-breaking infiniteloop, a low-level primitive such astrap, a standard-library terminating API such asabortorexit, or equivalent terminating control flow. returnandreturn valueare not valid in aneverfunction.- Falling off the end of a
neverfunction is a compile error. - A call whose type is
neverterminates the current control path. - Code after
return,break,continue, or anevercall in the same block is unreachable. - Unreachable statements have no runtime semantics. They do not contribute to body result typing, definite-initialization joins, move/drop liveness on later reachable paths, or buildability.
- Unreachable statements after a proven terminal statement are accepted. A future lint may report them, but unreachable code is not a required compile-time error.
- Accepted unreachable statements are still parsed, name-resolved, and type-checked. Visibility,
call arity, generic requirements, member selection, type compatibility, and structural place
requirements such as whether
&+names a writable kind of place remain enforced. - Flow-dependent ownership, initialization, loan-liveness, and provenance checks do not invent a continuation after the terminal statement. The compiler does not report use-after-move, maybe-initialized use, borrow conflicts, or an escaping local provenance solely inside that unreachable continuation.
- Declarations and expressions wholly inside unreachable code may provide semantic identities for diagnostics and editor features, but they never become executable reachability roots.
- A final
_match arm that is unreachable only because all current enum variants were listed is not covered by this relaxed flow rule. That arm is fully checked under the enum fallback rules because a future dependency variant may select it. - A
never-typed expression can appear where another expression type is required because it produces no value. - A
neverexpression does not infer an unknown generic parameter. Another argument, receiver, or enclosing expected type must determine the required data type before the terminating expression is accepted there. nevercannot be constructed or used in a data-bearing type position. It is valid only as a callable result type and as the inferred type of terminating control flow; the complete position rules are specified in Values and Types.nevercannot be the eventual payload of an optional or fallible type. Outcome constructors represent values and do not turn terminating control flow into a value-level state.- Calling a
neverfunction does not imply stack unwinding, statement-end temporary drops, or caller-scopedropexecution. - If cleanup is required before a terminating API such as
exitorabort, the program must perform that cleanup before thenevercall or use a normalreturn,break, orcontinuepath. - Fallible failure is recoverable failure and is valid only through fallible type
T!. trapis non-recoverable failure caused by a program defect, violated compiler check, or impossible execution path.abortis immediate process termination and does not run Nocter cleanup.panicand stack unwinding are not language features.panicis not reserved. A user-defined function namedpanicis ordinary and has no language-defined behavior.
Example:
func require_path_short(path: &str?): &str {
return path otherwise { process.abort() }
}
The otherwise expression above has type &str. The fallback body does not produce a fallback &str; it terminates the current path.
never also satisfies catch block termination:
let file = File.open(path) catch error {
process.abort()
}
Invalid:
func invalid(): never {
return
}
func also_invalid(): i32 {
process.abort()
return 0
}
The first function returns normally. The second contains unreachable code after a never call.
Safety Checks and Build Modes
Safety checks are part of Nocter semantics and remain enabled in every build mode.
Build modes may change diagnostics, debug information, and optimization level. They must not change the safety meaning of a valid Nocter program.
Always-on checks:
- Bounds checks for indexing.
- Integer overflow checks for normal arithmetic.
- Division and remainder by zero checks.
- Signed division and remainder overflow checks for minimum signed value with divisor
-1. - Shift count range checks.
- Forced unwrap of a fallible failure or optional absence through postfix
!. - Invalid live
boolbit-pattern checks where a value can enter from a primitive or ABI boundary. - Invalid enum tag checks where a value can enter from a primitive or ABI boundary.
- Invalid optional or fallible tag checks where a value can enter from a primitive or ABI boundary. Validation recursively follows only the active payload.
- Reaching
unreachable()or an equivalent impossible-path marker.
Rules:
- Debug and release builds have the same trap conditions.
- A build mode must not turn a checked operation into undefined behavior.
- The optimizer may remove a safety check only when it proves that the trap condition cannot occur on that path.
- Removing a check is valid only when the source-level observable behavior is unchanged.
- If a check is statically known to fail, the compiler may emit an unconditional trap for that path.
- General user code has no unchecked arithmetic, unchecked indexing, or unchecked enum-tag operation.
- Wrapping arithmetic is not unchecked arithmetic. It must be exposed through explicit numeric APIs.
- Target overlays and compiler primitive lowering may use target-specific machine instructions internally, but that must not expose undefined behavior to ordinary Nocter code.