nativegate 0.1.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.
- nativegate/__init__.py +1 -0
- nativegate/__main__.py +4 -0
- nativegate/buildinfo.py +344 -0
- nativegate/cli.py +2007 -0
- nativegate/config.py +991 -0
- nativegate/declared_invariants.py +565 -0
- nativegate/discovery.py +167 -0
- nativegate/driverbuild.py +626 -0
- nativegate/drivers/__init__.py +5 -0
- nativegate/drivers/cpp.py +616 -0
- nativegate/drivers/fortran.py +507 -0
- nativegate/generators/__init__.py +0 -0
- nativegate/generators/cmake_gen.py +101 -0
- nativegate/generators/docker_gen.py +614 -0
- nativegate/generators/error_gen.py +104 -0
- nativegate/generators/f2py_gen.py +91 -0
- nativegate/generators/gateway_gen.py +110 -0
- nativegate/generators/golden_gen.py +50 -0
- nativegate/generators/k8s_gen.py +212 -0
- nativegate/generators/mcp_gen.py +281 -0
- nativegate/generators/middleware_gen.py +717 -0
- nativegate/generators/pybind_gen.py +406 -0
- nativegate/generators/pyproject_gen.py +61 -0
- nativegate/generators/python_pkg_gen.py +1164 -0
- nativegate/generators/test_gen.py +160 -0
- nativegate/golden.py +747 -0
- nativegate/invariants.py +532 -0
- nativegate/ir.py +789 -0
- nativegate/lattice.py +350 -0
- nativegate/locking.py +216 -0
- nativegate/oracle.py +904 -0
- nativegate/parsers/__init__.py +0 -0
- nativegate/parsers/cpp.py +105 -0
- nativegate/parsers/cpp_ast.py +1652 -0
- nativegate/parsers/cpp_regex.py +812 -0
- nativegate/parsers/fixed_form.py +868 -0
- nativegate/parsers/fortran.py +157 -0
- nativegate/parsers/fortran_fparser.py +1116 -0
- nativegate/parsers/fortran_regex.py +686 -0
- nativegate/preprocess.py +335 -0
- nativegate/structural_invariants.py +762 -0
- nativegate/suggest.py +208 -0
- nativegate/templates/__init__.py +20 -0
- nativegate/templates/golden_test_template.py +248 -0
- nativegate/wire.py +438 -0
- nativegate-0.1.0.dist-info/METADATA +547 -0
- nativegate-0.1.0.dist-info/RECORD +50 -0
- nativegate-0.1.0.dist-info/WHEEL +5 -0
- nativegate-0.1.0.dist-info/entry_points.txt +3 -0
- nativegate-0.1.0.dist-info/top_level.txt +1 -0
nativegate/invariants.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""T12 -- `invariants.json`: the layer-3 artifact and its aggregate gate.
|
|
2
|
+
|
|
3
|
+
Spec: design-verification-layers.md section 3.7 (file schema, `uncovered`),
|
|
4
|
+
section 2.6 (CI ordering: oracle check -> golden -> invariants), section 5
|
|
5
|
+
(where each layer runs).
|
|
6
|
+
|
|
7
|
+
This module is the closing task of the verification-layers effort: it does
|
|
8
|
+
not implement new checking logic (T10's structural properties and T11's
|
|
9
|
+
declared properties already do that), it *aggregates* them into one result
|
|
10
|
+
artifact per entry, and enforces the one rule none of the earlier tasks were
|
|
11
|
+
positioned to enforce on their own -- "an invariants file that silently
|
|
12
|
+
checks nothing must not look like a pass" (section 3.7, quoted in
|
|
13
|
+
`EmptyCheckedError`).
|
|
14
|
+
|
|
15
|
+
Ambiguities resolved here (T10/T11 were silent on them because they operate
|
|
16
|
+
one property/one sweep at a time, not on the whole file):
|
|
17
|
+
|
|
18
|
+
* **Which sweep(s) `bounds`/`sum_to_one` run against.** Neither carries an
|
|
19
|
+
`in:` parameter (unlike `monotone`/`scales_linearly_in`), so section 3.4's
|
|
20
|
+
lattice does not say which of an entry's several 1-D sweeps a parameter-
|
|
21
|
+
less property is evaluated over. This module evaluates them over *every*
|
|
22
|
+
swept parameter of the entry -- each of the entry's `ParameterSweep`s in
|
|
23
|
+
turn -- rather than inventing a "the first one" or "the base point only"
|
|
24
|
+
reading, on the theory that a `bounds` claim is supposed to hold
|
|
25
|
+
everywhere the lattice looks, not only along one arbitrarily-chosen axis.
|
|
26
|
+
An entry with no swept parameter at all (nothing in `ranges:` matches its
|
|
27
|
+
numeric arguments) cannot evaluate the property and is recorded under
|
|
28
|
+
`uncovered` instead of silently skipped.
|
|
29
|
+
* **`uncovered`'s scope.** Section 3.4 says "a swept parameter with no
|
|
30
|
+
declared range is an error ... recorded under `uncovered`" for the
|
|
31
|
+
*lattice* in general, not only for parameters a declared invariant
|
|
32
|
+
mentions. This module therefore records EVERY entry with an unswept
|
|
33
|
+
numeric parameter under `uncovered` (via `EntryLattice.unswept_parameters`,
|
|
34
|
+
T9), whether or not that entry carries a declared invariant at all --
|
|
35
|
+
this is what surfaces `tubing_bhp`'s undeclared `rate` range even though
|
|
36
|
+
no `invariants: tubing_bhp: ...` block exists in `nativegate.yaml`.
|
|
37
|
+
* **`corners`/`scatter`/`n` default from `VerificationConfig.lattice`.**
|
|
38
|
+
T8's `VerificationConfig` now parses a `lattice:` block (`n`,
|
|
39
|
+
`scatter: {seed, count}`, `corners:`) -- see `config.LatticeConfig`. Every
|
|
40
|
+
`n=`/`scatter_seed=`/`scatter_count=`/`corners=` keyword below defaults to
|
|
41
|
+
`None`, meaning "take it from `verification.lattice`"; an explicit
|
|
42
|
+
argument (still accepted, for tests and any other direct caller) always
|
|
43
|
+
wins over the declared config. `corners` is reported in `invariants.json`
|
|
44
|
+
as a mapping of function name to its declared corner tuples (matching
|
|
45
|
+
`LatticeConfig.corners`'s own shape) rather than the flat list this module
|
|
46
|
+
used before the config surface existed, since a flat list cannot say which
|
|
47
|
+
function each corner belongs to once more than one function declares
|
|
48
|
+
corners.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
from dataclasses import dataclass, field, replace
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import TYPE_CHECKING, Sequence
|
|
57
|
+
|
|
58
|
+
from . import golden, lattice, structural_invariants as si
|
|
59
|
+
from .config import (
|
|
60
|
+
BoundsProperty,
|
|
61
|
+
MonotoneProperty,
|
|
62
|
+
ScalesLinearlyInProperty,
|
|
63
|
+
SumToOneProperty,
|
|
64
|
+
SymmetricInProperty,
|
|
65
|
+
VerificationConfig,
|
|
66
|
+
)
|
|
67
|
+
from .declared_invariants import NotImplementedInvariant, PropertyReport, evaluate_property, _label
|
|
68
|
+
from .lattice import EntryLattice, ParameterSweep
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from .ir import ModuleIR
|
|
72
|
+
|
|
73
|
+
INVARIANTS_FILENAME = "invariants.json"
|
|
74
|
+
FORMAT_VERSION = 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InvariantsError(RuntimeError):
|
|
78
|
+
"""Base for a hard failure of the invariants runner itself."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EmptyCheckedError(InvariantsError):
|
|
82
|
+
"""`checked` ended up empty -- section 3.7: "an invariants file that
|
|
83
|
+
silently checks nothing must not look like a pass." Never swallowed."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _UncoveredError(Exception):
|
|
87
|
+
"""Internal: a declared property could not be evaluated. Turned into an
|
|
88
|
+
`uncovered` reason string, never raised past this module."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class InvariantsResult:
|
|
93
|
+
document: dict
|
|
94
|
+
passed: bool
|
|
95
|
+
failure_messages: list[str] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- picking which sweep(s) a declared property runs against ---------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_n(n: int | None, verification: VerificationConfig) -> int:
|
|
102
|
+
"""`n`'s "explicit argument, else declared config, else the module
|
|
103
|
+
default" resolution rule -- shared by `build_invariants_document` and
|
|
104
|
+
`verify_invariants` so the two entry points can never silently disagree
|
|
105
|
+
on what an unset `n` means for the same `VerificationConfig`."""
|
|
106
|
+
if n is not None:
|
|
107
|
+
return n
|
|
108
|
+
return verification.lattice.n if verification.lattice.n is not None else lattice.DEFAULT_SWEEP_POINTS
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sweeps_for_property(
|
|
112
|
+
prop, entry_lattice: EntryLattice, parameter_names: Sequence[str]
|
|
113
|
+
) -> list[tuple[ParameterSweep, int]]:
|
|
114
|
+
"""`[(sweep, argument_position), ...]` to evaluate `prop` against, or
|
|
115
|
+
raise `_UncoveredError` with the reason it cannot be evaluated at all.
|
|
116
|
+
|
|
117
|
+
See the module docstring's "Ambiguities resolved here" for why
|
|
118
|
+
`bounds`/`sum_to_one` (no `in:`) run against every swept parameter while
|
|
119
|
+
`monotone`/`scales_linearly_in` (declared `in:`) run against exactly one.
|
|
120
|
+
"""
|
|
121
|
+
if isinstance(prop, (MonotoneProperty, ScalesLinearlyInProperty)):
|
|
122
|
+
parameter = prop.parameter
|
|
123
|
+
sweep_obj = next((s for s in entry_lattice.sweeps if s.parameter == parameter), None)
|
|
124
|
+
if sweep_obj is None:
|
|
125
|
+
raise _UncoveredError(f"no range declared for parameter '{parameter}'")
|
|
126
|
+
return [(sweep_obj, parameter_names.index(parameter))]
|
|
127
|
+
|
|
128
|
+
if isinstance(prop, SymmetricInProperty):
|
|
129
|
+
# No evaluator exists yet (declared_invariants.evaluate_property
|
|
130
|
+
# raises NotImplementedInvariant for this word regardless of which
|
|
131
|
+
# sweep is chosen) -- pick a sweep only so evaluate_property has one
|
|
132
|
+
# to raise against; if there is none, report the range gap instead
|
|
133
|
+
# of the unimplemented-word gap, since that is the more actionable
|
|
134
|
+
# of the two.
|
|
135
|
+
if not entry_lattice.sweeps:
|
|
136
|
+
raise _UncoveredError(
|
|
137
|
+
"no swept parameter declared to evaluate `symmetric_in` against"
|
|
138
|
+
)
|
|
139
|
+
sweep_obj = entry_lattice.sweeps[0]
|
|
140
|
+
return [(sweep_obj, parameter_names.index(sweep_obj.parameter))]
|
|
141
|
+
|
|
142
|
+
# bounds / sum_to_one: no declared `in:` -- evaluated over every swept
|
|
143
|
+
# parameter of the entry (see module docstring).
|
|
144
|
+
if not entry_lattice.sweeps:
|
|
145
|
+
missing = entry_lattice.unswept_parameters
|
|
146
|
+
reason = (
|
|
147
|
+
f"no range declared for parameter(s) {', '.join(missing)}"
|
|
148
|
+
if missing
|
|
149
|
+
else "no swept parameter available to evaluate this property against"
|
|
150
|
+
)
|
|
151
|
+
raise _UncoveredError(reason)
|
|
152
|
+
return [(s, parameter_names.index(s.parameter)) for s in entry_lattice.sweeps]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _callable_for(package, entry: dict):
|
|
156
|
+
"""A plain positional callable `declared_invariants.evaluate_property`
|
|
157
|
+
can call directly -- reuses `golden.invoke` (materialisation, method/
|
|
158
|
+
static/function dispatch) rather than re-deriving calling conventions."""
|
|
159
|
+
|
|
160
|
+
def call(*args):
|
|
161
|
+
synthetic = {**entry, "arguments": list(args)}
|
|
162
|
+
result, _effects = golden.invoke(synthetic, package)
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
return call
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _entries_by_function_name(document: dict) -> dict[str, tuple[str, dict]]:
|
|
169
|
+
"""First golden.json entry per function name, paired with its own key.
|
|
170
|
+
|
|
171
|
+
Returning the `(key, entry)` pair (rather than just `entry`) lets callers
|
|
172
|
+
look up the matching lattice by the *same* golden key that picked this
|
|
173
|
+
entry, so a function with more than one recorded entry (e.g. one golden
|
|
174
|
+
entry per constructed instance) always evaluates a property against one
|
|
175
|
+
consistent (entry, lattice) pair instead of silently mixing one entry's
|
|
176
|
+
call arguments with a different entry's swept ranges.
|
|
177
|
+
"""
|
|
178
|
+
result: dict[str, tuple[str, dict]] = {}
|
|
179
|
+
for key, entry in document.get("entries", {}).items():
|
|
180
|
+
result.setdefault(entry.get("name") or key, (key, entry))
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _limited(sweep_obj: ParameterSweep, points_limit: int | None) -> ParameterSweep:
|
|
185
|
+
if points_limit is None:
|
|
186
|
+
return sweep_obj
|
|
187
|
+
return ParameterSweep(
|
|
188
|
+
parameter=sweep_obj.parameter,
|
|
189
|
+
lo=sweep_obj.lo,
|
|
190
|
+
hi=sweep_obj.hi,
|
|
191
|
+
points=sweep_obj.points[:points_limit],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --- declared-invariant pass, aggregated over a whole entry -----------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run_declared_invariants(
|
|
199
|
+
document: dict,
|
|
200
|
+
functions_by_name: dict,
|
|
201
|
+
verification: VerificationConfig,
|
|
202
|
+
package,
|
|
203
|
+
lattices: dict[str, EntryLattice],
|
|
204
|
+
*,
|
|
205
|
+
points_limit: int | None = None,
|
|
206
|
+
) -> tuple[dict[str, list[tuple[object, list[PropertyReport]]]], dict[str, list[str]]]:
|
|
207
|
+
"""Every declared property in `verification.invariants`, evaluated.
|
|
208
|
+
|
|
209
|
+
Returns `(reports, uncovered)`:
|
|
210
|
+
* `reports[function_name]` is `[(prop, [PropertyReport, ...]), ...]` --
|
|
211
|
+
one `PropertyReport` per sweep the property ran against (see
|
|
212
|
+
`_sweeps_for_property`).
|
|
213
|
+
* `uncovered[function_name]` is every reason a declared property for
|
|
214
|
+
that function could not be evaluated at all (an undeclared range, or
|
|
215
|
+
`NotImplementedInvariant`'s vocabulary gap) -- never silently dropped.
|
|
216
|
+
"""
|
|
217
|
+
entries_by_function = _entries_by_function_name(document)
|
|
218
|
+
|
|
219
|
+
reports: dict[str, list[tuple[object, list[PropertyReport]]]] = {}
|
|
220
|
+
uncovered: dict[str, list[str]] = {}
|
|
221
|
+
|
|
222
|
+
for fn_name, properties in verification.invariants.items():
|
|
223
|
+
key_and_entry = entries_by_function.get(fn_name)
|
|
224
|
+
fn = functions_by_name.get(fn_name)
|
|
225
|
+
entry = key_and_entry[1] if key_and_entry is not None else None
|
|
226
|
+
entry_lattice = lattices.get(key_and_entry[0]) if key_and_entry is not None else None
|
|
227
|
+
if entry is None or fn is None or entry_lattice is None:
|
|
228
|
+
uncovered.setdefault(fn_name, []).append(
|
|
229
|
+
"no golden.json entry (or no matching lattice) to evaluate "
|
|
230
|
+
"declared invariants against"
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
parameter_names = [p.name for p in fn.parameters if p.intent != "out"]
|
|
235
|
+
call = _callable_for(package, entry)
|
|
236
|
+
|
|
237
|
+
for prop in properties:
|
|
238
|
+
try:
|
|
239
|
+
sweeps = _sweeps_for_property(prop, entry_lattice, parameter_names)
|
|
240
|
+
except _UncoveredError as exc:
|
|
241
|
+
uncovered.setdefault(fn_name, []).append(str(exc))
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
sub_reports = [
|
|
246
|
+
evaluate_property(
|
|
247
|
+
call,
|
|
248
|
+
_limited(sweep_obj, points_limit),
|
|
249
|
+
prop,
|
|
250
|
+
fn_name,
|
|
251
|
+
position=position,
|
|
252
|
+
base_arguments=entry_lattice.base_arguments,
|
|
253
|
+
)
|
|
254
|
+
for sweep_obj, position in sweeps
|
|
255
|
+
]
|
|
256
|
+
except NotImplementedInvariant as exc:
|
|
257
|
+
uncovered.setdefault(fn_name, []).append(str(exc))
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
reports.setdefault(fn_name, []).append((prop, sub_reports))
|
|
261
|
+
|
|
262
|
+
return reports, uncovered
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# --- assembling the file (spec section 3.7) ---------------------------------
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def build_invariants_document(
|
|
269
|
+
module: "ModuleIR",
|
|
270
|
+
document: dict,
|
|
271
|
+
verification: VerificationConfig,
|
|
272
|
+
structural_results: si.StructuralInvariantResults,
|
|
273
|
+
declared_reports: dict[str, list[tuple[object, list[PropertyReport]]]],
|
|
274
|
+
declared_uncovered: dict[str, list[str]],
|
|
275
|
+
lattices: dict[str, EntryLattice],
|
|
276
|
+
*,
|
|
277
|
+
n: int | None = None,
|
|
278
|
+
scatter_seed: int | None = None,
|
|
279
|
+
scatter_count: int | None = None,
|
|
280
|
+
corners: dict[str, list] | None = None,
|
|
281
|
+
environment: dict | None = None,
|
|
282
|
+
) -> dict:
|
|
283
|
+
"""The `invariants.json` contents, per spec section 3.7's schema.
|
|
284
|
+
|
|
285
|
+
`n`/`scatter_seed`/`scatter_count`/`corners` each default to `None`,
|
|
286
|
+
meaning "read it from `verification.lattice`" (see the module
|
|
287
|
+
docstring) -- an explicit argument always overrides the declared
|
|
288
|
+
config.
|
|
289
|
+
"""
|
|
290
|
+
n = _resolve_n(n, verification)
|
|
291
|
+
if scatter_seed is None:
|
|
292
|
+
scatter_seed = verification.lattice.scatter.seed
|
|
293
|
+
if scatter_count is None:
|
|
294
|
+
scatter_count = verification.lattice.scatter.count
|
|
295
|
+
if corners is None:
|
|
296
|
+
corners = verification.lattice.corners
|
|
297
|
+
checked: dict[str, dict] = {}
|
|
298
|
+
uncovered: dict[str, str] = {}
|
|
299
|
+
|
|
300
|
+
# First entry wins per function name -- must match `_entries_by_function_name`'s
|
|
301
|
+
# convention exactly, or the `uncovered` check below (which needs the
|
|
302
|
+
# matching lattice) picks a *different* entry than the one
|
|
303
|
+
# `run_declared_invariants` actually evaluated properties against,
|
|
304
|
+
# producing an invariants.json where a function's checked properties and
|
|
305
|
+
# its uncovered reasons come from two different recorded calls.
|
|
306
|
+
entry_key_by_function: dict[str, str] = {}
|
|
307
|
+
for key, entry in document.get("entries", {}).items():
|
|
308
|
+
entry_key_by_function.setdefault(entry.get("name") or key, key)
|
|
309
|
+
|
|
310
|
+
# Structural properties, grouped by function name.
|
|
311
|
+
structural_by_function: dict[str, list] = {}
|
|
312
|
+
for outcome in structural_results.outcomes:
|
|
313
|
+
structural_by_function.setdefault(outcome.function, []).append(outcome)
|
|
314
|
+
|
|
315
|
+
# Every function with a built lattice is a candidate for `uncovered`
|
|
316
|
+
# (an unswept numeric parameter is a property of the *lattice*, spec
|
|
317
|
+
# section 3.4, independent of whether any property is actually declared
|
|
318
|
+
# for that function) -- so this must not be limited to functions that
|
|
319
|
+
# already have a structural/declared outcome, or a function with zero
|
|
320
|
+
# declared invariants and an undeclared range (e.g. petro_api's
|
|
321
|
+
# `tubing_bhp`) would never be reported.
|
|
322
|
+
lattice_functions = {
|
|
323
|
+
(entry.get("name") or key)
|
|
324
|
+
for key, entry in document.get("entries", {}).items()
|
|
325
|
+
if key in lattices
|
|
326
|
+
}
|
|
327
|
+
all_functions = (
|
|
328
|
+
set(structural_by_function)
|
|
329
|
+
| set(declared_reports)
|
|
330
|
+
| set(declared_uncovered)
|
|
331
|
+
| lattice_functions
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
for fn_name in all_functions:
|
|
335
|
+
properties: list[str] = []
|
|
336
|
+
points = 0
|
|
337
|
+
passed = True
|
|
338
|
+
|
|
339
|
+
for outcome in structural_by_function.get(fn_name, []):
|
|
340
|
+
properties.append(outcome.property)
|
|
341
|
+
points = max(points, outcome.points_checked)
|
|
342
|
+
if outcome.status != "pass":
|
|
343
|
+
passed = False
|
|
344
|
+
|
|
345
|
+
for prop, sub_reports in declared_reports.get(fn_name, []):
|
|
346
|
+
properties.append(_label(prop))
|
|
347
|
+
points = max(points, sum(r.points_checked for r in sub_reports))
|
|
348
|
+
if any(not r.passed for r in sub_reports):
|
|
349
|
+
passed = False
|
|
350
|
+
|
|
351
|
+
if properties:
|
|
352
|
+
checked[fn_name] = {
|
|
353
|
+
"properties": properties,
|
|
354
|
+
"points": points,
|
|
355
|
+
"status": "pass" if passed else "fail",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# An unswept numeric parameter on this entry's lattice (spec section
|
|
359
|
+
# 3.4: "a swept parameter with no declared range is an error, not a
|
|
360
|
+
# guess, recorded under uncovered") -- true independent of whether
|
|
361
|
+
# any invariant is declared for this function at all.
|
|
362
|
+
reasons: list[str] = []
|
|
363
|
+
entry_key = entry_key_by_function.get(fn_name)
|
|
364
|
+
entry_lattice = lattices.get(entry_key) if entry_key is not None else None
|
|
365
|
+
if entry_lattice is not None and entry_lattice.unswept_parameters:
|
|
366
|
+
reasons.append(
|
|
367
|
+
"no range declared for parameter(s) "
|
|
368
|
+
+ ", ".join(entry_lattice.unswept_parameters)
|
|
369
|
+
)
|
|
370
|
+
reasons.extend(declared_uncovered.get(fn_name, []))
|
|
371
|
+
if reasons:
|
|
372
|
+
uncovered[fn_name] = "; ".join(reasons)
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
"format": FORMAT_VERSION,
|
|
376
|
+
"service": module.name,
|
|
377
|
+
"provenance": environment if environment is not None else golden.provenance(),
|
|
378
|
+
"lattice": {
|
|
379
|
+
"points_per_sweep": n,
|
|
380
|
+
"ranges": {name: [rng.lo, rng.hi] for name, rng in verification.ranges.items()},
|
|
381
|
+
"corners": {
|
|
382
|
+
fn_name: [list(corner) for corner in fn_corners]
|
|
383
|
+
for fn_name, fn_corners in (corners or {}).items()
|
|
384
|
+
},
|
|
385
|
+
"scatter": {"count": scatter_count, "seed": scatter_seed},
|
|
386
|
+
},
|
|
387
|
+
"state": {
|
|
388
|
+
"setup": list(verification.state.setup),
|
|
389
|
+
"mutating": list(verification.state.mutating),
|
|
390
|
+
"error_flag": verification.state.error_flag,
|
|
391
|
+
},
|
|
392
|
+
# Insertion order follows the entries actually checked -- sorted by
|
|
393
|
+
# name for a stable, reviewable diff (unlike golden.json, call order
|
|
394
|
+
# carries no meaning here: nothing in this file replays a sequence).
|
|
395
|
+
"checked": dict(sorted(checked.items())),
|
|
396
|
+
"uncovered": dict(sorted(uncovered.items())),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# --- the public entry point --------------------------------------------------
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def verify_invariants(
|
|
404
|
+
module: "ModuleIR",
|
|
405
|
+
document: dict,
|
|
406
|
+
verification: VerificationConfig,
|
|
407
|
+
package,
|
|
408
|
+
target: si.SubprocessTarget,
|
|
409
|
+
*,
|
|
410
|
+
n: int | None = None,
|
|
411
|
+
points_limit: int | None = None,
|
|
412
|
+
scatter_seed: int | None = None,
|
|
413
|
+
scatter_count: int | None = None,
|
|
414
|
+
corners: dict[str, list] | None = None,
|
|
415
|
+
environment: dict | None = None,
|
|
416
|
+
python_executable: str | None = None,
|
|
417
|
+
) -> InvariantsResult:
|
|
418
|
+
"""Run structural (T10) + declared (T11) properties and assemble the
|
|
419
|
+
`invariants.json` document (spec section 3.7).
|
|
420
|
+
|
|
421
|
+
`n`/`scatter_seed`/`scatter_count`/`corners` default to `None`, meaning
|
|
422
|
+
"read it from `verification.lattice`" -- see the module docstring and
|
|
423
|
+
`config.LatticeConfig`. An explicit argument always overrides the
|
|
424
|
+
declared config. `n` in particular also controls how many points
|
|
425
|
+
`si.build_lattices` (and therefore every structural/declared check)
|
|
426
|
+
actually evaluates, not just what is reported in the document.
|
|
427
|
+
|
|
428
|
+
Raises `EmptyCheckedError` if the resulting `checked` block is empty --
|
|
429
|
+
per spec, that must never be reported as a pass. Otherwise returns an
|
|
430
|
+
`InvariantsResult` whose `.passed` reflects every checked entry's status
|
|
431
|
+
and whose `.document` is ready to be written with `write()`.
|
|
432
|
+
"""
|
|
433
|
+
n = _resolve_n(n, verification)
|
|
434
|
+
|
|
435
|
+
# `si.build_lattices` reads corners/scatter straight off
|
|
436
|
+
# `verification.lattice` -- it has no override parameters of its own. If
|
|
437
|
+
# this call's explicit `scatter_seed`/`scatter_count`/`corners` differ
|
|
438
|
+
# from what's declared, build the lattices against an overridden copy of
|
|
439
|
+
# `verification` so the points actually checked below match what
|
|
440
|
+
# `build_invariants_document` (called with these same explicit
|
|
441
|
+
# arguments, further down) reports as having been checked -- otherwise
|
|
442
|
+
# the report would describe a sweep this call never actually ran.
|
|
443
|
+
lattice_verification = verification
|
|
444
|
+
if scatter_seed is not None or scatter_count is not None or corners is not None:
|
|
445
|
+
effective_scatter = replace(
|
|
446
|
+
verification.lattice.scatter,
|
|
447
|
+
seed=verification.lattice.scatter.seed if scatter_seed is None else scatter_seed,
|
|
448
|
+
count=verification.lattice.scatter.count if scatter_count is None else scatter_count,
|
|
449
|
+
)
|
|
450
|
+
effective_lattice = replace(
|
|
451
|
+
verification.lattice,
|
|
452
|
+
scatter=effective_scatter,
|
|
453
|
+
corners=verification.lattice.corners if corners is None else corners,
|
|
454
|
+
)
|
|
455
|
+
lattice_verification = replace(verification, lattice=effective_lattice)
|
|
456
|
+
|
|
457
|
+
functions_by_name = {fn.name: fn for fn in module.functions}
|
|
458
|
+
lattices = si.build_lattices(document, functions_by_name, lattice_verification, n=n)
|
|
459
|
+
|
|
460
|
+
structural_results = si.run_structural_invariants(
|
|
461
|
+
module,
|
|
462
|
+
document,
|
|
463
|
+
verification,
|
|
464
|
+
package,
|
|
465
|
+
target,
|
|
466
|
+
lattices=lattices,
|
|
467
|
+
n=n,
|
|
468
|
+
points_limit=points_limit,
|
|
469
|
+
python_executable=python_executable,
|
|
470
|
+
)
|
|
471
|
+
declared_reports, declared_uncovered = run_declared_invariants(
|
|
472
|
+
document, functions_by_name, verification, package, lattices, points_limit=points_limit
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
invariants_document = build_invariants_document(
|
|
476
|
+
module,
|
|
477
|
+
document,
|
|
478
|
+
verification,
|
|
479
|
+
structural_results,
|
|
480
|
+
declared_reports,
|
|
481
|
+
declared_uncovered,
|
|
482
|
+
lattices,
|
|
483
|
+
n=n,
|
|
484
|
+
scatter_seed=scatter_seed,
|
|
485
|
+
scatter_count=scatter_count,
|
|
486
|
+
corners=corners,
|
|
487
|
+
environment=environment,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if not invariants_document["checked"]:
|
|
491
|
+
raise EmptyCheckedError(
|
|
492
|
+
"invariants run checked nothing -- an invariants file that "
|
|
493
|
+
"silently checks nothing must not look like a pass "
|
|
494
|
+
"(design-verification-layers.md section 3.7). Declare `ranges:` "
|
|
495
|
+
"for at least one numeric parameter, or `state`/`invariants` in "
|
|
496
|
+
"nativegate.yaml, before running `ngate invariants verify`."
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
passed = all(v["status"] == "pass" for v in invariants_document["checked"].values())
|
|
500
|
+
|
|
501
|
+
failure_messages: list[str] = []
|
|
502
|
+
for outcome in structural_results.failures():
|
|
503
|
+
for point_failure in outcome.failures:
|
|
504
|
+
failure_messages.append(
|
|
505
|
+
f"{outcome.property} `{outcome.function}`: {point_failure.detail}"
|
|
506
|
+
)
|
|
507
|
+
for reports in declared_reports.values():
|
|
508
|
+
for _prop, sub_reports in reports:
|
|
509
|
+
for r in sub_reports:
|
|
510
|
+
if not r.passed:
|
|
511
|
+
failure_messages.append(r.message)
|
|
512
|
+
|
|
513
|
+
return InvariantsResult(
|
|
514
|
+
document=invariants_document, passed=passed, failure_messages=failure_messages
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# --- file I/O (matching golden.write()'s conventions exactly) --------------
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def write(path: Path, document: dict) -> None:
|
|
522
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
523
|
+
path.write_text(json.dumps(document, indent=2, sort_keys=False) + "\n")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def read(path: Path) -> dict:
|
|
527
|
+
return json.loads(path.read_text())
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def coverage(document: dict) -> tuple[int, int]:
|
|
531
|
+
"""(checked, uncovered) counts -- mirrors `golden.coverage()`."""
|
|
532
|
+
return len(document.get("checked") or {}), len(document.get("uncovered") or {})
|