Skip to main content

Module analysis

Module analysis 

Source
Expand description

ANALYSIS (vj-sim::analysis) — the WOCB static-analysis pass’s hand-off surface (netlist-analysis.md §Key Data Structures — partitioning, CDC edges, DPI/VPI tagging, and the ForceObserved runtime-invalidation hook ARCHITECTURE §4/§9 requires), plus (as of Task 8) the real, sv-lang-Analysis-backed driver graph, WOCB partitioning and clock-domain/CDC-edge inference: crate::analysis:: analyze(). See docs/superpowers/plans/2026-09-23-wave-A-netlist.md Task 8.

Structs§

AnalysisOptions
Task 8 (and harness Task 18 / VF-116, whose duplicate definition was folded into this one at the harness merge): the knobs analyze() takes. Default is the fail-closed configuration ARCHITECTURE §6.5 requires verbatim: a CDC classification below cdc_confidence_floor must degrade to a partition merge rather than stay a directed edge, and an unclassifiable DPI/VPI call defaults to the most conservative tag (Barrier), never Pure.
CallSiteId
CdcEdge
ClockDomain
Task 8: a real, physical clock signal — edge-triggered — and the dense ClockDomainId every process whose StaticSensitivity::Explicit resolves to the SAME clock_signal (by InstantiatedSignalId equality, NL-076) shares.
ClockDomainId
DpiTagStats
One aggregate metric per test corpus: the regression-monitored Pure/DeferredSafe vs Barrier(/SpeculationUnsafe) ratio NL-103 requires. Not part of super::StaticAnalysisView (that trait is per-call-site dpi_tag); this is a harness-facing aggregation a corpus runner (out of this wave — the harness track’s own job, per the plan) calls once per test corpus.
DriverGraph
The {net,process} driver-dependency graph (NL-063): write_edges() is (process) -write-> (net) for every real (non-input-port-boundary) DriverInfo; read_edges() is (net) -read-> (process) for every AnalyzedProcedure::read_set() entry whose net has at least one internal (non-input-port) driver somewhere in this graph — a pure top-level input has no driver of its own to synchronize with, so a read of it is never a partition-membership-relevant dependency (see build_driver_graph’s own doc comment for the fixture that pins this exact filter).
Partition
One WOCB partition: a set of {net,process} graph nodes that must be evaluated together (netlist-analysis.md’s Partition). clock_domain is None for a purely combinational/mixed region.
RegionId
StaticAnalysisResult
Task 8: the real analyze() hand-off value — partitions/ clock_domains/cdc_edges populated by the driver graph + WOCB partitioning + clock-domain/CDC-edge inference this task implements; on_force_observed stays on its safe placeholder body (out of this wave). Frontend-only (CONTRACTS D4): partitions: Vec<Partition> names a type that only exists under this crate’s frontend feature (Partition lives in the sv_lang::Analysis-consuming graph module).

Enums§

