1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//! Task 9 (NL-103): fail-closed DPI/VPI call tagging (netlist-analysis.md
//! Decision 6/Decision 7/§E) and the Pure/DeferredSafe/Barrier(/
//! SpeculationUnsafe) ratio harness ARCHITECTURE §9's residual-risk item
//! (netlist-analysis.md RISKS #5) demands: "the ratio ... must be measured
//! on representative DPI-heavy testbenches ... to catch cases where nearly
//! everything is marked Barrier, tracked as a regression-monitored metric".
//!
//! **Deviation from the plan's literal "run once per call site found by
//! `ReadSetLattice`'s ... `DfaEventKind::Call` events" wording, for the
//! TOP-LEVEL call-site enumeration only.** A custom `sv_lang::dataflow::
//! Lattice`'s `transfer(&mut self, event: DfaEvent<'_>)` is a trait method
//! with an INPUT lifetime elided at the TRAIT's own definition — `rustc`
//! rejects narrowing it to the implementing type's own lifetime parameter
//! (`error[E0308]: method not compatible with trait`, tried and confirmed
//! empirically this task), so a `Lattice` impl can only ever *use* a
//! `Symbol`/`Expression` from an event for the duration of that one
//! `transfer` call, never store it in `Self` — exactly why `netlist::
//! lattice::ReadSetLattice` (Task 7) stores only the owned, context-free
//! `crate::symid::SymbolId`, never `Symbol<'d>` itself (see that module's own
//! doc comment). Top-level call-SITE enumeration (`collect_call_tags`)
//! therefore uses `sv_lang::Analysis::procedures(scope).call_expressions()`
//! (`slang::analysis::AnalyzedProcedure::getCallExpressions`) instead of a
//! dataflow `Call` event: the SAME `Analysis` object Task 8 already
//! established (`analysis::graph::build_driver_graph`) already
//! pre-computes exactly this list per procedure, with a real,
//! non-ephemeral `Expression<'d>` lifetime — never a second,
//! independently-reasoned traversal, and never the ephemeral-lifetime trap
//! dataflow events carry.
//!
//! **Deviation, documented (§E step 3's recursion is depth-1 only, not
//! depth-`RECURSION_DEPTH_BUDGET`).** §E step 3 wants to recurse into a
//! resolvable callee's OWN nested calls. Two mechanisms were tried and
//! confirmed NOT to give a way to do this against `sv-lang` 0.1.9's public
//! API: (a) a dataflow `Lattice`'s `Call` event cannot yield a `Symbol`
//! that survives past its own `transfer` call (see above) — only an owned
//! `crate::symid::SymbolId`, which `sv-lang` publishes no way to resolve back
//! into a `Symbol`; (b) `Analysis::procedures(scope)` does NOT expose an
//! arbitrary called `Subroutine` as its own independently-queryable
//! `AnalyzedProcedure` unless that subroutine is a direct, itself-analyzed
//! member of `scope` — confirmed empirically this task: for `module m;
//! function pure_add(...); ... endfunction always_ff @(posedge clk)
//! local_sum = pure_add(a,b); endmodule`, `slang_analysis.procedures
//! (m_body)` returns only the `always_ff` block's own `AnalyzedProcedure`,
//! never a second entry for `pure_add` (even though `pure_add` is a direct
//! member of `m`'s scope) — `Analysis` only independently analyzes a
//! subroutine as its own top-level procedure when it is itself a
//! `scope`-level analysis target (e.g. a package function), not when it is
//! merely called from another analyzed procedure. Consequence: this task's
//! `classify_by_recursing_into_body` proves `Pure` for a callee with an
//! EMPTY write-set (outside its own locals) and NO further calls of ANY
//! kind at all (`DfaEventKind::Call` count == 0, via the dataflow pass
//! that already computes the write-set) — a callee that itself makes ANY
//! further call, resolvable or not, conservatively classifies `Barrier`
//! rather than attempting to recurse into it. This is still a SOUND,
//! fail-closed answer (Decision 6's core invariant — never wrongly `Pure`
//! — holds exactly as before: a truly pure callee with no further calls
//! still comes back `Pure`; anything else conservatively comes back
//! `Barrier`, which is always a safe over-approximation), just not the
//! maximally-precise depth-`RECURSION_DEPTH_BUDGET` proof the plan
//! describes. `depth_budget`/`RECURSION_DEPTH_BUDGET` are still threaded
//! and enforced at the top level (§E step 4) so a future wave that adds a
//! `SymbolId -> Symbol` resolution table (this crate's `dedup::
//! DefinitionTable` is a precedent for building exactly that kind of
//! table) can re-enable genuine multi-level recursion on the SAME
//! plumbing without another signature change.
//!
//! `Analysis::procedures(scope)` also reports module-scope `Subroutine`
//! members alongside `ProceduralBlock`/`ContinuousAssign` members; this
//! module never enumerates a `Subroutine`'s OWN top-level call sites
//! (skipped by [`collect_call_tags`]) since its calls are only ever
//! visited as part of classifying whichever call site invokes it (§E step
//! 3) — a `Subroutine`'s internal calls are never independently
//! ratio-tracked, which would double-count them against the calling
//! site's own classification.
use crate;
use crateSymbolUid;
use DpiVpiTag;
/// 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.
/// NL-103's own ACC, made concrete: the regression-monitored ratio metric
/// over every call site `analyze()` tagged.
/// Task 9's own pinned recursion-depth budget (netlist-analysis.md OPEN
/// QUESTIONS #2, left unresolved there — resolved here as this task's own
/// scoped decision, per the plan's Deviation #4): how many further levels
/// of callee-body recursion [`classify_call_site`] is allowed to descend
/// into before defaulting to `Barrier` (§E step 4). See this module's own
/// doc comment for why the current implementation never actually reaches
/// depth > 1 in practice.
const RECURSION_DEPTH_BUDGET: u32 = 8;
/// Enumerates every DPI/VPI call site made directly by a process
/// (`ProceduralBlock`/`ContinuousAssign`) in the design — never a
/// `Subroutine`'s own internal calls, which are visited recursively as
/// part of classifying whichever call site invokes it, never independently
/// ratio-tracked (see this module's own doc comment) — and classifies each
/// (§E). One entry per `Template`, against its canonical representative
/// instance (the SAME restriction `graph::build_driver_graph` documents:
/// `sv_lang::Analysis` only has real procedure data against one
/// representative instance per deduped `Template`), in `Analysis::
/// procedures()`'s own declaration order, never fanned out per instance —
/// `CallSiteId` values are therefore per-`Template`, not per-
/// `InstantiatedSignalId`; NL-103's own ACC is a corpus-level ratio metric,
/// not a per-instance identity, so this is a deliberate, documented
/// simplification, not a gap.
pub
/// Classifies one call-expression node: resolves its callee (`Expression::
/// call_subroutine`) and hands off to [`classify_call_site`], or tags it
/// `Barrier` directly if `sv-lang` cannot resolve a concrete callee
/// `Symbol` at all (a system call like `$display`, or a call this pass
/// cannot statically resolve, e.g. a virtual method call through a class
/// handle — netlist-analysis.md §E step 2's "chandle/virtual/unresolvable"
/// case) — Decision 6's explicit fail-closed default, never
/// `Pure`/`DeferredSafe`.
/// True if `sub` is exported to C via an `export "DPI-C"` directive
/// anywhere in the design (`Design::dpi_exports()`'s reverse index, §E
/// step 1): a DPI-exported blocking task/function can suspend/resume
/// across simulation time, which is unsound to ever speculate past.
/// The classifier itself (Decision 6/§E): `call` is the already-resolved
/// callee subroutine `Symbol` — the caller ([`classify_call_expression`]/
/// this function's own recursive step) is the one place an unresolvable
/// callee is handled, defaulting straight to `Barrier` WITHOUT ever
/// reaching this function, so `Pure`/`DeferredSafe` here are only ever
/// produced through an explicit, provable success path — never a
/// wildcard/default arm (this task's own acceptance check).
///
/// **Deviation, documented (no caller write-set threaded through):** the
/// plan's own signature for this function carries no caller-context
/// parameter, so `DeferredSafe` ("writes only into things already inside
/// the CALLER's own write-set") is not reachable by this pinned-wave
/// implementation — any write the callee performs outside its own
/// locals/return falls through to `Barrier` instead, which is still a
/// sound, fail-closed answer (never wrongly promoted to `Pure`), just not
/// the maximally-precise one `DeferredSafe` would be. A future wave that
/// wants real `DeferredSafe` classification needs to widen this
/// function's signature — recorded here rather than silently stubbed.
/// A per-callee-body dataflow pass (§E step 3): `sv_lang::dataflow::
/// run_dataflow` accepts a `Subroutine` symbol directly as its `procedure`
/// argument (`dataflow.rs`'s own doc: "an always/initial/final block, a
/// subroutine, or a continuous assign"), so `call` itself is run through
/// the SAME CFG-walking machinery Task 7's `ReadSetLattice` uses. Records
/// only the owned, context-free `crate::symid::SymbolId` of each write target
/// (never a live `Symbol<'d>` — see this module's own doc comment on why a
/// custom `Lattice` cannot store one across `transfer` calls) plus whether
/// ANY `Call` event occurred at all (see this module's own doc comment on
/// why a nested call's identity cannot be recovered for genuine
/// recursion).
/// Every write/call event of the analysis run in progress, across ALL paths.
///
/// The purity test is a "may" question (does ANY path write outside the callee or call
/// something?), so it cannot be read from the forward analysis's exit state: a `return`
/// makes the remainder of its path unreachable, so the exit state of a function whose paths
/// end in `return` no longer carries the events those paths saw, and every such function
/// used to classify `Pure` (a fail-closed violation). `transfer` also records each event
/// here; [`classify_by_recursing_into_body`] owns the accumulator for exactly one
/// `run_dataflow` call (the analysis never re-enters itself on the same thread).
thread_local!
/// §E step 3's real body: `call`'s own write-set (excluding writes to its
/// own locals/formals/return-value variable — `call.members()` plus
/// `call.subroutine_return_val_var()` defensively, since a `Subroutine` is
/// itself a `Scope`) must be empty, AND `call` must make no further call of
/// ANY kind (see this module's own doc comment on why a further call
/// cannot be recursively proven `Pure` rather than conservatively folded
/// to `Barrier` in this pinned wave) — otherwise `Barrier` (see
/// [`classify_call_site`]'s own doc comment on why `DeferredSafe` is not
/// reachable here).