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