Proposal: Value Trait — ARC-Free Value Types
Status: Approved
Author: Eric (with AI assistance)
Created: 2026-03-05
Approved: 2026-03-05
Affects: Compiler (type checker, ARC pipeline, LLVM codegen), type system, memory model, spec
Related: capability-unification-generics-proposal.md (approved), memory-model-edge-cases-proposal.md (approved), sendable-interior-mutability-proposal.md (approved), clone-trait-proposal.md (approved)
Supersedes: Copy trait slot from copy-semantics-and-reserved-keywords-proposal.md, inline type approach from low-level-future-proofing-proposal.md
Summary
Introduce a Value marker trait that guarantees a type is always stored inline, copied by value, and completely bypasses ARC (automatic reference counting). Types declared with Value are never heap-allocated, never reference-counted, and always bitwise-copyable — like primitives, but for user-defined types.
type Point: Value, Eq = { x: float, y: float }
type Color: Value, Eq = { r: byte, g: byte, b: byte, a: byte }
type Vec3: Value = { x: float, y: float, z: float }
Problem Statement
Ori’s ARC system already optimizes small all-scalar structs to skip reference counting (the ArcClassifier in ori_arc/src/classify/mod.rs classifies them as Scalar). However, this optimization is implicit and fragile:
1. Silent Performance Regression
Adding a single reference-counted field silently changes a type from Scalar to DefiniteRef, introducing ARC overhead with no compiler feedback:
type GameEntity = { x: float, y: float, health: int } // Scalar today
// Later, someone adds a name field:
type GameEntity = { x: float, y: float, health: int, name: str }
// Now DefiniteRef — ARC overhead on every copy, but no warning
2. No Semantic Contract
Library authors cannot express “this type must always be a value type” in their API. Users can unknowingly break performance invariants that the library depends on.
3. Implicit Clone vs Copy Ambiguity
Ori currently treats all value-passing as conceptual “copy” — but there’s no way to distinguish types that are trivially copyable (bitwise copy, no side effects) from types that require deep cloning. The Clone trait exists for explicit deep copies, but there’s no marker for “this type is always trivially copyable.”
4. Const Generic Eligibility
The capability-unification proposal ties const generic eligibility to Eq + Hashable. Value types are natural candidates for const generics — but there’s no way to express “this type is always a compile-time value.”
Design
The Value Trait
Value is a marker trait in the prelude with no methods:
trait Value: Clone, Eq {
// Marker trait — no methods.
// Guarantees: inline storage, bitwise copy, no ARC, no Drop.
}
Supertrait rationale: Value types are always copyable (trivially, via bitwise copy — satisfying Clone) and must support equality comparison (required for identity-free semantics). If a type can be bitwise-copied, Clone is automatically satisfied with zero cost.
Declaration Syntax
Following the capability-unification proposal (: for structural capabilities):
// Value type with derived Eq
type Point: Value, Eq = { x: float, y: float }
// Value type with derived Eq and Hashable
type Color: Value, Eq, Hashable = { x: byte, y: byte, z: byte, a: byte }
// Value type — Eq is implied by Value's supertrait, but derived impl still needed
type Vec3: Value, Eq = { x: float, y: float, z: float }
// Sum types can also be Value
type Direction: Value, Eq = North | South | East | West
// Newtypes wrapping value types
type Meters: Value, Eq = float
type UserId: Value, Eq = int
Primitive Types Are Implicitly Value
All primitive types satisfy Value without declaration:
| Type | Value? | Reason |
|---|---|---|
int | Yes | 8-byte scalar |
float | Yes | 8-byte scalar |
bool | Yes | 1-byte scalar |
char | Yes | 4-byte scalar |
byte | Yes | 1-byte scalar |
void | Yes | Zero-size |
Duration | Yes | 8-byte scalar |
Size | Yes | 8-byte scalar |
Ordering | Yes | Enum of 3 unit variants |
Never | Yes | Uninhabited (vacuously true) |
str | No | Heap-allocated, reference-counted |
Compound Type Rules
| Type | Value When |
|---|---|
(T, U, ...) | All elements are Value |
Option<T> | T: Value |
Result<T, E> | T: Value and E: Value |
Range<T> | T: Value |
[T] | Never — heap-allocated |
{K: V} | Never — heap-allocated |
Set<T> | Never — heap-allocated |
(T) -> U | Never — may capture heap-allocated values |
Fixed-Capacity Lists
Fixed-capacity lists ([T, max N]) are a special case. They are inline-allocated with a compile-time maximum size, so they could satisfy Value when T: Value:
type SmallBuffer: Value, Eq = { data: [byte, max 64], len: int }
Whether [T, max N] satisfies Value depends on implementation strategy. This proposal defers this decision — initially, [T, max N] does NOT satisfy Value. A future proposal can add it once the fixed-capacity list representation is finalized.
Semantic Rules
Rule 1: All Fields Must Be Value
Every field of a Value type must itself satisfy Value:
type Valid: Value, Eq = { x: int, y: float } // OK: int and float are Value
type Invalid: Value, Eq = { x: int, name: str } // ERROR: str is not Value
Rule 2: No Drop Implementation
Value types cannot implement Drop. Bitwise-copyable types with custom destructors are contradictory — if a type needs cleanup, it has identity semantics and is not a pure value:
type Handle: Value, Eq = { fd: int }
impl Handle: Drop { // ERROR: Value types cannot implement Drop
@drop (self) -> void = close(self.fd)
}
Rule 3: No Recursive Types
Recursive types require heap indirection and cannot be stored inline:
type Tree: Value, Eq = { // ERROR: recursive type cannot be Value
value: int,
children: [Tree], // [Tree] is heap-allocated
}
Rule 4: Size Limit
Value types must fit within a reasonable inline size threshold. Types exceeding 256 bytes produce a warning; types exceeding 512 bytes produce an error:
type TooLarge: Value, Eq = {
data: (float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float,
float, float, float, float)
}
// ERROR: Value type exceeds 512 bytes (512 bytes)
// help: consider using a heap-allocated type instead
Rationale: Excessively large value types cause stack overflow risk and poor cache performance from frequent large copies. The 256/512-byte thresholds accommodate common use cases like 4×4 float matrices (128 bytes) and 4×4 double matrices (256 bytes), while preventing truly excessive inline types.
Rule 5: Automatic Clone Satisfaction
A Value type automatically satisfies Clone via bitwise copy. No derived or manual Clone implementation is needed:
type Point: Value, Eq = { x: float, y: float }
// Point automatically satisfies Clone — .clone() returns a bitwise copy
let p1 = Point { x: 1.0, y: 2.0 }
let p2 = p1.clone() // Works, but is a no-op (same as let p2 = p1)
Rule 6: Automatic Sendable Satisfaction
All Value types are automatically Sendable. Since they have no heap references and no identity, they are trivially safe to send across task boundaries:
type Point: Value, Eq = { x: float, y: float }
// Point is automatically Sendable — no verification needed
parallel(tasks: [
() -> compute(Point { x: 1.0, y: 2.0 }), // OK
])
Rule 7: No Manual Implementation
Like Sendable, Value cannot be manually implemented:
impl MyType: Value { } // ERROR: Value cannot be implemented manually
Value is declared on the type definition (via :) and verified by the compiler. This prevents users from lying about value semantics.
Interaction with ARC Pipeline
Current Behavior (Implicit)
The ArcClassifier already classifies all-scalar structs as Scalar:
type Point = { x: float, y: float }
→ ArcClassifier: Scalar (no RC ops emitted)
But this is implicit — the compiler could silently change classification if fields change.
New Behavior (Explicit + Enforced)
With Value, the classifier has an explicit signal:
type Point: Value, Eq = { x: float, y: float }
→ Type checker: verify all fields are Value ✓
→ ArcClassifier: Scalar (guaranteed by Value contract)
If a field violates the contract, the type checker catches it before the ARC pipeline runs:
type Bad: Value, Eq = { x: float, name: str }
→ Type checker: ERROR — str is not Value
Implementation Strategy
The Value trait acts as a compile-time assertion that feeds into the existing ARC pipeline:
- Type checker validates all
Valuerules (fields, no Drop, no recursion, size) - ARC classifier can fast-path
Valuetypes toScalarwithout field traversal - LLVM codegen can use
memcpyforValuetypes without RC inc/dec - Borrow inference skips
Valueparameters (alwaysOwned, trivially cheap)
Interaction with Other Features
Generics and Bounds
Value can be used as a generic bound:
@fast_swap<T: Value> (a: T, b: T) -> (T, T) = (b, a)
@zero_copy_buffer<T: Value, $N: int> (initial: T) -> [T, max N] =
// T is guaranteed inline — no RC overhead in buffer operations
...
Const Generic Eligibility
The capability-unification proposal requires Eq + Hashable for const generic types. Since Value implies Eq (via supertrait), Value + Hashable types are natural const generic candidates:
type Color: Value, Eq, Hashable = { r: byte, g: byte, b: byte }
@create_palette<$theme: Color> () -> [Color] = [theme, ...]
Struct Update Syntax
Value types work naturally with struct update syntax — no COW overhead:
type Point: Value, Eq = { x: float, y: float }
let p1 = Point { x: 1.0, y: 2.0 }
let p2 = { ...p1, x: 3.0 } // Bitwise copy + field override, no RC
Pattern Matching
Value types in pattern matching need no RC operations for binding:
type Color: Value, Eq = { r: byte, g: byte, b: byte, a: byte }
match pixel {
Color { r, g, b, a: 255 } -> rgb_blend(r, g, b), // No RC inc for r, g, b
Color { a: 0, .. } -> transparent(),
c -> default_blend(c), // Bitwise copy, no RC
}
as Conversions
Value types may implement As for conversions between value types:
type Celsius: Value, Eq = float
type Fahrenheit: Value, Eq = float
impl Celsius: As<Fahrenheit> {
@as (self) -> Fahrenheit = Fahrenheit(self.inner * 9.0 / 5.0 + 32.0)
}
Error Messages
Non-Value Field
error[E2040]: field `name` of type `str` does not satisfy `Value`
--> src/types.ori:2:5
|
1 | type Entity: Value, Eq = {
| ----- `Value` declared here
2 | name: str,
| ^^^^^^^^^ `str` is heap-allocated and reference-counted
|
= note: all fields of a `Value` type must themselves be `Value`
= help: remove `Value` from the type declaration, or change `name` to a value type
Drop Conflict
error[E2041]: `Value` type `Handle` cannot implement `Drop`
--> src/types.ori:3:1
|
1 | type Handle: Value, Eq = { fd: int }
| ----- `Value` declared here
...
3 | impl Handle: Drop {
| ^^^^^^^^^^^^^^^^^^^^^ `Drop` requires identity semantics
|
= note: `Value` types are bitwise-copyable — custom destructors would
run on every copy, not just the "last" one
= help: remove `Value` to use reference-counted semantics with `Drop`
Size Exceeded
error[E2042]: `Value` type `HugeMatrix` exceeds maximum size (512 bytes)
--> src/types.ori:1:1
|
1 | type HugeMatrix: Value, Eq = { ... }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ size is 1024 bytes
|
= note: Value types are copied on every assignment — large values
cause stack pressure and poor cache performance
= help: remove `Value` to use heap allocation, or reduce the type's size
Size Warning
warning[W2040]: `Value` type `LargeStruct` is large (384 bytes)
--> src/types.ori:1:1
|
1 | type LargeStruct: Value, Eq = { ... }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ consider whether heap allocation is more appropriate
|
= note: Value types are copied on every assignment
= help: types over 256 bytes often perform better with reference counting
Generic Bound Violation
error[E2043]: type `[int]` does not satisfy bound `Value`
--> src/types.ori:5:20
|
5 | fast_swap(a: list1, b: list2)
| ^^^^^ `[int]` is heap-allocated
|
= note: `fast_swap` requires `T: Value`
= help: `[int]` uses reference counting and cannot be a `Value`
Prior Art
Rust — Copy Trait
Rust’s Copy trait is the closest analogue. Types implementing Copy are bitwise-copied on assignment (no move semantics), and must not implement Drop. Copy implies Clone.
Similarities: marker trait, field recursion, no Drop, implies Clone.
Differences: Rust’s Copy is auto-derivable with #[derive(Copy)]; Ori’s Value is declared on the type. Rust separates Copy from Clone; Ori’s Value implies both.
C# — struct vs class
C# uses struct for value types and class for reference types. Value types are stack-allocated, copied by value, and cannot be null.
Similarities: explicit opt-in, inline storage, copy semantics.
Differences: C# makes this a fundamental type-system split. Ori keeps a single type keyword and uses the Value trait for opt-in value semantics.
Swift — Value Types (Structs)
Swift structs are value types with COW (copy-on-write) optimization. All Swift structs are value types; classes are reference types.
Similarities: value semantics, copy-by-default.
Differences: Swift makes ALL structs value types. Ori defaults to ARC for all types and uses Value for opt-in ARC bypass. Swift uses COW for collections inside structs; Ori’s Value types cannot contain collections.
Lean 4 — Scalar Classification
Lean 4’s LCNF IR classifies types as scalar or object for RC insertion, similar to Ori’s ArcClassifier. However, this is an implementation detail, not a user-facing trait.
Similarities: scalar classification bypasses RC.
Differences: Lean’s classification is implicit; Ori’s Value makes it explicit and enforced.
Koka — Value Types
Koka has value types that are unboxed and stored inline. Like Lean, this is mostly automatic rather than user-declared.
Similarities: inline storage, no boxing overhead. Differences: Koka infers value representation; Ori lets users declare it.
Examples
Game Development
type Vec2: Value, Eq = { x: float, y: float }
type Vec3: Value, Eq = { x: float, y: float, z: float }
type Quaternion: Value, Eq = { x: float, y: float, z: float, w: float }
type AABB: Value, Eq = { min: Vec3, max: Vec3 } // 48 bytes — fine
@lerp (a: Vec3, b: Vec3, t: float) -> Vec3 =
Vec3 {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
z: a.z + (b.z - a.z) * t,
}
// No ARC overhead anywhere in this hot path
@update_positions (positions: [Vec3], velocities: [Vec3], dt: float) -> [Vec3] =
positions.zip(other: velocities)
|> .map(transform: (p, v) -> Vec3 {
x: p.x + v.x * dt,
y: p.y + v.y * dt,
z: p.z + v.z * dt,
})
|> .collect()
Domain Modeling with Newtypes
type Meters: Value, Eq, Comparable = float
type Seconds: Value, Eq, Comparable = float
type MetersPerSecond: Value, Eq, Comparable = float
@velocity (distance: Meters, time: Seconds) -> MetersPerSecond =
MetersPerSecond(distance.inner / time.inner)
// All operations are zero-overhead — no heap, no RC
Color Processing
type RGBA: Value, Eq, Hashable = { r: byte, g: byte, b: byte, a: byte }
@blend (src: RGBA, dst: RGBA) -> RGBA = {
let sa = src.a as float / 255.0
let da = dst.a as float / 255.0
let out_a = sa + da * (1.0 - sa)
RGBA {
r: ((src.r as float * sa + dst.r as float * da * (1.0 - sa)) / out_a) as byte,
g: ((src.g as float * sa + dst.g as float * da * (1.0 - sa)) / out_a) as byte,
b: ((src.b as float * sa + dst.b as float * da * (1.0 - sa)) / out_a) as byte,
a: (out_a * 255.0) as byte,
}
}
Value Sum Types
type Axis: Value, Eq = X | Y | Z
type CardinalDirection: Value, Eq = North | South | East | West
type Tile: Value, Eq = Wall | Floor | Door(locked: bool) | Stairs(direction: int)
@opposite (dir: CardinalDirection) -> CardinalDirection =
match dir {
North -> South,
South -> North,
East -> West,
West -> East,
}
Implementation
Phase 1: Type Checker Validation
- Add
Valueto the prelude as a marker trait (no methods) - In
ori_types/check/registration/, registerValuewith supertraitClone + Eq - In
ori_types/check/, add validation pass forValuetypes:- All fields satisfy
Value(recursive check) - No
Dropimpl registered for the type - No recursive type structure
- Size within threshold
- All fields satisfy
- Add error codes E2040-E2043, warning W2040
Valuetypes automatically satisfySendable
Phase 2: ARC Pipeline Fast Path
- In
ori_arc/src/classify/mod.rs, add fast path: types withValuetrait →ArcClass::Scalar(skip field traversal) - In
ori_arc/src/borrow/, skip borrow inference forValueparameters (always cheap to copy) - In
ori_arc/src/rc_insert/, skip RC insertion forValue-typed variables
Phase 3: LLVM Codegen Optimization
- In
ori_llvm, usememcpyforValuetypes instead of field-by-field copy with RC Valuetypes can be passed in registers or by value (no indirection through pointers)- LLVM can fully optimize
Valuetype operations (SROA, constant folding)
Phase 4: Const Generic Integration
Value + Hashabletypes are eligible as const generic parametersValuetypes can appear in const expressions
Test Cases
- Basic
Valuetype declaration and usage Valuetype with all-scalar fields — verify no RC operations emittedValuetype with non-Value field — verify compile error E2040Valuetype withDropimpl — verify compile error E2041Valuetype exceeding size limit — verify error E2042 / warning W2040Valueas generic bound — verify enforcement at call sitesValuesum types (all-unit and with Value payloads)Valuenewtype wrapping Value typeValuenewtype wrapping non-Value type — verify compile error- Nested
Valuetypes (AABBcontainingVec3) Valuetype in pattern matching — verify no RC operationsValuetype satisfiesClonewithout explicit deriveValuetype satisfiesSendableautomaticallyValuetype inparallel()tasks — no Sendable verification needed- Implicit
Valuefor primitive types in generic contexts
Spec Changes Required
Update 06-types.md
Add “Value Types” section:
Valuetrait definition (marker, no methods)- Supertrait relationship (
Clone + Eq) - Field recursion rules
- Size thresholds
- Interaction with primitives
Update 15-memory-model.md
Add “Value Type Memory Semantics” section:
- Inline storage guarantee
- Bitwise copy semantics
- No ARC participation
- No Drop allowed
Update 08-traits.md
Add Value to prelude traits table:
- Marker trait, no methods
- Cannot be manually implemented
- Automatic
CloneandSendablesatisfaction
Update grammar.ebnf
No grammar changes needed — Value uses the existing : syntax from the capability-unification proposal.
Add Error Codes
| Code | Description |
|---|---|
| E2040 | Field of Value type does not satisfy Value |
| E2041 | Value type cannot implement Drop |
| E2042 | Value type exceeds maximum size (512 bytes) |
| E2043 | Type does not satisfy Value bound |
| W2040 | Value type is large (>256 bytes) |
Design Decisions
-
Marker trait over keyword — Using
type Point: Valueinstead of a separate keyword (value type Point,struct Point,inline type Point) keeps onetypekeyword and leverages the capability-unification model. Value semantics is a capability of the type, not a different kind of type. -
Opt-in over opt-out — Defaulting to ARC and opting into value semantics is safer than the reverse. Most types benefit from ARC (shared ownership, no large copies). Only performance-critical small types need
Value. This avoids C#‘s problem where choosingstructvsclassis a premature decision. -
Supertrait
Clone + Eq— Every value type is trivially cloneable (bitwise copy) and should support equality (value types have no identity, only value). This avoids orphanValuetypes that can’t be compared or copied. -
No manual impl — Same rationale as
Sendable:Valueis a safety property verified by the compiler. Manual implementation could cause misclassification in the ARC pipeline, leading to use-after-free or leaks. -
Size limits — Excessively large value types defeat the purpose (stack pressure, cache misses from large copies). The 256/512-byte thresholds accommodate common use cases (4×4 float matrices at 128 bytes, 4×4 double matrices at 256 bytes) while preventing truly excessive inline types. Most real-world value types (Vec3, Color, Quaternion) are well under 128 bytes.
-
strexcluded — Strings are reference-counted in Ori (including SSO for ≤23 bytes). Even though short strings are inline, thestrtype itself participates in ARC at the type level. Mixing SSO withValuesemantics creates confusion about when copies are free vs expensive. -
Functions excluded — Closures may capture heap-allocated values. Even function pointers (no captures) use a fat-value representation. Excluding functions keeps
Valuesimple and predictable. -
Fixed-capacity lists deferred —
[T, max N]could beValue(inline-allocated, compile-time bounded), but the representation details need to settle first. A follow-up proposal can add this.
Relationship to Existing Proposals
Supersedes: Copy Trait Slot
The copy-semantics-and-reserved-keywords-proposal (approved 2026-02-02) reserved a Copy trait slot:
pub trait Copy: Clone {}
Value supersedes this reservation. Both traits serve the same purpose (bitwise-copyable, implies Clone, no Drop), but Value is strictly stronger (also requires Eq). Having both Copy and Value as separate traits would create confusion — Value is the single concept Ori uses for this purpose.
The Copy trait slot in copy-semantics-and-reserved-keywords-proposal should be considered retired. The reserved keywords (union, static, asm) from that proposal remain unchanged.
Supersedes: inline type Approach
The low-level-future-proofing-proposal (approved 2026-02-02) reserved the inline keyword and ValueCategory::Inline for stack-allocated types:
inline type Vec3 = { x: float, y: float, z: float }
Value replaces this approach with a trait-based design:
type Vec3: Value, Eq = { x: float, y: float, z: float }
A trait is more composable than a keyword — it supports generic bounds (T: Value), conditional satisfaction, and integrates naturally with the capability-unification syntax. The inline keyword remains reserved for potential future fine-grained control (e.g., forcing inline storage for non-Value types in specific contexts). ValueCategory::Inline in the IR maps to types with the Value trait.
Updates: Memory Model Edge Cases
The memory-model-edge-cases-proposal states “Ori doesn’t distinguish Copy vs Clone at the language level.” The Value trait introduces this distinction — Value types are trivially copyable (bitwise copy satisfies Clone), while non-Value types may require deep cloning. An erratum will be added to that proposal.
Resolved Questions
-
ValueimpliesEq(supertrait). Value types have no identity, only value — equality by value is fundamental.floatalready has anEqimpl in Ori (NaN == NaN returns false, which is valid). Users must explicitly write: Value, Eqto get both traits. -
Size limits are fixed, not configurable per type. 256-byte warning and 512-byte error thresholds cover 99% of use cases. Types exceeding 512 bytes should use heap allocation.
-
Valuedoes NOT auto-deriveEq. Users must explicitly write: Value, Eq. This makes the Eq impl visible and explicit, consistent with how other derived traits work. -
[T, max N]is deferred. Fixed-capacity lists could satisfyValuewhenT: Value, but representation details need to settle first. A follow-up proposal can add this.
Future Extensions
Value Generics
@stack_allocate<T: Value> (count: int) -> [T, max 1024] =
// Guaranteed no heap allocation for T — all inline
...
SIMD-Friendly Value Types
#repr("aligned", 16)
type Vec4f: Value, Eq = { x: float, y: float, z: float, w: float }
// LLVM can auto-vectorize operations on aligned Value types
Value in embed()
let $palette: [RGBA, max 256] = embed("palette.bin")
// Compile-time embedded binary data as Value array — zero runtime allocation
Summary
| Aspect | Rule |
|---|---|
| What it is | Marker trait guaranteeing inline, ARC-free value semantics |
| Syntax | type T: Value, Eq = { ... } |
| Supertrait | Clone + Eq |
| Fields | All must satisfy Value |
| Drop | Prohibited |
| Size | Warning >256 bytes, error >512 bytes |
| Sendable | Automatically satisfied |
| Manual impl | Prohibited |
| Primitives | Implicitly Value |
| Collections | [T], {K: V}, Set<T> — never Value |
| Functions | Never Value |
| ARC effect | Classified as Scalar — no RC operations |
| Keyword | None — uses existing : syntax |