Control Flow
Control flow in Reussir is expression-oriented. A block computes a value, and so do ifif and matchmatch. There is no separate returnreturn statement that interrupts a function: the value of its body is the value of its final expression. This makes a branch usable anywhere an ordinary value is expected.
A block is a sequence of expressions enclosed by braces. Semicolons separate the expressions, letlet introduces a name for the rest of the block, and the last expression supplies the block’s result:
fn rectangle_area(width: i64, height: i64) -> i64 { let area: i64 = width * height; area}
fn rectangle_area(width: i64, height: i64) -> i64 { let area: i64 = width * height; area}
The annotation on areaarea is optional because its type can be inferred. A name introduced by letlet remains in scope through the rest of that block and does not escape its closing brace. The letlet expression itself has type unitunit; it is the later use of areaarea that produces the function’s i64i64 result.
An empty block has type unitunit. A one-expression block has the type and value of that expression. Unlike Rust, a trailing semicolon is only an optional separator in Reussir: { 1 }{ 1 } and { 1; }{ 1; } both evaluate to 11. Use an empty block or a unit-valued expression when a unit result is required.
Because there is currently no surface returnreturn construct, every route through a function body must naturally produce the declared result type. Put the value to return last, or make the final expression an ifif or matchmatch whose branches produce it.
An ifif condition must have type boolbool; numbers and other values are not implicitly truthy. Both branches are blocks, and their final values must have the same type:
fn absolute(x: i64) -> i64 { if x < 0 { -x } else { x }}
fn absolute(x: i64) -> i64 { if x < 0 { -x } else { x }}
Here the entire ifif has type i64i64. It is already the final expression, so no assignment or return statement is needed.
An ifif may omit elseelse, but only for conditional work that returns unitunit. The omitted branch behaves as an empty else {}else {}; consequently, the thenthen block must also be unit-valued. An ifif without elseelse cannot conditionally produce an i64i64, for example, because one route would have no i64i64 value.
The current grammar requires a block immediately after elseelse. To express an else ifelse if chain, put the next conditional inside that block:
fn classify(n: i64) -> i64 { if n < 0 { -1 } else { if n == 0 { 0 } else { 1 } }}
fn classify(n: i64) -> i64 { if n < 0 { -1 } else { if n == 0 { 0 } else { 1 } }}
There is one syntactic ambiguity worth knowing. In a condition or a matchmatch scrutinee, Name {Name { normally begins the following block or arm list, so a braced constructor expression in that position must be parenthesized:
struct Flag { enabled: bool }fn from_constructed_flag() -> i64 { if (Flag { enabled: true }.enabled) { 1 } else { 0 }}
struct Flag { enabled: bool }fn from_constructed_flag() -> i64 { if (Flag { enabled: true }.enabled) { 1 } else { 0 }}
Parentheses are not necessary for an ordinary variable, field access, or function call in the same position.
matchmatch selects an arm by the shape or value of one expression. It evaluates the scrutinee, tries arms in source order, and evaluates the body of the first matching arm whose guard succeeds.
Start with a closed enum and cover each of its constructors:
enum Direction { North, South, East, West}fn is_horizontal(direction: Direction) -> bool { match direction { Direction::East => true, Direction::West => true, Direction::North => false, Direction::South => false }}
enum Direction { North, South, East, West}fn is_horizontal(direction: Direction) -> bool { match direction { Direction::East => true, Direction::West => true, Direction::North => false, Direction::South => false }}
Every arm has the form pattern => expressionpattern => expression. A block may be used as the arm expression when it needs bindings or several steps. Commas separate arms, but the current grammar does not allow a comma after the final arm. As with ifif, the arm bodies must agree on one result type; this matchmatch therefore has type boolbool.
Reussir currently provides four pattern families:
-
__matches any value and does not bind it. -
A single lowercase name, such as
valuevalue, matches any value and binds it for that arm’s guard and body. -
Integer, Boolean, character, and string literals match equal values, such as
00,falsefalse,'x''x', and"ready""ready". Floating-point patterns are not supported. A negative number is expressed with a guard because--is an expression operator, not part of an integer pattern. -
A constructor pattern selects an enum variant and may recursively match its payload, such as
Option::Some(value)Option::Some(value)orTree::LeafTree::Leaf.
A qualified name or a name beginning with an uppercase letter is read as a constructor. A single lowercase name is a binding. Qualifying enum variants, as in Direction::EastDirection::East, makes that distinction explicit and avoids accidental bindings. There are not yet alternative (||) or range patterns; write separate arms for separate cases.
An unguarded wildcard or binding makes a match exhaustive when placed as the final fallback. The difference is whether the value is needed:
fn classify_code(code: i64) -> i64 { match code { 0 => 100, 1 => 200, other => other }}
fn classify_code(code: i64) -> i64 { match code { 0 => 100, 1 => 200, other => other }}
Use __ instead of otherother when the fallback body does not inspect the value.
Constructor patterns can decompose several payload slots at once. Parentheses give the positional form:
enum Packet { Data(i64, i64, i64), Empty}fn checksum(packet: Packet) -> i64 { match packet { Packet::Data(first, second, third) => first + second + third, Packet::Empty => 0 }}
enum Packet { Data(i64, i64, i64), Empty}fn checksum(packet: Packet) -> i64 { match packet { Packet::Data(first, second, third) => first + second + third, Packet::Empty => 0 }}
The braced constructor spelling can be used for the same payload slots:
fn checksum_braced(packet: Packet) -> i64 { match packet { Packet::Data { first, second, third } => first + second + third, Packet::Empty => 0 }}
fn checksum_braced(packet: Packet) -> i64 { match packet { Packet::Data { first, second, third } => first + second + third, Packet::Empty => 0 }}
For enum variants, both forms bind payloads in declaration order. The braced form does not turn the slots into reorderable named fields. Ordinary structstruct values are currently read with field projections such as point.xpoint.x; matchmatch deconstructs enum variants rather than structs.
A trailing .... ignores all remaining payload slots. Without it, a constructor pattern must account for every slot:
fn first_item(packet: Packet) -> i64 { match packet { Packet::Data(first, ..) => first, Packet::Empty => 0 }}
fn first_item(packet: Packet) -> i64 { match packet { Packet::Data(first, ..) => first, Packet::Empty => 0 }}
Nullary variants need no delimiters: Packet::EmptyPacket::Empty, Packet::Empty()Packet::Empty(), and Packet::Empty {}Packet::Empty {} denote the same constructor pattern.
A constructor’s payload patterns may themselves be constructors, constants, bindings, or wildcards. This makes the structure of a case visible without a second matchmatch:
enum Color { Red, Green, Blue}enum Paint { Solid(Color), Clear}fn is_red(paint: Paint) -> bool { match paint { Paint::Solid(Color::Red) => true, Paint::Solid(_) => false, Paint::Clear => false }}
enum Color { Red, Green, Blue}enum Paint { Solid(Color), Clear}fn is_red(paint: Paint) -> bool { match paint { Paint::Solid(Color::Red) => true, Paint::Solid(_) => false, Paint::Clear => false }}
The second arm covers every SolidSolid payload not handled by the first. Omitting it would leave Solid(Green)Solid(Green) and Solid(Blue)Solid(Blue) uncovered, even though the outer SolidSolid constructor appears in another arm. Exhaustiveness checking follows the complete nested pattern, not only its outermost constructor.
The built-in nullable type follows the same model. Nullable::NonNull(pattern)Nullable::NonNull(pattern) exposes its contained value, while Nullable::NullNullable::Null has no payload:
struct Boxed { value: i64 }fn read_or(value: Nullable<Boxed>, fallback: i64) -> i64 { match value { Nullable::NonNull(boxed) => boxed.value, Nullable::Null => fallback }}
struct Boxed { value: i64 }fn read_or(value: Nullable<Boxed>, fallback: i64) -> i64 { match value { Nullable::NonNull(boxed) => boxed.value, Nullable::Null => fallback }}
Append if conditionif condition to a pattern to test something the pattern language does not express. Bindings from the pattern are available in the guard:
fn sign(n: i64) -> i64 { match n { value if value < 0 => -1, 0 => 0, _ => 1 }}
fn sign(n: i64) -> i64 { match n { value if value < 0 => -1, 0 => 0, _ => 1 }}
The guard must produce boolbool. If it is false, matching resumes at the next arm with the same scrutinee. A guarded wildcard or binding is not exhaustive by itself because its guard may fail, so signsign still needs the final __ arm.
Arm order matters whenever patterns overlap. Put specific cases before broad bindings and wildcards. An unguarded wildcard or binding prevents every later arm from being selected.
The compiler rejects a matchmatch when some possible value has no arm. For enums, that means every variant and every nested variant case must be covered. For boolbool and NullableNullable, both cases must be covered. Integer, character, and string domains are open, so a finite list of literal arms always needs __ or a binding fallback.
The compiler also warns about an arm that can never be selected, as in this ordering:
fn redundant(direction: Direction) -> bool { match direction { _ => false, Direction::East => true }}
fn redundant(direction: Direction) -> bool { match direction { _ => false, Direction::East => true }}
Direction::EastDirection::East is unreachable because __ has already accepted every direction. Reordering the specific arm first makes both arms useful.
The current surface language has no whilewhile, forfor, looploop, breakbreak, or continuecontinue constructs. Express repetition with recursion and make the stopping case explicit with ifif or matchmatch. The most direct definition of a sum calls itself on the smaller problem:
fn sum_to(n: u64) -> u64 { if n == 0 { 0 } else { n + sum_to(n - 1) }}
fn sum_to(n: u64) -> u64 { if n == 0 { 0 } else { n + sum_to(n - 1) }}
This version is recursive, but it is not tail recursive. After sum_to(n - 1)sum_to(n - 1) produces its result, the caller must still add nn. Each suspended call therefore retains its pending addition and the state needed to finish it. For n = 3n = 3, evaluation expands before it can collapse:
sum_to(3)= 3 + sum_to(2)= 3 + (2 + sum_to(1))= 3 + (2 + (1 + sum_to(0)))= 3 + (2 + (1 + 0))= 6
sum_to(3)= 3 + sum_to(2)= 3 + (2 + sum_to(1))= 3 + (2 + (1 + sum_to(0)))= 3 + (2 + (1 + 0))= 6
An accumulator moves that pending work into an explicit argument:
fn sum_to_impl(n: u64, acc: u64) -> u64 { if n == 0 { acc } else { sum_to_impl(n - 1, acc + n) }}fn sum_to(n: u64) -> u64 { sum_to_impl(n, 0)}
fn sum_to_impl(n: u64, acc: u64) -> u64 { if n == 0 { acc } else { sum_to_impl(n - 1, acc + n) }}fn sum_to(n: u64) -> u64 { sum_to_impl(n, 0)}
Now the recursive call is the final expression of the elseelse branch. Its result is also the branch’s result; no addition or other work remains after the call returns. The same input advances as a sequence of complete accumulator states:
sum_to_impl(3, 0)→ sum_to_impl(2, 3)→ sum_to_impl(1, 5)→ sum_to_impl(0, 6)→ 6
sum_to_impl(3, 0)→ sum_to_impl(2, 3)→ sum_to_impl(1, 5)→ sum_to_impl(0, 6)→ 6
This tail-recursive shape exposes loop-like control flow and carried state to the optimizer, creating an opportunity to reuse a frame or lower the recursion to a loop. Tail-call elimination is not a source-language guarantee, however; the generated machine code still depends on the selected optimization pipeline and target.