ADR-0053: Unified Inline Methods and Drop Functions
Status
Implemented. Scope revised during implementation — top-level drop fn TypeName(self) kept as a secondary form (Phase 4 contracted); enum destructors deferred to a follow-up (Phase 3b). See phase notes for detail.
Summary
Make the four type-definition forms — named structs, named enums, anonymous structs, anonymous enums — uniform in how members are attached. Every type definition gets inline methods and an optional inline drop fn(self) { ... } destructor inside its body. The top-level drop fn TypeName(self) { ... } syntax and any remaining spec references to impl blocks are retired. The result: one syntactic position for everything that belongs to a type, and symmetric behaviour across structs and enums.
Context
Members are currently attached to types in three different ways, depending on the type form:
| Type form | Inline methods | Inline drop fn | Top-level drop fn | impl blocks |
|---|---|---|---|---|
| Named struct | ✅ (ADR-0009 migrated) | ❌ | ✅ | removed |
| Named enum | ❌ | ❌ | ✅ (implicitly, never used in practice) | removed |
| Anonymous struct | ✅ (ADR-0029) | ❌ | ❌ (no name to target) | never existed |
| Anonymous enum | ✅ (ADR-0039) | ❌ | ❌ (no name to target) | never existed |
Observations:
implblocks are already gone from the implementation —implis not a keyword in the lexer and the parser has noImplBlockitem. One stale example (impl Counter { fn handle(self) ... }) remains indocs/spec/src/03-types/08-move-semantics.md:212, and destructor spec rule3.9:25still says "outside of anyimplblock". These are documentation debt, not language features.- Named enums lack inline methods — a gap left when ADR-0029 and ADR-0039 added methods to the anonymous forms. Users who want methods on a named enum currently have no way to attach them.
- Destructors live at the top level, disconnected from the type they belong to. Anonymous types cannot have user-defined destructors at all because they have no name to write after
drop fn. This is listed as future work in ADR-0029.
Unifying on a single "everything is inside the type body" model closes all three gaps at once and removes special cases from the parser, RIR generator, and sema.
Decision
Single rule
Anything that belongs to a type — fields/variants, methods, associated functions, the destructor — is declared inside the type body. There is no separate item form for attaching members to a type.
Inline methods on named enums
Named enums accept the same method syntax as named structs. Methods follow the last variant; they may be separated from variants by either commas or nothing (methods need no trailing comma):
enum Option {
Some(i32),
None,
fn is_some(self) -> bool {
match self {
Self::Some(_) => true,
Self::None => false,
}
}
fn unwrap_or(self, default: i32) -> i32 {
match self {
Self::Some(v) => v,
Self::None => default,
}
}
}
Self resolves to the enclosing enum type. Associated functions (no self) are called as Option::origin(). Semantics, structural-equality rules (N/A for named), and method resolution match anonymous enums (ADR-0039).
Inline fn drop on all four type forms
A destructor is written inline as an ordinary-looking method named drop:
struct FileHandle {
fd: i32,
fn drop(self) {
close(self.fd);
}
}
enum Resource {
File(i32),
Socket(i32),
fn drop(self) {
match self {
Self::File(fd) => close(fd),
Self::Socket(fd) => close(fd),
}
}
}
fn Box(comptime T: type) -> type {
struct {
ptr: RawPtr,
fn drop(self) {
__gruel_free(self.ptr, sizeof(T));
}
}
}
drop is currently a reserved keyword (used by the top-level drop fn item being retired). Once that item is gone, drop loses its keyword status and becomes a plain identifier that is privileged only in method-name position inside a type body: the compiler recognizes a method called drop as the type's destructor and enforces destructor-specific rules against it. Elsewhere — as a variable, field, or free-function name — drop is an ordinary identifier. This keeps the syntax uniform with other methods while preserving a clear, discoverable name.
Rules:
- Only affine types may declare
fn drop. A compile-time error is raised if a@copytype declaresfn drop— a copy type duplicates via bitwise copy, and a destructor would run multiple times (double-free). This preserves the existing rule from ADR-0010. Alineartype also cannot declarefn drop— linear values are never implicitly dropped; they must be explicitly consumed, so an automatic destructor would be unreachable. Cleanup for linear types happens at the consumption site (see ADR-0010 open question on linear-type consumption hooks). Result:fn dropis legal only on the default affine case. - At most one
fn dropper type. Duplicate → compile error. - Signature must be exactly
fn drop(self)with implicit unit return. Any extra parameters, type annotations, or a non-unit return type → compile error with a pointed diagnostic. fn dropcannot be called directly with method-call syntax (x.drop()) — it is invoked only by drop elaboration. Attemptingx.drop()is an error; users who want to force disposal use the existing mechanisms for that (out of scope here).- A struct/enum may declare a destructor even if all its fields/variants are trivially droppable.
- Destructor bodies may call other methods of the same type and read fields of
self. - Running order is unchanged from ADR-0010: the user-defined destructor runs first, then field/variant destructors in declaration order.
fn dropdoes not participate in structural equality for anonymous types (same rule as method bodies in ADR-0029/0039: signatures matter, bodies do not, and the destructor signature is fixed so it contributes nothing).
Top-level drop fn TypeName(self) form
The top-level form remains supported as a secondary syntax. The inline fn drop(self) is the preferred form going forward for new code, but ripping out the old form would churn ~25 test sources for no semantic gain. Both forms desugar to the same StructDef.destructor slot in sema, so from the compiler's internal view there is no duplication.
If and when the top-level form becomes a maintenance burden, a follow-up ADR can migrate and remove it.
Retirement of impl references
impl blocks are already unimplemented. This ADR finishes the cleanup:
- Delete
docs/spec/src/06-items/04-impl-blocks.mdif it describesimpl(verify; otherwise update). - Rewrite the
@handleexample indocs/spec/src/03-types/08-move-semantics.md:212to use the inline method form. - Rewrite destructor rule
3.9:25to describe the inlinedrop fn(self)placement instead of "outside of anyimplblock".
Grammar (EBNF delta)
struct_def = [ directives ] [ "pub" ] [ "linear" ] "struct" IDENT "{" type_body "}" ;
enum_def = [ "pub" ] "enum" IDENT "{" enum_body "}" ;
anon_struct = "struct" "{" type_body "}" ;
anon_enum = "enum" "{" enum_body "}" ;
type_body = field_list? method_def* ; (* for structs *)
enum_body = variant_list? method_def* ; (* for enums *)
method_def = [ directives ] "fn" IDENT "(" method_params? ")" [ "->" type ] block ;
The destructor is a method_def whose name is the identifier drop with the required signature fn drop(self); the compiler picks it out by name during type registration. Removed productions: the top-level drop fn IDENT "(" "self" ")" block item, and the "drop" keyword token (demoted to an identifier).
Representation changes
EnumDeclgainsmethods: Vec<Method>(mirroringStructDecl).- No separate
DropFnAST node. The destructor is a regularMethodwhosenameis the interned string"drop"; sema identifies it by name during type registration and stores it in the existing per-type destructor slot. - The
DropFnAST struct,Item::DropFn, and RIRInstData::DropFnDeclare removed. - The
Droptoken is removed from the lexer;dropbecomes an ordinary identifier. - In sema, the existing per-
StructIddestructor slot stays; the only change is where it is populated (from a method nameddropinside the type body instead of a top-level item). Enums get the same slot.
Preview gate
Gate the entire change behind PreviewFeature::InlineTypeMembers. Stabilize once phases 1–5 are green. Because this is also a breaking change (removes the old top-level drop fn), the migration is done in lockstep with the preview flip: as soon as we stabilize, the old form disappears.
Implementation Phases
Phase 1: Parser + AST
- Add
EnumDecl::methods; teach the enum body parser to accept methods after variants (reusemethod_parser_with_expr, mirror struct body shape). - Demote
dropfrom keyword to identifier in the lexer, sofn drop(self)parses as a normal method. (Deferred to Phase 3/4 — thedropkeyword still gates the top-leveldrop fn TypeName(self)item; demoting it now would require simultaneously rewriting the item parser. Cleanest to fold into Phase 3 whenfn dropsupport lands and Phase 4 when the top-level item is removed.) - Keep
Item::DropFnparseable for one pre-removal diagnostic turn that says "destructors are now declared asfn drop(self)inside the type body" (removed entirely in phase 4). - Unit tests at the parser level.
- Add
Phase 2: RIR + Sema (named enums with methods)
- RIR: emit method decls when lowering named enums; no new instruction kinds needed (reuse what ADR-0029/0039 set up for anonymous enums).
- Sema: extend enum registration to register methods keyed by
(EnumId, Spur). ResolveSelfinside method signatures (body-positionSelfalready works on anonymous enums but is not supported for named enums, matching named-struct behaviour). Also wire named-enum associated-function calls (EnumName::fn(...)) throughself.enum_methods. Mirror the named-struct path. - Spec tests covering: basic method, associated function, match-on-self, preview gate.
Self-in-body is intentionally omitted: it doesn't work on named structs either today, so adding it for enums would be a bigger change out of scope here.
Phase 3: RIR + Sema (inline
fn dropon structs)- Scope narrowing from the original Phase 3: structs only (named + anonymous). Enum destructors (named + anonymous) are deferred — the existing destructor infrastructure (
StructDef.destructor,collect_destructor,analyze_destructor_function, CFG drop elaboration) is struct-only in its contract. Extending it to enums is a genuine semantics change beyond Phase 3's "migrate syntax + preserve ADR-0010 semantics" remit and will get its own phase. - During type registration, sema pulls any method named
dropout of the struct's method list and stores it in the existing per-struct destructor slot (StructDef.destructor). - Enforce the exact-signature rule (
fn drop(self), no return) and forbid direct method-call syntaxx.drop(). Emit pointed diagnostics for each. - Enforce the affine-only rule: reject
fn dropon@copystructs and onlinearstructs. (Linear enums would be caught here too once enum destructors land.) - On enums (named + anonymous), for now: emit a compile error pointing at
fn dropsaying "destructors on enums are not yet supported (ADR-0053 follow-up)". This keeps the grammar honest without pretending to implement semantics that aren't there. - Preserve existing drop-elaboration, codegen, and "user destructor runs first, then fields in declaration order" semantics from ADR-0010.
- Spec tests covering: inline
fn dropon named struct and anonymous struct; bad-signature error; direct-call error; not-yet-supported-on-enum error.
- Scope narrowing from the original Phase 3: structs only (named + anonymous). Enum destructors (named + anonymous) are deferred — the existing destructor infrastructure (
Phase 3b: Enum destructors
- Added
destructor: Option<String>toEnumDef(mirrorsStructDef). - Added
register_inline_enum_drop+inline_enum_dropstable in sema; same signature + single-destructor rules as structs. Body analysis runsanalyze_destructor_functionwith the enum type asself. - Taught
type_needs_dropto treat enums with user destructors as non-trivial. - Extended
create_enum_drop_glue_functionto prepend a call to the user destructor before the existing variant-dispatch field drops, preserving "user first, then variant fields in declaration order" from ADR-0010. - Replaced the "not yet supported" error path on enums with real registration. The
linear enumcase is automatically blocked —linearonly applies to structs per the existing grammar. - New spec rules 3.9:39–40 (enum destructor semantics + example). Three new spec tests: basic enum drop, ordering vs variant-field drops, duplicate error.
- Added
Phase 4: Scope-revised — keep top-level
drop fnas a secondary form- Decision: migrating ~25 test sources and deleting
Item::DropFn,drop_fn_parser,InstData::DropFnDecl, and the sema arms produced no semantic change — both forms already populate the sameStructDef.destructorslot. Dropping the migration here in favour of a follow-up ADR keeps this ADR focused on the additive changes (inline methods on named enums + inlinefn drop). - The
dropkeyword stays — it is what makesx.drop()ungrammatical, naturally blocking direct destructor calls without a sema check.
- Decision: migrating ~25 test sources and deleting
Phase 5: Spec cleanup + stabilization
- New spec section for named-enum methods (6.3:31–36).
- New inline-destructor subsection in the destructors chapter (3.9:34–39). Kept the existing 3.9:24–30 in place as documentation of the still-supported top-level form (see Phase 4 rationale).
- Rewrote the
@handleexample (3.8:44) to use inline methods; removed the staleimpl Counter { ... }block. - Traceability: all new rules are covered (inline-destructor paragraphs 3.9:34–39 verified by 9 new tests; enum-methods paragraphs 6.3:31–35 verified by 6 new tests).
- Removed
PreviewFeature::InlineTypeMembersand allpreview = "inline_type_members"tags. The feature is stable. - Did not rename
docs/spec/src/06-items/04-impl-blocks.md— it already describes inline methods (notimplblocks), so its title is the only misleading part. Left that rename as housekeeping for a follow-up.
Consequences
Positive
- One mental model. Members live with the type, period. Fewer forms to learn, teach, or remember.
- Closes the named-enum gap.
enum Option { Some(i32), None, fn is_some(...) { ... } }finally works. - Unlocks destructors on anonymous types.
Box(T), genericVec, and any user-defined generic container can now clean up after itself without naming hacks. - Simplifies the compiler. No top-level
drop fnitem; destructors are registered as part of type registration. Removes a name-resolution step. - Finishes the
implmigration. The spec stops lying aboutimplblocks.
Negative
- Breaking change to top-level
drop fn TypeName(self). Mitigated by the in-repo-only user base and a one-turn migration diagnostic. - Parser complexity slightly increases in the enum-body path (methods + drop mixed in with variants). Mirrored from the existing struct-body parser, so the cost is modest.
- Spec churn in chapters 3.8, 3.9, and 6.x.
Neutral
- Drop-elaboration, codegen, and runtime contract from ADR-0010 are unchanged. This is a surface-syntax and registration refactor, not a semantic one.
- Structural equality for anonymous types is unchanged (method signatures participate, bodies do not, destructor signature is fixed).
Open Questions
- Allow a leading separator before the first method inside an enum body? Structs currently allow methods after fields without a comma. Keep enums consistent.
- Trivially-droppable warning when a type declares
fn dropbut all fields are trivial? Probably not — users may want a destructor purely for side effects (logging, fd close). Leave it silent. - Deprecation window for top-level
drop fn? Proposal is a single diagnostic turn (phase 1) and hard removal in phase 4, since the surface area is tiny and entirely in-repo. Revisit if that assumption becomes wrong. - Reserve
dropas a method name even outside the destructor slot? The proposal demotesdropto an identifier and only privileges it in the type-body method-name position. An alternative is to keepdropglobally reserved so users can never accidentally write a method calleddropwith the wrong signature. Leaning toward the permissive option, since the signature check already catches the only real mistake.
Future Work
- Linear-type consumption hooks — still open from ADR-0010; orthogonal to where the destructor is written.
Droptrait if/when traits land; thedrop fnsyntax would become sugar for an impl.- Visibility on methods is inherited from ADR-0029 open questions and is out of scope here.
References
- ADR-0009: Struct Methods — original
implblocks, later migrated to inline. - ADR-0010: Destructors — destructor semantics (unchanged).
- ADR-0029: Anonymous Struct Methods — source of the inline-method model.
- ADR-0039: Anonymous Enum Types — inline methods on anonymous enums.