1Lexical Structure
This section describes the building blocks the lexer recognises before any syntactic or semantic meaning is assigned.
1.1 Identifiers
An identifier begins with a letter (a-z, A-Z) or underscore, followed by any sequence of letters, digits, and underscores.
identifier = letter { letter | digit | "_" }
The compiler does not enforce naming, but the standard library and ecosystem use the following conventions, which the bundled TextMate grammar and LSP also assume:
snake_casefor variables, functions, methods.PascalCasefor types (struct, union, enum, trait, alias).SCREAMING_SNAKE_CASEfor compile-time constants.
1.2 Keywords
Keywords are reserved identifiers. They cannot be used as variable, function, or type names — but they can name a member: a field or a method, declared and used.
![repr(C)]
type struct VkDescriptorPoolSize {
type: u32; // the C header's own field name
count: u32;
}
mut p: VkDescriptorPoolSize = VkDescriptorPoolSize { type: 1, count: 4 };
p.type = 2;
A member position is never a declaration or an operator, so the keyword is unambiguous there: what follows the name says which member it is — ( or < a method, : a field — and after . or :: a token can only be a member name. This exists because a bound C struct's field names are not Cryo's to choose; a binding generator cannot rename type without breaking the ABI it exists to preserve.
The exceptions are the keywords a member position reads first: public and private open a visibility label, static and async are member modifiers, and protected, virtual and override are read there to be refused (E0117). A field with one of those seven names cannot be declared, and a C struct carrying one still needs a ![repr(C)] shadow struct.
A binding is the other way round. A statement that begins with a keyword opening a declaration is parsed as that declaration, so a local named by one could never be assigned or called through: module = q; is not an assignment. The 19 keywords that open a declaration - function, const, mut, type, struct, class, enum, trait, import, module, extern, implement, public, private, protected, namespace, intrinsic, static, async - therefore cannot name a variable, a parameter, a pattern binding or a destructured local (E0100): a keyword is never an identifier. Keywords that open nothing (default, string) remain legal names, and a destructuring shorthand over a field named by one of the 19 needs a rename: const { type: t }: S = s;.
const module: i32 = 42; // E0100: expected a variable name, found `module`, which is a reserved word
match (r) { R::D(static) => { .. } } // E0100: expected a binding name
| Control flow | Declarations | Modifiers | Operator keywords | Special values | Reserved for future use | |
|---|---|---|---|---|---|---|
if | function | from | const | new | true | yield |
else | class | as | mut | delete | false | auto |
match | struct | implement | static | sizeof | null | unsigned |
while | enum | intrinsic | public | alignof | this | tuple |
for | trait | where | private | typeof | This | optional |
loop | type | extern | protected | in | with | |
do | namespace | virtual | as | |||
break | module | override | await | |||
continue | import | inline | ||||
return | export | unsafe | ||||
asm | static_assert | move | ||||
union | async | |||||
default |
class, protected, virtual (outside a trait object's type), override and default open
no form of the language: they are reserved, so a declaration or member written with one is
refused where it is written (E0117) rather than misread.
move marks a closure that captures its environment by move (see section 15.3).
async marks a function, method, or trait method whose body is compiled into a state machine and whose call returns a future; await suspends the enclosing async body until a future completes (see section 18).
Reserved-for-future-use keywords are recognised by the lexer; the parser may accept them in places that have no semantic implementation. See section 21.
1.3 Comments
Cryo recognises four comment styles. Documentation comments are semantically meaningful: they attach to the declaration that follows them and surface in LSP hovers and generated documentation.
// Line comment.
/* Block comment.
Spans multiple lines. */
/// Outer documentation comment (line form). Attaches to the next declaration.
/// Multiple consecutive /// lines are joined.
/** Outer documentation comment (block form). Attaches to the next declaration. */
///! Inner documentation comment. Attaches to the enclosing module/namespace.
1.4 Literals
Numeric Literals
Integer literals support four bases. Underscores are visual separators that the compiler ignores. A type suffix pins the literal to a width; without one, the type is inferred from context (defaulting to i32 for integers and f64 for floats).
42 // decimal
1_000_000 // separators: identical to 1000000
0xFF // hex
0b1010 // binary
0o755 // octal
42u64 // typed: unsigned 64-bit
42i8 // typed: signed 8-bit
3.14 // float (defaults to f64)
3.14f32 // explicit 32-bit float
1.0e10 // scientific notation
2.5e-3f64 // scientific with explicit type
Type suffixes: u8 u16 u32 u64 u128 i8 i16 i32 i64 i128 usize isize f32 f64
A literal stored where an integer of a written type is - a constant's or a variable's initializer, an assignment's value, a call's argument - takes that type, and must hold its value there. So must an expression made only of literals: its value is what the arithmetic gives, and a type that does not hold it is refused (E0010) rather than wrapped. A value is held up to the type's unsigned maximum (a bit pattern) and, for a signed type, down to its minimum.
const a: u8 = 100 + 100; // 200: held
const b: u8 = 200 + 100; // error: 300 does not fit `u8`, and would silently truncate
const c: i8 = 0 - 128; // -128: held
Trap. Integer literals exceeding
i64::MAX(e.g.0xFFFF_FFFF_FFFF_FFFF) wrap to negative when used inline against au64operand. Hoist the literal into aconst u64 NAME = ...binding to compare correctly.
String and Character Literals
Strings are enclosed in double quotes; characters in single quotes. Both share the same set of escape sequences.
"Hello, world!"
"line one\nline two"
'A'
'\n'
'\x41' // hex byte: equivalent to 'A'
Escape sequences:
| escape | value |
|---|---|
\n \t \r | newline (10), tab (9), carriage return (13) |
\a \b \f \v | bell (7), backspace (8), form feed (12), vertical tab (11) |
\\ \' \" | the backslash or quote itself |
\xHH | the byte one or two hex digits spell |
\OOO | the byte one to three octal digits spell |
\u{H...} | the Unicode code point one or more hex digits spell, up to 10FFFF |
An octal escape is one to three digits 0-7, read as C reads them: the escape ends after three digits or at the first character that is not one, so \0 is NUL, "\033[1m" is ESC and then [1m, \177 is 127, and "\0123" is \012 (newline) and then 3. \8 and \9, and an octal value above \377, are refused (E0013).
In a string, \u{...} is the code point's UTF-8 bytes, so "\u{e9}" is the two bytes C3 A9; in a character it is the code point itself, so '\u{42}' is 'B'. What a char holds for a code point above 255 depends on char's width, which is not yet decided. A malformed \u (no braces, no digit, or past 10FFFF), a \x with no hex digit, and an escape this table does not name (\q) are kept as written in a string; in a character, an escape the table does not name is the character after the backslash.
A string literal keeps every byte it spells, a NUL among them: the constant the compiler emits for "a\0b" is a, NUL, b and the terminating NUL. A string is read up to its first NUL by what measures it, so "a\0b".length() is 1 while the bytes after the NUL are still there to be read through a pointer.
Raw strings (r"...") are reserved but not yet implemented - see section 21.
f-strings (string interpolation)
An f-string, prefixed with f, builds an owned String by interpolating
expressions written inside {...}:
const x: i32 = 42;
const opt: Option<i32> = Option::Some(7);
const s: String = f"x = {x}, opt = {opt:?}"; // "x = 42, opt = Some(7)"
-
{expr}formatsexprthrough theDisplaytrait;{expr:?}formats it throughDebug. Any type implementing the relevant trait works, includingOption,Result, andArray<T>. -
The embedded expression is a full expression:
f"{a + b}",f"{p.x}",f"{m.get(k)}". Because a hole holds source rather than literal text, quoting nests inside it - a string or char literal written in a hole is its own literal, and its quotes and braces are not f-string syntax:f"panel {if (shown) { "shown" } else { "hidden" }}"Only a
"outside every{...}closes the f-string. -
A hole may carry a format spec after a colon:
{expr:[[fill]align][#][0][width][.precision][type]}alignis<(left),>(right), or^(centre); the default is left.fillis any single character placed before the alignment ({n:*>6}→****42).widthis a minimum field width counted in Unicode scalars; shorter values are padded withfill(space by default).- A leading
0zero-pads numbers, keeping the sign and any radix prefix leftmost ({−5:06}→-00005,{255:#06x}→0x00ff). .precisiontruncates a Display value to that many scalars ({"hello":.3}→hel) and sets the fractional digits of a float ({pi:.2}→3.14).typeselects an integer radix:x/X(hex),o(octal),b(binary);#adds the0x/0o/0bprefix. A radix formats the value's own-width two's-complement bits, so{(-5i32):x}→fffffffb.- The spec separator is the first top-level
:. A hole that mixes a ternary with a spec must parenthesise the ternary (f"{(c ? a : b):>4}"); a barea ? b : c(spaces around:) is not mistaken for a spec.
-
{{and}}produce literal{and}. Standard escape sequences in the literal text are processed as in a normal string. -
The result is a heap-backed
Stringthe caller owns (and drops). The parser desugars the whole f-string to calls intostd::fmt::interp, which is auto-imported into any module that uses one.
For raw, untyped formatted output (C printf semantics, %d/%s
specifiers, not type-checked), use printf from std::fmt. It is an
ordinary variadic function, not an intrinsic, and it is not in the
prelude: write import std::fmt; and call fmt::printf(...), or
import std::fmt::{ printf }; to call it bare. For typed,
Display-formatted output, build a String (usually with an f-string) and
pass it to print / println from std::fmt; both take a single
already-formatted argument (println(f"{x}")), not a printf-style format
string with trailing values.
Boolean and Null Literals
true
false
null // null pointer; valid in any pointer context
Booleans are strict. Nothing converts to boolean, implicitly or with as: no integer, char, float, pointer, enum, Option or Result has a truth value. Every place that wants a truth value requires exactly boolean: an if, while, do/while or for condition, a match guard, an if expression, the operands of &&, || and !, and any value assigned, passed or returned as a boolean. A test is written as the comparison it means:
if (n != 0) { ... } // not `if (n)`; `n as boolean` is refused too
while (p != null) { ... } // not `while (p)`
if (c == Color::Red) { ... } // not `if (c)`, for any enum
The other direction stays: boolean as an integer type is 1 for true and 0 for false.
type struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct VkDescriptorPoolSizetype Item;
type Output = i64;An associated type on a trait: named in the trait body, filled in by each implementation, and referred to as This::Item or I::Item.
VkDescriptorPoolSize.count: u32mut 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 p: VkDescriptorPoolSizetype struct VkDescriptorPoolSizetype Name = Existing;A type alias: a new, transparent name for an existing type - the two are interchangeable everywhere. Aliases may be generic: type StringResult<T> = Result<T, string>;.
VkDescriptorPoolSize.count: u32mut p: VkDescriptorPoolSizeconst 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.
public module collections::array;In a directory's _module.cryo aggregator, lists a submodule and whether it is public. Importing the directory's namespace sees only the modules declared public here.
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.
static name(param: Type) -> Ret { ... }A method that belongs to the type rather than an instance, called as Type::name(..). For structs, static new(..) returning a struct literal is the idiomatic constructor.
pattern => { body }Separates a match arm's pattern from its body. Arms do not fall through.
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).
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 a: u8const b: u8const c: i8const x: i32const opt: Option<i32>Option::Some(T)const s: Stringtype struct String<A = GlobalAlloc> {
buffer: RawBuffer<u8, A>;
length: u64;
}String: owned, heap-allocated, length-typed UTF-8.
std::collections::stringconst x: i32(parameter) opt: ?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.
trueThe 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.
nullThe 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.
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.
while (condition) { ... }Repeats its body while the condition holds. The condition is checked before every iteration, so the body may run zero times.
language referenceconst c: i8const a: u8public function name() { ... }
public:Visible to any module that imports this one - the default for top-level items and for struct fields, so it is mostly written to be explicit. public: opens a visibility block that groups the members after it.
private type struct Name { ... }
private:On a field: only the declaring type's own methods may touch it (E0353). On a top-level item: confined to its own module, and naming a private type elsewhere is E0503. private: opens a block of such members.
async function name(param: Type) -> T { ... }Compiles the function into a stackless state machine. Calling it builds a future whose output is the declared return type; nothing runs until that future is polled. Allowed on free functions, methods, and trait methods.
language reference// reservedOpens no form of the language: a declaration or member written with it is refused where it is written (E0117). A struct's fields and methods are public or private, and both are scoped to the declaring module.
virtual &Trait
virtual mut &Trait
virtual Trait*Makes a trait object: a value of any type implementing the trait, held with a table of that type's methods for it, so code taking one is compiled once for every implementing type. The three forms borrow the value, borrow it mutably, or point at it. Written on anything but a trait it is refused (E0316); written on a method, it is E0117.
language reference// reservedOpens no form of the language: a declaration or member written with it is refused where it is written (E0117). An implementation replaces a trait's default method by writing the method.
language referencefunction 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.
// reservedOpens no form of the language: a declaration or member written with it is refused where it is written (E0117). Declare a type struct and implement a trait for it; code that takes any type implementing the trait takes a trait object, virtual &Trait.
type enum Name {
Unit,
Payload(T),
Named { x: i32 },
}An algebraic data type. Variants may be bare or carry a tuple or struct payload, and a match on an enum must cover every variant. Name::Variant names a variant; methods are added with implement enum Name { ... }.
type trait Name : Base {
method(&this, other: &This) -> Ret;
}Names a set of methods a type may implement. Inside the body This is the implementing type; methods may carry default bodies, and a trait may require a base trait (type trait Ord : Eq). Generic code asks for a trait with where T: Name.
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).
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 referenceimplement 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.
namespace app::module;Every source file opens with one: its position in the module hierarchy. The compiler uses it to resolve imports and to mangle symbol names, so two parse functions in different modules never collide at link time.
intrinsic function name(param: Type) -> Ret;Declares a function the compiler lowers directly to LLVM IR rather than calling. Intrinsics are the contract between the standard library and the compiler (malloc, memcpy, printf, ...); user code does not normally declare its own.
// reservedOpens no form of the language: a declaration or member written with it is refused where it is written (E0117). It still names a method: Default::default() is the trait's constructor.
primitive stringNUL-terminated raw string (u8*), matching the C ABI. Length-typed text is Str / String.
const s: String// reservedA reserved keyword with no syntax of its own in the current grammar. The standard library uses it freely as a method and parameter name (Str::from, from: u64).
new T { field: value }
new E::Variant(args)
new T[n]Heap allocation. new T { .. } places a struct literal on the heap and yields a T*, and new E::V(args) does the same for an enum variant; new T[n] reserves n uninitialised T - the typed malloc. The storage comes from T's Allocate implementation, or else the global allocator. Parentheses after a struct's name are refused (E0117): there are no constructors, and a struct's static new(..) is an ordinary method.
// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning. The parser accepts a yield expression, but no generator semantics exist; async / await are the implemented form of suspension.
value 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.
delete p;Releases what new T { .. } allocated: the pointee's Drop and the values its fields own, then its storage, through T's Allocate::release. Memory from new T[n] or malloc is released with free instead.
// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning.
language referencesizeof(T)The size of a type in bytes, as a compile-time constant - usable in static_assert. alignof(T) gives the alignment.
alignof(T)The alignment of a type in bytes, as a compile-time constant - usable in static_assert. sizeof(T) gives the size.
&this
mut &this
thisThe receiver of a method. Its first parameter declares how the method takes it: &this borrows read-only, mut &this borrows exclusively, and this / mut this move the value in and consume it. In the body, this.field reaches the members.
// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning. Tuple types are written (A, B).
for (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.
where 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.
typeof(expr)The static type of an expression, usable anywhere a type is expected: bindings, pointer and optional wrappers, generic arguments, as targets. The expression is type-checked but never evaluated.
ThisThe implementing type, inside a trait or implement body. other: &This in a trait method is a reference to whichever type implements it; This::Item names an associated type.
// reservedReserved for future use: the lexer recognises it, but no current syntax gives it meaning. Optional types are written T?, sugar for Option<T>.
loop { ... }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.
for (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 } }.
do { ... } while (condition);The body runs once before the condition is first checked, then repeats while it holds.
language referencebreak;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.
await futureSuspends the enclosing async body until the operand - which must implement Future - completes, and evaluates to its output. A prefix operator allowed anywhere an expression is (conditions, arms, guards), but only inside async code.
continue;Skips the rest of the current iteration: the innermost loop's condition (or the iterator's next()) runs again.
![inline]
function hot_path(x: i32) -> i32 { ... }A modifier keyword reserved for inlining hints. The supported spelling today is the ![inline] attribute on a function.
return 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.
// reservedListed among the declaration keywords, but the reference defines no export form today: top-level items are public by default, and private is what narrows them.
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.
![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.
static_assert(sizeof(T) == 4, "message");A module-scope, compile-time assertion. The condition is folded after layouts are computed - literals, sizeof, alignof, and the arithmetic, comparison, logical, and bitwise operators - and a false or non-constant condition fails the build (E0237). Its main use is checking that a struct matches a C layout.
move (params) -> Ret { body }Prefixes a lambda that captures its environment by move: captured outer bindings are consumed where the lambda is built and released by a synthesised Drop at scope exit. Capturing a non-Copy value already gives a lambda move semantics; move states it explicitly.
type union Name {
i: i64;
f: f64;
}An untagged, C-style union: every field shares the same storage at offset 0, so the size is that of the largest member. Writing one field and reading another reinterprets the bytes; there is no runtime tag.
language referenceprimitive usizePointer-width unsigned integer; the natural type for sizes and indices.
primitive typeconst u64const b: u8const x: i32string::length(&this) -> u64type trait Display {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError>
where W: FmtWrite;
}Display / Debug / Formatter.
std::fmt::displaytype trait Debug {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError>
where W: FmtWrite;
}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::arrayfunction 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.