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/discovery.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Language detection and source discovery (design.md section 5, 13)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Fixed-form (.f/.for/.f77) vs free-form (.f90+) matters beyond discovery:
|
|
9
|
+
# they have different comment, continuation, and column rules, so the parser
|
|
10
|
+
# needs to know which dialect it's reading.
|
|
11
|
+
_FIXED_FORM_SUFFIXES = {".f", ".for", ".f77"}
|
|
12
|
+
_FREE_FORM_SUFFIXES = {".f90", ".f95", ".f03", ".f08"}
|
|
13
|
+
# Uppercase = "run the C preprocessor first", by decades-old convention.
|
|
14
|
+
# These were never in the table at all, so a .F90 was not merely mis-read —
|
|
15
|
+
# `find_native_sources` could not SEE it, and generate reported "No Fortran
|
|
16
|
+
# sources found" for a directory full of Fortran.
|
|
17
|
+
_PREPROCESSED_SUFFIXES = {s.upper() for s in _FIXED_FORM_SUFFIXES | _FREE_FORM_SUFFIXES}
|
|
18
|
+
|
|
19
|
+
_EXTENSIONS = {
|
|
20
|
+
".hpp": "cpp",
|
|
21
|
+
".hh": "cpp",
|
|
22
|
+
".h": "cpp",
|
|
23
|
+
".cpp": "cpp",
|
|
24
|
+
".cc": "cpp",
|
|
25
|
+
**{s: "fortran" for s in _FIXED_FORM_SUFFIXES},
|
|
26
|
+
**{s: "fortran" for s in _FREE_FORM_SUFFIXES},
|
|
27
|
+
**{s: "fortran" for s in _PREPROCESSED_SUFFIXES},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detect_language(path: Path) -> str | None:
|
|
32
|
+
return _EXTENSIONS.get(path.suffix.lower())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_fixed_form(path: Path) -> bool:
|
|
36
|
+
"""True for FORTRAN 77 fixed-form source (comment in col 1, continuation in col 6)."""
|
|
37
|
+
return path.suffix.lower() in _FIXED_FORM_SUFFIXES
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --- C-preprocessed Fortran (.F90/.F/.FOR) ------------------------------
|
|
41
|
+
#
|
|
42
|
+
# By convention an UPPERCASE Fortran suffix means the file is run through the
|
|
43
|
+
# C preprocessor before the compiler sees it. nativegate has no preprocessor,
|
|
44
|
+
# so parsing such a file treats every #ifdef branch as live — the wrong code
|
|
45
|
+
# is bound and nothing says so. detect_language deliberately still maps these
|
|
46
|
+
# by lowercased suffix; these predicates let the caller refuse or warn.
|
|
47
|
+
|
|
48
|
+
_PREPROCESSED_SUFFIXES = {".F", ".FOR", ".F77", ".F90", ".F95", ".F03", ".F08", ".FPP"}
|
|
49
|
+
|
|
50
|
+
_CPP_DIRECTIVE_RE = re.compile(
|
|
51
|
+
r"^\s*#\s*(include|define|undef|if|ifdef|ifndef|elif|else|endif|error|pragma|line)\b",
|
|
52
|
+
re.MULTILINE,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def has_uppercase_preprocessed_suffix(path: Path) -> bool:
|
|
57
|
+
"""True for the `.F90`/`.F`/`.FOR` spelling that means "run cpp first"."""
|
|
58
|
+
return path.suffix in _PREPROCESSED_SUFFIXES
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_cpp_directives(source: str) -> list[str]:
|
|
62
|
+
"""Report which C-preprocessor directives appear in `source`.
|
|
63
|
+
|
|
64
|
+
Returns the directive names in order of first appearance ("ifdef",
|
|
65
|
+
"include", ...), so a caller can say exactly what it cannot handle.
|
|
66
|
+
"""
|
|
67
|
+
found: list[str] = []
|
|
68
|
+
for match in _CPP_DIRECTIVE_RE.finditer(source):
|
|
69
|
+
name = match.group(1).lower()
|
|
70
|
+
if name not in found:
|
|
71
|
+
found.append(name)
|
|
72
|
+
return found
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def requires_preprocessing(path: Path, source: str | None = None) -> bool:
|
|
76
|
+
"""True if `path` needs the C preprocessor before it can be parsed correctly.
|
|
77
|
+
|
|
78
|
+
Either the uppercase suffix convention or the actual presence of cpp
|
|
79
|
+
directives is enough — lowercase `.f90` files with `#ifdef` in them are
|
|
80
|
+
common, and mis-parse identically.
|
|
81
|
+
"""
|
|
82
|
+
if detect_language(path) != "fortran":
|
|
83
|
+
return False
|
|
84
|
+
if has_uppercase_preprocessed_suffix(path):
|
|
85
|
+
return True
|
|
86
|
+
if source is None:
|
|
87
|
+
try:
|
|
88
|
+
source = path.read_text(errors="replace")
|
|
89
|
+
except OSError:
|
|
90
|
+
return False
|
|
91
|
+
return bool(find_cpp_directives(source))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# nativegate writes INCLUDE-expanded copies under native/_expanded/. They must
|
|
95
|
+
# never be picked up as inputs too, or the same routines get compiled twice
|
|
96
|
+
# and the link fails with "redefinition of f2py_rout_...".
|
|
97
|
+
GENERATED_DIRS = frozenset({"_expanded"})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_IMPL_SUFFIXES = (".cpp", ".cc", ".cxx")
|
|
101
|
+
|
|
102
|
+
# A header under one of these directories is assumed to belong to a project
|
|
103
|
+
# that splits declarations from definitions, so its .cpp lives elsewhere.
|
|
104
|
+
_HEADER_DIR_NAMES = frozenset({"include", "inc", "headers"})
|
|
105
|
+
_IMPL_DIR_NAMES = ("src", "source", "sources", "lib")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def find_implementation_files(header: Path) -> list[Path]:
|
|
109
|
+
"""Locate the .cpp/.cc/.cxx files that define what `header` declares.
|
|
110
|
+
|
|
111
|
+
Two layouts are handled. The simple one puts calculator.hpp and
|
|
112
|
+
calculator.cpp side by side. The conventional one — which every real C++
|
|
113
|
+
project of any size uses — splits them into include/ and src/, and
|
|
114
|
+
matching only on `header.with_suffix('.cpp')` silently misses it: the
|
|
115
|
+
service builds and then dies on first import with an undefined-symbol
|
|
116
|
+
error, because the declarations were bound but nothing defined them.
|
|
117
|
+
"""
|
|
118
|
+
found: list[Path] = []
|
|
119
|
+
seen: set[Path] = set()
|
|
120
|
+
|
|
121
|
+
def add(candidate: Path) -> None:
|
|
122
|
+
if candidate.is_file() and candidate not in seen:
|
|
123
|
+
seen.add(candidate)
|
|
124
|
+
found.append(candidate)
|
|
125
|
+
|
|
126
|
+
# Side-by-side layout.
|
|
127
|
+
for suffix in _IMPL_SUFFIXES:
|
|
128
|
+
add(header.with_suffix(suffix))
|
|
129
|
+
|
|
130
|
+
# Split layout: walk up to the nearest include/ ancestor, then look for
|
|
131
|
+
# src/ beside it. Nearest wins — for include/pkg/sub/Foo.hpp the relevant
|
|
132
|
+
# project root is the one holding include/, not any outer checkout.
|
|
133
|
+
for ancestor in header.parents:
|
|
134
|
+
if ancestor.name.lower() not in _HEADER_DIR_NAMES:
|
|
135
|
+
continue
|
|
136
|
+
for impl_dir_name in _IMPL_DIR_NAMES:
|
|
137
|
+
impl_dir = ancestor.parent / impl_dir_name
|
|
138
|
+
if not impl_dir.is_dir():
|
|
139
|
+
continue
|
|
140
|
+
for suffix in _IMPL_SUFFIXES:
|
|
141
|
+
# rglob, not glob: src/ is often subdivided to mirror include/.
|
|
142
|
+
for match in sorted(impl_dir.rglob(f"{header.stem}{suffix}")):
|
|
143
|
+
add(match)
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
return found
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def find_native_sources(root: Path, language: str) -> list[Path]:
|
|
150
|
+
suffixes = [ext for ext, lang in _EXTENSIONS.items() if lang == language]
|
|
151
|
+
found: list[Path] = []
|
|
152
|
+
# De-duplicated by identity, not by name: on a case-insensitive
|
|
153
|
+
# filesystem (macOS, Windows) rglob("*.f90") and rglob("*.F90") both
|
|
154
|
+
# match the same file, and compiling it twice is a duplicate-symbol
|
|
155
|
+
# link error. The SUFFIX the file actually has still decides whether
|
|
156
|
+
# the preprocessor runs.
|
|
157
|
+
seen: set[Path] = set()
|
|
158
|
+
for suffix in suffixes:
|
|
159
|
+
for p in sorted(root.rglob(f"*{suffix}")):
|
|
160
|
+
if GENERATED_DIRS.intersection(p.parts):
|
|
161
|
+
continue
|
|
162
|
+
resolved = p.resolve()
|
|
163
|
+
if resolved in seen:
|
|
164
|
+
continue
|
|
165
|
+
seen.add(resolved)
|
|
166
|
+
found.append(p)
|
|
167
|
+
return found
|