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,616 @@
|
|
|
1
|
+
"""The C++ oracle driver generator (T7, design-verification-layers.md §2).
|
|
2
|
+
|
|
3
|
+
Mirrors `drivers/fortran.py` (T3) in every load-bearing respect — same
|
|
4
|
+
`DriverResult` shape, same determinism contract (spec section 4: the source
|
|
5
|
+
is a pure function of `(document, module, headers, return_kinds)`, no
|
|
6
|
+
timestamps, no absolute paths), same "replay golden.json's recorded calls
|
|
7
|
+
verbatim, in file order, never `plan()`" rule (spec section 2.2) — but emits
|
|
8
|
+
one C++ translation unit instead of one Fortran program unit.
|
|
9
|
+
|
|
10
|
+
Turns a `(golden.json document, ModuleIR)` pair into ONE C++ driver that:
|
|
11
|
+
|
|
12
|
+
* replays exactly the calls `golden.json` recorded, in file order, with the
|
|
13
|
+
file's recorded `arguments`/`constructor_arguments`;
|
|
14
|
+
* constructs an instance-method entry's object from the file's recorded
|
|
15
|
+
`constructor_arguments` (spec section 2.9);
|
|
16
|
+
* prints one wire-format line per observable value (spec section 2.5),
|
|
17
|
+
tab-separated, via `wire.slots_for_entry` — the single source of truth
|
|
18
|
+
T3/T7/T5 all read from, so "which slots does this call produce" cannot
|
|
19
|
+
drift between the Fortran and C++ generators;
|
|
20
|
+
* packs every float by `memcpy`-ing into a `std::uint64_t` and printing
|
|
21
|
+
`%016llx` — **never** a union, **never** a pointer cast: both are strict-
|
|
22
|
+
aliasing violations an optimiser is entitled to miscompile (spec section
|
|
23
|
+
2.4, called out explicitly for C++);
|
|
24
|
+
* widens `float` to `double` before that `memcpy` — unconditionally, on
|
|
25
|
+
every float value this driver prints, so a value that started out
|
|
26
|
+
`double` is merely widened to itself (exact, a no-op) and a value that
|
|
27
|
+
started out `float` never touches the 64-bit channel with undefined
|
|
28
|
+
padding bits (spec section 2.4's "narrow floats widen; they do not get
|
|
29
|
+
their own channel").
|
|
30
|
+
|
|
31
|
+
**Array/buffer parameters and `std::vector<T>` returns are supported.** A
|
|
32
|
+
parameter that is an array (`Parameter.is_array`) or a raw-pointer buffer
|
|
33
|
+
(`Parameter.is_mutable_buffer` / `Parameter.length_param`) is emitted as a
|
|
34
|
+
fixed-size local C array, initialised from the recorded value when one was
|
|
35
|
+
recorded (an array's `arguments` entry is a plain list — see
|
|
36
|
+
`golden.py`'s `sample_arguments`) and otherwise zero-initialised; its size
|
|
37
|
+
comes from `len(value)` when the recorded value is a list, or otherwise
|
|
38
|
+
from the `length_param`'s materialised value in the same entry (mirroring
|
|
39
|
+
`drivers/fortran.py`'s `intent(out)` array handling, the same underlying
|
|
40
|
+
"a length-carrying array needs the driver to know the length" problem in a
|
|
41
|
+
different language). Each element is printed as its own wire slot
|
|
42
|
+
(`arg:<i>[<n>]`) with the same `memcpy`-into-`uint64_t`/widen-to-double
|
|
43
|
+
discipline scalar values already use. A `std::vector<T>` return is captured
|
|
44
|
+
into a local `std::vector<...>` and its elements printed as `return[<n>]`
|
|
45
|
+
the same way. The only remaining hard skip in this module — mirroring T3's
|
|
46
|
+
own "genuinely no way to know the length" fallback — is an array/buffer
|
|
47
|
+
parameter that has neither a recorded list value nor a resolvable
|
|
48
|
+
`length_param`; that is the one case this driver truly cannot size at the
|
|
49
|
+
call site. Struct-by-value entries remain out of scope (see below) and are
|
|
50
|
+
still skipped, unchanged.
|
|
51
|
+
|
|
52
|
+
**wire.py note.** Unlike `drivers/fortran.py` (written before `wire.py`
|
|
53
|
+
existed), this module imports `wire.slots_for_entry` directly rather than
|
|
54
|
+
reimplementing slot enumeration — see wire.py's module docstring for why
|
|
55
|
+
that single source of truth exists.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import hashlib
|
|
61
|
+
from dataclasses import dataclass, field
|
|
62
|
+
|
|
63
|
+
from .. import wire
|
|
64
|
+
from ..ir import ClassDef, ModuleIR, Parameter
|
|
65
|
+
|
|
66
|
+
# --- result ----------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class DriverResult:
|
|
71
|
+
source: str
|
|
72
|
+
driver_sha256: str
|
|
73
|
+
skipped: dict = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _SkipEntry(Exception):
|
|
77
|
+
"""Raised internally when one entry cannot be emitted as a call."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- C++ literal formatting --------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _float_literal(value) -> str:
|
|
84
|
+
"""A C++ literal that is unambiguously a `double` (no `f` suffix)."""
|
|
85
|
+
text = repr(float(value))
|
|
86
|
+
if "e" in text or "E" in text:
|
|
87
|
+
return text
|
|
88
|
+
if "." not in text:
|
|
89
|
+
text += ".0"
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _int_literal(value) -> str:
|
|
94
|
+
return str(int(value))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _bool_literal(value) -> str:
|
|
98
|
+
return "true" if value else "false"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _str_literal(value: str) -> str:
|
|
102
|
+
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"')
|
|
103
|
+
return f'"{escaped}"'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _quote(text: str) -> str:
|
|
107
|
+
"""A C++ string literal for a driver-internal label (key/slot)."""
|
|
108
|
+
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
|
109
|
+
return f'"{escaped}"'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _scalar_literal(value, python_type: str) -> str:
|
|
113
|
+
if isinstance(value, (list, dict)):
|
|
114
|
+
# A nested/2D array element, or a struct-shaped element, reaching
|
|
115
|
+
# here (e.g. from `_plan_array`'s per-element loop) is a call-site
|
|
116
|
+
# shape this v1 driver does not understand -- `int(value)`/
|
|
117
|
+
# `float(value)` on a list/dict raises an unhandled `TypeError`
|
|
118
|
+
# instead of the clean, caught `_SkipEntry` every other unsupported
|
|
119
|
+
# shape in this module produces. Skip cleanly instead.
|
|
120
|
+
raise _SkipEntry(
|
|
121
|
+
f"array element {value!r} is not a scalar -- nested arrays and "
|
|
122
|
+
"struct-valued array elements are not supported by the C++ "
|
|
123
|
+
"driver (v1)"
|
|
124
|
+
)
|
|
125
|
+
if python_type == "bool":
|
|
126
|
+
return _bool_literal(value)
|
|
127
|
+
if python_type == "int":
|
|
128
|
+
return _int_literal(value)
|
|
129
|
+
if python_type == "float":
|
|
130
|
+
return _float_literal(value)
|
|
131
|
+
if python_type == "str":
|
|
132
|
+
return _str_literal(value)
|
|
133
|
+
raise _SkipEntry(f"no C++ literal for value {value!r} of type {python_type!r}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --- C++ type spellings -----------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_float32(native_type: str | None) -> bool:
|
|
140
|
+
spelling = (native_type or "").strip().rstrip("&").strip()
|
|
141
|
+
return spelling in ("float", "const float", "float const")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _decl_type(python_type: str, is_float32: bool) -> str:
|
|
145
|
+
"""The local variable type this driver declares for one scalar value.
|
|
146
|
+
|
|
147
|
+
Deliberately independent of the exact native spelling except for the
|
|
148
|
+
float/double distinction, which is the one distinction that changes
|
|
149
|
+
which channel a value must go through before printing (spec section
|
|
150
|
+
2.4). An `int`-family or `std::string` local's exact width/spelling does
|
|
151
|
+
not affect the bits the *callee* computes — the callee's own parameter
|
|
152
|
+
type governs that at the call site, via ordinary C++ implicit
|
|
153
|
+
conversion — so a single stable local type is used for every such value.
|
|
154
|
+
"""
|
|
155
|
+
if python_type == "float":
|
|
156
|
+
return "float" if is_float32 else "double"
|
|
157
|
+
if python_type == "int":
|
|
158
|
+
return "long long"
|
|
159
|
+
if python_type == "bool":
|
|
160
|
+
return "bool"
|
|
161
|
+
if python_type == "str":
|
|
162
|
+
return "std::string"
|
|
163
|
+
raise _SkipEntry(f"no C++ declaration for type {python_type!r}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _pointee_native_type(native_type: str | None) -> str | None:
|
|
167
|
+
"""Strip one trailing `*` from a recorded native pointer spelling.
|
|
168
|
+
|
|
169
|
+
`Parameter.is_scalar_ref` params carry the pointer spelling
|
|
170
|
+
(`"double*"`) — the local variable this driver declares to take the
|
|
171
|
+
address of has to be the pointee type, or `&var` does not convert to
|
|
172
|
+
the callee's parameter type.
|
|
173
|
+
"""
|
|
174
|
+
if native_type is None:
|
|
175
|
+
return None
|
|
176
|
+
spelling = native_type.strip()
|
|
177
|
+
if spelling.endswith("*"):
|
|
178
|
+
return spelling[:-1].strip()
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --- scope guards: what this generator (v1) will not attempt ---------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _is_struct_value(value) -> bool:
|
|
186
|
+
return isinstance(value, dict) and "__struct__" in value
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _refuse_unsupported_shapes(fn_or_method, entry: dict) -> None:
|
|
190
|
+
"""Hard-skip anything outside this module's v1 scope. See module docstring.
|
|
191
|
+
|
|
192
|
+
Array/buffer parameters and `std::vector<T>` returns are supported (see
|
|
193
|
+
`_plan_array`/`_resolve_array_size` and the return-array handling in
|
|
194
|
+
`_emit_call`) and are therefore NOT refused here — only structs by value
|
|
195
|
+
remain an unconditional skip.
|
|
196
|
+
"""
|
|
197
|
+
for value in (entry.get("arguments") or []) + (entry.get("constructor_arguments") or []):
|
|
198
|
+
if _is_struct_value(value):
|
|
199
|
+
raise _SkipEntry("structs by value are not supported by the C++ driver (v1)")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# --- per-parameter plan ------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class _Slot:
|
|
207
|
+
"""One native call-site argument, resolved for one call."""
|
|
208
|
+
|
|
209
|
+
param: Parameter
|
|
210
|
+
expr: str # the C++ expression to pass at the call site (a literal or a var name)
|
|
211
|
+
var: str | None # the local variable name, if one was declared; else None
|
|
212
|
+
decl: str | None # the full declaration+init statement, or None
|
|
213
|
+
is_ref: bool # True when this parameter is a scalar reference (address-of a local)
|
|
214
|
+
is_array: bool = False # True when this parameter is an array/buffer local
|
|
215
|
+
size: int | None = None # element count, for an array/buffer slot
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _matching_constructor(cls: ClassDef, n: int) -> list[Parameter] | None:
|
|
219
|
+
for ctor in cls.constructors:
|
|
220
|
+
if len(ctor) == n:
|
|
221
|
+
return ctor
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _resolve_array_size(param: Parameter, value, value_by_name: dict) -> int:
|
|
226
|
+
"""The element count for an array/buffer parameter at this call site.
|
|
227
|
+
|
|
228
|
+
Prefers the recorded value's own length (a plain list, per
|
|
229
|
+
`golden.py`'s `sample_arguments`) — the same precedent
|
|
230
|
+
`drivers/fortran.py` uses for a Fortran `intent(in)` array. Falls back
|
|
231
|
+
to the `length_param`'s materialised value in this same entry — the
|
|
232
|
+
same "no direct value, use the length parameter" fallback
|
|
233
|
+
`drivers/fortran.py` uses for its `intent(out)` array case — when the
|
|
234
|
+
recorded value is not itself a list (e.g. an output-only mutable
|
|
235
|
+
buffer, which golden.py does not populate with sample content today).
|
|
236
|
+
"""
|
|
237
|
+
if isinstance(value, list):
|
|
238
|
+
return len(value)
|
|
239
|
+
length_param = getattr(param, "length_param", None)
|
|
240
|
+
if not length_param:
|
|
241
|
+
raise _SkipEntry(
|
|
242
|
+
f"array/buffer parameter '{param.name}' has no recorded list value "
|
|
243
|
+
"and no length_param — the C++ driver cannot size it at the call site"
|
|
244
|
+
)
|
|
245
|
+
size_value = value_by_name.get(length_param)
|
|
246
|
+
if size_value is None:
|
|
247
|
+
raise _SkipEntry(
|
|
248
|
+
f"array/buffer parameter '{param.name}' is sized by "
|
|
249
|
+
f"'{length_param}', which is not a visible argument"
|
|
250
|
+
)
|
|
251
|
+
return int(size_value)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _plan_array(
|
|
255
|
+
idx: int, prefix: str, position: int, param: Parameter, value, value_by_name: dict
|
|
256
|
+
) -> _Slot:
|
|
257
|
+
"""Resolve one array/buffer call-site argument as a local C array.
|
|
258
|
+
|
|
259
|
+
The local decays to a pointer at the call site, matching the callee's
|
|
260
|
+
`const T*`/`T*` parameter — no `&` is needed (unlike `_plan_scalar`'s
|
|
261
|
+
scalar-reference case).
|
|
262
|
+
"""
|
|
263
|
+
size = _resolve_array_size(param, value, value_by_name)
|
|
264
|
+
if size == 0:
|
|
265
|
+
# A zero-length C array declaration (`T name[0]`) is not valid
|
|
266
|
+
# standard C++ -- a legitimately empty buffer/array call at this
|
|
267
|
+
# entry cannot be represented as a local array the way every
|
|
268
|
+
# non-empty case is, so skip it cleanly rather than emit source a
|
|
269
|
+
# standards-conforming compiler will reject.
|
|
270
|
+
raise _SkipEntry(
|
|
271
|
+
f"array/buffer parameter '{param.name}' has length 0 at this "
|
|
272
|
+
"call site -- a zero-length local array is not valid C++"
|
|
273
|
+
)
|
|
274
|
+
is_float32 = _is_float32(_pointee_native_type(param.native_type)) if param.native_type else False
|
|
275
|
+
elem_type = _decl_type(param.type, is_float32)
|
|
276
|
+
var = f"n2p_{idx}_{prefix}{position}"
|
|
277
|
+
if isinstance(value, list):
|
|
278
|
+
init = ", ".join(_scalar_literal(v, param.type) for v in value)
|
|
279
|
+
else:
|
|
280
|
+
init = "" # zero-initialised: an output-only buffer with no recorded content
|
|
281
|
+
decl = f" {elem_type} {var}[{size}] = {{{init}}};"
|
|
282
|
+
return _Slot(param=param, expr=var, var=var, decl=decl, is_ref=False, is_array=True, size=size)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _plan_scalar(
|
|
286
|
+
idx: int, prefix: str, position: int, param: Parameter | None, value, python_type: str
|
|
287
|
+
) -> _Slot:
|
|
288
|
+
"""Resolve one call-site argument (a plain value or a scalar reference)."""
|
|
289
|
+
is_ref = bool(param is not None and getattr(param, "is_scalar_ref", False))
|
|
290
|
+
if not is_ref:
|
|
291
|
+
return _Slot(param=param, expr=_scalar_literal(value, python_type), var=None, decl=None, is_ref=False)
|
|
292
|
+
|
|
293
|
+
is_float32 = _is_float32(_pointee_native_type(param.native_type)) if param.native_type else False
|
|
294
|
+
decl_type = _pointee_native_type(param.native_type) or _decl_type(python_type, is_float32)
|
|
295
|
+
var = f"n2p_{idx}_{prefix}{position}"
|
|
296
|
+
decl = f" {decl_type} {var} = {_scalar_literal(value, python_type)};"
|
|
297
|
+
return _Slot(param=param, expr=f"&{var}", var=var, decl=decl, is_ref=True)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _plan_call(
|
|
301
|
+
idx: int, entry: dict, fn_or_method
|
|
302
|
+
) -> tuple[list[_Slot], list[Parameter]]:
|
|
303
|
+
"""Resolve every call-site argument for one entry.
|
|
304
|
+
|
|
305
|
+
Returns (slots in call order, the parameter list `arguments` was
|
|
306
|
+
recorded against). For C++ every parameter is Python-visible — unlike
|
|
307
|
+
Fortran's `intent(out)`, there is no dropped native slot — so
|
|
308
|
+
`entry["arguments"]` lines up with `fn_or_method.parameters` 1:1.
|
|
309
|
+
"""
|
|
310
|
+
_refuse_unsupported_shapes(fn_or_method, entry)
|
|
311
|
+
params = fn_or_method.parameters
|
|
312
|
+
arguments = entry.get("arguments") or []
|
|
313
|
+
if len(arguments) != len(params):
|
|
314
|
+
raise _SkipEntry(
|
|
315
|
+
"recorded arguments do not match the current signature "
|
|
316
|
+
f"({len(arguments)} recorded, {len(params)} expected)"
|
|
317
|
+
)
|
|
318
|
+
value_by_name = {p.name: v for p, v in zip(params, arguments)}
|
|
319
|
+
slots = []
|
|
320
|
+
for i, (param, value) in enumerate(zip(params, arguments)):
|
|
321
|
+
if param.is_array or getattr(param, "is_mutable_buffer", False):
|
|
322
|
+
slots.append(_plan_array(idx, "arg", i, param, value, value_by_name))
|
|
323
|
+
else:
|
|
324
|
+
slots.append(_plan_scalar(idx, "arg", i, param, value, param.type))
|
|
325
|
+
return slots, params
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _plan_constructor(idx: int, cls: ClassDef, entry: dict) -> list[_Slot]:
|
|
329
|
+
ctor_args = entry.get("constructor_arguments") or []
|
|
330
|
+
matched = _matching_constructor(cls, len(ctor_args))
|
|
331
|
+
slots: list[_Slot] = []
|
|
332
|
+
for i, value in enumerate(ctor_args):
|
|
333
|
+
param = matched[i] if matched is not None else None
|
|
334
|
+
python_type = param.type if param is not None else _python_type_of(value)
|
|
335
|
+
slots.append(_plan_scalar(idx, "ctor", i, param, value, python_type))
|
|
336
|
+
return slots
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _python_type_of(value) -> str:
|
|
340
|
+
if isinstance(value, bool):
|
|
341
|
+
return "bool"
|
|
342
|
+
if isinstance(value, int):
|
|
343
|
+
return "int"
|
|
344
|
+
if isinstance(value, float):
|
|
345
|
+
return "float"
|
|
346
|
+
if isinstance(value, str):
|
|
347
|
+
return "str"
|
|
348
|
+
raise _SkipEntry(f"no Python type recognised for constructor argument {value!r}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# --- emitting the wire protocol ---------------------------------------------
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _print_scalar_slot(key: str, slot: str, expr: str, python_type: str) -> list[str]:
|
|
355
|
+
key_q, slot_q = _quote(key), _quote(slot)
|
|
356
|
+
if python_type == "float":
|
|
357
|
+
return [
|
|
358
|
+
" {",
|
|
359
|
+
f" double n2p_dbl = static_cast<double>({expr});",
|
|
360
|
+
" std::uint64_t n2p_bits;",
|
|
361
|
+
" std::memcpy(&n2p_bits, &n2p_dbl, sizeof n2p_bits);",
|
|
362
|
+
f' std::printf("%s\\t%s\\t%016llx\\n", {key_q}, {slot_q}, '
|
|
363
|
+
"(unsigned long long)n2p_bits);",
|
|
364
|
+
" }",
|
|
365
|
+
]
|
|
366
|
+
if python_type == "int":
|
|
367
|
+
return [
|
|
368
|
+
" {",
|
|
369
|
+
f" long long n2p_i = static_cast<long long>({expr});",
|
|
370
|
+
f' std::printf("%s\\t%s\\t%lld\\n", {key_q}, {slot_q}, n2p_i);',
|
|
371
|
+
" }",
|
|
372
|
+
]
|
|
373
|
+
if python_type == "bool":
|
|
374
|
+
return [
|
|
375
|
+
" {",
|
|
376
|
+
f' std::printf("%s\\t%s\\t%s\\n", {key_q}, {slot_q}, ({expr}) ? "1" : "0");',
|
|
377
|
+
" }",
|
|
378
|
+
]
|
|
379
|
+
if python_type == "str":
|
|
380
|
+
return [
|
|
381
|
+
" {",
|
|
382
|
+
f" std::string n2p_s = n2p_escape({expr});",
|
|
383
|
+
f' std::printf("%s\\t%s\\t%s\\n", {key_q}, {slot_q}, n2p_s.c_str());',
|
|
384
|
+
" }",
|
|
385
|
+
]
|
|
386
|
+
raise _SkipEntry(f"no wire formatting for type {python_type!r}")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# --- emitting one call's C++ --------------------------------------------------
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _qualified(name: str, namespace: str | None) -> str:
|
|
393
|
+
return f"{namespace}::{name}" if namespace else name
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _emit_call(
|
|
397
|
+
idx: int,
|
|
398
|
+
key: str,
|
|
399
|
+
entry: dict,
|
|
400
|
+
module: ModuleIR,
|
|
401
|
+
classes_by_name: dict,
|
|
402
|
+
functions_by_name: dict,
|
|
403
|
+
return_kind: str | None = None,
|
|
404
|
+
) -> list[str]:
|
|
405
|
+
"""Returns the C++ statement lines for one call, including its wire output."""
|
|
406
|
+
kind = entry.get("kind", "function")
|
|
407
|
+
if kind == "function":
|
|
408
|
+
fn = functions_by_name.get(entry.get("name") or key)
|
|
409
|
+
if fn is None:
|
|
410
|
+
raise _SkipEntry("no matching C++ function in the IR")
|
|
411
|
+
cls = None
|
|
412
|
+
elif kind in ("static", "method"):
|
|
413
|
+
cls = classes_by_name.get(entry.get("class"))
|
|
414
|
+
if cls is None:
|
|
415
|
+
raise _SkipEntry(f"no matching C++ class {entry.get('class')!r} in the IR")
|
|
416
|
+
fn = next((m for m in cls.methods if m.name == entry.get("name")), None)
|
|
417
|
+
if fn is None:
|
|
418
|
+
raise _SkipEntry(f"no matching method {entry.get('name')!r} on {cls.name!r} in the IR")
|
|
419
|
+
else:
|
|
420
|
+
raise _SkipEntry(f"unsupported entry kind {kind!r}")
|
|
421
|
+
|
|
422
|
+
ir_function_like = fn # duck-typed: has .returns and .parameters, like FunctionDef
|
|
423
|
+
call_slots, visible_params = _plan_call(idx, entry, fn)
|
|
424
|
+
|
|
425
|
+
lines: list[str] = []
|
|
426
|
+
|
|
427
|
+
ctor_var = None
|
|
428
|
+
if kind == "method":
|
|
429
|
+
ctor_slots = _plan_constructor(idx, cls, entry)
|
|
430
|
+
_refuse_unsupported_shapes_ctor(ctor_slots)
|
|
431
|
+
for slot in ctor_slots:
|
|
432
|
+
if slot.decl:
|
|
433
|
+
lines.append(slot.decl)
|
|
434
|
+
ctor_var = f"n2p_{idx}_instance"
|
|
435
|
+
ctor_args = ", ".join(s.expr for s in ctor_slots)
|
|
436
|
+
namespace = cls.namespace
|
|
437
|
+
lines.append(f" {_qualified(cls.name, namespace)} {ctor_var}({ctor_args});")
|
|
438
|
+
|
|
439
|
+
for slot in call_slots:
|
|
440
|
+
if slot.decl:
|
|
441
|
+
lines.append(slot.decl)
|
|
442
|
+
|
|
443
|
+
call_args = ", ".join(s.expr for s in call_slots)
|
|
444
|
+
result_var = None
|
|
445
|
+
has_result = fn.returns != "void"
|
|
446
|
+
if kind == "static":
|
|
447
|
+
target = f"{_qualified(cls.name, cls.namespace)}::{fn.name}"
|
|
448
|
+
elif kind == "method":
|
|
449
|
+
target = f"{ctor_var}.{fn.name}"
|
|
450
|
+
else:
|
|
451
|
+
target = _qualified(fn.name, getattr(fn, "namespace", None))
|
|
452
|
+
|
|
453
|
+
returns_array = bool(getattr(fn, "returns_array", False))
|
|
454
|
+
call_expr = f"{target}({call_args})"
|
|
455
|
+
if has_result:
|
|
456
|
+
result_var = f"n2p_{idx}_result"
|
|
457
|
+
is_float32 = fn.returns == "float" and (return_kind or "").strip().lower() == "float"
|
|
458
|
+
if returns_array:
|
|
459
|
+
elem_type = _decl_type(fn.returns, is_float32)
|
|
460
|
+
decl_type = f"std::vector<{elem_type}>"
|
|
461
|
+
else:
|
|
462
|
+
decl_type = _decl_type(fn.returns, is_float32) if fn.returns != "void" else "void"
|
|
463
|
+
lines.append(f" {decl_type} {result_var} = {call_expr};")
|
|
464
|
+
else:
|
|
465
|
+
lines.append(f" {call_expr};")
|
|
466
|
+
|
|
467
|
+
# wire.slots_for_entry is the single source of truth for what to print.
|
|
468
|
+
wire_slots = wire.slots_for_entry(entry, ir_function_like)
|
|
469
|
+
arg_by_index = {i: slot for i, slot in enumerate(call_slots)}
|
|
470
|
+
|
|
471
|
+
for wslot in wire_slots:
|
|
472
|
+
if wslot.role == "return":
|
|
473
|
+
if wslot.element is not None:
|
|
474
|
+
if not returns_array:
|
|
475
|
+
raise _SkipEntry(
|
|
476
|
+
"a tuple return is not supported by the C++ driver (v1)"
|
|
477
|
+
)
|
|
478
|
+
expr = f"{result_var}[{wslot.element}]"
|
|
479
|
+
else:
|
|
480
|
+
expr = result_var
|
|
481
|
+
lines.extend(_print_scalar_slot(key, str(wslot), expr, fn.returns))
|
|
482
|
+
else:
|
|
483
|
+
slot = arg_by_index.get(wslot.arg_index)
|
|
484
|
+
param = visible_params[wslot.arg_index] if wslot.arg_index < len(visible_params) else None
|
|
485
|
+
if slot is None or not (slot.is_ref or slot.is_array):
|
|
486
|
+
raise _SkipEntry(
|
|
487
|
+
f"argument {wslot.arg_index} was recorded as modified in place, "
|
|
488
|
+
"but is not a scalar-reference or array/buffer parameter the "
|
|
489
|
+
"C++ driver (v1) can read back"
|
|
490
|
+
)
|
|
491
|
+
if wslot.element is not None:
|
|
492
|
+
if not slot.is_array:
|
|
493
|
+
raise _SkipEntry(
|
|
494
|
+
"an array element effect was recorded for a non-array "
|
|
495
|
+
f"argument {wslot.arg_index}"
|
|
496
|
+
)
|
|
497
|
+
expr = f"{slot.var}[{wslot.element}]"
|
|
498
|
+
else:
|
|
499
|
+
if slot.is_array:
|
|
500
|
+
raise _SkipEntry(
|
|
501
|
+
f"argument {wslot.arg_index} is an array/buffer parameter but "
|
|
502
|
+
"was recorded with a scalar effect"
|
|
503
|
+
)
|
|
504
|
+
expr = slot.var
|
|
505
|
+
python_type = param.type if param is not None else "float"
|
|
506
|
+
lines.extend(_print_scalar_slot(key, str(wslot), expr, python_type))
|
|
507
|
+
|
|
508
|
+
return lines
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _refuse_unsupported_shapes_ctor(ctor_slots: list[_Slot]) -> None:
|
|
512
|
+
# Constructor arguments are already scope-checked for structs by
|
|
513
|
+
# `_refuse_unsupported_shapes` (called from `_plan_call` before this
|
|
514
|
+
# runs, on the same entry) — nothing further to check here today. Kept
|
|
515
|
+
# as an explicit hook so a future constructor-side array/buffer case
|
|
516
|
+
# fails loudly rather than silently compiling something wrong.
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# --- assembling the whole driver ---------------------------------------------
|
|
521
|
+
|
|
522
|
+
_HELPERS = """\
|
|
523
|
+
static std::string n2p_escape(const std::string& raw) {
|
|
524
|
+
// Percent-escape tab (%09), newline (%0A) and percent (%25) so a string
|
|
525
|
+
// output containing one of them cannot break the wire protocol's line
|
|
526
|
+
// format (spec section 2.4). Order matters in the caller's alphabet, but
|
|
527
|
+
// here each input byte is classified once, so there is no double-escape
|
|
528
|
+
// hazard the way there is composing string replacements in sequence.
|
|
529
|
+
static const char hexd[] = "0123456789ABCDEF";
|
|
530
|
+
std::string out;
|
|
531
|
+
out.reserve(raw.size());
|
|
532
|
+
for (unsigned char c : raw) {
|
|
533
|
+
if (c == 9 || c == 10 || c == 37) {
|
|
534
|
+
out.push_back('%');
|
|
535
|
+
out.push_back(hexd[c / 16]);
|
|
536
|
+
out.push_back(hexd[c % 16]);
|
|
537
|
+
} else {
|
|
538
|
+
out.push_back(static_cast<char>(c));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def generate_driver(
|
|
547
|
+
document: dict,
|
|
548
|
+
module: ModuleIR,
|
|
549
|
+
headers: str | list[str],
|
|
550
|
+
return_kinds: dict | None = None,
|
|
551
|
+
) -> DriverResult:
|
|
552
|
+
"""Build the driver source for `document`'s recorded entries.
|
|
553
|
+
|
|
554
|
+
`headers` are the header(s) the driver `#include`s to see the
|
|
555
|
+
declarations it calls — passed explicitly, the same convention
|
|
556
|
+
`generators/pybind_gen.generate_bindings` uses, rather than read off
|
|
557
|
+
`ModuleIR.source_file`: embedding whatever absolute path a parser run
|
|
558
|
+
happened to capture would violate spec section 4's "no absolute paths"
|
|
559
|
+
determinism rule.
|
|
560
|
+
|
|
561
|
+
`return_kinds` maps an entry key to `"float"` or `"double"` — the native
|
|
562
|
+
C++ spelling of that entry's return type — for the same reason
|
|
563
|
+
`drivers/fortran.py` accepts `return_kinds`: the IR's `.returns` is
|
|
564
|
+
already collapsed to the Python type and cannot distinguish `float` from
|
|
565
|
+
`double`. An entry absent from it is assumed `double`.
|
|
566
|
+
"""
|
|
567
|
+
if isinstance(headers, str):
|
|
568
|
+
headers = [headers]
|
|
569
|
+
return_kinds = return_kinds or {}
|
|
570
|
+
|
|
571
|
+
classes_by_name = {cls.name: cls for cls in module.classes}
|
|
572
|
+
functions_by_name = {fn.name: fn for fn in module.functions}
|
|
573
|
+
skipped: dict = dict(document.get("skipped") or {})
|
|
574
|
+
|
|
575
|
+
all_lines: list[str] = []
|
|
576
|
+
for idx, (key, entry) in enumerate(sorted_entries(document)):
|
|
577
|
+
try:
|
|
578
|
+
lines = _emit_call(
|
|
579
|
+
idx, key, entry, module, classes_by_name, functions_by_name, return_kinds.get(key)
|
|
580
|
+
)
|
|
581
|
+
except _SkipEntry as exc:
|
|
582
|
+
skipped[key] = str(exc)
|
|
583
|
+
continue
|
|
584
|
+
all_lines.append(f" // --- {key} ---")
|
|
585
|
+
all_lines.extend(lines)
|
|
586
|
+
|
|
587
|
+
source = _assemble(headers, all_lines)
|
|
588
|
+
digest = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
589
|
+
return DriverResult(source=source, driver_sha256=digest, skipped=skipped)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def sorted_entries(document: dict):
|
|
593
|
+
"""`document["entries"]` in file order — never re-sorted (spec sec 2.2)."""
|
|
594
|
+
return list((document.get("entries") or {}).items())
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _assemble(headers: list[str], lines: list[str]) -> str:
|
|
598
|
+
include_lines = "\n".join(f'#include "{h}"' for h in headers)
|
|
599
|
+
body = "\n".join(lines)
|
|
600
|
+
return (
|
|
601
|
+
"// Generated by nativegate. Do not edit by hand — regenerate the oracle driver.\n"
|
|
602
|
+
"#include <cstdio>\n"
|
|
603
|
+
"#include <cstdint>\n"
|
|
604
|
+
"#include <cstring>\n"
|
|
605
|
+
"#include <string>\n"
|
|
606
|
+
"#include <vector>\n"
|
|
607
|
+
"\n"
|
|
608
|
+
f"{include_lines}\n"
|
|
609
|
+
"\n"
|
|
610
|
+
f"{_HELPERS}\n"
|
|
611
|
+
"int main() {\n"
|
|
612
|
+
f"{body}\n"
|
|
613
|
+
"\n"
|
|
614
|
+
" return 0;\n"
|
|
615
|
+
"}\n"
|
|
616
|
+
)
|