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.
Files changed (50) hide show
  1. nativegate/__init__.py +1 -0
  2. nativegate/__main__.py +4 -0
  3. nativegate/buildinfo.py +344 -0
  4. nativegate/cli.py +2007 -0
  5. nativegate/config.py +991 -0
  6. nativegate/declared_invariants.py +565 -0
  7. nativegate/discovery.py +167 -0
  8. nativegate/driverbuild.py +626 -0
  9. nativegate/drivers/__init__.py +5 -0
  10. nativegate/drivers/cpp.py +616 -0
  11. nativegate/drivers/fortran.py +507 -0
  12. nativegate/generators/__init__.py +0 -0
  13. nativegate/generators/cmake_gen.py +101 -0
  14. nativegate/generators/docker_gen.py +614 -0
  15. nativegate/generators/error_gen.py +104 -0
  16. nativegate/generators/f2py_gen.py +91 -0
  17. nativegate/generators/gateway_gen.py +110 -0
  18. nativegate/generators/golden_gen.py +50 -0
  19. nativegate/generators/k8s_gen.py +212 -0
  20. nativegate/generators/mcp_gen.py +281 -0
  21. nativegate/generators/middleware_gen.py +717 -0
  22. nativegate/generators/pybind_gen.py +406 -0
  23. nativegate/generators/pyproject_gen.py +61 -0
  24. nativegate/generators/python_pkg_gen.py +1164 -0
  25. nativegate/generators/test_gen.py +160 -0
  26. nativegate/golden.py +747 -0
  27. nativegate/invariants.py +532 -0
  28. nativegate/ir.py +789 -0
  29. nativegate/lattice.py +350 -0
  30. nativegate/locking.py +216 -0
  31. nativegate/oracle.py +904 -0
  32. nativegate/parsers/__init__.py +0 -0
  33. nativegate/parsers/cpp.py +105 -0
  34. nativegate/parsers/cpp_ast.py +1652 -0
  35. nativegate/parsers/cpp_regex.py +812 -0
  36. nativegate/parsers/fixed_form.py +868 -0
  37. nativegate/parsers/fortran.py +157 -0
  38. nativegate/parsers/fortran_fparser.py +1116 -0
  39. nativegate/parsers/fortran_regex.py +686 -0
  40. nativegate/preprocess.py +335 -0
  41. nativegate/structural_invariants.py +762 -0
  42. nativegate/suggest.py +208 -0
  43. nativegate/templates/__init__.py +20 -0
  44. nativegate/templates/golden_test_template.py +248 -0
  45. nativegate/wire.py +438 -0
  46. nativegate-0.1.0.dist-info/METADATA +547 -0
  47. nativegate-0.1.0.dist-info/RECORD +50 -0
  48. nativegate-0.1.0.dist-info/WHEEL +5 -0
  49. nativegate-0.1.0.dist-info/entry_points.txt +3 -0
  50. 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
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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()