AnalysisError
Task 8: every analyze() failure. Kept even though this task’s own implementation never constructs one (every fallible step it performs — driver/read-set resolution — degrades to a documented skip rather than an error, per analysis::graph’s own doc comments): the type is part of the plan’s binding public signature (analyze() -> Result<.., AnalysisError>) and a future wave’s DPI/VPI classification pass (Task 9) or dataflow-dependent Implicit-sensitivity derivation is the natural place an Err first becomes reachable.
CdcClassification
A CROSS-region edge, never a merge (netlist-analysis.md §C / ARCHITECTURE §4 item 4; ledger NL-059, NL-069, NL-077, NL-078, NL-084, NL-085): a net multiply driven across provisional partitions forces a union instead — only a directed read dependency on an already-resolved value from another partition is eligible to stay a CdcEdge.
CdcConfidence
Fail-closed by default: Unproven below cdc_confidence_floor degrades to a partition merge, never stays an edge (netlist-analysis.md §C).
DpiVpiTag
Fail-closed DPI/VPI call tagging (netlist-analysis.md Decision 6/7; ledger CG-105, ST-149, NL-092, NL-095, NL-100, DT-045): unprovable always defaults to Barrier, never Pure.
ForceObservedAction
The on_force_observed runtime-checked escape hatch (ARCHITECTURE §4/§9, netlist-analysis.md §G / DECISION_RECORD_CONFLICTS #1; ledger ST-121, ST-122, NL-106, NL-108, NL-110, DT-047, RT-159): force/release/vpi_put_value on a region-crossing net must call this so the engine can detect and fall back from an invalidated independence assumption instead of trusting a stale one. Not a static proof — a runtime-checked escape hatch, because any net is in general VPI-addressable regardless of what elaboration could prove.

Traits§

StaticAnalysisView
The analyze() hand-off surface (netlist-analysis.md’s impl StaticAnalysisResult; ledger NL-005, NL-006, NL-009, NL-053, NL-085, NL-106), reframed as a trait for the same reason as crate::netlist::NetlistView.

Functions§

analyze
Task 8 (NL-007, NL-063, NL-076, NL-082): the real static-analysis pass — backfills SignalDecl.driver_set onto netlist (graph:: populate_driver_kinds, NL-063’s classification clause — the one mutation this whole pipeline performs on an already-elaborated Netlist, since sv_lang::Analysis does not exist until after elaborate() returns), builds the {net,process} driver graph (graph::build_driver_graph), partitions it (graph::partition), classifies every un-unioned cross-domain read edge as a CdcEdge (clock::classify_cdc_edges), and (Task 9, NL-103) fail-closed- classifies every DPI/VPI call site (dpi::collect_call_tags).
build_driver_graph
Second load-bearing finding, TDD’d against real sv-lang output (verified this task): sv_lang::Analysis (slang::analysis:: AnalysisManager) tracks driver/procedure data per CANONICAL instance only — confirmed empirically against a fixture with two instances (i0, i1) of one shared module: slang_analysis.procedures(i0_body) .count() == 3 (i0 happened to be canonical) while slang_analysis.procedures(i1_body).count() == 0, even though Task 2’s own DisableInstanceCaching finding already established i0’s and i1’s own AST bodies are genuinely distinct Symbols (never pointer- shared). This is AnalysisManager.h’s own documented behavior (“driver state tracked PER CANONICAL INSTANCE”) — a SEPARATE, analysis-layer-only canonicalization from DisableInstanceCaching‘s elaboration-layer pointer-identity guarantee. Consequence: Analysis::drivers/ Analysis::procedures can only ever be queried against ONE representative instance per deduped Template — never a genuinely per-instance call — so this function computes the write/read STRUCTURAL PATTERN (as (SymbolKey, SymbolKey) pairs) once per Template, against its own canonical instance (found by [representative_instance_of], which — unlike populate_driver_kinds’ “any instance is correct, Decision 4 guarantees identical shape” reasoning — must specifically locate the one instance Analysis actually populated data for, not merely the lowest InstanceId), then FANS that pattern OUT onto every instance sharing the Template, using each instance’s own InstanceId. Clock-domain seeding stays genuinely per-instance regardless (NL-076: it never calls Analysis at all — it reads Template.processes, already derived directly from raw sv_lang in elaborate(), so i0’s and i1’s own distinct InstantiatedSignalIds for their clk port already differ correctly with no canonicalization pitfall).
dpi_tag_stats
NL-103’s own ACC, made concrete: the regression-monitored ratio metric over every call site analyze() tagged.
infer_procedural_clock
The procedure’s effective inferred clock: LRM 16.14.6 (a)/(b)/(c) when those three conditions are jointly satisfied, otherwise LRM 16.15’s default-clocking fallback when a default clocking block applies to proc’s scope, otherwise Ok(None). See the module doc’s “Correction” section for why these two sources cannot be told apart from this return value, and why this module cannot change that given sv-lang 0.1.9’s exposed surface.
infer_structural_clock
LRM 16.14.6 (a)/(b)/(c) for a procedure that slang’s analysis does not list: one inside a procedural checker instance’s body (17.3). slang analyzes such a body as a side effect of analyzing the enclosing procedure and files no scope result for it, so infer_procedural_clock has no AnalyzedProcedure to ask (and an AnalyzedAssertion::procedure handle into that scope is not safe to follow: slang keeps the procedures in a std::vector and the assertions point into it before it has finished growing). This is therefore the one place the three conditions are decided here rather than by slang, mirroring DFAResults::inferClock for the shapes a checker’s procedure can take:
partition
The WOCB partitioning pass (Decision-record union-find): every write edge always unions process with net (LRM multi-driver atomicity falls out for free — two processes that both write one net both union with it, hence transitively with each other, with no special-casing). A read edge unions net with reader UNLESS both sides already carry a resolved, DIFFERING clock domain — a genuine cross-domain register-to-register read, which [crate::analysis::clock::classify_cdc_edges] turns into a CdcEdge instead (never a merge, per NL-082). Two same-domain clocked nodes (NL-076’s fixture) also do NOT union on a read alone — finer partition granularity, still correct (each register independently reads old values within one clock domain) and not required to merge by anything this pass’s tests assert. Iterated to a fixed point (a merge can turn a group’s domain from None to Some, unblocking a previously-skipped union later in the same or a later pass), bounded by max_passes.