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/preprocess.py
ADDED
|
@@ -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
|
+
|