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).