10 Declarations
A declaration binds an identifier to a program entity: a function, type, trait, implementation, constant, or module.
Grammar: See grammar.ebnf § DECLARATIONS
10.0 General
10.0.1 Predeclared identifiers
Certain identifiers are predeclared in the universe scope (see 11.1). They are visible in all modules without explicit import and may be shadowed by user declarations.
Types: int, float, bool, str, byte, char, void, Never, Duration, Size
Compound types: Option, Result, Error, Range, Set, Ordering
Prelude types: TraceEntry, PanicInfo, FormatSpec, Alignment, Sign, FormatType, CancellationError, CancellationReason
Traits: Eq, Comparable, Hashable, Printable, Formattable, Debug, Clone, Default, Drop, Len, IsEmpty, Iterator, DoubleEndedIterator, Iterable, Collect, Into, Traceable, Index, Sendable
Operator traits: Add, Sub, Mul, Div, FloorDiv, Rem, Pow, MatMul, Neg, Not, BitNot, BitAnd, BitOr, BitXor, Shl, Shr, As, TryAs
Constructors: Some, None, Ok, Err, Less, Equal, Greater
Functions: print, panic, todo, unreachable, dbg, assert, assert_eq, assert_ne, assert_some, assert_none, assert_ok, assert_err, assert_panics, assert_panics_with, len, is_empty, is_some, is_none, is_ok, is_err, compare, min, max, hash_combine, repeat, is_cancelled, drop_early, compile_error, embed, has_embed
See Annex C for built-in function semantics.
10.0.2 Uniqueness of identifiers
Within a scope, an identifier shall refer to at most one declaration. Two declarations in the same scope with the same identifier are a compile-time error, with the following exceptions:
- Function clauses: Multiple declarations of the same function name in the same scope define multi-clause pattern matching (see 10.1.1).
- Impl blocks: Multiple
implblocks for the same type are permitted. - Trait implementations: Implementations of different traits for the same type are permitted.
A type and a function with the same name in the same scope is an error. Overloading by parameter types is not supported.
NOTE Identifiers in inner scopes may shadow identifiers from outer scopes. Shadowing does not create a conflict; it temporarily hides the outer binding. See 13.3.1.
10.0.3 Exported identifiers
An identifier prefixed with pub in its declaration is exported and visible to importing modules. The pub keyword applies to:
- Functions:
pub @name - Types:
pub type Name - Constants:
pub let $name - Traits:
pub trait Name - Default implementations:
pub def impl Trait - Extensions:
pub extend Type
pub does not apply to local bindings, parameters, or loop variables.
Visibility of pub type is type-level: exporting a type exports its name and constructors. Field access is restricted to the defining module.
Trait method visibility is inherited from the trait’s visibility.
See Clause 18 for import and re-export rules.
10.0.4 Forward references
Top-level declarations are visible throughout the entire module, regardless of textual order. A function may call another function defined later in the file. Mutual recursion between top-level functions is permitted.
Local bindings are not order-independent. A reference to a local binding before its let declaration is a compile-time error.
EXAMPLE
// Top-level: order-independent
@is_even (n: int) -> bool = if n == 0 then true else is_odd(n - 1);
@is_odd (n: int) -> bool = if n == 0 then false else is_even(n - 1);
@f () -> int = {
let $a = b; // error: 'b' is not yet declared
let $b = 1;
$a + $b
}
Recursive types are permitted when the recursion passes through an indirection (such as Option, a list, or a sum variant). A struct field whose type is the struct itself (without indirection) is a compile-time error because it would require infinite size.
10.1 Functions
@add (a: int, b: int) -> int = a + b;
pub @identity<T> (x: T) -> T = x;
@sort<T: Comparable> (items: [T]) -> [T] = ...;
@fetch (url: str) -> Result<str, Error> uses Http = Http.get(url);
@prefix required- Return type required (
voidfor no value) - Parameters are immutable (except
self— see 13.5) - Private by default;
pubexports usesdeclares capability dependencies
10.1.1 Multiple Clauses
A function may have multiple definitions (clauses) with patterns in parameter position:
@factorial (0: int) -> int = 1;
@factorial (n) -> int = n * factorial(n - 1);
@fib (0: int) -> int = 0;
@fib (1) -> int = 1;
@fib (n) -> int = fib(n - 1) + fib(n - 2);
Clauses are matched top-to-bottom. All clauses shall have:
- Same name
- Same number of parameters
- Same return type
- Same capabilities
The first clause establishes the function signature:
- Visibility:
pubonly on first clause - Generics: Type parameters declared on first clause; in scope for all clauses
- Type annotations: Required on first clause parameters; optional on subsequent clauses
pub @len<T> ([]: [T]) -> int = 0;
@len ([_, ..tail]) -> int = 1 + len(tail);
Guards use if before =:
@abs (n: int) -> int if n < 0 = -n;
@abs (n) -> int = n;
All clauses together shall be exhaustive. The compiler warns about unreachable clauses.
10.1.2 Default Parameter Values
Parameters may specify default values:
@greet (name: str = "World") -> str = `Hello, {name}!`;
@connect (host: str, port: int = 8080, timeout: Duration = 30s) -> Connection;
- Callers may omit parameters with defaults
- Named arguments allow any defaulted parameter to be omitted, not just trailing ones
- Default expressions are evaluated at call time, not definition time
- Default expressions shall not reference other parameters
greet() // "Hello, World!"
greet(name: "Alice") // "Hello, Alice!"
connect(host: "localhost") // uses default port and timeout
connect(host: "localhost", timeout: 60s) // override timeout only
See Expressions § Function Call for call semantics.
10.1.3 Variadic Parameters
A variadic parameter accepts zero or more arguments of the same type:
@sum (numbers: ...int) -> int =
numbers.fold(initial: 0, op: (acc, n) -> acc + n);
sum(1, 2, 3) // 6
sum() // 0 (empty variadic)
Inside the function, the variadic parameter is received as a list.
Constraints:
| Rule | Description |
|---|---|
| One per function | At most one variadic parameter allowed |
| Shall be last | Variadic parameter shall appear after all required parameters |
| No default | Variadic parameters cannot have default values (default is empty list) |
| Positional at call | Variadic arguments are always positional; parameter name cannot be used |
@log (level: str, messages: ...str) -> void;
log(level: "INFO", "Request", "User: 123") // Named + variadic
@max (first: int, rest: ...int) -> int; // Requires at least one argument
Allowed variadic types:
| Type | Example | Behavior |
|---|---|---|
| Concrete | ...int | All arguments shall be int |
| Generic | ...T | Type inferred from arguments |
| Trait object | ...Printable | Arguments boxed as trait objects |
@print_all<T: Printable> (items: ...T) -> void;
print_all(1, 2, 3) // T = int
print_all(1, "a") // ERROR: cannot unify int and str
@print_any (items: ...Printable) -> void;
print_any(1, "hello", true) // OK: all implement Printable
Spread into variadic:
The spread operator ... expands a list into variadic arguments:
let nums = [1, 2, 3];
sum(...nums) // 6
sum(0, ...nums, 10) // 14
Spread in non-variadic function calls remains an error.
Type inference:
When a generic type parameter is only constrained by a variadic parameter, calls with zero arguments cannot infer the type:
@collect<T> (items: ...T) -> [T] = items;
collect(1, 2, 3) // T = int inferred
collect() // ERROR: cannot infer T
collect<int>() // OK: explicit type
Function type representation:
A variadic function’s type is represented as accepting a list:
@sum (numbers: ...int) -> int = ...;
let f: ([int]) -> int = sum; // Variadic stored as list-accepting function
f([1, 2, 3]) // Must call with list when using function value
sum(1, 2, 3) // Direct call retains variadic syntax
10.1.4 C Variadic Functions
C variadic functions use a different, untyped mechanism:
extern "c" from "libc" {
@printf (format: CPtr, ...) -> c_int as "printf";
}
| Feature | Ori ...T | C ... |
|---|---|---|
| Type safety | Homogeneous, checked | Unchecked |
| Context | Safe code | unsafe { ... } block only |
| Type annotation | Required | None |
C-style ... (without type) is only valid in extern "c" declarations. Calling C variadic functions requires unsafe.
See FFI for details on C interop.
10.2 Types
type Point = { x: int, y: int }
type Status = Pending | Running | Done | Failed(reason: str);
type UserId = int;
#derive(Eq, Clone)
type User = { id: int, name: str }
10.3 Traits
trait Printable {
@to_str (self) -> str;
}
trait Comparable: Eq {
@compare (self, other: Self) -> Ordering;
}
trait Iterator {
type Item;
@next (self) -> Option<Self.Item>;
}
self— instanceSelf— implementing type
10.3.1 Default Type Parameters
Type parameters on traits may have default values:
trait Add<Rhs = Self> {
type Output;
@add (self, rhs: Rhs) -> Self.Output;
}
Semantics:
- Default applies when impl omits the type argument
Selfin default position refers to the implementing type at the impl site- Defaults are evaluated at impl site, not trait definition site
- Parameters with defaults shall appear after all parameters without defaults
impl Point: Add {
// Rhs defaults to Self = Point
@add (self, rhs: Point) -> Self = ...;
}
impl Vector2: Add<int> {
// Explicit Rhs = int
@add (self, rhs: int) -> Self = ...;
}
Later default parameters may reference earlier ones:
trait Transform<Input = Self, Output = Input> {
@transform (self, input: Input) -> Output;
}
impl Parser: Transform { ... } // Input = Self = Parser, Output = Parser
impl Parser: Transform<str> { ... } // Input = str, Output = str
impl Parser: Transform<str, Ast> { ... } // Input = str, Output = Ast
10.3.2 Default Associated Types
Associated types in traits may have default values:
trait Add<Rhs = Self> {
type Output = Self; // Defaults to implementing type
@add (self, rhs: Rhs) -> Self.Output;
}
trait Container {
type Item;
type Iter = [Self.Item]; // Default references another associated type
}
Semantics:
- Default applies when impl omits the associated type
Selfin default position refers to the implementing type at the impl site- Defaults may reference type parameters and other associated types
- Defaults are evaluated at impl site, not trait definition site
impl Point: Add {
// Output defaults to Self = Point
@add (self, rhs: Point) -> Self = ...;
}
impl Vector2: Add<int> {
type Output = Vector2; // Explicit override
@add (self, rhs: int) -> Vector2 = ...;
}
Bounds on Defaults
Defaults shall satisfy any bounds on the associated type:
trait Process {
type Output: Clone = Self; // Default only valid if Self: Clone
@process (self) -> Self.Output;
}
impl String: Process { // OK: String: Clone
@process (self) -> Self = self.clone();
}
impl Connection: Process { // ERROR if Connection: !Clone and no override
// Must provide explicit Output type since Self doesn't satisfy Clone
type Output = ConnectionHandle;
@process (self) -> ConnectionHandle = ...;
}
When an impl uses a default:
- Substitute
Selfwith the implementing type - Substitute any referenced associated types
- Verify the resulting type satisfies all bounds
If the default does not satisfy bounds after substitution, it is a compile error at the impl site.
10.3.3 Trait Associated Functions
Traits may define associated functions (methods without self) that implementors shall provide:
trait Default {
@default () -> Self;
}
impl Point: Default {
@default () -> Self = Point { x: 0, y: 0 };
}
Associated functions returning Self prevent the trait from being used as a trait object. See Object Safety in Types.
10.4 Implementations
impl Point {
@new (x: int, y: int) -> Point = Point { x, y };
}
impl Point: Printable {
@to_str (self) -> str = "(" + str(self.x) + ", " + str(self.y) + ")";
}
impl<T: Printable> [T]: Printable {
@to_str (self) -> str = ...;
}
10.4.1 Associated Functions
An associated function is a method defined in an impl block without a self parameter. Associated functions are called on the type itself, not on an instance.
impl Point {
// Associated function (no self)
@origin () -> Point = Point { x: 0, y: 0 };
@new (x: int, y: int) -> Self = Point { x, y };
// Instance method (has self)
@distance (self, other: Point) -> float = ...;
}
Associated functions are called using Type.method(args):
let p = Point.origin();
let q = Point.new(x: 10, y: 20);
Self may be used as a return type in associated functions, referring to the implementing type.
For generic types, full type arguments are required:
let x: Option<int> = Option<int>.some(value: 42);
Extensions cannot define associated functions. Use inherent impl blocks for associated functions.
10.5 Default Implementations
A default implementation provides the standard behavior for a trait:
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...;
@post (url: str, body: str) -> Result<Response, Error> = ...;
}
When a module exports both a trait and its def impl, importing the trait automatically binds the default implementation.
Default implementation methods do not have a self parameter — they are stateless. For configuration, use module-level bindings:
let $timeout = 30s;
pub def impl Http {
@get (url: str) -> Result<Response, Error> =
__http_get(url: url, timeout: $timeout);
}
Constraints:
- One
def implper trait per module - Shall implement all trait methods
- Method signatures shall match the trait
- No
selfparameter
10.5.1 Import Conflicts
A scope can have at most one def impl for each trait. Importing the same trait with defaults from two modules is a compile error:
use "module_a" { Logger }; // Brings def impl
use "module_b" { Logger }; // Error: conflicting default for Logger
To import a trait without its default:
use "module_a" { Logger without def }; // Import trait, skip def impl
10.5.2 Resolution Order
When resolving a capability name:
- Innermost
with...inbinding - Imported
def impl - Module-local
def impl
Imported def impl takes precedence over module-local def impl.
See Capabilities for usage with capability traits.
10.6 Trait Resolution
10.6.1 Trait Inheritance (Diamond Problem)
When a type inherits a trait through multiple paths, a single implementation satisfies all paths:
trait A { @method (self) -> int; }
trait B: A { }
trait C: A { }
trait D: B + C { } // D inherits A through both B and C
impl MyType: D {
@method (self) -> int = 42; // Single implementation satisfies A via B and C
}
10.6.2 Conflicting Default Implementations
When multiple supertraits provide different default implementations for the same method, the implementing type shall provide an explicit implementation:
trait A { @method (self) -> int = 0; }
trait B: A { @method (self) -> int = 1; }
trait C: A { @method (self) -> int = 2; }
trait D: B + C { }
impl MyType: D { } // ERROR: ambiguous default for @method
impl MyType: D {
@method (self) -> int = 3; // Explicit implementation resolves ambiguity
}
10.6.3 Coherence Rules
Coherence ensures that for any type T and trait Trait, there is at most one implementation of Trait for T visible in any compilation unit.
An implementation impl Type: Trait is allowed only if at least one of these is true:
Traitis defined in the current moduleTypeis defined in the current moduleTypeis a generic parameter constrained in the current module
// OK: Type is local
type MyType = { ... }
impl MyType: ExternalTrait { }
// OK: Trait is local
trait MyTrait { ... }
impl ExternalType: MyTrait { }
// ERROR: Both trait and type are external (orphan)
impl std.Vec: std.Display { } // Error: orphan implementation
Blanket implementations (impl<T> T: Trait where ...) follow the same rules.
A duplicate implementation — where the same Trait and Type combination is implemented twice — is an error (E2010). When two blanket implementations with equal specificity could both apply, it is an error (E2021).
10.6.4 Method Resolution Order
When calling value.method():
- Inherent methods — methods in
impl Type { }(not trait impl) - Trait methods from explicit bounds — methods from
where T: Trait - Trait methods from in-scope traits — traits imported into current scope
- Extension methods — methods added via
extend
If multiple traits provide the same method and none are inherent, the call is ambiguous (E2023). Use fully-qualified syntax to disambiguate:
A.method(x) // Calls A's implementation
B.method(x) // Calls B's implementation
10.6.5 Super Trait Method Calls
An implementation can call the parent trait’s default implementation using Trait.method(self):
trait Parent {
@method (self) -> int = 10;
}
trait Child: Parent {
@method (self) -> int = Parent.method(self) + 1;
}
impl MyType: Parent {
@method (self) -> int = Parent.method(self) * 2;
}
10.6.6 Associated Type Disambiguation
When a type implements multiple traits with same-named associated types, use qualified paths:
trait A { type Item; }
trait B { type Item; }
// Qualified path syntax: Type::Trait::AssocType
@f<C: A + B> (c: C) where C::A::Item: Clone = ...;
// To require both Items to be the same type:
@g<C: A + B> (c: C) where C::A::Item == C::B::Item = ...;
10.6.7 Implementation Specificity
When multiple implementations could apply, the most specific wins:
- Concrete —
impl MyType: Trait(most specific) - Constrained blanket —
impl<T: Clone> T: Trait - Generic blanket —
impl<T> T: Trait(least specific)
It is an error if two applicable implementations have equal specificity (E2021).
10.6.8 Extension Method Conflicts
Only one extension for a given method may be in scope. Conflicts are detected based on what is in scope, including re-exports:
extension "a" { Iterator.sum }
extension "b" { Iterator.sum } // ERROR: conflicting extension imports
10.7 Attributes
Attributes modify declarations with metadata or directives.
Grammar: See grammar.ebnf § ATTRIBUTES
10.7.1 Syntax
#attribute_name
#attribute_name(args)
Attributes precede the declaration they modify. Multiple attributes can be applied:
#derive(Eq, Clone)
#repr("c")
type Point = { x: int, y: int }
10.7.2 Standard Attributes
#derive
Generates trait implementations automatically:
#derive(Eq, Hashable, Clone, Debug)
type Point = { x: int, y: int }
See Types § Derive for derivable traits and semantics.
#repr
Controls memory representation:
| Syntax | Effect |
|---|---|
#repr("c") | C-compatible struct layout |
#repr("c")
type CTimeSpec = {
tv_sec: int,
tv_nsec: int
}
Required for structs passed to C via FFI. See FFI § C Structs.
#target
Conditional compilation based on platform:
#target(os: "linux")
@linux_only () -> void = ...;
#target(arch: "x86_64", os: "linux")
@linux_x64 () -> void = ...;
See Conditional Compilation for full syntax.
#cfg
Conditional compilation based on build configuration:
#cfg(debug)
@debug_log (msg: str) -> void = print(msg: `[DEBUG] {msg}`);
#cfg(feature: "ssl")
@secure_connect () -> void = ...;
See Conditional Compilation for full syntax.
10.7.3 Test Attributes
#skip
Skips a test with an optional reason:
#skip("pending implementation")
@test_feature tests @feature () -> void = ...;
#compile_fail
Asserts that code fails to compile with the expected error:
#compile_fail("E0100")
@test_type_error tests @f () -> void =
let x: int = "string"; // Expected type error
#fail
Asserts that a test panics with the expected message:
#fail("index out of bounds")
@test_panic tests @f () -> void =
let list: [int] = [];
list[0] // Expected panic
See Testing for test semantics.
10.7.4 File-Level Attributes
The #! prefix applies an attribute to the entire file:
#!target(os: "linux")
// Entire file is Linux-only
File-level attributes shall appear before any declarations.
10.8 Tests
@test_add tests @add () -> void = {
assert_eq(actual: add(a: 2, b: 3), expected: 5)
}
See Testing.