Control Flow
This file is part of the Nocter language specification. The specification entry point is README.md.
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 root-file function named main is selected as the executable entry point because v0 fixes that entry name. drop is not reserved; inherent destructor declarations and explicit drop statements are contextual source forms.
Parameters are written as name: Type. var name: Type parameters are not part of v0. 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 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 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. - 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. 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
Adopted: v0 uses positional arguments only.
func copy(allocator: &+Allocator, source: &str): String! {
...
}
let text = String.copy(&+allocator, "hello")?
Rules:
- Function, associated function, method, and primitive calls use positional arguments.
- Argument expressions are matched to parameters by position.
- Argument count must match parameter count exactly.
- Each argument must type-check against the corresponding parameter type under the normal contextual typing, ownership, move, copy, and borrow rules.
- 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 are not part of v0.
- Default parameters are not part of v0.
- Variadic functions are not part of v0.
- Function, associated function, and method overload by type, arity, or return type is not part of v0.
- A duplicate callable name in the same namespace is a compile error.
- A trailing comma is allowed in multi-line parameter lists and multi-line argument lists.
- A trailing comma is not allowed in single-line parameter lists or single-line argument lists in v0.
Examples:
pub func copy(
allocator: &+Allocator,
source: &str,
): String! {
...
}
let text = String.copy(
&+allocator,
"hello",
)?
Invalid in v0:
String.copy(allocator: &+allocator, source: "hello") // named arguments
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. Variadic capture is a future ...values design, not v0 syntax.
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
Adopted: 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. - 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. - 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 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.
Adopted: 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.- Payload names introduced by
if expr is Enum.variant(payload)are visible only inside the then body. if expr is Enum.variant(_)checks the variant and discards the payload without introducing a binding.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. - 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 a function.return error_valueexplicitly returns a failure from a fallible function.return noneexplicitly returns absence from an optional function, or success absence from a fallible optional function.
Examples:
let value = if condition {
a
} else {
b
}
return match error {
AppError.open_failed(path) { 1 }
_ { 0 }
}
Removed:
- 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 part of the initial grammar.
- One statement per line is the normal style.
- A newline separates statements where the grammar can end a statement.
- 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 lexical source text and comment rules are specified in Lexical Grammar.
Evaluation Order and Temporaries
Adopted: 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 right-hand side before replacing the target place. The detailed assignment rules are specified in Values and Types.
- 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 ownership is moved into a longer-lived owner.
- 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.
- 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 part of the initial design.
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(allocator, "abc")?).view()
String.copy(...) produces a temporary owned String. .view() 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(allocator, "abc")?
let view = text.view()
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
Adopted: the initial loop forms are while, loop, range 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.while let,while var,if let, andif varare not Nocter syntax.- Optional values do not have dedicated loop syntax in v0; 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 the initialforheader 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 step is always
+1in the initial design. breakexits the innermost loop.continueskips to the next iteration of the innermost loop.break valueis not part of the initial design.- Loops are statements and do not produce values.
- Exiting a loop runs the normal scope-end
dropbehavior for values whose scopes end. breakandcontinuerun the same cleanup for scopes they leave.
Deferred:
for item in expr { ... }- mutable element iteration over
&+[T] - compiler-lowered iteration syntax that treats ordinary names such as
iterornextspecially - reverse iteration and custom step syntax
Collection iteration is not part of the initial for syntax. The compiler must not lower for item in items into calls to methods named iter or next.
Use range for with indexing:
for i in 0..<bytes.len() {
let byte = bytes[i]
consume(byte)
}
Never and Reachability
Adopted: 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(): neverstd/process.abort(): neverstd/process.exit(code): never- 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.
abort and exit are standard-library process APIs. They are not compiler primitives.
panic is not a language feature in v0. No stack unwinding mechanism is part of v0.
Example:
use std/process as 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 v0 buildability requirements.
- v0 accepts unreachable statements after a proven terminal statement. A future lint may report them, but unreachable code is not a required compile-time error.
- A
never-typed expression can appear where another expression type is required because it produces no value. nevercannot be constructed, stored in a variable, used as a field type, or used as an array element type in the initial design.- 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 part of v0.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
Adopted: 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 overflow checks.
- Shift count range checks.
- 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.
- 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 in v0.
- 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.