# Dedup oracle verification + settled design decisions (Task 10)
Ledger IDs: **NL-019**, **NL-116**, **NL-117**. Written per
`docs/superpowers/plans/2026-09-23-wave-A-netlist.md` Task 10. Companion
tests: `crates/vj-sim/tests/netlist_dedup_verification.rs`. Every fixture
below was compiled and probed live against pinned `sv-lang = "=0.1.9"` this
task; nothing here is asserted without having been run.
## NL-019 — `instance_body_has_same_type` parameter-equality verification
**Requirement:** independently verify `Symbol::instance_body_has_same_type`'s
exact equality semantics for (1) interface parameters, (2) virtual-interface
type parameters, and (3) unpacked-array/string parameters, before CODEGEN's
`Template` body sharing relies on it.
### Category 1/3 — interface parameter
**Result: AGREES (same interface type ⇒ same body), with two documented
deviations from the plan's literal fixture.**
- **Deviation A (SV surface syntax).** The plan's literal fixture
(`module leaf #(parameter type IFACE_T = my_if) (IFACE_T.mp p);`) uses a
plain, non-`virtual` interface name as a `parameter type` default value.
Verified live: pinned `sv-lang = "=0.1.9"` rejects this with a real
diagnostic error, `'my_if' is an interface definition but is used like a
type` — LRM-correct, since only a `virtual` interface type is a legal
parameter/variable type; a plain interface can only be bound through an
interface *port*. The test instead uses a generic interface port
(`module leaf(my_if p); ...`), instantiated twice against two different
interface *instances* of the same interface *type* — still exactly "a
module parameterized by which interface it's bound to," just expressed the
LRM-legal way.
- **Result:** `instance_body_has_same_type(u0_body, u1_body) == true` for two
`leaf` instances bound to `if0`/`if1` (two distinct instances of the same
`my_if` interface type) — confirmed live, `has_errors() == false`.
- **Deviation B (dedup exercised without full `elaborate()`).** This wave's
`elaborate()` does not yet model interface ports end-to-end:
`netlist::template::build_members` intentionally leaves
`SymbolKind::InterfacePort` as a no-op contributing no `PortDecl` (a Task 7
scope boundary, documented in that module's own match arm comment, not
reopened by this task). Confirmed live: calling `elaborate()` on this exact
fixture returns `Err(Frontend("port connection \`top.u0.p\` has no matching
PortDecl in its child Template"))`. The companion test therefore verifies
(a) the raw oracle directly, and (b) this crate's own real merge decision
function, `dedup::merge_groups_within_bucket` (Task 2), called directly on
the two real bodies — genuinely exercising "the crate's own dedup pass,"
just not routed through the not-yet-built interface-port connection step.
Full interface-port support (a `PortDecl` variant/connection-resolution
path for `InterfacePort`) is out of this task's scope and remains
documented, not implemented, future work — it is Task 7's/CODEGEN's
concern, not a settled-vs-unsettled question this task's own ledger rows
own.
### Category 2/3 — virtual-interface type parameter
**Result: fully expressible, both directions verified, no hedge (supersedes
the plan's original "to the extent expressible" language, per Deviation #9 in
the plan itself).**
```systemverilog
interface I1; endinterface
interface I2; endinterface
module leaf #(parameter type VIF_T = virtual I1) ();
VIF_T v;
endmodule
module top;
leaf #(.VIF_T(virtual I1)) u0();
leaf #(.VIF_T(virtual I1)) u1();
leaf #(.VIF_T(virtual I2)) u2();
endmodule
```
Confirmed live, `has_errors() == false`:
`instance_body_has_same_type(u0, u1) == true` (same `virtual I1` bound to
`VIF_T`), `instance_body_has_same_type(u0, u2) == false` (`virtual I1` vs
`virtual I2`). Exercised through the FULL `elaborate()` + Task 2/3 dedup
pipeline (no interface-port involved, so Deviation A/B above do not apply):
`u0`/`u1` share one `TemplateId`, `u0`/`u2` do not.
### Category 3/3 — unpacked-array and string parameters
**Result: AGREES on equal values, DISAGREES on differing values, both
through the full `elaborate()` pipeline — no deviation.**
Two independent fixtures (`string TAG` and `bit [3:0] TBL [0:1]`), each
instantiated with two equal-value instances and one differing-value
instance. Confirmed live, both diagnostic-clean: the equal-value pair shares
one `TemplateId`, the differing-value instance gets a distinct one, in both
fixtures. This also exercises `TemplateKey`'s hash bucketing (not merely the
pairwise oracle in isolation): the regression asserted is on the FINAL
`Template` identity/count, so a hash that accidentally collapsed the
differing case would still be caught by the differing-`TemplateId` assertion.
## NL-116 — generate-block/config-rule `TemplateKey` equality
**Settled decision: generate-block/config-rule identity is NOT part of
`TemplateKey` equality.** Two textually-identical instance bodies with
identical resolved parameters, reached via different `generate if`/`generate
case` branches, DO share one `Template` — dedup is purely
structural/parametric, blind to which generate branch produced the instance.
**Rationale:**
1. `Symbol::instance_body_has_same_type` is documented as comparing the
elaborated body's own shape, not its enclosing generate-block provenance.
2. Nothing in this wave's consumer contracts (CODEGEN kernel sharing, STATE
arena layout) depends on generate-branch identity.
3. Collapsing them is strictly the higher-value default: more sharing, still
LRM-correct, since a `generate if`'s only effect on the elaborated body is
which text produced it, not its executable semantics.
**Test fixture:**
```systemverilog
module leaf(input logic a, output logic y); assign y = a; endmodule
module top #(parameter bit SEL = 1);
generate
if (SEL) begin : g_true
leaf u(.a(1'b0), .y());
end else begin : g_false
leaf u(.a(1'b0), .y());
end
endgenerate
endmodule
module tb;
top #(.SEL(1)) t1();
top #(.SEL(0)) t0();
endmodule
```
`t1` (SEL=1) elaborates `g_true.u`; `t0` (SEL=0) elaborates `g_false.u`. Per
the settled decision, `t1.g_true.u` and `t0.g_false.u` must share one
`Template` — asserted directly (`TemplateId` equality) and confirmed green.
**Deviation, discovered by this task (a genuine, minimal, necessary
correction — not a stylistic choice):** `netlist::elaborate`'s instance-
hierarchy walk (`collect_instance_children_into`, `elaborate.rs`) previously
descended into BOTH branches of a `generate if`/`case` unconditionally,
including the untaken one. Verified live: for the fixture above,
`g_false.generate_block_is_uninstantiated() == true` on `t1` (SEL=1) and
`g_false`'s own `u` member has `kind() == SymbolKind::UninstantiatedDef`, not
`Instance` — `sv-lang`'s documented placeholder for "an instance of a
definition that is not actually instantiated in the design... kept around
only so name lookup inside it can still be checked." Descending into it
unconditionally surfaced as a spurious `ElabError::UnsupportedConstruct` on
this ordinary, LRM-legal source — before this fix, NL-116's own required test
construct could not be elaborated at all. Fixed by checking
`Symbol::generate_block_is_uninstantiated()` before descending into a
`GenerateBlock` member and skipping the branch entirely (contributing
nothing) when it is `true` — consistent with NL-002's "preserve the REAL
elaborated hierarchy" guarantee, since an untaken branch was never part of
the elaborated design to begin with. Confirmed to be a real, load-bearing fix
(not a no-op gate): reverting it makes
`generate_if_branch_identity_decision_is_settled_and_tested` fail with
exactly the `UnsupportedConstruct { detail: "UninstantiatedDef" }` error
above.
## NL-117 — `Interconnect` net classification
**Settled decision: an `Interconnect` net is treated conservatively — placed
in its own singleton `Partition` (like a `force`-able net), never folded
into the ordinary strength lattice, and never dropped from analysis
entirely.** Rationale: an interconnect net has no fixed data type until
resolved by connection, so this pass cannot soundly reason about its
value-level driver strength at elaboration time; a singleton partition is the
safe default consistent with this whole pass's fail-closed posture.
**Test fixture:** `module m; interconnect w; endmodule` — a bare
`interconnect` net declaration, nothing driving or reading it. Confirmed
live: `elaborate()` returns `Ok`; `w`'s own `SignalDecl.kind` is
`SignalKind::Net(sv_lang::NetKind::Interconnect)`; `analysis::analyze`
succeeds and places `w` in its own singleton `Partition` (`.nets == [w]`,
`.processes == []`).
Two deviations, both discovered by this task and both genuine, minimal,
necessary corrections (not stylistic choices) — without either, the settled
decision above is not actually achievable, so this task implements them
rather than leaving NL-117 as a documented-but-unmet limitation:
**Deviation A — `NetKind` resolution (`netlist::template`).** The
pre-existing `net_kind_of` helper (Task 5) resolves a net's `NetKind` via the
*enclosing scope's* default net type — a documented, load-bearing limitation
of the pinned `sv-lang = "=0.1.9"` C API, which exposes no accessor from a
`Net`-kind `Symbol` to its own declared `NetType` (verified exhaustively
against every `NetSymbol`-touching C API call site; see `net_kind_of`'s own
doc comment, unchanged by this task). Consequence, verified live: a bare
`interconnect w;` declaration was silently reported as the scope's ordinary
default net kind (`Wire`), never `Interconnect` — precisely the
"misclassifying it" NL-117's acceptance text rules out.
Fixed by `net_kind_of_member`, additive to (not a replacement for)
`net_kind_of`: `interconnect` turns out to be independently, reliably
detectable a different way. LRM: an `interconnect` net has no fixed data type
until resolved by connection. Verified live that pinned `sv-lang` represents
this as a **0-bit placeholder `Type`** on the net's own `value_type()`:
for `module m; interconnect w; wire ordinary; wire [3:0] wide; endmodule`
(`has_errors() == false`), `w.value_type().unwrap().bit_width() == 0` while
`ordinary`'s is `1` and `wide`'s is `4`. Every other LRM net-type keyword
(`wire`/`wand`/`wor`/`tri`/`tri0`/`tri1`/`trireg`/`supply0`/`supply1`/`uwire`)
always carries a concrete ≥1-bit packed data type in valid, diagnostic-clean
SV, so a 0-bit `value_type()` on a `Net`-kind symbol cannot arise from any
case other than `interconnect` — this is a definite signal, not a heuristic
guess. `net_kind_of_member` checks it first and falls back to the existing
(still-limited, unchanged) scope-default heuristic for every other net kind.
Confirmed to be a real, load-bearing fix: reverting it makes the companion
test's `SignalKind` assertion fail with `left: Net(Wire), right:
Net(Interconnect)`.
**Deviation B — isolated-node partitioning (`analysis::graph`).**
`partition`'s node discovery previously seeded its node set only from
`DriverGraph::write_edges`/`read_edges`. A net with zero drivers and zero
reads (this fixture's `w`) was therefore never added as a graph node at all
and had no `Partition` whatsoever — silently vanishing from the analysis
entirely, which is a *more* severe form of "misclassifying it" than any
wrong-but-present classification would be, and directly contradicts the
settled decision above (a singleton partition, not no partition). Fixed by
adding `DriverGraph::all_nodes` — every net/process the graph builder knows
about (from every `Template.signals`/`Template.processes` member, fanned out
per instance), in the same deterministic declaration-then-registration order
`build_driver_graph` already uses elsewhere — and seeding `partition`'s node
discovery from it before falling through to the (now redundant in the common
case, kept as a defensive fallback) edge scan. Confirmed to be a real,
load-bearing fix: reverting it makes the companion test panic with `no entry
found for key` (a `HashMap` lookup miss in `StaticAnalysisResult::
partition_of`, since `w` was never assigned a `RegionId` at all).
## Summary
All three ledger rows are settled with a written decision, all five required
tests pass, and every deviation from the plan's literal fixtures/expected
behavior above was forced by something verified live against the pinned
`sv-lang = "=0.1.9"` API/implementation this task, never a convenience
shortcut. The one genuinely unimplemented surface this task surfaced but does
not implement — full interface-port `PortDecl`/connection-resolution support
— is explicitly out of NL-019's own scope (NL-019 only requires verifying the
dedup *oracle*, not building interface-port connection modeling) and remains
future work for whichever task next touches `netlist::template::build_members`
/ port-connection resolution.