ADR 0049 - Wire Phantom Record↔Ports Adapter for Topology Fans
Adds the * topology operator that elaborates to a phantom node from a parametric record↔ports adapter family in Wire's closed alphabet.
On this page
ADR 0049 - Wire Phantom Record↔Ports Adapter for Topology Fans
Status
Proposed - this ADR introduces the * topology operator. It depends on ADR 0047 for the linear
endpoint rule, match determinism, and the precedence slot reserved for *. ADR 0048 supplies the
typical multi-port frontier (make(N, K)) that * consumes. ADR 0052 proposes the finite-product
generalization; this ADR remains the nominal-record first slice until that lands.
Context
ADR 0024 forbids implicit aggregation: authored inputs are cardinality-one, and aggregation belongs in an explicit node. ADR 0047 sharpens this with the linear endpoint rule: each output and each input is consumed at most once during composition.
Together those rules make make(N, K) => sink a static error for N > 1 when several generated
children expose the same (label, contract) and the sink’s input is cardinality one. The correct
authoring shape is an explicit adapter that converts the multi-port frontier into one
aggregate-typed port (gather) or one aggregate-typed port into a multi-port frontier (scatter).
This ADR adds that adapter and the operator that surfaces it.
The v1 adapter shape is nominal record↔ports. ADR 0052 proposes the broader finite-product reading that also covers bounded indexed products.
Decision
Wire adds a * topology operator that elaborates to a phantom record↔ports adapter inserted
between its two operands; ordinary => does all the wiring.
The defining equivalence:
a * b ≡ a => phantom => b
Both => edges are ordinary boundary contractions under ADR 0047. The phantom node has two
boundaries:
- The multi-side, mirroring the multi-port frontier of whichever operand carries one. Each port
is a regular cardinality-one port; ordinary
=>matches them against the multi-side operand’s ports under ADR 0047’s match rule. - The singular side, a single port of nominal record contract.
There is no spoke_i surface vocabulary at the topology layer. Pairing happens by record field and
port label inside the phantom’s CorePure body.
The phantom belongs to a parametric family of structural adapters indexed by nominal record
shape: the closed alphabet contains the family, and * selects the instance whose shape matches the
call site. This is the same parametric move that makes Option[T] part of the language without
admitting user-defined generics.
Record-form discriminator
The phantom’s body and multi-side shape are determined by the singular side’s expected nominal record contract:
- When the singular side’s port contract is a nominal record contract with fields
{label_1: T_1, ..., label_N: T_N}, the phantom synthesizes multi-side portslabel_i: T_iper field. - The body pairs by label:
agg.label_i↔ multi-side portlabel_i. - The multi-side operand’s exposed port set must match the record’s fields exactly: same labels, same contracts. A mismatch is a static error.
- A singular side whose contract is not a nominal record is a static error.
List-style aggregation (a homogeneous N-ary list contract) is not part of this ADR’s v1. ADR 0052
proposes bounded indexed products such as [T; N]; unbounded list-shaped topology remains out of
scope.
No mixed forms
There is no Wire contract that combines a nominal record with a positional list. Authors who want
partially-uniform aggregates compose multiple * calls or write the adapter by hand. The exclusion
of mixed forms is a corollary of the nominal-record discriminator.
Phantom synthesis
The phantom is always generated, including for the degenerate cases:
- Empty fan (multi-side has zero ports): the phantom is generated with empty multi-side and a
singular-side port of empty nominal record contract. The two
=>edges connect normally; the empty multi-side simply contributes no edges. The elaborator emits a lint warning at synthesis: “*over an empty fan generates a degenerate phantom; consider whether the empty case was intended.” Firing at synthesis (not parse) catches non-literal-zero N too. The always-generate stance keeps elaboration uniform and forward-compatible with conditional graph elaboration if that lands. - Singleton fan (multi-side has one port): the phantom is generated. Authors who do not want the
phantom for N=1 use
=>directly.
Port-clash diagnostic
A port clash on the synthesized phantom is a static error: “phantom would have two outputs of the
same (label, contract); rename or use a record contract.” The diagnostic points at the * call site
and at the phantom’s synthesized boundary. Clashes only matter once the phantom is synthesized;
multi-side operand label collisions are handled there, not at the operand site.
Match determinism
Under phantom-insertion, all topology-level wiring is ordinary => boundary contraction. Match
determinism for * therefore reduces to ADR 0047’s rule for =>: every endpoint has zero or one
compatible counterpart; multiple is a static error. No new tiebreak surface is introduced.
Canonical rule
Fan operators expand only to ordinary graph vertices and ordinary valid port edges. * inserts
exactly one phantom vertex and emits two => edges (one to each operand); the phantom’s body
performs the only aggregate-field projection in the entire elaboration, and that projection is
confined to CorePure. There are no hidden adapters, no implicit aggregation, no changes to =>
semantics, and no surface vocabulary for spokes.
Static errors
- Singular side’s contract is not a nominal record.
- Multi-side operand’s exposed port set does not exactly match the nominal record’s fields (label or contract mismatch).
- Phantom synthesis would produce a port clash.
- Both sides flat singleton: “
*requires a multi-element frontier on at least one side; for 1:1 wiring use=>.” - Both sides multi-element: “
*requires exactly one singular side; both operands carry multi-element frontiers.”
Alternatives considered
- Separate
broadcastandgathermacros. Rejected because the fan direction is unambiguously determined by the boundary shape; one operator with shape-driven direction is honest about what is happening, and it composes cleanly with the existing topology operators. Two macros would need their own grammar, identity rules, and diagnostics. <and>as directional fan operators. Considered briefly. Visually appealing (a < (b, c) > dreads as a funnel) but bites on overlays (a < b <> c > dis ill-formed because no operand is a tuple), and<shares tokens with CorePure comparison. Rejected for the simpler*with shape-driven direction.*synthesizes adapters with no visible vertex. Rejected because every Wire vertex is an ordinarynodedeclaration with a CorePure or executor body; hidden adapters would violate the rule that the elaborated program is fully readable as Wire source.- List-form aggregation in v1. Rejected. A bounded homogeneous list adapter would need
[T; N]contracts and positional projection in CorePure before it can type-check. Record-form*is the smaller first slice because it pairs existing labeled ports to nominal record fields by name. ADR 0052 later proposes the finite-product version of that extension. - Unnamed ports for list-form aggregation. Considered and rejected with list-form v1. List-form
*feels like it wants nameless homogeneous multi-side ports, since they are all the same contract. That would introduce a second port flavor and weaken the label discipline. If bounded lists land later, they should still preserve explicit field/index ownership rather than discard authored labels silently.
Consequences
Positive
- Bounded aggregation and scatter become compact and readable. The aggregation hub is an explicit, visible node with a known body.
- The static-topology invariant is preserved: every vertex is still introduced by a source-level construct that the expander resolves before lowering.
*’s phantom is an ordinary pure node that obeys port matching, authority, and runtime invariants without special-case handling.- Match determinism for
*is inherited from ADR 0047; no new rule surface is introduced.
Negative
- One new built-in identifier (
*) and a closed-alphabet adapter family enter the Wire surface. - The closed alphabet now includes a parametric structural-adapter family (record↔ports adapters). This is a real extension of the alphabet; it must be documented as such, and any formal claims about Wire’s alphabet (in the architecture chapter or papers) must reflect that the family is parametric over aggregate shape but otherwise fixed.
Obligations
- Reserve
*as a built-in topology operator under ADR 0047’s reserved precedence slot. - Reject
*with two flat singletons (use=>) and*with two multi-element frontiers (ambiguous fan). - Document the phantom-insertion equivalence in
docs/Reference/Wire/grammar.md. - Update the tree-sitter grammar to recognize
*as a topology operator. - Update the
wire-code-styleskill. - Amend
docs/Architecture/04-graph-and-circuit.mdin the same change set as this ADR’s acceptance to document the parametric record↔ports adapter family as part of Wire’s closed alphabet. - Add expansion tests for: record-form
*, empty-fan*lint warning, port-clash diagnostic at phantom synthesis, record-form rejection on label-set mismatch, rejection of flat-singleton*and double-multi*,make(N, K) => sinkresolution via*. - Prove or document the preservation claim: after
*expansion, the resulting Wire program admits unchanged under all existing graph, port, authority, and runtime invariants.
Lean note: LinearPortGraph.PhantomAdapterWitness models the source-linearity side of this
obligation. LinearPortGraph.PhantomRecordShape pins the generated adapter to one phantom node with
a declared multi/singular boundary, and * is exhibited as that phantom object plus two certified
bulk contractions. Linearity is inherited from the certified source primitives. The executable
Haskell expander, record-discriminator diagnostics, and witness production remain separate
correspondence work.
Resolved during implementation
- Nominal-record contract surface.
*reads nominal shapes from the host contract registry or from source declarations of the formcontract Name { field: Contract; };. Source fields resolve through theusescope visible at the declaration point. This keeps the singular side nominal without adding list-form aggregation or implicit topology copying.
Open questions
- Unbounded aggregate shape. ADR 0052 proposes bounded indexed products for static topology. Runtime-length aggregates remain open and must not infer topology without a separate decision.
Related
- ADR 0024 - Typed Executor Node Interface
- ADR 0028 - Wire Topology Composition and Boundary Labels
- ADR 0045 - Wire Compile-Time Node-Body Kinds
- ADR 0046 - Wire Compile-Time Graph Forms
- ADR 0047 - Wire Frontier Linearity and Topology Operator Precedence
- ADR 0048 - Wire Compile-Time Make for Bounded Node Generation
- ADR 0052 - Wire Bounded Indexed Boundary Products
- Chapter 04 - Graph and Circuit
- Chapter 05 - Wire Language
- Wire Grammar Reference