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,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