6Control Flow
6.1 If / Else
Conditions are parenthesised and bodies are always braced; there is no single-statement form.
import std::fmt::{ println };
if (x > 0) {
println("positive");
} else if (x < 0) {
println("negative");
} else {
println("zero");
}
if may also be used as an expression that evaluates to the value of the chosen branch. When used this way, both arms are required and must produce the same type. An arm that is null converts to any pointer, so the expression has the other arm's type, whichever arm the null is in: if (c) { null } else { b } is a B* for b: B*, and is refused where an unrelated pointer type is declared. The conditional form c ? x : y follows the same rule.
const is_even: boolean = if (n % 2 == 0) { true } else { false };
The expression form has exactly two arms and no else if chain: if (a) { 1 } else if (b) { 2 } else { 3 } is a parse error in expression position. Nest the second if inside the else arm's braces, or use a match:
const sign: i32 = if (n > 0) { 1 } else { if (n < 0) { -1 } else { 0 } };
else if is fine in the statement form above.
6.2 While Loops
import std::fmt::{ printf };
mut i: int = 0;
while (i < 10) {
printf("%d\n", i);
i = i + 1;
}
6.3 For Loops
A C-style for with three components: declare-initialiser, condition, post-update. The loop variable is scoped to the loop body.
import std::fmt::{ printf };
for (mut i: int = 0; i < 10; i++) {
printf("%d\n", i);
}
for (x in <expr>) iterates a sequence. The expression is evaluated exactly once and bound to a hidden mutable local; the parser lowers the loop to a loop { match (iter.next()) { Some(x) => { ... } None => break; } }, where next is std::core::iter::Iterator's and Some/None are std::core::option::Option's - a module that declares its own Option or Iterator changes nothing about the loop. break, continue, and return inside the body bind to the synthesised loop, and the iterator binding is dropped at the end of the enclosing block.
The scrutinee may be:
- An
Iteratordirectly - a type thatimplement trait Iterator<T>, including the stdlib'sRange<T>/RangeInclusive<T>, or a type parameter bounded byIterator. A method that is merely spellednextdoes not make a type iterable: the loop refuses it (E0306, "does not implementIterator"). - A range literal
a..b(half-open) ora..=b(inclusive). These are sugar forRange::new(a, b)/RangeInclusive::new(a, b); see the precedence table in section 5.7. - An iterable that exposes
iter()returning an iterator -Array<T>andSlice<T>(theiriter()is gatedwhere T: Copy). The lowering inserts the.iter()call. - A fixed-size array
[T; N]. The lowering views it as aSlice<T>over itsNelements.
mut sum: i32 = 0;
for (i in 0..5) { // range literal; 0,1,2,3,4
sum = sum + i;
}
// sum == 10
const xs: [i32; 3] = [7, 8, 9];
for (x in xs) { // fixed-size array
sum = sum + x;
}
Range literals are ordinary expressions and may appear anywhere, not only in a for header: const r: Range<i32> = 2..7;. .. binds looser than the arithmetic operators, so a..b + 1 parses as a..(b + 1).
6.4 Loop
loop { ... } is an unconditional infinite loop. Prefer it over while (true); it communicates "this runs until something inside breaks out" without misdirection.
import std::fmt::{ printf };
mut count: int = 0;
loop {
if (count >= 5) { break; }
printf("%d\n", count);
count = count + 1;
}
6.5 Do-While
do {
body();
} while (condition);
The body executes at least once, then the condition is checked.
6.6 Break and Continue
break exits the innermost loop. continue skips to the next iteration.
import std::fmt::{ printf };
for (mut i: int = 0; i < 20; i++) {
if (i % 2 == 0) { continue; }
if (i > 10) { break; }
printf("%d\n", i);
}
6.7 Match: Statement Form
match is the language's primary discriminator. It branches on enum variants, integer values, and other patterns; the compiler enforces that every case is covered.
import std::fmt::{ println };
match (color) {
Color::Red => { println("red"); }
Color::Green => { println("green"); }
Color::Blue => { println("blue"); }
}
There is no fallthrough between arms. See section 7 for the full pattern language.
6.8 Match: Expression Form
Used as an expression, match evaluates to the value of the matching arm. All arms must produce the same type.
const name: string = match (n) {
1 => { "one" }
2 => { "two" }
_ => { "other" }
};
6.9 Ternary
const abs: int = x >= 0 ? x : -x;
The ternary is right-associative: a ? b : c ? d : e parses as a ? b : (c ? d : e).
6.10 Return
function add(a: int, b: int) -> int {
return a + b;
}
return exits the current function. If the function has a non-void return type, a value is required.
6.11 Unsafe Blocks
unsafe { ... } is recognised at parse time and lowers identically to a plain block. It serves as a documentation marker: a visible signal that the enclosed code performs raw pointer arithmetic, calls extern functions, or otherwise sits at the edge of the language's safety story. The compiler does not currently impose any extra restriction outside an unsafe block, and does not relax any check inside one - every operation Cryo permits today is permitted everywhere.
import std::core::intrinsics::{ malloc };
unsafe {
const raw: void* = malloc(64);
// raw pointer manipulation here
}
This is the committed 1.0 behavior: unsafe is a documentation marker and nothing more. It is not reserved to silently become enforcing - 1.0 code will not break under a future release on account of unsafe. Should later versions add safety checks around raw pointer dereference, raw-to-pointer as-casts, or extern calls, they would arrive compatibly (as an opt-in lint/warning first), not as a breaking change to code that already compiles.
6.12 Inline Assembly
asm { ... } embeds target assembly directly, lowering to an LLVM inline-assembly call. The block body is raw assembly - written without string quoting - and Cryo values are bound into it through ${ ... } operand holes. Bare { and } are literal assembly text, so target syntax that uses braces (AVX-512 mask registers such as {k1}, for instance) passes straight through.
A mandatory ![arch(<arch>, <dialect>)] directive must appear immediately above the block. It names the target architecture (which gates the block - see below) and the assembly dialect (intel or att):
![arch(x86_64, intel)]
asm {
mov ${=out}, ${in}
}
Operands. Each ${ ... } hole binds a Cryo variable. A prefix selects its direction and an optional : suffix pins a register or constraint class:
| Form | Meaning |
|---|---|
${x} | input - the value of x is read into a register |
${=x} | output - the result is written back to x |
${+x} | in-out - x is both read and written |
${x:"rax"} | pin the operand to a specific register |
${x:m} | memory operand - x is addressed in memory |
${x:i} | immediate - x must be a compile-time constant |
Referencing the same variable more than once collapses to a single operand, and a variable used as both an input and an output is promoted to in-out. Operands must be register-sized scalars or pointers.
Clobbers. Registers, flags, or memory that the block overwrites but doesn't name as operands are declared with ![clobber(...)], so the compiler doesn't assume their values survive the block:
![arch(x86_64, intel)]
![clobber(rcx, r11, flags, memory)]
asm {
mov rax, ${v}
add rax, rax
mov ${=out}, rax
}
Outputs and results. An asm block is a statement; values leave it through ${=x} / ${+x} operands, and a block may have any number of outputs.
Dialects. intel is destination-first and prefix-less (mov rax, 60); att is source-first with % registers and $ immediates (movq $60, %rax). The dialect is always stated explicitly in the ![arch(...)] directive.
Arch gating. <arch> is matched against the compile target: a block whose arch differs from the target is dropped, exactly like a ![linux] / ![windows] gate. This lets per-architecture blocks sit side by side, each written for its own target:
![arch(x86_64, intel)]
asm { syscall }
![arch(aarch64, att)]
asm { svc #0 }
Module-level assembly. An asm { ... } written at module scope (outside any function, with no operands) emits module-level inline assembly - for naked/global stubs, .globl symbols, or raw data.
A write system call on x86_64 Linux, pulling the buffer and length in as operands:
![arch(x86_64, att)]
![clobber(rcx, r11, memory)]
asm {
movq $1, %rax // SYS_write
movq $1, %rdi // fd = stdout
movq ${buf}, %rsi // buffer pointer
movq ${len}, %rdx // length
syscall
}
Note. LLVM passes the assembly text through to the target assembler unchanged - Cryo does not parse it - so a typo in a mnemonic or register name surfaces as an assembler error at build time, not a Cryo diagnostic. Only
${ ... }introduces an operand; a literal$(an AT&T immediate such as$60) and bare{/}are emitted verbatim.
import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
if (condition) { ... } else if (condition) { ... } else { ... }Branches on a parenthesised boolean condition. Bodies are always braced; there is no single-statement form. Used as an expression, both arms are required and must produce the same type: const sign = if (n < 0) { -1 } else { 1 };.
if (condition) { ... } else { ... }The branch taken when every preceding if condition is false; else if chains another condition. In expression form the else arm is mandatory, so the expression always has a value.
const NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const is_even: booleantrueThe boolean literal. Booleans are a distinct 1-byte type, not interchangeable with integers; if and while conditions must be boolean.
falseThe boolean literal. Booleans are a distinct 1-byte type, not interchangeable with integers; if and while conditions must be boolean.
const sign: i32function printf(fmt: string, args...) -> i32Write a printf-style formatted string to stdout. Returns the number of bytes written, or a negative value on error (libc printf convention). Use this for genuinely variadic %s/%d output; for value formatting prefer an f-string through println (Display-based) below.
mut name: Type = value;A mutable binding, reassignable after declaration - mutability is opt-in, const is the default. At module level it declares mutable global state. Before & it marks an exclusive reference or receiver instead.
mut i: intwhile (condition) { ... }Repeats its body while the condition holds. The condition is checked before every iteration, so the body may run zero times.
language referencemut i: intfor (mut i: i32 = 0; i < n; i++) { ... }
for (item in iterable) { ... }Two forms. The C-style for (init; condition; step) scopes its loop variable to the body. for (x in expr) iterates anything with next() -> Option<T>: an iterator, a range such as 0..n, an Array or Slice (their iter() is inserted for you), or a fixed-size array.
mut i: intmut sum: i32ifor (item in iterable) { ... }Separates the loop variable from the sequence in a for loop. The sequence is evaluated once; the loop lowers to loop { match (it.next()) { Some(x) => .., None => break } }.
start..endA half-open range - start up to but excluding end - and sugar for Range::new(start, end): an iterator usable in for, and an ordinary value anywhere else. It binds looser than arithmetic, so a..b + 1 is a..(b + 1).
mut sum: i32const xs: [i32; 3]xconst xs: [i32; 3]mut count: intloop { ... }An unconditional loop; it runs until a break (or return) leaves it. Prefer it to while (true): it says outright that the exit is somewhere inside.
mut count: intbreak;Exits the innermost enclosing loop immediately - for, while, loop, or do. Inside for (x in ..) it leaves the loop the compiler synthesised around the iterator.
do { ... } while (condition);The body runs once before the condition is first checked, then repeats while it holds.
language referencemut i: intcontinue;Skips the rest of the current iteration: the innermost loop's condition (or the iterator's next()) runs again.
match (subject) {
Pattern(x) => { ... }
_ => { ... }
}The primary discriminator: branches on enum variants, integers, ranges, and destructuring patterns, and the compiler rejects a match that misses a case. There is no fallthrough. As an expression it evaluates to the chosen arm's value, so every arm must produce the same type.
pattern => { body }Separates a match arm's pattern from its body. Arms do not fall through.
const name: stringprimitive stringNUL-terminated raw string (u8*), matching the C ABI. Length-typed text is Str / String.
_const abs: intfunction name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function add(a: int, b: int) -> int(parameter) a: int(parameter) b: intreturn value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
(parameter) a: int(parameter) b: intintrinsic function malloc(size: u64) -> void*unsafe { ... }A documentation marker for code that does raw pointer arithmetic or crosses into extern calls. It lowers to a plain block: no check is relaxed inside it and none is imposed outside it - and 1.0 commits to keeping it that way.
const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const raw: void*![arch(x86_64, intel)]
asm {
mov ${=out}, ${in}
}Embeds raw target assembly as an LLVM inline-assembly call. A preceding ![arch(<arch>, <dialect>)] directive is mandatory and gates the block by target; Cryo values are bound into the text through ${...} operand holes. At module scope, with no operands, it emits module-level assembly.
function add(a: int, b: int) -> intextern "C" function syscall(number: Syscall, ...) -> i64nullThe null pointer literal, valid in any pointer context - including the primitive string, which is a NUL-terminated u8*. Compare with == null; it is not an Option.
Option::Some(T)function name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
Option::Nonetype trait Iterator {
type Item;
next(mut &this) -> Option<This::Item>;
count(mut &this) -> u64;
fold<Acc>(mut &this, initial: Acc, f: (Acc, This::Item) -> Acc) -> Acc;
for_each(mut &this, f: (This::Item) -> void) -> void;
take(this, n: u64) -> TakeIter<This>;
skip(this, n: u64) -> SkipIter<This>;
map<B>(this, f: (This::Item) -> B) -> MapIter<This, B>;
filter(this, pred: (&This::Item) -> boolean) -> FilterIter<This>;
chain<J>(this, other: J) -> ChainIter<This, J>;
// … 7 more
}implement struct Name { ... }
implement trait Trait for Type { ... }
implement u64 { ... }Adds methods to a type outside its declaration. implement struct T (or enum, or a bare primitive) opens an inherent block; implement trait Tr for T supplies a trait's methods for one type. Resolution is at compile time - no dynamic dispatch.
implement trait Trait for Type { ... }Introduces a trait implementation: the block supplies, for one concrete type, the method bodies the trait requires.
language referencetype struct Range<T> {
start: T;
end: T;
}type struct RangeInclusive<T> {
start: T;
end: T;
exhausted: boolean;
}start..=endAn inclusive range - start through end - and sugar for RangeInclusive::new(start, end). Also a match pattern: 1..=5 => { ... }.
static Range::new(start: T, end: T) -> Range<T>static RangeInclusive::new(start: T, end: T) -> RangeInclusive<T>type struct Array<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
length: i64;
capacity: i64;
alloc: A;
}Array<T, A>: growable, heap-backed, contiguous sequence.
std::collections::arraytype struct Slice<T> {
ptr: T*;
length: u64;
}Slice<T>: a borrowed view into a contiguous sequence.
std::core::slicewhere T: Ord + Clone, U: HashConstrains generic parameters to types that implement the listed traits, which is what makes those traits' methods callable on T. Bounds on one parameter join with +; parameters separate with ,. A where on a single method narrows the type's parameter for that method only.
type trait Copy {}Types whose values can be duplicated by a bitwise copy. Integers, floats, pointers, and pure-POD structs qualify; anything that owns resources (heap storage, file handles, refcount) does not.
std::core::markerconst r: Range<i32>extern function exit(code: i32) -> void;
extern "C" {
function puts(s: string) -> i32;
}Declares a function whose body the linker supplies - typically a C library symbol. The compiler trusts the Cryo signature; keeping it in step with the real C signature is on you.
language referencevalue as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.