9 Properties of Types

Type identity, assignability, and constraints.

Grammar: See grammar.ebnf § DECLARATIONS (generics, where_clause)

9.1 Type Identity

Two types are identical if they have the same definition.

Primitives: Each primitive is identical only to itself.

Compounds: Same constructor and pairwise identical type arguments.

[int] ≡ [int]
[int] ≢ [str]
(int, str) ≢ (str, int)

Nominal: Same type definition, not structural equivalence.

type Point2D = { x: int, y: int }
type Vector2D = { x: int, y: int }
// Point2D ≢ Vector2D

Generics: Same definition and pairwise identical arguments.

Option<int> ≡ Option<int>
Option<int> ≢ Option<str>

9.2 Assignability

A value of type S is assignable to type T if:

  • S is identical to T, or
  • S implements trait T and target is dyn T

No implicit conversions:

let x: float = 42;        // error
let x: float = float(42); // OK

9.3 Variance

Generics are invariant. Container<T> is only compatible with Container<T>.

9.4 Type Constraints

@sort<T: Comparable> (items: [T]) -> [T] = ...;

@process<T, U> (items: [T], f: (T) -> U) -> [U]
    where T: Clone, U: Default = ...;

9.5 Default Values

TypeDefault
int0
float0.0
boolfalse
str""
byte0
void()
Option<T>None
[T][]
{K: V}{}

Types implementing Default provide default() method.

9.6 Printable Trait

The Printable trait provides human-readable string conversion.

trait Printable {
    @to_str (self) -> str;
}

Printable is required for string interpolation without format specifiers:

let x = 42;
`value: {x}`  // Calls x.to_str()

9.6.1 Standard Implementations

TypeOutput
int"42"
float"3.14"
bool"true" or "false"
strIdentity
charSingle character string
byteNumeric string
[T] where T: Printable"[1, 2, 3]"
Option<T> where T: Printable"Some(42)" or "None"
Result<T, E> where both Printable"Ok(42)" or "Err(msg)"

9.6.2 Derivation

Printable is derivable for user-defined types when all fields implement Printable:

#derive(Printable)
type Point = { x: int, y: int }

Point { x: 1, y: 2 }.to_str()  // "Point(1, 2)"

Derived implementation creates human-readable format with type name and field values in order.

9.7 Formattable Trait

The Formattable trait provides formatted string conversion with format specifications.

trait Formattable {
    @format (self, spec: FormatSpec) -> str;
}

Formattable is required for string interpolation with format specifiers:

let n = 42;
`hex: {n:x}`     // Calls n.format(spec: ...) with Hex format type
`padded: {n:08}` // Calls n.format(spec: ...) with width 8, zero-pad

9.7.1 FormatSpec Type

type FormatSpec = {
    fill: Option<char>,
    align: Option<Alignment>,
    sign: Option<Sign>,
    width: Option<int>,
    precision: Option<int>,
    format_type: Option<FormatType>,
}

type Alignment = Left | Center | Right;

type Sign = Plus | Minus | Space;

type FormatType = Binary | Octal | Hex | HexUpper | Exp | ExpUpper | Fixed | Percent;

These types are in the prelude.

9.7.2 Format Spec Syntax

Format specifications in template strings use the syntax:

