/ Specification
Playground Docs Performance GitHub
Chapter 3

Types

Lattice is dynamically typed: values carry their type at runtime, and type annotations are optional constraints enforced at runtime rather than compile time. Every value belongs to exactly one of 15 value types.

Value Types

Type NameDescriptionLiteral Syntax
Int64-bit signed integer42, -7
Float64-bit double-precision float3.14, 0.5
BoolBooleantrue, false
StringHeap-allocated UTF-8 string"hello", 'raw'
ArrayDynamic array of values[1, 2, 3]
MapHash map of key-value pairsMap::new()
TupleFixed-size ordered collection(1, "a", true)
StructNamed record with typed fieldsPoint { x: 1, y: 2 }
EnumTagged union variantColor::Red
SetUnordered collection of unique valuesSet::new()
ClosureFirst-class function value|x| { x + 1 }
ChannelConcurrency communication channelChannel::new()
RangeInteger range1..10
NilAbsence of a valuenil
UnitNo meaningful value (void-like)implicit

Int

The Int type represents 64-bit signed integers (int64_t). Integers support arithmetic, bitwise, comparison, and shift operations.

let a = 42
let b = -7
let c = a + b * 2    // 28
let d = a & 0xFF     // bitwise AND

Float

The Float type represents 64-bit IEEE 754 double-precision floating point values. Mixed arithmetic between Int and Float promotes the integer to float.

Bool

The Bool type has exactly two values: true and false. Logical operators &&, ||, and ! operate on booleans. Conditional expressions (if, while) require boolean conditions.

String

Strings are heap-allocated, immutable sequences of UTF-8 bytes. They support interpolation, concatenation with +, and a rich set of methods (see Standard Library).

Array

Arrays are dynamically-sized, ordered collections of values. Elements may be of mixed types. Arrays support index access, mutation via .push() and index assignment, and higher-order methods like .map(), .filter(), and .reduce().

flux arr = [1, "two", 3.0]
arr.push(4)        // [1, "two", 3.0, 4]
arr[0] = 99         // [99, "two", 3.0, 4]
let len = arr.len() // 4
Note Array .push() and index assignment mutate in-place. However, arrays are passed by value to functions — the callee receives a copy.

Map

Maps are hash maps from string keys to values. Maps are created via Map::new() and manipulated with .set(), .get(), .has(), and other methods.

Note Maps are pass-by-value. Passing a map to a function creates a deep copy. Mutations in the callee do not propagate back to the caller.

Tuple

Tuples are fixed-size, ordered collections created with parenthesized comma-separated expressions. Unlike arrays, tuples are intended for heterogeneous groupings and cannot be resized.

let pair = (1, "hello")
let first = pair[0]  // 1

Struct

Structs are named records with a fixed set of fields. Each field has a name, a value, and optionally a type annotation and a phase. Struct fields may hold closures, enabling object-like patterns. See Items: Structs for declaration syntax.

Enum

Enums are tagged unions with named variants. Each variant may optionally carry a payload of one or more values. See Items: Enums for declaration syntax.

enum Shape {
    Circle(Float),
    Rectangle(Float, Float),
    Point
}

let s = Shape::Circle(5.0)

Set

Sets are unordered collections of unique values, backed by a hash map. They support standard set operations: .add(), .remove(), .has(), .union(), .intersection(), and .difference().

Closure

Closures are first-class function values that capture their enclosing environment. They are created with pipe-delimited parameter lists and may have default parameter values or variadic parameters.

closure        ::= "|" [ closure_params ] "|" ( expression | block )
closure_params ::= closure_param { "," closure_param }
closure_param  ::= [ "..." ] IDENT [ "=" expression ]

Unlike function declarations, closure parameters do not support type annotations. Parameters are bare identifiers with optional defaults or a variadic prefix.

Channel

Channels are typed communication primitives for structured concurrency. Values sent across channels must be in the crystal (frozen) phase to prevent data races. See Concurrency.

Range

Ranges represent a half-open interval of integers from a start value (inclusive) to an end value (exclusive). They are primarily used in for loops.

for i in 0..10 {
    print(i)  // 0, 1, 2, ..., 9
}

Nil

Nil represents the absence of a value. It is the result of operations that fail gracefully (e.g., accessing a missing map key) and is used with optional chaining.

Unit

Unit is the type of expressions that produce no meaningful value, such as assignment statements or functions with no explicit return. It is similar to void in other languages. The REPL does not display Unit results.

Type Annotations

type_expr    ::= [ phase_prefix ] type_name
             |   "[" type_expr "]"
phase_prefix ::= "~" | "*"
type_name    ::= "Int" | "Float" | "Bool" | "String" | "Array"
             |   "Map" | "Tuple" | "Set" | "Fn" | "Channel"
             |   "Range" | identifier

Type annotations appear after colons in bindings, function parameters, and return types. They are enforced at runtime. The phase prefix ~ denotes a fluid (mutable) type and * denotes a crystal (immutable) type.

fn process(data: ~Array, config: *Map) -> String {
    // data must be fluid, config must be crystal
    return "done"
}

Phase Tags

Every value carries a phase tag that controls its mutability. See Phase System for full details.

Phase TagNameDescription
fluidFluidMutable — can be modified freely
crystalCrystalImmutable — frozen, cannot be modified
unphasedUnphasedDefault — phase not explicitly set
sublimatedSublimatedShallow freeze — structure locked, nested elements remain mutable