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
nativegate/wire.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""The bit-emission wire protocol shared by the oracle's two sides.
|
|
2
|
+
|
|
3
|
+
Layer 2 (design-verification-layers.md, spec section 2) compares a native
|
|
4
|
+
driver's output against the Python binding's output *bitwise*, in the same
|
|
5
|
+
build, on the same machine, in the same run. Both sides have to agree, byte
|
|
6
|
+
for byte, on how a value becomes text and back — that agreement lives here,
|
|
7
|
+
once, so the Fortran driver generator (T3), the C++ driver generator (T7),
|
|
8
|
+
and the comparator (T5) cannot drift into disagreeing with each other about
|
|
9
|
+
what a line means.
|
|
10
|
+
|
|
11
|
+
This module owns both directions:
|
|
12
|
+
|
|
13
|
+
* **Emitting** — turning a Python value into the wire's `<value>` text
|
|
14
|
+
(`pack_float`, `format_value`) and a whole line (`format_line`).
|
|
15
|
+
* **Parsing** — turning driver stdout back into structured data
|
|
16
|
+
(`parse_line`, `parse_lines`) and turning a `slot` string into a typed
|
|
17
|
+
`Slot` (`Slot.parse`).
|
|
18
|
+
* **The plan** — given one golden.json entry and the IR signature it was
|
|
19
|
+
recorded against, the expected list of slots (`slots_for_entry`). T3/T7
|
|
20
|
+
generate their print statements from this list; T5 compares against it.
|
|
21
|
+
This is the single source of truth other generators read from, so "which
|
|
22
|
+
slots does this call produce" is answered in exactly one place.
|
|
23
|
+
|
|
24
|
+
Wire format (spec section 2.5)
|
|
25
|
+
-------------------------------
|
|
26
|
+
|
|
27
|
+
One line per observable value, tab-separated::
|
|
28
|
+
|
|
29
|
+
<key>\\t<slot>\\t<value>
|
|
30
|
+
|
|
31
|
+
`<value>` is 16 lowercase hex digits for floats (`pack_float`), a decimal
|
|
32
|
+
integer for integers, `0`/`1` for booleans, or a percent-escaped string
|
|
33
|
+
otherwise (`escape_string`).
|
|
34
|
+
|
|
35
|
+
Slot grammar (spec section 2.5's table)
|
|
36
|
+
----------------------------------------
|
|
37
|
+
|
|
38
|
+
====================== ===========================================
|
|
39
|
+
``return`` the function's return value
|
|
40
|
+
``return[<n>]`` element ``n`` of a tuple return — f2py
|
|
41
|
+
surfaces ``intent(out)`` scalars/arrays as
|
|
42
|
+
return values, not as argument effects
|
|
43
|
+
``arg:<i>`` scalar argument ``i`` after the call
|
|
44
|
+
(``intent(inout)``)
|
|
45
|
+
``arg:<i>[<n>]`` element ``n`` of array argument ``i``
|
|
46
|
+
====================== ===========================================
|
|
47
|
+
|
|
48
|
+
**`<i>` counts over the Python-visible argument list** — the same index
|
|
49
|
+
space as golden.json's `arguments` array and `argument_effects` keys. This
|
|
50
|
+
is deliberately *not* the native call site's parameter index: `plan()`
|
|
51
|
+
(golden.py) drops `intent(out)` parameters from the Python argument list,
|
|
52
|
+
and an f2py call site additionally carries hidden `CHARACTER` length
|
|
53
|
+
arguments the Python signature never sees. The driver generator owns the
|
|
54
|
+
mapping from this Python-visible index to whatever native position the
|
|
55
|
+
value actually lives at; the wire format itself never exposes native
|
|
56
|
+
positions, so a comparator reading the wire never needs to know the native
|
|
57
|
+
calling convention at all.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import re
|
|
63
|
+
import struct
|
|
64
|
+
from dataclasses import dataclass
|
|
65
|
+
|
|
66
|
+
from .ir import FunctionDef
|
|
67
|
+
|
|
68
|
+
# --- floats: the bitwise channel (spec section 2.4) ----------------------
|
|
69
|
+
|
|
70
|
+
# "Big-endian, and this is not a preference." Both native sides print the
|
|
71
|
+
# *integer value* of the reinterpreted bits, most-significant nibble first,
|
|
72
|
+
# regardless of host byte order — struct.pack(">d", ...) matches that; the
|
|
73
|
+
# native-endian "<d" produces the byte-reversed string on every little-endian
|
|
74
|
+
# host nativegate runs on, and every comparison would fail. The harness must
|
|
75
|
+
# assert this on a known constant at startup rather than trust it (spec
|
|
76
|
+
# section 2.4's closing paragraph) — this constant and its expected hex are
|
|
77
|
+
# copied verbatim from the spec, which verified them against gfortran's
|
|
78
|
+
# `Z16.16`, clang's `%016llx`, and Python's `struct.pack(">d", ...).hex()`.
|
|
79
|
+
_SELF_CHECK_VALUE = 355.0764805578969
|
|
80
|
+
_SELF_CHECK_HEX = "4076313943ad6f27"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class WireEndiannessError(RuntimeError):
|
|
84
|
+
"""The float channel is not big-endian on this host/interpreter.
|
|
85
|
+
|
|
86
|
+
This should be unreachable in practice — struct's ">d" format is
|
|
87
|
+
byte-order-explicit by definition — but the bitwise comparison this
|
|
88
|
+
module exists for is worthless if it is ever wrong, so it is checked
|
|
89
|
+
rather than assumed.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _self_check() -> None:
|
|
94
|
+
actual = struct.pack(">d", _SELF_CHECK_VALUE).hex()
|
|
95
|
+
if actual != _SELF_CHECK_HEX:
|
|
96
|
+
raise WireEndiannessError(
|
|
97
|
+
f"big-endian float self-check failed: struct.pack('>d', "
|
|
98
|
+
f"{_SELF_CHECK_VALUE!r}).hex() == {actual!r}, expected "
|
|
99
|
+
f"{_SELF_CHECK_HEX!r}. The oracle's bitwise comparison is "
|
|
100
|
+
"meaningless if this does not hold — refusing to proceed."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Run once per process import, not per call: pack_float is on the hot path
|
|
105
|
+
# for every slot of every entry, and the check result cannot change between
|
|
106
|
+
# calls in the same interpreter.
|
|
107
|
+
_self_check()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def pack_float(value: float) -> str:
|
|
111
|
+
"""The wire's `<value>` text for a float: 16 lowercase hex digits.
|
|
112
|
+
|
|
113
|
+
`struct.pack(">d", value).hex()` — big-endian IEEE-754 double, not
|
|
114
|
+
`repr` and not `float.hex()`. A `real(4)`/`float` value must be widened
|
|
115
|
+
to double *before* it reaches here (spec section 2.4: widening is exact,
|
|
116
|
+
so the bitwise claim survives; a 32-bit value transferred directly into
|
|
117
|
+
a 64-bit slot has undefined padding bits). Widening is the driver
|
|
118
|
+
generator's job (T3/T7), not this function's — `pack_float` always packs
|
|
119
|
+
a full 64-bit double.
|
|
120
|
+
"""
|
|
121
|
+
return struct.pack(">d", value).hex()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def unpack_float(hex_digits: str) -> float:
|
|
125
|
+
"""The inverse of `pack_float`, for tests and for reading driver output
|
|
126
|
+
back into a comparable float where a comparator wants the numeric value
|
|
127
|
+
rather than the hex text (the default and preferred comparison is
|
|
128
|
+
hex-text equality, which sidesteps NaN != NaN)."""
|
|
129
|
+
if len(hex_digits) != 16:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"not a 16-hex-digit float slot value: {hex_digits!r}"
|
|
132
|
+
)
|
|
133
|
+
return struct.unpack(">d", bytes.fromhex(hex_digits))[0]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --- non-float value channel (spec section 2.4/2.5) -----------------------
|
|
137
|
+
|
|
138
|
+
# Escape nothing else: the design is explicit that %09/%0A/%25 are the whole
|
|
139
|
+
# alphabet. Order matters — '%' must be escaped first, or the '%' introduced
|
|
140
|
+
# by escaping tab/newline would itself be re-escaped.
|
|
141
|
+
_ESCAPES = (("%", "%25"), ("\t", "%09"), ("\n", "%0A"))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def escape_string(value: str) -> str:
|
|
145
|
+
"""Percent-escape a string for the wire's value channel.
|
|
146
|
+
|
|
147
|
+
Escapes only tab (`%09`), newline (`%0A`) and percent (`%25`) — nothing
|
|
148
|
+
else — so a `CHARACTER` output containing a tab or newline cannot break
|
|
149
|
+
the `<key>\\t<slot>\\t<value>` line protocol (spec section 2.4).
|
|
150
|
+
"""
|
|
151
|
+
for raw, escaped in _ESCAPES:
|
|
152
|
+
value = value.replace(raw, escaped)
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_UNESCAPE_RE = re.compile("%09|%0A|%25")
|
|
157
|
+
_UNESCAPE_MAP = {"%09": "\t", "%0A": "\n", "%25": "%"}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def unescape_string(value: str) -> str:
|
|
161
|
+
"""The inverse of `escape_string`."""
|
|
162
|
+
return _UNESCAPE_RE.sub(lambda m: _UNESCAPE_MAP[m.group(0)], value)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class WireValueError(ValueError):
|
|
166
|
+
"""A Python value has no representation in the wire's value channel."""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def format_value(value) -> str:
|
|
170
|
+
"""The wire's `<value>` text for any scalar the protocol carries.
|
|
171
|
+
|
|
172
|
+
Dispatches on Python type: `bool` -> `0`/`1`, `int` -> decimal, `float`
|
|
173
|
+
-> `pack_float` (16 hex digits), `str` -> `escape_string`. `bool` is
|
|
174
|
+
checked before `int` because `bool` is an `int` subclass in Python.
|
|
175
|
+
"""
|
|
176
|
+
if isinstance(value, bool):
|
|
177
|
+
return "1" if value else "0"
|
|
178
|
+
if isinstance(value, int):
|
|
179
|
+
return str(value)
|
|
180
|
+
if isinstance(value, float):
|
|
181
|
+
return pack_float(value)
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
return escape_string(value)
|
|
184
|
+
raise WireValueError(
|
|
185
|
+
f"no wire representation for {value!r} of type {type(value).__name__}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- slot grammar (spec section 2.5) ---------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SlotParseError(ValueError):
|
|
193
|
+
"""A `slot` field could not be parsed as one of the four wire forms."""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_RETURN_RE = re.compile(r"^return(?:\[(\d+)\])?$")
|
|
197
|
+
_ARG_RE = re.compile(r"^arg:(\d+)(?:\[(\d+)\])?$")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(frozen=True)
|
|
201
|
+
class Slot:
|
|
202
|
+
"""One observable value's address within a call.
|
|
203
|
+
|
|
204
|
+
`role` is `"return"` or `"arg"`. For `"arg"`, `arg_index` is the
|
|
205
|
+
Python-visible argument index (see module docstring — golden.json's
|
|
206
|
+
`arguments`/`argument_effects` index space, not the native parameter
|
|
207
|
+
position). `element` is the sub-index for `return[<n>]`/`arg:<i>[<n>]`
|
|
208
|
+
(a tuple-return position or an array element) and is `None` for the
|
|
209
|
+
scalar forms `return`/`arg:<i>`.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
role: str # "return" | "arg"
|
|
213
|
+
arg_index: int | None = None
|
|
214
|
+
element: int | None = None
|
|
215
|
+
|
|
216
|
+
def __post_init__(self) -> None:
|
|
217
|
+
if self.role not in ("return", "arg"):
|
|
218
|
+
raise SlotParseError(f"unknown slot role {self.role!r}")
|
|
219
|
+
if self.role == "arg" and self.arg_index is None:
|
|
220
|
+
raise SlotParseError("an 'arg' slot needs arg_index")
|
|
221
|
+
if self.role == "return" and self.arg_index is not None:
|
|
222
|
+
raise SlotParseError("a 'return' slot must not carry arg_index")
|
|
223
|
+
|
|
224
|
+
def __str__(self) -> str:
|
|
225
|
+
base = "return" if self.role == "return" else f"arg:{self.arg_index}"
|
|
226
|
+
return base if self.element is None else f"{base}[{self.element}]"
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def parse(cls, text: str) -> "Slot":
|
|
230
|
+
match = _RETURN_RE.match(text)
|
|
231
|
+
if match:
|
|
232
|
+
element = match.group(1)
|
|
233
|
+
return cls("return", element=int(element) if element is not None else None)
|
|
234
|
+
match = _ARG_RE.match(text)
|
|
235
|
+
if match:
|
|
236
|
+
index, element = match.groups()
|
|
237
|
+
return cls(
|
|
238
|
+
"arg",
|
|
239
|
+
arg_index=int(index),
|
|
240
|
+
element=int(element) if element is not None else None,
|
|
241
|
+
)
|
|
242
|
+
raise SlotParseError(
|
|
243
|
+
f"malformed slot {text!r}: expected 'return', 'return[<n>]', "
|
|
244
|
+
"'arg:<i>' or 'arg:<i>[<n>]'"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def return_slot(element: int | None = None) -> Slot:
|
|
249
|
+
return Slot("return", element=element)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def arg_slot(index: int, element: int | None = None) -> Slot:
|
|
253
|
+
return Slot("arg", arg_index=index, element=element)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# --- line format (spec section 2.5) -----------------------------------------
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class WireFormatError(ValueError):
|
|
260
|
+
"""A line, or a stream of lines, violates the wire protocol.
|
|
261
|
+
|
|
262
|
+
Always carries the offending line's text (spec: "a malformed line,
|
|
263
|
+
duplicate (key, slot), or key out of expected order is an error carrying
|
|
264
|
+
the offending line text") — never just an index or a summary, so the
|
|
265
|
+
failure is legible without re-running the driver.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def format_line(key: str, slot: "Slot | str", value) -> str:
|
|
270
|
+
"""One wire line: `<key>\\t<slot>\\t<value>`.
|
|
271
|
+
|
|
272
|
+
`value` is the already-Python-typed value (a float, int, bool or str);
|
|
273
|
+
`format_value` produces its `<value>` text. Pass a pre-formatted string
|
|
274
|
+
only if the caller has already escaped/packed it itself.
|
|
275
|
+
"""
|
|
276
|
+
slot_text = str(slot)
|
|
277
|
+
value_text = value if isinstance(value, str) and _looks_preformatted(value) else format_value(value)
|
|
278
|
+
return f"{key}\t{slot_text}\t{value_text}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _looks_preformatted(_value: str) -> bool:
|
|
282
|
+
# format_value already handles str via escape_string; there is no case
|
|
283
|
+
# where a caller needs to bypass it. Kept as an explicit hook (rather
|
|
284
|
+
# than silently double-escaping) in case a future caller hands in text
|
|
285
|
+
# it already escaped itself — today, always False.
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def parse_line(line: str) -> tuple[str, Slot, str]:
|
|
290
|
+
"""Split one wire line into `(key, slot, raw_value_text)`.
|
|
291
|
+
|
|
292
|
+
`raw_value_text` is returned unparsed (still hex/decimal/escaped) —
|
|
293
|
+
callers that know the expected type call `unpack_float`/`int`/
|
|
294
|
+
`unescape_string` themselves, because this function has no way to know
|
|
295
|
+
which channel a given slot uses without the IR.
|
|
296
|
+
|
|
297
|
+
Raises `WireFormatError` carrying the offending line on anything that
|
|
298
|
+
is not exactly three tab-separated fields, or whose slot field does not
|
|
299
|
+
parse.
|
|
300
|
+
"""
|
|
301
|
+
fields = line.split("\t")
|
|
302
|
+
if len(fields) != 3:
|
|
303
|
+
raise WireFormatError(
|
|
304
|
+
f"malformed wire line (expected 3 tab-separated fields, got "
|
|
305
|
+
f"{len(fields)}): {line!r}"
|
|
306
|
+
)
|
|
307
|
+
key, slot_text, value_text = fields
|
|
308
|
+
if not key:
|
|
309
|
+
raise WireFormatError(f"malformed wire line (empty key): {line!r}")
|
|
310
|
+
try:
|
|
311
|
+
slot = Slot.parse(slot_text)
|
|
312
|
+
except SlotParseError as exc:
|
|
313
|
+
raise WireFormatError(f"malformed wire line ({exc}): {line!r}") from exc
|
|
314
|
+
return key, slot, value_text
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def parse_lines(lines, expected_key_order: list[str] | None = None) -> dict[str, dict[Slot, str]]:
|
|
318
|
+
"""Parse a whole driver output stream into `{key: {slot: raw_value}}`.
|
|
319
|
+
|
|
320
|
+
Enforces, across the whole stream, the three failure modes spec section
|
|
321
|
+
2.5 names: a malformed line, a duplicate `(key, slot)` pair, or a key
|
|
322
|
+
appearing out of the expected order. Each is a hard `WireFormatError`
|
|
323
|
+
carrying the offending line's text — never a skip, per spec section 2.2
|
|
324
|
+
("the driver executes exactly the file's entries ... nothing after it is
|
|
325
|
+
compared").
|
|
326
|
+
|
|
327
|
+
`expected_key_order` is the call order the driver was generated from
|
|
328
|
+
(golden.json's entry order, spec section 2.2) — omit it to skip the
|
|
329
|
+
ordering check (e.g. when parsing a partial stream for a unit test).
|
|
330
|
+
"""
|
|
331
|
+
result: dict[str, dict[Slot, str]] = {}
|
|
332
|
+
seen_pairs: set[tuple[str, Slot]] = set()
|
|
333
|
+
expected = list(expected_key_order) if expected_key_order is not None else None
|
|
334
|
+
next_expected_position = 0
|
|
335
|
+
key_position: dict[str, int] = {}
|
|
336
|
+
|
|
337
|
+
for raw_line in lines:
|
|
338
|
+
line = raw_line.rstrip("\n")
|
|
339
|
+
if line == "":
|
|
340
|
+
continue
|
|
341
|
+
key, slot, value_text = parse_line(line)
|
|
342
|
+
|
|
343
|
+
if expected is not None:
|
|
344
|
+
if key not in key_position:
|
|
345
|
+
# First time this key appears: it must be the next one due,
|
|
346
|
+
# per the file's (and therefore the driver's) call order.
|
|
347
|
+
if next_expected_position >= len(expected) or expected[next_expected_position] != key:
|
|
348
|
+
raise WireFormatError(
|
|
349
|
+
f"key {key!r} appears out of the expected call order "
|
|
350
|
+
f"(expected {expected[next_expected_position:next_expected_position + 1] or '(end)'} next): "
|
|
351
|
+
f"{line!r}"
|
|
352
|
+
)
|
|
353
|
+
key_position[key] = next_expected_position
|
|
354
|
+
next_expected_position += 1
|
|
355
|
+
# A later line for an already-started key is fine (multiple
|
|
356
|
+
# slots per call); only the key's *first* appearance is ordered.
|
|
357
|
+
|
|
358
|
+
pair = (key, slot)
|
|
359
|
+
if pair in seen_pairs:
|
|
360
|
+
raise WireFormatError(
|
|
361
|
+
f"duplicate slot {slot} for key {key!r}: {line!r}"
|
|
362
|
+
)
|
|
363
|
+
seen_pairs.add(pair)
|
|
364
|
+
result.setdefault(key, {})[slot] = value_text
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# --- slots_for_entry: the source of truth (spec section 2.5, T3/T7/T5) ----
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def slots_for_entry(entry: dict, ir_function: FunctionDef) -> list[Slot]:
|
|
373
|
+
"""The expected slot list for one golden.json entry.
|
|
374
|
+
|
|
375
|
+
This is the single source of truth the Fortran/C++ driver generators
|
|
376
|
+
(T3/T7) emit their print statements from, and the comparator (T5)
|
|
377
|
+
compares against — so "which slots does this call produce" is answered
|
|
378
|
+
in exactly one place rather than three generators independently
|
|
379
|
+
reverse-engineering f2py/pybind11's output conventions.
|
|
380
|
+
|
|
381
|
+
`entry` is one value of golden.json's `"entries"` mapping (as `golden.py`
|
|
382
|
+
writes it): `{"arguments": [...], "result": ..., "argument_effects":
|
|
383
|
+
{...}, ...}`. `ir_function` is that entry's `FunctionDef` from the IR
|
|
384
|
+
(used to confirm whether the routine has a return path at all — a
|
|
385
|
+
subroutine with no `intent(out)` parameter and no recorded result has
|
|
386
|
+
no `return` slot).
|
|
387
|
+
|
|
388
|
+
Slot derivation:
|
|
389
|
+
|
|
390
|
+
* **Return.** `ir_function` has a return path when its declared return
|
|
391
|
+
type is not `"void"`, or it declares at least one `intent(out)`
|
|
392
|
+
parameter (f2py folds those into the return tuple — spec section
|
|
393
|
+
2.5's table). Where there is a return path, the *shape* of the
|
|
394
|
+
recorded `entry["result"]` decides the slot(s): `None` -> no slot (a
|
|
395
|
+
subroutine whose only outputs are `intent(inout)` arguments, which
|
|
396
|
+
golden.py already records as `None` — see golden.py's `invoke`
|
|
397
|
+
docstring); a scalar (`int`/`float`/`bool`/`str`) -> one `return`
|
|
398
|
+
slot; a flat list -> `return[<n>]` for each element, in order (f2py's
|
|
399
|
+
tuple-of-outputs convention). A function declaring a return path whose
|
|
400
|
+
recorded result is `None` is a golden-recording anomaly, not a driver
|
|
401
|
+
bug, and is treated the same as "no return slot" here — the mismatch
|
|
402
|
+
would already have been caught upstream by golden itself.
|
|
403
|
+
* **Arguments.** One `arg:<i>` (or, for a list-valued effect,
|
|
404
|
+
`arg:<i>[<n>]` per element) for every key in the recorded
|
|
405
|
+
`entry["argument_effects"]` — golden.py only records an effect for an
|
|
406
|
+
argument the call actually modified in place, and that is exactly the
|
|
407
|
+
set of argument positions this layer needs to observe. `<i>` is
|
|
408
|
+
already the Python-visible index (golden.py builds
|
|
409
|
+
`argument_effects` keyed that way), so it is used as-is — see the
|
|
410
|
+
module docstring for why this is *not* the native parameter index.
|
|
411
|
+
|
|
412
|
+
Order: `return`/`return[<n>]` slots first (in tuple order), then
|
|
413
|
+
`arg:<i>[...]` slots in ascending argument-index order (and ascending
|
|
414
|
+
element order within one argument) — the order the driver generator
|
|
415
|
+
emits print statements in and the order a comparator walks.
|
|
416
|
+
"""
|
|
417
|
+
slots: list[Slot] = []
|
|
418
|
+
|
|
419
|
+
has_return_path = ir_function.returns != "void" or any(
|
|
420
|
+
p.intent == "out" for p in ir_function.parameters
|
|
421
|
+
)
|
|
422
|
+
result = entry.get("result")
|
|
423
|
+
if has_return_path and result is not None:
|
|
424
|
+
if isinstance(result, list):
|
|
425
|
+
slots.extend(return_slot(i) for i in range(len(result)))
|
|
426
|
+
else:
|
|
427
|
+
slots.append(return_slot())
|
|
428
|
+
|
|
429
|
+
effects = entry.get("argument_effects") or {}
|
|
430
|
+
for index_text in sorted(effects, key=int):
|
|
431
|
+
index = int(index_text)
|
|
432
|
+
value = effects[index_text]
|
|
433
|
+
if isinstance(value, list):
|
|
434
|
+
slots.extend(arg_slot(index, i) for i in range(len(value)))
|
|
435
|
+
else:
|
|
436
|
+
slots.append(arg_slot(index))
|
|
437
|
+
|
|
438
|
+
return slots
|