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
@@ -0,0 +1,335 @@
1
+ """Fortran INCLUDE expansion.
2
+
3
+ Legacy F77 keeps its COMMON blocks and PARAMETERs in .INC files pulled in
4
+ with `INCLUDE 'PETRO.INC'` inside each routine body. f2py accepts an
5
+ --include-paths flag and *appears* to handle these, but the wrapper it
6
+ generates for a routine containing an in-body INCLUDE is silently broken:
7
+ arguments never reach the Fortran side, so calls return uninitialized
8
+ memory instead of an error. Verified against libraries/petro:
9
+
10
+ INCLUDE 'PETRO.INC' present -> probe1(35.0) == 1.09e-314 (garbage)
11
+ same IMPLICIT rules, no INCLUDE -> probe1(35.0) == 70.0 (correct)
12
+ INCLUDE expanded textually -> probe1(35.0) == 70.0 (correct)
13
+
14
+ So nativegate expands INCLUDEs itself and hands f2py a flat source file.
15
+ This is a correctness requirement, not an optimization — without it the
16
+ bindings compile, import, and return plausible-looking wrong numbers.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ import warnings
23
+ from pathlib import Path
24
+
25
+ _INCLUDE_RE = re.compile(r"^\s*INCLUDE\s+['\"]([^'\"]+)['\"]", re.IGNORECASE)
26
+
27
+ MAX_INCLUDE_DEPTH = 25
28
+
29
+
30
+ class IncludeError(ValueError):
31
+ """An INCLUDE could not be resolved against the configured search path."""
32
+
33
+
34
+ class EncodingError(ValueError):
35
+ """A source file could not be decoded with the encoding the user named."""
36
+
37
+
38
+ class SourceEncodingWarning(UserWarning):
39
+ """A source file was not valid UTF-8 and was decoded as latin-1 instead."""
40
+
41
+
42
+ # Comment styles for the marker lines expand_includes writes around an inlined
43
+ # file. A `C` in column 1 is a comment in FIXED-form Fortran only; in free-form
44
+ # it is a syntax error, so free-form callers need `!`.
45
+ COMMENT_STYLES = {"fixed": "C ", "free": "! "}
46
+
47
+ DEFAULT_FALLBACK_ENCODING = "latin-1"
48
+
49
+
50
+ def read_source(path: Path, encoding: str | None = None) -> str:
51
+ """Decode `path` honestly.
52
+
53
+ UTF-8 is tried first. If it fails, the file is decoded as latin-1 — which
54
+ never fails, and is the right guess for decks with VMS/mainframe lineage —
55
+ and the fallback is reported as a `SourceEncodingWarning` rather than
56
+ swallowed. Passing `encoding` states the real encoding; a decode failure
57
+ with an explicit encoding is an error, never a silent substitution.
58
+
59
+ Undecodable bytes are never replaced with U+FFFD.
60
+ """
61
+ raw = path.read_bytes()
62
+ if encoding is not None:
63
+ try:
64
+ return raw.decode(encoding)
65
+ except (UnicodeDecodeError, LookupError) as exc:
66
+ raise EncodingError(
67
+ f"{path}: cannot decode as '{encoding}': {exc}. "
68
+ "Set the correct encoding for this source."
69
+ ) from exc
70
+
71
+ try:
72
+ return raw.decode("utf-8")
73
+ except UnicodeDecodeError as exc:
74
+ text = raw.decode(DEFAULT_FALLBACK_ENCODING)
75
+ warnings.warn(
76
+ f"{path}: not valid UTF-8 ({exc.reason} at byte {exc.start}); "
77
+ f"decoded as {DEFAULT_FALLBACK_ENCODING} instead. If that is wrong, "
78
+ "state the real encoding explicitly.",
79
+ SourceEncodingWarning,
80
+ stacklevel=2,
81
+ )
82
+ return text
83
+
84
+
85
+ def _resolve(name: str, search_paths: list[Path]) -> Path | None:
86
+ for directory in search_paths:
87
+ candidate = directory / name
88
+ if candidate.is_file():
89
+ return candidate
90
+ # Legacy sources are inconsistent about case ('petro.inc' vs 'PETRO.INC'),
91
+ # which matters on case-sensitive filesystems even when it worked on the
92
+ # original VMS/Windows host.
93
+ for existing in directory.iterdir() if directory.is_dir() else []:
94
+ if existing.is_file() and existing.name.lower() == name.lower():
95
+ return existing
96
+ return None
97
+
98
+
99
+ def expand_includes(
100
+ source_path: Path,
101
+ search_paths: list[Path] | None = None,
102
+ _depth: int = 0,
103
+ _stack: tuple[str, ...] = (),
104
+ *,
105
+ comment_style: str = "fixed",
106
+ encoding: str | None = None,
107
+ ) -> str:
108
+ """Return the source of `source_path` with every INCLUDE inlined.
109
+
110
+ `comment_style` selects the marker-line comment character: "fixed" (the
111
+ default, `C` in column 1) for fixed-form sources, "free" (`!`) for
112
+ free-form, where a column-1 `C` is a syntax error.
113
+ """
114
+ if comment_style not in COMMENT_STYLES:
115
+ raise ValueError(
116
+ f"Unknown comment_style {comment_style!r}; expected one of "
117
+ f"{sorted(COMMENT_STYLES)}."
118
+ )
119
+ marker = COMMENT_STYLES[comment_style]
120
+
121
+ if _depth > MAX_INCLUDE_DEPTH:
122
+ raise IncludeError(
123
+ f"INCLUDE nesting deeper than {MAX_INCLUDE_DEPTH} while expanding "
124
+ f"{source_path} (cycle? chain: {' -> '.join(_stack)})"
125
+ )
126
+
127
+ search_paths = search_paths or []
128
+ # An INCLUDE is resolved relative to the including file first, matching
129
+ # the behaviour every Fortran compiler implements.
130
+ local_paths = [source_path.parent, *search_paths]
131
+
132
+ lines: list[str] = []
133
+ for line in read_source(source_path, encoding).splitlines():
134
+ match = _INCLUDE_RE.match(line)
135
+ if not match:
136
+ lines.append(line)
137
+ continue
138
+
139
+ include_name = match.group(1)
140
+ resolved = _resolve(include_name, local_paths)
141
+ if resolved is None:
142
+ searched = ", ".join(str(p) for p in local_paths)
143
+ raise IncludeError(
144
+ f"{source_path.name}: cannot resolve INCLUDE '{include_name}'. "
145
+ f"Searched: {searched}. Add the directory to `include_paths:` in nativegate.yaml."
146
+ )
147
+
148
+ lines.append(f"{marker}--- nativegate: expanded INCLUDE '{include_name}' ---")
149
+ lines.append(
150
+ expand_includes(
151
+ resolved,
152
+ search_paths,
153
+ _depth + 1,
154
+ _stack + (source_path.name,),
155
+ comment_style=comment_style,
156
+ encoding=encoding,
157
+ )
158
+ )
159
+ lines.append(f"{marker}--- nativegate: end INCLUDE '{include_name}' ---")
160
+
161
+ return "\n".join(lines)
162
+
163
+
164
+ def find_include_files(root: Path) -> list[Path]:
165
+ """Locate .INC/.inc files under `root`, for reporting and dependency tracking."""
166
+ return sorted(
167
+ p for p in root.rglob("*") if p.is_file() and p.suffix.lower() in (".inc",)
168
+ )
169
+
170
+
171
+ # --- kind-parameter resolution ------------------------------------------
172
+ #
173
+ # f2py builds a wrapper file (`-f2pywrappers2.f90`) that redeclares each
174
+ # routine's arguments, but it does NOT carry the module's PARAMETER
175
+ # declarations across into that wrapper. So the standard modern idiom
176
+ #
177
+ # integer, parameter :: dp = kind(1.0d0)
178
+ # real(dp), intent(in) :: x
179
+ #
180
+ # compiles as `real(dp)` in a scope where `dp` does not exist. gfortran then
181
+ # reports "Parameter 'dp' has not been declared", silently degrades the
182
+ # argument to REAL(4), and the build dies ~200 lines later with a type
183
+ # mismatch that names neither `dp` nor the line that caused it.
184
+ #
185
+ # Substituting the resolved literal (`real(8)`) makes the same source build
186
+ # unchanged. This is done on a generated copy — the original file is never
187
+ # modified.
188
+
189
+ _KIND_PARAM_RE = re.compile(
190
+ r"^\s*integer\s*(?:\([^)]*\))?\s*,\s*parameter\s*::\s*(?P<assignments>.+?)\s*$",
191
+ re.IGNORECASE | re.MULTILINE,
192
+ )
193
+
194
+ # kind(1.0d0) / selected_real_kind(15, 307) / 8
195
+ _KIND_VALUE_RE = re.compile(
196
+ r"^(?P<name>\w+)\s*=\s*(?P<expr>.+?)\s*$",
197
+ )
198
+
199
+
200
+ def _kind_literal(expr: str) -> str | None:
201
+ """Resolve a kind-defining expression to its literal value."""
202
+ text = expr.strip().lower().replace(" ", "")
203
+
204
+ if text.isdigit():
205
+ return text
206
+
207
+ match = re.fullmatch(r"kind\((.+)\)", text)
208
+ if match:
209
+ literal = match.group(1)
210
+ if "d" in literal:
211
+ return "8"
212
+ if re.fullmatch(r"[\d.]+(e[-+]?\d+)?", literal):
213
+ return "4"
214
+ return None
215
+
216
+ match = re.fullmatch(r"selected_real_kind\((?P<args>.*)\)", text)
217
+ if match:
218
+ args = [a for a in match.group("args").split(",") if a]
219
+ try:
220
+ precision = int(re.sub(r"^p=", "", args[0])) if args else 6
221
+ except ValueError:
222
+ return None
223
+ return "8" if precision > 6 else "4"
224
+
225
+ match = re.fullmatch(r"selected_int_kind\((?P<args>.*)\)", text)
226
+ if match:
227
+ return None # integer kinds are not substituted; f2py handles them
228
+
229
+ return None
230
+
231
+
232
+ def resolve_kind_parameters(source: str) -> str:
233
+ """Replace `real(dp)` with `real(8)` for kind parameters defined in-file.
234
+
235
+ Only substitutes names it can resolve to a literal; anything else is left
236
+ exactly as written so an unresolvable kind still fails loudly at compile
237
+ time rather than being silently rewritten to the wrong width.
238
+ """
239
+ kinds: dict[str, str] = {}
240
+ for decl in _KIND_PARAM_RE.finditer(source):
241
+ for assignment in decl.group("assignments").split(","):
242
+ # A kind expression can itself contain a comma
243
+ # (selected_real_kind(15, 307)); only split on top-level ones.
244
+ match = _KIND_VALUE_RE.match(assignment.strip())
245
+ if not match:
246
+ continue
247
+ literal = _kind_literal(match.group("expr"))
248
+ if literal:
249
+ kinds[match.group("name").lower()] = literal
250
+
251
+ # Recover comma-split kind expressions like selected_real_kind(15, 307).
252
+ for decl in _KIND_PARAM_RE.finditer(source):
253
+ for match in re.finditer(
254
+ r"(?P<name>\w+)\s*=\s*(?P<expr>selected_real_kind\s*\([^)]*\)|kind\s*\([^)]*\))",
255
+ decl.group("assignments"),
256
+ re.IGNORECASE,
257
+ ):
258
+ literal = _kind_literal(match.group("expr"))
259
+ if literal:
260
+ kinds[match.group("name").lower()] = literal
261
+
262
+ if not kinds:
263
+ return source
264
+
265
+ def substitute(match: re.Match) -> str:
266
+ base, kind_name = match.group("base"), match.group("kind")
267
+ literal = kinds.get(kind_name.lower())
268
+ return f"{base}({literal})" if literal else match.group(0)
269
+
270
+ return re.sub(
271
+ r"\b(?P<base>real|complex|integer|logical)\s*\(\s*(?:kind\s*=\s*)?(?P<kind>[a-z_]\w*)\s*\)",
272
+ substitute,
273
+ source,
274
+ flags=re.IGNORECASE,
275
+ )
276
+
277
+
278
+ def uses_kind_parameters(source: str) -> bool:
279
+ """True if `source` would be changed by resolve_kind_parameters."""
280
+ return resolve_kind_parameters(source) != source
281
+
282
+
283
+ # --- the C preprocessor, for .F90/.F sources ------------------------------
284
+
285
+
286
+ class PreprocessError(RuntimeError):
287
+ """The C preprocessor failed or is unavailable for a source that needs it."""
288
+
289
+
290
+ def run_c_preprocessor(
291
+ source_path: Path,
292
+ include_paths: list[Path] | None = None,
293
+ defines: list[str] | None = None,
294
+ ) -> str:
295
+ """Run gfortran's preprocessor over one source and return the text.
296
+
297
+ An uppercase suffix (`.F90`, `.F`, `.FOR`) — or `#ifdef` in a lowercase
298
+ file — means the code is not Fortran yet: it is input to the C
299
+ preprocessor, and reading it directly treats every conditional branch as
300
+ simultaneously live. That is how nativegate used to read these files (with
301
+ a warning), which silently binds whichever branch happens to lex.
302
+
303
+ gfortran's own preprocessor rather than a bare `cpp`, because Fortran is
304
+ not C: a traditional cpp mangles Hollerith constants, `//` concatenation
305
+ (a C comment to cpp), and fixed-form continuation columns. `-cpp -E`
306
+ applies the preprocessor the compiler itself would use, so what nativegate
307
+ parses is what gfortran would have compiled. `-P` drops linemarkers,
308
+ which are not Fortran and would poison the parse downstream.
309
+ """
310
+ import shutil as _shutil
311
+ import subprocess as _subprocess
312
+
313
+ compiler = _shutil.which("gfortran")
314
+ if compiler is None:
315
+ raise PreprocessError(
316
+ f"{source_path.name} needs the C preprocessor (#ifdef / uppercase "
317
+ "suffix) and gfortran is not on PATH to run it. Install gfortran, "
318
+ "or preprocess the file yourself and point nativegate at the output."
319
+ )
320
+
321
+ command = [compiler, "-cpp", "-E", "-P"]
322
+ for directory in include_paths or []:
323
+ command += ["-I", str(directory)]
324
+ for define in defines or []:
325
+ command += ["-D", define]
326
+ command.append(str(source_path))
327
+
328
+ result = _subprocess.run(command, capture_output=True, text=True)
329
+ if result.returncode != 0:
330
+ raise PreprocessError(
331
+ f"gfortran -cpp -E failed on {source_path.name}:\n"
332
+ f"{result.stderr.strip()[:800]}"
333
+ )
334
+ return result.stdout
335
+