Value Types vs Reference Types in Nova

Plan 120 (D215, 2026-05-31): explicit stack/heap allocation guide. Cross-ref: spec/decisions/02-types.md → D215.

The bracket rule

Nova uses bracket syntax to encode allocation semantics:

SyntaxCategoryAllocationSemantics
type X(T1, T2)positional tuplestackvalue (copy on pass)
type X(name T, ...)named tuplestackvalue (copy on pass)
type X { field T }recordheap (GC-managed)reference (pointer on pass)
type X | A | Bsum typeheapreference
int, bool, f64, char, u8primitivesregistervalue

The choice of () vs {} is not arbitrary: it explicitly communicates performance and lifetime expectations to both humans and AI.

Value types: stack-allocated tuples

Positional tuples

type Point(f64, f64)      // .0 / .1 access

ro p = Point(1.0, 2.0)
assert(p.0 == 1.0)

Named tuples (Plan 120, D215)

type Vec3(x f64, y f64, z f64)   // .x / .y / .z access
type Color(r u8, g u8, b u8, a u8)

ro v = Vec3(x: 1.0, y: 2.0, z: 3.0)
ro c = Color(r: 255, g: 0, b: 128, a: 255)

assert(v.x == 1.0)
assert(c.r == 255)

Named tuples are identical to positional tuples in performance — both are stack-allocated C structs, no heap allocation, no GC tracking.

Methods on value types

fn Vec3 @dot(other Vec3) -> f64 =>
    @.x * other.x + @.y * other.y + @.z * other.z

fn Vec3 @scale(s f64) -> Vec3 =>
    Vec3(x: @.x * s, y: @.y * s, z: @.z * s)

ro v1 = Vec3(x: 1.0, y: 0.0, z: 0.0)
ro v2 = Vec3(x: 0.0, y: 1.0, z: 0.0)
assert(v1.dot(v2) == 0.0)   // perpendicular vectors

Reference types: heap-allocated records

type User {
    id u64
    name str
    email str
}

ro u = User { id: 1, name: "alice", email: "alice@example.com" }
// u is a pointer to managed heap; GC-tracked

When to use which

Use caseTypeWhy
Hot-path math (Vec3, Matrix, Quaternion)named tuplezero GC, predictable perf
Pixel/sample formats (Color, AudioSample)named tuplesmall, copy-cheap
FFI multi-value returnsnamed tuplefits in registers
Iterator state (idx, end, step)named tuplelocal-lifetime, no heap
Geometric primitives (Point, Rect, AABB)named tuplevalue semantics
Domain entities (User, Order, Account)recordidentity, sharing across fibers
Large aggregates with many fieldsrecordcopy expensive
Types shared between modules / persistedrecordreference semantics natural

General rule: small + copy-cheap + value-semantics → tuple; large / identity-bearing / shared → record.

Cross-access errors

Nova enforces the boundary between named and positional tuples:

type Named(x f64, y f64)
type Positional(f64, f64)

fn Named @bad() -> f64 => @.0      // E_TUPLE_POSITIONAL_ACCESS_ON_NAMED
fn Positional @bad() -> f64 => @.x  // E_TUPLE_NAMED_ACCESS_ON_POSITIONAL

Comparison with other languages

LanguageStack value type with named fields
Ruststruct Point { x: f64, y: f64 } — stack by default
Swiftstruct Point { let x: Double; let y: Double } — value type
C#struct Point { public double X; public double Y; } — value type
Gotype Point struct { X, Y float64 } — escape analysis decides
Novatype Point(x f64, y f64)explicitly stack, bracket syntax

Nova’s bracket choice (() vs {}) makes the allocation decision explicit at the type declaration site — no need to read docs or remember language rules.

Last updated August 3, 2026