pythsol 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.
- pythsol/__init__.py +317 -0
- pythsol/cli/__init__.py +7 -0
- pythsol/cli/cache.py +78 -0
- pythsol/cli/cluster.py +45 -0
- pythsol/cli/commands/build.py +214 -0
- pythsol/cli/commands/check.py +54 -0
- pythsol/cli/commands/client_cmd.py +97 -0
- pythsol/cli/commands/deploy.py +103 -0
- pythsol/cli/commands/docs_cmd.py +62 -0
- pythsol/cli/commands/doctor.py +211 -0
- pythsol/cli/commands/emit_c.py +69 -0
- pythsol/cli/commands/faucet.py +66 -0
- pythsol/cli/commands/idl_cmd.py +69 -0
- pythsol/cli/commands/idl_publish.py +80 -0
- pythsol/cli/commands/init.py +65 -0
- pythsol/cli/commands/port_cmd.py +42 -0
- pythsol/cli/commands/profile.py +57 -0
- pythsol/cli/commands/serve.py +52 -0
- pythsol/cli/commands/test_cmd.py +217 -0
- pythsol/cli/commands/verify.py +96 -0
- pythsol/cli/commands/wallet.py +128 -0
- pythsol/cli/config.py +84 -0
- pythsol/cli/main.py +72 -0
- pythsol/cli/rpc.py +56 -0
- pythsol/cli/template_programs/__init__.py +1 -0
- pythsol/cli/template_programs/counter.py +52 -0
- pythsol/cli/template_programs/escrow.py +75 -0
- pythsol/cli/template_programs/ledger.py +68 -0
- pythsol/cli/template_programs/multisig.py +91 -0
- pythsol/cli/template_programs/poll.py +70 -0
- pythsol/cli/template_programs/vault.py +67 -0
- pythsol/cli/templates.py +192 -0
- pythsol/client/__init__.py +5 -0
- pythsol/client/from_idl.py +356 -0
- pythsol/client/gen.py +496 -0
- pythsol/client/port.py +355 -0
- pythsol/compiler/__init__.py +39 -0
- pythsol/compiler/diagnostics.py +130 -0
- pythsol/compiler/emit.py +3550 -0
- pythsol/compiler/front.py +84 -0
- pythsol/compiler/idl.py +465 -0
- pythsol/compiler/model.py +1329 -0
- pythsol/compiler/modules.py +320 -0
- pythsol/compiler/parse.py +59 -0
- pythsol/compiler/pipeline.py +152 -0
- pythsol/compiler/resolve.py +518 -0
- pythsol/compiler/subset.py +1061 -0
- pythsol/compiler/toolchain.py +438 -0
- pythsol/compiler/typecheck.py +2447 -0
- pythsol/playground/__init__.py +7 -0
- pythsol/playground/app.css +182 -0
- pythsol/playground/app.js +260 -0
- pythsol/playground/apple-touch-icon.png +0 -0
- pythsol/playground/docs.html +61 -0
- pythsol/playground/favicon-32.png +0 -0
- pythsol/playground/favicon.ico +0 -0
- pythsol/playground/favicon.svg +24 -0
- pythsol/playground/hero-bg.webp +0 -0
- pythsol/playground/index.html +66 -0
- pythsol/playground/landing.html +810 -0
- pythsol/playground/playground.html +77 -0
- pythsol/playground/robots.txt +4 -0
- pythsol/playground/server.py +336 -0
- pythsol/playground/site-bg.svg +146 -0
- pythsol/playground/site.css +1105 -0
- pythsol/py.typed +0 -0
- pythsol/refrt/__init__.py +200 -0
- pythsol/runtime/__init__.py +17 -0
- pythsol/runtime/pythsol_rt.h +1216 -0
- pythsol/testing/__init__.py +12 -0
- pythsol/testing/plugin.py +437 -0
- pythsol-0.1.0.dist-info/METADATA +178 -0
- pythsol-0.1.0.dist-info/RECORD +76 -0
- pythsol-0.1.0.dist-info/WHEEL +4 -0
- pythsol-0.1.0.dist-info/entry_points.txt +6 -0
- pythsol-0.1.0.dist-info/licenses/LICENSE +202 -0
pythsol/compiler/emit.py
ADDED
|
@@ -0,0 +1,3550 @@
|
|
|
1
|
+
"""Stage 6 — lower a :class:`ProgramModel` into the generated C source.
|
|
2
|
+
|
|
3
|
+
The emitter is deliberately boring C: no compound evaluation tricks, no
|
|
4
|
+
pointer arithmetic beyond ``data + offset``, every read/write of an account
|
|
5
|
+
field goes through a byte-wise helper, and every arithmetic operation is
|
|
6
|
+
overflow-checked with a source-tagged abort. Golden invariants (TRD §16,
|
|
7
|
+
"Emitter invariants"):
|
|
8
|
+
|
|
9
|
+
* Python evaluation order is preserved via statement-level temporaries;
|
|
10
|
+
* references alias account data in-place (the runtime's zero-copy view);
|
|
11
|
+
* field writes only happen after the runtime proved
|
|
12
|
+
``data_len == ACCOUNT.size`` (mut) or created fresh accounts via cpi (init);
|
|
13
|
+
* runtime failure logs always carry ``(source.py:LINE)``;
|
|
14
|
+
* the stack is the only memory an instruction touches beyond account data.
|
|
15
|
+
|
|
16
|
+
Lowered subset:
|
|
17
|
+
|
|
18
|
+
* account params: ``Signer``, ``Mut[X]``, ``Account[X]``, ``Init[X]``;
|
|
19
|
+
* data params and locals: every integer width through 128, ``bool``, ``Pubkey``,
|
|
20
|
+
and unit enums (one ``uint8_t``, variant ``Status.Open`` is the tag literal);
|
|
21
|
+
* ``FixedArray[scalar, N]`` fields and locals, indexed read and write
|
|
22
|
+
(the scalar may be a unit enum);
|
|
23
|
+
* ``Option[scalar]`` fields, locals, and instruction arguments: a tag byte
|
|
24
|
+
plus the inner value always reserved. ``None`` clears the tag. ``.is_some``
|
|
25
|
+
and ``.value`` read it; ``.value`` on a clear tag aborts with 1012;
|
|
26
|
+
* ``FixedString[N]`` fields, locals, and instruction arguments: a ``u32``
|
|
27
|
+
byte length plus ``N`` reserved bytes. A literal that fits stores the
|
|
28
|
+
UTF-8 bytes. ``len`` reads the stored count. A length past ``N`` aborts
|
|
29
|
+
with 1013;
|
|
30
|
+
* statements: assign, checked aug-assign, ``require``, ``if``/``elif``/``else``,
|
|
31
|
+
``while`` (and ``while``/``else``), ``for x in range(...)``, ``break``,
|
|
32
|
+
``continue``, bare ``return``, ``match`` on a unit enum;
|
|
33
|
+
* plain helpers, inlined at the call (``do { ... } while (0)``) so a
|
|
34
|
+
``require`` or checked-math abort still returns from the instruction;
|
|
35
|
+
* ``emit(Event(...))`` logs ``sha256("event:Name")[..8]`` plus the packed
|
|
36
|
+
fields through ``sol_log_data`` (``rt_log_event``);
|
|
37
|
+
* expressions: checked arithmetic (including shifts), bitwise ``&|^``,
|
|
38
|
+
comparisons, ``and``/``or``/``not``, unary ``-``, ``len``, width casts
|
|
39
|
+
such as ``u64(x)`` (narrowing is checked).
|
|
40
|
+
|
|
41
|
+
Anything else becomes a ``PS500x`` diagnostic (never ``PS9000``), so the
|
|
42
|
+
build fails with a stable code instead of a compiler crash.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import ast
|
|
48
|
+
from dataclasses import dataclass, replace
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
from pythsol.compiler.diagnostics import (
|
|
52
|
+
PS5001,
|
|
53
|
+
PS5002,
|
|
54
|
+
PS5003,
|
|
55
|
+
Diagnostic,
|
|
56
|
+
)
|
|
57
|
+
from pythsol.compiler.model import (
|
|
58
|
+
ACCOUNT_DATA_OFFSET,
|
|
59
|
+
AccountModel,
|
|
60
|
+
DataParamModel,
|
|
61
|
+
EnumModel,
|
|
62
|
+
EventModel,
|
|
63
|
+
FieldModel,
|
|
64
|
+
HelperModel,
|
|
65
|
+
InstructionModel,
|
|
66
|
+
ProgramModel,
|
|
67
|
+
StructModel,
|
|
68
|
+
snake_case,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"RT_ERR_CONSTRAINT",
|
|
73
|
+
"RT_ERR_CPI",
|
|
74
|
+
"RT_ERR_DISC",
|
|
75
|
+
"RT_ERR_DIV_ZERO",
|
|
76
|
+
"RT_ERR_ENUM",
|
|
77
|
+
"RT_ERR_OOB",
|
|
78
|
+
"RT_ERR_OPTION",
|
|
79
|
+
"RT_ERR_OVERFLOW",
|
|
80
|
+
"RT_ERR_OWNER",
|
|
81
|
+
"RT_ERR_PDA",
|
|
82
|
+
"RT_ERR_SIGNER",
|
|
83
|
+
"RT_ERR_SIZE",
|
|
84
|
+
"RT_ERR_STRING",
|
|
85
|
+
"RT_ERR_UNKNOWN",
|
|
86
|
+
"RT_ERR_WRITABLE",
|
|
87
|
+
"emit_program_c",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Framework error codes used while lowering (keep in sync with pythsol_rt.h).
|
|
91
|
+
RT_ERR_UNKNOWN = 1
|
|
92
|
+
RT_ERR_SIGNER = 1001
|
|
93
|
+
RT_ERR_WRITABLE = 1002
|
|
94
|
+
RT_ERR_OWNER = 1003
|
|
95
|
+
RT_ERR_SIZE = 1004
|
|
96
|
+
RT_ERR_DISC = 1005
|
|
97
|
+
RT_ERR_OOB = 1006
|
|
98
|
+
RT_ERR_CPI = 1007
|
|
99
|
+
RT_ERR_PDA = 1008
|
|
100
|
+
RT_ERR_OVERFLOW = 1009
|
|
101
|
+
RT_ERR_DIV_ZERO = 1010
|
|
102
|
+
RT_ERR_ENUM = 1011
|
|
103
|
+
RT_ERR_OPTION = 1012
|
|
104
|
+
RT_ERR_STRING = 1013
|
|
105
|
+
RT_ERR_CONSTRAINT = 1014
|
|
106
|
+
|
|
107
|
+
_GENERATOR = "PythSol"
|
|
108
|
+
_Q = '"'
|
|
109
|
+
|
|
110
|
+
#: rent-exempt reserve = (size + 128) * 3480 * 2 lamports — equal to the f64
|
|
111
|
+
#: math lamports_per_byte_year(3480) * exemption_threshold(2.0), exact for the
|
|
112
|
+
#: integer sizes. Recorded in ADR-0010.
|
|
113
|
+
_RENT_OVERHEAD_BYTES = 128
|
|
114
|
+
_RENT_LAMPORTS_PER_BYTE = 3480
|
|
115
|
+
_RENT_EXEMPTION_NUMER = 2
|
|
116
|
+
|
|
117
|
+
_WIDTH_C: dict[str, str] = {
|
|
118
|
+
"uint8_t": "uint8_t",
|
|
119
|
+
"uint16_t": "uint16_t",
|
|
120
|
+
"uint32_t": "uint32_t",
|
|
121
|
+
"uint64_t": "uint64_t",
|
|
122
|
+
"uint128_t": "RtU128",
|
|
123
|
+
"int8_t": "int8_t",
|
|
124
|
+
"int16_t": "int16_t",
|
|
125
|
+
"int32_t": "int32_t",
|
|
126
|
+
"int64_t": "int64_t",
|
|
127
|
+
"int128_t": "RtI128",
|
|
128
|
+
"bool": "uint8_t",
|
|
129
|
+
"SolPubkey": "SolPubkey",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_WIDTH_BYTES: dict[str, int] = {
|
|
133
|
+
"uint8_t": 1,
|
|
134
|
+
"uint16_t": 2,
|
|
135
|
+
"uint32_t": 4,
|
|
136
|
+
"uint64_t": 8,
|
|
137
|
+
"uint128_t": 16,
|
|
138
|
+
"int8_t": 1,
|
|
139
|
+
"int16_t": 2,
|
|
140
|
+
"int32_t": 4,
|
|
141
|
+
"int64_t": 8,
|
|
142
|
+
"int128_t": 16,
|
|
143
|
+
"bool": 1,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#: rt_checked_<op>_<family> helper suffix per model C type (ADR-0011).
|
|
147
|
+
_FAMILY_OF: dict[str, str] = {
|
|
148
|
+
"uint8_t": "u8",
|
|
149
|
+
"uint16_t": "u16",
|
|
150
|
+
"uint32_t": "u32",
|
|
151
|
+
"uint64_t": "u64",
|
|
152
|
+
"uint128_t": "u128",
|
|
153
|
+
"RtU128": "u128",
|
|
154
|
+
"int8_t": "i8",
|
|
155
|
+
"int16_t": "i16",
|
|
156
|
+
"int32_t": "i32",
|
|
157
|
+
"int64_t": "i64",
|
|
158
|
+
"int128_t": "i128",
|
|
159
|
+
"RtI128": "i128",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#: The bytewise store helpers take *unsigned* word widths; signed values
|
|
163
|
+
#: reinterpret through these before the helper call.
|
|
164
|
+
_UNSIGNED_OF: dict[str, str] = {
|
|
165
|
+
"int8_t": "uint8_t",
|
|
166
|
+
"int16_t": "uint16_t",
|
|
167
|
+
"int32_t": "uint32_t",
|
|
168
|
+
"int64_t": "uint64_t",
|
|
169
|
+
"int128_t": "RtU128",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_ST_HELPER: dict[int, str] = {2: "rt_st_u16", 4: "rt_st_u32", 8: "rt_st_u64", 16: "rt_st_u128"}
|
|
173
|
+
_LD_HELPER: dict[int, str] = {2: "rt_ld_u16", 4: "rt_ld_u32", 8: "rt_ld_u64", 16: "rt_ld_u128"}
|
|
174
|
+
|
|
175
|
+
_ARITH_OP_C: dict[type, str] = {
|
|
176
|
+
ast.Add: "add",
|
|
177
|
+
ast.Sub: "sub",
|
|
178
|
+
ast.Mult: "mul",
|
|
179
|
+
ast.FloorDiv: "div",
|
|
180
|
+
ast.Mod: "mod",
|
|
181
|
+
ast.LShift: "shl",
|
|
182
|
+
ast.RShift: "shr",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_BIT_OP_C: dict[type, str] = {ast.BitAnd: "&", ast.BitOr: "|", ast.BitXor: "^"}
|
|
186
|
+
|
|
187
|
+
#: For diagnostics: the checked-opname back to Python's spelling.
|
|
188
|
+
_PY_OP: dict[str, str] = {
|
|
189
|
+
"add": "+",
|
|
190
|
+
"sub": "-",
|
|
191
|
+
"mul": "*",
|
|
192
|
+
"div": "//",
|
|
193
|
+
"mod": "%",
|
|
194
|
+
"shl": "<<",
|
|
195
|
+
"shr": ">>",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_COMPARE_C: dict[type, str] = {
|
|
199
|
+
ast.Eq: "==",
|
|
200
|
+
ast.NotEq: "!=",
|
|
201
|
+
ast.Lt: "<",
|
|
202
|
+
ast.LtE: "<=",
|
|
203
|
+
ast.Gt: ">",
|
|
204
|
+
ast.GtE: ">=",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_LOCAL_TYPES_C: dict[str, str] = {
|
|
208
|
+
"u8": "uint8_t",
|
|
209
|
+
"u16": "uint16_t",
|
|
210
|
+
"u32": "uint32_t",
|
|
211
|
+
"u64": "uint64_t",
|
|
212
|
+
"u128": "RtU128",
|
|
213
|
+
"i8": "int8_t",
|
|
214
|
+
"i16": "int16_t",
|
|
215
|
+
"i32": "int32_t",
|
|
216
|
+
"i64": "int64_t",
|
|
217
|
+
"i128": "RtI128",
|
|
218
|
+
"bool": "uint8_t",
|
|
219
|
+
"Pubkey": "SolPubkey",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(frozen=True)
|
|
224
|
+
class _Frag:
|
|
225
|
+
"""A lowered expression: C text plus its kind.
|
|
226
|
+
|
|
227
|
+
``kind`` is one of:
|
|
228
|
+
|
|
229
|
+
* ``"pub"`` — a ``const SolPubkey *``-compatible pointer expression;
|
|
230
|
+
* ``"bool"`` — a C 0/1 value usable directly in ``if``;
|
|
231
|
+
* a fixed-width numeric C type such as ``uint64_t`` / ``int64_t``.
|
|
232
|
+
|
|
233
|
+
``literal`` is set when the fragment is a pure integer constant, so a
|
|
234
|
+
later store or operator can re-type it (``u8 + 1`` stays ``u8``).
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
text: str
|
|
238
|
+
kind: str
|
|
239
|
+
literal: int | None = None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_none(node: ast.expr) -> bool:
|
|
243
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _utf8_literal(node: ast.expr) -> bytes | None:
|
|
247
|
+
"""UTF-8 bytes of a string constant, or ``None`` when ``node`` is not one."""
|
|
248
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
249
|
+
return node.value.encode("utf-8")
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class _StrVar:
|
|
255
|
+
"""A local or data-parameter fixed string: length plus reserved bytes."""
|
|
256
|
+
|
|
257
|
+
length_var: str
|
|
258
|
+
data_var: str
|
|
259
|
+
capacity: int
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class _OptVar:
|
|
264
|
+
"""A local or data-parameter option: tag byte plus reserved inner value."""
|
|
265
|
+
|
|
266
|
+
tag: str
|
|
267
|
+
value: str
|
|
268
|
+
ctype: str
|
|
269
|
+
defined: str
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class _Loop:
|
|
274
|
+
"""Labels for the innermost ``while`` / ``for`` (break, continue, else)."""
|
|
275
|
+
|
|
276
|
+
head: str
|
|
277
|
+
cont: str
|
|
278
|
+
done: str
|
|
279
|
+
flag: str | None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class _Inline:
|
|
284
|
+
"""One helper frame. ``result`` is ``None`` when the helper returns nothing.
|
|
285
|
+
|
|
286
|
+
``break`` leaves this frame. User loops lower to ``goto``, so a helper
|
|
287
|
+
``return`` cannot cancel the caller's loop, and a C ``for`` that copies
|
|
288
|
+
bytes is closed before the ``break``.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
name: str
|
|
292
|
+
result: str | None
|
|
293
|
+
c_type: str
|
|
294
|
+
defined: str
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_KIND_META: dict[str, tuple[int, bool]] = {
|
|
298
|
+
"uint8_t": (8, False),
|
|
299
|
+
"uint16_t": (16, False),
|
|
300
|
+
"uint32_t": (32, False),
|
|
301
|
+
"uint64_t": (64, False),
|
|
302
|
+
"RtU128": (128, False),
|
|
303
|
+
"int8_t": (8, True),
|
|
304
|
+
"int16_t": (16, True),
|
|
305
|
+
"int32_t": (32, True),
|
|
306
|
+
"int64_t": (64, True),
|
|
307
|
+
"RtI128": (128, True),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_CAST_NAMES: frozenset[str] = frozenset(
|
|
311
|
+
{"u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _canon(kind: str) -> str:
|
|
316
|
+
"""Map model C names onto the expression types the runtime helpers use."""
|
|
317
|
+
return {"uint128_t": "RtU128", "int128_t": "RtI128", "bool": "uint8_t"}.get(kind, kind)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _fits_c(value: int, kind: str) -> bool:
|
|
321
|
+
bits, signed = _KIND_META[_canon(kind)]
|
|
322
|
+
if signed:
|
|
323
|
+
return -(1 << (bits - 1)) <= value <= (1 << (bits - 1)) - 1
|
|
324
|
+
return 0 <= value < (1 << bits)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _typed_lit(value: int, kind: str) -> str:
|
|
328
|
+
"""A C literal of ``kind`` holding ``value`` (caller checked the range)."""
|
|
329
|
+
kind = _canon(kind)
|
|
330
|
+
if kind in ("RtU128", "RtI128"):
|
|
331
|
+
raw = value & ((1 << 128) - 1)
|
|
332
|
+
hi = (raw >> 64) & ((1 << 64) - 1)
|
|
333
|
+
lo = raw & ((1 << 64) - 1)
|
|
334
|
+
composed = f"RT_U128_VAL(0x{hi:x}ULL, 0x{lo:x}ULL)"
|
|
335
|
+
if kind == "RtI128":
|
|
336
|
+
return f"((RtI128){composed})"
|
|
337
|
+
return composed
|
|
338
|
+
if _KIND_META[kind][1] and value < 0:
|
|
339
|
+
return f"(({kind}){value})"
|
|
340
|
+
return f"(({kind}){value}u)"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _wider_kind(left: str, right: str) -> str:
|
|
344
|
+
"""Result width of a binary op: max bits, signed if either side is."""
|
|
345
|
+
lk = _canon(left)
|
|
346
|
+
rk = _canon(right)
|
|
347
|
+
lb, ls = _KIND_META[lk]
|
|
348
|
+
rb, rs = _KIND_META[rk]
|
|
349
|
+
bits = max(lb, rb)
|
|
350
|
+
prefix = "i" if (ls or rs) else "u"
|
|
351
|
+
return _LOCAL_TYPES_C[f"{prefix}{bits}"]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _widen_safe(src: str, dest: str) -> bool:
|
|
355
|
+
"""True when every ``src`` value fits in ``dest`` with a C cast."""
|
|
356
|
+
sb, ss = _KIND_META[src]
|
|
357
|
+
db, ds = _KIND_META[dest]
|
|
358
|
+
if ss == ds:
|
|
359
|
+
return sb <= db
|
|
360
|
+
# Unsigned into a wider signed type (u32 fits in i64; u64 does not fit in i64).
|
|
361
|
+
if (not ss) and ds:
|
|
362
|
+
return db > sb
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _hi_lit(hi: int) -> str:
|
|
367
|
+
"""C literal for an inclusive upper bound used in a cast check."""
|
|
368
|
+
if hi > 2**64 - 1:
|
|
369
|
+
return _typed_lit(hi, "RtU128")
|
|
370
|
+
if hi > 2**63 - 1:
|
|
371
|
+
return f"{hi}ULL"
|
|
372
|
+
return str(hi)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _bound_lit(value: int) -> str:
|
|
376
|
+
"""C literal for a cast-check bound, including negatives wider than i64."""
|
|
377
|
+
if value >= 0:
|
|
378
|
+
return _hi_lit(value)
|
|
379
|
+
# ``-9223372036854775808`` is not a valid signed long-long literal.
|
|
380
|
+
if value == -(2**63):
|
|
381
|
+
return "(-9223372036854775807LL - 1)"
|
|
382
|
+
if value > -(2**63):
|
|
383
|
+
return f"({value}LL)"
|
|
384
|
+
return _typed_lit(value, "RtI128")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _init_rent_lamports(account_size: int) -> int:
|
|
388
|
+
return (account_size + _RENT_OVERHEAD_BYTES) * _RENT_LAMPORTS_PER_BYTE * _RENT_EXEMPTION_NUMER
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _c_int_lit(value: int) -> str:
|
|
392
|
+
return f"{value}u" if value >= 0 else f"({value})"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class _Emitter:
|
|
396
|
+
"""Per-program C emitter; accumulates lines and diagnostics."""
|
|
397
|
+
|
|
398
|
+
def __init__(self, model: ProgramModel) -> None:
|
|
399
|
+
self._model = model
|
|
400
|
+
self._lines: list[str] = []
|
|
401
|
+
self._diags: list[Diagnostic] = []
|
|
402
|
+
self._tmp_no = 0
|
|
403
|
+
self._accounts: dict[str, AccountModel] = {
|
|
404
|
+
account.name: account for account in model.accounts
|
|
405
|
+
}
|
|
406
|
+
self._structs: dict[str, StructModel] = {item.name: item for item in model.structs}
|
|
407
|
+
self._events: dict[str, EventModel] = {item.name: item for item in model.events}
|
|
408
|
+
self._error_codes: dict[tuple[str, str], int] = {
|
|
409
|
+
(error.owner, error.name): error.code for error in model.errors
|
|
410
|
+
}
|
|
411
|
+
self._enums = {enum.name: enum for enum in model.enums}
|
|
412
|
+
# Per-instruction state (reset in `_emit_instruction`).
|
|
413
|
+
self._data_vars: dict[str, tuple[str, str]] = {}
|
|
414
|
+
self._acct_vars: dict[str, tuple[str, str]] = {}
|
|
415
|
+
self._locals: dict[str, tuple[str, str]] = {}
|
|
416
|
+
self._arrays: dict[str, tuple[str, str, int]] = {}
|
|
417
|
+
self._options: dict[str, _OptVar] = {}
|
|
418
|
+
self._strings: dict[str, _StrVar] = {}
|
|
419
|
+
self._value_enum: dict[str, str] = {}
|
|
420
|
+
self._array_enum: dict[str, str] = {}
|
|
421
|
+
self._bools: set[str] = set()
|
|
422
|
+
self._bool_arrays: set[str] = set()
|
|
423
|
+
self._loops: list[_Loop] = []
|
|
424
|
+
self._loop_no = 0
|
|
425
|
+
self._indent = 1
|
|
426
|
+
self._iparams: list[Any] = []
|
|
427
|
+
self._helpers: dict[str, HelperModel] = {
|
|
428
|
+
helper.name: helper for helper in model.helpers
|
|
429
|
+
}
|
|
430
|
+
self._inline: list[_Inline] = []
|
|
431
|
+
self._instr: InstructionModel | None = None
|
|
432
|
+
self._log_file = ""
|
|
433
|
+
|
|
434
|
+
def _s(self, text: str, extra: int = 0) -> None:
|
|
435
|
+
"""Emit one body line at the current block indent."""
|
|
436
|
+
self._line(text, self._indent + extra)
|
|
437
|
+
|
|
438
|
+
# -- infrastructure ----------------------------------------------------
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def file(self) -> str:
|
|
442
|
+
"""Source file base name used in runtime logs."""
|
|
443
|
+
return self._log_file or self._model.source
|
|
444
|
+
|
|
445
|
+
def _line(self, text: str, indent: int = 1) -> None:
|
|
446
|
+
prefix = " " * indent
|
|
447
|
+
self._lines.append(f"{prefix}{text}" if text else "")
|
|
448
|
+
|
|
449
|
+
def _mk_tmp(self) -> str:
|
|
450
|
+
self._tmp_no += 1
|
|
451
|
+
return f"rt_t{self._tmp_no}"
|
|
452
|
+
|
|
453
|
+
def _loc(self, line: int) -> str:
|
|
454
|
+
return f"{_Q}{self.file}{_Q}, {line}u"
|
|
455
|
+
|
|
456
|
+
def _diag(self, code: str, node: ast.AST | None, message: str) -> None:
|
|
457
|
+
self._diags.append(
|
|
458
|
+
Diagnostic(
|
|
459
|
+
code=code,
|
|
460
|
+
message=message,
|
|
461
|
+
file=self.file,
|
|
462
|
+
line=getattr(node, "lineno", 1) or 1,
|
|
463
|
+
col=(getattr(node, "col_offset", 0) or 0) + 1,
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _err_macro(self, owner: str, member: str) -> str:
|
|
468
|
+
return f"ERR_{owner.upper()}_{member.upper()}"
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def diagnostics(self) -> list[Diagnostic]:
|
|
472
|
+
"""Diagnostics collected while emitting."""
|
|
473
|
+
return self._diags
|
|
474
|
+
|
|
475
|
+
# -- program topology --------------------------------------------------
|
|
476
|
+
|
|
477
|
+
def run(self) -> str:
|
|
478
|
+
"""Emit the whole program: metadata, handlers, dispatch + entry."""
|
|
479
|
+
model = self._model
|
|
480
|
+
self._line(f"/* Generated by {_GENERATOR} from {self.file}; do not edit.", 0)
|
|
481
|
+
self._line(f" * Program: {model.name}", 0)
|
|
482
|
+
self._line(
|
|
483
|
+
" * Custom errors: 6000 + index; framework codes live in pythsol_rt.h. */",
|
|
484
|
+
0,
|
|
485
|
+
)
|
|
486
|
+
self._line('#include "pythsol_rt.h"', 0)
|
|
487
|
+
self._line("", 0)
|
|
488
|
+
for account in model.accounts:
|
|
489
|
+
self._emit_account_meta(account)
|
|
490
|
+
for error in model.errors:
|
|
491
|
+
self._line(
|
|
492
|
+
f"#define {self._err_macro(error.owner, error.name)} {error.code}u"
|
|
493
|
+
f" /* {error.owner}.{error.name} */",
|
|
494
|
+
0,
|
|
495
|
+
)
|
|
496
|
+
if model.errors:
|
|
497
|
+
self._line("", 0)
|
|
498
|
+
for instruction in model.instructions:
|
|
499
|
+
self._emit_instruction(instruction)
|
|
500
|
+
self._line("", 0)
|
|
501
|
+
self._emit_dispatch_table()
|
|
502
|
+
return "\n".join(self._lines) + "\n"
|
|
503
|
+
|
|
504
|
+
def _emit_account_meta(self, account: AccountModel) -> None:
|
|
505
|
+
upper = account.name.upper()
|
|
506
|
+
disc = ", ".join(f"{byte:#x}u" for byte in account.discriminator)
|
|
507
|
+
self._line(
|
|
508
|
+
f"/* @account {account.name}: {account.size} bytes (disc + payload). */",
|
|
509
|
+
0,
|
|
510
|
+
)
|
|
511
|
+
self._line(f"static const unsigned char RT_DISC_{upper}[8] = {{ {disc} }};", 0)
|
|
512
|
+
self._line(f"#define RT_SIZE_{upper} {account.size}u", 0)
|
|
513
|
+
for field in account.fields:
|
|
514
|
+
self._line(
|
|
515
|
+
f"#define RT_OFF_{upper}_{field.name.upper()} "
|
|
516
|
+
f"{ACCOUNT_DATA_OFFSET + field.offset}u",
|
|
517
|
+
0,
|
|
518
|
+
)
|
|
519
|
+
self._line("", 0)
|
|
520
|
+
|
|
521
|
+
# -- instructions ------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
def _emit_instruction(self, instruction: InstructionModel) -> None:
|
|
524
|
+
self._data_vars = {}
|
|
525
|
+
self._acct_vars = {}
|
|
526
|
+
self._locals = {}
|
|
527
|
+
self._arrays = {}
|
|
528
|
+
self._options = {}
|
|
529
|
+
self._strings = {}
|
|
530
|
+
self._value_enum = {}
|
|
531
|
+
self._array_enum = {}
|
|
532
|
+
self._bools = set()
|
|
533
|
+
self._bool_arrays = set()
|
|
534
|
+
self._loops = []
|
|
535
|
+
self._loop_no = 0
|
|
536
|
+
self._indent = 1
|
|
537
|
+
self._inline = []
|
|
538
|
+
self._instr = instruction
|
|
539
|
+
self._log_file = instruction.source
|
|
540
|
+
self._iparams = list(instruction.account_params)
|
|
541
|
+
self._line(
|
|
542
|
+
f"/* instruction {instruction.name} (disc "
|
|
543
|
+
f"{list(instruction.discriminator)}) declared at "
|
|
544
|
+
f"{self.file}:{instruction.line} */",
|
|
545
|
+
0,
|
|
546
|
+
)
|
|
547
|
+
self._line(f"static uint64_t pyf_{instruction.snake_name}(RtCtx *rt) {{", 0)
|
|
548
|
+
self._emit_data_section(instruction)
|
|
549
|
+
self._emit_account_section(instruction)
|
|
550
|
+
self._line("/* body */", 1)
|
|
551
|
+
for stmt in instruction.body:
|
|
552
|
+
self._emit_stmt(stmt, instruction)
|
|
553
|
+
for param in instruction.account_params:
|
|
554
|
+
if param.close_to:
|
|
555
|
+
self._emit_close(param)
|
|
556
|
+
self._line("return 0u; /* RT_OK */", 1)
|
|
557
|
+
self._line("}", 0)
|
|
558
|
+
|
|
559
|
+
def _emit_data_section(self, instruction: InstructionModel) -> None:
|
|
560
|
+
body_size = sum(param.size for param in instruction.data_params)
|
|
561
|
+
self._line("/* 1. instruction data (Borsh, disc already cropped by rt) */", 1)
|
|
562
|
+
# rt_entrypoint points rt->data past the 8-byte discriminator and
|
|
563
|
+
# reports rt->data_len without it, so the runtime-computed payload
|
|
564
|
+
# length is exactly `body_size` here.
|
|
565
|
+
self._line(f"if (rt->data_len != {body_size}u) {{", 1)
|
|
566
|
+
self._line(
|
|
567
|
+
f"rt_fail_log({RT_ERR_UNKNOWN}u, {self._loc(instruction.line)},"
|
|
568
|
+
' "instruction data length is wrong");',
|
|
569
|
+
2,
|
|
570
|
+
)
|
|
571
|
+
self._line(f"return {RT_ERR_UNKNOWN}u;", 2)
|
|
572
|
+
self._line("}", 1)
|
|
573
|
+
offset = 0
|
|
574
|
+
for param in instruction.data_params:
|
|
575
|
+
if param.shape == "string":
|
|
576
|
+
self._emit_string_data(param, offset)
|
|
577
|
+
offset += param.size
|
|
578
|
+
continue
|
|
579
|
+
if param.shape == "option":
|
|
580
|
+
self._emit_option_data(param, offset)
|
|
581
|
+
offset += param.size
|
|
582
|
+
continue
|
|
583
|
+
var = f"dp_{param.name}"
|
|
584
|
+
ctype = _WIDTH_C[param.c_type]
|
|
585
|
+
if param.c_type == "SolPubkey":
|
|
586
|
+
self._line(f"SolPubkey {var};", 1)
|
|
587
|
+
self._line(
|
|
588
|
+
f"rt_key_copy(&{var}, (const SolPubkey *)(const void *)"
|
|
589
|
+
f"(rt->data + {offset}u));",
|
|
590
|
+
1,
|
|
591
|
+
)
|
|
592
|
+
elif param.c_type == "int64_t":
|
|
593
|
+
self._line(f"int64_t {var} = 0;", 1)
|
|
594
|
+
self._line(
|
|
595
|
+
"{ uint64_t rt_bits = 0u;"
|
|
596
|
+
f" rt_ld_u64(rt->data, {offset}u, &rt_bits);"
|
|
597
|
+
f" {var} = (int64_t)rt_bits; }}",
|
|
598
|
+
1,
|
|
599
|
+
)
|
|
600
|
+
elif param.c_type in ("uint8_t", "bool"):
|
|
601
|
+
self._line(f"uint8_t {var} = rt->data[{offset}u];", 1)
|
|
602
|
+
ctype = "uint8_t"
|
|
603
|
+
elif param.c_type == "uint16_t":
|
|
604
|
+
self._line(f"uint16_t {var} = 0u;", 1)
|
|
605
|
+
self._line(f"rt_ld_u16(rt->data, {offset}u, &{var});", 1)
|
|
606
|
+
elif param.c_type == "uint32_t":
|
|
607
|
+
self._line(f"uint32_t {var} = 0u;", 1)
|
|
608
|
+
self._line(f"rt_ld_u32(rt->data, {offset}u, &{var});", 1)
|
|
609
|
+
elif param.c_type == "int8_t":
|
|
610
|
+
self._line(f"int8_t {var} = 0;", 1)
|
|
611
|
+
self._line(
|
|
612
|
+
f"{{ uint8_t rt_bits = rt->data[{offset}u]; {var} = (int8_t)rt_bits; }}",
|
|
613
|
+
1,
|
|
614
|
+
)
|
|
615
|
+
ctype = "int8_t"
|
|
616
|
+
elif param.c_type == "int16_t":
|
|
617
|
+
self._line(f"int16_t {var} = 0;", 1)
|
|
618
|
+
self._line(
|
|
619
|
+
"{ uint16_t rt_bits = 0u;"
|
|
620
|
+
f" rt_ld_u16(rt->data, {offset}u, &rt_bits);"
|
|
621
|
+
f" {var} = (int16_t)rt_bits; }}",
|
|
622
|
+
1,
|
|
623
|
+
)
|
|
624
|
+
ctype = "int16_t"
|
|
625
|
+
elif param.c_type == "int32_t":
|
|
626
|
+
self._line(f"int32_t {var} = 0;", 1)
|
|
627
|
+
self._line(
|
|
628
|
+
"{ uint32_t rt_bits = 0u;"
|
|
629
|
+
f" rt_ld_u32(rt->data, {offset}u, &rt_bits);"
|
|
630
|
+
f" {var} = (int32_t)rt_bits; }}",
|
|
631
|
+
1,
|
|
632
|
+
)
|
|
633
|
+
ctype = "int32_t"
|
|
634
|
+
elif param.c_type == "uint128_t":
|
|
635
|
+
self._line(f"RtU128 {var} = 0;", 1)
|
|
636
|
+
self._line(f"rt_ld_u128(rt->data, {offset}u, &{var});", 1)
|
|
637
|
+
ctype = "RtU128"
|
|
638
|
+
elif param.c_type == "int128_t":
|
|
639
|
+
self._line(f"RtI128 {var} = 0;", 1)
|
|
640
|
+
self._line(
|
|
641
|
+
"{ RtU128 rt_bits = 0;"
|
|
642
|
+
f" rt_ld_u128(rt->data, {offset}u, &rt_bits);"
|
|
643
|
+
f" {var} = (RtI128)rt_bits; }}",
|
|
644
|
+
1,
|
|
645
|
+
)
|
|
646
|
+
ctype = "RtI128"
|
|
647
|
+
else:
|
|
648
|
+
self._line(f"uint64_t {var} = 0u;", 1)
|
|
649
|
+
self._line(f"rt_ld_u64(rt->data, {offset}u, &{var});", 1)
|
|
650
|
+
self._data_vars[param.name] = (var, ctype)
|
|
651
|
+
if param.c_type == "bool":
|
|
652
|
+
self._bools.add(param.name)
|
|
653
|
+
if param.defined:
|
|
654
|
+
self._value_enum[param.name] = param.defined
|
|
655
|
+
offset += param.size
|
|
656
|
+
for param in instruction.data_params: # -Wunused silencing (deterministic)
|
|
657
|
+
if param.shape == "string":
|
|
658
|
+
self._line(f"(void)dp_{param.name}_len;", 1)
|
|
659
|
+
self._line(f"(void)dp_{param.name};", 1)
|
|
660
|
+
continue
|
|
661
|
+
if param.shape == "option":
|
|
662
|
+
self._line(f"(void)dp_{param.name}_tag;", 1)
|
|
663
|
+
self._line(f"(void)dp_{param.name};", 1)
|
|
664
|
+
|
|
665
|
+
def _emit_account_section(self, instruction: InstructionModel) -> None:
|
|
666
|
+
count = len(instruction.account_params)
|
|
667
|
+
self._line(f"/* 2. accounts: {count} role-checked in declared order */", 1)
|
|
668
|
+
self._line(f"if (rt->n_accounts < {count}u) {{", 1)
|
|
669
|
+
self._line(
|
|
670
|
+
f'rt_fail_log({RT_ERR_OOB}u, {self._loc(instruction.line)}, "missing accounts");',
|
|
671
|
+
2,
|
|
672
|
+
)
|
|
673
|
+
self._line(f"return {RT_ERR_OOB}u;", 2)
|
|
674
|
+
self._line("}", 1)
|
|
675
|
+
for param in instruction.account_params:
|
|
676
|
+
var = f"acc_{param.name}"
|
|
677
|
+
self._acct_vars[param.name] = (var, param.account)
|
|
678
|
+
self._line(f"RtAccount *{var} = &rt->accounts[{param.index}u];", 1)
|
|
679
|
+
self._emit_role_checks(param, var)
|
|
680
|
+
|
|
681
|
+
# -- role checks -------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
def _emit_role_checks(self, param: Any, var: str) -> None:
|
|
684
|
+
loc = self._loc(param.line)
|
|
685
|
+
if param.kind == "mint":
|
|
686
|
+
return
|
|
687
|
+
if param.kind == "token":
|
|
688
|
+
self._line(f"if (!{var}->is_writable) {{", 1)
|
|
689
|
+
self._line(
|
|
690
|
+
f'rt_fail_log({RT_ERR_WRITABLE}u, {loc}, "writable required: {param.name}");',
|
|
691
|
+
2,
|
|
692
|
+
)
|
|
693
|
+
self._line(f"return {RT_ERR_WRITABLE}u;", 2)
|
|
694
|
+
self._line("}", 1)
|
|
695
|
+
return
|
|
696
|
+
if param.kind == "signer":
|
|
697
|
+
self._line(f"if (!{var}->is_signer) {{", 1)
|
|
698
|
+
self._line(
|
|
699
|
+
f'rt_fail_log({RT_ERR_SIGNER}u, {loc}, "signer required: {param.name}");',
|
|
700
|
+
2,
|
|
701
|
+
)
|
|
702
|
+
self._line(f"return {RT_ERR_SIGNER}u;", 2)
|
|
703
|
+
self._line("}", 1)
|
|
704
|
+
return
|
|
705
|
+
if not param.writable:
|
|
706
|
+
# read-only `Account[X]`: owner + size + discriminator still apply
|
|
707
|
+
self._emit_persistent_checks(param, var)
|
|
708
|
+
return
|
|
709
|
+
self._line(f"if (!{var}->is_writable) {{", 1)
|
|
710
|
+
self._line(
|
|
711
|
+
f'rt_fail_log({RT_ERR_WRITABLE}u, {loc}, "writable required: {param.name}");',
|
|
712
|
+
2,
|
|
713
|
+
)
|
|
714
|
+
self._line(f"return {RT_ERR_WRITABLE}u;", 2)
|
|
715
|
+
self._line("}", 1)
|
|
716
|
+
if param.kind == "init":
|
|
717
|
+
self._emit_init(param, var)
|
|
718
|
+
return
|
|
719
|
+
if param.kind in ("init_if_needed", "zeroed"):
|
|
720
|
+
self._diag(
|
|
721
|
+
PS5003,
|
|
722
|
+
None,
|
|
723
|
+
f"`{param.kind}[{param.account}]` on `{param.name}` is not lowered yet (phase 4)",
|
|
724
|
+
)
|
|
725
|
+
return
|
|
726
|
+
self._emit_persistent_checks(param, var)
|
|
727
|
+
|
|
728
|
+
def _emit_persistent_checks(self, param: Any, var: str) -> None:
|
|
729
|
+
"""Owner/size/discriminator for an initialized account view."""
|
|
730
|
+
if self._accounts.get(param.account) is None:
|
|
731
|
+
return
|
|
732
|
+
loc = self._loc(param.line)
|
|
733
|
+
upper = param.account.upper()
|
|
734
|
+
if param.owner_of:
|
|
735
|
+
other = self._acct_vars.get(param.owner_of)
|
|
736
|
+
owner_expr = (
|
|
737
|
+
f"(const uint8_t *)(const void *){other[0]}->key"
|
|
738
|
+
if other is not None
|
|
739
|
+
else "rt->program_id"
|
|
740
|
+
)
|
|
741
|
+
if other is None:
|
|
742
|
+
self._diag(PS5002, None, f"`Owner` on `{param.name}` is not lowered")
|
|
743
|
+
self._line(f"if (!rt_owner_eq({var}, {owner_expr})) {{", 1)
|
|
744
|
+
self._line(
|
|
745
|
+
f'rt_fail_log({RT_ERR_CONSTRAINT}u, {loc}, "owner: {param.name}");',
|
|
746
|
+
2,
|
|
747
|
+
)
|
|
748
|
+
self._line(f"return {RT_ERR_CONSTRAINT}u;", 2)
|
|
749
|
+
self._line("}", 1)
|
|
750
|
+
else:
|
|
751
|
+
self._line(f"if (!rt_owner_eq({var}, rt->program_id)) {{", 1)
|
|
752
|
+
self._line(
|
|
753
|
+
f'rt_fail_log({RT_ERR_OWNER}u, {loc}, "owner mismatch: {param.name}");',
|
|
754
|
+
2,
|
|
755
|
+
)
|
|
756
|
+
self._line(f"return {RT_ERR_OWNER}u;", 2)
|
|
757
|
+
self._line("}", 1)
|
|
758
|
+
self._line(f"if ({var}->data_len != RT_SIZE_{upper}) {{", 1)
|
|
759
|
+
self._line(
|
|
760
|
+
f'rt_fail_log({RT_ERR_SIZE}u, {loc}, "size mismatch: {param.name}");',
|
|
761
|
+
2,
|
|
762
|
+
)
|
|
763
|
+
self._line(f"return {RT_ERR_SIZE}u;", 2)
|
|
764
|
+
self._line("}", 1)
|
|
765
|
+
self._line(f"if (!rt_disc_eq({var}->data, RT_DISC_{upper})) {{", 1)
|
|
766
|
+
self._line(
|
|
767
|
+
f'rt_fail_log({RT_ERR_DISC}u, {loc}, "discriminator mismatch: {param.name}");',
|
|
768
|
+
2,
|
|
769
|
+
)
|
|
770
|
+
self._line(f"return {RT_ERR_DISC}u;", 2)
|
|
771
|
+
self._line("}", 1)
|
|
772
|
+
self._emit_relation_checks(param, var)
|
|
773
|
+
|
|
774
|
+
def _emit_relation_checks(self, param: Any, var: str) -> None:
|
|
775
|
+
"""HasOne and Address run after the account is known to be ours."""
|
|
776
|
+
loc = self._loc(param.line)
|
|
777
|
+
if param.has_one:
|
|
778
|
+
model = self._accounts.get(param.account)
|
|
779
|
+
field = model.field(param.has_one) if model is not None else None
|
|
780
|
+
other = self._acct_vars.get(param.has_one)
|
|
781
|
+
if field is None or field.c_type != "SolPubkey" or other is None:
|
|
782
|
+
self._diag(PS5002, None, f"`HasOne` on `{param.name}` is not lowered")
|
|
783
|
+
else:
|
|
784
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
785
|
+
self._line(
|
|
786
|
+
f"if (!rt_key_eq((const SolPubkey *)(const void *)({var}->data + {off}u),"
|
|
787
|
+
f" {other[0]}->key)) {{",
|
|
788
|
+
1,
|
|
789
|
+
)
|
|
790
|
+
self._line(
|
|
791
|
+
f'rt_fail_log({RT_ERR_CONSTRAINT}u, {loc},'
|
|
792
|
+
f' "has one: {param.name}.{param.has_one}");',
|
|
793
|
+
2,
|
|
794
|
+
)
|
|
795
|
+
self._line(f"return {RT_ERR_CONSTRAINT}u;", 2)
|
|
796
|
+
self._line("}", 1)
|
|
797
|
+
if param.address_of:
|
|
798
|
+
data = self._data_vars.get(param.address_of)
|
|
799
|
+
if data is None:
|
|
800
|
+
self._diag(PS5002, None, f"`Address` on `{param.name}` needs a Pubkey argument")
|
|
801
|
+
else:
|
|
802
|
+
self._line(
|
|
803
|
+
f"if (!rt_key_eq({var}->key, &{data[0]})) {{",
|
|
804
|
+
1,
|
|
805
|
+
)
|
|
806
|
+
self._line(
|
|
807
|
+
f'rt_fail_log({RT_ERR_CONSTRAINT}u, {loc},'
|
|
808
|
+
f' "address: {param.name}");',
|
|
809
|
+
2,
|
|
810
|
+
)
|
|
811
|
+
self._line(f"return {RT_ERR_CONSTRAINT}u;", 2)
|
|
812
|
+
self._line("}", 1)
|
|
813
|
+
|
|
814
|
+
def _emit_close(self, param: Any) -> None:
|
|
815
|
+
"""Move lamports to ``close_to``, zero the account, assign the system program."""
|
|
816
|
+
src_entry = self._acct_vars.get(param.name)
|
|
817
|
+
dest_entry = self._acct_vars.get(param.close_to)
|
|
818
|
+
if src_entry is None or dest_entry is None:
|
|
819
|
+
self._diag(PS5002, None, f"`CloseTo` on `{param.name}` is not lowered")
|
|
820
|
+
return
|
|
821
|
+
src = src_entry[0]
|
|
822
|
+
dest = dest_entry[0]
|
|
823
|
+
loc = self._loc(param.line)
|
|
824
|
+
moved = self._mk_tmp()
|
|
825
|
+
rc = self._mk_tmp()
|
|
826
|
+
self._s(f"if (!{dest}->is_writable) {{")
|
|
827
|
+
self._s(
|
|
828
|
+
f'rt_fail_log({RT_ERR_WRITABLE}u, {loc}, "close destination must be writable");',
|
|
829
|
+
1,
|
|
830
|
+
)
|
|
831
|
+
self._s(f"return {RT_ERR_WRITABLE}u;", 1)
|
|
832
|
+
self._s("}")
|
|
833
|
+
self._s(f"uint64_t {moved} = 0u;")
|
|
834
|
+
self._s(
|
|
835
|
+
f"uint64_t {rc} = rt_checked_add_u64(*{dest}->lamports, *{src}->lamports, &{moved});"
|
|
836
|
+
)
|
|
837
|
+
self._s(f"if ({rc} != RT_OK) {{")
|
|
838
|
+
self._s(
|
|
839
|
+
f'rt_fail_log({rc}, {loc}, "checked add (u64)");',
|
|
840
|
+
1,
|
|
841
|
+
)
|
|
842
|
+
self._s(f"return {rc};", 1)
|
|
843
|
+
self._s("}")
|
|
844
|
+
self._s(f"*{dest}->lamports = {moved};")
|
|
845
|
+
self._s(f"*{src}->lamports = 0u;")
|
|
846
|
+
idx = self._mk_tmp()
|
|
847
|
+
self._s(f"uint64_t {idx};")
|
|
848
|
+
self._s(f"for ({idx} = 0u; {idx} < {src}->data_len; {idx}++) {{")
|
|
849
|
+
self._s(f"{src}->data[{idx}] = 0u;", 1)
|
|
850
|
+
self._s("}")
|
|
851
|
+
self._s(f"rt_st_u64({src}->data - 8, 0u, 0u);")
|
|
852
|
+
self._s(
|
|
853
|
+
f"rt_key_copy({src}->owner, (const SolPubkey *)(const void *)RT_SYSTEM_ID);"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def _emit_init(self, param: Any, var: str) -> None:
|
|
857
|
+
"""PDA verify + System create_account CPI for one Init param."""
|
|
858
|
+
model = self._accounts.get(param.account)
|
|
859
|
+
if model is None:
|
|
860
|
+
self._diag(
|
|
861
|
+
PS5003,
|
|
862
|
+
None,
|
|
863
|
+
f"`Init[{param.account}]` on `{param.name}` has no model (bug)",
|
|
864
|
+
)
|
|
865
|
+
return
|
|
866
|
+
loc = self._loc(param.line)
|
|
867
|
+
bump = self._find_bump()
|
|
868
|
+
if bump is None:
|
|
869
|
+
self._diag(
|
|
870
|
+
PS5003,
|
|
871
|
+
None,
|
|
872
|
+
f"`Init[{param.account}]` needs a `bump: u8` data parameter",
|
|
873
|
+
)
|
|
874
|
+
bump = "0u"
|
|
875
|
+
payer = self._find_payer()
|
|
876
|
+
if payer is None:
|
|
877
|
+
self._diag(
|
|
878
|
+
PS5003,
|
|
879
|
+
None,
|
|
880
|
+
"Init needs a `Signer` parameter to pay for the account (phase 3)",
|
|
881
|
+
)
|
|
882
|
+
payer = "(RtAccount *)0"
|
|
883
|
+
seed0 = param.seed or snake_case(param.account)
|
|
884
|
+
self._line("/* init: fresh system-owned PDA, then System create_account */", 1)
|
|
885
|
+
# The CPI needs the System program itself in the caller's accounts.
|
|
886
|
+
self._line("{", 1)
|
|
887
|
+
self._line("uint64_t rt_sys_i = 0u;", 2)
|
|
888
|
+
self._line("uint8_t rt_has_system = 0u;", 2)
|
|
889
|
+
self._line("for (rt_sys_i = 0u; rt_sys_i < rt->n_accounts; rt_sys_i++) {", 2)
|
|
890
|
+
self._line(
|
|
891
|
+
"if (rt_key_eq(rt->accounts[rt_sys_i].key, "
|
|
892
|
+
"(const SolPubkey *)(const void *)RT_SYSTEM_ID)) {",
|
|
893
|
+
3,
|
|
894
|
+
)
|
|
895
|
+
self._line("rt_has_system = 1u;", 4)
|
|
896
|
+
self._line("}", 3)
|
|
897
|
+
self._line("}", 2)
|
|
898
|
+
self._line("if (!rt_has_system) {", 2)
|
|
899
|
+
self._line(
|
|
900
|
+
f'rt_fail_log({RT_ERR_CPI}u, {loc}, "System program account is required: init");',
|
|
901
|
+
3,
|
|
902
|
+
)
|
|
903
|
+
self._line(f"return {RT_ERR_CPI}u;", 3)
|
|
904
|
+
self._line("}", 2)
|
|
905
|
+
self._line("}", 1)
|
|
906
|
+
self._line(f"if (!rt_owner_eq({var}, RT_SYSTEM_ID)) {{", 1)
|
|
907
|
+
self._line(
|
|
908
|
+
f'rt_fail_log({RT_ERR_OWNER}u, {loc}, "{param.name} must be system-owned to init");',
|
|
909
|
+
2,
|
|
910
|
+
)
|
|
911
|
+
self._line(f"return {RT_ERR_OWNER}u;", 2)
|
|
912
|
+
self._line("}", 1)
|
|
913
|
+
self._line(f"if ({var}->data_len != 0u) {{", 1)
|
|
914
|
+
self._line(
|
|
915
|
+
f'rt_fail_log({RT_ERR_SIZE}u, {loc}, "{param.name} is already initialized");',
|
|
916
|
+
2,
|
|
917
|
+
)
|
|
918
|
+
self._line(f"return {RT_ERR_SIZE}u;", 2)
|
|
919
|
+
self._line("}", 1)
|
|
920
|
+
self._line(
|
|
921
|
+
f'if (!rt_verify_pda(rt, {var}, (const uint8_t *)"{seed0}",'
|
|
922
|
+
f" {len(seed0)}u, (uint8_t)({bump}))) {{",
|
|
923
|
+
1,
|
|
924
|
+
)
|
|
925
|
+
self._line(
|
|
926
|
+
f'rt_fail_log({RT_ERR_PDA}u, {loc}, "{param.name} is not the expected PDA");',
|
|
927
|
+
2,
|
|
928
|
+
)
|
|
929
|
+
self._line(f"return {RT_ERR_PDA}u;", 2)
|
|
930
|
+
self._line("}", 1)
|
|
931
|
+
rent = _init_rent_lamports(model.size)
|
|
932
|
+
self._line(
|
|
933
|
+
f"if (rt_system_create_account(rt, {payer}, {var},"
|
|
934
|
+
f' (const uint8_t *)"{seed0}", {len(seed0)}u, (uint8_t)({bump}),'
|
|
935
|
+
f" {rent}u, {model.size}u) != RT_OK) {{",
|
|
936
|
+
1,
|
|
937
|
+
)
|
|
938
|
+
self._line(
|
|
939
|
+
f'rt_fail_log({RT_ERR_CPI}u, {loc}, "System create_account failed");',
|
|
940
|
+
2,
|
|
941
|
+
)
|
|
942
|
+
self._line(f"return {RT_ERR_CPI}u;", 2)
|
|
943
|
+
self._line("}", 1)
|
|
944
|
+
upper = param.account.upper()
|
|
945
|
+
self._line(f"rt_st_disc({var}->data, RT_DISC_{upper});", 1)
|
|
946
|
+
|
|
947
|
+
def _find_bump(self) -> str | None:
|
|
948
|
+
for name, (_var, ctype) in self._data_vars.items():
|
|
949
|
+
if name == "bump" and ctype in ("uint8_t", "bool"):
|
|
950
|
+
return f"dp_{name}"
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
def _find_payer(self) -> str | None:
|
|
954
|
+
for param in self._iparams:
|
|
955
|
+
if param.kind == "signer":
|
|
956
|
+
return f"acc_{param.name}"
|
|
957
|
+
return None
|
|
958
|
+
|
|
959
|
+
# -- dispatch ----------------------------------------------------------
|
|
960
|
+
|
|
961
|
+
def _emit_dispatch_table(self) -> None:
|
|
962
|
+
self._line("static const RtDispatchEntry RT_DISPATCH[] = {", 0)
|
|
963
|
+
for instruction in self._model.instructions:
|
|
964
|
+
disc = ", ".join(f"{byte:#x}u" for byte in instruction.discriminator)
|
|
965
|
+
self._line(
|
|
966
|
+
f" {{ {{ {disc} }}, &pyf_{instruction.snake_name} }},",
|
|
967
|
+
0,
|
|
968
|
+
)
|
|
969
|
+
self._line("};", 0)
|
|
970
|
+
self._line("", 0)
|
|
971
|
+
self._line("uint64_t entrypoint(const unsigned char *input) {", 0)
|
|
972
|
+
self._line(
|
|
973
|
+
f"return rt_entrypoint(input, RT_DISPATCH, {len(self._model.instructions)}u);",
|
|
974
|
+
1,
|
|
975
|
+
)
|
|
976
|
+
self._line("}", 0)
|
|
977
|
+
|
|
978
|
+
# -- statements --------------------------------------------------------
|
|
979
|
+
|
|
980
|
+
def _emit_stmt(self, node: ast.stmt, instruction: InstructionModel) -> None:
|
|
981
|
+
if isinstance(node, ast.AnnAssign):
|
|
982
|
+
self._emit_ann_assign(node)
|
|
983
|
+
return
|
|
984
|
+
if isinstance(node, ast.Assign):
|
|
985
|
+
self._emit_assign(node)
|
|
986
|
+
return
|
|
987
|
+
if isinstance(node, ast.AugAssign):
|
|
988
|
+
self._emit_aug_assign(node)
|
|
989
|
+
return
|
|
990
|
+
if isinstance(node, ast.If):
|
|
991
|
+
self._emit_if(node, instruction)
|
|
992
|
+
return
|
|
993
|
+
if isinstance(node, ast.While):
|
|
994
|
+
self._emit_while(node, instruction)
|
|
995
|
+
return
|
|
996
|
+
if isinstance(node, ast.For):
|
|
997
|
+
self._emit_for(node, instruction)
|
|
998
|
+
return
|
|
999
|
+
if isinstance(node, ast.Break):
|
|
1000
|
+
self._emit_break(node)
|
|
1001
|
+
return
|
|
1002
|
+
if isinstance(node, ast.Continue):
|
|
1003
|
+
self._emit_continue(node)
|
|
1004
|
+
return
|
|
1005
|
+
if isinstance(node, ast.Return):
|
|
1006
|
+
self._emit_return(node)
|
|
1007
|
+
return
|
|
1008
|
+
if isinstance(node, ast.Match):
|
|
1009
|
+
self._emit_match(node, instruction)
|
|
1010
|
+
return
|
|
1011
|
+
if isinstance(node, ast.Expr):
|
|
1012
|
+
if isinstance(node.value, ast.Call):
|
|
1013
|
+
self._emit_call_stmt(node.value)
|
|
1014
|
+
return
|
|
1015
|
+
if isinstance(node, ast.Pass):
|
|
1016
|
+
self._s("/* pass */")
|
|
1017
|
+
return
|
|
1018
|
+
self._diag(
|
|
1019
|
+
PS5001,
|
|
1020
|
+
node,
|
|
1021
|
+
f"unsupported statement `{type(node).__name__}` in `{instruction.name}`",
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
# -- control flow --------------------------------------------------------
|
|
1025
|
+
|
|
1026
|
+
def _emit_if(self, node: ast.If, instruction: InstructionModel) -> None:
|
|
1027
|
+
cond = self._load(node.test)
|
|
1028
|
+
if cond.kind != "bool":
|
|
1029
|
+
self._diag(PS5002, node.test, "if condition must be bool")
|
|
1030
|
+
self._s(f"if ({cond.text}) {{")
|
|
1031
|
+
self._emit_block(node.body, instruction)
|
|
1032
|
+
if node.orelse:
|
|
1033
|
+
self._s("} else {")
|
|
1034
|
+
self._emit_block(node.orelse, instruction)
|
|
1035
|
+
self._s("}")
|
|
1036
|
+
|
|
1037
|
+
def _emit_block(self, stmts: list[ast.stmt], instruction: InstructionModel) -> None:
|
|
1038
|
+
self._indent += 1
|
|
1039
|
+
for stmt in stmts:
|
|
1040
|
+
self._emit_stmt(stmt, instruction)
|
|
1041
|
+
self._indent -= 1
|
|
1042
|
+
|
|
1043
|
+
def _emit_while(self, node: ast.While, instruction: InstructionModel) -> None:
|
|
1044
|
+
frame = self._push_loop(bool(node.orelse), continue_at_head=True)
|
|
1045
|
+
self._s(f"{frame.head}:")
|
|
1046
|
+
# Condition temps live in this block so `goto head` does not cross a
|
|
1047
|
+
# C declaration (clang rejects that).
|
|
1048
|
+
self._s("{")
|
|
1049
|
+
self._indent += 1
|
|
1050
|
+
cond = self._load(node.test)
|
|
1051
|
+
if cond.kind != "bool":
|
|
1052
|
+
self._diag(PS5002, node.test, "while condition must be bool")
|
|
1053
|
+
self._s(f"if (!({cond.text})) goto {frame.done};")
|
|
1054
|
+
for stmt in node.body:
|
|
1055
|
+
self._emit_stmt(stmt, instruction)
|
|
1056
|
+
self._indent -= 1
|
|
1057
|
+
self._s("}")
|
|
1058
|
+
self._s(f"goto {frame.head};")
|
|
1059
|
+
self._finish_loop(frame, node.orelse, instruction)
|
|
1060
|
+
|
|
1061
|
+
def _emit_for(self, node: ast.For, instruction: InstructionModel) -> None:
|
|
1062
|
+
it = node.iter
|
|
1063
|
+
if not (
|
|
1064
|
+
isinstance(it, ast.Call) and isinstance(it.func, ast.Name) and it.func.id == "range"
|
|
1065
|
+
):
|
|
1066
|
+
self._diag(PS5001, node, "only `for x in range(...)` is lowered")
|
|
1067
|
+
return
|
|
1068
|
+
if not isinstance(node.target, ast.Name):
|
|
1069
|
+
self._diag(PS5001, node, "loop variables must be plain names")
|
|
1070
|
+
return
|
|
1071
|
+
name = node.target.id
|
|
1072
|
+
if name in self._locals or name in self._data_vars or name in self._acct_vars:
|
|
1073
|
+
self._diag(PS5002, node, f"`{name}` is already declared")
|
|
1074
|
+
return
|
|
1075
|
+
args = list(it.args)
|
|
1076
|
+
if not 1 <= len(args) <= 3:
|
|
1077
|
+
self._diag(PS5001, it, "range() takes 1, 2, or 3 arguments")
|
|
1078
|
+
return
|
|
1079
|
+
start_n, stop_n, step_n = self._range_args(args, node)
|
|
1080
|
+
if isinstance(step_n, ast.Constant) and isinstance(step_n.value, int):
|
|
1081
|
+
if step_n.value == 0:
|
|
1082
|
+
self._diag(PS5002, step_n, "range() step must not be zero")
|
|
1083
|
+
return
|
|
1084
|
+
if step_n.value < 0:
|
|
1085
|
+
self._diag(PS5002, step_n, "range() step must be positive")
|
|
1086
|
+
return
|
|
1087
|
+
start = self._as_u64(start_n)
|
|
1088
|
+
stop = self._as_u64(stop_n)
|
|
1089
|
+
step = self._as_u64(step_n)
|
|
1090
|
+
var = f"v_{name}"
|
|
1091
|
+
self._locals[name] = (var, "uint64_t")
|
|
1092
|
+
self._s(f"uint64_t {var} = {start};")
|
|
1093
|
+
self._s(f"if ({step} == 0u) {{")
|
|
1094
|
+
self._s(
|
|
1095
|
+
f'rt_fail_log({RT_ERR_DIV_ZERO}u, {self._loc(node.lineno)}, "range step is zero");',
|
|
1096
|
+
1,
|
|
1097
|
+
)
|
|
1098
|
+
self._s(f"return {RT_ERR_DIV_ZERO}u;", 1)
|
|
1099
|
+
self._s("}")
|
|
1100
|
+
frame = self._push_loop(bool(node.orelse), continue_at_head=False)
|
|
1101
|
+
self._s(f"{frame.head}:")
|
|
1102
|
+
self._s(f"if (!({var} < {stop})) goto {frame.done};")
|
|
1103
|
+
self._s("{")
|
|
1104
|
+
self._emit_block(node.body, instruction)
|
|
1105
|
+
self._s("}")
|
|
1106
|
+
self._s(f"{frame.cont}:")
|
|
1107
|
+
self._s("{")
|
|
1108
|
+
self._indent += 1
|
|
1109
|
+
nxt = self._mk_tmp()
|
|
1110
|
+
self._s(f"uint64_t {nxt};")
|
|
1111
|
+
self._checked_call("add", var, step, nxt, "u64", node)
|
|
1112
|
+
self._s(f"{var} = {nxt};")
|
|
1113
|
+
self._indent -= 1
|
|
1114
|
+
self._s("}")
|
|
1115
|
+
self._s(f"goto {frame.head};")
|
|
1116
|
+
self._finish_loop(frame, node.orelse, instruction)
|
|
1117
|
+
|
|
1118
|
+
def _range_args(
|
|
1119
|
+
self, args: list[ast.expr], node: ast.AST
|
|
1120
|
+
) -> tuple[ast.expr, ast.expr, ast.expr]:
|
|
1121
|
+
zero = ast.Constant(value=0)
|
|
1122
|
+
one = ast.Constant(value=1)
|
|
1123
|
+
zero.lineno = getattr(node, "lineno", 1) or 1
|
|
1124
|
+
zero.col_offset = getattr(node, "col_offset", 0) or 0
|
|
1125
|
+
one.lineno = zero.lineno
|
|
1126
|
+
one.col_offset = zero.col_offset
|
|
1127
|
+
if len(args) == 1:
|
|
1128
|
+
return zero, args[0], one
|
|
1129
|
+
if len(args) == 2:
|
|
1130
|
+
return args[0], args[1], one
|
|
1131
|
+
return args[0], args[1], args[2]
|
|
1132
|
+
|
|
1133
|
+
def _push_loop(self, has_else: bool, *, continue_at_head: bool) -> _Loop:
|
|
1134
|
+
self._loop_no += 1
|
|
1135
|
+
n = self._loop_no
|
|
1136
|
+
flag: str | None = None
|
|
1137
|
+
if has_else:
|
|
1138
|
+
flag = f"rt_broke_{n}"
|
|
1139
|
+
self._s(f"uint8_t {flag} = 0u;")
|
|
1140
|
+
head = f"rt_h{n}"
|
|
1141
|
+
cont = head if continue_at_head else f"rt_c{n}"
|
|
1142
|
+
frame = _Loop(head=head, cont=cont, done=f"rt_d{n}", flag=flag)
|
|
1143
|
+
self._loops.append(frame)
|
|
1144
|
+
return frame
|
|
1145
|
+
|
|
1146
|
+
def _finish_loop(
|
|
1147
|
+
self,
|
|
1148
|
+
frame: _Loop,
|
|
1149
|
+
orelse: list[ast.stmt],
|
|
1150
|
+
instruction: InstructionModel,
|
|
1151
|
+
) -> None:
|
|
1152
|
+
self._s(f"{frame.done}: ;")
|
|
1153
|
+
if frame.flag is not None:
|
|
1154
|
+
self._s(f"if (!{frame.flag}) {{")
|
|
1155
|
+
self._emit_block(orelse, instruction)
|
|
1156
|
+
self._s("}")
|
|
1157
|
+
self._loops.pop()
|
|
1158
|
+
|
|
1159
|
+
def _emit_break(self, node: ast.Break) -> None:
|
|
1160
|
+
if not self._loops:
|
|
1161
|
+
self._diag(PS5001, node, "`break` is outside a loop")
|
|
1162
|
+
return
|
|
1163
|
+
frame = self._loops[-1]
|
|
1164
|
+
if frame.flag is not None:
|
|
1165
|
+
self._s(f"{frame.flag} = 1u;")
|
|
1166
|
+
self._s(f"goto {frame.done};")
|
|
1167
|
+
|
|
1168
|
+
def _emit_continue(self, node: ast.Continue) -> None:
|
|
1169
|
+
if not self._loops:
|
|
1170
|
+
self._diag(PS5001, node, "`continue` is outside a loop")
|
|
1171
|
+
return
|
|
1172
|
+
self._s(f"goto {self._loops[-1].cont};")
|
|
1173
|
+
|
|
1174
|
+
def _emit_match(self, node: ast.Match, instruction: InstructionModel) -> None:
|
|
1175
|
+
"""Lower ``match`` on a unit enum to ``if`` / ``else if`` plus a tag check."""
|
|
1176
|
+
enum_name = self._enum_of_expr(node.subject)
|
|
1177
|
+
if enum_name is None or enum_name not in self._enums:
|
|
1178
|
+
self._diag(PS5002, node.subject, "match subject must be a unit enum")
|
|
1179
|
+
return
|
|
1180
|
+
enum = self._enums[enum_name]
|
|
1181
|
+
arms: list[tuple[int, list[ast.stmt]]] = []
|
|
1182
|
+
wild: list[ast.stmt] | None = None
|
|
1183
|
+
seen: set[int] = set()
|
|
1184
|
+
for case in node.cases:
|
|
1185
|
+
parsed = self._match_arm(case, enum_name)
|
|
1186
|
+
if parsed is None:
|
|
1187
|
+
return
|
|
1188
|
+
tag, body = parsed
|
|
1189
|
+
if tag is None:
|
|
1190
|
+
if wild is not None:
|
|
1191
|
+
self._diag(PS5002, case.pattern, "wildcard must be the last match case")
|
|
1192
|
+
return
|
|
1193
|
+
wild = body
|
|
1194
|
+
continue
|
|
1195
|
+
if wild is not None:
|
|
1196
|
+
self._diag(PS5002, case.pattern, "wildcard must be the last match case")
|
|
1197
|
+
return
|
|
1198
|
+
if tag in seen:
|
|
1199
|
+
self._diag(PS5002, case.pattern, "variant is already matched")
|
|
1200
|
+
return
|
|
1201
|
+
seen.add(tag)
|
|
1202
|
+
arms.append((tag, body))
|
|
1203
|
+
if wild is None:
|
|
1204
|
+
missing = [name for name, tag in enum.variants if tag not in seen]
|
|
1205
|
+
if missing:
|
|
1206
|
+
self._diag(
|
|
1207
|
+
PS5002,
|
|
1208
|
+
node,
|
|
1209
|
+
f"match on `{enum_name}` is not exhaustive; missing {', '.join(missing)}",
|
|
1210
|
+
)
|
|
1211
|
+
return
|
|
1212
|
+
if not arms:
|
|
1213
|
+
if wild is not None:
|
|
1214
|
+
for stmt in wild:
|
|
1215
|
+
self._emit_stmt(stmt, instruction)
|
|
1216
|
+
return
|
|
1217
|
+
scrut = self._load(node.subject)
|
|
1218
|
+
if _canon(scrut.kind) != "uint8_t":
|
|
1219
|
+
self._diag(PS5002, node.subject, "match subject must be a unit enum")
|
|
1220
|
+
return
|
|
1221
|
+
for index, (tag, body) in enumerate(arms):
|
|
1222
|
+
keyword = "if" if index == 0 else "} else if"
|
|
1223
|
+
self._s(f"{keyword} ({scrut.text} == {tag}u) {{")
|
|
1224
|
+
self._emit_block(body, instruction)
|
|
1225
|
+
self._s("} else {")
|
|
1226
|
+
if wild is not None:
|
|
1227
|
+
self._emit_block(wild, instruction)
|
|
1228
|
+
else:
|
|
1229
|
+
line = getattr(node, "lineno", 1) or 1
|
|
1230
|
+
self._s(
|
|
1231
|
+
f'rt_fail_log({RT_ERR_ENUM}u, {self._loc(line)}, "invalid enum tag");',
|
|
1232
|
+
1,
|
|
1233
|
+
)
|
|
1234
|
+
self._s(f"return {RT_ERR_ENUM}u;", 1)
|
|
1235
|
+
self._s("}")
|
|
1236
|
+
|
|
1237
|
+
def _match_arm(
|
|
1238
|
+
self, case: ast.match_case, enum_name: str
|
|
1239
|
+
) -> tuple[int | None, list[ast.stmt]] | None:
|
|
1240
|
+
if case.guard is not None:
|
|
1241
|
+
self._diag(PS5001, case.guard, "match guards are not supported")
|
|
1242
|
+
return None
|
|
1243
|
+
pattern = case.pattern
|
|
1244
|
+
if isinstance(pattern, ast.MatchAs) and pattern.pattern is None and pattern.name is None:
|
|
1245
|
+
return None, case.body
|
|
1246
|
+
if isinstance(pattern, ast.MatchValue) and isinstance(pattern.value, ast.Attribute):
|
|
1247
|
+
value = pattern.value
|
|
1248
|
+
if isinstance(value.value, ast.Name) and value.value.id == enum_name:
|
|
1249
|
+
for variant, tag in self._enums[enum_name].variants:
|
|
1250
|
+
if variant == value.attr:
|
|
1251
|
+
return tag, case.body
|
|
1252
|
+
self._diag(PS5002, pattern, f"`{enum_name}` has no variant `{value.attr}`")
|
|
1253
|
+
return None
|
|
1254
|
+
self._diag(PS5001, pattern, "match cases must be `Enum.Variant` or `_`")
|
|
1255
|
+
return None
|
|
1256
|
+
|
|
1257
|
+
def _enum_of_expr(self, node: ast.expr) -> str | None:
|
|
1258
|
+
"""Unit-enum name of a value, or ``None`` when it is not one."""
|
|
1259
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
1260
|
+
helper = self._helpers.get(node.func.id)
|
|
1261
|
+
if helper is not None and helper.return_defined:
|
|
1262
|
+
return helper.return_defined
|
|
1263
|
+
if isinstance(node, ast.Name):
|
|
1264
|
+
return self._value_enum.get(node.id)
|
|
1265
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Attribute):
|
|
1266
|
+
found = self._lookup_field(node)
|
|
1267
|
+
if found is not None:
|
|
1268
|
+
leaf, _acc = found
|
|
1269
|
+
if leaf.defined and leaf.shape not in ("option", "struct", "string"):
|
|
1270
|
+
return leaf.defined
|
|
1271
|
+
if isinstance(node, ast.Attribute) and node.attr == "value":
|
|
1272
|
+
meta = self._option_meta(node.value)
|
|
1273
|
+
if meta is not None and meta[1]:
|
|
1274
|
+
return meta[1]
|
|
1275
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
1276
|
+
base = node.value.id
|
|
1277
|
+
if base in self._enums:
|
|
1278
|
+
return base
|
|
1279
|
+
entry = self._acct_vars.get(base)
|
|
1280
|
+
if entry is None:
|
|
1281
|
+
return None
|
|
1282
|
+
account = self._accounts.get(entry[1])
|
|
1283
|
+
if account is None:
|
|
1284
|
+
return None
|
|
1285
|
+
field = account.field(node.attr)
|
|
1286
|
+
if field is not None and field.defined and field.shape != "option":
|
|
1287
|
+
return field.defined
|
|
1288
|
+
return None
|
|
1289
|
+
if isinstance(node, ast.Subscript):
|
|
1290
|
+
target = node.value
|
|
1291
|
+
if isinstance(target, ast.Name):
|
|
1292
|
+
return self._array_enum.get(target.id)
|
|
1293
|
+
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name):
|
|
1294
|
+
entry = self._acct_vars.get(target.value.id)
|
|
1295
|
+
if entry is None:
|
|
1296
|
+
return None
|
|
1297
|
+
account = self._accounts.get(entry[1])
|
|
1298
|
+
if account is None:
|
|
1299
|
+
return None
|
|
1300
|
+
field = account.field(target.attr)
|
|
1301
|
+
if field is not None and field.shape == "array" and field.defined:
|
|
1302
|
+
return field.defined
|
|
1303
|
+
return None
|
|
1304
|
+
|
|
1305
|
+
def _emit_return(self, node: ast.Return) -> None:
|
|
1306
|
+
if self._inline:
|
|
1307
|
+
self._emit_helper_return(node)
|
|
1308
|
+
return
|
|
1309
|
+
if node.value is not None and not (
|
|
1310
|
+
isinstance(node.value, ast.Constant) and node.value.value is None
|
|
1311
|
+
):
|
|
1312
|
+
self._diag(PS5001, node, "only a bare `return` is lowered")
|
|
1313
|
+
return
|
|
1314
|
+
self._s("return 0u;")
|
|
1315
|
+
|
|
1316
|
+
def _emit_helper_return(self, node: ast.Return) -> None:
|
|
1317
|
+
frame = self._inline[-1]
|
|
1318
|
+
bare = node.value is None or (
|
|
1319
|
+
isinstance(node.value, ast.Constant) and node.value.value is None
|
|
1320
|
+
)
|
|
1321
|
+
if frame.result is None:
|
|
1322
|
+
if not bare:
|
|
1323
|
+
self._diag(PS5002, node, "this helper returns nothing")
|
|
1324
|
+
self._s("break;")
|
|
1325
|
+
return
|
|
1326
|
+
if bare or node.value is None:
|
|
1327
|
+
self._diag(PS5002, node, "helper must return a value")
|
|
1328
|
+
self._s("break;")
|
|
1329
|
+
return
|
|
1330
|
+
frag = self._load(node.value)
|
|
1331
|
+
self._store_frag(frame.result, frame.c_type, frag, node.value)
|
|
1332
|
+
self._s("break;")
|
|
1333
|
+
|
|
1334
|
+
def _emit_call_stmt(self, call: ast.Call) -> None:
|
|
1335
|
+
func = call.func
|
|
1336
|
+
if isinstance(func, ast.Name) and func.id == "require":
|
|
1337
|
+
self._emit_require(call)
|
|
1338
|
+
return
|
|
1339
|
+
if isinstance(func, ast.Name) and func.id == "emit":
|
|
1340
|
+
self._emit_event(call)
|
|
1341
|
+
return
|
|
1342
|
+
if isinstance(func, ast.Name) and func.id == "system_transfer":
|
|
1343
|
+
self._emit_system_transfer(call)
|
|
1344
|
+
return
|
|
1345
|
+
if isinstance(func, ast.Name) and func.id in ("token_transfer", "token2022_transfer"):
|
|
1346
|
+
self._emit_token_transfer(call, func.id == "token2022_transfer")
|
|
1347
|
+
return
|
|
1348
|
+
if isinstance(func, ast.Name) and func.id == "ata_create":
|
|
1349
|
+
self._emit_ata_create(call)
|
|
1350
|
+
return
|
|
1351
|
+
if isinstance(func, ast.Name) and func.id in _CAST_NAMES:
|
|
1352
|
+
self._diag(PS5001, call, "a cast is not a statement; store the result")
|
|
1353
|
+
return
|
|
1354
|
+
if isinstance(func, ast.Name) and func.id in self._helpers:
|
|
1355
|
+
self._inline_helper(call, self._helpers[func.id], as_value=False)
|
|
1356
|
+
return
|
|
1357
|
+
self._diag(
|
|
1358
|
+
PS5001,
|
|
1359
|
+
func,
|
|
1360
|
+
"only `require`, `emit`, and `system_transfer` calls are statements",
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
def _inline_helper(self, call: ast.Call, helper: HelperModel, *, as_value: bool) -> _Frag:
|
|
1364
|
+
"""Inline ``helper`` in the caller. Arguments are evaluated first.
|
|
1365
|
+
|
|
1366
|
+
The helper's maps replace the caller's for the body, so it cannot see
|
|
1367
|
+
accounts it was not passed. ``break`` stores the result and leaves the
|
|
1368
|
+
``do``/``while``; checked math still ``return``s from the instruction.
|
|
1369
|
+
"""
|
|
1370
|
+
if self._instr is None:
|
|
1371
|
+
self._diag(PS5001, call, "a helper call is outside an instruction")
|
|
1372
|
+
return _Frag("0u", "uint64_t")
|
|
1373
|
+
if len(self._inline) >= 8 or any(frame.name == helper.name for frame in self._inline):
|
|
1374
|
+
self._diag(PS5002, call, f"helper `{helper.name}` recurses or is too deep")
|
|
1375
|
+
return _Frag("0u", "uint64_t")
|
|
1376
|
+
if call.keywords or len(call.args) != len(helper.params):
|
|
1377
|
+
self._diag(
|
|
1378
|
+
PS5001,
|
|
1379
|
+
call,
|
|
1380
|
+
f"`{helper.name}()` takes {len(helper.params)} argument(s)",
|
|
1381
|
+
)
|
|
1382
|
+
return _Frag("0u", "uint64_t")
|
|
1383
|
+
if as_value and not helper.return_c:
|
|
1384
|
+
self._diag(PS5002, call, f"`{helper.name}` returns nothing")
|
|
1385
|
+
return _Frag("0u", "uint64_t")
|
|
1386
|
+
|
|
1387
|
+
bound_locals: dict[str, tuple[str, str]] = {}
|
|
1388
|
+
bound_accounts: dict[str, tuple[str, str]] = {}
|
|
1389
|
+
bound_enums: dict[str, str] = {}
|
|
1390
|
+
for arg, param in zip(call.args, helper.params, strict=False):
|
|
1391
|
+
if param.kind == "account":
|
|
1392
|
+
if not isinstance(arg, ast.Name) or arg.id not in self._acct_vars:
|
|
1393
|
+
self._diag(PS5002, arg, "pass an account parameter by name")
|
|
1394
|
+
continue
|
|
1395
|
+
acc_var, cls = self._acct_vars[arg.id]
|
|
1396
|
+
if param.account != "Pubkey" and cls != param.account:
|
|
1397
|
+
self._diag(PS5002, arg, f"expected account `{param.account}`")
|
|
1398
|
+
continue
|
|
1399
|
+
bound_accounts[param.name] = (acc_var, cls)
|
|
1400
|
+
continue
|
|
1401
|
+
if (
|
|
1402
|
+
param.c_type == "SolPubkey"
|
|
1403
|
+
and isinstance(arg, ast.Name)
|
|
1404
|
+
and arg.id in self._acct_vars
|
|
1405
|
+
):
|
|
1406
|
+
acc_var = self._acct_vars[arg.id][0]
|
|
1407
|
+
tmp = self._mk_tmp()
|
|
1408
|
+
self._s(f"SolPubkey {tmp};")
|
|
1409
|
+
self._s(f"rt_key_copy(&{tmp}, {acc_var}->key);")
|
|
1410
|
+
bound_locals[param.name] = (tmp, "SolPubkey")
|
|
1411
|
+
continue
|
|
1412
|
+
frag = self._load(arg)
|
|
1413
|
+
tmp = self._mk_tmp()
|
|
1414
|
+
ctype = self._declare_zero(tmp, param.c_type)
|
|
1415
|
+
self._store_frag(tmp, ctype, frag, arg)
|
|
1416
|
+
bound_locals[param.name] = (tmp, ctype)
|
|
1417
|
+
if param.defined:
|
|
1418
|
+
bound_enums[param.name] = param.defined
|
|
1419
|
+
|
|
1420
|
+
result_var: str | None = None
|
|
1421
|
+
result_c = ""
|
|
1422
|
+
if helper.return_c:
|
|
1423
|
+
result_var = self._mk_tmp()
|
|
1424
|
+
result_c = self._declare_zero(result_var, helper.return_c)
|
|
1425
|
+
|
|
1426
|
+
saved = (
|
|
1427
|
+
self._locals,
|
|
1428
|
+
self._acct_vars,
|
|
1429
|
+
self._arrays,
|
|
1430
|
+
self._options,
|
|
1431
|
+
self._strings,
|
|
1432
|
+
self._value_enum,
|
|
1433
|
+
self._array_enum,
|
|
1434
|
+
self._loops,
|
|
1435
|
+
)
|
|
1436
|
+
self._locals = bound_locals
|
|
1437
|
+
self._acct_vars = bound_accounts
|
|
1438
|
+
self._arrays = {}
|
|
1439
|
+
self._options = {}
|
|
1440
|
+
self._strings = {}
|
|
1441
|
+
self._value_enum = bound_enums
|
|
1442
|
+
self._array_enum = {}
|
|
1443
|
+
self._loops = []
|
|
1444
|
+
self._inline.append(
|
|
1445
|
+
_Inline(helper.name, result_var, result_c, helper.return_defined)
|
|
1446
|
+
)
|
|
1447
|
+
saved_log = self._log_file
|
|
1448
|
+
if helper.source:
|
|
1449
|
+
self._log_file = helper.source
|
|
1450
|
+
try:
|
|
1451
|
+
self._s("do {")
|
|
1452
|
+
self._indent += 1
|
|
1453
|
+
for _name, (var, _ctype) in bound_locals.items():
|
|
1454
|
+
self._s(f"(void){var};")
|
|
1455
|
+
for _name, (var, _cls) in bound_accounts.items():
|
|
1456
|
+
self._s(f"(void){var};")
|
|
1457
|
+
for stmt in helper.body:
|
|
1458
|
+
self._emit_stmt(stmt, self._instr)
|
|
1459
|
+
self._indent -= 1
|
|
1460
|
+
self._s("} while (0);")
|
|
1461
|
+
finally:
|
|
1462
|
+
self._log_file = saved_log
|
|
1463
|
+
self._inline.pop()
|
|
1464
|
+
(
|
|
1465
|
+
self._locals,
|
|
1466
|
+
self._acct_vars,
|
|
1467
|
+
self._arrays,
|
|
1468
|
+
self._options,
|
|
1469
|
+
self._strings,
|
|
1470
|
+
self._value_enum,
|
|
1471
|
+
self._array_enum,
|
|
1472
|
+
self._loops,
|
|
1473
|
+
) = saved
|
|
1474
|
+
if result_var is None:
|
|
1475
|
+
return _Frag("0u", "uint64_t")
|
|
1476
|
+
if not as_value:
|
|
1477
|
+
self._s(f"(void){result_var};")
|
|
1478
|
+
return _Frag("0u", "uint64_t")
|
|
1479
|
+
if result_c == "SolPubkey":
|
|
1480
|
+
return _Frag(f"&{result_var}", "pub")
|
|
1481
|
+
if helper.return_c == "bool":
|
|
1482
|
+
return _Frag(result_var, "bool")
|
|
1483
|
+
return _Frag(result_var, result_c)
|
|
1484
|
+
|
|
1485
|
+
def _declare_zero(self, var: str, model_c: str) -> str:
|
|
1486
|
+
"""Declare ``var`` zeroed. Returns the expression C type."""
|
|
1487
|
+
ctype = _WIDTH_C.get(model_c, model_c)
|
|
1488
|
+
if ctype == "SolPubkey":
|
|
1489
|
+
self._s(f"SolPubkey {var} = {{{{0}}}};")
|
|
1490
|
+
elif ctype in ("RtU128", "RtI128") or ctype.startswith("int"):
|
|
1491
|
+
self._s(f"{ctype} {var} = 0;")
|
|
1492
|
+
else:
|
|
1493
|
+
self._s(f"{ctype} {var} = 0u;")
|
|
1494
|
+
return ctype
|
|
1495
|
+
|
|
1496
|
+
def _store_frag(self, var: str, ctype: str, frag: _Frag, node: ast.AST) -> None:
|
|
1497
|
+
if ctype == "SolPubkey":
|
|
1498
|
+
if frag.kind != "pub":
|
|
1499
|
+
self._diag(PS5002, node, "cannot store a non-Pubkey into a Pubkey")
|
|
1500
|
+
return
|
|
1501
|
+
self._s(f"rt_key_copy(&{var}, {frag.text});")
|
|
1502
|
+
return
|
|
1503
|
+
if frag.kind == "pub":
|
|
1504
|
+
self._diag(PS5002, node, "cannot store a Pubkey into a number")
|
|
1505
|
+
return
|
|
1506
|
+
text = frag.text
|
|
1507
|
+
if _canon(frag.kind) != _canon(ctype):
|
|
1508
|
+
text = self._coerce_numeric(frag, ctype, node)
|
|
1509
|
+
self._s(f"{var} = ({ctype})({text});")
|
|
1510
|
+
|
|
1511
|
+
# -- fixed-width options -----------------------------------------------
|
|
1512
|
+
|
|
1513
|
+
def _emit_option_data(self, param: DataParamModel, offset: int) -> None:
|
|
1514
|
+
"""Load a tag byte and the reserved inner value from instruction data."""
|
|
1515
|
+
tag = f"dp_{param.name}_tag"
|
|
1516
|
+
var = f"dp_{param.name}"
|
|
1517
|
+
self._s(f"uint8_t {tag} = rt->data[{offset}u];")
|
|
1518
|
+
ctype = self._emit_loaded_var(var, param.c_type, offset + 1, "rt->data")
|
|
1519
|
+
self._options[param.name] = _OptVar(tag, var, ctype, param.defined)
|
|
1520
|
+
|
|
1521
|
+
def _emit_loaded_var(self, var: str, c_type: str, offset: int, data: str) -> str:
|
|
1522
|
+
"""Declare ``var`` and load one scalar. Returns the expression C type."""
|
|
1523
|
+
if c_type == "SolPubkey":
|
|
1524
|
+
self._s(f"SolPubkey {var};")
|
|
1525
|
+
self._s(
|
|
1526
|
+
"rt_key_copy(&"
|
|
1527
|
+
f"{var}, (const SolPubkey *)(const void *)({data} + {offset}u));"
|
|
1528
|
+
)
|
|
1529
|
+
return "SolPubkey"
|
|
1530
|
+
if c_type in ("bool", "uint8_t"):
|
|
1531
|
+
self._s(f"uint8_t {var} = {data}[{offset}u];")
|
|
1532
|
+
return "uint8_t"
|
|
1533
|
+
if c_type == "int8_t":
|
|
1534
|
+
self._s(f"int8_t {var} = 0;")
|
|
1535
|
+
self._s(f"{{ uint8_t rt_bits = {data}[{offset}u]; {var} = (int8_t)rt_bits; }}")
|
|
1536
|
+
return "int8_t"
|
|
1537
|
+
if c_type == "uint16_t":
|
|
1538
|
+
self._s(f"uint16_t {var} = 0u;")
|
|
1539
|
+
self._s(f"rt_ld_u16({data}, {offset}u, &{var});")
|
|
1540
|
+
return "uint16_t"
|
|
1541
|
+
if c_type == "uint32_t":
|
|
1542
|
+
self._s(f"uint32_t {var} = 0u;")
|
|
1543
|
+
self._s(f"rt_ld_u32({data}, {offset}u, &{var});")
|
|
1544
|
+
return "uint32_t"
|
|
1545
|
+
if c_type == "int16_t":
|
|
1546
|
+
self._s(f"int16_t {var} = 0;")
|
|
1547
|
+
self._s(
|
|
1548
|
+
"{ uint16_t rt_bits = 0u;"
|
|
1549
|
+
f" rt_ld_u16({data}, {offset}u, &rt_bits); {var} = (int16_t)rt_bits; }}"
|
|
1550
|
+
)
|
|
1551
|
+
return "int16_t"
|
|
1552
|
+
if c_type == "int32_t":
|
|
1553
|
+
self._s(f"int32_t {var} = 0;")
|
|
1554
|
+
self._s(
|
|
1555
|
+
"{ uint32_t rt_bits = 0u;"
|
|
1556
|
+
f" rt_ld_u32({data}, {offset}u, &rt_bits); {var} = (int32_t)rt_bits; }}"
|
|
1557
|
+
)
|
|
1558
|
+
return "int32_t"
|
|
1559
|
+
if c_type == "int64_t":
|
|
1560
|
+
self._s(f"int64_t {var} = 0;")
|
|
1561
|
+
self._s(
|
|
1562
|
+
"{ uint64_t rt_bits = 0u;"
|
|
1563
|
+
f" rt_ld_u64({data}, {offset}u, &rt_bits); {var} = (int64_t)rt_bits; }}"
|
|
1564
|
+
)
|
|
1565
|
+
return "int64_t"
|
|
1566
|
+
if c_type == "uint128_t":
|
|
1567
|
+
self._s(f"RtU128 {var} = 0;")
|
|
1568
|
+
self._s(f"rt_ld_u128({data}, {offset}u, &{var});")
|
|
1569
|
+
return "RtU128"
|
|
1570
|
+
if c_type == "int128_t":
|
|
1571
|
+
self._s(f"RtI128 {var} = 0;")
|
|
1572
|
+
self._s(
|
|
1573
|
+
"{ RtU128 rt_bits = 0;"
|
|
1574
|
+
f" rt_ld_u128({data}, {offset}u, &rt_bits); {var} = (RtI128)rt_bits; }}"
|
|
1575
|
+
)
|
|
1576
|
+
return "RtI128"
|
|
1577
|
+
self._s(f"uint64_t {var} = 0u;")
|
|
1578
|
+
self._s(f"rt_ld_u64({data}, {offset}u, &{var});")
|
|
1579
|
+
return "uint64_t"
|
|
1580
|
+
|
|
1581
|
+
def _option_ann(self, ann: ast.Subscript) -> tuple[str, str] | None:
|
|
1582
|
+
"""``(model c type, inner enum name)`` for ``Option[scalar]``."""
|
|
1583
|
+
if not isinstance(ann.value, ast.Name) or ann.value.id != "Option":
|
|
1584
|
+
return None
|
|
1585
|
+
items = list(ann.slice.elts) if isinstance(ann.slice, ast.Tuple) else [ann.slice]
|
|
1586
|
+
if len(items) != 1 or not isinstance(items[0], ast.Name):
|
|
1587
|
+
return None
|
|
1588
|
+
inner = items[0].id
|
|
1589
|
+
if inner in self._enums:
|
|
1590
|
+
return "uint8_t", inner
|
|
1591
|
+
mapped = {
|
|
1592
|
+
"u8": "uint8_t",
|
|
1593
|
+
"u16": "uint16_t",
|
|
1594
|
+
"u32": "uint32_t",
|
|
1595
|
+
"u64": "uint64_t",
|
|
1596
|
+
"u128": "uint128_t",
|
|
1597
|
+
"i8": "int8_t",
|
|
1598
|
+
"i16": "int16_t",
|
|
1599
|
+
"i32": "int32_t",
|
|
1600
|
+
"i64": "int64_t",
|
|
1601
|
+
"i128": "int128_t",
|
|
1602
|
+
"bool": "bool",
|
|
1603
|
+
"Pubkey": "SolPubkey",
|
|
1604
|
+
}.get(inner)
|
|
1605
|
+
if mapped is None:
|
|
1606
|
+
return None
|
|
1607
|
+
return mapped, ""
|
|
1608
|
+
|
|
1609
|
+
def _emit_option_local(self, node: ast.AnnAssign, name: str, ann: ast.Subscript) -> None:
|
|
1610
|
+
parsed = self._option_ann(ann)
|
|
1611
|
+
if parsed is None:
|
|
1612
|
+
self._diag(PS5002, node, "only `Option[scalar]` locals are lowered")
|
|
1613
|
+
return
|
|
1614
|
+
model_c, defined = parsed
|
|
1615
|
+
ctype = "SolPubkey" if model_c == "SolPubkey" else _canon(model_c)
|
|
1616
|
+
tag = f"v_{name}_tag"
|
|
1617
|
+
var = f"v_{name}"
|
|
1618
|
+
self._s(f"uint8_t {tag} = 0u;")
|
|
1619
|
+
if ctype == "SolPubkey":
|
|
1620
|
+
self._s(f"SolPubkey {var} = {{{{0}}}};")
|
|
1621
|
+
elif ctype in ("RtU128", "RtI128") or ctype.startswith("int"):
|
|
1622
|
+
self._s(f"{ctype} {var} = 0;")
|
|
1623
|
+
else:
|
|
1624
|
+
self._s(f"{ctype} {var} = 0u;")
|
|
1625
|
+
binding = _OptVar(tag, var, ctype, defined)
|
|
1626
|
+
self._options[name] = binding
|
|
1627
|
+
if node.value is not None:
|
|
1628
|
+
self._store_option_var(binding, node.value)
|
|
1629
|
+
return
|
|
1630
|
+
self._s(f"(void){tag};")
|
|
1631
|
+
self._s(f"(void){var};")
|
|
1632
|
+
|
|
1633
|
+
def _peek_field(self, node: ast.expr) -> tuple[FieldModel, str] | None:
|
|
1634
|
+
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
|
|
1635
|
+
return None
|
|
1636
|
+
entry = self._acct_vars.get(node.value.id)
|
|
1637
|
+
if entry is None:
|
|
1638
|
+
return None
|
|
1639
|
+
acc_var, cls = entry
|
|
1640
|
+
account = self._accounts.get(cls)
|
|
1641
|
+
if account is None:
|
|
1642
|
+
return None
|
|
1643
|
+
field = account.field(node.attr)
|
|
1644
|
+
if field is None:
|
|
1645
|
+
return None
|
|
1646
|
+
return field, acc_var
|
|
1647
|
+
|
|
1648
|
+
def _peek_option_field(self, node: ast.expr) -> tuple[FieldModel, str] | None:
|
|
1649
|
+
peeked = self._peek_field(node)
|
|
1650
|
+
if peeked is None or peeked[0].shape != "option":
|
|
1651
|
+
return None
|
|
1652
|
+
return peeked
|
|
1653
|
+
|
|
1654
|
+
def _option_meta(self, node: ast.expr) -> tuple[str, str] | None:
|
|
1655
|
+
"""``(inner ctype, enum name)`` when ``node`` is a whole option."""
|
|
1656
|
+
if isinstance(node, ast.Name):
|
|
1657
|
+
bound = self._options.get(node.id)
|
|
1658
|
+
if bound is None:
|
|
1659
|
+
return None
|
|
1660
|
+
return bound.ctype, bound.defined
|
|
1661
|
+
peeked = self._peek_option_field(node)
|
|
1662
|
+
if peeked is None:
|
|
1663
|
+
return None
|
|
1664
|
+
field, _acc = peeked
|
|
1665
|
+
ctype = "SolPubkey" if field.c_type == "SolPubkey" else _canon(field.c_type)
|
|
1666
|
+
return ctype, field.defined
|
|
1667
|
+
|
|
1668
|
+
def _option_tag_expr(self, node: ast.expr) -> str | None:
|
|
1669
|
+
if isinstance(node, ast.Name):
|
|
1670
|
+
bound = self._options.get(node.id)
|
|
1671
|
+
if bound is None:
|
|
1672
|
+
return None
|
|
1673
|
+
return bound.tag
|
|
1674
|
+
peeked = self._peek_option_field(node)
|
|
1675
|
+
if peeked is None:
|
|
1676
|
+
return None
|
|
1677
|
+
field, acc = peeked
|
|
1678
|
+
return f"{acc}->data[{ACCOUNT_DATA_OFFSET + field.offset}u]"
|
|
1679
|
+
|
|
1680
|
+
def _load_option_attr(self, node: ast.Attribute) -> _Frag | None:
|
|
1681
|
+
if node.attr not in ("is_some", "value"):
|
|
1682
|
+
return None
|
|
1683
|
+
tag = self._option_tag_expr(node.value)
|
|
1684
|
+
if tag is None:
|
|
1685
|
+
return None
|
|
1686
|
+
if node.attr == "is_some":
|
|
1687
|
+
return _Frag(f"({tag} == 1u)", "bool")
|
|
1688
|
+
line = getattr(node, "lineno", 1) or 1
|
|
1689
|
+
self._s(f"if ({tag} != 1u) {{")
|
|
1690
|
+
self._s(
|
|
1691
|
+
f'rt_fail_log({RT_ERR_OPTION}u, {self._loc(line)}, "option is none");',
|
|
1692
|
+
1,
|
|
1693
|
+
)
|
|
1694
|
+
self._s(f"return {RT_ERR_OPTION}u;", 1)
|
|
1695
|
+
self._s("}")
|
|
1696
|
+
return self._load_option_inner(node.value)
|
|
1697
|
+
|
|
1698
|
+
def _load_option_inner(self, node: ast.expr) -> _Frag:
|
|
1699
|
+
if isinstance(node, ast.Name):
|
|
1700
|
+
bound = self._options.get(node.id)
|
|
1701
|
+
if bound is None:
|
|
1702
|
+
self._diag(PS5002, node, "read `.is_some` or `.value`")
|
|
1703
|
+
return _Frag("0u", "uint64_t")
|
|
1704
|
+
if bound.ctype == "SolPubkey":
|
|
1705
|
+
return _Frag(f"&{bound.value}", "pub")
|
|
1706
|
+
return _Frag(bound.value, bound.ctype)
|
|
1707
|
+
peeked = self._peek_option_field(node)
|
|
1708
|
+
if peeked is None:
|
|
1709
|
+
self._diag(PS5002, node, "read `.is_some` or `.value`")
|
|
1710
|
+
return _Frag("0u", "uint64_t")
|
|
1711
|
+
field, acc = peeked
|
|
1712
|
+
off = f"{ACCOUNT_DATA_OFFSET + field.offset + 1}u"
|
|
1713
|
+
return self._load_field(field, acc, offset_expr=off)
|
|
1714
|
+
|
|
1715
|
+
def _store_option_var(self, binding: _OptVar, value: ast.expr) -> None:
|
|
1716
|
+
if isinstance(value, ast.IfExp):
|
|
1717
|
+
cond = self._load(value.test)
|
|
1718
|
+
if cond.kind != "bool":
|
|
1719
|
+
self._diag(PS5002, value.test, "conditional test must be bool")
|
|
1720
|
+
self._s(f"if ({cond.text}) {{")
|
|
1721
|
+
self._indent += 1
|
|
1722
|
+
self._store_option_var(binding, value.body)
|
|
1723
|
+
self._indent -= 1
|
|
1724
|
+
self._s("} else {")
|
|
1725
|
+
self._indent += 1
|
|
1726
|
+
self._store_option_var(binding, value.orelse)
|
|
1727
|
+
self._indent -= 1
|
|
1728
|
+
self._s("}")
|
|
1729
|
+
return
|
|
1730
|
+
if _is_none(value):
|
|
1731
|
+
self._clear_opt_var(binding)
|
|
1732
|
+
return
|
|
1733
|
+
if isinstance(value, ast.Name) and value.id in self._options:
|
|
1734
|
+
self._copy_opt_var(binding, self._options[value.id])
|
|
1735
|
+
return
|
|
1736
|
+
peeked = self._peek_option_field(value)
|
|
1737
|
+
if peeked is not None:
|
|
1738
|
+
tag = self._option_tag_expr(value)
|
|
1739
|
+
if tag is None:
|
|
1740
|
+
return
|
|
1741
|
+
inner = self._load_option_inner(value)
|
|
1742
|
+
self._s(f"{binding.tag} = {tag};")
|
|
1743
|
+
self._write_opt_var_value(binding, inner, value)
|
|
1744
|
+
return
|
|
1745
|
+
frag = self._load(value)
|
|
1746
|
+
self._s(f"{binding.tag} = 1u;")
|
|
1747
|
+
self._write_opt_var_value(binding, frag, value)
|
|
1748
|
+
|
|
1749
|
+
def _clear_opt_var(self, binding: _OptVar) -> None:
|
|
1750
|
+
self._s(f"{binding.tag} = 0u;")
|
|
1751
|
+
if binding.ctype == "SolPubkey":
|
|
1752
|
+
self._s(f"{{ SolPubkey rt_z = {{{{0}}}}; rt_key_copy(&{binding.value}, &rt_z); }}")
|
|
1753
|
+
return
|
|
1754
|
+
if binding.ctype in ("RtU128", "RtI128") or binding.ctype.startswith("int"):
|
|
1755
|
+
self._s(f"{binding.value} = 0;")
|
|
1756
|
+
return
|
|
1757
|
+
self._s(f"{binding.value} = 0u;")
|
|
1758
|
+
|
|
1759
|
+
def _copy_opt_var(self, dst: _OptVar, src: _OptVar) -> None:
|
|
1760
|
+
self._s(f"{dst.tag} = {src.tag};")
|
|
1761
|
+
if dst.ctype == "SolPubkey":
|
|
1762
|
+
self._s(f"rt_key_copy(&{dst.value}, &{src.value});")
|
|
1763
|
+
return
|
|
1764
|
+
self._s(f"{dst.value} = {src.value};")
|
|
1765
|
+
|
|
1766
|
+
def _write_opt_var_value(self, binding: _OptVar, frag: _Frag, at: ast.AST) -> None:
|
|
1767
|
+
if binding.ctype == "SolPubkey":
|
|
1768
|
+
if frag.kind != "pub":
|
|
1769
|
+
self._diag(PS5002, at, "cannot store non-Pubkey into an Option[Pubkey]")
|
|
1770
|
+
return
|
|
1771
|
+
self._s(f"rt_key_copy(&{binding.value}, {frag.text});")
|
|
1772
|
+
return
|
|
1773
|
+
if frag.kind == "pub":
|
|
1774
|
+
self._diag(PS5002, at, "cannot store a Pubkey into this option")
|
|
1775
|
+
return
|
|
1776
|
+
text = frag.text
|
|
1777
|
+
if _canon(frag.kind) != _canon(binding.ctype):
|
|
1778
|
+
text = self._coerce_numeric(frag, binding.ctype, at)
|
|
1779
|
+
self._s(f"{binding.value} = ({binding.ctype})({text});")
|
|
1780
|
+
|
|
1781
|
+
def _store_option_field(self, field: FieldModel, acc: str, value: ast.expr) -> None:
|
|
1782
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
1783
|
+
if isinstance(value, ast.IfExp):
|
|
1784
|
+
cond = self._load(value.test)
|
|
1785
|
+
if cond.kind != "bool":
|
|
1786
|
+
self._diag(PS5002, value.test, "conditional test must be bool")
|
|
1787
|
+
self._s(f"if ({cond.text}) {{")
|
|
1788
|
+
self._indent += 1
|
|
1789
|
+
self._store_option_field(field, acc, value.body)
|
|
1790
|
+
self._indent -= 1
|
|
1791
|
+
self._s("} else {")
|
|
1792
|
+
self._indent += 1
|
|
1793
|
+
self._store_option_field(field, acc, value.orelse)
|
|
1794
|
+
self._indent -= 1
|
|
1795
|
+
self._s("}")
|
|
1796
|
+
return
|
|
1797
|
+
if _is_none(value):
|
|
1798
|
+
self._s(f"{acc}->data[{off}u] = 0u;")
|
|
1799
|
+
self._zero_option_field(field, acc, off + 1)
|
|
1800
|
+
return
|
|
1801
|
+
if isinstance(value, ast.Name) and value.id in self._options:
|
|
1802
|
+
src = self._options[value.id]
|
|
1803
|
+
self._s(f"{acc}->data[{off}u] = {src.tag};")
|
|
1804
|
+
self._store_inner_at(field, acc, off + 1, self._frag_opt_var(src), value)
|
|
1805
|
+
return
|
|
1806
|
+
peeked = self._peek_option_field(value)
|
|
1807
|
+
if peeked is not None:
|
|
1808
|
+
tmp = self._mk_tmp()
|
|
1809
|
+
tag = self._option_tag_expr(value)
|
|
1810
|
+
if tag is None:
|
|
1811
|
+
return
|
|
1812
|
+
inner = self._load_option_inner(value)
|
|
1813
|
+
self._s(f"uint8_t {tmp} = {tag};")
|
|
1814
|
+
self._s(f"{acc}->data[{off}u] = {tmp};")
|
|
1815
|
+
self._store_inner_at(field, acc, off + 1, inner, value)
|
|
1816
|
+
return
|
|
1817
|
+
frag = self._load(value)
|
|
1818
|
+
self._s(f"{acc}->data[{off}u] = 1u;")
|
|
1819
|
+
self._store_inner_at(field, acc, off + 1, frag, value)
|
|
1820
|
+
|
|
1821
|
+
def _frag_opt_var(self, binding: _OptVar) -> _Frag:
|
|
1822
|
+
if binding.ctype == "SolPubkey":
|
|
1823
|
+
return _Frag(f"&{binding.value}", "pub")
|
|
1824
|
+
return _Frag(binding.value, binding.ctype)
|
|
1825
|
+
|
|
1826
|
+
def _zero_option_field(self, field: FieldModel, acc: str, data_off: int) -> None:
|
|
1827
|
+
if field.c_type == "SolPubkey":
|
|
1828
|
+
tmp = self._mk_tmp()
|
|
1829
|
+
self._s(f"SolPubkey {tmp} = {{{{0}}}};")
|
|
1830
|
+
self._s(f"rt_st_key({acc}->data, {data_off}u, &{tmp});")
|
|
1831
|
+
return
|
|
1832
|
+
kind = "SolPubkey" if field.c_type == "SolPubkey" else _canon(field.c_type)
|
|
1833
|
+
zero = "0" if kind.startswith("int") or kind in ("RtU128", "RtI128") else "0u"
|
|
1834
|
+
self._store_inner_at(field, acc, data_off, _Frag(zero, kind), None)
|
|
1835
|
+
|
|
1836
|
+
def _store_inner_at(
|
|
1837
|
+
self,
|
|
1838
|
+
field: FieldModel,
|
|
1839
|
+
acc: str,
|
|
1840
|
+
data_off: int,
|
|
1841
|
+
frag: _Frag,
|
|
1842
|
+
at: ast.AST | None,
|
|
1843
|
+
) -> None:
|
|
1844
|
+
off = f"{data_off}u"
|
|
1845
|
+
if field.c_type == "SolPubkey":
|
|
1846
|
+
if frag.kind != "pub":
|
|
1847
|
+
self._diag(PS5002, at, "cannot store non-Pubkey into an Option[Pubkey]")
|
|
1848
|
+
return
|
|
1849
|
+
self._s(f"rt_st_key({acc}->data, {off}, {frag.text});")
|
|
1850
|
+
return
|
|
1851
|
+
if frag.kind == "pub":
|
|
1852
|
+
self._diag(PS5002, at, "cannot store a Pubkey into this option")
|
|
1853
|
+
return
|
|
1854
|
+
self._store_field_value(field, acc, frag, offset_expr=off, at=at)
|
|
1855
|
+
|
|
1856
|
+
# -- fixed-width strings (u32 length + N reserved bytes) ---------------
|
|
1857
|
+
|
|
1858
|
+
def _emit_string_data(self, param: DataParamModel, offset: int) -> None:
|
|
1859
|
+
"""Load a ``u32`` length and ``N`` payload bytes. Length past ``N`` is 1013."""
|
|
1860
|
+
capacity = param.length
|
|
1861
|
+
length_var = f"dp_{param.name}_len"
|
|
1862
|
+
data_var = f"dp_{param.name}"
|
|
1863
|
+
self._s(f"uint32_t {length_var} = 0u;")
|
|
1864
|
+
self._s(f"rt_ld_u32(rt->data, {offset}u, &{length_var});")
|
|
1865
|
+
self._fail_long_string(length_var, capacity, param.line)
|
|
1866
|
+
self._s(f"uint8_t {data_var}[{capacity}];")
|
|
1867
|
+
idx = self._mk_tmp()
|
|
1868
|
+
self._s(f"uint32_t {idx};")
|
|
1869
|
+
self._s(f"for ({idx} = 0u; {idx} < {capacity}u; {idx}++) {{")
|
|
1870
|
+
self._s(f"{data_var}[{idx}] = rt->data[{offset + 4}u + {idx}];", 1)
|
|
1871
|
+
self._s("}")
|
|
1872
|
+
self._strings[param.name] = _StrVar(length_var, data_var, capacity)
|
|
1873
|
+
|
|
1874
|
+
def _fixed_string_ann(self, ann: ast.Subscript) -> int | None:
|
|
1875
|
+
if not isinstance(ann.value, ast.Name) or ann.value.id != "FixedString":
|
|
1876
|
+
return None
|
|
1877
|
+
items = list(ann.slice.elts) if isinstance(ann.slice, ast.Tuple) else [ann.slice]
|
|
1878
|
+
if len(items) != 1:
|
|
1879
|
+
return None
|
|
1880
|
+
count = items[0]
|
|
1881
|
+
if (
|
|
1882
|
+
not isinstance(count, ast.Constant)
|
|
1883
|
+
or isinstance(count.value, bool)
|
|
1884
|
+
or not isinstance(count.value, int)
|
|
1885
|
+
or not 1 <= count.value <= 4096
|
|
1886
|
+
):
|
|
1887
|
+
return None
|
|
1888
|
+
return count.value
|
|
1889
|
+
|
|
1890
|
+
def _emit_string_local(self, node: ast.AnnAssign, name: str, ann: ast.Subscript) -> None:
|
|
1891
|
+
capacity = self._fixed_string_ann(ann)
|
|
1892
|
+
if capacity is None:
|
|
1893
|
+
self._diag(PS5002, node, "only `FixedString[N]` locals are lowered")
|
|
1894
|
+
return
|
|
1895
|
+
length_var = f"v_{name}_len"
|
|
1896
|
+
data_var = f"v_{name}"
|
|
1897
|
+
self._s(f"uint32_t {length_var} = 0u;")
|
|
1898
|
+
self._s(f"uint8_t {data_var}[{capacity}] = {{0}};")
|
|
1899
|
+
binding = _StrVar(length_var, data_var, capacity)
|
|
1900
|
+
self._strings[name] = binding
|
|
1901
|
+
if node.value is not None:
|
|
1902
|
+
self._store_string_var(binding, node.value)
|
|
1903
|
+
return
|
|
1904
|
+
self._s(f"(void){length_var};")
|
|
1905
|
+
self._s(f"(void){data_var};")
|
|
1906
|
+
|
|
1907
|
+
def _store_string_var(self, binding: _StrVar, value: ast.expr) -> None:
|
|
1908
|
+
if isinstance(value, ast.IfExp):
|
|
1909
|
+
cond = self._load(value.test)
|
|
1910
|
+
if cond.kind != "bool":
|
|
1911
|
+
self._diag(PS5002, value.test, "conditional test must be bool")
|
|
1912
|
+
self._s(f"if ({cond.text}) {{")
|
|
1913
|
+
self._indent += 1
|
|
1914
|
+
self._store_string_var(binding, value.body)
|
|
1915
|
+
self._indent -= 1
|
|
1916
|
+
self._s("} else {")
|
|
1917
|
+
self._indent += 1
|
|
1918
|
+
self._store_string_var(binding, value.orelse)
|
|
1919
|
+
self._indent -= 1
|
|
1920
|
+
self._s("}")
|
|
1921
|
+
return
|
|
1922
|
+
raw = _utf8_literal(value)
|
|
1923
|
+
if raw is not None:
|
|
1924
|
+
if len(raw) > binding.capacity:
|
|
1925
|
+
self._diag(PS5002, value, "string literal does not fit this FixedString")
|
|
1926
|
+
return
|
|
1927
|
+
self._s(f"{binding.length_var} = {len(raw)}u;")
|
|
1928
|
+
self._paint_bytes(binding.data_var, binding.capacity, raw)
|
|
1929
|
+
return
|
|
1930
|
+
if isinstance(value, ast.Name) and value.id in self._strings:
|
|
1931
|
+
self._copy_string_var(binding, self._strings[value.id], value)
|
|
1932
|
+
return
|
|
1933
|
+
field_src = self._string_field(value)
|
|
1934
|
+
if field_src is not None:
|
|
1935
|
+
self._copy_field_to_var(binding, field_src[0], field_src[1], value)
|
|
1936
|
+
return
|
|
1937
|
+
self._diag(
|
|
1938
|
+
PS5002,
|
|
1939
|
+
value,
|
|
1940
|
+
"a fixed string stores a literal or another FixedString of the same size",
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
def _store_string_field(self, field: FieldModel, acc: str, value: ast.expr) -> None:
|
|
1944
|
+
if isinstance(value, ast.IfExp):
|
|
1945
|
+
cond = self._load(value.test)
|
|
1946
|
+
if cond.kind != "bool":
|
|
1947
|
+
self._diag(PS5002, value.test, "conditional test must be bool")
|
|
1948
|
+
self._s(f"if ({cond.text}) {{")
|
|
1949
|
+
self._indent += 1
|
|
1950
|
+
self._store_string_field(field, acc, value.body)
|
|
1951
|
+
self._indent -= 1
|
|
1952
|
+
self._s("} else {")
|
|
1953
|
+
self._indent += 1
|
|
1954
|
+
self._store_string_field(field, acc, value.orelse)
|
|
1955
|
+
self._indent -= 1
|
|
1956
|
+
self._s("}")
|
|
1957
|
+
return
|
|
1958
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
1959
|
+
raw = _utf8_literal(value)
|
|
1960
|
+
if raw is not None:
|
|
1961
|
+
if len(raw) > field.length:
|
|
1962
|
+
self._diag(PS5002, value, "string literal does not fit this FixedString")
|
|
1963
|
+
return
|
|
1964
|
+
self._s(f"rt_st_u32({acc}->data, {off}u, {len(raw)}u);")
|
|
1965
|
+
self._paint_bytes(self._payload_ptr(acc, off + 4), field.length, raw)
|
|
1966
|
+
return
|
|
1967
|
+
if isinstance(value, ast.Name) and value.id in self._strings:
|
|
1968
|
+
src = self._strings[value.id]
|
|
1969
|
+
if src.capacity != field.length:
|
|
1970
|
+
self._diag(PS5002, value, "fixed strings have different sizes")
|
|
1971
|
+
return
|
|
1972
|
+
line = getattr(value, "lineno", 1) or 1
|
|
1973
|
+
self._fail_long_string(src.length_var, field.length, line)
|
|
1974
|
+
self._s(f"rt_st_u32({acc}->data, {off}u, {src.length_var});")
|
|
1975
|
+
self._copy_bytes(self._payload_ptr(acc, off + 4), src.data_var, field.length)
|
|
1976
|
+
return
|
|
1977
|
+
field_src = self._string_field(value)
|
|
1978
|
+
if field_src is not None:
|
|
1979
|
+
src_field, src_acc = field_src
|
|
1980
|
+
if src_field.length != field.length:
|
|
1981
|
+
self._diag(PS5002, value, "fixed strings have different sizes")
|
|
1982
|
+
return
|
|
1983
|
+
length = self._read_u32(src_acc, ACCOUNT_DATA_OFFSET + src_field.offset)
|
|
1984
|
+
line = getattr(value, "lineno", 1) or 1
|
|
1985
|
+
self._fail_long_string(length, field.length, line)
|
|
1986
|
+
self._s(f"rt_st_u32({acc}->data, {off}u, {length});")
|
|
1987
|
+
src_ptr = self._payload_ptr(src_acc, ACCOUNT_DATA_OFFSET + src_field.offset + 4)
|
|
1988
|
+
self._copy_bytes(self._payload_ptr(acc, off + 4), src_ptr, field.length)
|
|
1989
|
+
return
|
|
1990
|
+
self._diag(
|
|
1991
|
+
PS5002,
|
|
1992
|
+
value,
|
|
1993
|
+
"a fixed string stores a literal or another FixedString of the same size",
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
def _copy_string_var(self, dst: _StrVar, src: _StrVar, at: ast.expr) -> None:
|
|
1997
|
+
if dst.capacity != src.capacity:
|
|
1998
|
+
self._diag(PS5002, at, "fixed strings have different sizes")
|
|
1999
|
+
return
|
|
2000
|
+
line = getattr(at, "lineno", 1) or 1
|
|
2001
|
+
self._fail_long_string(src.length_var, dst.capacity, line)
|
|
2002
|
+
self._s(f"{dst.length_var} = {src.length_var};")
|
|
2003
|
+
self._copy_bytes(dst.data_var, src.data_var, dst.capacity)
|
|
2004
|
+
|
|
2005
|
+
def _copy_field_to_var(
|
|
2006
|
+
self,
|
|
2007
|
+
dst: _StrVar,
|
|
2008
|
+
field: FieldModel,
|
|
2009
|
+
acc: str,
|
|
2010
|
+
at: ast.expr,
|
|
2011
|
+
) -> None:
|
|
2012
|
+
if field.length != dst.capacity:
|
|
2013
|
+
self._diag(PS5002, at, "fixed strings have different sizes")
|
|
2014
|
+
return
|
|
2015
|
+
length = self._read_u32(acc, ACCOUNT_DATA_OFFSET + field.offset)
|
|
2016
|
+
line = getattr(at, "lineno", 1) or 1
|
|
2017
|
+
self._fail_long_string(length, dst.capacity, line)
|
|
2018
|
+
self._s(f"{dst.length_var} = {length};")
|
|
2019
|
+
src = self._payload_ptr(acc, ACCOUNT_DATA_OFFSET + field.offset + 4)
|
|
2020
|
+
self._copy_bytes(dst.data_var, src, dst.capacity)
|
|
2021
|
+
|
|
2022
|
+
def _string_field(self, node: ast.expr) -> tuple[FieldModel, str] | None:
|
|
2023
|
+
peeked = self._peek_field(node)
|
|
2024
|
+
if peeked is None or peeked[0].shape != "string":
|
|
2025
|
+
return None
|
|
2026
|
+
return peeked
|
|
2027
|
+
|
|
2028
|
+
def _fail_long_string(self, length_expr: str, capacity: int, line: int) -> None:
|
|
2029
|
+
self._s(f"if ((uint32_t)({length_expr}) > {capacity}u) {{")
|
|
2030
|
+
self._s(
|
|
2031
|
+
f"rt_fail_log({RT_ERR_STRING}u, {self._loc(line)}, "
|
|
2032
|
+
'"FixedString length is past its reserved bytes");',
|
|
2033
|
+
1,
|
|
2034
|
+
)
|
|
2035
|
+
self._s(f"return {RT_ERR_STRING}u;", 1)
|
|
2036
|
+
self._s("}")
|
|
2037
|
+
|
|
2038
|
+
def _read_u32(self, acc: str, byte_off: int) -> str:
|
|
2039
|
+
tmp = self._mk_tmp()
|
|
2040
|
+
self._s(f"uint32_t {tmp} = 0u;")
|
|
2041
|
+
self._s(f"rt_ld_u32({acc}->data, {byte_off}u, &{tmp});")
|
|
2042
|
+
return tmp
|
|
2043
|
+
|
|
2044
|
+
def _payload_ptr(self, acc: str, byte_off: int) -> str:
|
|
2045
|
+
ptr = self._mk_tmp()
|
|
2046
|
+
self._s(f"uint8_t *{ptr} = {acc}->data + {byte_off}u;")
|
|
2047
|
+
return ptr
|
|
2048
|
+
|
|
2049
|
+
def _paint_bytes(self, dest: str, capacity: int, raw: bytes) -> None:
|
|
2050
|
+
"""Write ``raw`` then zero the rest of the reserved payload. No globals."""
|
|
2051
|
+
if capacity <= 0:
|
|
2052
|
+
return
|
|
2053
|
+
if not raw:
|
|
2054
|
+
self._zero_bytes(dest, capacity)
|
|
2055
|
+
return
|
|
2056
|
+
src = self._mk_tmp()
|
|
2057
|
+
parts = ", ".join(f"{byte}u" for byte in raw)
|
|
2058
|
+
self._s(f"uint8_t {src}[{len(raw)}] = {{{parts}}};")
|
|
2059
|
+
idx = self._mk_tmp()
|
|
2060
|
+
self._s(f"uint32_t {idx};")
|
|
2061
|
+
self._s(f"for ({idx} = 0u; {idx} < {capacity}u; {idx}++) {{")
|
|
2062
|
+
self._s(f"{dest}[{idx}] = ({idx} < {len(raw)}u ? {src}[{idx}] : 0u);", 1)
|
|
2063
|
+
self._s("}")
|
|
2064
|
+
|
|
2065
|
+
def _copy_bytes(self, dest: str, src: str, count: int) -> None:
|
|
2066
|
+
if count <= 0:
|
|
2067
|
+
return
|
|
2068
|
+
idx = self._mk_tmp()
|
|
2069
|
+
self._s(f"uint32_t {idx};")
|
|
2070
|
+
self._s(f"for ({idx} = 0u; {idx} < {count}u; {idx}++) {{")
|
|
2071
|
+
self._s(f"{dest}[{idx}] = {src}[{idx}];", 1)
|
|
2072
|
+
self._s("}")
|
|
2073
|
+
|
|
2074
|
+
def _zero_bytes(self, dest: str, count: int) -> None:
|
|
2075
|
+
if count <= 0:
|
|
2076
|
+
return
|
|
2077
|
+
idx = self._mk_tmp()
|
|
2078
|
+
self._s(f"uint32_t {idx};")
|
|
2079
|
+
self._s(f"for ({idx} = 0u; {idx} < {count}u; {idx}++) {{")
|
|
2080
|
+
self._s(f"{dest}[{idx}] = 0u;", 1)
|
|
2081
|
+
self._s("}")
|
|
2082
|
+
|
|
2083
|
+
# -- annotated locals --------------------------------------------------
|
|
2084
|
+
|
|
2085
|
+
def _emit_ann_assign(self, node: ast.AnnAssign) -> None:
|
|
2086
|
+
if not isinstance(node.target, ast.Name):
|
|
2087
|
+
self._diag(PS5001, node, "annotated assigns may only target simple names")
|
|
2088
|
+
return
|
|
2089
|
+
name = node.target.id
|
|
2090
|
+
if name in self._data_vars or name in self._acct_vars:
|
|
2091
|
+
self._diag(PS5002, node, f"`{name}` is already a parameter name")
|
|
2092
|
+
return
|
|
2093
|
+
bound = self._options.get(name)
|
|
2094
|
+
if bound is not None and bound.tag.startswith("dp_"):
|
|
2095
|
+
self._diag(PS5002, node, f"`{name}` is already a parameter name")
|
|
2096
|
+
return
|
|
2097
|
+
if name in self._strings and self._strings[name].length_var.startswith("dp_"):
|
|
2098
|
+
self._diag(PS5002, node, f"`{name}` is already a parameter name")
|
|
2099
|
+
return
|
|
2100
|
+
if (
|
|
2101
|
+
name in self._locals
|
|
2102
|
+
or name in self._arrays
|
|
2103
|
+
or name in self._options
|
|
2104
|
+
or name in self._strings
|
|
2105
|
+
):
|
|
2106
|
+
self._diag(PS5002, node, f"`{name}` is already declared")
|
|
2107
|
+
return
|
|
2108
|
+
ann = node.annotation
|
|
2109
|
+
if isinstance(ann, ast.Subscript):
|
|
2110
|
+
if isinstance(ann.value, ast.Name) and ann.value.id == "FixedString":
|
|
2111
|
+
self._emit_string_local(node, name, ann)
|
|
2112
|
+
return
|
|
2113
|
+
if isinstance(ann.value, ast.Name) and ann.value.id == "Option":
|
|
2114
|
+
self._emit_option_local(node, name, ann)
|
|
2115
|
+
return
|
|
2116
|
+
self._emit_array_local(node, name, ann)
|
|
2117
|
+
return
|
|
2118
|
+
if not isinstance(ann, ast.Name):
|
|
2119
|
+
self._diag(PS5002, node, "unsupported local type annotation form")
|
|
2120
|
+
return
|
|
2121
|
+
c_ctype = _LOCAL_TYPES_C.get(ann.id)
|
|
2122
|
+
enum_name = ann.id if ann.id in self._enums else ""
|
|
2123
|
+
if c_ctype is None and enum_name:
|
|
2124
|
+
c_ctype = "uint8_t"
|
|
2125
|
+
if c_ctype is None:
|
|
2126
|
+
self._diag(
|
|
2127
|
+
PS5002,
|
|
2128
|
+
node,
|
|
2129
|
+
f"local type `{ann.id}` is not lowered "
|
|
2130
|
+
"(supported: integer widths, bool, Pubkey, FixedArray[scalar, N], "
|
|
2131
|
+
"unit enums, Option[scalar])",
|
|
2132
|
+
)
|
|
2133
|
+
return
|
|
2134
|
+
var = f"v_{name}"
|
|
2135
|
+
if c_ctype == "SolPubkey":
|
|
2136
|
+
self._s(f"SolPubkey {var};")
|
|
2137
|
+
elif c_ctype in ("RtU128", "RtI128") or c_ctype.startswith("int"):
|
|
2138
|
+
self._s(f"{c_ctype} {var} = 0;")
|
|
2139
|
+
else:
|
|
2140
|
+
self._s(f"{c_ctype} {var} = 0u;")
|
|
2141
|
+
self._locals[name] = (var, c_ctype)
|
|
2142
|
+
if ann.id == "bool":
|
|
2143
|
+
self._bools.add(name)
|
|
2144
|
+
if enum_name:
|
|
2145
|
+
self._value_enum[name] = enum_name
|
|
2146
|
+
if node.value is not None:
|
|
2147
|
+
self._store_local(name, node.value)
|
|
2148
|
+
else:
|
|
2149
|
+
self._s(f"(void){var};")
|
|
2150
|
+
|
|
2151
|
+
def _emit_array_local(self, node: ast.AnnAssign, name: str, ann: ast.Subscript) -> None:
|
|
2152
|
+
parsed = self._fixed_array_ann(ann)
|
|
2153
|
+
if parsed is None:
|
|
2154
|
+
self._diag(PS5002, node, "only `FixedArray[scalar, N]` locals are lowered")
|
|
2155
|
+
return
|
|
2156
|
+
ctype, length = parsed
|
|
2157
|
+
var = f"v_{name}"
|
|
2158
|
+
self._s(f"{ctype} {var}[{length}] = {{0}};")
|
|
2159
|
+
self._arrays[name] = (var, ctype, length)
|
|
2160
|
+
items = list(ann.slice.elts) if isinstance(ann.slice, ast.Tuple) else [ann.slice]
|
|
2161
|
+
if items and isinstance(items[0], ast.Name) and items[0].id in self._enums:
|
|
2162
|
+
self._array_enum[name] = items[0].id
|
|
2163
|
+
if items and isinstance(items[0], ast.Name) and items[0].id == "bool":
|
|
2164
|
+
self._bool_arrays.add(name)
|
|
2165
|
+
if node.value is not None:
|
|
2166
|
+
self._emit_array_init(var, ctype, length, node.value)
|
|
2167
|
+
|
|
2168
|
+
def _emit_array_init(
|
|
2169
|
+
self, var: str, ctype: str, length: int, value: ast.expr
|
|
2170
|
+
) -> None:
|
|
2171
|
+
"""Fill a FixedArray local from a list comprehension."""
|
|
2172
|
+
if not isinstance(value, ast.ListComp):
|
|
2173
|
+
self._diag(PS5002, value, "assign array elements with `name[i] = ...`")
|
|
2174
|
+
return
|
|
2175
|
+
gen = value.generators[0] if len(value.generators) == 1 else None
|
|
2176
|
+
if gen is None or not isinstance(gen.target, ast.Name):
|
|
2177
|
+
self._diag(PS5002, value, "unsupported list comprehension")
|
|
2178
|
+
return
|
|
2179
|
+
name = gen.target.id
|
|
2180
|
+
ivar = f"v_{name}"
|
|
2181
|
+
self._locals[name] = (ivar, "uint64_t")
|
|
2182
|
+
self._loop_no += 1
|
|
2183
|
+
tag = self._loop_no
|
|
2184
|
+
self._s("{")
|
|
2185
|
+
self._indent += 1
|
|
2186
|
+
self._s(f"uint64_t {ivar} = 0u;")
|
|
2187
|
+
self._s(f"rt_h{tag}:")
|
|
2188
|
+
self._s(f"if (!({ivar} < {length}u)) goto rt_d{tag};")
|
|
2189
|
+
self._store_array_element(var, ctype, ivar, value.elt)
|
|
2190
|
+
self._s(f"{ivar} = {ivar} + 1u;")
|
|
2191
|
+
self._s(f"goto rt_h{tag};")
|
|
2192
|
+
self._s(f"rt_d{tag}: ;")
|
|
2193
|
+
self._indent -= 1
|
|
2194
|
+
self._s("}")
|
|
2195
|
+
self._locals.pop(name, None)
|
|
2196
|
+
|
|
2197
|
+
def _store_array_element(
|
|
2198
|
+
self, var: str, ctype: str, index: str, value: ast.expr
|
|
2199
|
+
) -> None:
|
|
2200
|
+
frag = self._load(value)
|
|
2201
|
+
if ctype == "SolPubkey":
|
|
2202
|
+
if frag.kind != "pub":
|
|
2203
|
+
self._diag(PS5002, value, "cannot store a non-Pubkey into a Pubkey array")
|
|
2204
|
+
return
|
|
2205
|
+
self._s(f"rt_key_copy(&{var}[{index}], {frag.text});")
|
|
2206
|
+
return
|
|
2207
|
+
text = frag.text
|
|
2208
|
+
if _canon(frag.kind) != _canon(ctype):
|
|
2209
|
+
text = self._coerce_numeric(frag, ctype, value)
|
|
2210
|
+
self._s(f"{var}[{index}] = ({ctype})({text});")
|
|
2211
|
+
|
|
2212
|
+
def _fixed_array_ann(self, ann: ast.Subscript) -> tuple[str, int] | None:
|
|
2213
|
+
if not isinstance(ann.value, ast.Name) or ann.value.id != "FixedArray":
|
|
2214
|
+
return None
|
|
2215
|
+
items = list(ann.slice.elts) if isinstance(ann.slice, ast.Tuple) else [ann.slice]
|
|
2216
|
+
if len(items) != 2 or not isinstance(items[0], ast.Name):
|
|
2217
|
+
return None
|
|
2218
|
+
if items[0].id in self._enums:
|
|
2219
|
+
ctype: str | None = "uint8_t"
|
|
2220
|
+
else:
|
|
2221
|
+
ctype = _LOCAL_TYPES_C.get(items[0].id)
|
|
2222
|
+
count = items[1]
|
|
2223
|
+
if (
|
|
2224
|
+
ctype is None
|
|
2225
|
+
or not isinstance(count, ast.Constant)
|
|
2226
|
+
or isinstance(count.value, bool)
|
|
2227
|
+
or not isinstance(count.value, int)
|
|
2228
|
+
or not 1 <= count.value <= 4096
|
|
2229
|
+
):
|
|
2230
|
+
return None
|
|
2231
|
+
return ctype, count.value
|
|
2232
|
+
|
|
2233
|
+
def _store_local(self, name: str, value: ast.expr) -> None:
|
|
2234
|
+
info = self._locals.get(name)
|
|
2235
|
+
if info is None:
|
|
2236
|
+
self._diag(PS5001, None, f"`{name}` is not declared")
|
|
2237
|
+
return
|
|
2238
|
+
var, c_ctype = info
|
|
2239
|
+
frag = self._load(value)
|
|
2240
|
+
if c_ctype == "SolPubkey":
|
|
2241
|
+
if frag.kind != "pub":
|
|
2242
|
+
self._diag(PS5002, value, "cannot store non-Pubkey into a Pubkey local")
|
|
2243
|
+
return
|
|
2244
|
+
self._s(f"rt_key_copy(&{var}, {frag.text});")
|
|
2245
|
+
return
|
|
2246
|
+
if frag.kind == "pub":
|
|
2247
|
+
self._diag(PS5002, value, "cannot store a Pubkey into a numeric local")
|
|
2248
|
+
return
|
|
2249
|
+
text = frag.text
|
|
2250
|
+
if _canon(frag.kind) != _canon(c_ctype):
|
|
2251
|
+
text = self._coerce_numeric(frag, c_ctype, value)
|
|
2252
|
+
self._s(f"{var} = ({c_ctype})({text});")
|
|
2253
|
+
|
|
2254
|
+
# -- plain assigns -------------------------------------------------------
|
|
2255
|
+
|
|
2256
|
+
def _emit_assign(self, node: ast.Assign) -> None:
|
|
2257
|
+
for target in node.targets:
|
|
2258
|
+
self._emit_assign_target(target, node.value)
|
|
2259
|
+
|
|
2260
|
+
def _emit_assign_target(self, target: ast.expr, value: ast.expr) -> None:
|
|
2261
|
+
if isinstance(target, ast.Name):
|
|
2262
|
+
if target.id in self._options:
|
|
2263
|
+
self._store_option_var(self._options[target.id], value)
|
|
2264
|
+
return
|
|
2265
|
+
if target.id in self._strings:
|
|
2266
|
+
self._store_string_var(self._strings[target.id], value)
|
|
2267
|
+
return
|
|
2268
|
+
if target.id not in self._locals:
|
|
2269
|
+
self._diag(
|
|
2270
|
+
PS5001,
|
|
2271
|
+
target,
|
|
2272
|
+
f"`{target.id}` is not declared; annotate it once (`x: u64 = ...`)",
|
|
2273
|
+
)
|
|
2274
|
+
return
|
|
2275
|
+
self._store_local(target.id, value)
|
|
2276
|
+
return
|
|
2277
|
+
if isinstance(target, ast.Subscript):
|
|
2278
|
+
self._emit_subscript_store(target, value)
|
|
2279
|
+
return
|
|
2280
|
+
self._emit_field_store(target, value)
|
|
2281
|
+
|
|
2282
|
+
def _emit_field_store(self, target: ast.expr, value: ast.expr) -> None:
|
|
2283
|
+
if not isinstance(target, ast.Attribute):
|
|
2284
|
+
self._diag(PS5001, target, "unsupported assignment target")
|
|
2285
|
+
return
|
|
2286
|
+
if target.attr == "value" and self._store_enum_value(target, value):
|
|
2287
|
+
return
|
|
2288
|
+
resolved = self._resolve_field(target)
|
|
2289
|
+
if resolved is None:
|
|
2290
|
+
return
|
|
2291
|
+
fmodel, acc_var = resolved
|
|
2292
|
+
if fmodel.shape == "array":
|
|
2293
|
+
self._diag(PS5001, target, f"index `{fmodel.name}[i]` to write one element")
|
|
2294
|
+
return
|
|
2295
|
+
if fmodel.shape == "option":
|
|
2296
|
+
self._store_option_field(fmodel, acc_var, value)
|
|
2297
|
+
return
|
|
2298
|
+
if fmodel.shape == "string":
|
|
2299
|
+
self._store_string_field(fmodel, acc_var, value)
|
|
2300
|
+
return
|
|
2301
|
+
if fmodel.shape == "struct":
|
|
2302
|
+
self._copy_struct(fmodel, acc_var, value)
|
|
2303
|
+
return
|
|
2304
|
+
frag = self._load(value)
|
|
2305
|
+
byte_offset = ACCOUNT_DATA_OFFSET + fmodel.offset
|
|
2306
|
+
if fmodel.c_type == "SolPubkey":
|
|
2307
|
+
if frag.kind != "pub":
|
|
2308
|
+
self._diag(PS5002, value, "cannot store non-Pubkey into a Pubkey field")
|
|
2309
|
+
return
|
|
2310
|
+
self._line(f"rt_st_key({acc_var}->data, {byte_offset}u, {frag.text});", 1)
|
|
2311
|
+
return
|
|
2312
|
+
if frag.kind == "pub":
|
|
2313
|
+
self._diag(PS5002, value, "cannot store a Pubkey into a numeric field")
|
|
2314
|
+
return
|
|
2315
|
+
self._store_field_value(fmodel, acc_var, frag)
|
|
2316
|
+
|
|
2317
|
+
def _store_field_value(
|
|
2318
|
+
self,
|
|
2319
|
+
fmodel: FieldModel,
|
|
2320
|
+
acc_var: str,
|
|
2321
|
+
frag: _Frag,
|
|
2322
|
+
offset_expr: str | None = None,
|
|
2323
|
+
at: ast.AST | None = None,
|
|
2324
|
+
) -> None:
|
|
2325
|
+
"""Store one numeric field value (casting signeds to their raw word)."""
|
|
2326
|
+
off = offset_expr if offset_expr is not None else f"{ACCOUNT_DATA_OFFSET + fmodel.offset}u"
|
|
2327
|
+
if fmodel.c_type == "SolPubkey":
|
|
2328
|
+
if frag.kind != "pub":
|
|
2329
|
+
self._diag(PS5002, at, "cannot store a non-Pubkey into a Pubkey field")
|
|
2330
|
+
return
|
|
2331
|
+
self._s(f"rt_st_key({acc_var}->data, {off}, {frag.text});")
|
|
2332
|
+
return
|
|
2333
|
+
if frag.kind != "pub" and _canon(frag.kind) != _canon(fmodel.c_type):
|
|
2334
|
+
text = self._coerce_numeric(frag, fmodel.c_type, at)
|
|
2335
|
+
frag = _Frag(text, _canon(fmodel.c_type))
|
|
2336
|
+
size = _WIDTH_BYTES[fmodel.c_type]
|
|
2337
|
+
if size == 1:
|
|
2338
|
+
# One byte: store the raw bits directly (works for i8/u8/bool).
|
|
2339
|
+
self._s(f"{acc_var}->data[{off}] = (uint8_t)({frag.text});")
|
|
2340
|
+
self._zero_enum_payload(fmodel, acc_var, off)
|
|
2341
|
+
return
|
|
2342
|
+
value = frag.text
|
|
2343
|
+
if fmodel.c_type in _UNSIGNED_OF:
|
|
2344
|
+
value = f"({_UNSIGNED_OF[fmodel.c_type]})({value})"
|
|
2345
|
+
self._s(f"{_ST_HELPER[size]}({acc_var}->data, {off}, {value});")
|
|
2346
|
+
|
|
2347
|
+
# -- checked arithmetic --------------------------------------------------
|
|
2348
|
+
|
|
2349
|
+
def _emit_aug_assign(self, node: ast.AugAssign) -> None:
|
|
2350
|
+
opname = _ARITH_OP_C.get(type(node.op))
|
|
2351
|
+
if opname is None:
|
|
2352
|
+
self._diag(
|
|
2353
|
+
PS5001,
|
|
2354
|
+
node,
|
|
2355
|
+
f"operator `{type(node.op).__name__}` is not a supported "
|
|
2356
|
+
"accumulator (+= -= *= //= %= <<= >>=)",
|
|
2357
|
+
)
|
|
2358
|
+
return
|
|
2359
|
+
if isinstance(node.target, ast.Subscript):
|
|
2360
|
+
self._diag(
|
|
2361
|
+
PS5001,
|
|
2362
|
+
node,
|
|
2363
|
+
"index updates use `a[i] = a[i] + x` (checked, same width)",
|
|
2364
|
+
)
|
|
2365
|
+
return
|
|
2366
|
+
if isinstance(node.target, ast.Attribute):
|
|
2367
|
+
resolved = self._resolve_field(node.target)
|
|
2368
|
+
if resolved is None:
|
|
2369
|
+
return
|
|
2370
|
+
fmodel, acc_var = resolved
|
|
2371
|
+
if fmodel.shape == "array":
|
|
2372
|
+
self._diag(PS5001, node.target, f"index `{fmodel.name}[i]` before updating it")
|
|
2373
|
+
return
|
|
2374
|
+
if fmodel.shape == "option":
|
|
2375
|
+
self._diag(PS5002, node, "an option is not a number; assign `None` or a value")
|
|
2376
|
+
return
|
|
2377
|
+
if fmodel.shape == "string":
|
|
2378
|
+
self._diag(PS5002, node, "a fixed string is not a number")
|
|
2379
|
+
return
|
|
2380
|
+
self._field_aug(opname, fmodel, acc_var, node, node.value)
|
|
2381
|
+
return
|
|
2382
|
+
if isinstance(node.target, ast.Name):
|
|
2383
|
+
if node.target.id in self._options:
|
|
2384
|
+
self._diag(PS5002, node, "an option is not a number; assign `None` or a value")
|
|
2385
|
+
return
|
|
2386
|
+
if node.target.id in self._strings:
|
|
2387
|
+
self._diag(PS5002, node, "a fixed string is not a number")
|
|
2388
|
+
return
|
|
2389
|
+
info = self._locals.get(node.target.id)
|
|
2390
|
+
if info is None:
|
|
2391
|
+
self._diag(PS5001, node, f"`{node.target.id}` is not declared")
|
|
2392
|
+
return
|
|
2393
|
+
var, c_ctype = info
|
|
2394
|
+
self._local_aug(opname, var, c_ctype, node, node.value)
|
|
2395
|
+
return
|
|
2396
|
+
self._diag(PS5001, node, "unsupported AugAssign target")
|
|
2397
|
+
|
|
2398
|
+
def _field_aug(
|
|
2399
|
+
self,
|
|
2400
|
+
opname: str,
|
|
2401
|
+
fmodel: FieldModel,
|
|
2402
|
+
acc_var: str,
|
|
2403
|
+
node: ast.AST,
|
|
2404
|
+
rhs: ast.expr,
|
|
2405
|
+
) -> None:
|
|
2406
|
+
family = _FAMILY_OF.get(fmodel.c_type)
|
|
2407
|
+
if family is None:
|
|
2408
|
+
self._diag(
|
|
2409
|
+
PS5002,
|
|
2410
|
+
node,
|
|
2411
|
+
f"cannot `{_PY_OP[opname]}`-assign a `{fmodel.c_type}` field",
|
|
2412
|
+
)
|
|
2413
|
+
return
|
|
2414
|
+
left = self._load_field(fmodel, acc_var)
|
|
2415
|
+
right = self._load(rhs)
|
|
2416
|
+
if right.kind == "pub":
|
|
2417
|
+
self._diag(PS5002, rhs, "cannot mix Pubkey with numbers")
|
|
2418
|
+
return
|
|
2419
|
+
dest = _WIDTH_C[fmodel.c_type]
|
|
2420
|
+
right_text = right.text
|
|
2421
|
+
if _canon(right.kind) != _canon(dest):
|
|
2422
|
+
right_text = self._coerce_numeric(right, dest, rhs)
|
|
2423
|
+
accum = self._mk_tmp()
|
|
2424
|
+
self._s(f"{dest} {accum};")
|
|
2425
|
+
self._checked_call(opname, left.text, right_text, accum, family, node)
|
|
2426
|
+
self._store_field_value(fmodel, acc_var, _Frag(accum, dest))
|
|
2427
|
+
|
|
2428
|
+
def _local_aug(
|
|
2429
|
+
self,
|
|
2430
|
+
opname: str,
|
|
2431
|
+
var: str,
|
|
2432
|
+
c_ctype: str,
|
|
2433
|
+
node: ast.AST,
|
|
2434
|
+
rhs: ast.expr,
|
|
2435
|
+
) -> None:
|
|
2436
|
+
if c_ctype == "SolPubkey":
|
|
2437
|
+
self._diag(PS5001, node, "cannot update a Pubkey with arithmetic")
|
|
2438
|
+
return
|
|
2439
|
+
family = _FAMILY_OF.get(c_ctype)
|
|
2440
|
+
if family is None:
|
|
2441
|
+
self._diag(
|
|
2442
|
+
PS5002,
|
|
2443
|
+
node,
|
|
2444
|
+
f"cannot `{_PY_OP[opname]}`-assign a `{c_ctype}` local",
|
|
2445
|
+
)
|
|
2446
|
+
return
|
|
2447
|
+
right = self._load(rhs)
|
|
2448
|
+
if right.kind == "pub":
|
|
2449
|
+
self._diag(PS5002, rhs, "cannot mix Pubkey with numbers")
|
|
2450
|
+
return
|
|
2451
|
+
right_text = right.text
|
|
2452
|
+
if _canon(right.kind) != _canon(c_ctype):
|
|
2453
|
+
right_text = self._coerce_numeric(right, c_ctype, rhs)
|
|
2454
|
+
accum = self._mk_tmp()
|
|
2455
|
+
self._s(f"{c_ctype} {accum};")
|
|
2456
|
+
self._checked_call(opname, var, right_text, accum, family, node)
|
|
2457
|
+
self._s(f"{var} = ({c_ctype})({accum});")
|
|
2458
|
+
|
|
2459
|
+
def _checked_call(
|
|
2460
|
+
self,
|
|
2461
|
+
opname: str,
|
|
2462
|
+
left: str,
|
|
2463
|
+
right: str,
|
|
2464
|
+
accum: str,
|
|
2465
|
+
family: str,
|
|
2466
|
+
node: ast.AST,
|
|
2467
|
+
) -> None:
|
|
2468
|
+
"""Emit one rt_checked_<op>_<family> call with the real error code.
|
|
2469
|
+
|
|
2470
|
+
The runtime helpers return ``RT_OK`` or the actual RT_ERR_* code, so
|
|
2471
|
+
div-zero aborts log 1010 and arithmetic overflow logs 1009 — the
|
|
2472
|
+
distinction carried by ``refrt`` holds in the generated C.
|
|
2473
|
+
"""
|
|
2474
|
+
rc = self._mk_tmp()
|
|
2475
|
+
what = f"checked {opname} ({family})"
|
|
2476
|
+
self._s(f"uint64_t {rc} = rt_checked_{opname}_{family}({left}, {right}, &{accum});")
|
|
2477
|
+
self._s(f"if ({rc} != RT_OK) {{")
|
|
2478
|
+
self._s(
|
|
2479
|
+
f'rt_fail_log({rc}, {self._loc(getattr(node, "lineno", 1) or 1)}, "{what}");',
|
|
2480
|
+
1,
|
|
2481
|
+
)
|
|
2482
|
+
self._s(f"return {rc};", 1)
|
|
2483
|
+
self._s("}")
|
|
2484
|
+
|
|
2485
|
+
# -- field addressing ----------------------------------------------------
|
|
2486
|
+
|
|
2487
|
+
def _copy_struct(self, dest: FieldModel, dest_acc: str, value: ast.expr) -> None:
|
|
2488
|
+
"""Copy one fixed struct onto another. Both sides are account fields."""
|
|
2489
|
+
if not isinstance(value, ast.Attribute):
|
|
2490
|
+
self._diag(PS5002, value, "copy a struct from another account field")
|
|
2491
|
+
return
|
|
2492
|
+
found = self._resolve_field(value)
|
|
2493
|
+
if found is None:
|
|
2494
|
+
return
|
|
2495
|
+
src, src_acc = found
|
|
2496
|
+
if src.shape != "struct" or src.defined != dest.defined or src.size != dest.size:
|
|
2497
|
+
self._diag(PS5002, value, f"cannot copy this value onto `{dest.defined}`")
|
|
2498
|
+
return
|
|
2499
|
+
self._copy_span(
|
|
2500
|
+
dest_acc,
|
|
2501
|
+
ACCOUNT_DATA_OFFSET + dest.offset,
|
|
2502
|
+
src_acc,
|
|
2503
|
+
ACCOUNT_DATA_OFFSET + src.offset,
|
|
2504
|
+
dest.size,
|
|
2505
|
+
)
|
|
2506
|
+
|
|
2507
|
+
def _copy_span(
|
|
2508
|
+
self,
|
|
2509
|
+
dest_acc: str,
|
|
2510
|
+
dest_off: int,
|
|
2511
|
+
src_acc: str,
|
|
2512
|
+
src_off: int,
|
|
2513
|
+
count: int,
|
|
2514
|
+
) -> None:
|
|
2515
|
+
if count <= 0:
|
|
2516
|
+
return
|
|
2517
|
+
idx = self._mk_tmp()
|
|
2518
|
+
self._s(f"uint32_t {idx};")
|
|
2519
|
+
self._s(f"for ({idx} = 0u; {idx} < {count}u; {idx}++) {{")
|
|
2520
|
+
self._s(
|
|
2521
|
+
f"{dest_acc}->data[{dest_off}u + {idx}] = {src_acc}->data[{src_off}u + {idx}];",
|
|
2522
|
+
1,
|
|
2523
|
+
)
|
|
2524
|
+
self._s("}")
|
|
2525
|
+
|
|
2526
|
+
def _lookup_field(self, node: ast.expr) -> tuple[FieldModel, str] | None:
|
|
2527
|
+
"""Quiet lookup of ``account.a.b``. The leaf offset is within the account payload."""
|
|
2528
|
+
names: list[str] = []
|
|
2529
|
+
current: ast.expr = node
|
|
2530
|
+
while isinstance(current, ast.Attribute):
|
|
2531
|
+
names.append(current.attr)
|
|
2532
|
+
current = current.value
|
|
2533
|
+
if not isinstance(current, ast.Name) or not names:
|
|
2534
|
+
return None
|
|
2535
|
+
names.reverse()
|
|
2536
|
+
entry = self._acct_vars.get(current.id)
|
|
2537
|
+
if entry is None:
|
|
2538
|
+
return None
|
|
2539
|
+
acc_var, cls = entry
|
|
2540
|
+
account = self._accounts.get(cls)
|
|
2541
|
+
if account is None:
|
|
2542
|
+
return None
|
|
2543
|
+
field = account.field(names[0])
|
|
2544
|
+
if field is None:
|
|
2545
|
+
return None
|
|
2546
|
+
base = field.offset
|
|
2547
|
+
leaf = field
|
|
2548
|
+
for attr in names[1:]:
|
|
2549
|
+
if leaf.shape != "struct":
|
|
2550
|
+
return None
|
|
2551
|
+
struct = self._structs.get(leaf.defined)
|
|
2552
|
+
if struct is None:
|
|
2553
|
+
return None
|
|
2554
|
+
inner = struct.field(attr)
|
|
2555
|
+
if inner is None:
|
|
2556
|
+
return None
|
|
2557
|
+
base += inner.offset
|
|
2558
|
+
leaf = inner
|
|
2559
|
+
if base != leaf.offset:
|
|
2560
|
+
leaf = replace(leaf, offset=base)
|
|
2561
|
+
return leaf, acc_var
|
|
2562
|
+
|
|
2563
|
+
def _resolve_field(self, node: ast.expr) -> tuple[FieldModel, str] | None:
|
|
2564
|
+
"""Lookup `account.field` or `account.struct.leaf` -> (FieldModel, acc C var)."""
|
|
2565
|
+
if not isinstance(node, ast.Attribute):
|
|
2566
|
+
self._diag(PS5001, node, "only `account.field` accesses are lowered")
|
|
2567
|
+
return None
|
|
2568
|
+
if not isinstance(node.value, ast.Name):
|
|
2569
|
+
found = self._lookup_field(node)
|
|
2570
|
+
if found is None:
|
|
2571
|
+
self._diag(PS5002, node, "no such nested field")
|
|
2572
|
+
return None
|
|
2573
|
+
return found
|
|
2574
|
+
entry = self._acct_vars.get(node.value.id)
|
|
2575
|
+
if entry is None:
|
|
2576
|
+
self._diag(
|
|
2577
|
+
PS5002,
|
|
2578
|
+
node,
|
|
2579
|
+
f"`{node.value.id}` is not an account parameter",
|
|
2580
|
+
)
|
|
2581
|
+
return None
|
|
2582
|
+
acc_var, cls = entry
|
|
2583
|
+
model = self._accounts.get(cls)
|
|
2584
|
+
if model is None:
|
|
2585
|
+
self._diag(
|
|
2586
|
+
PS5002,
|
|
2587
|
+
node,
|
|
2588
|
+
f"`{node.value.id}` is a Signer; only `.key` is available",
|
|
2589
|
+
)
|
|
2590
|
+
return None
|
|
2591
|
+
for field in model.fields:
|
|
2592
|
+
if field.name == node.attr:
|
|
2593
|
+
return field, acc_var
|
|
2594
|
+
self._diag(
|
|
2595
|
+
PS5001,
|
|
2596
|
+
node,
|
|
2597
|
+
f"account `{model.name}` has no field `{node.attr}`",
|
|
2598
|
+
)
|
|
2599
|
+
return None
|
|
2600
|
+
|
|
2601
|
+
def _load_field(
|
|
2602
|
+
self,
|
|
2603
|
+
fmodel: FieldModel,
|
|
2604
|
+
acc_var: str | None,
|
|
2605
|
+
offset_expr: str | None = None,
|
|
2606
|
+
) -> _Frag:
|
|
2607
|
+
"""Emit a load of an account field (or one array element) into a temp."""
|
|
2608
|
+
off = offset_expr if offset_expr is not None else f"{ACCOUNT_DATA_OFFSET + fmodel.offset}u"
|
|
2609
|
+
data = f"{acc_var}->data" if acc_var is not None else "acc_unknown->data"
|
|
2610
|
+
var = self._mk_tmp()
|
|
2611
|
+
if fmodel.c_type == "SolPubkey":
|
|
2612
|
+
return _Frag(f"(const SolPubkey *)(const void *)({data} + {off})", "pub")
|
|
2613
|
+
if fmodel.c_type in ("uint8_t", "bool"):
|
|
2614
|
+
self._s(f"uint8_t {var} = {data}[{off}];")
|
|
2615
|
+
return _Frag(var, "bool" if fmodel.c_type == "bool" else "uint8_t")
|
|
2616
|
+
if fmodel.c_type == "int128_t":
|
|
2617
|
+
bits = self._mk_tmp()
|
|
2618
|
+
self._s(f"RtU128 {bits} = 0;")
|
|
2619
|
+
self._s(f"rt_ld_u128({data}, {off}, &{bits});")
|
|
2620
|
+
self._s(f"RtI128 {var} = (RtI128){bits};")
|
|
2621
|
+
return _Frag(var, "RtI128")
|
|
2622
|
+
if fmodel.c_type in ("int8_t", "int16_t", "int32_t", "int64_t"):
|
|
2623
|
+
size = _WIDTH_BYTES[fmodel.c_type]
|
|
2624
|
+
bits = self._mk_tmp()
|
|
2625
|
+
if size == 8:
|
|
2626
|
+
self._s(f"uint64_t {bits} = 0u;")
|
|
2627
|
+
self._s(f"rt_ld_u64({data}, {off}, &{bits});")
|
|
2628
|
+
elif size == 1:
|
|
2629
|
+
self._s(f"uint8_t {bits} = {data}[{off}];")
|
|
2630
|
+
else:
|
|
2631
|
+
self._s(f"uint{size * 8}_t {bits} = 0u;")
|
|
2632
|
+
self._s(f"{_LD_HELPER[size]}({data}, {off}, &{bits});")
|
|
2633
|
+
ctype = _WIDTH_C[fmodel.c_type]
|
|
2634
|
+
self._s(f"{ctype} {var} = ({ctype})(({fmodel.c_type}){bits});")
|
|
2635
|
+
return _Frag(var, ctype)
|
|
2636
|
+
size = _WIDTH_BYTES[fmodel.c_type]
|
|
2637
|
+
if size == 8:
|
|
2638
|
+
self._s(f"uint64_t {var} = 0u;")
|
|
2639
|
+
self._s(f"rt_ld_u64({data}, {off}, &{var});")
|
|
2640
|
+
else:
|
|
2641
|
+
ctype = _WIDTH_C[fmodel.c_type]
|
|
2642
|
+
self._s(f"{ctype} {var} = 0;")
|
|
2643
|
+
self._s(f"{_LD_HELPER[size]}({data}, {off}, &{var});")
|
|
2644
|
+
return _Frag(var, _WIDTH_C[fmodel.c_type])
|
|
2645
|
+
|
|
2646
|
+
# -- expression lowering ---------------------------------------------------
|
|
2647
|
+
|
|
2648
|
+
def _load(self, node: ast.expr) -> _Frag:
|
|
2649
|
+
"""Emit any prelude loads, then return the reactive fragment."""
|
|
2650
|
+
if isinstance(node, ast.Constant):
|
|
2651
|
+
value = node.value
|
|
2652
|
+
if isinstance(value, bool):
|
|
2653
|
+
return _Frag("1u" if value else "0u", "bool")
|
|
2654
|
+
if isinstance(value, int):
|
|
2655
|
+
return self._int_literal(value, node)
|
|
2656
|
+
self._diag(PS5002, node, f"literal {value!r} is not lowered")
|
|
2657
|
+
return _Frag("0u", "uint64_t")
|
|
2658
|
+
if isinstance(node, ast.Name):
|
|
2659
|
+
if node.id in self._arrays:
|
|
2660
|
+
self._diag(PS5002, node, f"index `{node.id}[i]` before using it")
|
|
2661
|
+
return _Frag("0u", "uint64_t")
|
|
2662
|
+
if node.id in self._options:
|
|
2663
|
+
self._diag(
|
|
2664
|
+
PS5002,
|
|
2665
|
+
node,
|
|
2666
|
+
"read `.is_some` or `.value`, or assign the whole option",
|
|
2667
|
+
)
|
|
2668
|
+
return _Frag("0u", "uint64_t")
|
|
2669
|
+
if node.id in self._strings:
|
|
2670
|
+
self._diag(PS5002, node, "index the string or call `len`")
|
|
2671
|
+
return _Frag("0u", "uint64_t")
|
|
2672
|
+
info = self._locals.get(node.id)
|
|
2673
|
+
if info is None:
|
|
2674
|
+
info = self._data_vars.get(node.id)
|
|
2675
|
+
if info is None:
|
|
2676
|
+
self._diag(PS5002, node, f"`{node.id}` is not a lowered name")
|
|
2677
|
+
return _Frag("0u", "uint64_t")
|
|
2678
|
+
var, c_ctype = info
|
|
2679
|
+
if c_ctype == "SolPubkey":
|
|
2680
|
+
return _Frag(f"&{var}", "pub")
|
|
2681
|
+
if node.id in self._bools:
|
|
2682
|
+
return _Frag(var, "bool")
|
|
2683
|
+
return _Frag(var, c_ctype)
|
|
2684
|
+
if isinstance(node, ast.Attribute):
|
|
2685
|
+
return self._load_attr(node)
|
|
2686
|
+
if isinstance(node, ast.Subscript):
|
|
2687
|
+
return self._load_subscript(node)
|
|
2688
|
+
if isinstance(node, ast.Compare):
|
|
2689
|
+
return self._load_compare(node)
|
|
2690
|
+
if isinstance(node, ast.BoolOp):
|
|
2691
|
+
return self._load_boolop(node)
|
|
2692
|
+
if isinstance(node, ast.IfExp):
|
|
2693
|
+
return self._load_ifexp(node)
|
|
2694
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
2695
|
+
operand = self._load(node.operand)
|
|
2696
|
+
return _Frag(f"!({operand.text})", "bool")
|
|
2697
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
|
2698
|
+
return self._load_negate(node)
|
|
2699
|
+
if isinstance(node, ast.BinOp):
|
|
2700
|
+
return self._load_binop(node)
|
|
2701
|
+
if isinstance(node, ast.Call):
|
|
2702
|
+
return self._load_call(node)
|
|
2703
|
+
self._diag(PS5002, node, f"expression `{type(node).__name__}` is not lowered")
|
|
2704
|
+
return _Frag("0u", "uint64_t")
|
|
2705
|
+
|
|
2706
|
+
def _int_literal(self, value: int, node: ast.AST) -> _Frag:
|
|
2707
|
+
if not -(2**127) <= value <= 2**128 - 1:
|
|
2708
|
+
self._diag(PS5002, node, "integer literal outside i128/u128 range")
|
|
2709
|
+
return _Frag("0u", "uint64_t")
|
|
2710
|
+
if value < 0:
|
|
2711
|
+
if value >= -(2**63):
|
|
2712
|
+
return _Frag(_c_int_lit(value), "int64_t", literal=value)
|
|
2713
|
+
return _Frag(_typed_lit(value, "RtI128"), "RtI128", literal=value)
|
|
2714
|
+
if value <= 2**64 - 1:
|
|
2715
|
+
return _Frag(_c_int_lit(value), "uint64_t", literal=value)
|
|
2716
|
+
return _Frag(_typed_lit(value, "RtU128"), "RtU128", literal=value)
|
|
2717
|
+
|
|
2718
|
+
def _load_attr(self, node: ast.Attribute) -> _Frag:
|
|
2719
|
+
if node.attr == "value":
|
|
2720
|
+
loaded = self._load_enum_value(node)
|
|
2721
|
+
if loaded is not None:
|
|
2722
|
+
return loaded
|
|
2723
|
+
projected = self._load_option_attr(node)
|
|
2724
|
+
if projected is not None:
|
|
2725
|
+
return projected
|
|
2726
|
+
if isinstance(node.value, ast.Name):
|
|
2727
|
+
base = node.value.id
|
|
2728
|
+
enum = self._enums.get(base)
|
|
2729
|
+
if enum is not None:
|
|
2730
|
+
for variant, tag in enum.variants:
|
|
2731
|
+
if variant == node.attr:
|
|
2732
|
+
return _Frag(f"{tag}u", "uint8_t", literal=tag)
|
|
2733
|
+
self._diag(PS5002, node, f"`{base}` has no variant `{node.attr}`")
|
|
2734
|
+
return _Frag("0u", "uint8_t")
|
|
2735
|
+
if base in self._acct_vars:
|
|
2736
|
+
if node.attr == "key":
|
|
2737
|
+
return _Frag(f"{self._acct_vars[base][0]}->key", "pub")
|
|
2738
|
+
fmodel = self._resolve_field(node)
|
|
2739
|
+
if fmodel is None:
|
|
2740
|
+
return _Frag("0u", "uint64_t")
|
|
2741
|
+
return self._load_resolved(node, fmodel)
|
|
2742
|
+
if base in self._data_vars:
|
|
2743
|
+
var, c_ctype = self._data_vars[base]
|
|
2744
|
+
if node.attr == "key" and c_ctype == "SolPubkey":
|
|
2745
|
+
return _Frag(f"&{var}", "pub")
|
|
2746
|
+
if isinstance(node.value, ast.Attribute):
|
|
2747
|
+
resolved = self._resolve_field(node)
|
|
2748
|
+
if resolved is None:
|
|
2749
|
+
return _Frag("0u", "uint64_t")
|
|
2750
|
+
return self._load_resolved(node, resolved)
|
|
2751
|
+
self._diag(PS5002, node, "attribute is not lowered in phase 3")
|
|
2752
|
+
return _Frag("0u", "uint64_t")
|
|
2753
|
+
|
|
2754
|
+
def _load_resolved(
|
|
2755
|
+
self,
|
|
2756
|
+
node: ast.Attribute,
|
|
2757
|
+
resolved: tuple[FieldModel, str],
|
|
2758
|
+
) -> _Frag:
|
|
2759
|
+
field, acc_var = resolved
|
|
2760
|
+
if field.shape == "struct":
|
|
2761
|
+
self._diag(PS5002, node, "read a field of the struct, or copy the whole struct")
|
|
2762
|
+
return _Frag("0u", "uint64_t")
|
|
2763
|
+
if field.shape in ("array", "vec"):
|
|
2764
|
+
self._diag(PS5002, node, f"index `{field.name}[i]` before using it")
|
|
2765
|
+
return _Frag("0u", "uint64_t")
|
|
2766
|
+
if field.shape == "option":
|
|
2767
|
+
self._diag(
|
|
2768
|
+
PS5002,
|
|
2769
|
+
node,
|
|
2770
|
+
"read `.is_some` or `.value`, or assign the whole option",
|
|
2771
|
+
)
|
|
2772
|
+
return _Frag("0u", "uint64_t")
|
|
2773
|
+
if field.shape == "string":
|
|
2774
|
+
self._diag(PS5002, node, "index the string or call `len`")
|
|
2775
|
+
return _Frag("0u", "uint64_t")
|
|
2776
|
+
return self._load_field(field, acc_var)
|
|
2777
|
+
|
|
2778
|
+
def _load_compare(self, node: ast.Compare) -> _Frag:
|
|
2779
|
+
left = self._load(node.left)
|
|
2780
|
+
parts: list[str] = []
|
|
2781
|
+
for op, comparator in zip(node.ops, node.comparators, strict=False):
|
|
2782
|
+
right = self._load(comparator)
|
|
2783
|
+
symbol = _COMPARE_C.get(type(op))
|
|
2784
|
+
if symbol is None:
|
|
2785
|
+
self._diag(PS5002, node, f"comparison `{type(op).__name__}` unsupported")
|
|
2786
|
+
return _Frag("0u", "uint64_t")
|
|
2787
|
+
left, right = self._align_pair(left, right, comparator)
|
|
2788
|
+
parts.append(self._compare_text(left, right, symbol, node))
|
|
2789
|
+
left = right
|
|
2790
|
+
if len(parts) == 1:
|
|
2791
|
+
return _Frag(parts[0], "bool")
|
|
2792
|
+
return _Frag("(" + " && ".join(parts) + ")", "bool")
|
|
2793
|
+
|
|
2794
|
+
def _compare_text(self, left: _Frag, right: _Frag, symbol: str, node: ast.AST) -> str:
|
|
2795
|
+
if left.kind == "pub" and right.kind == "pub":
|
|
2796
|
+
if symbol == "==":
|
|
2797
|
+
return f"rt_key_eq({left.text}, {right.text})"
|
|
2798
|
+
if symbol == "!=":
|
|
2799
|
+
return f"(!rt_key_eq({left.text}, {right.text}))"
|
|
2800
|
+
self._diag(PS5002, node, "pubkeys support only == and !=")
|
|
2801
|
+
return "0u"
|
|
2802
|
+
if left.kind == "pub" or right.kind == "pub":
|
|
2803
|
+
self._diag(PS5002, node, "cannot mix Pubkey with numbers in a comparison")
|
|
2804
|
+
return "0u"
|
|
2805
|
+
return f"({left.text} {symbol} {right.text})"
|
|
2806
|
+
|
|
2807
|
+
def _load_boolop(self, node: ast.BoolOp) -> _Frag:
|
|
2808
|
+
joiner = " && " if isinstance(node.op, ast.And) else " || "
|
|
2809
|
+
parts: list[str] = []
|
|
2810
|
+
for operand in node.values:
|
|
2811
|
+
frag = self._load(operand)
|
|
2812
|
+
if frag.kind != "bool":
|
|
2813
|
+
self._diag(PS5002, operand, "boolean operators need bool operands")
|
|
2814
|
+
continue
|
|
2815
|
+
parts.append(frag.text)
|
|
2816
|
+
if not parts:
|
|
2817
|
+
return _Frag("0u", "bool")
|
|
2818
|
+
return _Frag("(" + joiner.join(parts) + ")", "bool")
|
|
2819
|
+
|
|
2820
|
+
def _coerce_numeric(self, frag: _Frag, dest: str, node: ast.AST | None) -> str:
|
|
2821
|
+
"""Expression of C type ``dest`` (checked when the value might not fit)."""
|
|
2822
|
+
dest_c = _canon(dest)
|
|
2823
|
+
src = _canon(frag.kind)
|
|
2824
|
+
if src not in _KIND_META or dest_c not in _KIND_META:
|
|
2825
|
+
self._diag(PS5002, node, "expected an integer")
|
|
2826
|
+
return "0"
|
|
2827
|
+
if frag.literal is not None:
|
|
2828
|
+
if not _fits_c(frag.literal, dest_c):
|
|
2829
|
+
self._diag(PS5002, node, f"literal {frag.literal} does not fit in `{dest_c}`")
|
|
2830
|
+
return "0"
|
|
2831
|
+
if src == dest_c:
|
|
2832
|
+
return frag.text
|
|
2833
|
+
return _typed_lit(frag.literal, dest_c)
|
|
2834
|
+
if src == dest_c:
|
|
2835
|
+
return frag.text
|
|
2836
|
+
if _widen_safe(src, dest_c):
|
|
2837
|
+
return f"(({dest_c})({frag.text}))"
|
|
2838
|
+
tmp = self._mk_tmp()
|
|
2839
|
+
self._emit_fit_check(frag.text, src, dest_c, node)
|
|
2840
|
+
self._s(f"{dest_c} {tmp} = ({dest_c})({frag.text});")
|
|
2841
|
+
return tmp
|
|
2842
|
+
|
|
2843
|
+
def _emit_fit_check(self, text: str, src: str, dest: str, node: ast.AST | None) -> None:
|
|
2844
|
+
"""Abort with 1009 when ``text`` (type ``src``) does not fit in ``dest``."""
|
|
2845
|
+
sb, ss = _KIND_META[src]
|
|
2846
|
+
db, ds = _KIND_META[dest]
|
|
2847
|
+
line = getattr(node, "lineno", 1) or 1
|
|
2848
|
+
loc = self._loc(line)
|
|
2849
|
+
if ds and not ss:
|
|
2850
|
+
hi = (1 << (db - 1)) - 1
|
|
2851
|
+
cond = f"({text}) > {_bound_lit(hi)}"
|
|
2852
|
+
elif ss and not ds:
|
|
2853
|
+
if db >= sb:
|
|
2854
|
+
cond = f"({text}) < 0"
|
|
2855
|
+
else:
|
|
2856
|
+
hi = (1 << db) - 1
|
|
2857
|
+
cond = f"({text}) < 0 || ({text}) > {_bound_lit(hi)}"
|
|
2858
|
+
elif ds and ss:
|
|
2859
|
+
lo = -(1 << (db - 1))
|
|
2860
|
+
hi = (1 << (db - 1)) - 1
|
|
2861
|
+
cond = f"({text}) < {_bound_lit(lo)} || ({text}) > {_bound_lit(hi)}"
|
|
2862
|
+
else:
|
|
2863
|
+
hi = (1 << db) - 1
|
|
2864
|
+
cond = f"({text}) > {_bound_lit(hi)}"
|
|
2865
|
+
self._s(f"if ({cond}) {{")
|
|
2866
|
+
self._s(f'rt_fail_log({RT_ERR_OVERFLOW}u, {loc}, "cast out of range");', 1)
|
|
2867
|
+
self._s(f"return {RT_ERR_OVERFLOW}u;", 1)
|
|
2868
|
+
self._s("}")
|
|
2869
|
+
|
|
2870
|
+
def _align_pair(self, left: _Frag, right: _Frag, node: ast.AST) -> tuple[_Frag, _Frag]:
|
|
2871
|
+
if left.kind in ("pub", "bool") or right.kind in ("pub", "bool"):
|
|
2872
|
+
return left, right
|
|
2873
|
+
if _canon(left.kind) not in _KIND_META or _canon(right.kind) not in _KIND_META:
|
|
2874
|
+
return left, right
|
|
2875
|
+
if right.literal is not None and _fits_c(right.literal, left.kind):
|
|
2876
|
+
kind = _canon(left.kind)
|
|
2877
|
+
text = right.text if _canon(right.kind) == kind else _typed_lit(right.literal, kind)
|
|
2878
|
+
return left, _Frag(text, kind, literal=right.literal)
|
|
2879
|
+
if left.literal is not None and _fits_c(left.literal, right.kind):
|
|
2880
|
+
kind = _canon(right.kind)
|
|
2881
|
+
text = left.text if _canon(left.kind) == kind else _typed_lit(left.literal, kind)
|
|
2882
|
+
return _Frag(text, kind, literal=left.literal), right
|
|
2883
|
+
if _canon(left.kind) == _canon(right.kind):
|
|
2884
|
+
return left, right
|
|
2885
|
+
dest = _wider_kind(left.kind, right.kind)
|
|
2886
|
+
return (
|
|
2887
|
+
_Frag(self._coerce_numeric(left, dest, node), dest),
|
|
2888
|
+
_Frag(self._coerce_numeric(right, dest, node), dest),
|
|
2889
|
+
)
|
|
2890
|
+
|
|
2891
|
+
def _binop_dest(self, left: _Frag, right: _Frag) -> str:
|
|
2892
|
+
if (
|
|
2893
|
+
left.literal is not None
|
|
2894
|
+
and right.literal is None
|
|
2895
|
+
and _canon(right.kind) in _KIND_META
|
|
2896
|
+
and _fits_c(left.literal, right.kind)
|
|
2897
|
+
):
|
|
2898
|
+
return _canon(right.kind)
|
|
2899
|
+
if (
|
|
2900
|
+
right.literal is not None
|
|
2901
|
+
and left.literal is None
|
|
2902
|
+
and _canon(left.kind) in _KIND_META
|
|
2903
|
+
and _fits_c(right.literal, left.kind)
|
|
2904
|
+
):
|
|
2905
|
+
return _canon(left.kind)
|
|
2906
|
+
if _canon(left.kind) in _KIND_META and _canon(right.kind) in _KIND_META:
|
|
2907
|
+
return _wider_kind(left.kind, right.kind)
|
|
2908
|
+
return "uint64_t"
|
|
2909
|
+
|
|
2910
|
+
def _load_binop(self, node: ast.BinOp) -> _Frag:
|
|
2911
|
+
if type(node.op) in _BIT_OP_C:
|
|
2912
|
+
return self._load_bitop(node)
|
|
2913
|
+
opname = _ARITH_OP_C.get(type(node.op))
|
|
2914
|
+
if opname is None:
|
|
2915
|
+
self._diag(PS5002, node, f"operator `{type(node.op).__name__}` is not lowered")
|
|
2916
|
+
return _Frag("0u", "uint64_t")
|
|
2917
|
+
left = self._load(node.left)
|
|
2918
|
+
right = self._load(node.right)
|
|
2919
|
+
if left.kind in ("pub", "bool") or right.kind in ("pub", "bool"):
|
|
2920
|
+
self._diag(PS5002, node, "arithmetic needs integer operands")
|
|
2921
|
+
return _Frag("0u", "uint64_t")
|
|
2922
|
+
dest = self._binop_dest(left, right)
|
|
2923
|
+
left_text = self._coerce_numeric(left, dest, node.left)
|
|
2924
|
+
right_text = self._coerce_numeric(right, dest, node.right)
|
|
2925
|
+
accum = self._mk_tmp()
|
|
2926
|
+
self._s(f"{dest} {accum};")
|
|
2927
|
+
self._checked_call(opname, left_text, right_text, accum, _FAMILY_OF[dest], node)
|
|
2928
|
+
return _Frag(accum, dest)
|
|
2929
|
+
|
|
2930
|
+
def _load_bitop(self, node: ast.BinOp) -> _Frag:
|
|
2931
|
+
left = self._load(node.left)
|
|
2932
|
+
right = self._load(node.right)
|
|
2933
|
+
if left.kind in ("pub", "bool") or right.kind in ("pub", "bool"):
|
|
2934
|
+
self._diag(PS5002, node, "bitwise operators need integer operands")
|
|
2935
|
+
return _Frag("0u", "uint64_t")
|
|
2936
|
+
if _canon(left.kind) not in _KIND_META or _canon(right.kind) not in _KIND_META:
|
|
2937
|
+
self._diag(PS5002, node, "bitwise operators need integer operands")
|
|
2938
|
+
return _Frag("0u", "uint64_t")
|
|
2939
|
+
dest = self._binop_dest(left, right)
|
|
2940
|
+
left_text = self._coerce_numeric(left, dest, node.left)
|
|
2941
|
+
right_text = self._coerce_numeric(right, dest, node.right)
|
|
2942
|
+
symbol = _BIT_OP_C[type(node.op)]
|
|
2943
|
+
return _Frag(f"(({dest})(({left_text}) {symbol} ({right_text})))", dest)
|
|
2944
|
+
|
|
2945
|
+
def _load_negate(self, node: ast.UnaryOp) -> _Frag:
|
|
2946
|
+
operand_node = node.operand
|
|
2947
|
+
if (
|
|
2948
|
+
isinstance(operand_node, ast.Constant)
|
|
2949
|
+
and isinstance(operand_node.value, int)
|
|
2950
|
+
and not isinstance(operand_node.value, bool)
|
|
2951
|
+
):
|
|
2952
|
+
return self._int_literal(-operand_node.value, node)
|
|
2953
|
+
operand = self._load(operand_node)
|
|
2954
|
+
dest = _canon(operand.kind)
|
|
2955
|
+
if dest not in _KIND_META:
|
|
2956
|
+
self._diag(PS5002, node, "unary `-` needs an integer operand")
|
|
2957
|
+
return _Frag("0", "int64_t")
|
|
2958
|
+
accum = self._mk_tmp()
|
|
2959
|
+
self._s(f"{dest} {accum};")
|
|
2960
|
+
self._checked_call("sub", _typed_lit(0, dest), operand.text, accum, _FAMILY_OF[dest], node)
|
|
2961
|
+
return _Frag(accum, dest)
|
|
2962
|
+
|
|
2963
|
+
def _load_call(self, node: ast.Call) -> _Frag:
|
|
2964
|
+
func = node.func
|
|
2965
|
+
if not isinstance(func, ast.Name):
|
|
2966
|
+
self._diag(PS5002, node, "unsupported call")
|
|
2967
|
+
return _Frag("0u", "uint64_t")
|
|
2968
|
+
if func.id == "len":
|
|
2969
|
+
return self._load_len(node)
|
|
2970
|
+
if func.id in _CAST_NAMES:
|
|
2971
|
+
if len(node.args) != 1 or node.keywords:
|
|
2972
|
+
self._diag(PS5002, node, f"`{func.id}(...)` takes one integer")
|
|
2973
|
+
return _Frag("0u", "uint64_t")
|
|
2974
|
+
dest = _LOCAL_TYPES_C[func.id]
|
|
2975
|
+
frag = self._load(node.args[0])
|
|
2976
|
+
return _Frag(self._coerce_numeric(frag, dest, node.args[0]), dest)
|
|
2977
|
+
helper = self._helpers.get(func.id)
|
|
2978
|
+
if helper is not None:
|
|
2979
|
+
return self._inline_helper(node, helper, as_value=True)
|
|
2980
|
+
self._diag(PS5002, node, f"call `{func.id}` is not an expression")
|
|
2981
|
+
return _Frag("0u", "uint64_t")
|
|
2982
|
+
|
|
2983
|
+
def _load_len(self, node: ast.Call) -> _Frag:
|
|
2984
|
+
if len(node.args) != 1:
|
|
2985
|
+
self._diag(PS5002, node, "len() takes one argument")
|
|
2986
|
+
return _Frag("0u", "uint64_t")
|
|
2987
|
+
arg = node.args[0]
|
|
2988
|
+
if isinstance(arg, ast.Name) and arg.id in self._arrays:
|
|
2989
|
+
length = self._arrays[arg.id][2]
|
|
2990
|
+
return _Frag(f"{length}u", "uint64_t", literal=length)
|
|
2991
|
+
if isinstance(arg, ast.Name) and arg.id in self._strings:
|
|
2992
|
+
return _Frag(f"(uint64_t){self._strings[arg.id].length_var}", "uint64_t")
|
|
2993
|
+
if isinstance(arg, ast.Attribute):
|
|
2994
|
+
resolved = self._resolve_field(arg)
|
|
2995
|
+
if resolved is not None and resolved[0].shape == "array":
|
|
2996
|
+
length = resolved[0].length
|
|
2997
|
+
return _Frag(f"{length}u", "uint64_t", literal=length)
|
|
2998
|
+
if resolved is not None and resolved[0].shape == "vec":
|
|
2999
|
+
field, acc = resolved
|
|
3000
|
+
tmp = self._mk_tmp()
|
|
3001
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
3002
|
+
self._s(f"uint32_t {tmp} = 0u;")
|
|
3003
|
+
self._s(f"rt_ld_u32({acc}->data, {off}u, &{tmp});")
|
|
3004
|
+
return _Frag(f"(uint64_t){tmp}", "uint64_t")
|
|
3005
|
+
if resolved is not None and resolved[0].shape == "string":
|
|
3006
|
+
field, acc = resolved
|
|
3007
|
+
tmp = self._mk_tmp()
|
|
3008
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
3009
|
+
self._s(f"uint32_t {tmp} = 0u;")
|
|
3010
|
+
self._s(f"rt_ld_u32({acc}->data, {off}u, &{tmp});")
|
|
3011
|
+
return _Frag(f"(uint64_t){tmp}", "uint64_t")
|
|
3012
|
+
self._diag(PS5002, arg, "len() needs a FixedArray or FixedString")
|
|
3013
|
+
return _Frag("0u", "uint64_t")
|
|
3014
|
+
|
|
3015
|
+
def _load_subscript(self, node: ast.Subscript) -> _Frag:
|
|
3016
|
+
base = node.value
|
|
3017
|
+
if isinstance(base, ast.Name) and base.id in self._strings:
|
|
3018
|
+
binding = self._strings[base.id]
|
|
3019
|
+
idx = self._index_text(node.slice, binding.capacity)
|
|
3020
|
+
tmp = self._mk_tmp()
|
|
3021
|
+
self._s(f"uint8_t {tmp} = {binding.data_var}[{idx}];")
|
|
3022
|
+
return _Frag(tmp, "uint8_t")
|
|
3023
|
+
if isinstance(base, ast.Name) and base.id in self._arrays:
|
|
3024
|
+
var, ctype, length = self._arrays[base.id]
|
|
3025
|
+
idx = self._index_text(node.slice, length)
|
|
3026
|
+
if ctype == "SolPubkey":
|
|
3027
|
+
return _Frag(f"&{var}[{idx}]", "pub")
|
|
3028
|
+
tmp = self._mk_tmp()
|
|
3029
|
+
self._s(f"{ctype} {tmp} = {var}[{idx}];")
|
|
3030
|
+
kind = "bool" if base.id in self._bool_arrays else ctype
|
|
3031
|
+
return _Frag(tmp, kind)
|
|
3032
|
+
if isinstance(base, ast.Attribute):
|
|
3033
|
+
resolved = self._resolve_field(base)
|
|
3034
|
+
if resolved is None:
|
|
3035
|
+
return _Frag("0u", "uint64_t")
|
|
3036
|
+
field, acc = resolved
|
|
3037
|
+
if field.shape == "string":
|
|
3038
|
+
idx = self._index_text(node.slice, field.length)
|
|
3039
|
+
return self._load_field(field, acc, offset_expr=self._string_at(field, idx))
|
|
3040
|
+
if field.shape not in ("array", "vec"):
|
|
3041
|
+
self._diag(PS5002, node, f"`{field.name}` is not an array")
|
|
3042
|
+
return _Frag("0u", "uint64_t")
|
|
3043
|
+
idx = self._index_text(node.slice, field.length)
|
|
3044
|
+
return self._load_field(field, acc, offset_expr=self._elem_offset(field, idx))
|
|
3045
|
+
self._diag(PS5002, node, "only `name[i]` and `account.field[i]` can be indexed")
|
|
3046
|
+
return _Frag("0u", "uint64_t")
|
|
3047
|
+
|
|
3048
|
+
def _emit_subscript_store(self, target: ast.Subscript, value: ast.expr) -> None:
|
|
3049
|
+
base = target.value
|
|
3050
|
+
if isinstance(base, ast.Name) and base.id in self._strings:
|
|
3051
|
+
binding = self._strings[base.id]
|
|
3052
|
+
idx = self._index_text(target.slice, binding.capacity)
|
|
3053
|
+
text = self._u8_text(value)
|
|
3054
|
+
if text is None:
|
|
3055
|
+
return
|
|
3056
|
+
self._s(f"{binding.data_var}[{idx}] = (uint8_t)({text});")
|
|
3057
|
+
return
|
|
3058
|
+
if isinstance(base, ast.Name) and base.id in self._arrays:
|
|
3059
|
+
var, ctype, length = self._arrays[base.id]
|
|
3060
|
+
idx = self._index_text(target.slice, length)
|
|
3061
|
+
frag = self._load(value)
|
|
3062
|
+
if ctype == "SolPubkey":
|
|
3063
|
+
if frag.kind != "pub":
|
|
3064
|
+
self._diag(PS5002, value, "cannot store a non-Pubkey into a Pubkey array")
|
|
3065
|
+
return
|
|
3066
|
+
self._s(f"rt_key_copy(&{var}[{idx}], {frag.text});")
|
|
3067
|
+
return
|
|
3068
|
+
text = frag.text
|
|
3069
|
+
if _canon(frag.kind) != _canon(ctype):
|
|
3070
|
+
text = self._coerce_numeric(frag, ctype, value)
|
|
3071
|
+
self._s(f"{var}[{idx}] = ({ctype})({text});")
|
|
3072
|
+
return
|
|
3073
|
+
if isinstance(base, ast.Attribute):
|
|
3074
|
+
resolved = self._resolve_field(base)
|
|
3075
|
+
if resolved is None:
|
|
3076
|
+
return
|
|
3077
|
+
field, acc = resolved
|
|
3078
|
+
if field.shape == "string":
|
|
3079
|
+
idx = self._index_text(target.slice, field.length)
|
|
3080
|
+
text = self._u8_text(value)
|
|
3081
|
+
if text is None:
|
|
3082
|
+
return
|
|
3083
|
+
off = self._string_at(field, idx)
|
|
3084
|
+
self._s(f"{acc}->data[{off}] = (uint8_t)({text});")
|
|
3085
|
+
return
|
|
3086
|
+
if field.shape not in ("array", "vec"):
|
|
3087
|
+
self._diag(PS5001, target, f"`{field.name}` is not an array")
|
|
3088
|
+
return
|
|
3089
|
+
idx = self._index_text(target.slice, field.length)
|
|
3090
|
+
frag = self._load(value)
|
|
3091
|
+
self._store_field_value(
|
|
3092
|
+
field,
|
|
3093
|
+
acc,
|
|
3094
|
+
frag,
|
|
3095
|
+
offset_expr=self._elem_offset(field, idx),
|
|
3096
|
+
at=value,
|
|
3097
|
+
)
|
|
3098
|
+
if field.shape == "vec":
|
|
3099
|
+
self._note_vec_len(field, acc, idx)
|
|
3100
|
+
return
|
|
3101
|
+
self._diag(PS5001, target, "only `name[i]` and `account.field[i]` can be assigned")
|
|
3102
|
+
|
|
3103
|
+
def _u8_text(self, value: ast.expr) -> str | None:
|
|
3104
|
+
frag = self._load(value)
|
|
3105
|
+
if frag.kind == "pub" or _canon(frag.kind) not in _KIND_META:
|
|
3106
|
+
self._diag(PS5002, value, "a string byte is a u8")
|
|
3107
|
+
return None
|
|
3108
|
+
if _canon(frag.kind) == "uint8_t":
|
|
3109
|
+
return frag.text
|
|
3110
|
+
return self._coerce_numeric(frag, "uint8_t", value)
|
|
3111
|
+
|
|
3112
|
+
def _string_at(self, field: FieldModel, index_text: str) -> str:
|
|
3113
|
+
"""Byte offset of one reserved payload byte (after the ``u32`` length)."""
|
|
3114
|
+
base = ACCOUNT_DATA_OFFSET + field.offset + 4
|
|
3115
|
+
tmp = self._mk_tmp()
|
|
3116
|
+
self._s(f"uint64_t {tmp} = {base}u + ({index_text});")
|
|
3117
|
+
return tmp
|
|
3118
|
+
|
|
3119
|
+
def _index_text(self, index: ast.expr, length: int) -> str:
|
|
3120
|
+
frag = self._load(index)
|
|
3121
|
+
text = self._coerce_numeric(frag, "uint64_t", index)
|
|
3122
|
+
tmp = self._mk_tmp()
|
|
3123
|
+
self._s(f"uint64_t {tmp} = {text};")
|
|
3124
|
+
line = getattr(index, "lineno", 1) or 1
|
|
3125
|
+
self._s(f"if ({tmp} >= {length}u) {{")
|
|
3126
|
+
self._s(
|
|
3127
|
+
f'rt_fail_log({RT_ERR_OOB}u, {self._loc(line)}, "index out of bounds");',
|
|
3128
|
+
1,
|
|
3129
|
+
)
|
|
3130
|
+
self._s(f"return {RT_ERR_OOB}u;", 1)
|
|
3131
|
+
self._s("}")
|
|
3132
|
+
return tmp
|
|
3133
|
+
|
|
3134
|
+
def _elem_offset(self, field: FieldModel, index_text: str) -> str:
|
|
3135
|
+
base = ACCOUNT_DATA_OFFSET + field.offset
|
|
3136
|
+
if field.shape == "vec":
|
|
3137
|
+
base += 4
|
|
3138
|
+
tmp = self._mk_tmp()
|
|
3139
|
+
elem = field.elem_size or _WIDTH_BYTES[field.c_type]
|
|
3140
|
+
self._s(f"uint64_t {tmp} = {base}u + ({index_text}) * {elem}u;")
|
|
3141
|
+
return tmp
|
|
3142
|
+
|
|
3143
|
+
def _note_vec_len(self, field: FieldModel, acc: str, index_text: str) -> None:
|
|
3144
|
+
"""A store at ``index`` grows the stored length when it writes past the end."""
|
|
3145
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
3146
|
+
tmp = self._mk_tmp()
|
|
3147
|
+
self._s(f"uint32_t {tmp} = 0u;")
|
|
3148
|
+
self._s(f"rt_ld_u32({acc}->data, {off}u, &{tmp});")
|
|
3149
|
+
self._s(f"if ((uint64_t){tmp} <= {index_text}) {{")
|
|
3150
|
+
self._s(f"rt_st_u32({acc}->data, {off}u, (uint32_t)({index_text} + 1u));", 1)
|
|
3151
|
+
self._s("}")
|
|
3152
|
+
|
|
3153
|
+
def _as_u64(self, node: ast.expr) -> str:
|
|
3154
|
+
return self._coerce_numeric(self._load(node), "uint64_t", node)
|
|
3155
|
+
|
|
3156
|
+
def _load_ifexp(self, node: ast.IfExp) -> _Frag:
|
|
3157
|
+
cond = self._load(node.test)
|
|
3158
|
+
if cond.kind != "bool":
|
|
3159
|
+
self._diag(PS5002, node.test, "conditional test must be bool")
|
|
3160
|
+
dest = self._guess_ctype(node.body) or self._guess_ctype(node.orelse) or "uint64_t"
|
|
3161
|
+
other = self._guess_ctype(node.orelse)
|
|
3162
|
+
if other is not None and other != dest and dest in _KIND_META and other in _KIND_META:
|
|
3163
|
+
dest = _wider_kind(dest, other)
|
|
3164
|
+
tmp = self._mk_tmp()
|
|
3165
|
+
if dest == "SolPubkey":
|
|
3166
|
+
self._s(f"SolPubkey {tmp};")
|
|
3167
|
+
elif dest in ("RtU128", "RtI128") or dest.startswith("int"):
|
|
3168
|
+
self._s(f"{dest} {tmp} = 0;")
|
|
3169
|
+
else:
|
|
3170
|
+
self._s(f"{dest} {tmp} = 0u;")
|
|
3171
|
+
self._s(f"if ({cond.text}) {{")
|
|
3172
|
+
self._indent += 1
|
|
3173
|
+
self._store_into(tmp, dest, self._load(node.body), node.body)
|
|
3174
|
+
self._indent -= 1
|
|
3175
|
+
self._s("} else {")
|
|
3176
|
+
self._indent += 1
|
|
3177
|
+
self._store_into(tmp, dest, self._load(node.orelse), node.orelse)
|
|
3178
|
+
self._indent -= 1
|
|
3179
|
+
self._s("}")
|
|
3180
|
+
if dest == "SolPubkey":
|
|
3181
|
+
return _Frag(f"&{tmp}", "pub")
|
|
3182
|
+
return _Frag(tmp, dest)
|
|
3183
|
+
|
|
3184
|
+
def _store_into(self, tmp: str, dest: str, frag: _Frag, node: ast.AST) -> None:
|
|
3185
|
+
if dest == "SolPubkey":
|
|
3186
|
+
self._s(f"rt_key_copy(&{tmp}, {frag.text});")
|
|
3187
|
+
return
|
|
3188
|
+
text = frag.text
|
|
3189
|
+
if _canon(frag.kind) != _canon(dest):
|
|
3190
|
+
text = self._coerce_numeric(frag, dest, node)
|
|
3191
|
+
self._s(f"{tmp} = ({dest})({text});")
|
|
3192
|
+
|
|
3193
|
+
def _guess_ctype(self, node: ast.expr) -> str | None:
|
|
3194
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, bool):
|
|
3195
|
+
return "uint8_t"
|
|
3196
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
|
3197
|
+
if node.value < 0:
|
|
3198
|
+
return "int64_t" if node.value >= -(2**63) else "RtI128"
|
|
3199
|
+
return "uint64_t" if node.value <= 2**64 - 1 else "RtU128"
|
|
3200
|
+
if isinstance(node, ast.Name):
|
|
3201
|
+
info = self._locals.get(node.id) or self._data_vars.get(node.id)
|
|
3202
|
+
if info is not None and info[1] != "SolPubkey":
|
|
3203
|
+
return info[1]
|
|
3204
|
+
if info is not None:
|
|
3205
|
+
return "SolPubkey"
|
|
3206
|
+
if (
|
|
3207
|
+
isinstance(node, ast.Call)
|
|
3208
|
+
and isinstance(node.func, ast.Name)
|
|
3209
|
+
and node.func.id in _CAST_NAMES
|
|
3210
|
+
):
|
|
3211
|
+
return _LOCAL_TYPES_C[node.func.id]
|
|
3212
|
+
if isinstance(node, ast.Attribute) and node.attr == "value":
|
|
3213
|
+
meta = self._option_meta(node.value)
|
|
3214
|
+
if meta is not None:
|
|
3215
|
+
return meta[0]
|
|
3216
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
3217
|
+
if node.value.id in self._enums:
|
|
3218
|
+
return "uint8_t"
|
|
3219
|
+
if node.attr == "key":
|
|
3220
|
+
return "SolPubkey"
|
|
3221
|
+
entry = self._acct_vars.get(node.value.id)
|
|
3222
|
+
if entry is None:
|
|
3223
|
+
return None
|
|
3224
|
+
model = self._accounts.get(entry[1])
|
|
3225
|
+
if model is None:
|
|
3226
|
+
return None
|
|
3227
|
+
for field in model.fields:
|
|
3228
|
+
if field.name == node.attr and field.shape == "scalar":
|
|
3229
|
+
return _WIDTH_C[field.c_type]
|
|
3230
|
+
return None
|
|
3231
|
+
|
|
3232
|
+
# -- require -----------------------------------------------------------------
|
|
3233
|
+
|
|
3234
|
+
def _emit_token_transfer(self, call: ast.Call, token2022: bool) -> None:
|
|
3235
|
+
"""SPL Token or Token-2022 Transfer (instruction byte 3)."""
|
|
3236
|
+
if call.keywords or len(call.args) != 4:
|
|
3237
|
+
self._diag(PS5001, call, "token_transfer takes src, dest, authority, amount")
|
|
3238
|
+
return
|
|
3239
|
+
src = self._transfer_var(call.args[0])
|
|
3240
|
+
dest = self._transfer_var(call.args[1])
|
|
3241
|
+
authority = self._transfer_var(call.args[2])
|
|
3242
|
+
if src is None or dest is None or authority is None:
|
|
3243
|
+
return
|
|
3244
|
+
frag = self._load(call.args[3])
|
|
3245
|
+
if frag.kind == "pub":
|
|
3246
|
+
self._diag(PS5002, call.args[3], "amount must be an integer")
|
|
3247
|
+
return
|
|
3248
|
+
text = frag.text
|
|
3249
|
+
if _canon(frag.kind) != "uint64_t":
|
|
3250
|
+
text = self._coerce_numeric(frag, "uint64_t", call.args[3])
|
|
3251
|
+
program = "RT_TOKEN_2022_ID" if token2022 else "RT_TOKEN_ID"
|
|
3252
|
+
rc = self._mk_tmp()
|
|
3253
|
+
self._s("{")
|
|
3254
|
+
self._indent += 1
|
|
3255
|
+
self._s(
|
|
3256
|
+
f"uint64_t {rc} = rt_token_transfer(rt, {program}, {src}, {dest}, "
|
|
3257
|
+
f"{authority}, (uint64_t)({text}));"
|
|
3258
|
+
)
|
|
3259
|
+
self._s(f"if ({rc} != RT_OK) {{")
|
|
3260
|
+
self._s(f'rt_fail_log({rc}, {self._loc(call.lineno)}, "token transfer");', 1)
|
|
3261
|
+
self._s(f"return {rc};", 1)
|
|
3262
|
+
self._s("}")
|
|
3263
|
+
self._indent -= 1
|
|
3264
|
+
self._s("}")
|
|
3265
|
+
|
|
3266
|
+
def _emit_ata_create(self, call: ast.Call) -> None:
|
|
3267
|
+
"""Associated-token CreateIdempotent (data byte 1) for the SPL Token program."""
|
|
3268
|
+
if call.keywords or len(call.args) != 4:
|
|
3269
|
+
self._diag(PS5001, call, "ata_create takes payer, ata, owner, mint")
|
|
3270
|
+
return
|
|
3271
|
+
payer = self._transfer_var(call.args[0])
|
|
3272
|
+
ata = self._transfer_var(call.args[1])
|
|
3273
|
+
owner = self._transfer_var(call.args[2])
|
|
3274
|
+
mint = self._transfer_var(call.args[3])
|
|
3275
|
+
if payer is None or ata is None or owner is None or mint is None:
|
|
3276
|
+
return
|
|
3277
|
+
rc = self._mk_tmp()
|
|
3278
|
+
self._s("{")
|
|
3279
|
+
self._indent += 1
|
|
3280
|
+
self._s(
|
|
3281
|
+
f"uint64_t {rc} = rt_ata_create(rt, {payer}, {ata}, {owner}, {mint}, RT_TOKEN_ID);"
|
|
3282
|
+
)
|
|
3283
|
+
self._s(f"if ({rc} != RT_OK) {{")
|
|
3284
|
+
self._s(f'rt_fail_log({rc}, {self._loc(call.lineno)}, "create ata");', 1)
|
|
3285
|
+
self._s(f"return {rc};", 1)
|
|
3286
|
+
self._s("}")
|
|
3287
|
+
self._indent -= 1
|
|
3288
|
+
self._s("}")
|
|
3289
|
+
|
|
3290
|
+
def _emit_system_transfer(self, call: ast.Call) -> None:
|
|
3291
|
+
"""System-program Transfer (tag 2): lamports move from ``src`` to ``dest``."""
|
|
3292
|
+
if call.keywords or len(call.args) != 3:
|
|
3293
|
+
self._diag(PS5001, call, "system_transfer(src, dest, lamports) takes three arguments")
|
|
3294
|
+
return
|
|
3295
|
+
src = self._transfer_var(call.args[0])
|
|
3296
|
+
dest = self._transfer_var(call.args[1])
|
|
3297
|
+
if src is None or dest is None:
|
|
3298
|
+
return
|
|
3299
|
+
frag = self._load(call.args[2])
|
|
3300
|
+
if frag.kind == "pub":
|
|
3301
|
+
self._diag(PS5002, call.args[2], "lamports must be an integer")
|
|
3302
|
+
return
|
|
3303
|
+
text = frag.text
|
|
3304
|
+
if _canon(frag.kind) != "uint64_t":
|
|
3305
|
+
text = self._coerce_numeric(frag, "uint64_t", call.args[2])
|
|
3306
|
+
rc = self._mk_tmp()
|
|
3307
|
+
self._s("{")
|
|
3308
|
+
self._indent += 1
|
|
3309
|
+
signed = self._pda_transfer(call.args[0])
|
|
3310
|
+
if signed is not None and not signed[1]:
|
|
3311
|
+
self._indent -= 1
|
|
3312
|
+
self._s("}")
|
|
3313
|
+
return
|
|
3314
|
+
if signed is None:
|
|
3315
|
+
self._s(
|
|
3316
|
+
f"uint64_t {rc} = rt_system_transfer(rt, {src}, {dest}, (uint64_t)({text}));"
|
|
3317
|
+
)
|
|
3318
|
+
else:
|
|
3319
|
+
self._s(
|
|
3320
|
+
f"uint64_t {rc} = rt_move_lamports(rt, {src}, {dest}, (uint64_t)({text}));"
|
|
3321
|
+
)
|
|
3322
|
+
self._s(f"if ({rc} != RT_OK) {{")
|
|
3323
|
+
self._s(f'rt_fail_log({rc}, {self._loc(call.lineno)}, "system transfer");', 1)
|
|
3324
|
+
self._s(f"return {rc};", 1)
|
|
3325
|
+
self._s("}")
|
|
3326
|
+
self._indent -= 1
|
|
3327
|
+
self._s("}")
|
|
3328
|
+
|
|
3329
|
+
def _enum_field(
|
|
3330
|
+
self, node: ast.Attribute
|
|
3331
|
+
) -> tuple[FieldModel, str, EnumModel] | None:
|
|
3332
|
+
"""The account field under ``field.value``, when that field is a data enum."""
|
|
3333
|
+
field_node = node.value
|
|
3334
|
+
if not isinstance(field_node, ast.Attribute):
|
|
3335
|
+
return None
|
|
3336
|
+
resolved = self._resolve_field(field_node)
|
|
3337
|
+
if resolved is None:
|
|
3338
|
+
return None
|
|
3339
|
+
field, acc = resolved
|
|
3340
|
+
enum = self._enums.get(field.defined) if field.defined else None
|
|
3341
|
+
if enum is None or not enum.payload_size:
|
|
3342
|
+
return None
|
|
3343
|
+
return field, acc, enum
|
|
3344
|
+
|
|
3345
|
+
def _require_payload_tag(self, acc: str, off: int, enum: EnumModel, line: int) -> str:
|
|
3346
|
+
tag = self._mk_tmp()
|
|
3347
|
+
self._s(f"uint8_t {tag} = {acc}->data[{off}u];")
|
|
3348
|
+
allowed = " || ".join(f"{tag} == {item}u" for item in enum.payload_tags)
|
|
3349
|
+
self._s(f"if (!({allowed})) {{")
|
|
3350
|
+
self._s(
|
|
3351
|
+
f'rt_fail_log({RT_ERR_ENUM}u, {self._loc(line)}, "enum payload");',
|
|
3352
|
+
1,
|
|
3353
|
+
)
|
|
3354
|
+
self._s(f"return {RT_ERR_ENUM}u;", 1)
|
|
3355
|
+
self._s("}")
|
|
3356
|
+
return tag
|
|
3357
|
+
|
|
3358
|
+
def _load_enum_value(self, node: ast.Attribute) -> _Frag | None:
|
|
3359
|
+
found = self._enum_field(node)
|
|
3360
|
+
if found is None:
|
|
3361
|
+
return None
|
|
3362
|
+
field, acc, enum = found
|
|
3363
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
3364
|
+
self._require_payload_tag(acc, off, enum, node.lineno)
|
|
3365
|
+
payload = off + 1
|
|
3366
|
+
size = enum.payload_size
|
|
3367
|
+
tmp = self._mk_tmp()
|
|
3368
|
+
if size == 1:
|
|
3369
|
+
self._s(f"uint8_t {tmp} = {acc}->data[{payload}u];")
|
|
3370
|
+
return _Frag(tmp, "uint8_t")
|
|
3371
|
+
self._s(f"uint64_t {tmp} = 0u;")
|
|
3372
|
+
self._s(f"{_LD_HELPER[size]}({acc}->data, {payload}u, &{tmp});")
|
|
3373
|
+
return _Frag(tmp, "uint64_t")
|
|
3374
|
+
|
|
3375
|
+
def _store_enum_value(self, target: ast.Attribute, value: ast.expr) -> bool:
|
|
3376
|
+
found = self._enum_field(target)
|
|
3377
|
+
if found is None:
|
|
3378
|
+
return False
|
|
3379
|
+
field, acc, enum = found
|
|
3380
|
+
off = ACCOUNT_DATA_OFFSET + field.offset
|
|
3381
|
+
self._require_payload_tag(acc, off, enum, target.lineno)
|
|
3382
|
+
frag = self._load(value)
|
|
3383
|
+
if frag.kind == "pub":
|
|
3384
|
+
self._diag(PS5002, value, "an enum payload is a scalar")
|
|
3385
|
+
return True
|
|
3386
|
+
text = frag.text
|
|
3387
|
+
payload = off + 1
|
|
3388
|
+
size = enum.payload_size
|
|
3389
|
+
if size == 1:
|
|
3390
|
+
self._s(f"{acc}->data[{payload}u] = (uint8_t)({text});")
|
|
3391
|
+
return True
|
|
3392
|
+
if _canon(frag.kind) != "uint64_t":
|
|
3393
|
+
text = self._coerce_numeric(frag, "uint64_t", value)
|
|
3394
|
+
self._s(f"{_ST_HELPER[size]}({acc}->data, {payload}u, {text});")
|
|
3395
|
+
return True
|
|
3396
|
+
|
|
3397
|
+
def _zero_enum_payload(self, field: FieldModel, acc: str, off: str) -> None:
|
|
3398
|
+
enum = self._enums.get(field.defined) if field.defined else None
|
|
3399
|
+
if enum is None or not enum.payload_size:
|
|
3400
|
+
return
|
|
3401
|
+
index = self._mk_tmp()
|
|
3402
|
+
self._s(f"for (uint64_t {index} = 0u; {index} < {enum.payload_size}u; {index}++) {{")
|
|
3403
|
+
self._s(f"{acc}->data[({off}) + 1u + {index}] = 0u;", 1)
|
|
3404
|
+
self._s("}")
|
|
3405
|
+
|
|
3406
|
+
def _account_seed(self, account: str) -> str:
|
|
3407
|
+
"""Seed literal for ``account``, from ``Seeds`` or the snake_case init name."""
|
|
3408
|
+
found = ""
|
|
3409
|
+
for instr in self._model.instructions:
|
|
3410
|
+
for param in instr.account_params:
|
|
3411
|
+
if param.account != account:
|
|
3412
|
+
continue
|
|
3413
|
+
if param.seed:
|
|
3414
|
+
return param.seed
|
|
3415
|
+
if param.is_init and not found:
|
|
3416
|
+
found = snake_case(account)
|
|
3417
|
+
return found
|
|
3418
|
+
|
|
3419
|
+
def _pda_transfer(self, node: ast.expr) -> tuple[str, str, int] | None:
|
|
3420
|
+
"""``(account var, seed, bump offset)`` when ``node`` is a program PDA.
|
|
3421
|
+
|
|
3422
|
+
A missing ``bump`` field is a diagnostic and this returns ``None`` after
|
|
3423
|
+
recording it. Callers must not also emit the unsigned transfer. The
|
|
3424
|
+
sentinel is an empty seed.
|
|
3425
|
+
"""
|
|
3426
|
+
if not isinstance(node, ast.Name):
|
|
3427
|
+
return None
|
|
3428
|
+
param = next((item for item in self._iparams if item.name == node.id), None)
|
|
3429
|
+
if param is None or param.kind == "signer":
|
|
3430
|
+
return None
|
|
3431
|
+
seed = self._account_seed(param.account)
|
|
3432
|
+
if not seed:
|
|
3433
|
+
return None
|
|
3434
|
+
account = self._accounts.get(param.account)
|
|
3435
|
+
bump = None if account is None else account.field("bump")
|
|
3436
|
+
if bump is None or bump.shape != "scalar" or bump.c_type != "uint8_t":
|
|
3437
|
+
self._diag(PS5001, node, "a PDA system_transfer needs a u8 field named bump")
|
|
3438
|
+
return ("", "", -1)
|
|
3439
|
+
acc = self._acct_vars[node.id][0]
|
|
3440
|
+
return acc, seed, ACCOUNT_DATA_OFFSET + bump.offset
|
|
3441
|
+
|
|
3442
|
+
def _transfer_var(self, node: ast.expr) -> str | None:
|
|
3443
|
+
if isinstance(node, ast.Name):
|
|
3444
|
+
entry = self._acct_vars.get(node.id)
|
|
3445
|
+
if entry is not None:
|
|
3446
|
+
return entry[0]
|
|
3447
|
+
self._diag(PS5002, node, "system_transfer account is not an instruction parameter")
|
|
3448
|
+
return None
|
|
3449
|
+
|
|
3450
|
+
def _emit_event(self, call: ast.Call) -> None:
|
|
3451
|
+
"""Log one event: 8-byte discriminator, then packed scalar fields."""
|
|
3452
|
+
if call.keywords or len(call.args) != 1:
|
|
3453
|
+
self._diag(PS5001, call, "emit() takes one event: emit(Name(...))")
|
|
3454
|
+
return
|
|
3455
|
+
arg = call.args[0]
|
|
3456
|
+
if not isinstance(arg, ast.Call) or not isinstance(arg.func, ast.Name):
|
|
3457
|
+
self._diag(PS5001, call, "emit() takes one event: emit(Name(...))")
|
|
3458
|
+
return
|
|
3459
|
+
event = self._events.get(arg.func.id)
|
|
3460
|
+
if event is None:
|
|
3461
|
+
self._diag(PS5002, arg.func, f"unknown event `{arg.func.id}`")
|
|
3462
|
+
return
|
|
3463
|
+
if arg.keywords or len(arg.args) != len(event.fields):
|
|
3464
|
+
self._diag(PS5001, arg, f"`{event.name}` takes {len(event.fields)} field(s)")
|
|
3465
|
+
return
|
|
3466
|
+
total = 8 + event.size
|
|
3467
|
+
buf = self._mk_tmp()
|
|
3468
|
+
self._s("{")
|
|
3469
|
+
self._indent += 1
|
|
3470
|
+
self._s(f"uint8_t {buf}[{total}u];")
|
|
3471
|
+
for index, byte in enumerate(event.discriminator):
|
|
3472
|
+
self._s(f"{buf}[{index}u] = {byte:#x}u;")
|
|
3473
|
+
for field, value in zip(event.fields, arg.args, strict=False):
|
|
3474
|
+
self._store_event_field(buf, field, value)
|
|
3475
|
+
self._s(f"rt_log_event({buf}, {total}u);")
|
|
3476
|
+
self._indent -= 1
|
|
3477
|
+
self._s("}")
|
|
3478
|
+
|
|
3479
|
+
def _store_event_field(self, buf: str, field: FieldModel, value: ast.expr) -> None:
|
|
3480
|
+
frag = self._load(value)
|
|
3481
|
+
off = 8 + field.offset
|
|
3482
|
+
if field.c_type == "SolPubkey":
|
|
3483
|
+
if frag.kind != "pub":
|
|
3484
|
+
self._diag(PS5002, value, "cannot store non-Pubkey into a Pubkey field")
|
|
3485
|
+
return
|
|
3486
|
+
self._s(f"rt_st_key({buf}, {off}u, {frag.text});")
|
|
3487
|
+
return
|
|
3488
|
+
if frag.kind == "pub":
|
|
3489
|
+
self._diag(PS5002, value, "cannot store a Pubkey into a numeric field")
|
|
3490
|
+
return
|
|
3491
|
+
if field.c_type not in _WIDTH_BYTES:
|
|
3492
|
+
self._diag(PS5002, value, f"event field `{field.name}` is not lowered")
|
|
3493
|
+
return
|
|
3494
|
+
text = frag.text
|
|
3495
|
+
if _canon(frag.kind) != _canon(field.c_type):
|
|
3496
|
+
text = self._coerce_numeric(frag, field.c_type, value)
|
|
3497
|
+
size = _WIDTH_BYTES[field.c_type]
|
|
3498
|
+
if size == 1:
|
|
3499
|
+
self._s(f"{buf}[{off}u] = (uint8_t)({text});")
|
|
3500
|
+
return
|
|
3501
|
+
if field.c_type in _UNSIGNED_OF:
|
|
3502
|
+
text = f"({_UNSIGNED_OF[field.c_type]})({text})"
|
|
3503
|
+
self._s(f"{_ST_HELPER[size]}({buf}, {off}u, {text});")
|
|
3504
|
+
|
|
3505
|
+
def _emit_require(self, call: ast.Call) -> None:
|
|
3506
|
+
func = call.func
|
|
3507
|
+
if not isinstance(func, ast.Name) or func.id != "require":
|
|
3508
|
+
self._diag(PS5001, func, "only `require(...)` calls lower (phase 3)")
|
|
3509
|
+
return
|
|
3510
|
+
if len(call.args) != 2 or call.keywords:
|
|
3511
|
+
self._diag(
|
|
3512
|
+
PS5001,
|
|
3513
|
+
call,
|
|
3514
|
+
"require(condition, ERR.Member) needs two positional args",
|
|
3515
|
+
)
|
|
3516
|
+
return
|
|
3517
|
+
cond = self._load(call.args[0])
|
|
3518
|
+
if cond.kind != "bool":
|
|
3519
|
+
self._diag(PS5002, call.args[0], "require() condition must be bool")
|
|
3520
|
+
_code, macro = self._require_error_code(call.args[1])
|
|
3521
|
+
text = f"require failed: {self._require_name(call.args[1])}"
|
|
3522
|
+
self._s("if (!(" + cond.text + ")) {")
|
|
3523
|
+
self._s(
|
|
3524
|
+
f'rt_fail_log({macro}, {self._loc(call.lineno)}, "{text}");',
|
|
3525
|
+
1,
|
|
3526
|
+
)
|
|
3527
|
+
self._s(f"return {macro};", 1)
|
|
3528
|
+
self._s("}")
|
|
3529
|
+
|
|
3530
|
+
def _require_error_code(self, node: ast.expr) -> tuple[int, str]:
|
|
3531
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
3532
|
+
owner = node.value.id
|
|
3533
|
+
member = node.attr
|
|
3534
|
+
code = self._error_codes.get((owner, member))
|
|
3535
|
+
if code is not None:
|
|
3536
|
+
return code, self._err_macro(owner, member)
|
|
3537
|
+
self._diag(PS5002, node, "require() error must be an @error member `Err.Member`")
|
|
3538
|
+
return RT_ERR_UNKNOWN, f"{RT_ERR_UNKNOWN}u"
|
|
3539
|
+
|
|
3540
|
+
def _require_name(self, node: ast.expr) -> str:
|
|
3541
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
3542
|
+
return f"{node.value.id}.{node.attr}"
|
|
3543
|
+
return "?"
|
|
3544
|
+
|
|
3545
|
+
|
|
3546
|
+
def emit_program_c(model: ProgramModel) -> tuple[str, list[Diagnostic]]:
|
|
3547
|
+
"""Lower ``model`` into C source text plus any emitter diagnostics."""
|
|
3548
|
+
emitter = _Emitter(model)
|
|
3549
|
+
source = emitter.run()
|
|
3550
|
+
return source, emitter.diagnostics
|