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,507 @@
1
+ """The Fortran oracle driver generator (T3, design-verification-layers.md §2).
2
+
3
+ Turns a `(golden.json document, ModuleIR)` pair into ONE Fortran driver
4
+ translation unit that:
5
+
6
+ * replays exactly the calls `golden.json` recorded, in file order, with the
7
+ file's recorded arguments (never `plan()` — see spec §2.2: the committed
8
+ file may hold hand-edited inputs that `plan()` would not reproduce);
9
+ * re-inserts the native parameters the Python side never sees — `intent(out)`
10
+ scalars/arrays and, for an `intent(out)` array, the size Fortran needs at
11
+ the call site, taken from `length_param`;
12
+ * prints one wire-format line per observable value (spec §2.5), tab
13
+ separated, floats as `transfer`-ed `integer(8)` in lowercase `Z16.16` hex,
14
+ ints/bools by value, strings trimmed and percent-escaped.
15
+
16
+ Determinism (spec §4): the generated source is a pure function of
17
+ `(document, module)` — no timestamps, no absolute paths, no environment.
18
+ `driver_sha256` is the SHA-256 of exactly the bytes returned as `source`.
19
+
20
+ **wire.py note.** At the time this was written, `nativegate/wire.py` (T2, the
21
+ shared `slots_for_entry` source of truth) did not exist yet. The slot
22
+ enumeration below (`_slots_for_call`) is this module's OWN implementation of
23
+ the §2.4/§2.5 rules and is not guaranteed to agree byte-for-byte with
24
+ whatever T2 lands with — in particular the handling of an entry with more
25
+ than one observable output where one of the outputs is itself an array has
26
+ no worked example in the spec table (`return[<n>]` is only defined for a
27
+ *scalar* tuple element). This module extends that pattern to
28
+ `return[<n>][<m>]` for an array at tuple position `n`, and to a bare
29
+ `return[<n>]` (no outer index) for the single-output-is-an-array case. Both
30
+ are independent design calls, not spec text. If `wire.py` appears with a
31
+ `slots_for_entry` function, this module should import it and drop its own
32
+ copy rather than keep two implementations that can drift apart.
33
+
34
+ Similarly, `ir.py`'s `FunctionDef` carries no field for a function's native
35
+ *return* kind (only the already-collapsed Python type in `.returns`), so
36
+ there is no way to know from the IR alone whether a function's return value
37
+ is `real(4)` or `real(8)`. Rather than extend the shared IR schema for this
38
+ one generator, `generate_driver` accepts an optional `return_kinds` mapping
39
+ (`{entry_key: "real(4)" | "real(8)"}`); an entry absent from it is assumed
40
+ `real(8)`, matching every real routine in this codebase (`petro_api`'s
41
+ `real(dp)` with `dp = kind(1.0d0)`).
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import hashlib
47
+ from dataclasses import dataclass, field
48
+
49
+ from ..ir import FunctionDef, ModuleIR, Parameter
50
+
51
+ # --- result -----------------------------------------------------------
52
+
53
+
54
+ @dataclass
55
+ class DriverResult:
56
+ source: str
57
+ driver_sha256: str
58
+ skipped: dict = field(default_factory=dict)
59
+
60
+
61
+ class _SkipEntry(Exception):
62
+ """Raised internally when one entry cannot be emitted as a call."""
63
+
64
+
65
+ # --- Fortran literal formatting -----------------------------------------
66
+
67
+
68
+ def _float_literal(value) -> str:
69
+ """`repr`-exact double-precision Fortran literal (`d0` exponent)."""
70
+ text = repr(float(value))
71
+ if "e" in text or "E" in text:
72
+ marker = "e" if "e" in text else "E"
73
+ mantissa, _, exponent = text.partition(marker)
74
+ if "." not in mantissa:
75
+ mantissa += ".0"
76
+ return f"{mantissa}d{exponent}"
77
+ if "." not in text:
78
+ text += ".0"
79
+ return f"{text}d0"
80
+
81
+
82
+ def _int_literal(value) -> str:
83
+ return str(int(value))
84
+
85
+
86
+ def _bool_literal(value) -> str:
87
+ return ".true." if value else ".false."
88
+
89
+
90
+ def _str_literal(value: str) -> str:
91
+ return "'" + str(value).replace("'", "''") + "'"
92
+
93
+
94
+ def _quote(text: str) -> str:
95
+ """A Fortran CHARACTER literal for a driver-internal label (key/slot)."""
96
+ return "'" + text.replace("'", "''") + "'"
97
+
98
+
99
+ def _scalar_literal(value, python_type: str) -> str:
100
+ if python_type == "bool":
101
+ return _bool_literal(value)
102
+ if python_type == "int":
103
+ return _int_literal(value)
104
+ if python_type == "float":
105
+ return _float_literal(value)
106
+ if python_type == "str":
107
+ return _str_literal(value)
108
+ raise _SkipEntry(f"no Fortran literal for value {value!r} of type {python_type!r}")
109
+
110
+
111
+ def _array_literal(values: list, python_type: str) -> str:
112
+ return "(/ " + ", ".join(_scalar_literal(v, python_type) for v in values) + " /)"
113
+
114
+
115
+ # --- Fortran type declarations -------------------------------------------
116
+
117
+
118
+ def _is_real4(param: Parameter) -> bool:
119
+ native = (param.native_type or "").lower().replace(" ", "")
120
+ return native in ("real", "real*4", "real(4)", "real(kind=4)")
121
+
122
+
123
+ def _decl_type(python_type: str, is_real4: bool, native_type: str | None) -> str:
124
+ if python_type == "float":
125
+ return "real(4)" if is_real4 else "real(8)"
126
+ if python_type == "int":
127
+ return "integer"
128
+ if python_type == "bool":
129
+ return "logical"
130
+ if python_type == "str":
131
+ if native_type and native_type.lower().startswith("character"):
132
+ return native_type
133
+ return "character(len=256)"
134
+ raise _SkipEntry(f"no Fortran declaration for type {python_type!r}")
135
+
136
+
137
+ # --- per-parameter plan ---------------------------------------------------
138
+
139
+
140
+ @dataclass
141
+ class _Slot:
142
+ """One native call-site argument, resolved for one call."""
143
+
144
+ param: Parameter
145
+ var: str # the local Fortran variable name
146
+ decl: str # the full declaration statement (without trailing newline)
147
+ init: str | None # an assignment statement, or None if uninitialised
148
+ size: int | None # element count, for arrays
149
+
150
+
151
+ def _plan_call(idx: int, key: str, entry: dict, fn: FunctionDef) -> tuple[list[_Slot], list[Parameter]]:
152
+ """Resolve every native parameter for one call.
153
+
154
+ Returns (slots in native parameter order, the python-visible parameter
155
+ list in the same order golden.plan() built the recorded `arguments`).
156
+ """
157
+ if entry.get("kind", "function") != "function":
158
+ raise _SkipEntry(
159
+ "the Fortran driver (v1) only calls free functions/subroutines, "
160
+ f"not entry kind {entry.get('kind')!r}"
161
+ )
162
+
163
+ visible_params = [p for p in fn.parameters if p.intent != "out"]
164
+ arguments = entry.get("arguments") or []
165
+ if len(arguments) != len(visible_params):
166
+ raise _SkipEntry(
167
+ "recorded arguments do not match the current signature "
168
+ f"({len(arguments)} recorded, {len(visible_params)} expected)"
169
+ )
170
+ for value in arguments:
171
+ if isinstance(value, dict) and "__struct__" in value:
172
+ raise _SkipEntry(
173
+ "structs by value are not supported by the Fortran driver (v1)"
174
+ )
175
+
176
+ value_by_name = {p.name: v for p, v in zip(visible_params, arguments)}
177
+
178
+ slots: list[_Slot] = []
179
+ for param in fn.parameters:
180
+ var = f"c{idx}_{param.name}"
181
+ is_real4 = _is_real4(param)
182
+ decl_type = _decl_type(param.type, is_real4, param.native_type)
183
+
184
+ if param.intent != "out":
185
+ value = value_by_name[param.name]
186
+ if param.is_array:
187
+ size = len(value)
188
+ decl = f"{decl_type} :: {var}({size})"
189
+ init = f"{var} = {_array_literal(value, param.type)}"
190
+ else:
191
+ size = None
192
+ decl = f"{decl_type} :: {var}"
193
+ init = f"{var} = {_scalar_literal(value, param.type)}"
194
+ else:
195
+ if param.is_array:
196
+ if not param.length_param:
197
+ raise _SkipEntry(
198
+ f"intent(out) array '{param.name}' has no length_param — "
199
+ "the driver cannot size it at the call site"
200
+ )
201
+ size_value = value_by_name.get(param.length_param)
202
+ if size_value is None:
203
+ raise _SkipEntry(
204
+ f"intent(out) array '{param.name}' is sized by "
205
+ f"'{param.length_param}', which is not a visible argument"
206
+ )
207
+ size = int(size_value)
208
+ decl = f"{decl_type} :: {var}({size})"
209
+ init = None
210
+ else:
211
+ size = None
212
+ decl = f"{decl_type} :: {var}"
213
+ init = None
214
+
215
+ slots.append(_Slot(param=param, var=var, decl=decl, init=init, size=size))
216
+
217
+ return slots, visible_params
218
+
219
+
220
+ # --- wire slots (own copy — see module docstring) -------------------------
221
+
222
+
223
+ def _slots_for_call(entry: dict, fn: FunctionDef, visible_params: list[Parameter]) -> list[str]:
224
+ """The exact slot names this call must print, in the order printed."""
225
+ slots: list[str] = []
226
+
227
+ outputs: list[Parameter | None] = []
228
+ if not fn.is_subroutine and fn.returns != "void":
229
+ outputs.append(None) # None marks "the function's own return value"
230
+ outputs.extend(p for p in fn.parameters if p.intent == "out")
231
+
232
+ if len(outputs) == 1:
233
+ out = outputs[0]
234
+ is_array = out is not None and out.is_array
235
+ if is_array:
236
+ slots.extend(f"return[{n}]" for n in range(_out_size(out, entry, visible_params)))
237
+ else:
238
+ slots.append("return")
239
+ elif len(outputs) > 1:
240
+ for k, out in enumerate(outputs):
241
+ if out is not None and out.is_array:
242
+ size = _out_size(out, entry, visible_params)
243
+ slots.extend(f"return[{k}][{n}]" for n in range(size))
244
+ else:
245
+ slots.append(f"return[{k}]")
246
+
247
+ value_by_name = {p.name: v for p, v in zip(visible_params, entry.get("arguments") or [])}
248
+ for i, param in enumerate(visible_params):
249
+ if param.intent != "inout":
250
+ continue
251
+ if param.is_array:
252
+ size = len(value_by_name[param.name])
253
+ slots.extend(f"arg:{i}[{n}]" for n in range(size))
254
+ else:
255
+ slots.append(f"arg:{i}")
256
+
257
+ return slots
258
+
259
+
260
+ def _out_size(param: Parameter, entry: dict, visible_params: list[Parameter]) -> int:
261
+ value_by_name = {p.name: v for p, v in zip(visible_params, entry.get("arguments") or [])}
262
+ size_value = value_by_name.get(param.length_param)
263
+ if size_value is None:
264
+ raise _SkipEntry(
265
+ f"intent(out) array '{param.name}' is sized by "
266
+ f"'{param.length_param}', which is not a visible argument"
267
+ )
268
+ return int(size_value)
269
+
270
+
271
+ # --- emitting one call's Fortran -------------------------------------------
272
+
273
+
274
+ def _print_scalar_slot(key: str, slot: str, expr: str, python_type: str, is_real4: bool) -> list[str]:
275
+ label = f"write(*,'(A,A1,A,A1,A)') {_quote(key)}, char(9), {_quote(slot)}, char(9), trim(n2p_valstr)"
276
+ if python_type == "float":
277
+ widen = f"n2p_dbl = real({expr}, 8)" if is_real4 else f"n2p_dbl = {expr}"
278
+ return [
279
+ widen,
280
+ "n2p_bits = transfer(n2p_dbl, 0_8)",
281
+ "call n2p_hex64(n2p_bits, n2p_hex)",
282
+ "n2p_valstr = n2p_hex",
283
+ label,
284
+ ]
285
+ if python_type == "int":
286
+ return [f"write(n2p_valstr, '(I0)') {expr}", label]
287
+ if python_type == "bool":
288
+ return [
289
+ f"if ({expr}) then",
290
+ " n2p_valstr = '1'",
291
+ "else",
292
+ " n2p_valstr = '0'",
293
+ "end if",
294
+ label,
295
+ ]
296
+ if python_type == "str":
297
+ return [f"call n2p_escape(trim({expr}), n2p_valstr)", label]
298
+ raise _SkipEntry(f"no wire formatting for type {python_type!r}")
299
+
300
+
301
+ def _emit_call(
302
+ idx: int, key: str, entry: dict, fn: FunctionDef, return_kind: str | None
303
+ ) -> tuple[list[str], list[str]]:
304
+ """Returns (declaration lines, executable-statement lines) for one call.
305
+
306
+ Kept separate because Fortran requires every specification statement in
307
+ a program unit to precede every executable statement — interleaving
308
+ per-call declarations with per-call executable statements (call N's
309
+ `write` before call N+1's `real(8) ::`) does not compile.
310
+ """
311
+ slots, visible_params = _plan_call(idx, key, entry, fn)
312
+ wire_slots = _slots_for_call(entry, fn, visible_params)
313
+
314
+ decls: list[str] = []
315
+ lines: list[str] = []
316
+ for slot in slots:
317
+ decls.append(f" {slot.decl}")
318
+ result_var = None
319
+ result_is_real4 = (return_kind or "").lower().replace(" ", "") in (
320
+ "real",
321
+ "real*4",
322
+ "real(4)",
323
+ "real(kind=4)",
324
+ )
325
+ if not fn.is_subroutine and fn.returns != "void":
326
+ result_var = f"c{idx}_result"
327
+ decl_type = _decl_type(fn.returns, result_is_real4, None)
328
+ decls.append(f" {decl_type} :: {result_var}")
329
+
330
+ for slot in slots:
331
+ if slot.init is not None:
332
+ lines.append(f" {slot.init}")
333
+
334
+ call_args = ", ".join(slot.var for slot in slots)
335
+ if fn.is_subroutine:
336
+ lines.append(f" call {fn.name}({call_args})")
337
+ else:
338
+ lines.append(f" {result_var} = {fn.name}({call_args})")
339
+
340
+ # Outputs, in the order _slots_for_call enumerated them.
341
+ outputs: list[tuple[str, Parameter | None]] = []
342
+ if result_var is not None:
343
+ outputs.append((result_var, None))
344
+ out_params = [(s.var, s.param) for s in slots if s.param.intent == "out"]
345
+ outputs.extend(out_params)
346
+
347
+ slot_iter = iter(wire_slots)
348
+ if len(outputs) == 1:
349
+ var, param = outputs[0]
350
+ python_type = fn.returns if param is None else param.type
351
+ is_real4 = result_is_real4 if param is None else _is_real4(param)
352
+ if param is not None and param.is_array:
353
+ size = next(s.size for s in slots if s.param is param)
354
+ for n in range(size):
355
+ slot_name = next(slot_iter)
356
+ lines.extend(
357
+ _print_scalar_slot(key, slot_name, f"{var}({n + 1})", python_type, is_real4)
358
+ )
359
+ else:
360
+ slot_name = next(slot_iter)
361
+ lines.extend(_print_scalar_slot(key, slot_name, var, python_type, is_real4))
362
+ elif len(outputs) > 1:
363
+ for var, param in outputs:
364
+ python_type = fn.returns if param is None else param.type
365
+ is_real4 = result_is_real4 if param is None else _is_real4(param)
366
+ if param is not None and param.is_array:
367
+ size = next(s.size for s in slots if s.param is param)
368
+ for n in range(size):
369
+ slot_name = next(slot_iter)
370
+ lines.extend(
371
+ _print_scalar_slot(key, slot_name, f"{var}({n + 1})", python_type, is_real4)
372
+ )
373
+ else:
374
+ slot_name = next(slot_iter)
375
+ lines.extend(_print_scalar_slot(key, slot_name, var, python_type, is_real4))
376
+
377
+ # arg:<i> / arg:<i>[<n>] — intent(inout) python-visible arguments.
378
+ for i, param in enumerate(visible_params):
379
+ if param.intent != "inout":
380
+ continue
381
+ slot_obj = next(s for s in slots if s.param is param)
382
+ if param.is_array:
383
+ for n in range(slot_obj.size):
384
+ slot_name = next(slot_iter)
385
+ lines.extend(
386
+ _print_scalar_slot(
387
+ key, slot_name, f"{slot_obj.var}({n + 1})", param.type, False
388
+ )
389
+ )
390
+ else:
391
+ slot_name = next(slot_iter)
392
+ lines.extend(_print_scalar_slot(key, slot_name, slot_obj.var, param.type, False))
393
+
394
+ return decls, lines
395
+
396
+
397
+ # --- helper subprograms, verbatim in every driver --------------------------
398
+
399
+ _HELPERS = """\
400
+ contains
401
+
402
+ subroutine n2p_escape(raw, escaped)
403
+ ! Percent-escape tab (%09), newline (%0A) and percent (%25) so a
404
+ ! CHARACTER value containing one of them cannot break the wire
405
+ ! protocol's line format. `raw` must already be trimmed by the
406
+ ! caller — trailing blanks are the declared string normalisation
407
+ ! (spec sec 2.4), not something this routine decides.
408
+ character(len=*), intent(in) :: raw
409
+ character(len=*), intent(out) :: escaped
410
+ integer :: i, j, code
411
+ character(len=16), parameter :: hexd = '0123456789ABCDEF'
412
+ escaped = ' '
413
+ j = 0
414
+ do i = 1, len_trim(raw)
415
+ code = iachar(raw(i:i))
416
+ if (code == 9 .or. code == 10 .or. code == 37) then
417
+ escaped(j+1:j+1) = '%'
418
+ escaped(j+2:j+2) = hexd(code/16+1:code/16+1)
419
+ escaped(j+3:j+3) = hexd(mod(code,16)+1:mod(code,16)+1)
420
+ j = j + 3
421
+ else
422
+ escaped(j+1:j+1) = raw(i:i)
423
+ j = j + 1
424
+ end if
425
+ end do
426
+ end subroutine n2p_escape
427
+
428
+ subroutine n2p_hex64(bits, hexstr)
429
+ ! 16 lowercase hex digits of an integer(8) bit pattern. The `Z16.16`
430
+ ! edit descriptor prints uppercase, so this lowercases afterwards —
431
+ ! spec sec 2.4 requires lowercase, and this must not depend on any
432
+ ! locale or environment setting to stay deterministic.
433
+ integer(8), intent(in) :: bits
434
+ character(len=16), intent(out) :: hexstr
435
+ integer :: i, code
436
+ write(hexstr, '(Z16.16)') bits
437
+ do i = 1, 16
438
+ code = iachar(hexstr(i:i))
439
+ if (code >= iachar('A') .and. code <= iachar('F')) then
440
+ hexstr(i:i) = achar(code + 32)
441
+ end if
442
+ end do
443
+ end subroutine n2p_hex64
444
+
445
+ end program n2p_oracle_driver
446
+ """
447
+
448
+
449
+ def generate_driver(
450
+ document: dict, module: ModuleIR, return_kinds: dict | None = None
451
+ ) -> DriverResult:
452
+ """Build the driver source for `document`'s recorded entries.
453
+
454
+ `return_kinds` maps an entry key to the native Fortran spelling of that
455
+ function's return kind ("real(4)" vs "real(8)") — see the module
456
+ docstring for why the IR alone cannot answer this. Entries not present
457
+ default to real(8)/double precision.
458
+ """
459
+ return_kinds = return_kinds or {}
460
+ functions = {fn.name: fn for fn in module.functions}
461
+ skipped: dict = dict(document.get("skipped") or {})
462
+
463
+ all_decls: list[str] = []
464
+ all_execs: list[str] = []
465
+ for idx, (key, entry) in enumerate(sorted_entries(document)):
466
+ fn = functions.get(entry.get("name") or key)
467
+ if fn is None:
468
+ skipped[key] = "no matching Fortran function in the IR"
469
+ continue
470
+ try:
471
+ decls, execs = _emit_call(idx, key, entry, fn, return_kinds.get(key))
472
+ except _SkipEntry as exc:
473
+ skipped[key] = str(exc)
474
+ continue
475
+ all_decls.append(f" ! --- {key} ---")
476
+ all_decls.extend(decls)
477
+ all_execs.append(f" ! --- {key} ---")
478
+ all_execs.extend(execs)
479
+
480
+ source = _assemble(module, all_decls, all_execs)
481
+ digest = hashlib.sha256(source.encode("utf-8")).hexdigest()
482
+ return DriverResult(source=source, driver_sha256=digest, skipped=skipped)
483
+
484
+
485
+ def sorted_entries(document: dict):
486
+ """`document["entries"]` in file order — never re-sorted (spec sec 2.2)."""
487
+ return list((document.get("entries") or {}).items())
488
+
489
+
490
+ def _assemble(module: ModuleIR, decls: list[str], execs: list[str]) -> str:
491
+ use_line = f" use {module.fortran_module}\n" if module.fortran_module else ""
492
+ decl_body = "\n".join(decls)
493
+ exec_body = "\n".join(execs)
494
+ return (
495
+ "program n2p_oracle_driver\n"
496
+ f"{use_line}"
497
+ " implicit none\n"
498
+ " real(8) :: n2p_dbl\n"
499
+ " integer(8) :: n2p_bits\n"
500
+ " character(len=16) :: n2p_hex\n"
501
+ " character(len=512) :: n2p_valstr\n"
502
+ f"{decl_body}\n"
503
+ "\n"
504
+ f"{exec_body}\n"
505
+ "\n"
506
+ f"{_HELPERS}"
507
+ )
File without changes
@@ -0,0 +1,101 @@
1
+ """CMake build generator (design.md section 14)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..ir import ModuleIR
6
+
7
+ # CMake target names can't contain hyphens the way directory names can, and
8
+ # libraries/common-cpp/ declares `project(common_cpp)` / `add_library(common_cpp)`.
9
+ def _library_target(library_dir_name: str) -> str:
10
+ return library_dir_name.replace("-", "_")
11
+
12
+
13
+ def uses_numpy_buffers(module: ModuleIR) -> bool:
14
+ """True when any binding goes through `pybind11/numpy.h`.
15
+
16
+ A bare `T*` paired with a length argument is bound as a numpy buffer, which
17
+ needs numpy's HEADERS at build time and numpy at runtime. A C++ service
18
+ that binds no such pointer should not acquire either, so this is asked
19
+ rather than assumed — see pybind_gen._buffer_lambda.
20
+ """
21
+ return any(
22
+ getattr(p, "length_param", None)
23
+ for fn in module.functions
24
+ for p in fn.parameters
25
+ )
26
+
27
+
28
+ def generate_cmake(
29
+ module: ModuleIR,
30
+ service_name: str,
31
+ native_sources: list[str],
32
+ libraries: list[str] | None = None,
33
+ include_paths: list[str] | None = None,
34
+ ) -> str:
35
+ module_symbol = f"{module.name}_cpp"
36
+ sources = "\n".join(f" {src}" for src in native_sources)
37
+ libraries = libraries or []
38
+
39
+ # `include_paths:` entries are repo-root-relative, and the service's
40
+ # CMakeLists.txt lives two levels down at services/<name>/.
41
+ extra_includes = "".join(
42
+ f"\n ${{CMAKE_CURRENT_SOURCE_DIR}}/../../{p}" for p in (include_paths or [])
43
+ )
44
+ if extra_includes:
45
+ extra_includes = extra_includes + "\n"
46
+
47
+ library_block = ""
48
+ link_block = ""
49
+ if libraries:
50
+ # add_subdirectory needs a distinct binary dir per library because the
51
+ # source lives outside this project's tree (../../libraries/<name>).
52
+ subdirs = "\n".join(
53
+ f"add_subdirectory(${{CMAKE_CURRENT_SOURCE_DIR}}/../../libraries/{lib} "
54
+ f"${{CMAKE_CURRENT_BINARY_DIR}}/_libraries/{lib})"
55
+ for lib in libraries
56
+ )
57
+ targets = " ".join(_library_target(lib) for lib in libraries)
58
+ library_block = (
59
+ "\n# Shared native libraries (design.md section 4). Each declares its own\n"
60
+ "# CMake target; include directories come through PUBLIC usage requirements.\n"
61
+ f"{subdirs}\n"
62
+ )
63
+ link_block = f"\ntarget_link_libraries({module_symbol} PRIVATE {targets})\n"
64
+
65
+ # `pybind11/numpy.h` includes numpy's own headers, which live inside the
66
+ # installed numpy package rather than anywhere CMake looks by default.
67
+ numpy_block = ""
68
+ numpy_link = ""
69
+ if uses_numpy_buffers(module):
70
+ numpy_block = (
71
+ "\n# A raw `T*` argument is bound as a numpy buffer, so pybind11/numpy.h\n"
72
+ "# needs numpy's headers. Asked of the interpreter that will import the\n"
73
+ "# extension, so the headers always match the numpy it runs against.\n"
74
+ "find_package(Python REQUIRED COMPONENTS Interpreter Development.Module NumPy)\n"
75
+ )
76
+ numpy_link = (
77
+ f"\ntarget_link_libraries({module_symbol} PRIVATE Python::NumPy)\n"
78
+ )
79
+
80
+ return f"""cmake_minimum_required(VERSION 3.18)
81
+ project({service_name} LANGUAGES CXX)
82
+
83
+ set(CMAKE_CXX_STANDARD 17)
84
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
85
+
86
+ # Emitted unconditionally so a normal `ngate build` leaves a
87
+ # compile_commands.json in the build directory, the same way the f2py/meson
88
+ # path does through meson's own default — see buildinfo.py, which reads this
89
+ # file to extract per-source compile flags for the C++ oracle driver.
90
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
91
+
92
+ find_package(pybind11 CONFIG REQUIRED)
93
+ {numpy_block}{library_block}
94
+ pybind11_add_module({module_symbol}
95
+ {sources}
96
+ )
97
+
98
+ target_include_directories({module_symbol} PRIVATE native{extra_includes})
99
+ {numpy_link}{link_block}
100
+ install(TARGETS {module_symbol} LIBRARY DESTINATION {service_name}/_native)
101
+ """