[[fill]align][sign][#][0][width][.precision][type]
ComponentSyntaxDescription
FillAny characterPadding character (default: space)
Align< > ^Left, right, center alignment
Sign+ - Sign display for numbers
##Alternate form (prefix for hex, etc.)
00Zero-pad (implies right-align)
WidthIntegerMinimum field width
Precision.NDecimal places or max string length
TypeLetterFormat type (b, o, x, X, e, E, f, %)

9.7.3 Format Types

Integer Types:

TypeDescriptionExample
bBinary42"101010"
oOctal42"52"
xHex (lowercase)255"ff"
XHex (uppercase)255"FF"

Float Types:

TypeDescriptionExample
eScientific (lowercase)1234.5"1.2345e+03"
EScientific (uppercase)1234.5"1.2345E+03"
fFixed-point (6 decimals)1234.5"1234.500000"
%Percentage0.75"75%"

Alternate Form (#):

TypeWithout #With #
b"101010""0b101010"
o"52""0o52"
x"ff""0xff"
X"FF""0xFF"

9.7.4 Standard Implementations

TypeBehavior
intSupports b, o, x, X format types; sign and alternate form
floatSupports e, E, f, % format types; precision and sign
strWidth, alignment, fill; precision truncates
boolWidth and alignment only
charWidth and alignment only

9.7.5 Blanket Implementation

All Printable types have a blanket Formattable implementation:

impl<T: Printable> T: Formattable {
    @format (self, spec: FormatSpec) -> str = {
        let base = self.to_str();
        apply_format(s: base, spec: spec)
    }
}

This applies width, alignment, and fill. Type-specific formatting (binary, hex, etc.) is only available for types that implement Formattable directly.

9.7.6 Custom Implementation

User types may implement Formattable for custom formatting:

type Money = { cents: int }

impl Money: Formattable {
    @format (self, spec: FormatSpec) -> str = {
        let dollars = self.cents / 100;
        let cents = self.cents % 100;
        let base = `{dollars}.{cents:02}`;
        apply_alignment(s: base, spec: spec)
    }
}

Newtypes can delegate to their inner value:

type UserId = int;

impl UserId: Formattable {
    @format (self, spec: FormatSpec) -> str = self.inner.format(spec: spec);
}

9.7.7 Error Codes

CodeDescription
E0970Invalid format specification syntax
E0971Format type not supported for this type
E0972Type does not implement Formattable

9.8 Default Trait

The Default trait provides zero/empty values.

trait Default {
    @default () -> Self;
}

9.8.1 Standard Implementations

TypeDefault Value
int0
float0.0
boolfalse
str""
byte0
char'\0'
void()
[T][]
{K: V}{}
Set<T>Set.new()
Option<T>None
Duration0ns
Size0b

9.8.2 Derivation

Default is derivable for struct types when all fields implement Default:

#derive(Default)
type Config = {
    host: str,    // ""
    port: int,    // 0
    debug: bool,  // false
}

Sum types cannot derive Default (ambiguous variant):

#derive(Default)  // error: cannot derive Default for sum type
type Status = Pending | Running | Done;

9.9 Traceable Trait

The Traceable trait enables error trace propagation.

trait Traceable {
    @with_trace (self, entry: TraceEntry) -> Self;
    @trace (self) -> str;
    @trace_entries (self) -> [TraceEntry];
    @has_trace (self) -> bool;
}

The ? operator automatically adds trace entries at propagation points:

@outer () -> Result<int, Error> = {
    let x = inner()?;  // Adds trace entry for this location
    Ok(x * 2)
}

9.9.1 TraceEntry Type

type TraceEntry = {
    function: str,   // Function name with @ prefix
    file: str,       // Source file path
    line: int,       // Line number
    column: int,     // Column number
}

9.9.2 Standard Implementations

TypeImplements
ErrorYes
Result<T, E> where E: TraceableYes (delegates to E)

9.10 Len Trait

The Len trait provides length information for collections and sequences.

trait Len {
    @len (self) -> int;
}

9.10.1 Semantic Requirements

Implementations shall satisfy:

  • Non-negative: x.len() >= 0 for all x
  • Deterministic: x.len() returns the same value for unchanged x

9.10.2 String Length

For str, .len() returns the byte count, not the codepoint or grapheme count:

"hello".len()  // 5
"café".len()   // 5 (é is 2 bytes in UTF-8)
"日本".len()   // 6 (each character is 3 bytes)

9.10.3 Standard Implementations

TypeImplements LenReturns
[T]YesNumber of elements
strYesNumber of bytes
{K: V}YesNumber of entries
Set<T>YesNumber of elements
Range<int>YesNumber of values in range
(T₁, T₂, ...)YesNumber of elements (statically known)

9.10.4 Derivation

Len cannot be derived. Types shall implement it explicitly or be built-in.

9.10.5 Distinction from Iterator.count()

The Len trait is distinct from Iterator.count():

Len.len()Iterator.count()
ComplexityO(1) for built-in typesO(n) — consumes the iterator
Side effectsNone — non-consumingConsuming — iterator is exhausted
SemanticsCurrent size of collectionNumber of remaining elements

Iterators do not implement Len. To count iterator elements, use .count().

9.11 IsEmpty Trait

The IsEmpty trait provides emptiness checking for collections and sequences.

trait IsEmpty {
    @is_empty (self) -> bool
}

9.11.1 Semantic Requirements

Implementations shall satisfy:

  • Consistency: is_empty() returns true if and only if the collection contains no elements
  • Deterministic: x.is_empty() returns the same value for unchanged x
  • Relationship to Len: If a type implements both Len and IsEmpty, then x.is_empty() == (x.len() == 0) shall hold

9.11.2 Standard Implementations

All built-in Iterable types implement IsEmpty:

TypeImplements IsEmptyCondition
[T]Yestrue if no elements
strYestrue if zero bytes
{K: V}Yestrue if no entries
Set<T>Yestrue if no elements
[T, max N]Yestrue if length is 0
Range<int>Yestrue if start >= end

9.11.3 Types That Do Not Implement IsEmpty

TypeReason
Tuples (T₁, T₂, ...)Fixed-size at compile time; never empty
Option<T>Not a collection; use is_none() or pattern matching
Range<float>Not iterable

9.11.4 Relationship to Iterable

IsEmpty is independent of Iterable. While all built-in Iterable types implement IsEmpty, a type may implement IsEmpty without implementing Iterable (e.g., types where emptiness can be checked without iteration).

9.11.5 Derivation

IsEmpty cannot be derived. Types shall implement it explicitly or be built-in.

9.12 Comparable Trait

The Comparable trait provides total ordering for values.

trait Comparable: Eq {
    @compare (self, other: Self) -> Ordering;
}

Comparable extends Eq — all comparable types shall also be equatable.

9.12.1 Mathematical Properties

A valid Comparable implementation shall satisfy:

Reflexivity: a.compare(other: a) == Equal

Antisymmetry: If a.compare(other: b) == Less, then b.compare(other: a) == Greater

Transitivity: If a.compare(other: b) == Less and b.compare(other: c) == Less, then a.compare(other: c) == Less

Consistency with Eq: a.compare(other: b) == Equal if and only if a == b

9.12.2 Operator Derivation

Types implementing Comparable automatically get comparison operators. The builtin compare function calls the trait method:

compare(left: a, right: b)  // → a.compare(other: b)

Operators desugar to Ordering method calls:

a < b   // a.compare(other: b).is_less()
a <= b  // a.compare(other: b).is_less_or_equal()
a > b   // a.compare(other: b).is_greater()
a >= b  // a.compare(other: b).is_greater_or_equal()

9.12.3 Standard Implementations

TypeOrdering
intNumeric order
floatIEEE 754 total order (NaN handling)
boolfalse < true
strLexicographic (Unicode codepoint)
charUnicode codepoint
byteNumeric order
DurationShorter < longer
SizeSmaller < larger
[T] where T: ComparableLexicographic
(T1, T2, ...) where all Ti: ComparableLexicographic
Option<T> where T: ComparableNone < Some(_)
Result<T, E> where T: Comparable, E: ComparableOk(_) < Err(_), then compare inner
OrderingLess < Equal < Greater

Maps and Sets are not Comparable (unordered collections).

9.12.4 Float Comparison

Floats follow IEEE 754 total ordering:

  • -Inf < negative < -0.0 < +0.0 < positive < +Inf
  • NaN compares equal to itself and greater than all other values

Note: For ordering purposes, NaN == NaN. This differs from == where NaN != NaN.

9.12.5 Derivation

Comparable is derivable for user-defined types when all fields implement Comparable:

#derive(Eq, Comparable)
type Point = { x: int, y: int }

// Generated: lexicographic comparison by field declaration order

For sum types, variants compare by declaration order (Low < Medium < High).

9.13 Hashable Trait

The Hashable trait provides hash values for map keys and set elements.

trait Hashable: Eq {
    @hash (self) -> int;
}

Hashable extends Eq — all hashable types shall also be equatable.

9.13.1 Hash Invariant

Consistency with Eq: If a == b, then a.hash() == b.hash()

The converse is NOT required — different values may have the same hash (collisions are expected).

9.13.2 Standard Implementations

TypeHash Method
intIdentity or bit-mixing
floatBit representation hash
boolfalse → 0, true → 1
strFNV-1a or similar
charCodepoint value
byteIdentity
DurationHash of nanoseconds
SizeHash of bytes
[T] where T: HashableCombined element hashes
{K: V} where K: Hashable, V: HashableCombined entry hashes (order-independent)
Set<T> where T: HashableCombined element hashes (order-independent)
(T1, T2, ...) where all Ti: HashableCombined element hashes
Option<T> where T: HashableNone → 0, Some(x)x.hash() with salt
Result<T, E> where T: Hashable, E: HashableCombined variant and value hash

9.13.3 Float Hashing

Floats hash consistently with equality:

  • +0.0 and -0.0 hash the same (they’re equal)
  • NaN values hash consistently (all NaN equal for hashing)

9.13.4 Map Key and Set Element Requirements

To use a type as a map key or set element, it shall implement both Eq and Hashable. Using a type that does not implement Hashable as a map key is an error (E2031):

let map: {Point: str} = {};  // Point must be Eq + Hashable
let set: Set<Point> = Set.new();  // Point must be Eq + Hashable

9.13.5 hash_combine Function

The hash_combine function in the prelude mixes hash values:

@hash_combine (seed: int, value: int) -> int =
    seed ^ (value + 0x9e3779b9 + (seed << 6) + (seed >> 2));

This follows the boost hash_combine pattern for good distribution. Users implementing custom Hashable can use this function directly.

9.13.6 Derivation

Hashable is derivable for user-defined types when all fields implement Hashable:

#derive(Eq, Hashable)
type Point = { x: int, y: int }

// Generated: combine field hashes using hash_combine

Deriving Hashable without Eq is an error (E2029). The hash invariant requires that equal values produce equal hashes, which cannot be guaranteed without an Eq implementation.

9.14 Into Trait

The Into trait provides semantic, lossless type conversions.

trait Into<T> {
    @into (self) -> T;
}

Into<T> represents a conversion from Self to T. Unlike as conversions (which are built-in and handle representation changes), Into is user-extensible and represents semantic conversions between types.

9.14.1 Usage

Conversions are always explicit. The caller shall invoke .into():

let error: Error = "something went wrong".into();

When a function accepts impl Into<T>, the caller shall still call .into() explicitly:

@fail (err: impl Into<Error>) -> Never = panic(msg: err.into().message);

fail(err: "simple message".into());  // Explicit .into() required
fail(err: Error { message: "detailed" });  // No conversion needed

No implicit conversion occurs at call sites. This maintains Ori’s “no implicit conversions” principle.

9.14.2 Standard Implementations

SourceTargetNotes
strErrorCreates Error with message
intfloatLossless numeric widening
Set<T>[T]Requires T: Eq + Hashable

NOTE Into is for lossless conversions only. Lossy conversions (like float to int truncation) require explicit as syntax.

9.14.3 Relationship to as

MechanismFallibleImplicitExtensibleUse Case
asNoNoNoPrimitive representation changes
as?YesNoNoParsing, checked conversions
IntoNoNoYesSemantic type conversions

9.14.4 Custom Implementations

User types may implement Into for meaningful conversions:

type UserId = int;

impl UserId: Into<str> {
    @into (self) -> str = `user-{self.inner}`;
}

let id = UserId(42);
let s: str = id.into();  // "user-42"

9.14.5 No Blanket Identity

There is no blanket impl<T> T: Into<T>. Each conversion shall be explicitly implemented. This ensures impl Into<T> parameters remain meaningful — they indicate types that can be converted to T, not any type.

9.14.6 No Automatic Chaining

Conversions do not chain automatically:

// Given: A implements Into<B>, B implements Into<C>
let a: A = ...;
let c: C = a.into();         // ERROR: A does not implement Into<C>
let c: C = a.into().into();  // OK: explicit A → B → C

9.14.7 Orphan Rules

Into implementations follow standard orphan rules:

  • Implement in the module defining the source type, OR
  • Implement in the module defining the target type

9.14.8 Error Codes

CodeDescription
E2036Type does not implement Into<T>
E2037Multiple Into implementations apply (ambiguous)