vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
# 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.