Variables and Types
Shape is statically typed with inference.
Binding Forms
Section titled “Binding Forms”| Keyword | Mutable? | Ownership | Typical use |
|---|---|---|---|
let | No | Unique (move by default) | Default — immutable data |
let mut | Yes | Unique | Single-owner mutation |
var | Yes | Shared | Shared mutable state, closures |
const | No | N/A | Compile-time constants |
let x = 42 // immutable — cannot reassignlet mut count = 0 // mutable — count = count + 1 OKvar shared = [1, 2, 3] // shared mutable — can be aliasedconst PI = 3.14159 // compile-time constant
print(x)print(count)print(shared.len())print(PI)Variable Bindings and Ownership
Section titled “Variable Bindings and Ownership”Shape has three variable binding forms with distinct ownership semantics. The form you choose determines how values are stored, shared, and mutated.
let — Immutable owned binding (default)
Section titled “let — Immutable owned binding (default)”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 consumedprint(arr2.len()) // use arr2 — arr is no longer valid hereFor heap-allocated types (strings, arrays, objects), let bindings use zero-overhead allocation with no reference counting.
let mut — Mutable owned binding
Section titled “let mut — Mutable owned binding”let mut count = 0count = 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 mutable binding
Section titled “var — Shared mutable binding”var shared = [1, 2, 3]let alias = shared // Both reference the same datashared.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
varbindings 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.
Choosing the right binding
Section titled “Choosing the right binding”| Binding | Ownership | Mutable? | Overhead | Use when |
|---|---|---|---|---|
let | Unique | No | Zero | Default — immutable data |
let mut | Unique | Yes | Zero | Single-owner mutation |
var | Shared | Yes | Refcount + CoW | Shared 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.
Type Inference
Section titled “Type Inference”let count = 42 // intlet ratio = 0.25 // numberlet name = "shape" // stringlet enabled = true // bool
print(count)print(ratio)print(name)print(enabled)Type Annotations
Section titled “Type Annotations”let retries: int = 3let timeout_ms: int = 5000let factor: number = 1.5let 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.
No Null Sentinel
Section titled “No Null Sentinel”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"Objects and Structural Typing
Section titled “Objects and Structural Typing”let point = { x: 1 }point.y = 2
let sum = point.x + point.yprint(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.
Named Types
Section titled “Named Types”type User { id: int, name: string, active: bool}
let u: User = { id: 1, name: "A", active: true }print(u.name)Generic Parameters (Bounds + Defaults)
Section titled “Generic Parameters (Bounds + Defaults)”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 errorGeneric Builtin Types
Section titled “Generic Builtin Types”Shape provides several generic builtin types:
| Type | Description |
|---|---|
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.
Tuple Types
Section titled “Tuple 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)Type Aliases
Section titled “Type Aliases”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) // 0This 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)Reserved Keywords
Section titled “Reserved Keywords”The following identifiers are reserved and cannot be used as variable or function names:
| Category | Keywords |
|---|---|
| Declarations | let, var, const, mut, function, type, trait, interface, impl, enum, pub |
| Control flow | if, else, for, while, match, return, break, continue |
| Operators | and, or, in, as |
| Values | true, false, null, None, Some |
| Async | async, await |
| Modules | import, from, use |
| Object manipulation | extend, method |
| Advanced | comptime, datasource, builtin |
Best Practices
Section titled “Best Practices”- Prefer
letfor local bindings — it has zero overhead and is the safest default. - Use
let mutwhen you need to modify a value and no other code needs access to it. - Use
varonly when you need shared mutable state (e.g., across closures). - Use
constfor 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.