ADR-0075: Handle interface; retire @handle; reject unknown directives
Status
Implemented
Summary
Reframe @handle as a compiler-recognized Handle interface (fn handle(borrow self) -> Self), with conformance via method presence — the same model Drop uses post-ADR-0059. Delete the @handle directive, the is_handle flag, and the validate_handle_structs pass. Tighten the directive surface by making any directive name not in {allow, derive} a compile-time error, replacing today's silent acceptance of unknown directives.
Context
ADR-0059 reframed @copy as @derive(Copy) and ADR-0065 added Clone the same way. The newly-added BUILTIN_INTERFACES registry (post-ADR-0059 cleanup) holds Drop, Copy, and Clone in one place. @handle is the only ownership-related directive that hasn't made the same migration, and an audit of the implementation shows it's the weakest of the three:
- The
is_handleflag set onStructDefis never read anywhere in the compiler. It is dead state. validate_handle_structsonly checks that a method namedhandleexists with the right shape. That is a structural-conformance check spelled with a directive.- The check doesn't even pin the receiver mode. Both
fn handle(self)andfn handle(borrow self)parse and validate, but only the second matches spec rule §3.8:45 ("the original is still valid after the call"). The directive's contract is documented but unenforced.
Separately, the directive surface today is unsound about typos. The parser accepts @<ident> generically; only @allow, @derive, and @handle are checked at sema. Anything else — @xyzzy, @hadnle, @dervie(Copy) — compiles silently. With @copy retired and @handle slated for removal, the recognized set is small enough that closing it is cheap and prevents a real footgun.
The Handle interface needs to exist as a distinct interface (not collapsed into Clone) because it has a different linear-type story: Clone is rejected on linear types (ADR-0065); explicit duplication of a linear handle (forking a transaction, a refcount bump) is the canonical use case for Handle and must remain allowed (today's spec rule §3.8:49).
Decision
Handle becomes a compiler-recognized interface
Add a fourth entry to BUILTIN_INTERFACES:
interface Handle {
fn handle(borrow self) -> Self;
}
Conformance is method-presence, identical to Drop. A type satisfies Handle iff it provides a method matching that exact signature (name handle, receiver borrow self, no other parameters, return type Self). Pinning the receiver as borrow self resolves the spec/implementation inconsistency documented above: receiver-mode mismatch is a conformance miss, not a silent semantic bug.
There is no @derive(Handle). Unlike Copy (where the directive validates field shape and tags the type) and Clone (where the directive synthesizes the body), Handle has no compiler-side work to perform — every body is type-specific and the only check is method shape. Method presence is the right idiom.
Handle is allowed on linear structs. This is the one semantic property that distinguishes it from Clone and the principal reason for keeping it as a separate interface.
@handle directive is removed
- Delete
has_handle_directiveandvalidate_handle_structsingruel-air/src/sema/declarations.rs. - Delete the
is_handle: boolfield onStructDef. All call sites set it but none read it. - Delete
ErrorKind::HandleStructMissingMethodandErrorKind::HandleMethodWrongSignature, plus the matchingErrorCodeconstants. - Update spec rules §3.8:40–49 to describe the
Handleinterface (withborrow selfreceiver) instead of the directive. Old rule IDs are reused with new normative text — these rules are pre-stabilization and have no external citers beyond the spec tests, which migrate as part of this work.
The migration is a hard cut, not preview-gated. ADR-0059's @copy retirement set the precedent: post-cleanup, the directive ceases to exist atomically with the corpus update. @handle has fewer call sites today (~3 spec tests, the spec page, two scratch files) than @copy had at retirement.
Unknown directives become a compile-time error
Add a sema validation pass that runs once over collected directives and rejects any directive name not in the closed set {allow, derive}. The error is new:
- New variant:
ErrorKind::UnknownDirective { name: String, suggestion: Option<String> } - New error code in the
E04xxrange (next free slot) - Display format:
unknown directive `@{name}`{; did you mean `@{suggestion}`?} - Suggestion is computed via Levenshtein distance ≤ 2 against the known set.
Spec-side, this is a new legality rule in §2.5:
A directive whose name is not one of the recognized directives (@allow,@derive) is a compile-time error.
Out of scope
- The receiver-mode bug in the current
@handlevalidator is fixed by removal, not by patching the directive in place. - No interaction with future user-defined directives. If a directive extension mechanism is ever proposed, it will need to thread through this validation point — that's an explicit pin, not a problem this ADR has to solve.
@allowwarning-name validation already exists (an unknown warning name is a separate error per §2.5:8) and is unaffected.
Implementation Phases
Phase 1: Add
Handleto the built-in interface registry. AppendHANDLE_INTERFACEtoBUILTIN_INTERFACESingruel-builtins. Method-presence conformance — no derive variant. Sema picks it up automatically through the existing iteration. Generated built-in types reference rebuilds viamake gen-builtins-docs;Handleappears in the Quick Reference and detail sections alongsideDrop,Copy,Clone. Verify@conforms(T, Handle)returns the expected truth for a hand-written conforming type. Adds the new path; touches nothing existing yet.Phase 2: Migrate the corpus off
@handle. Convert all@handleusers incrates/gruel-spec/cases/,docs/spec/src/,website/content/learn/, andscratch/to definefn handle(borrow self) -> Selfdirectly. Update spec rules §3.8:40–49 to describe theHandleinterface (the rule IDs are reused; surrounding structure preserved). Verify each migrated test still passes against the Phase 1 compiler (both forms coexist at this point).Phase 3: Delete the
@handledirective. Removehas_handle_directive,validate_handle_structs,StructDef::is_handle,ErrorKind::HandleStructMissingMethod,ErrorKind::HandleMethodWrongSignature, and the matchingErrorCodeconstants. Update the call site inprocess_struct_declsto stop settingis_handle. After this,@handlebecomes one more silently-accepted unknown directive (resolved by Phase 4).Phase 4: Reject unknown directives. Add
ErrorKind::UnknownDirectivewith Levenshtein-suggestion logic. Validate every directive collected during decl gathering against{allow, derive}. Special-case@handleand@copywith retirement notes pointing to ADR-0075 and ADR-0059 respectively. Add spec rule in §2.5 documenting the legality rule. Add UI tests covering: unknown directive with no suggestion, with near-match suggestion, with retirement message for@handle/@copy. Add a spec test case that confirms the legality rule.
Consequences
Positive
- One fewer surface-language directive — the recognized set shrinks to two (
@allow,@derive). HandlejoinsDrop/Copy/Clonein theBUILTIN_INTERFACESregistry, with uniform documentation generation and uniform introspection via@conforms(T, ...).- The receiver-mode contract is enforced by the type system instead of being an unenforced spec note.
@xyzzy(and@dervie(Copy),@allwo(...)) become loud errors with suggestions, removing a real footgun.- Dead state (
is_handle) and a dedicated validation pass leave the compiler.
Negative
- Method-presence conformance means a type that names a method
handlewith the right signature for an unrelated reason will silently conform. The risk is low (the name + exact shape are specific), but it exists.Dropaccepts the same trade-off. - Source-compat break: any code using
@handlewon't compile. Mitigated by the targeted retirement message in Phase 4 and the small in-tree corpus. HandleandClonehave identical signatures, which can confuse users. The interface descriptions in the generated reference must explain the linear-type difference clearly.
Open Questions
- For Phase 4's near-match suggestions, what distance threshold? Proposed: edit distance ≤ 2 against the known set. Anything larger surfaces too many false positives.
Future Work
- A general directive-extension mechanism (e.g. user-defined attributes that the compiler routes to a hook). Out of scope here; if pursued, it would need to extend the unknown-directive validation to consult the user's registered set.
- Revisit whether
HandleandCloneshould share a default implementation when the conformance sets overlap (i.e. for non-linearClonetypes, an automaticHandleconformance). Worth a follow-up after both interfaces have soaked.
References
- ADR-0058 — Comptime derives (the
@derive(Name)mechanism) - ADR-0059 — Drop and Copy interfaces (the precedent this ADR follows for retirement-without-preview-gating)
- ADR-0065 — Clone and Option (third compiler-recognized interface; source of the linear-type-clone restriction)
- ADR-0008 — Affine types MVS (introduced
@handleoriginally) - Spec §2.5 — Builtins / directives
- Spec §3.8 — Move semantics (current
@handlerules)