ADR-0062: Reference Types Replacing Borrow Modes
Status
Implemented (new surface form stable; old borrow/inout keywords retained as legacy aliases — see Phase 8 note)
Summary
Replace ADR-0013's parameter passing modes (borrow x: T, inout x: T) with parameter types Ref(T) and MutRef(T), constructed at call sites with Rust-style &x and &mut x expressions. Refs remain scope-bound — they cannot be stored in fields, returned, or escape — so this is a syntactic and structural unification, not the introduction of lifetimes. The companion ADR-0061 introduces the BuiltinTypeConstructor infrastructure this ADR reuses for Ref/MutRef.
Context
ADR-0013 chose to model borrows as parameter modes ("Borrowing is a calling convention, not a type constructor"). That choice keeps the type system simple but creates two pieces of friction:
- Surface inconsistency. The user writes
Ptr(T)(after ADR-0061),Vec(T),Option(T)— butborrow x: Tfor borrows. Three different syntactic forms for "a kind of T". - Dead end for stored references. If Gruel ever wants
Ref(T)in struct fields or return positions, borrows-as-modes can't extend there. Borrows-as-types, even when scope-bound today, is the on-ramp.
The semantic content of ADR-0013 (no mutation, no move-out, no escape, exclusivity between mutable and immutable access) is sound. This ADR keeps the rules and changes how they're expressed.
Decision
New types: Ref(T) and MutRef(T)
Parameter modes borrow and inout become parameter types via the BuiltinTypeConstructor registry (ADR-0061). At call sites, &expr constructs a Ref(T) and &mut expr constructs a MutRef(T).
// Before
fn print_summary(borrow data: BigData) -> i32 {
data.field1 + data.field2
}
fn append_byte(inout buf: Buf, b: u8) { ... }
fn main() -> i32 {
let d = BigData { field1: 10, field2: 32 };
let r = print_summary(borrow d);
let mut buf = Buf::new();
append_byte(inout buf, b'!');
r + d.field1
}
// After
fn print_summary(data: Ref(BigData)) -> i32 {
data.field1 + data.field2
}
fn append_byte(buf: MutRef(Buf), b: u8) { ... }
fn main() -> i32 {
let d = BigData { field1: 10, field2: 32 };
let r = print_summary(&d);
let mut buf = Buf::new();
append_byte(&mut buf, b'!');
r + d.field1
}
Semantics — unchanged from ADR-0013
- A
Ref(T)cannot be mutated through, moved out of, or escape its scope. - A
MutRef(T)is exclusive — at most one liveMutRefto a place at a time, and no concurrentRefs. - Refs cannot be stored in struct fields, returned from functions, or captured by closures that outlive the function.
- Field projection works: from
Ref(Pair)you can read.a. - Method receivers:
fn read(self: Ref(Self))andfn mutate(self: MutRef(Self)). Sugar&self/&mut selfis also accepted.
Construction syntax: Rust-style &x / &mut x
&expr and &mut expr are prefix expressions yielding Ref(T) / MutRef(T). Mutability is explicit — &x always produces Ref(T), never MutRef(T), regardless of whether x is bound by let or let mut. This preserves ADR-0013's "explicit at call site" principle.
The & prefix form is contextually unambiguous with bitwise-and: prefix in expression position, infix between two operands.
& and &mut may appear anywhere a value of the target type is expected — argument position, the right side of a let, etc. — but the resulting Ref/MutRef value is still subject to the non-escape rules. So let r = &x; f(r); is permitted, but let r = &x; return r; is rejected because r's type forbids escape.
What this ADR does NOT include
- Lifetimes: refs remain scope-bound; this is not Rust's
&'a T. - Stored references in struct fields or return types — out of scope. A future ADR could relax this once lifetimes (or another mechanism) exist.
- Pinned references (per ADR-0013 future work).
- Auto-borrow: callers must write
&xexplicitly.
Implementation shape
Introduce TypeKind::Ref(TypeId) and TypeKind::MutRef(TypeId). Register Ref and MutRef in the BuiltinTypeConstructor registry from ADR-0061. The borrow checker (today: a sweep over ParamMode::Borrow/Inout) is reformulated to operate on values of Ref/MutRef types — same rules, different trigger. Codegen is unchanged: refs lower to LLVM pointers with the same calling-convention attributes today's borrows use.
Migration
Cut over once feature-complete (matches ADR-0061's approach):
- Implement new syntax behind the
reference_typespreview flag, with oldborrow/inoutmodes still accepted (parallel grammars). - Codemod the test suite, scratch programs, ADR examples.
- Remove old syntax in the same commit that stabilizes the feature.
Implementation Phases
This ADR depends on ADR-0061 Phase 1 (builtin type-constructor registry) being complete.
- Phase 1: Type system — introduce
TypeKind::Ref(TypeId)andTypeKind::MutRef(TypeId)with intern-pool support, mirroring the existing pointer pool pattern. - Phase 2: Parser — accept
Ref(T)/MutRef(T)as type expressions (via the constructor registry from ADR-0061). Accept&exprand&mut expras prefix expressions. Gate behind thereference_typespreview flag. - Phase 3: Borrow checker port — adapt ADR-0013's exclusivity, non-escape, and no-mutate rules to operate on values of
Ref/MutReftypes instead of parameter modes. Bidirectional during migration:borrow x: Tandx: Ref(T)produce identical AIR for the body of the borrow checker. - Phase 4: Method receivers — accept
self: Ref(Self)andself: MutRef(Self), plus the&self/&mut selfsugar. - Phase 5: Codegen — confirm refs lower identically to today's borrows (LLVM pointer with appropriate
noalias/readonlyattrs). Verify with the test suite. - Phase 6: Codemod — convert all
borrow x: T→x: Ref(T),inout x: T→x: MutRef(T),borrow expr→&expr,inout expr→&mut expr. Touches spec tests, UI tests, scratch programs, ADR examples. (Phase 6 lands a representative parallel test demonstrating the new syntax; the full sweep is bundled with phase 8 because two pre-existing limitations make a one-shot codemod infeasible: through-assignment tests likea = bon aMutRef-typed param need a deref operator that doesn't exist yet, and interface-typed params (Sized(i32)) require special ABI handling that doesn't compose withRef(...). Phase 8 deals with these alongside keyword removal so the test suite reaches a single coherent state.) - Phase 7: Spec rewrite — update
docs/spec/src/06-itemsand any borrow mentions in chapters 04/05. Mark ADR-0013's surface-syntax sections as superseded by this ADR. - Phase 8: Remove old syntax and stabilize — drop
ParamMode::Borrow/ParamMode::Inout; remove theborrowandinoutkeywords; remove allrequire_preview()calls forreference_typesand thePreviewFeature::ReferenceTypesenum variant. Update ADR status toimplemented. (Partial. Therequire_preview()call andPreviewFeature::ReferenceTypesvariant are removed — the new surface form (Ref(T)/MutRef(T)/&x/&mut x/&self/&mut self) is stable.ParamMode::Borrow/Inoutand theborrow/inoutkeywords are retained as legacy aliases because the codemod is blocked on two pre-existing language gaps: a deref operator for through-assignment (*r = v), and ABI handling forRef(T)-wrapped interface types. Removing them in this state would force deletion or rewriting of dozens of existing borrow/inout tests with no equivalent expression in the new syntax. A follow-up ADR should land deref + interface-Ref ABI, then drop the legacy aliases as a final cleanup.)
Consequences
Positive
- Uniform surface form for refs alongside
Ptr/MutPtr/Vec/Option. - Two keywords removed (
borrow,inout). - On-ramp for stored references: a future "lifetimes for refs" ADR is a natural extension rather than a re-architecture.
- Cleaner mental model: indirect access is "a kind of type", not "a kind of parameter".
Negative
- Heavy churn: every spec test, UI test, and example with borrows is rewritten.
&becomes a new prefix operator: a notable break from Gruel's keyword preference. Users coming from Rust will recognize it instantly; users coming from elsewhere lose the visualborrow/inoutmarkers.- ADR-0013 partially superseded: it keeps its semantic content but its surface-syntax sections become historical.
- Slightly more characters per use at parameter declarations (
x: Ref(BigData)vsborrow x: BigData).
Neutral
- No semantic change: borrow rules are identical.
- No codegen change: refs lower like borrows.
Resolved Questions
- Construction syntax —
&x/&mut x(Rust-style, explicit mutability). - Naming —
Ref/MutRef(parallelsPtr/MutPtrfrom ADR-0061). - Method receiver sugar — accept both
self: Ref(Self)/self: MutRef(Self)and the short&self/&mut selfforms. - Is
&xa general expression or only in argument position? General expression — works inletbindings too. Non-escape rules still apply via the type. - Migration approach — parallel grammars during phases 1–7, cut over in phase 8.
Open Questions
None.
Future Work
- Lifetimes for stored references — would lift the non-escape rule, allowing
Ref(T)in struct fields and return types. Big design space (lifetime inference, variance, etc.). Strict superset of this ADR. - Pinned references (per ADR-0013 future work).
- Auto-borrow — convenience: callers don't write
&if the parameter type is aRef/MutRef. Loses call-site explicitness; defer.
References
- ADR-0008: Affine Types and Mutable Value Semantics
- ADR-0013: Borrowing Modes
- ADR-0020: Built-in Types as Structs
- ADR-0025: Comptime
- ADR-0061: Generic Pointer Types (companion ADR; provides the
BuiltinTypeConstructorinfrastructure) - Hylo Language Tour: Functions