Section 02: Transitive Triviality & ARC Elision
Context: Two independent triviality systems exist today:
ori_arc::ArcClassifier(re-exported at crate root; internal moduleori_arc::classifyispub(crate)) — used during ARC IR lowering, classifies asScalar/DefiniteRef/PossibleRefori_llvm::codegen::type_info::TypeInfoStore::is_trivial()— used during LLVM codegen, walks type tree
These MUST agree, but they use different algorithms and data structures, and they currently disagree on iterators: ArcClassifier classifies Iterator/DoubleEndedIterator as Scalar (Box-allocated, no RC header), while TypeInfoStore::is_trivial() classifies them as non-trivial (heap-backed). Unifying them ensures no redundant RC operations on types that can never hold heap references.
Reference implementations:
- Swift
lib/SIL/SILType.cpp:isTrivial()walks type structure recursively, caches results - Lean4
src/Lean/Compiler/IR/RC.lean:VarInfo.isPossibleRef/isDefiniteRef— two-bit classification - ori_arc
compiler/ori_arc/src/classify/mod.rs:ArcClassifierwithFxHashMapcache + cycle detection
Depends on: §01 (ReprPlan stores triviality decisions).
§01 dependency scope: This section requires two things from §01:
ReprPlan::is_trivial()query method (§01.4) — so codegen can check trivialityReprPlan::set_repr()builder method (§01.2) — so triviality pass can record decisions
If §01 is incomplete, the core algorithm (classify_triviality() in ori_types) can still be implemented and tested standalone. The ArcClassifier delegation (02.1) and the ARC elision (02.3) do NOT require ReprPlan — they only require the shared classify_triviality() function. The ReprPlan integration (02.1 bullet 3, “Make TypeInfoStore delegate”) is the only step that truly blocks on §01 completion. Mark it as deferred if §01 is not ready.
Evaluator impact: ori_eval does NOT use triviality classification and is NOT affected by this section. The evaluator operates at the value level with Rust-native reference counting (no ori_rc_* calls). All changes in this section are confined to ori_types, ori_arc, and ori_llvm.
02.1 Unify Triviality Classification
File(s): compiler/ori_types/src/triviality/mod.rs (NOT ori_repr — avoids circular dep since both ori_arc and ori_repr depend on ori_types), compiler/ori_arc/src/classify/mod.rs
Today, ArcClassifier and TypeInfoStore::is_trivial() duplicate logic. We need a single source of truth.
-
Create
compiler/ori_types/src/triviality/mod.rswith theTrivialityenum andclassify_triviality()entry point (placed inori_types, NOTori_repr, to avoid circular deps — bothori_arcandori_reprdepend onori_types)://! Transitive triviality classification for type-level ARC elision. //! //! A type is *trivial* when it (and all its transitive children) contain //! no heap references requiring ARC operations. Trivial types can skip //! all `ori_rc_inc`/`ori_rc_dec` calls in generated code. //! //! Single source of truth: both `ori_arc::ArcClassifier` and //! `ori_llvm::TypeInfoStore` delegate to [`classify_triviality`]. /// Triviality classification for a type in the Pool. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Triviality { /// No heap references anywhere in the type tree Trivial, /// Contains at least one heap reference (str, [T], etc.) NonTrivial, /// Contains unresolved type variables — must assume non-trivial Unknown, } /// Classify whether `idx` is trivial (no ARC needed) in the given Pool. pub fn classify_triviality(idx: Idx, pool: &Pool) -> Triviality { let mut visiting = FxHashSet::default(); classify_recursive(idx, pool, &mut visiting) } -
Wire up delegation from both consumers (no duplicate logic allowed):
ori_arc::ArcClassifier::classify()delegates toori_types::classify_triviality(), mapping:Trivial→ArcClass::Scalar,NonTrivial→ArcClass::DefiniteRef,Unknown→ArcClass::PossibleRefori_repr::ReprPlan::is_trivial()delegates toori_types::classify_triviality()
-
Make
TypeInfoStore::is_trivial()delegate toReprPlan::is_trivial():ReprPlancaches the result from the triviality pass- Codegen never re-computes triviality
-
Add consistency test: for every type that
ArcClassifierclassifies asScalar,ReprPlan::is_trivial()must returntrue, and vice versa -
Salsa integration:
Trivialityis NOT a Salsa query. It is a pure function(Idx, &Pool) -> Trivialitywith no mutable state. Caching is handled at the consumer level:ArcClassifieralready caches viaRefCell<FxHashMap<Idx, ArcClass>>— the delegation toclassify_triviality()replaces the body ofclassify_by_tag(), keeping the existing cacheReprPlanstores triviality as a field inReprDecision— computed once duringanalyze_triviality(), never recomputedTypeInfoStoredelegates toReprPlan(which is already computed) — no Salsa query needed- If future JIT hot-reload needs incremental triviality, it recomputes per changed function’s types (same model as §01.6)
TrivialityderivesClone, Copy, PartialEq, Eq, Hash, Debug— Salsa-compatible if ever wrapped in a query
02.2 Transitive Walk with Cycle Detection
File(s): compiler/ori_types/src/triviality/mod.rs (was triviality.rs — uses directory module for sibling test file)
The recursive walk must handle all compound types and detect cycles (recursive structs/enums).
-
Implement transitive classification (private helper — not
pub):fn classify_recursive( idx: Idx, pool: &Pool, visiting: &mut FxHashSet<Idx>, ) -> Triviality { let resolved = pool.resolve_fully(idx); let tag = pool.tag(resolved); // Fast path: primitives match tag { Tag::Int | Tag::Float | Tag::Bool | Tag::Char | Tag::Byte | Tag::Unit | Tag::Never | Tag::Duration | Tag::Size | Tag::Ordering => return Triviality::Trivial, // Always heap-allocated with RC headers Tag::Str | Tag::List | Tag::Map | Tag::Set | Tag::Channel => return Triviality::NonTrivial, // Iterators: Box-allocated (no RC header, no ori_rc_alloc) — Scalar // per ArcClassifier. TypeInfoStore::is_trivial() currently disagrees // (classifies as non-trivial); this unification resolves in favor of // ArcClassifier's Scalar classification. Tag::Iterator | Tag::DoubleEndedIterator => return Triviality::Trivial, // Error placeholder: propagates silently, classified as Scalar // by ArcClassifier (Idx::ERROR is a pre-interned primitive). Tag::Error => return Triviality::Trivial, // Unresolved type variables Tag::Var | Tag::BoundVar | Tag::RigidVar => return Triviality::Unknown, // Borrowed is reserved (future: &T, Slice<T>); should never // reach triviality analysis. Conservative fallback. Tag::Borrowed => return Triviality::Unknown, // Named/Applied/Alias: resolve and re-classify. // ArcClassifier handles these via resolve_fully() + re-dispatch. // // IMPORTANT: Newtypes (e.g., `type UserId = int`) use Tag::Named in // the Pool. `resolve_fully()` resolves Named→concrete when a Pool // resolution exists. For newtypes, the Pool resolution points to the // underlying type (e.g., Named("UserId") resolves to Int). This // means newtypes are handled transparently: `type UserId = int` → // resolve_fully → Tag::Int → Trivial. No special-case needed. // // If resolve_fully returns the same idx (unresolvable Named), this // could be a newtype whose resolution hasn't been set yet (typeck // bug) or a forward reference. We return Unknown (conservative). // // CPtr, JsValue, c_int, etc. are NOT Pool primitives — they are // user-level named types in the FFI prelude. At the Pool level, // CPtr is Tag::Named("CPtr") which resolves to an opaque pointer. // CPtr should be Trivial (it's a raw pointer with no RC header, // same as OpaquePtr in MachineRepr). JsValue is platform-specific. // Since these resolve via Named→concrete, they are handled by // this same resolution path. If they resolve to a type with no // heap RC semantics, they classify as Trivial. Tag::Named | Tag::Applied | Tag::Alias => { let inner = pool.resolve_fully(resolved); if inner == resolved { return Triviality::Unknown; // unresolvable } return classify_recursive(inner, pool, visiting); } // Type schemes, projections, module namespaces, inference // placeholders, Self type — conservative fallback. Tag::Scheme | Tag::Projection | Tag::ModuleNs | Tag::Infer | Tag::SelfType => return Triviality::Unknown, _ => {} // compound types — recurse } // Cycle detection if !visiting.insert(resolved) { // Recursive type — must be heap-allocated (requires indirection) return Triviality::NonTrivial; } let result = match tag { Tag::Option => classify_recursive(pool.option_inner(resolved), pool, visiting), Tag::Result => { let ok = classify_recursive(pool.result_ok(resolved), pool, visiting); let err = classify_recursive(pool.result_err(resolved), pool, visiting); merge_triviality(ok, err) } Tag::Tuple => { let elems = pool.tuple_elems(resolved); let mut result = Triviality::Trivial; for elem in &elems { result = merge_triviality( result, classify_recursive(*elem, pool, visiting), ); if result == Triviality::NonTrivial { break; } } result } Tag::Struct => { // Walk all fields — struct_fields() returns Vec<(Name, Idx)> let fields = pool.struct_fields(resolved); let mut result = Triviality::Trivial; for (_, field_ty) in &fields { result = merge_triviality( result, classify_recursive(*field_ty, pool, visiting), ); if result == Triviality::NonTrivial { break; } } result } Tag::Enum => { // Walk all variant fields — enum_variants() returns Vec<(Name, Vec<Idx>)> let variants = pool.enum_variants(resolved); let mut result = Triviality::Trivial; for (_, field_types) in &variants { for field_ty in field_types { result = merge_triviality( result, classify_recursive(*field_ty, pool, visiting), ); if result == Triviality::NonTrivial { break; } } if result == Triviality::NonTrivial { break; } } result } Tag::Function => Triviality::NonTrivial, // closures capture heap refs // Range is currently always int/float (both trivial). If Range<T> // ever supports non-scalar T, this must recurse into the element. Tag::Range => { let elem = pool.range_elem(resolved); classify_recursive(elem, pool, visiting) } // All other tags handled in fast-path above; this arm is // unreachable after the exhaustive early returns. _ => Triviality::Unknown, }; visiting.remove(&resolved); result } /// Private helper — lattice merge (NonTrivial > Unknown > Trivial). fn merge_triviality(a: Triviality, b: Triviality) -> Triviality { match (a, b) { (Triviality::NonTrivial, _) | (_, Triviality::NonTrivial) => Triviality::NonTrivial, (Triviality::Unknown, _) | (_, Triviality::Unknown) => Triviality::Unknown, _ => Triviality::Trivial, } } -
Write tests in
compiler/ori_types/src/triviality/tests.rs(sibling convention:triviality.rsbecomestriviality/mod.rswith#[cfg(test)] mod tests;at the bottom; test body intriviality/tests.rs) covering:Primitive tags (exhaustive — one test per Tag variant):
int→ Trivialfloat→ Trivialbool→ Trivialchar→ Trivialbyte→ Trivialvoid(Unit) → TrivialNever→ TrivialDuration→ TrivialSize→ TrivialOrdering→ Trivialstr→ NonTrivialError→ Trivial (pre-interned primitive, Idx::ERROR)
Simple containers:
[int]→ NonTrivial (list itself is heap-allocated)Option<int>→ TrivialOption<str>→ NonTrivialOption<Option<int>>→ TrivialSet<int>→ NonTrivialChannel<int>→ NonTrivialRange<int>→ TrivialIterator<int>→ Trivial (Box-allocated, no RC header)DoubleEndedIterator<int>→ Trivial
Two-child containers:
{str: int}(Map) → NonTrivialResult<int, Ordering>→ TrivialResult<int, str>→ NonTrivial
Compound types:
(int, float, bool)→ Trivial(int, str)→ NonTrivialstruct Point { x: int, y: int }→ Trivialstruct Named { name: str, age: int }→ NonTrivial- Recursive struct → NonTrivial
- Enum with all-scalar variants → Trivial
- Enum with one non-trivial variant → NonTrivial
Functiontype → NonTrivial (closures capture heap refs)
Named type resolution:
- Newtype
type UserId = int→ Trivial (resolves through Named to Int) - Newtype
type Name = str→ NonTrivial (resolves through Named to Str) - Newtype wrapping trivial struct
type Coord = Point→ Trivial - Unresolvable Named → Unknown
Type variables:
Var(unresolved) → UnknownBoundVar→ UnknownRigidVar→ Unknown
Special types:
Borrowed→ UnknownScheme→ UnknownProjection→ UnknownIdx::NONEsentinel → Trivial (matches ArcClassifier behavior)
02.3 ARC Elision in ori_arc Pipeline
File(s): compiler/ori_arc/src/rc_insert/mod.rs, compiler/ori_arc/src/classify/mod.rs
When the triviality pass marks a type as Trivial, the ARC pipeline must skip ALL RC operations for values of that type.
-
In
rc_insert, check triviality before insertingRcInc/RcDec:- If the variable’s type is
Trivialin theReprPlan, skip RC insertion entirely - This must work for compound types:
Option<int>variables must skip RC even thoughOptionitself has a generic form that might need RC
- If the variable’s type is
-
In
compute_var_reprs()(compiler/ori_arc/src/ir/repr.rs), setValueRepr::Scalarfor all trivial types:ValueReprhas four variants:Scalar,RcPointer,Aggregate,FatValue- Currently,
Option<T>always getsValueRepr::Aggregateregardless of T (driven byValueRepr::from_arc_class()+from_ref_tag()) - With triviality,
Option<int>should getValueRepr::Scalar(no RC fields) - The fix point is
ValueRepr::from_arc_class(): whenArcClass::Scalar, result is alreadyScalar; the real change is makingArcClassifierreturnScalarfor trivially-composed compound types
-
Update
drop/mod.rs—compute_drop_info()should returnNonefor trivial types:- Currently,
compute_drop_info()returnsNoneonly whenclassifier.is_scalar(ty)is true (line 135). ForPossibleReftypes whose children are all scalar, it returnsSome(DropInfo { kind: DropKind::Trivial })— a trivial drop that still generates a drop function (justori_rc_free+ ret). - With unified triviality, these should return
None(no drop function needed at all) - Note:
compute_fields_drop()already returnsDropKind::Trivial(notDropKind::Fields([])) when no fields need RC; the issue is thatcompute_drop_infowraps it inSome(DropInfo)instead of returningNone
- Currently,
02.4 Drop Function Elision
File(s): compiler/ori_llvm/src/codegen/arc_emitter/drop_gen.rs
When compute_drop_info() returns None, the LLVM drop function generator must NOT emit a drop function. This saves code size and eliminates dead function definitions.
- In
get_or_generate_drop_fn()(compiler/ori_llvm/src/codegen/arc_emitter/element_fn_gen.rs) andgenerate_drop_fn()(drop_gen.rs), return a null function pointer for trivial types - In
ori_rc_deccall sites, skip the call entirely when drop_fn is null - Verify:
ORI_LOG=ori_llvm=debug ori buildshould NOT emit_ori_drop$functions forOption<int>,(int, float),Result<int, bool>, etc.
02.5 Newtype & FFI Type Handling
File(s): compiler/ori_types/src/triviality/mod.rs (algorithm), compiler/ori_types/src/registry/types/mod.rs (reference for TypeKind::Newtype)
Newtypes and FFI types are not separate Tag variants — they use Tag::Named and resolve via pool.resolve_fully(). This section documents the handling and adds targeted tests.
Newtypes:
-
type UserId = intcreates aTag::Namedentry in the Pool with a resolution toIdx::INT -
resolve_fully()follows the Named→concrete chain transparently -
The
TypeRegistrystoresTypeKind::Newtype { underlying }for semantic purposes (e.g.,.inneraccess), but triviality classification only needs the Pool-level resolution -
No special case needed in
classify_recursive()— theTag::Namedarm already handles this -
Verify:
type UserId = int→resolve_fully()→Tag::Int→Trivial -
Verify:
type Wrapper = [int]→resolve_fully()→Tag::List→NonTrivial -
Verify: nested newtype
type Id = UserId→resolve_fully()chains toTag::Int→Trivial -
Edge case: newtype wrapping a generic parameter that hasn’t been monomorphized → the Named won’t resolve →
Unknown(typeck bug if reached in production)
FFI types (CPtr, JsValue, c_int, etc.):
-
CPtris defined as a named type in the FFI prelude, not a Pool primitive -
At the Pool level:
Tag::Named("CPtr")→ resolves to an opaque pointer representation -
CPtrhas no RC header and no heap allocation semantics → should classify as Trivial -
JsValueandJsPromise<T>are WASM-target types, opaque handles → classify as Trivial (no Ori-managed RC) -
C numeric types (
c_int,c_char,c_float, etc.) resolve to primitive numeric types → Trivial -
Verify:
CPtr→resolve_fully()→ opaque pointer → Trivial -
Verify:
c_int→resolve_fully()→Tag::Int(or appropriate numeric) → Trivial -
Verify:
Option<CPtr>→ Trivial (CPtr inner is trivial) -
Add test for FFI struct containing only C types → Trivial
Note: If a future FFI type has Ori-managed heap semantics (e.g., a reference-counted foreign object), it must resolve to a non-trivial representation. The current design handles this correctly because triviality is determined by what the Named type resolves to, not by the name itself.
02.6 Generic Type & Monomorphization Interaction
File(s): compiler/ori_types/src/triviality/mod.rs
Generic types interact with triviality classification in a specific way: triviality depends on what type parameters are instantiated as. A struct Pair<T> = { a: T, b: T } is trivial when T = int but non-trivial when T = str.
How this works in Ori’s type system:
- Ori does NOT have an explicit monomorphization pass — the type checker infers concrete types, and the Pool stores fully-instantiated versions
Pair<int>andPair<str>are distinctIdxvalues in the Pool, each withTag::Structand concrete field typesclassify_triviality()operates on concreteIdxvalues, so it naturally handles different instantiations correctlypool.resolve_fully()resolves type variables from inference, ensuring all fields are concrete before classification
Precondition: classify_triviality() MUST be called after type checking completes (all inference variables resolved). If any field type is still a Tag::Var, the classification returns Unknown.
- Verify:
Pair<int>(struct with fieldsint, int) → Trivial - Verify:
Pair<str>(struct with fieldsstr, str) → NonTrivial - Verify:
Option<Pair<int>>→ Trivial (recursion through Option inner to Pair struct to Int fields) - Verify: unresolved
Pair<T>where T is still a Var → Unknown (not an error — just conservative) - Verify:
Result<Pair<int>, Pair<float>>→ Trivial (both arms trivial)
No monomorphization-time specialization needed: Unlike integer narrowing (§04) which may produce different MachineRepr for Pair<int> vs Pair<float> (field widths differ), triviality is a simple binary property that falls out naturally from the recursive walk. Each concrete instantiation gets its own Idx and its own triviality result. No special handling required.
02.7 Completion Checklist
Algorithm & unification:
-
//!module doc ontriviality/mod.rsexplaining purpose and ownership - Single
classify_triviality()function inori_typesis the sole source of truth -
Trivialityenum derivesClone, Copy, PartialEq, Eq, Hash, Debug(Salsa-compatible) -
classify_recursive()andmerge_triviality()are private (notpub) -
ArcClassifierdelegates toori_types::classify_triviality()— no duplicate logic -
TypeInfoStore::is_trivial()delegates toReprPlan— no duplicate logic -
classify_triviality()handlesIdx::NONEsentinel (returns Trivial, matching ArcClassifier)
Tag coverage (exhaustive):
- All 12 primitive tags classified (Int, Float, Bool, Str, Char, Byte, Unit, Never, Error, Duration, Size, Ordering)
- All 7 simple container tags classified (List, Option, Set, Channel, Range, Iterator, DoubleEndedIterator)
- All 3 two-child container tags classified (Map, Result, Borrowed)
- All 4 complex type tags classified (Function, Tuple, Struct, Enum)
- All 3 named type tags classified (Named, Applied, Alias) — via resolve_fully
- All 3 type variable tags classified (Var, BoundVar, RigidVar) — Unknown
- All 5 special tags classified (Scheme, Projection, ModuleNs, Infer, SelfType) — Unknown
Newtype & FFI:
-
type UserId = int→ Trivial (resolves via Named) -
type Name = str→ NonTrivial (resolves via Named) - CPtr / c_int FFI types → Trivial (resolve via Named to opaque/primitive)
Generic types:
- Monomorphized generic struct with scalar fields → Trivial
- Monomorphized generic struct with heap fields → NonTrivial
- Unresolved type variable in generic → Unknown (conservative, not error)
ARC pipeline integration:
-
Option<int>,(int, float, bool),Result<int, Ordering>generate ZEROori_rc_inc/ori_rc_deccalls in LLVM IR - No
_ori_drop$functions emitted for trivial compound types -
compute_var_reprs()returnsValueRepr::Scalarfor all trivially-classified types -
compute_drop_info()returnsNone(notSome(DropKind::Trivial)) for trivially-classified types
Consistency & safety:
- Consistency test:
ArcClassifierandReprPlanagree on every type in the Pool - Consistency test: for a Pool with 50+ diverse types, no classification disagreement between old and new code paths
Test suites:
- Unit tests in
compiler/ori_types/src/triviality/tests.rs— all tag variants covered - Integration tests in
compiler/ori_arc/src/classify/tests.rs— ArcClassifier delegation verified - Integration tests in
compiler/ori_llvm/tests/aot/— LLVM IR verified for trivial compound types - Ori spec tests in
tests/spec/— at least one.orifile exercising trivial compound types end-to-end -
./test-all.shgreen -
./llvm-test.shgreen -
./diagnostics/valgrind-aot.shclean (no leaks introduced by elision) -
./clippy-all.shgreen
Files created or modified:
- Created:
compiler/ori_types/src/triviality/mod.rs(new module — algorithm +#[cfg(test)] mod tests;) - Created:
compiler/ori_types/src/triviality/tests.rs(new — unit tests, sibling convention) - Modified:
compiler/ori_types/src/lib.rs(addpub mod triviality;) - Modified:
compiler/ori_arc/src/classify/mod.rs(delegate toori_types::classify_triviality) - Modified:
compiler/ori_arc/src/classify/tests.rs(add delegation consistency tests) - Modified:
compiler/ori_arc/src/ir/repr.rs(compute_var_reprsuses unified classification) - Modified:
compiler/ori_arc/src/drop/mod.rs(compute_drop_inforeturns None for trivial) - Modified:
compiler/ori_arc/src/rc_insert/mod.rs(skip RC for trivial types) — only if §01 ReprPlan is available; otherwise ArcClassifier delegation alone achieves RC elision - Modified:
compiler/ori_llvm/src/codegen/type_info/store.rs(delegateis_trivialto ReprPlan — deferred until §01 complete) - Modified:
compiler/ori_llvm/src/codegen/arc_emitter/element_fn_gen.rs(null drop fn for trivial) - Modified:
compiler/ori_llvm/src/codegen/arc_emitter/drop_gen.rs(skip generation for trivial)
Exit Criteria: ori build on a program using Option<int>, (int, float), and struct Point { x: int, y: int } produces LLVM IR with zero ori_rc_* calls for these types, verified by grep -c "ori_rc" output.ll returning 0 for trivial-only programs.