Skip to main content

Module unit_signature

Module unit_signature 

Source
Expand description

P8.1 (#1512): UnitId and UnitSignature — the load-bearing types this whole phase’s firewall (R3.14) is stated in terms of. UnitSignature projects a UnitTable into design notes §15’s four required-annotation categories — cross-context types, fn signatures (free functions and methods), handler signatures plus storage, capability sets — with every body/body-adjacent field excluded ([DECISION B], design/tracks/incrementality.md §3.4/Q4) rather than merely ignored by the comparison: body/requires/ensures never reach FnSignature, body never reaches HandlerSignature, init/annotations never reach StoreFieldSignature — the type system, not a comparison function’s own discipline, makes “no body reachable from UnitSignature” a fact.

Stability is proved by comparing UnitSignature::canonical, not the raw struct: every included fragment still carries its own Span (Ident, Param, every TypeRef variant, TypeDecl’s own trivia/ documentation), and editing a body shifts every later declaration’s own spans in the same file (the byte-offset cascade PR #1509’s bot review caught one level up, in UnitSignature’s own design). [DECISION C] extends contract.rs’s canon_type/service_normal_form (ADR 0200) — already a span-free canonical rendering, proven correct by contract_hash.rs’s own no-false-positive fixture — to reach the new signature shapes here, rather than inventing a second erasure scheme.

The R3.14 firewall has two directions, and this module is responsible for both: a body edit must not move UnitSignature::canonical (the exclusions above), and a genuine signature edit MUST move it — a false “unchanged” verdict silently skips recomputing every downstream consumer, the more expensive failure mode for an incrementality firewall. PR #1517’s own bot review caught three real gaps on the second direction before this module first merged: [HandlerKind] was dropped entirely (so renaming an HTTP route or changing on GET to on POST didn’t move the form), methods never reached UnitSignature at all (UnitTable.fns only holds free functions — FnName::Method is filed under UnitTable.methods instead, symbols.rs:596-619), and a capability declaration’s own operation signatures (CapabilityDecl.ops, itself already body-free — “signature only; no body”, ast.rs:564) were never projected, so retyping a capability op left every consumer’s own UnitSignature unchanged even though its compiled contract with that capability’s provider did change. All three are fixed here; see each type’s own doc comment for what closed the gap.

Deliberately still excluded, named explicitly so a future reviewer doesn’t have to re-derive it: ProviderDecl.provider_name (an internal selector used only in tests/config to pick an implementation — never part of a Cap.op(...) call site, so it carries no information a consumer’s own compile depends on) and handler-position annotations (@cache, …, Handler.annotations: Vec<Annotation>) — an AnnotationArg.value is an arbitrary Expr, and canonicalising Expr the way canon_type already does for TypeRef is real, unscoped work this slice does not take on; flagged for whichever future slice needs it, the same “flag for the slice that will actually pin it” discipline this phase has applied throughout. ServiceProtocol::Events’s own pattern/schema_dispatch fields (the structural payload filter and via schema(N) clause) are captured only as “present or absent”, not rendered field-by-field, for the same Expr/pattern-canonicalisation reason — narrower than ideal, but strictly better than the pre-fix state where the whole protocol was invisible.

unit_signature_for’s only caller is tests/unit_signature_stability.rs (P8.2), and that is deliberate: this module is R3.14’s own proof — the firewall stated as a type and checked by a test — not a production path. Phase 8’s two structural consumers-to-be (ProjectGraph, P8.3, and the DefId-keyed Body/TypeOf queries, P8.5) were built beside it and deleted on 2 September 2026 (#1537, ADR in that PR’s pending file): nothing called them and no scheduler existed to. This module stays because R3.15’s trigger (#1523 — keystroke-to-diagnostic latency attributed by level) presupposes a unit level to attribute to; the gated incremental_query_types probe certifies it is present and its stability test exists.

Structs§

CapabilityOpSignature
A body-free mirror of [bynk_syntax::ast::CapabilityOp] — already “signature only; no body” in its own doc comment (ast.rs:564), so nothing here needs excluding beyond documentation/span/trivia. Added by PR #1517’s own bot review (finding #3): a capability declaration’s own ops are the abstract signature every consumer compiles its Cap.op(...) call sites against, not ProviderDecl.ops (which carry a real body: Block each — a provider’s own implementation, correctly excluded, same as any other body).
CapabilitySignature
The capability-set category (design notes §15’s fourth): what a unit exports (table.exported_capabilities, already plain strings), what a unit itself declares (table.capabilities’s own op signatures — added by PR #1517’s own bot review), and what each provider/service declares it needs (given/default_given). Every CapRef collapses to its rendered name (context.capability, or bare capability when local) — its own Span is dropped, matching every other category’s span-erasure discipline.
FnSignature
[DECISION B]: bynk_syntax::ast::FnDecl, body-free. Excludes body, requires, ensures (all body-adjacent — requires/ensures scope over parameters and the result but are not part of design notes §15’s own required-annotation list, per Q4) and documentation. Used for both free functions (UnitSignature::fns) and methods (UnitSignature::methods).
HandlerSignature
[DECISION B]: [Handler], body-free. Excludes body, by_clause (an actor binder, resolved at the call boundary rather than part of the wire-visible shape), annotations (see this module’s own doc comment — deferred, needs Expr canonicalisation) and documentation. given is kept — it’s this handler’s own slice of the capability-set category. kind is kept (see HandlerKindSignature) — dropping it was PR #1517’s own bot review finding #1.
MethodTableSignature
The instance/statics split PR #1517’s bot review found missing entirely: UnitTable.fns (what UnitSignature::fns projects) only ever holds FnName::Free entries — FnName::Method is filed under UnitTable.methods: HashMap<String, MethodTable> instead (symbols.rs:596-619), keyed by the attached type’s name, with instance and static methods in their own sub-maps (mirroring crate::resolver::MethodTable’s own shape exactly, rather than flattening into one map keyed by method name alone — an instance and a static method can share a name for the same type without colliding in MethodTable itself, so flattening here would silently drop one).
ParamSignature
[DECISION B]: [Param], body-free (it already is — kept as its own type only so FnSignature/HandlerSignature don’t reach back into bynk-syntax::ast::Param and risk a future field being added there and silently flowing through unexamined).
StoreFieldSignature
[DECISION B]: [bynk_syntax::ast::StoreField], body-free. Excludes init and annotations (both body-adjacent per Q4 — annotations govern the field’s own storage behaviour, not its externally-relevant shape) and documentation.
UnitId
[DECISION A]: a stable, hashable identity for a unit, reusing the unit-name String every UnitTable/combined_types_for caller already keys on today, rather than introducing a fresh interned-integer scheme — no index_vec crate (or hand-rolled equivalent) exists anywhere in this codebase, confirmed by grep. UnitSignature only needs UnitId to be a stable, hashable, comparable identity for R3.14’s own proof; it does not need to be dense or IndexVec-compatible. P8.3 resolved the fork this left open by adapting ProjectGraph to the string-keyed UnitId (ADR 0415); that graph was deleted by #1537, so if R3.15’s trigger ever fires, ADR 0415’s recorded shape is what a rebuild adapts to — this type stays a string newtype either way.
UnitSignature
P8.1 (#1512): a unit’s stable signature — everything about it that must survive an edit inside a function/handler body untouched (R3.14, the phase’s firewall).

Enums§

HandlerKindSignature
A closed, body-free mirror of [HandlerKind] — every field HandlerKind itself carries (HttpMethod, a route/cron-expression String) is already a plain value with nothing body-adjacent to exclude. PR #1517’s own bot review: dropping this entirely let two handlers with the same params and return type but different HTTP methods or routes canonicalise identically — renaming a route is not a body edit, and R3.14’s own firewall must treat it as a real signature change.
ProtocolSignature
A body-free mirror of [ServiceProtocol] — added by PR #1517’s own bot review (“worth a look”): from http vs. from queue(...) vs. from websocket(...) changes a service’s entire external surface, and nothing about that fact is a body or body-adjacent to a handler. Events’ own pattern/schema_dispatch are collapsed to bool presence (see this module’s own doc comment for why — deferred Expr/pattern canonicalisation) rather than dropped outright.

Functions§

unit_signature_for
Builds a UnitSignature for name from table, reusing combined_types (typically a fresh combined_types_for(name, ..) call, mirrored here as a parameter rather than recomputed so a caller building both alongside each other pays for one traversal, not two).