netlistmod 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netlistmod/__init__.py +211 -0
- netlistmod/dialects.py +25 -0
- netlistmod/document/__init__.py +36 -0
- netlistmod/document/diagnostics.py +474 -0
- netlistmod/document/document.py +255 -0
- netlistmod/document/index.py +523 -0
- netlistmod/document/lines.py +119 -0
- netlistmod/document/model-types.json +18 -0
- netlistmod/document/models.py +43 -0
- netlistmod/document/names.py +51 -0
- netlistmod/document/topology.py +274 -0
- netlistmod/edit/__init__.py +17 -0
- netlistmod/edit/_ops_behavioral.py +62 -0
- netlistmod/edit/_ops_common.py +223 -0
- netlistmod/edit/_ops_controlled.py +59 -0
- netlistmod/edit/_ops_directives.py +246 -0
- netlistmod/edit/_ops_elements.py +281 -0
- netlistmod/edit/_ops_sources.py +244 -0
- netlistmod/edit/_ops_statements.py +84 -0
- netlistmod/edit/api.py +685 -0
- netlistmod/edit/conventions.py +64 -0
- netlistmod/edit/operations.py +77 -0
- netlistmod/edit/placement.py +133 -0
- netlistmod/edit/plan.py +122 -0
- netlistmod/edit/verify.py +184 -0
- netlistmod/errors.py +68 -0
- netlistmod/grammar/__init__.py +5 -0
- netlistmod/grammar/core.lark +547 -0
- netlistmod/grammar/formula-reserved.json +18 -0
- netlistmod/grammar/formula.lark +77 -0
- netlistmod/grammar/formula.py +113 -0
- netlistmod/grammar/formula_ltspice.lark +21 -0
- netlistmod/grammar/loader.py +461 -0
- netlistmod/grammar/ltspice-devices.json +27 -0
- netlistmod/grammar/ltspice.lark +649 -0
- netlistmod/grammar/ngspice-devices.json +28 -0
- netlistmod/grammar/ngspice.lark +571 -0
- netlistmod/model/__init__.py +145 -0
- netlistmod/model/_rules_base.py +109 -0
- netlistmod/model/_rules_behavioral.py +70 -0
- netlistmod/model/_rules_controlled.py +185 -0
- netlistmod/model/_rules_elements.py +412 -0
- netlistmod/model/_rules_sources.py +268 -0
- netlistmod/model/_rules_statements.py +559 -0
- netlistmod/model/_rules_structure.py +136 -0
- netlistmod/model/_tree.py +358 -0
- netlistmod/model/nodes.py +731 -0
- netlistmod/model/transform.py +95 -0
- netlistmod/model/values.py +131 -0
- netlistmod/py.typed +0 -0
- netlistmod/source.py +289 -0
- netlistmod-0.5.0.dist-info/METADATA +94 -0
- netlistmod-0.5.0.dist-info/RECORD +55 -0
- netlistmod-0.5.0.dist-info/WHEEL +4 -0
- netlistmod-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""Non-fatal findings about a netlist, delivered as data (architecture §8, §17).
|
|
2
|
+
|
|
3
|
+
Diagnostics are computed once when a :class:`~netlistmod.document.Netlist` is built and are
|
|
4
|
+
never raised. Their codes and severities are fixed by decision 0006.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from netlistmod.dialects import Dialect
|
|
15
|
+
from netlistmod.document.index import Index, Reference, Scope, definition_scope, walk_nodes
|
|
16
|
+
from netlistmod.document.models import allowed_model_types, model_type_allowed
|
|
17
|
+
from netlistmod.document.names import instance_name, is_automatic_global, is_ground
|
|
18
|
+
from netlistmod.model.nodes import (
|
|
19
|
+
Cccs,
|
|
20
|
+
Ccvs,
|
|
21
|
+
Component,
|
|
22
|
+
CurrentSwitch,
|
|
23
|
+
GlobalDirective,
|
|
24
|
+
Inductor,
|
|
25
|
+
ModelStatement,
|
|
26
|
+
StepDirective,
|
|
27
|
+
SubcktDefinition,
|
|
28
|
+
SubcktInstance,
|
|
29
|
+
UnknownDirective,
|
|
30
|
+
Vcvs,
|
|
31
|
+
VoltageSource,
|
|
32
|
+
element_phrase,
|
|
33
|
+
)
|
|
34
|
+
from netlistmod.model.values import Formula, NumericValue
|
|
35
|
+
from netlistmod.source import SourceText, Span
|
|
36
|
+
|
|
37
|
+
Severity = Literal["warning", "info"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DiagnosticCode(StrEnum):
|
|
41
|
+
"""The code of a diagnostic (architecture §17)."""
|
|
42
|
+
|
|
43
|
+
UNRESOLVED_MODEL = "UNRESOLVED_MODEL"
|
|
44
|
+
UNRESOLVED_SUBCKT = "UNRESOLVED_SUBCKT"
|
|
45
|
+
UNRESOLVED_PARAM = "UNRESOLVED_PARAM"
|
|
46
|
+
UNRESOLVED_ELEMENT = "UNRESOLVED_ELEMENT"
|
|
47
|
+
DUPLICATE_DEFINITION = "DUPLICATE_DEFINITION"
|
|
48
|
+
DUPLICATE_ELEMENT = "DUPLICATE_ELEMENT"
|
|
49
|
+
SUBCKT_PORT_MISMATCH = "SUBCKT_PORT_MISMATCH"
|
|
50
|
+
PARAM_USED_BEFORE_DEFINITION = "PARAM_USED_BEFORE_DEFINITION"
|
|
51
|
+
MODEL_TYPE_MISMATCH = "MODEL_TYPE_MISMATCH"
|
|
52
|
+
ENDS_NAME_MISMATCH = "ENDS_NAME_MISMATCH"
|
|
53
|
+
DIALECT_GATED_SYNTAX = "DIALECT_GATED_SYNTAX"
|
|
54
|
+
RKM_SOLVER_MISMATCH = "RKM_SOLVER_MISMATCH"
|
|
55
|
+
UNTYPED_DIRECTIVE_PRESERVED = "UNTYPED_DIRECTIVE_PRESERVED"
|
|
56
|
+
NODE_INTO_INSTANCE = "NODE_INTO_INSTANCE"
|
|
57
|
+
CONTROL_SOURCE_MISMATCH = "CONTROL_SOURCE_MISMATCH"
|
|
58
|
+
UNRESOLVED_NODE = "UNRESOLVED_NODE"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class Diagnostic:
|
|
63
|
+
"""One finding: its code, severity, a message, and where it is (1-based line and column)."""
|
|
64
|
+
|
|
65
|
+
code: DiagnosticCode
|
|
66
|
+
severity: Severity
|
|
67
|
+
message: str
|
|
68
|
+
span: Span
|
|
69
|
+
line: int
|
|
70
|
+
column: int
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_UNRESOLVED: dict[str, tuple[DiagnosticCode, str]] = {
|
|
74
|
+
"model": (DiagnosticCode.UNRESOLVED_MODEL, "model"),
|
|
75
|
+
"subckt": (DiagnosticCode.UNRESOLVED_SUBCKT, "subcircuit"),
|
|
76
|
+
"param": (DiagnosticCode.UNRESOLVED_PARAM, "parameter"),
|
|
77
|
+
"element": (DiagnosticCode.UNRESOLVED_ELEMENT, "element"),
|
|
78
|
+
"node": (DiagnosticCode.UNRESOLVED_NODE, "node"),
|
|
79
|
+
}
|
|
80
|
+
_ORDER = {code: position for position, code in enumerate(DiagnosticCode)}
|
|
81
|
+
# The solver version whose behaviour a message states (validated_on of the rule tables).
|
|
82
|
+
_SOLVER: dict[Dialect, str] = {Dialect.NGSPICE: "ngspice 47", Dialect.LTSPICE: "LTspice 26.1.1"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def collect_diagnostics(index: Index, source: SourceText) -> tuple[Diagnostic, ...]:
|
|
86
|
+
"""Compute every diagnostic of a netlist, in source order (decision 0006 items 7 and 8)."""
|
|
87
|
+
found: list[tuple[DiagnosticCode, Severity, str, Span]] = []
|
|
88
|
+
for reference in index.references:
|
|
89
|
+
found.extend(_reference_findings(index, reference))
|
|
90
|
+
for scope in index.scopes:
|
|
91
|
+
found.extend(_duplicate_findings(scope))
|
|
92
|
+
if index.dialect is Dialect.LTSPICE:
|
|
93
|
+
found.extend(_instance_node_findings(index, scope))
|
|
94
|
+
for line in index.lines.walk():
|
|
95
|
+
item = line.item
|
|
96
|
+
if isinstance(item, SubcktDefinition) and item.ends.name is not None:
|
|
97
|
+
if index.root.fold(item.ends.name) != index.root.fold(item.name):
|
|
98
|
+
assert item.ends.name_span is not None
|
|
99
|
+
checks = (
|
|
100
|
+
"ngspice does not check the name (ngspice manual §2.6.2)"
|
|
101
|
+
if index.dialect is Dialect.NGSPICE
|
|
102
|
+
else f"{_SOLVER[index.dialect]} does not check the name"
|
|
103
|
+
)
|
|
104
|
+
found.append(
|
|
105
|
+
(
|
|
106
|
+
DiagnosticCode.ENDS_NAME_MISMATCH,
|
|
107
|
+
"info",
|
|
108
|
+
f"`.ends {item.ends.name}` closes subcircuit `{item.name}`; {checks}",
|
|
109
|
+
item.ends.name_span,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
if isinstance(item, UnknownDirective):
|
|
113
|
+
span = item.name_span or item.span
|
|
114
|
+
found.append(
|
|
115
|
+
(
|
|
116
|
+
DiagnosticCode.UNTYPED_DIRECTIVE_PRESERVED,
|
|
117
|
+
"info",
|
|
118
|
+
f"`{item.name}` is not typed by the library and is preserved verbatim",
|
|
119
|
+
span,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
# Bodies are walked as their own lines, so a definition contributes only its header.
|
|
123
|
+
for value in walk_nodes(item, NumericValue, bodies=False):
|
|
124
|
+
if index.dialect is Dialect.NGSPICE:
|
|
125
|
+
found.extend(_value_findings(value))
|
|
126
|
+
found.sort(key=lambda entry: (entry[3].start, entry[3].end, _ORDER[entry[0]]))
|
|
127
|
+
diagnostics = []
|
|
128
|
+
for code, severity, message, span in found:
|
|
129
|
+
line_number, column = source.position(span.start)
|
|
130
|
+
diagnostics.append(Diagnostic(code, severity, message, span, line_number, column))
|
|
131
|
+
return tuple(diagnostics)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _reference_findings(
|
|
135
|
+
index: Index, reference: Reference
|
|
136
|
+
) -> list[tuple[DiagnosticCode, Severity, str, Span]]:
|
|
137
|
+
findings: list[tuple[DiagnosticCode, Severity, str, Span]] = []
|
|
138
|
+
scope = index.scope(reference.scope)
|
|
139
|
+
if reference.target is None and reference.kind in _UNRESOLVED:
|
|
140
|
+
code, noun = _UNRESOLVED[reference.kind]
|
|
141
|
+
searched = (scope,) if reference.kind in ("element", "node") else tuple(scope.chain())
|
|
142
|
+
where = ", ".join(candidate.describe() for candidate in searched)
|
|
143
|
+
message = f"{noun} `{reference.name}` is not defined (searched: {where})"
|
|
144
|
+
severity: Severity = "warning"
|
|
145
|
+
into = _into_instance(index, scope, reference)
|
|
146
|
+
if into is not None:
|
|
147
|
+
severity = "info"
|
|
148
|
+
what = "a node" if reference.kind == "node" else "a source"
|
|
149
|
+
message += (
|
|
150
|
+
f"; it names {what} inside instance `{into}`, which {_SOLVER[index.dialect]} "
|
|
151
|
+
"reads and the library does not resolve"
|
|
152
|
+
)
|
|
153
|
+
elif any(candidate.may_define_elsewhere for candidate in searched):
|
|
154
|
+
severity = "info"
|
|
155
|
+
message += (
|
|
156
|
+
"; it may be defined in an `.include` or `.lib` file or an opaque block, which "
|
|
157
|
+
"is not read"
|
|
158
|
+
)
|
|
159
|
+
findings.append((code, severity, message, reference.span))
|
|
160
|
+
if reference in index.late_param_uses:
|
|
161
|
+
findings.append(
|
|
162
|
+
(
|
|
163
|
+
DiagnosticCode.PARAM_USED_BEFORE_DEFINITION,
|
|
164
|
+
"warning",
|
|
165
|
+
f"parameter `{reference.name}` is used before its `.param` definition"
|
|
166
|
+
f"{' (ngspice manual §2.11.1)' if index.dialect is Dialect.NGSPICE else ''}; "
|
|
167
|
+
f"{_SOLVER[index.dialect]} still resolves it",
|
|
168
|
+
reference.span,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
owner = reference.owner
|
|
172
|
+
target = reference.target
|
|
173
|
+
if (
|
|
174
|
+
reference.kind == "model"
|
|
175
|
+
and isinstance(target, ModelStatement)
|
|
176
|
+
and isinstance(owner, Component)
|
|
177
|
+
and not model_type_allowed(owner, target.model_type, index.dialect)
|
|
178
|
+
):
|
|
179
|
+
allowed = ", ".join(
|
|
180
|
+
f"`{kind.upper()}`" for kind in sorted(allowed_model_types(owner, index.dialect))
|
|
181
|
+
)
|
|
182
|
+
cited = " (ngspice manual §2.5)" if index.dialect is Dialect.NGSPICE else ""
|
|
183
|
+
findings.append(
|
|
184
|
+
(
|
|
185
|
+
DiagnosticCode.MODEL_TYPE_MISMATCH,
|
|
186
|
+
"warning",
|
|
187
|
+
f"`{owner.designator}` refers to model `{target.name}` of type "
|
|
188
|
+
f"`{target.model_type}`, but {element_phrase(owner)} takes a model of "
|
|
189
|
+
f"type {allowed}{cited}",
|
|
190
|
+
reference.span,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if (
|
|
194
|
+
reference.kind == "element"
|
|
195
|
+
and isinstance(owner, Cccs | Ccvs | CurrentSwitch)
|
|
196
|
+
and owner.control is not None
|
|
197
|
+
and reference.span == owner.control.span
|
|
198
|
+
and target is not None
|
|
199
|
+
and isinstance(target, Component)
|
|
200
|
+
and not isinstance(target, VoltageSource)
|
|
201
|
+
):
|
|
202
|
+
findings.append(
|
|
203
|
+
(
|
|
204
|
+
DiagnosticCode.CONTROL_SOURCE_MISMATCH,
|
|
205
|
+
"warning",
|
|
206
|
+
f"`{owner.designator}` is controlled by `{reference.name}`, "
|
|
207
|
+
f"{element_phrase(target)}, but the {_reference_for(owner, index.dialect)} "
|
|
208
|
+
f"names an independent voltage source (`V`); "
|
|
209
|
+
f"{_control_outcome(target, reference.name, index.dialect)}",
|
|
210
|
+
reference.span,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
if (
|
|
214
|
+
reference.kind == "subckt"
|
|
215
|
+
and isinstance(reference.target, SubcktDefinition)
|
|
216
|
+
and isinstance(owner, SubcktInstance)
|
|
217
|
+
and len(owner.nodes) != len(reference.target.ports)
|
|
218
|
+
):
|
|
219
|
+
findings.append(
|
|
220
|
+
(
|
|
221
|
+
DiagnosticCode.SUBCKT_PORT_MISMATCH,
|
|
222
|
+
"warning",
|
|
223
|
+
f"`{owner.designator}` connects {len(owner.nodes)} nodes, but subcircuit "
|
|
224
|
+
f"`{reference.target.name}` has {len(reference.target.ports)} ports",
|
|
225
|
+
reference.span,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return findings
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# What each solver does with a controlling source that is not a V (decision 0015 item 5).
|
|
232
|
+
_MANUAL_SECTION: dict[type, str] = {Cccs: "4.2.3", Ccvs: "4.2.4", CurrentSwitch: "3.3.15"}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _reference_for(owner: Component, dialect: Dialect) -> str:
|
|
236
|
+
if dialect is Dialect.LTSPICE:
|
|
237
|
+
return "LTspice reference"
|
|
238
|
+
return f"ngspice manual §{_MANUAL_SECTION[type(owner)]}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
_CONTROL_RUNS: dict[Dialect, tuple[type, ...]] = {
|
|
242
|
+
Dialect.NGSPICE: (Vcvs, Ccvs),
|
|
243
|
+
Dialect.LTSPICE: (Vcvs, Ccvs, Inductor),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _control_outcome(target: Component, name: str, dialect: Dialect) -> str:
|
|
248
|
+
"""What the dialect's solver does with this controlling source (review R2)."""
|
|
249
|
+
if isinstance(target, _CONTROL_RUNS[dialect]):
|
|
250
|
+
return f"{_SOLVER[dialect]} runs it, with the current through `{name}`"
|
|
251
|
+
return f"{_SOLVER[dialect]} stops on it"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _into_instance(index: Index, scope: Scope, reference: Reference) -> str | None:
|
|
255
|
+
"""The instance of the scope that an unresolved name reaches into, if it names one.
|
|
256
|
+
|
|
257
|
+
Both solvers read such a name and the library keeps it as text, a known limit (§7.8;
|
|
258
|
+
decision 0016 item 6):
|
|
259
|
+
|
|
260
|
+
- LTspice: ``X1:Vq`` for any element reference (a controlling source, an ``I()`` read, a
|
|
261
|
+
``K`` inductor, a ``.save I(...)``), and ``X1:n`` for a node an expression reads;
|
|
262
|
+
- ngspice: ``x1.n`` for a node an expression reads, and ``v.x1.vq`` (the device letter, the
|
|
263
|
+
instance, the name) for a source it reads. ngspice stops on ``i(x1.vq)``, so that one
|
|
264
|
+
stays a warning.
|
|
265
|
+
"""
|
|
266
|
+
if reference.kind not in ("element", "node"):
|
|
267
|
+
return None
|
|
268
|
+
if index.dialect is Dialect.LTSPICE:
|
|
269
|
+
head, colon, _ = reference.name.partition(":")
|
|
270
|
+
if not colon:
|
|
271
|
+
return None
|
|
272
|
+
elif reference.kind == "node":
|
|
273
|
+
head, dot, _ = reference.name.partition(".")
|
|
274
|
+
if not dot:
|
|
275
|
+
return None
|
|
276
|
+
else:
|
|
277
|
+
parts = reference.name.split(".")
|
|
278
|
+
if len(parts) < 3 or len(parts[0]) != 1 or reference.owner is None:
|
|
279
|
+
return None
|
|
280
|
+
if not _is_expression_read(reference):
|
|
281
|
+
return None
|
|
282
|
+
head = parts[1]
|
|
283
|
+
instance = next(iter(scope.elements.get(scope.fold(head), ())), None)
|
|
284
|
+
return head if isinstance(instance, SubcktInstance) else None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _is_expression_read(reference: Reference) -> bool:
|
|
288
|
+
"""Whether an element reference is an ``i()`` read in a behavioural expression."""
|
|
289
|
+
owner = reference.owner
|
|
290
|
+
formulas = [
|
|
291
|
+
getattr(owner, "expression", None),
|
|
292
|
+
getattr(owner, "charge", None),
|
|
293
|
+
getattr(owner, "flux", None),
|
|
294
|
+
getattr(owner, "value", None),
|
|
295
|
+
]
|
|
296
|
+
return any(
|
|
297
|
+
isinstance(formula, Formula)
|
|
298
|
+
and formula.span.start <= reference.span.start < formula.span.end
|
|
299
|
+
for formula in formulas
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _instance_node_findings(
|
|
304
|
+
index: Index, scope: Scope
|
|
305
|
+
) -> list[tuple[DiagnosticCode, Severity, str, Span]]:
|
|
306
|
+
"""LTspice nodes named ``<instance>:<node>``, which the library keeps as nets of their own."""
|
|
307
|
+
findings: list[tuple[DiagnosticCode, Severity, str, Span]] = []
|
|
308
|
+
solver = _SOLVER[scope.dialect]
|
|
309
|
+
for elements in scope.elements.values():
|
|
310
|
+
for element in elements:
|
|
311
|
+
for node in element.nodes:
|
|
312
|
+
reading = _reading_of_instance_node(index, scope, node.name)
|
|
313
|
+
if reading == "joined":
|
|
314
|
+
severity: Severity = "warning"
|
|
315
|
+
rule = f"{solver} connects it to that node inside the instance"
|
|
316
|
+
elif reading == "unknown":
|
|
317
|
+
severity = "info"
|
|
318
|
+
rule = f"{solver} connects it to a node of that name in the included definition"
|
|
319
|
+
elif reading == "stopped":
|
|
320
|
+
severity = "warning"
|
|
321
|
+
rule = f"{solver} stops: the part after the instance is not a local node"
|
|
322
|
+
else:
|
|
323
|
+
continue
|
|
324
|
+
findings.append(
|
|
325
|
+
(
|
|
326
|
+
DiagnosticCode.NODE_INTO_INSTANCE,
|
|
327
|
+
severity,
|
|
328
|
+
f"node `{node.name}` names an instance; {rule}, and the library keeps it "
|
|
329
|
+
"as a separate net",
|
|
330
|
+
node.span,
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
return findings
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _reading_of_instance_node(
|
|
337
|
+
index: Index, scope: Scope, name: str
|
|
338
|
+
) -> Literal["joined", "unknown", "stopped"] | None:
|
|
339
|
+
"""How LTspice reads ``<instance>:<node>`` from ``scope`` (``names.md`` N11), if it is one."""
|
|
340
|
+
head, colon, rest = name.partition(":")
|
|
341
|
+
if not colon:
|
|
342
|
+
return None
|
|
343
|
+
key = scope.fold(head)
|
|
344
|
+
instance = next(
|
|
345
|
+
(
|
|
346
|
+
element
|
|
347
|
+
for elements in scope.elements.values()
|
|
348
|
+
for element in elements
|
|
349
|
+
if isinstance(element, SubcktInstance)
|
|
350
|
+
and instance_name(element.designator, scope.dialect) == key
|
|
351
|
+
),
|
|
352
|
+
None,
|
|
353
|
+
)
|
|
354
|
+
if instance is None:
|
|
355
|
+
return None
|
|
356
|
+
child = definition_scope(scope, instance.subckt.name)
|
|
357
|
+
if ":" not in rest and (not rest or _is_global(index, rest)):
|
|
358
|
+
return "stopped"
|
|
359
|
+
if child is None:
|
|
360
|
+
included = any(candidate.may_define_elsewhere for candidate in scope.chain())
|
|
361
|
+
return "unknown" if included else None
|
|
362
|
+
if ":" in rest:
|
|
363
|
+
return _reading_of_instance_node(index, child, rest)
|
|
364
|
+
assert child.definition is not None
|
|
365
|
+
local = {child.fold(port.name) for port in child.definition.ports}
|
|
366
|
+
local.update(
|
|
367
|
+
child.fold(node.name)
|
|
368
|
+
for elements in child.elements.values()
|
|
369
|
+
for element in elements
|
|
370
|
+
for node in element.nodes
|
|
371
|
+
)
|
|
372
|
+
return "joined" if child.fold(rest) in local else None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _is_global(index: Index, name: str) -> bool:
|
|
376
|
+
"""Whether a node name means the same net everywhere: ground or a global node."""
|
|
377
|
+
if is_ground(name, index.dialect) or is_automatic_global(name, index.dialect):
|
|
378
|
+
return True
|
|
379
|
+
return any(
|
|
380
|
+
isinstance(line.item, GlobalDirective)
|
|
381
|
+
and any(index.root.fold(node.name) == index.root.fold(name) for node in line.item.nodes)
|
|
382
|
+
for line in index.lines
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _duplicate_findings(scope: Scope) -> list[tuple[DiagnosticCode, Severity, str, Span]]:
|
|
387
|
+
findings: list[tuple[DiagnosticCode, Severity, str, Span]] = []
|
|
388
|
+
where = scope.describe()
|
|
389
|
+
fatal = scope.dialect is Dialect.LTSPICE
|
|
390
|
+
tables: tuple[tuple[Mapping[str, tuple[object, ...]], str, Severity], ...] = (
|
|
391
|
+
# LTspice stops at a repeated .param; ngspice uses the last one silently.
|
|
392
|
+
(scope.params, "parameter", "warning" if fatal else "info"),
|
|
393
|
+
(scope.models, "model", "warning"),
|
|
394
|
+
(scope.subckts, "subcircuit", "warning"),
|
|
395
|
+
)
|
|
396
|
+
for table, noun, severity in tables:
|
|
397
|
+
for definitions in table.values():
|
|
398
|
+
for repeated in definitions[1:]:
|
|
399
|
+
name, span = _name_of(repeated)
|
|
400
|
+
if fatal:
|
|
401
|
+
rule = f"{_SOLVER[scope.dialect]} stops with an error"
|
|
402
|
+
if noun == "model":
|
|
403
|
+
rule += " if an element uses it and the definitions differ"
|
|
404
|
+
elif noun == "subcircuit":
|
|
405
|
+
rule += " if an instance uses it"
|
|
406
|
+
elif noun == "parameter":
|
|
407
|
+
rule = "the last assignment is used"
|
|
408
|
+
else:
|
|
409
|
+
rule = "ngspice uses the first definition"
|
|
410
|
+
findings.append(
|
|
411
|
+
(
|
|
412
|
+
DiagnosticCode.DUPLICATE_DEFINITION,
|
|
413
|
+
severity,
|
|
414
|
+
f"{noun} `{name}` is defined again in {where}; {rule}",
|
|
415
|
+
span,
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
seen: set[tuple[str, str]] = set()
|
|
419
|
+
for line in scope.lines:
|
|
420
|
+
if isinstance(line.item, StepDirective):
|
|
421
|
+
quantity = (line.item.kind, scope.fold(line.item.name))
|
|
422
|
+
if quantity in seen:
|
|
423
|
+
findings.append(
|
|
424
|
+
(
|
|
425
|
+
DiagnosticCode.DUPLICATE_DEFINITION,
|
|
426
|
+
"warning",
|
|
427
|
+
f"`.step` sweeps `{line.item.name}` again; {_SOLVER[scope.dialect]} stops "
|
|
428
|
+
"with an error",
|
|
429
|
+
line.item.name_span,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
seen.add(quantity)
|
|
433
|
+
for elements in scope.elements.values():
|
|
434
|
+
for repeated in elements[1:]:
|
|
435
|
+
findings.append(
|
|
436
|
+
(
|
|
437
|
+
DiagnosticCode.DUPLICATE_ELEMENT,
|
|
438
|
+
"warning",
|
|
439
|
+
f"element `{repeated.designator}` repeats a designator in {where} "
|
|
440
|
+
f"(case-insensitive); {_SOLVER[scope.dialect]} stops with an error",
|
|
441
|
+
repeated.designator_span,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
return findings
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _name_of(definition: object) -> tuple[str, Span]:
|
|
448
|
+
name: str = getattr(definition, "name")
|
|
449
|
+
span: Span = getattr(definition, "name_span")
|
|
450
|
+
return name, span
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _value_findings(value: NumericValue) -> list[tuple[DiagnosticCode, Severity, str, Span]]:
|
|
454
|
+
findings: list[tuple[DiagnosticCode, Severity, str, Span]] = []
|
|
455
|
+
if value.notation == "rkm":
|
|
456
|
+
findings.append(
|
|
457
|
+
(
|
|
458
|
+
DiagnosticCode.DIALECT_GATED_SYNTAX,
|
|
459
|
+
"info",
|
|
460
|
+
f"`{value.text}` is RKM notation, which ngspice reads only in LT compatibility "
|
|
461
|
+
"mode",
|
|
462
|
+
value.span,
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
if value.solver_mismatch:
|
|
466
|
+
findings.append(
|
|
467
|
+
(
|
|
468
|
+
DiagnosticCode.RKM_SOLVER_MISMATCH,
|
|
469
|
+
"warning",
|
|
470
|
+
f"`{value.text}` is read per IEC 60062, but ngspice reads it differently",
|
|
471
|
+
value.span,
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
return findings
|