Skip to content

Variables and Types

Shape is statically typed with inference.

KeywordMutable?OwnershipTypical use
letNoUnique (move by default)Default — immutable data
let mutYesUniqueSingle-owner mutation
varYesSharedShared mutable state, closures
constNoN/ACompile-time constants
let x = 42 // immutable — cannot reassign
let mut count = 0 // mutable — count = count + 1 OK
var shared = [1, 2, 3] // shared mutable — can be aliased
const PI = 3.14159 // compile-time constant
print(x)
print(count)
print(shared.len())
print(PI)

Shape has three variable binding forms with distinct ownership semantics. The form you choose determines how values are stored, shared, and mutated.

let name = "Alice"
let numbers = [1, 2, 3]
print(name)
print(numbers.len())

Values bound with let are uniquely owned and immutable. They cannot be reassigned or mutated. When a let binding is used as a value, ownership is moved — the original binding becomes invalid:

let arr = [1, 2, 3]
let arr2 = arr // Move: arr is consumed
print(arr2.len()) // use arr2 — arr is no longer valid here

For heap-allocated types (strings, arrays, objects), let bindings use zero-overhead allocation with no reference counting.

let mut count = 0
count = count + 1
let mut items = [1, 2, 3]
items.push(4) // Direct mutation — no copy-on-write
print(count)
print(items.len())

Values bound with let mut are uniquely owned and mutable. There is exactly one owner, so mutations happen directly without any copy-on-write overhead. Like let, the last use of a let mut binding is a move.

var shared = [1, 2, 3]
let alias = shared // Both reference the same data
shared.push(4) // Copy-on-write: clones if aliased
// (SURFACE: v0.3 G.1 (b)-class §3.B.1 — `var` + alias + `.push` CoW path
// currently SEGFAULTs under JIT and emits a dev-mode auto-print of an
// internal future-ptr under VM. Documented contract preserved; gating
// item tracked at `docs/cluster-audits/v0.3-g1-step1-fundamentals.md`.)

Values bound with var are shared and mutable. They use reference counting internally, which means:

  • Multiple bindings can reference the same value
  • Mutations use copy-on-write: if the value has multiple references, it is cloned before mutation
  • Closures can capture var bindings by reference

Use var when you need to share mutable state across closures or when multiple parts of your code need to reference the same value. For most code, prefer let or let mut.

BindingOwnershipMutable?OverheadUse when
letUniqueNoZeroDefault — immutable data
let mutUniqueYesZeroSingle-owner mutation
varSharedYesRefcount + CoWShared mutable state

Shape moves values by default (like reassigning a unique resource), it does not copy them. The compiler automatically determines when values need to be cloned based on usage.

See References and Borrowing for details on borrowing values without transferring ownership.

let count = 42 // int
let ratio = 0.25 // number
let name = "shape" // string
let enabled = true // bool
print(count)
print(ratio)
print(name)
print(enabled)
let retries: int = 3
let timeout_ms: int = 5000
let factor: number = 1.5
let flags: u8 = 0xFF
print(retries)
print(timeout_ms)
print(factor)
print(flags)

Use explicit annotations at API boundaries (function parameters and return types). Width-specific integer types (i8, u8, i16, etc.) are also available — see Integer Width Types.

Shape does not model absence with null. Use Option<T> and Result<T>.

let maybe_id: Option<int> = Some(7)
let missing: Option<int> = None
print(maybe_id)
print(missing)
fn read_text(path: string) -> Result<string> {
fs.read(path)
}
let text = read_text("config.txt")? !! "could not read config.txt"
let point = { x: 1 }
point.y = 2
let sum = point.x + point.y
print(sum)

Field reads are checked against assignment state. A field that exists in an evolved shape but has not been assigned yet is rejected at compile time.

type User {
id: int,
name: string,
active: bool
}
let u: User = { id: 1, name: "A", active: true }
print(u.name)
type Box<T = int> {
value: T
}
fn id<T>(x: T) -> T { x }
print(id(3))

Use T: Trait when a parameter must satisfy a trait bound.

let outer = "outside"
{
let inner = "inside"
print(outer)
print(inner)
}
// print(inner) // compile error

Shape provides several generic builtin types:

TypeDescription
Vec<T>Ordered collection of elements
Mat<T>Dense matrix values
Table<T>Typed row data (see Tables)
Option<T>Optional value (Some(v) or None)
Result<T>Fallible value (Ok(v) or Err(e))
HashMap<K, V>Ordered key-value map (see Objects and Vectors)

Generic type parameters flow through method chains. For example, Vec<number>.filter(fn) returns Vec<number>, and Vec<string>.map(fn(s) -> number) returns Vec<number>.

For the complete builtin type reference, see Builtin Types.

A tuple groups a fixed number of values. Shape writes tuple types with bracket syntax — [T1, T2, ...] — and the corresponding values as bracket literals. There is no (a, b) parenthesized tuple form: parentheses are expression grouping only.

let pair: [int, int] = [3, 4]
print(pair[0])
print(pair[1])

Tuple elements are accessed by index, the same as array elements. The type annotation [int, int] fixes both the length and the per-position element type at compile time.

For multi-value returns or heterogeneous groupings (e.g. an int paired with a string), prefer a named struct type — it documents each field by name and avoids relying on positional indices:

type LabeledCount { label: string, count: int }
let lc = LabeledCount { label: "rows", count: 7 }
print(lc.label)
print(lc.count)

A type alias creates an alternate name for an existing type. Aliases can be used as constructors:

type Point { x: int, y: int }
type P = Point
let origin = P { x: 0, y: 0 }
print(origin.x) // 0

This is useful for shortening long generic types or creating domain-specific names:

type Unit { label: string, value: number }
type Meter = Unit
let length = Meter { label: "m", value: 42.50 }
print(length.label)
print(length.value)

The following identifiers are reserved and cannot be used as variable or function names:

CategoryKeywords
Declarationslet, var, const, mut, function, type, trait, interface, impl, enum, pub
Control flowif, else, for, while, match, return, break, continue
Operatorsand, or, in, as
Valuestrue, false, null, None, Some
Asyncasync, await
Modulesimport, from, use
Object manipulationextend, method
Advancedcomptime, datasource, builtin
  • Prefer let for local bindings — it has zero overhead and is the safest default.
  • Use let mut when you need to modify a value and no other code needs access to it.
  • Use var only when you need shared mutable state (e.g., across closures).
  • Use const for true constants evaluated at compile time.
  • Use explicit Option<T>/Result<T> for absence and failure.
  • Use HashMap<K, V> for dynamic key-value data.
  • Add type annotations where interfaces are consumed by other modules.