Protocol method bodies: defaults, final methods, and private hooks
Status: core design agreed; not implemented. This is the implemented
protocol methods proposal: “implemented” means a method with a body, not a
feature that has shipped. The compiler still requires protocol methods to have
pass bodies; current protocols
remain authoritative. Future prioritization belongs to B29.
Start with the agreed rules
and short programs. The feasibility
comparison below and implementation evidence
record the investigation against d39e6ec on 2026-10-20. Alternatives such as
automatic method injection were considered, not adopted.
Assessment: shared bodies with explicitly selected protocol calls are a small-to-medium frontend feature. Convenient calls through a protocol bound are a medium feature. Automatically adding methods to every structurally matching object, especially with private hooks, is a substantially larger language-design change. None inherently requires dynamic dispatch, an object wrapper, a new runtime representation, or a different borrow checker.
Those estimates concern sharing and lookup alone. The subsequently agreed definition-time contract checking adds substantial frontend work; the complete design with that checking and private-hook grants is not a small syntactic change.
This investigates sharing implementations. The earlier library-owned operations assessment addresses preventing callers from bypassing an algorithm; it remains relevant, because ordinary overridable defaults do not provide that guarantee.
The follow-up final-method and visibility design records the agreed core semantics: defaults honor overrides consistently, final methods reject same-named inherent methods at the structural boundary, and private hooks use member-specific grants while preserving module privacy. Three short programs illustrate the rules. The final-name check is a modest addition once provided-method selection exists; hidden external hooks require additional access-control machinery. Some specification work remains, and no compiler implementation is approved here.
Protocol bodies must also be checked against their declared contract at the definition, rather than accepted because a concrete class has extra members. This agreed requirement increases the scope beyond a generic-function lowering: the existing per-instance checker is not sufficient for a complete implementation.
One example, three distinct choices
Hypothetical syntax, not executable Plenty:
protocol DocumentStorage:
def write(self: &mut DocumentStorage, document: &str) -> Result[(), IoError]:
pass
def save(self: &mut DocumentStorage, document: &str) -> Result[(), IoError]:
validate(document)?
self.write(document)?
record_success()
Ok(())
FileStorage and DatabaseStorage would supply write. The application wants
to call the shared save. Treating an AST body consisting solely of pass as
a requirement is straightforward; an intentional no-op implementation would
need an explicit unit-returning body. Comments do not change the AST body.
There are three independent questions:
- Sharing: where may the application call the supplied
save? - Replacement: may a concrete type supply its own
saveinstead? - Access: may the application call
writedirectly?
Implementing the first does not imply either answer to the other two. In particular, the example above deliberately uses ordinary accessible hooks. Cross-module private hooks need a separate access design.
Options and relative cost
All new spellings below are illustrative.
| Design | Example call | Scope and assessment |
|---|---|---|
| Existing bounded function | save(&mut files, document) | Works today. Sharing has no language implementation cost. |
| Protocol-owned operation | DocumentStorage.save(&mut files, document) | Small-to-medium. Explicitly selects the protocol body, lowers to a generic function, and adds no inherent method. |
| Default selected through a bound | storage.save(document) inside S: DocumentStorage code | Medium. Needs protocol-aware method resolution, missing-default handling, override rules, and receiver provenance through specialization. |
| Explicit adoption on a type | An illustrative use DocumentStorage declaration in FileStorage | Medium-to-large. A local place to opt into methods and diagnose conflicts, but introduces a new relationship beyond structural matching. |
| Automatic structural extension | files.save(document) wherever a matching protocol is visible | Large relative to the above. Needs candidate discovery, overlap rules, inference limits, and a decision about whether imports change available methods. |
| Final provided method | Reject a matching object with a same-named inherent method | Modest addition to provided-method selection: check the final-name restriction at conformance and consistently select the protocol body. |
| Private hooks across modules | Access controlled separately from structural matching | Separate substantial feature. Needs explicit authority, cross-module rules, and guarantees that survive every call path. |
These are engineering judgments from source inspection and experiments, not calendar estimates or measurements of a completed implementation. A narrow compiler spike would be small; a supported feature includes diagnostics, modules, generic combinations, tooling, and regression coverage.
The protocol-owned operation is closest to ordinary generic-function sugar:
# Conceptual lowering of DocumentStorage.save:
def __document_storage_save[S: DocumentStorage](storage: &mut S,
document: &str) -> Result[(), IoError]:
validate(document)?
storage.write(document)?
record_success()
Ok(())
Here DocumentStorage checks only its required hooks; implemented operations
do not require same-named inherent methods. The generated name is an internal
detail. Calls must preserve the protocol definition’s module visibility.
This design has deliberately fixed selection: DocumentStorage.save(...)
calls that body even when the object has an inherent save. It is a provided
operation, not a fallback override. It avoids global method searching and
preserves the existing rule that importing a protocol does not change an
object’s inherent methods.
It does not, by itself, give the application the convenient files.save(...)
spelling. A bounded-call design could add that later, but must deliberately
choose whether it denotes the fixed operation or an overridable default.
The important design traps
Defaults are not a validation boundary. If a concrete save replaces the
default, it can omit validation. Explicitly selecting a fixed protocol body
protects that call’s algorithm; it does not prohibit calling a public write
or a separate concrete save. An owning wrapper remains the current-language
way to expose only the intended operation.
Structural compatibility is not implementation selection. Two protocols
may both require write but provide different save algorithms. A concrete
object can match both without selecting either. A qualified call is unambiguous.
For bound-based calls, the bound should identify the protocol; searching every
matching protocol makes imports and unrelated declarations affect lookup.
Inherent overrides need exact rules. If defaults are replaceable, decide
whether a same-named but incompatible method is an error or is ignored. Prefer
an error over silently choosing the default. Also specify calls between defaults:
does save_twice invoke an override of save, or its original protocol body?
Do not let these answers depend accidentally on specialization order.
Bodies must be checked against the contract. Existing generic bodies are
checked per concrete instance and may use methods not listed in their bound.
That behavior is not acceptable for provided protocol bodies in the agreed
design. Abstract checking is required, including through aliases, helpers and
closures; a syntactic scan for self.foo is insufficient. This is additional
frontend work beyond the demonstrated generic-function lowering.
Inference needs real evidence. Required concrete signatures can determine protocol parameters. A supplied body cannot become circular evidence for its own signature. If a protocol parameter occurs only in a provided operation, require explicit arguments or other independent evidence. Avoid searching other protocol defaults to manufacture conformance.
Privacy is lexical today. A library-defined shared body cannot access an external class’s private method. Automatically granting that access because a public protocol happens to match would cross an existing encapsulation boundary without the class author’s consent. If pursued, access grants should identify the protocol/member explicitly and be distinct from declaring conformance. “Private to the class’s module” also differs from “callable only by protocol bodies,” which the original request might require.
Precedents
Python’s structural protocols provide a particularly relevant warning: implicit structural matching does not supply default implementations; explicit subclassing does. Its existing inheritance mechanism selects the shared code. Python typing specification.
Rust permits trait bodies as defaults that implementations may replace. Individual trait items do not have independent visibility qualifiers. Rust Reference.
Swift protocol extensions supply defaults; a conforming type’s implementation of a requirement takes precedence. Constrained extension overlap also requires selection rules. This is useful precedent for both the feature and its semantic cost, not a lookup model Plenty can copy unchanged. Swift language guide source.
Suggested direction
The selected direction uses overridable defaults and @final bodies, selected
through a bound or protocol qualification. Both forms honor overrides for
non-final methods, including calls inside other protocol methods. Objects do
not acquire inherent methods automatically. Matching requires signature and
visibility equality, and cross-module private hooks require a
@protocol.Protocol annotation on the hook method; module privacy otherwise
stays unchanged. Final methods are annotated @final.
The earlier qualified-call alternative that always invokes the original body is not selected. The design page separates agreed rules from the remaining specification work. Definition-time checking against the declared protocol contract is mandatory.
The implementation map and probes show what already works and where the remaining compiler work lies. No new feature is scheduled by this sketch.