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
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""pybind11 binding generator (design.md section 7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ..ir import ClassDef, FunctionDef, Method, ModuleIR, Parameter, StructDef
|
|
8
|
+
|
|
9
|
+
# Python type name -> the C++ spelling pybind11 needs in an init<> signature.
|
|
10
|
+
# Only the scalar types need translating; record types are already spelled
|
|
11
|
+
# with their C++ name in the IR.
|
|
12
|
+
_CPP_SPELLING = {
|
|
13
|
+
"int": "int",
|
|
14
|
+
"float": "double",
|
|
15
|
+
"bool": "bool",
|
|
16
|
+
"str": "std::string",
|
|
17
|
+
"None": "void",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _qualified(name: str, namespace: str | None) -> str:
|
|
22
|
+
return f"{namespace}::{name}" if namespace else name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Type spellings that are already complete on their own: qualifying them with
|
|
26
|
+
# the enclosing namespace would invent `petro::double`.
|
|
27
|
+
_BUILTIN_WORDS = {
|
|
28
|
+
"void", "bool", "char", "wchar_t", "char8_t", "char16_t", "char32_t",
|
|
29
|
+
"short", "int", "long", "float", "double", "signed", "unsigned",
|
|
30
|
+
"const", "volatile", "auto",
|
|
31
|
+
}
|
|
32
|
+
_STDINT_RE = re.compile(r"^(u?int(_least|_fast)?(8|16|32|64)_t|u?intptr_t|u?intmax_t|size_t|ssize_t|ptrdiff_t)$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _needs_qualification(spelling: str) -> bool:
|
|
36
|
+
"""True when `spelling` names a user type written without its namespace."""
|
|
37
|
+
core = spelling.replace("const", " ").replace("volatile", " ")
|
|
38
|
+
core = core.replace("&", " ").replace("*", " ").strip()
|
|
39
|
+
if not core or "::" in core or "<" in core:
|
|
40
|
+
return False
|
|
41
|
+
tokens = core.split()
|
|
42
|
+
if len(tokens) != 1:
|
|
43
|
+
# `unsigned long`, `long double`, ... — all builtin words.
|
|
44
|
+
return False
|
|
45
|
+
token = tokens[0]
|
|
46
|
+
return token not in _BUILTIN_WORDS and not _STDINT_RE.match(token)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _qualify_spelling(spelling: str, namespace: str | None) -> str:
|
|
50
|
+
if namespace and _needs_qualification(spelling):
|
|
51
|
+
# Keep any cv/ref decoration in place, qualify only the type name.
|
|
52
|
+
return re.sub(r"([A-Za-z_]\w*)", lambda m: f"{namespace}::{m.group(1)}"
|
|
53
|
+
if m.group(1) not in _BUILTIN_WORDS else m.group(1), spelling, count=0)
|
|
54
|
+
return spelling
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _param_spelling(param: Parameter, namespace: str | None) -> str:
|
|
58
|
+
"""The C++ type to emit for one parameter.
|
|
59
|
+
|
|
60
|
+
Prefer the native spelling the parser recorded; `Parameter.type` is the
|
|
61
|
+
*Python* name and is lossy by construction (uint64_t, int and an unscoped
|
|
62
|
+
enum all collapse to "int"), so `_CPP_SPELLING` is only the fallback for
|
|
63
|
+
IR produced before native types were recorded.
|
|
64
|
+
"""
|
|
65
|
+
if param.native_type is not None:
|
|
66
|
+
# Emitted verbatim. The parser resolves each type through its own
|
|
67
|
+
# declaration and qualifies it there (`petro::Correlation`,
|
|
68
|
+
# `std::uint64_t`), so it already knows the answer — whereas this
|
|
69
|
+
# generator only knows the namespace of the record being bound.
|
|
70
|
+
# Re-qualifying here guessed that every unqualified name lived in that
|
|
71
|
+
# same namespace, so a `ns::Widget` method taking a global-namespace
|
|
72
|
+
# `Color` emitted `ns::Color` and failed to compile with "no type
|
|
73
|
+
# named 'Color' in namespace 'ns'". A name the parser left unqualified
|
|
74
|
+
# is one that resolves unqualified at namespace scope, which is
|
|
75
|
+
# exactly where the generated binding writes it.
|
|
76
|
+
return param.native_type
|
|
77
|
+
return _CPP_SPELLING.get(param.type, _qualified(param.type, namespace))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _init_signature(params: list[Parameter], namespace: str | None) -> str:
|
|
81
|
+
"""C++ type list for `py::init<...>`."""
|
|
82
|
+
return ", ".join(_param_spelling(p, namespace) for p in params)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _overloaded_target(
|
|
86
|
+
target: str,
|
|
87
|
+
params: list[Parameter],
|
|
88
|
+
namespace: str | None,
|
|
89
|
+
is_const: bool = False,
|
|
90
|
+
) -> str:
|
|
91
|
+
"""`&f` is ambiguous once `f` is overloaded; spell the signature out."""
|
|
92
|
+
args = ", ".join(_param_spelling(p, namespace) for p in params)
|
|
93
|
+
extra = ", py::const_" if is_const else ""
|
|
94
|
+
return f"py::overload_cast<{args}>(&{target}{extra})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _method_target(method: Method, qualified: str, namespace: str | None) -> str:
|
|
98
|
+
if _buffer_parameters(method) or _scalar_ref_parameters(method):
|
|
99
|
+
return _buffer_lambda(method, self_type=qualified)
|
|
100
|
+
# `cpp_name` when the published name is not the C++ one — an operator is
|
|
101
|
+
# bound as a Python special method, so `&Cls::__add__` would name nothing.
|
|
102
|
+
target = f"{qualified}::{getattr(method, 'cpp_name', None) or method.name}"
|
|
103
|
+
if getattr(method, "is_overloaded", False):
|
|
104
|
+
return _overloaded_target(
|
|
105
|
+
target, method.parameters, namespace, getattr(method, "is_const", False)
|
|
106
|
+
)
|
|
107
|
+
return f"&{target}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _emit_record(lines: list[str], record: ClassDef | StructDef) -> None:
|
|
111
|
+
# Each record carries its own namespace: a service merging headers from two
|
|
112
|
+
# namespaces used to have the first record's namespace applied to all of
|
|
113
|
+
# them, and a struct-only service got None and emitted unqualified names.
|
|
114
|
+
namespace = record.namespace
|
|
115
|
+
qualified = _qualified(record.name, namespace)
|
|
116
|
+
# `py::class_<Derived, Base>` is what teaches pybind11 the relationship:
|
|
117
|
+
# without it, inherited methods are missing from Python and a Base& return
|
|
118
|
+
# never upcasts to the derived wrapper.
|
|
119
|
+
bases = getattr(record, "bases", [])
|
|
120
|
+
template_args = ", ".join([qualified, *(_qualified(b, namespace) for b in bases)])
|
|
121
|
+
lines.append(f' py::class_<{template_args}>(m, "{record.name}")')
|
|
122
|
+
|
|
123
|
+
body: list[str] = []
|
|
124
|
+
|
|
125
|
+
# A struct with a const or reference member has no default constructor;
|
|
126
|
+
# emitting py::init<>() for one is a compile error.
|
|
127
|
+
if record.has_default_constructor:
|
|
128
|
+
body.append(" .def(py::init<>())")
|
|
129
|
+
|
|
130
|
+
if not isinstance(record, StructDef):
|
|
131
|
+
for ctor in record.constructors:
|
|
132
|
+
body.append(f" .def(py::init<{_init_signature(ctor, namespace)}>())")
|
|
133
|
+
for method in record.methods:
|
|
134
|
+
target = _method_target(method, qualified, namespace)
|
|
135
|
+
body.append(f' .def("{method.name}", {target})')
|
|
136
|
+
|
|
137
|
+
for field in record.fields:
|
|
138
|
+
# A const member cannot be bound read/write: pybind11's def_readwrite
|
|
139
|
+
# assigns through the pointer-to-member and fails to compile.
|
|
140
|
+
binder = "def_readonly" if getattr(field, "is_const", False) else "def_readwrite"
|
|
141
|
+
body.append(
|
|
142
|
+
f' .{binder}("{field.name}", &{qualified}::{field.name})'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not body:
|
|
146
|
+
# A record with nothing bindable still needs a terminated statement.
|
|
147
|
+
lines[-1] += ";"
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
for i, line in enumerate(body):
|
|
151
|
+
lines.append(line + (";" if i == len(body) - 1 else ""))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _buffer_parameters(func: FunctionDef) -> list[Parameter]:
|
|
155
|
+
"""Parameters bound from a bare `T*` whose length another argument carries."""
|
|
156
|
+
return [p for p in func.parameters if getattr(p, "length_param", None)]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _scalar_ref_parameters(func: FunctionDef) -> list[Parameter]:
|
|
160
|
+
"""Scalars passed by reference — extern "C" Fortran-linkage arguments."""
|
|
161
|
+
return [p for p in func.parameters if getattr(p, "is_scalar_ref", False)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# numpy dtype for each element type nativegate binds through a raw pointer.
|
|
165
|
+
_NUMPY_DTYPE = {"float": "double", "int": "int", "bool": "bool"}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _buffer_lambda(func, self_type: str | None = None) -> str:
|
|
169
|
+
"""A lambda binding `f(T* data, int n, ...)` as `f(data, ...)`.
|
|
170
|
+
|
|
171
|
+
With `self_type`, the same lambda binds a METHOD: pybind11 passes the
|
|
172
|
+
instance as the first argument of a def'd callable, so
|
|
173
|
+
`[](Simulator& self, py::array values) { return self.set_porosity(...); }`
|
|
174
|
+
is a complete method binding — `int set_porosity(double* values, int n)`
|
|
175
|
+
is the single most common shape a numerical C++ class exposes, and it used
|
|
176
|
+
to be refused with "not yet for class methods".
|
|
177
|
+
|
|
178
|
+
THE LENGTH DISAPPEARS FROM THE PYTHON SIGNATURE, on purpose. It is read off
|
|
179
|
+
the buffer the caller actually passed, so `n` cannot disagree with the data
|
|
180
|
+
— which is the entire failure mode of a C array API. The Fortran side has
|
|
181
|
+
to keep its `n` (f2py fixes the signature) and validates it instead; here
|
|
182
|
+
the generator controls the signature, so the mismatch is made unspellable.
|
|
183
|
+
|
|
184
|
+
Mutable and read-only buffers are bound differently, and the difference is
|
|
185
|
+
load-bearing rather than stylistic:
|
|
186
|
+
|
|
187
|
+
* A **read-only** `const T*` is input. pybind11 may freely convert a list
|
|
188
|
+
or a wrong-dtype array into a temporary, because nothing is written back.
|
|
189
|
+
`forcecast` makes the endpoint convenient.
|
|
190
|
+
|
|
191
|
+
* A **mutable** `T*` is one the callee writes through. Conversion there is
|
|
192
|
+
catastrophic and silent: pybind11 would write into a temporary and
|
|
193
|
+
discard it. Measured, before this was written — `py::array_t<double>`
|
|
194
|
+
handed an int64 array accepted it, converted, and lost every write; and
|
|
195
|
+
`request()` without `true` wrote straight through a `writeable=False`
|
|
196
|
+
array. So a mutable buffer takes an untyped `py::array`, checks the dtype
|
|
197
|
+
itself, and calls `request(true)`, which raises on a read-only array.
|
|
198
|
+
"""
|
|
199
|
+
namespace = getattr(func, "namespace", None)
|
|
200
|
+
if self_type is not None:
|
|
201
|
+
target = f"self.{getattr(func, 'cpp_name', None) or func.name}"
|
|
202
|
+
else:
|
|
203
|
+
target = _qualified(func.name, namespace)
|
|
204
|
+
buffers = {p.name for p in _buffer_parameters(func)}
|
|
205
|
+
lengths = {p.length_param for p in _buffer_parameters(func)}
|
|
206
|
+
|
|
207
|
+
sig: list[str] = []
|
|
208
|
+
body: list[str] = []
|
|
209
|
+
call_args: list[str] = []
|
|
210
|
+
arg_names: list[str] = []
|
|
211
|
+
|
|
212
|
+
for param in func.parameters:
|
|
213
|
+
if param.name in lengths:
|
|
214
|
+
# Supplied from the buffer's own size; never asked of the caller.
|
|
215
|
+
continue
|
|
216
|
+
if param.name in buffers:
|
|
217
|
+
dtype = _NUMPY_DTYPE[param.type]
|
|
218
|
+
arg_names.append(param.name)
|
|
219
|
+
if param.is_mutable_buffer:
|
|
220
|
+
sig.append(f"py::array {param.name}")
|
|
221
|
+
body.append(
|
|
222
|
+
f' if (!{param.name}.dtype().is(py::dtype::of<{dtype}>()))\n'
|
|
223
|
+
f' throw py::type_error("{func.name}(): \'{param.name}\' must be a '
|
|
224
|
+
f'numpy array of dtype {dtype}; nativegate will not convert it because '
|
|
225
|
+
f'this function writes through the buffer and a converted array is a '
|
|
226
|
+
f'temporary whose writes would be discarded.");'
|
|
227
|
+
)
|
|
228
|
+
body.append(
|
|
229
|
+
f' if ({param.name}.ndim() != 1)\n'
|
|
230
|
+
f' throw py::value_error("{func.name}(): \'{param.name}\' must be 1-D.");'
|
|
231
|
+
)
|
|
232
|
+
# request(true) raises on a read-only array. Without it,
|
|
233
|
+
# pybind11 writes straight through `writeable=False`.
|
|
234
|
+
body.append(f" auto {param.name}_info = {param.name}.request(true);")
|
|
235
|
+
else:
|
|
236
|
+
sig.append(
|
|
237
|
+
f"py::array_t<{dtype}, py::array::c_style | py::array::forcecast> "
|
|
238
|
+
f"{param.name}"
|
|
239
|
+
)
|
|
240
|
+
body.append(f" auto {param.name}_info = {param.name}.request();")
|
|
241
|
+
continue
|
|
242
|
+
if getattr(param, "is_scalar_ref", False):
|
|
243
|
+
sig.append(f"{_CPP_SPELLING[param.type]} {param.name}")
|
|
244
|
+
else:
|
|
245
|
+
sig.append(f"{_param_spelling(param, namespace)} {param.name}")
|
|
246
|
+
arg_names.append(param.name)
|
|
247
|
+
|
|
248
|
+
for param in func.parameters:
|
|
249
|
+
if getattr(param, "is_scalar_ref", False):
|
|
250
|
+
# The value came in by value; the callee wants its address.
|
|
251
|
+
call_args.append(f"&{param.name}")
|
|
252
|
+
elif param.name in buffers:
|
|
253
|
+
const = "" if param.is_mutable_buffer else "const "
|
|
254
|
+
dtype = _NUMPY_DTYPE[param.type]
|
|
255
|
+
call_args.append(f"static_cast<{const}{dtype}*>({param.name}_info.ptr)")
|
|
256
|
+
elif param.name in lengths:
|
|
257
|
+
owner = next(p for p in _buffer_parameters(func) if p.length_param == param.name)
|
|
258
|
+
call_args.append(
|
|
259
|
+
f"static_cast<{param.cpp_spelling('int')}>({owner.name}_info.size)"
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
call_args.append(param.name)
|
|
263
|
+
|
|
264
|
+
invocation = f"{target}({', '.join(call_args)})"
|
|
265
|
+
# Non-const scalar refs are OUTPUTS: the callee wrote through the address
|
|
266
|
+
# of a lambda local, and that local dies with the call. Returning the
|
|
267
|
+
# final values is what stops PVTERR's `int* icode` vanishing silently.
|
|
268
|
+
# The shape follows f2py's convention, which the Fortran router already
|
|
269
|
+
# speaks: result first, then each output in declaration order; a bare
|
|
270
|
+
# value when there is exactly one thing to return.
|
|
271
|
+
outputs = [
|
|
272
|
+
p.name
|
|
273
|
+
for p in func.parameters
|
|
274
|
+
if getattr(p, "is_scalar_ref", False) and p.intent != "in"
|
|
275
|
+
]
|
|
276
|
+
if not outputs:
|
|
277
|
+
body.append(
|
|
278
|
+
f" {invocation};"
|
|
279
|
+
if func.returns == "None"
|
|
280
|
+
else f" return {invocation};"
|
|
281
|
+
)
|
|
282
|
+
elif func.returns == "None":
|
|
283
|
+
body.append(f" {invocation};")
|
|
284
|
+
if len(outputs) == 1:
|
|
285
|
+
body.append(f" return {outputs[0]};")
|
|
286
|
+
else:
|
|
287
|
+
body.append(
|
|
288
|
+
f" return py::make_tuple({', '.join(outputs)});"
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
body.append(f" auto result = {invocation};")
|
|
292
|
+
body.append(
|
|
293
|
+
f" return py::make_tuple(result, {', '.join(outputs)});"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
py_args = "".join(f', py::arg("{n}")' for n in arg_names)
|
|
297
|
+
if self_type is not None:
|
|
298
|
+
# `self` is positional-only from pybind11's point of view; naming it
|
|
299
|
+
# with py::arg would shift every argument name by one.
|
|
300
|
+
sig = [f"{self_type}& self"] + sig
|
|
301
|
+
lines = [f"[]({', '.join(sig)}) {{"]
|
|
302
|
+
lines += body
|
|
303
|
+
lines.append(" }")
|
|
304
|
+
return "\n".join(lines) + py_args
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _function_target(func: FunctionDef) -> str:
|
|
308
|
+
"""`&f` for a free function, qualified by its own namespace.
|
|
309
|
+
|
|
310
|
+
Free functions used to be emitted unqualified, so anything outside the
|
|
311
|
+
global namespace failed with "use of undeclared identifier".
|
|
312
|
+
"""
|
|
313
|
+
if _buffer_parameters(func) or _scalar_ref_parameters(func):
|
|
314
|
+
return _buffer_lambda(func)
|
|
315
|
+
namespace = getattr(func, "namespace", None)
|
|
316
|
+
target = _qualified(func.name, namespace)
|
|
317
|
+
if getattr(func, "is_overloaded", False):
|
|
318
|
+
return _overloaded_target(target, func.parameters, namespace)
|
|
319
|
+
return f"&{target}"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _base_first(classes: list[ClassDef]) -> list[ClassDef]:
|
|
323
|
+
"""Order classes so every base is registered before anything derived.
|
|
324
|
+
|
|
325
|
+
`py::class_<Derived, Base>` requires Base to already exist in the module;
|
|
326
|
+
registering them in header order raises "generic_type: type Base is not
|
|
327
|
+
registered" at *import* time, long after the build succeeded. Headers are
|
|
328
|
+
merged alphabetically, so this is not a rare ordering — it is a coin flip.
|
|
329
|
+
|
|
330
|
+
A cycle is impossible in valid C++ (you cannot derive from an incomplete
|
|
331
|
+
type), but the walk is guarded anyway so a malformed IR cannot hang codegen.
|
|
332
|
+
"""
|
|
333
|
+
by_name = {cls.name: cls for cls in classes}
|
|
334
|
+
ordered: list[ClassDef] = []
|
|
335
|
+
placed: set[str] = set()
|
|
336
|
+
|
|
337
|
+
def place(cls: ClassDef, pending: frozenset[str]) -> None:
|
|
338
|
+
if cls.name in placed or cls.name in pending:
|
|
339
|
+
return
|
|
340
|
+
for base in cls.bases:
|
|
341
|
+
parent = by_name.get(base)
|
|
342
|
+
if parent is not None:
|
|
343
|
+
place(parent, pending | {cls.name})
|
|
344
|
+
placed.add(cls.name)
|
|
345
|
+
ordered.append(cls)
|
|
346
|
+
|
|
347
|
+
for cls in classes:
|
|
348
|
+
place(cls, frozenset())
|
|
349
|
+
return ordered
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def generate_bindings(module: ModuleIR, header_names: str | list[str]) -> str:
|
|
353
|
+
"""Emit one pybind11 module binding every exposed symbol.
|
|
354
|
+
|
|
355
|
+
`header_names` may be several headers: a service with foo.hpp and bar.hpp
|
|
356
|
+
produces ONE extension including both. A Python extension can only have a
|
|
357
|
+
single PYBIND11_MODULE (one init symbol per .so), so generating one
|
|
358
|
+
bindings file per header would leave all but one orphaned — which is
|
|
359
|
+
exactly what used to happen, silently dropping those classes.
|
|
360
|
+
"""
|
|
361
|
+
if isinstance(header_names, str):
|
|
362
|
+
header_names = [header_names]
|
|
363
|
+
|
|
364
|
+
module_symbol = f"{module.name}_cpp"
|
|
365
|
+
|
|
366
|
+
# Only when something is actually bound through a raw pointer: numpy.h
|
|
367
|
+
# pulls in numpy's headers at build time and numpy at runtime, and a C++
|
|
368
|
+
# service that binds no buffers should not acquire that dependency.
|
|
369
|
+
needs_numpy = any(_buffer_parameters(fn) for fn in module.functions) or any(
|
|
370
|
+
_buffer_parameters(m) for cls in module.classes for m in cls.methods
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
lines = [
|
|
374
|
+
"// Generated by nativegate. Do not edit by hand — re-run `ngate generate`.",
|
|
375
|
+
"#include <pybind11/pybind11.h>",
|
|
376
|
+
# std::vector<T> <-> Python list. The conversion is a COPY in both
|
|
377
|
+
# directions, which is why the parser refuses a non-const
|
|
378
|
+
# `std::vector<T>&`: writes through one land in the temporary and are
|
|
379
|
+
# discarded. See parsers/cpp_ast._refuse_mutable_vector_ref.
|
|
380
|
+
"#include <pybind11/stl.h>",
|
|
381
|
+
*(["#include <pybind11/numpy.h>"] if needs_numpy else []),
|
|
382
|
+
"#include <cstdint>",
|
|
383
|
+
"#include <string>",
|
|
384
|
+
"#include <vector>",
|
|
385
|
+
"",
|
|
386
|
+
*[f'#include "{h}"' for h in header_names],
|
|
387
|
+
"",
|
|
388
|
+
"namespace py = pybind11;",
|
|
389
|
+
"",
|
|
390
|
+
f"PYBIND11_MODULE({module_symbol}, m) {{",
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
# Structs first: a method returning one can only be bound after the type
|
|
394
|
+
# itself is registered with pybind11.
|
|
395
|
+
for struct in module.structs:
|
|
396
|
+
_emit_record(lines, struct)
|
|
397
|
+
|
|
398
|
+
for cls in _base_first(module.classes):
|
|
399
|
+
_emit_record(lines, cls)
|
|
400
|
+
|
|
401
|
+
for func in module.functions:
|
|
402
|
+
lines.append(f' m.def("{func.name}", {_function_target(func)});')
|
|
403
|
+
|
|
404
|
+
lines.append("}")
|
|
405
|
+
lines.append("")
|
|
406
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Generated pyproject.toml for a service (design.md sections 10, 14)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_BUILD_REQUIRES = {
|
|
6
|
+
"cpp": '["scikit-build-core", "pybind11"]',
|
|
7
|
+
"fortran": '["scikit-build-core", "numpy", "meson", "ninja"]',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# gunicorn supervises the worker processes; see docker_gen's CMD and
|
|
11
|
+
# docs/deployment-topologies.md. It is a RUNTIME dependency, not a dev one:
|
|
12
|
+
# a bare `uvicorn` is a single process, so one segfault in native code takes
|
|
13
|
+
# the whole service down with nothing left to restart it.
|
|
14
|
+
#
|
|
15
|
+
# `uvicorn-worker` rather than `uvicorn.workers.UvicornWorker`: the in-uvicorn
|
|
16
|
+
# worker class is deprecated and slated for removal, and shipping a deprecated
|
|
17
|
+
# import path inside generated code nobody is allowed to hand-edit is how a
|
|
18
|
+
# service breaks on a routine dependency bump.
|
|
19
|
+
#
|
|
20
|
+
# fastmcp is a RUNTIME dependency, not an optional extra: `service.py` imports
|
|
21
|
+
# mcp_server unconditionally, so a wheel without it does not start at all. The
|
|
22
|
+
# MCP endpoint is part of every generated service's surface by design — an
|
|
23
|
+
# LLM-callable interface that has to be switched on is one that is not there
|
|
24
|
+
# when someone goes looking for it. See generators/mcp_gen.py.
|
|
25
|
+
_DEPENDENCIES = {
|
|
26
|
+
"cpp": '["fastapi", "uvicorn", "gunicorn", "uvicorn-worker", "fastmcp"]',
|
|
27
|
+
"fortran": '["fastapi", "uvicorn", "gunicorn", "uvicorn-worker", "fastmcp", "numpy"]',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_pyproject(
|
|
32
|
+
service_name: str, language: str, needs_numpy: bool = False
|
|
33
|
+
) -> str:
|
|
34
|
+
"""The generated service's pyproject.toml.
|
|
35
|
+
|
|
36
|
+
`needs_numpy` is set for a C++ service that binds a raw `T*` as a numpy
|
|
37
|
+
buffer: pybind11/numpy.h needs numpy's headers to build and numpy to
|
|
38
|
+
import. A C++ service with no such pointer does not get the dependency —
|
|
39
|
+
declaring it unconditionally would put numpy in every image for the
|
|
40
|
+
benefit of the services that do not use it.
|
|
41
|
+
"""
|
|
42
|
+
build_requires = _BUILD_REQUIRES.get(language, _BUILD_REQUIRES["cpp"])
|
|
43
|
+
dependencies = _DEPENDENCIES.get(language, _DEPENDENCIES["cpp"])
|
|
44
|
+
if needs_numpy and '"numpy"' not in dependencies:
|
|
45
|
+
build_requires = build_requires.rstrip("]") + ', "numpy"]'
|
|
46
|
+
dependencies = dependencies.rstrip("]") + ', "numpy"]'
|
|
47
|
+
|
|
48
|
+
return f"""[build-system]
|
|
49
|
+
requires = {build_requires}
|
|
50
|
+
build-backend = "scikit_build_core.build"
|
|
51
|
+
|
|
52
|
+
[project]
|
|
53
|
+
name = "{service_name}"
|
|
54
|
+
version = "1.0.0"
|
|
55
|
+
requires-python = ">=3.10"
|
|
56
|
+
dependencies = {dependencies}
|
|
57
|
+
|
|
58
|
+
[tool.scikit-build]
|
|
59
|
+
wheel.packages = ["python/{service_name}"]
|
|
60
|
+
cmake.source-dir = "."
|
|
61
|
+
"""
|