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.
Files changed (76) hide show
  1. pythsol/__init__.py +317 -0
  2. pythsol/cli/__init__.py +7 -0
  3. pythsol/cli/cache.py +78 -0
  4. pythsol/cli/cluster.py +45 -0
  5. pythsol/cli/commands/build.py +214 -0
  6. pythsol/cli/commands/check.py +54 -0
  7. pythsol/cli/commands/client_cmd.py +97 -0
  8. pythsol/cli/commands/deploy.py +103 -0
  9. pythsol/cli/commands/docs_cmd.py +62 -0
  10. pythsol/cli/commands/doctor.py +211 -0
  11. pythsol/cli/commands/emit_c.py +69 -0
  12. pythsol/cli/commands/faucet.py +66 -0
  13. pythsol/cli/commands/idl_cmd.py +69 -0
  14. pythsol/cli/commands/idl_publish.py +80 -0
  15. pythsol/cli/commands/init.py +65 -0
  16. pythsol/cli/commands/port_cmd.py +42 -0
  17. pythsol/cli/commands/profile.py +57 -0
  18. pythsol/cli/commands/serve.py +52 -0
  19. pythsol/cli/commands/test_cmd.py +217 -0
  20. pythsol/cli/commands/verify.py +96 -0
  21. pythsol/cli/commands/wallet.py +128 -0
  22. pythsol/cli/config.py +84 -0
  23. pythsol/cli/main.py +72 -0
  24. pythsol/cli/rpc.py +56 -0
  25. pythsol/cli/template_programs/__init__.py +1 -0
  26. pythsol/cli/template_programs/counter.py +52 -0
  27. pythsol/cli/template_programs/escrow.py +75 -0
  28. pythsol/cli/template_programs/ledger.py +68 -0
  29. pythsol/cli/template_programs/multisig.py +91 -0
  30. pythsol/cli/template_programs/poll.py +70 -0
  31. pythsol/cli/template_programs/vault.py +67 -0
  32. pythsol/cli/templates.py +192 -0
  33. pythsol/client/__init__.py +5 -0
  34. pythsol/client/from_idl.py +356 -0
  35. pythsol/client/gen.py +496 -0
  36. pythsol/client/port.py +355 -0
  37. pythsol/compiler/__init__.py +39 -0
  38. pythsol/compiler/diagnostics.py +130 -0
  39. pythsol/compiler/emit.py +3550 -0
  40. pythsol/compiler/front.py +84 -0
  41. pythsol/compiler/idl.py +465 -0
  42. pythsol/compiler/model.py +1329 -0
  43. pythsol/compiler/modules.py +320 -0
  44. pythsol/compiler/parse.py +59 -0
  45. pythsol/compiler/pipeline.py +152 -0
  46. pythsol/compiler/resolve.py +518 -0
  47. pythsol/compiler/subset.py +1061 -0
  48. pythsol/compiler/toolchain.py +438 -0
  49. pythsol/compiler/typecheck.py +2447 -0
  50. pythsol/playground/__init__.py +7 -0
  51. pythsol/playground/app.css +182 -0
  52. pythsol/playground/app.js +260 -0
  53. pythsol/playground/apple-touch-icon.png +0 -0
  54. pythsol/playground/docs.html +61 -0
  55. pythsol/playground/favicon-32.png +0 -0
  56. pythsol/playground/favicon.ico +0 -0
  57. pythsol/playground/favicon.svg +24 -0
  58. pythsol/playground/hero-bg.webp +0 -0
  59. pythsol/playground/index.html +66 -0
  60. pythsol/playground/landing.html +810 -0
  61. pythsol/playground/playground.html +77 -0
  62. pythsol/playground/robots.txt +4 -0
  63. pythsol/playground/server.py +336 -0
  64. pythsol/playground/site-bg.svg +146 -0
  65. pythsol/playground/site.css +1105 -0
  66. pythsol/py.typed +0 -0
  67. pythsol/refrt/__init__.py +200 -0
  68. pythsol/runtime/__init__.py +17 -0
  69. pythsol/runtime/pythsol_rt.h +1216 -0
  70. pythsol/testing/__init__.py +12 -0
  71. pythsol/testing/plugin.py +437 -0
  72. pythsol-0.1.0.dist-info/METADATA +178 -0
  73. pythsol-0.1.0.dist-info/RECORD +76 -0
  74. pythsol-0.1.0.dist-info/WHEEL +4 -0
  75. pythsol-0.1.0.dist-info/entry_points.txt +6 -0
  76. pythsol-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -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