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