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/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
nativegate/__main__.py
ADDED
nativegate/buildinfo.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""What the build actually did: extracted flags, safety gate, pinned env.
|
|
2
|
+
|
|
3
|
+
Layer 2 (the oracle, `design-verification-layers.md` section 2) makes a
|
|
4
|
+
bitwise claim — the driver and the extension must be *the same machine code*
|
|
5
|
+
called two ways. That claim is only as good as the flags it is checked
|
|
6
|
+
against, and checking it against `nativegate.yaml` or any other restated
|
|
7
|
+
configuration measures the configuration, not the build: a flag can be added
|
|
8
|
+
in a CMake cache variable, a toolchain file, an environment default, or a
|
|
9
|
+
compiler wrapper without ever touching the files this tool wrote. So this
|
|
10
|
+
module reads the build system's own record of what it ran —
|
|
11
|
+
`compile_commands.json`, the de-facto standard JSON Compilation Database that
|
|
12
|
+
CMake (with `CMAKE_EXPORT_COMPILE_COMMANDS=ON`) and Meson (which `f2py -c
|
|
13
|
+
--backend meson` drives, and which writes one unconditionally into its build
|
|
14
|
+
directory) both produce — and extracts flags from *that*.
|
|
15
|
+
|
|
16
|
+
Four things live here, matching spec sections 2.3, 2.8 and 4 rules 2-3:
|
|
17
|
+
|
|
18
|
+
* **Extraction** (`load_compile_commands`, `flags_for_source`): turn a
|
|
19
|
+
`compile_commands.json` entry for one source file into a flat flag list,
|
|
20
|
+
compiler executable and output/source bookkeeping tokens stripped out.
|
|
21
|
+
* **The safety gate** (`refuse_unsafe`): `-ffast-math`, `-Ofast` and
|
|
22
|
+
`-funsafe-math-optimizations` discard IEEE semantics, and a bitwise
|
|
23
|
+
comparison under them proves nothing. This is a hard error, checked against
|
|
24
|
+
the extracted flags, never a warning and never checked against config.
|
|
25
|
+
* **The codegen subset** (`codegen_flags`): the flags that can move a bit
|
|
26
|
+
pattern for otherwise-identical source — optimization level, FP
|
|
27
|
+
contraction, target/arch, and `-std=`. T4 compiles the driver translation
|
|
28
|
+
unit with exactly this subset of the extension's own extracted flags, and
|
|
29
|
+
diffs it against a driver-flags record to catch divergence before it shows
|
|
30
|
+
up as an unexplained last-bit mismatch.
|
|
31
|
+
* **The pinned environment** (`pinned_environment`): the harness *sets*
|
|
32
|
+
`OMP_NUM_THREADS=1` and the BLAS thread-count equivalents in both processes
|
|
33
|
+
it runs — it does not check whether they are already set and refuse if not.
|
|
34
|
+
Reduction order changes bits, and there is no configuration in which the
|
|
35
|
+
harness wants more than one thread; setting is strictly better than
|
|
36
|
+
refusing, because a refusal would require every caller to already know to
|
|
37
|
+
set these before the harness ever gets a chance to.
|
|
38
|
+
|
|
39
|
+
No new runtime dependency: `compile_commands.json` is parsed with `json` and
|
|
40
|
+
`shlex`, both stdlib, and hashing is `hashlib.sha256` — the same primitives
|
|
41
|
+
`golden.py` already uses for source digests.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import hashlib
|
|
47
|
+
import json
|
|
48
|
+
import shlex
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Sequence
|
|
51
|
+
|
|
52
|
+
# spec design-verification-layers.md section 2.8 / section 4 rule 2 — checked
|
|
53
|
+
# against extracted flags, never against nativegate.yaml or any other config.
|
|
54
|
+
_UNSAFE_FLAGS = frozenset({
|
|
55
|
+
"-ffast-math",
|
|
56
|
+
"-Ofast",
|
|
57
|
+
"-funsafe-math-optimizations",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
# Prefixes of flags that affect code generation for otherwise-identical
|
|
61
|
+
# source (spec section 2.3's "the extracted flags" and T4's "differ in any
|
|
62
|
+
# way that affects code generation"). Exact-match entries are compared
|
|
63
|
+
# case-sensitively against a whole token; prefix entries match `str.startswith`.
|
|
64
|
+
#
|
|
65
|
+
# -O<n>/-Os/-Og/-Ofast optimization level (the codegen the compiler picks)
|
|
66
|
+
# -ffast-math, -funsafe-* also codegen-affecting, but they are refused
|
|
67
|
+
# outright by `refuse_unsafe` rather than compared —
|
|
68
|
+
# they never reach a driver build to diverge on.
|
|
69
|
+
# -ffp-contract=, -mfma FP contraction / fused multiply-add selection
|
|
70
|
+
# -march=, -mtune=, -mcpu=, -target, --target=, -arch target/ISA selection
|
|
71
|
+
# -std= language standard, which can change constant
|
|
72
|
+
# folding and intrinsic selection
|
|
73
|
+
_CODEGEN_EXACT = frozenset({
|
|
74
|
+
"-Os",
|
|
75
|
+
"-Og",
|
|
76
|
+
})
|
|
77
|
+
_CODEGEN_PREFIXES = (
|
|
78
|
+
"-O", # -O0 .. -O3, -Ofast (also unsafe; caught by refuse_unsafe too)
|
|
79
|
+
"-ffp-contract=",
|
|
80
|
+
"-mfma",
|
|
81
|
+
"-march=",
|
|
82
|
+
"-mtune=",
|
|
83
|
+
"-mcpu=",
|
|
84
|
+
"-target",
|
|
85
|
+
"--target=",
|
|
86
|
+
"-arch",
|
|
87
|
+
"-std=",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Tokens that are part of the compile *invocation* rather than a flag: the
|
|
91
|
+
# "-c" mode switch and the "-o <file>" output pair. The compiler executable
|
|
92
|
+
# (argv[0]) and the source file argument are stripped by position/identity
|
|
93
|
+
# in `_strip_invocation`, not here, since they carry no flag prefix to match.
|
|
94
|
+
_INVOCATION_ONLY = frozenset({"-c"})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class UnsafeFlagError(RuntimeError):
|
|
98
|
+
"""Raised by `refuse_unsafe` when a fast-math flag is present.
|
|
99
|
+
|
|
100
|
+
A hard error, not a warning (spec section 2.8): the oracle's whole claim
|
|
101
|
+
is that no tolerance is needed because the same machine code produced
|
|
102
|
+
both sides, and a fast-math flag makes that claim false regardless of
|
|
103
|
+
what the bits happen to show on a given run.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_compile_commands(path: Path) -> list[dict]:
|
|
108
|
+
"""Parse a JSON Compilation Database.
|
|
109
|
+
|
|
110
|
+
Just `json.loads` plus a clearer error: a missing or malformed
|
|
111
|
+
`compile_commands.json` should say so, not surface as a bare
|
|
112
|
+
`JSONDecodeError` deep in a caller that has no idea what file it was
|
|
113
|
+
reading.
|
|
114
|
+
"""
|
|
115
|
+
path = Path(path)
|
|
116
|
+
try:
|
|
117
|
+
text = path.read_text()
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
raise FileNotFoundError(f"no compile_commands.json at {path}") from exc
|
|
120
|
+
try:
|
|
121
|
+
data = json.loads(text)
|
|
122
|
+
except json.JSONDecodeError as exc:
|
|
123
|
+
raise ValueError(f"{path} is not valid JSON: {exc}") from exc
|
|
124
|
+
if not isinstance(data, list):
|
|
125
|
+
raise ValueError(f"{path}: expected a JSON array of compile command entries")
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def entry_argv(entry: dict) -> list[str]:
|
|
130
|
+
"""The full argv for one compile command entry, compiler included.
|
|
131
|
+
|
|
132
|
+
CMake's `CMAKE_EXPORT_COMPILE_COMMANDS` and Meson (and therefore `f2py -c
|
|
133
|
+
--backend meson`, which shells out to Meson/ninja) both write a `command`
|
|
134
|
+
field holding a single shell-quoted string. Newer CMake can instead write
|
|
135
|
+
an `arguments` array directly. Support both rather than assuming one.
|
|
136
|
+
|
|
137
|
+
Public (T4/T5/cli.py all need this same argv-from-entry logic) — kept
|
|
138
|
+
under this name rather than duplicated per caller, per the dedup called
|
|
139
|
+
out in the verification-layers cleanup pass.
|
|
140
|
+
"""
|
|
141
|
+
if "arguments" in entry:
|
|
142
|
+
return list(entry["arguments"])
|
|
143
|
+
if "command" in entry:
|
|
144
|
+
return shlex.split(entry["command"])
|
|
145
|
+
raise ValueError(f"compile command entry has neither 'command' nor 'arguments': {entry!r}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _matches_source(entry: dict, source_name: str) -> bool:
|
|
149
|
+
"""Match an entry to a requested source by filename.
|
|
150
|
+
|
|
151
|
+
Entries record `file` as a path — sometimes absolute, sometimes relative
|
|
152
|
+
to `directory`, and in a driver build the requested name may be given as
|
|
153
|
+
a bare filename (`"pvtcor.f"`) without knowing which of those forms the
|
|
154
|
+
build recorded. Matching on the final path component is the only thing
|
|
155
|
+
guaranteed stable across CMake/Meson's differing path conventions; a
|
|
156
|
+
caller wanting to disambiguate identically-named sources in different
|
|
157
|
+
directories should match on the full path it already knows and pass that
|
|
158
|
+
through instead of relying on this alone.
|
|
159
|
+
"""
|
|
160
|
+
entry_file = entry.get("file", "")
|
|
161
|
+
return Path(entry_file).name == Path(source_name).name
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def find_entry(commands: Sequence[dict], source_name: str) -> dict:
|
|
165
|
+
"""The single compile command entry for `source_name`.
|
|
166
|
+
|
|
167
|
+
Raises `KeyError` naming the file actually being looked for and the
|
|
168
|
+
filenames that *were* recorded, so a mismatch (wrong service directory,
|
|
169
|
+
stale compile_commands.json) is diagnosable rather than a bare "not
|
|
170
|
+
found".
|
|
171
|
+
"""
|
|
172
|
+
matches = [entry for entry in commands if _matches_source(entry, source_name)]
|
|
173
|
+
if not matches:
|
|
174
|
+
available = sorted({Path(e.get("file", "")).name for e in commands})
|
|
175
|
+
raise KeyError(
|
|
176
|
+
f"no compile command for {source_name!r} in compile_commands.json "
|
|
177
|
+
f"(recorded sources: {available})"
|
|
178
|
+
)
|
|
179
|
+
if len(matches) > 1:
|
|
180
|
+
# Two sources with the same basename in different directories. T1's
|
|
181
|
+
# contract is "the extension's actual flags for each native source";
|
|
182
|
+
# ambiguity here is a caller bug (it should have disambiguated with a
|
|
183
|
+
# fuller path), not something to guess at silently.
|
|
184
|
+
available = [entry.get("file", "") for entry in matches]
|
|
185
|
+
raise KeyError(
|
|
186
|
+
f"{source_name!r} matches more than one compile command entry: {available}"
|
|
187
|
+
)
|
|
188
|
+
return matches[0]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def flags_for_source(commands: Sequence[dict], source_name: str) -> list[str]:
|
|
192
|
+
"""The extracted, order-preserving flag list for one native source.
|
|
193
|
+
|
|
194
|
+
Strips the compiler executable (argv[0]), the `-c` mode switch, the `-o
|
|
195
|
+
<output>` pair, and the source file argument itself, leaving exactly the
|
|
196
|
+
flags — the same list `refuse_unsafe` and `codegen_flags` are meant to be
|
|
197
|
+
called with. Order is preserved as the build system recorded it; nothing
|
|
198
|
+
here re-sorts or de-duplicates, because flag order is sometimes
|
|
199
|
+
semantically meaningful (an `-I` search path list, a later `-O` winning
|
|
200
|
+
over an earlier one) and re-ordering would misrepresent what the compiler
|
|
201
|
+
actually saw.
|
|
202
|
+
"""
|
|
203
|
+
entry = find_entry(commands, source_name)
|
|
204
|
+
argv = entry_argv(entry)
|
|
205
|
+
if not argv:
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
source_file = entry.get("file", "")
|
|
209
|
+
flags: list[str] = []
|
|
210
|
+
skip_next = False
|
|
211
|
+
for i, token in enumerate(argv):
|
|
212
|
+
if i == 0:
|
|
213
|
+
continue # compiler executable
|
|
214
|
+
if skip_next:
|
|
215
|
+
skip_next = False
|
|
216
|
+
continue
|
|
217
|
+
if token in _INVOCATION_ONLY:
|
|
218
|
+
continue
|
|
219
|
+
if token == "-o":
|
|
220
|
+
skip_next = True
|
|
221
|
+
continue
|
|
222
|
+
if token == source_file or Path(token).name == Path(source_file).name:
|
|
223
|
+
continue
|
|
224
|
+
flags.append(token)
|
|
225
|
+
return flags
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def extract_flags(compile_commands_path: Path, source_name: str) -> list[str]:
|
|
229
|
+
"""Convenience: load + find + strip in one call."""
|
|
230
|
+
commands = load_compile_commands(Path(compile_commands_path))
|
|
231
|
+
return flags_for_source(commands, source_name)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --- the safety gate ------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def refuse_unsafe(flags: Sequence[str]) -> None:
|
|
238
|
+
"""Hard error if a fast-math flag is present. Never a warning.
|
|
239
|
+
|
|
240
|
+
Checked by exact token match, not substring, so a hypothetical
|
|
241
|
+
`-fmy-ffast-math-thing` (not a real GCC/Clang flag, but the principle
|
|
242
|
+
holds for anything that merely contains the substring) does not trip a
|
|
243
|
+
check meant for the literal flag.
|
|
244
|
+
"""
|
|
245
|
+
present = sorted(set(flags) & _UNSAFE_FLAGS)
|
|
246
|
+
if present:
|
|
247
|
+
raise UnsafeFlagError(
|
|
248
|
+
"refusing to run the oracle: unsafe floating-point flag(s) "
|
|
249
|
+
f"{present} discard IEEE semantics, so a bitwise comparison "
|
|
250
|
+
"under them would prove nothing (design-verification-layers.md "
|
|
251
|
+
"section 2.8)"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# --- the codegen subset ----------------------------------------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# "-target" and "-arch" (unlike "-march="/"-mtune="/"--target=", which carry
|
|
259
|
+
# their value after "=" in the same token) take the value as a *separate*
|
|
260
|
+
# following argv token ("-target x86_64-apple-darwin", "-arch arm64"). Both
|
|
261
|
+
# tokens have to travel together or the subset is unparseable nonsense.
|
|
262
|
+
_CODEGEN_TAKES_NEXT_TOKEN = frozenset({"-target", "-arch"})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def codegen_flags(flags: Sequence[str]) -> list[str]:
|
|
266
|
+
"""The subset of `flags` that can move a bit pattern for identical source.
|
|
267
|
+
|
|
268
|
+
Order-preserving subsequence of the input — used by T4 to compile the
|
|
269
|
+
driver TU with the same codegen-affecting flags as the extension, and to
|
|
270
|
+
detect divergence by comparing this subset rather than the full
|
|
271
|
+
(much noisier — include paths, warnings, dependency-file flags) list.
|
|
272
|
+
"""
|
|
273
|
+
result = []
|
|
274
|
+
take_next = False
|
|
275
|
+
for flag in flags:
|
|
276
|
+
if take_next:
|
|
277
|
+
result.append(flag)
|
|
278
|
+
take_next = False
|
|
279
|
+
continue
|
|
280
|
+
if flag in _CODEGEN_EXACT or flag.startswith(_CODEGEN_PREFIXES):
|
|
281
|
+
result.append(flag)
|
|
282
|
+
if flag in _CODEGEN_TAKES_NEXT_TOKEN:
|
|
283
|
+
take_next = True
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# --- the pinned environment -------------------------------------------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def pinned_environment() -> dict[str, str]:
|
|
291
|
+
"""The env dict the harness runs native code under, in both processes.
|
|
292
|
+
|
|
293
|
+
Spec design-verification-layers.md section 2.8 / section 4 rule 3: the
|
|
294
|
+
harness *sets* these rather than checking they are already set and
|
|
295
|
+
refusing otherwise — reduction order changes bits, and there is no
|
|
296
|
+
configuration in which the harness wants more than one thread, so there
|
|
297
|
+
is nothing to legitimately opt out of.
|
|
298
|
+
"""
|
|
299
|
+
return {
|
|
300
|
+
"OMP_NUM_THREADS": "1",
|
|
301
|
+
"OPENBLAS_NUM_THREADS": "1",
|
|
302
|
+
"MKL_NUM_THREADS": "1",
|
|
303
|
+
"VECLIB_MAXIMUM_THREADS": "1",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- hashes, for provenance -------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def flags_hash(flags: Sequence[str]) -> str:
|
|
311
|
+
"""SHA-256 of the extracted flag list, in the order given.
|
|
312
|
+
|
|
313
|
+
"Canonical ordering" here means the order the build system itself
|
|
314
|
+
recorded (`flags_for_source`'s output), not a re-sort: flag order can be
|
|
315
|
+
semantically meaningful (repeated `-I`/`-D`, a later `-O` overriding an
|
|
316
|
+
earlier one), and hashing a sorted copy would call two builds identical
|
|
317
|
+
when the compiler would not have treated them that way. Callers that
|
|
318
|
+
want the hash to be stable across two extractions of the *same* build
|
|
319
|
+
get that for free, because a JSON Compilation Database's `command`/
|
|
320
|
+
`arguments` for one source is itself deterministic.
|
|
321
|
+
"""
|
|
322
|
+
payload = "\n".join(flags).encode("utf-8")
|
|
323
|
+
return hashlib.sha256(payload).hexdigest()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def link_target_hash(paths: Sequence[Path]) -> str:
|
|
327
|
+
"""SHA-256 over a set of linked archive/object files, for provenance.
|
|
328
|
+
|
|
329
|
+
Unlike `flags_hash`, the input here is a *set* of files (the objects/
|
|
330
|
+
archive the driver links against, per spec section 2.3) rather than an
|
|
331
|
+
ordered sequence with meaning of its own, so this sorts by path first —
|
|
332
|
+
the hash should not depend on the order a glob happened to return them
|
|
333
|
+
in. Each file's path (relative form, as given) and content both feed the
|
|
334
|
+
digest, so renaming one of two identical-content objects changes the
|
|
335
|
+
hash — appropriate for provenance, where "which file" is part of what is
|
|
336
|
+
being attested to.
|
|
337
|
+
"""
|
|
338
|
+
hasher = hashlib.sha256()
|
|
339
|
+
for path in sorted((Path(p) for p in paths), key=str):
|
|
340
|
+
hasher.update(str(path).encode("utf-8"))
|
|
341
|
+
hasher.update(b"\0")
|
|
342
|
+
hasher.update(path.read_bytes())
|
|
343
|
+
hasher.update(b"\0")
|
|
344
|
+
return hasher.hexdigest()
|