pythsol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythsol/__init__.py +317 -0
- pythsol/cli/__init__.py +7 -0
- pythsol/cli/cache.py +78 -0
- pythsol/cli/cluster.py +45 -0
- pythsol/cli/commands/build.py +214 -0
- pythsol/cli/commands/check.py +54 -0
- pythsol/cli/commands/client_cmd.py +97 -0
- pythsol/cli/commands/deploy.py +103 -0
- pythsol/cli/commands/docs_cmd.py +62 -0
- pythsol/cli/commands/doctor.py +211 -0
- pythsol/cli/commands/emit_c.py +69 -0
- pythsol/cli/commands/faucet.py +66 -0
- pythsol/cli/commands/idl_cmd.py +69 -0
- pythsol/cli/commands/idl_publish.py +80 -0
- pythsol/cli/commands/init.py +65 -0
- pythsol/cli/commands/port_cmd.py +42 -0
- pythsol/cli/commands/profile.py +57 -0
- pythsol/cli/commands/serve.py +52 -0
- pythsol/cli/commands/test_cmd.py +217 -0
- pythsol/cli/commands/verify.py +96 -0
- pythsol/cli/commands/wallet.py +128 -0
- pythsol/cli/config.py +84 -0
- pythsol/cli/main.py +72 -0
- pythsol/cli/rpc.py +56 -0
- pythsol/cli/template_programs/__init__.py +1 -0
- pythsol/cli/template_programs/counter.py +52 -0
- pythsol/cli/template_programs/escrow.py +75 -0
- pythsol/cli/template_programs/ledger.py +68 -0
- pythsol/cli/template_programs/multisig.py +91 -0
- pythsol/cli/template_programs/poll.py +70 -0
- pythsol/cli/template_programs/vault.py +67 -0
- pythsol/cli/templates.py +192 -0
- pythsol/client/__init__.py +5 -0
- pythsol/client/from_idl.py +356 -0
- pythsol/client/gen.py +496 -0
- pythsol/client/port.py +355 -0
- pythsol/compiler/__init__.py +39 -0
- pythsol/compiler/diagnostics.py +130 -0
- pythsol/compiler/emit.py +3550 -0
- pythsol/compiler/front.py +84 -0
- pythsol/compiler/idl.py +465 -0
- pythsol/compiler/model.py +1329 -0
- pythsol/compiler/modules.py +320 -0
- pythsol/compiler/parse.py +59 -0
- pythsol/compiler/pipeline.py +152 -0
- pythsol/compiler/resolve.py +518 -0
- pythsol/compiler/subset.py +1061 -0
- pythsol/compiler/toolchain.py +438 -0
- pythsol/compiler/typecheck.py +2447 -0
- pythsol/playground/__init__.py +7 -0
- pythsol/playground/app.css +182 -0
- pythsol/playground/app.js +260 -0
- pythsol/playground/apple-touch-icon.png +0 -0
- pythsol/playground/docs.html +61 -0
- pythsol/playground/favicon-32.png +0 -0
- pythsol/playground/favicon.ico +0 -0
- pythsol/playground/favicon.svg +24 -0
- pythsol/playground/hero-bg.webp +0 -0
- pythsol/playground/index.html +66 -0
- pythsol/playground/landing.html +810 -0
- pythsol/playground/playground.html +77 -0
- pythsol/playground/robots.txt +4 -0
- pythsol/playground/server.py +336 -0
- pythsol/playground/site-bg.svg +146 -0
- pythsol/playground/site.css +1105 -0
- pythsol/py.typed +0 -0
- pythsol/refrt/__init__.py +200 -0
- pythsol/runtime/__init__.py +17 -0
- pythsol/runtime/pythsol_rt.h +1216 -0
- pythsol/testing/__init__.py +12 -0
- pythsol/testing/plugin.py +437 -0
- pythsol-0.1.0.dist-info/METADATA +178 -0
- pythsol-0.1.0.dist-info/RECORD +76 -0
- pythsol-0.1.0.dist-info/WHEEL +4 -0
- pythsol-0.1.0.dist-info/entry_points.txt +6 -0
- pythsol-0.1.0.dist-info/licenses/LICENSE +202 -0
pythsol/client/port.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Turn an Anchor-legacy IDL into a PythSol program skeleton.
|
|
2
|
+
|
|
3
|
+
The bodies are ``pass``. Account wrappers are inferred: a ``seeds`` constraint
|
|
4
|
+
becomes ``Seeds[Init[T], "seed"]``, a mutable account on ``initialize`` /
|
|
5
|
+
``init`` / ``make`` / ``create`` / ``open`` becomes ``Init[T]``, and other
|
|
6
|
+
mutable accounts become ``Mut[T]``. The compiler still has to accept the
|
|
7
|
+
result; fill the bodies in before the program can run.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = ["render_port"]
|
|
15
|
+
|
|
16
|
+
_INIT_NAMES = {"initialize", "init", "make", "create", "open"}
|
|
17
|
+
_SCALARS = {"u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128"}
|
|
18
|
+
_WRAPPERS = ("Seeds[", "HasOne[", "CloseTo[", "Address[", "Owner[")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PortError(Exception):
|
|
22
|
+
"""The IDL is too incomplete to sketch a program."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_port(idl: dict[str, Any]) -> str:
|
|
26
|
+
"""Return Python source for a skeleton program."""
|
|
27
|
+
program = str(idl.get("name") or "program")
|
|
28
|
+
types = _type_map(idl)
|
|
29
|
+
accounts = [item for item in idl.get("accounts") or [] if isinstance(item, dict)]
|
|
30
|
+
if not accounts:
|
|
31
|
+
raise PortError("IDL has no accounts to port")
|
|
32
|
+
imports: set[str] = {"account", "instruction"}
|
|
33
|
+
blocks: list[str] = []
|
|
34
|
+
for info in types.values():
|
|
35
|
+
body = info.get("type")
|
|
36
|
+
if isinstance(body, dict) and body.get("kind") == "enum":
|
|
37
|
+
imports.add("enum")
|
|
38
|
+
blocks.append(_enum_block(info))
|
|
39
|
+
for info in types.values():
|
|
40
|
+
if _is_user_struct(info):
|
|
41
|
+
imports.add("struct")
|
|
42
|
+
blocks.append(_struct_block(info, types, imports))
|
|
43
|
+
for account in accounts:
|
|
44
|
+
blocks.append(_account_block(account, types, imports))
|
|
45
|
+
errors = [item for item in idl.get("errors") or [] if isinstance(item, dict)]
|
|
46
|
+
if errors:
|
|
47
|
+
imports.update({"error", "ProgramError"})
|
|
48
|
+
blocks.append(_error_block(program, errors))
|
|
49
|
+
instructions = [item for item in idl.get("instructions") or [] if isinstance(item, dict)]
|
|
50
|
+
if not instructions:
|
|
51
|
+
raise PortError("IDL has no instructions to port")
|
|
52
|
+
for instr in instructions:
|
|
53
|
+
blocks.append(_instruction_block(instr, accounts, types, imports))
|
|
54
|
+
header = [
|
|
55
|
+
f'"""Ported from the `{program}` IDL.',
|
|
56
|
+
"",
|
|
57
|
+
"Each instruction body is a placeholder. Replace ``pass`` with the program.",
|
|
58
|
+
'"""',
|
|
59
|
+
"",
|
|
60
|
+
"from pythsol import (",
|
|
61
|
+
]
|
|
62
|
+
for name in _import_lines(imports):
|
|
63
|
+
header.append(f" {name},")
|
|
64
|
+
header.append(")")
|
|
65
|
+
header.append("")
|
|
66
|
+
return "\n".join([*header, "", *blocks]).rstrip() + "\n"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _import_lines(names: set[str]) -> list[str]:
|
|
70
|
+
order = [
|
|
71
|
+
"Account",
|
|
72
|
+
"Address",
|
|
73
|
+
"CloseTo",
|
|
74
|
+
"FixedArray",
|
|
75
|
+
"FixedString",
|
|
76
|
+
"HasOne",
|
|
77
|
+
"Init",
|
|
78
|
+
"Mut",
|
|
79
|
+
"Option",
|
|
80
|
+
"Owner",
|
|
81
|
+
"ProgramError",
|
|
82
|
+
"Pubkey",
|
|
83
|
+
"Seeds",
|
|
84
|
+
"Signer",
|
|
85
|
+
"account",
|
|
86
|
+
"enum",
|
|
87
|
+
"error",
|
|
88
|
+
"instruction",
|
|
89
|
+
"struct",
|
|
90
|
+
"u8",
|
|
91
|
+
"u16",
|
|
92
|
+
"u32",
|
|
93
|
+
"u64",
|
|
94
|
+
"u128",
|
|
95
|
+
"i8",
|
|
96
|
+
"i16",
|
|
97
|
+
"i32",
|
|
98
|
+
"i64",
|
|
99
|
+
"i128",
|
|
100
|
+
]
|
|
101
|
+
return [name for name in order if name in names]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _type_map(idl: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
105
|
+
found: dict[str, dict[str, Any]] = {}
|
|
106
|
+
for item in idl.get("types") or []:
|
|
107
|
+
if isinstance(item, dict) and isinstance(item.get("name"), str):
|
|
108
|
+
found[item["name"]] = item
|
|
109
|
+
return found
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _snake(name: str) -> str:
|
|
113
|
+
out: list[str] = []
|
|
114
|
+
for char in name:
|
|
115
|
+
if char.isupper() and out and out[-1] != "_":
|
|
116
|
+
out.append("_")
|
|
117
|
+
out.append(char.lower())
|
|
118
|
+
return "".join(out)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _pascal(name: str) -> str:
|
|
122
|
+
parts = name.replace("-", "_").split("_")
|
|
123
|
+
return "".join(part[:1].upper() + part[1:] for part in parts if part) or "Program"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_user_struct(info: dict[str, Any]) -> bool:
|
|
127
|
+
body = info.get("type")
|
|
128
|
+
if not isinstance(body, dict) or body.get("kind") != "struct":
|
|
129
|
+
return False
|
|
130
|
+
fields = body.get("fields")
|
|
131
|
+
if not isinstance(fields, list):
|
|
132
|
+
return False
|
|
133
|
+
if (
|
|
134
|
+
len(fields) == 2
|
|
135
|
+
and isinstance(fields[0], dict)
|
|
136
|
+
and isinstance(fields[1], dict)
|
|
137
|
+
and fields[0].get("name") == "tag"
|
|
138
|
+
and fields[1].get("name") == "value"
|
|
139
|
+
):
|
|
140
|
+
return False
|
|
141
|
+
return not (
|
|
142
|
+
len(fields) == 2
|
|
143
|
+
and isinstance(fields[0], dict)
|
|
144
|
+
and isinstance(fields[1], dict)
|
|
145
|
+
and fields[0].get("name") == "len"
|
|
146
|
+
and fields[1].get("name") == "data"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _annotation(ty: Any, types: dict[str, dict[str, Any]], imports: set[str]) -> str:
|
|
151
|
+
if ty == "publicKey":
|
|
152
|
+
imports.add("Pubkey")
|
|
153
|
+
return "Pubkey"
|
|
154
|
+
if ty == "bool":
|
|
155
|
+
return "bool"
|
|
156
|
+
if isinstance(ty, str) and ty in _SCALARS:
|
|
157
|
+
imports.add(ty)
|
|
158
|
+
return ty
|
|
159
|
+
if isinstance(ty, dict) and isinstance(ty.get("array"), list) and len(ty["array"]) == 2:
|
|
160
|
+
inner, count = ty["array"]
|
|
161
|
+
imports.add("FixedArray")
|
|
162
|
+
return f"FixedArray[{_annotation(inner, types, imports)}, {count}]"
|
|
163
|
+
if isinstance(ty, dict) and isinstance(ty.get("option"), (str, dict)):
|
|
164
|
+
imports.add("Option")
|
|
165
|
+
return f"Option[{_annotation(ty['option'], types, imports)}]"
|
|
166
|
+
if isinstance(ty, dict) and isinstance(ty.get("defined"), str):
|
|
167
|
+
return _defined(ty["defined"], types, imports)
|
|
168
|
+
imports.add("u64")
|
|
169
|
+
return "u64"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _defined(name: str, types: dict[str, dict[str, Any]], imports: set[str]) -> str:
|
|
173
|
+
info = types.get(name)
|
|
174
|
+
body = info.get("type") if isinstance(info, dict) else None
|
|
175
|
+
if not isinstance(body, dict):
|
|
176
|
+
return name
|
|
177
|
+
if body.get("kind") == "enum":
|
|
178
|
+
return name
|
|
179
|
+
fields = body.get("fields")
|
|
180
|
+
if isinstance(fields, list) and len(fields) == 2:
|
|
181
|
+
first, second = fields[0], fields[1]
|
|
182
|
+
if (
|
|
183
|
+
isinstance(first, dict)
|
|
184
|
+
and isinstance(second, dict)
|
|
185
|
+
and first.get("name") == "tag"
|
|
186
|
+
and second.get("name") == "value"
|
|
187
|
+
):
|
|
188
|
+
imports.add("Option")
|
|
189
|
+
return f"Option[{_annotation(second.get('type'), types, imports)}]"
|
|
190
|
+
if (
|
|
191
|
+
isinstance(first, dict)
|
|
192
|
+
and isinstance(second, dict)
|
|
193
|
+
and first.get("name") == "len"
|
|
194
|
+
and second.get("name") == "data"
|
|
195
|
+
and isinstance(second.get("type"), dict)
|
|
196
|
+
and isinstance(second["type"].get("array"), list)
|
|
197
|
+
):
|
|
198
|
+
imports.add("FixedString")
|
|
199
|
+
return f"FixedString[{second['type']['array'][1]}]"
|
|
200
|
+
return name
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _enum_block(info: dict[str, Any]) -> str:
|
|
204
|
+
name = str(info.get("name") or "Enum")
|
|
205
|
+
body = info.get("type") if isinstance(info.get("type"), dict) else {}
|
|
206
|
+
variants = body.get("variants") if isinstance(body, dict) else []
|
|
207
|
+
lines = ["@enum", f"class {name}:"]
|
|
208
|
+
if not isinstance(variants, list) or not variants:
|
|
209
|
+
lines.append(" pass")
|
|
210
|
+
return "\n".join(lines)
|
|
211
|
+
for index, variant in enumerate(variants):
|
|
212
|
+
label = "Variant"
|
|
213
|
+
if isinstance(variant, dict) and isinstance(variant.get("name"), str):
|
|
214
|
+
label = variant["name"]
|
|
215
|
+
lines.append(f" {label} = {index}")
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _struct_block(info: dict[str, Any], types: dict[str, dict[str, Any]], imports: set[str]) -> str:
|
|
220
|
+
name = str(info.get("name") or "Struct")
|
|
221
|
+
body = info.get("type") if isinstance(info.get("type"), dict) else {}
|
|
222
|
+
fields = body.get("fields") if isinstance(body, dict) else []
|
|
223
|
+
lines = ["@struct", f"class {name}:"]
|
|
224
|
+
if not isinstance(fields, list) or not fields:
|
|
225
|
+
lines.append(" pass")
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
for field in fields:
|
|
228
|
+
if not isinstance(field, dict):
|
|
229
|
+
continue
|
|
230
|
+
field_name = str(field.get("name") or "field")
|
|
231
|
+
lines.append(f" {field_name}: {_annotation(field.get('type'), types, imports)}")
|
|
232
|
+
return "\n".join(lines)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _account_block(
|
|
236
|
+
account: dict[str, Any],
|
|
237
|
+
types: dict[str, dict[str, Any]],
|
|
238
|
+
imports: set[str],
|
|
239
|
+
) -> str:
|
|
240
|
+
name = str(account.get("name") or "Account")
|
|
241
|
+
body = account.get("type") if isinstance(account.get("type"), dict) else {}
|
|
242
|
+
fields = body.get("fields") if isinstance(body, dict) else []
|
|
243
|
+
lines = ["@account", f"class {name}:"]
|
|
244
|
+
if not isinstance(fields, list) or not fields:
|
|
245
|
+
lines.append(" pass")
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
for field in fields:
|
|
248
|
+
if not isinstance(field, dict):
|
|
249
|
+
continue
|
|
250
|
+
field_name = str(field.get("name") or "field")
|
|
251
|
+
lines.append(f" {field_name}: {_annotation(field.get('type'), types, imports)}")
|
|
252
|
+
return "\n".join(lines)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _error_block(program: str, errors: list[dict[str, Any]]) -> str:
|
|
256
|
+
lines = [
|
|
257
|
+
"@error",
|
|
258
|
+
f"class {_pascal(program)}Error(ProgramError):",
|
|
259
|
+
' """Ported error names. Codes on chain are 6000 + declaration index."""',
|
|
260
|
+
]
|
|
261
|
+
for index, error in enumerate(errors):
|
|
262
|
+
label = str(error.get("name") or f"Error{index}")
|
|
263
|
+
lines.append(f" {label} = {index}")
|
|
264
|
+
return "\n".join(lines)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _match_account(param: str, accounts: list[dict[str, Any]]) -> str | None:
|
|
268
|
+
for account in accounts:
|
|
269
|
+
name = str(account.get("name") or "")
|
|
270
|
+
if name == param or _snake(name) == param:
|
|
271
|
+
return name
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _constraint(account: dict[str, Any], kind: str, key: str) -> str | None:
|
|
276
|
+
raw = account.get("constraints")
|
|
277
|
+
if not isinstance(raw, list):
|
|
278
|
+
return None
|
|
279
|
+
for item in raw:
|
|
280
|
+
if isinstance(item, dict) and item.get("kind") == kind and isinstance(item.get(key), str):
|
|
281
|
+
return str(item[key])
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _account_annotation(
|
|
286
|
+
instr_name: str,
|
|
287
|
+
account: dict[str, Any],
|
|
288
|
+
accounts: list[dict[str, Any]],
|
|
289
|
+
imports: set[str],
|
|
290
|
+
) -> str:
|
|
291
|
+
if account.get("isSigner"):
|
|
292
|
+
imports.add("Signer")
|
|
293
|
+
return "Signer"
|
|
294
|
+
matched = _match_account(str(account.get("name") or ""), accounts)
|
|
295
|
+
if matched is None:
|
|
296
|
+
imports.add("Pubkey")
|
|
297
|
+
return "Pubkey"
|
|
298
|
+
seed = _constraint(account, "seeds", "seed")
|
|
299
|
+
has_one = _constraint(account, "has_one", "field")
|
|
300
|
+
close = _constraint(account, "close", "account")
|
|
301
|
+
owner = _constraint(account, "owner", "account")
|
|
302
|
+
address = _constraint(account, "address", "pubkey")
|
|
303
|
+
if seed is not None:
|
|
304
|
+
imports.update({"Seeds", "Init"})
|
|
305
|
+
return f'Seeds[Init[{matched}], "{seed}"]'
|
|
306
|
+
if has_one is not None:
|
|
307
|
+
imports.update({"HasOne", "Mut"})
|
|
308
|
+
return f'HasOne[Mut[{matched}], "{has_one}"]'
|
|
309
|
+
if close is not None:
|
|
310
|
+
imports.update({"CloseTo", "Mut"})
|
|
311
|
+
return f"CloseTo[Mut[{matched}], {close}]"
|
|
312
|
+
if owner is not None:
|
|
313
|
+
imports.update({"Owner", "Mut"})
|
|
314
|
+
return f"Owner[Mut[{matched}], {owner}]"
|
|
315
|
+
if address is not None:
|
|
316
|
+
imports.update({"Address", "Mut"})
|
|
317
|
+
return f"Address[Mut[{matched}], {address}]"
|
|
318
|
+
if instr_name in _INIT_NAMES and account.get("isMut"):
|
|
319
|
+
imports.add("Init")
|
|
320
|
+
return f"Init[{matched}]"
|
|
321
|
+
if account.get("isMut"):
|
|
322
|
+
imports.add("Mut")
|
|
323
|
+
return f"Mut[{matched}]"
|
|
324
|
+
imports.add("Account")
|
|
325
|
+
return f"Account[{matched}]"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _instruction_block(
|
|
329
|
+
instr: dict[str, Any],
|
|
330
|
+
accounts: list[dict[str, Any]],
|
|
331
|
+
types: dict[str, dict[str, Any]],
|
|
332
|
+
imports: set[str],
|
|
333
|
+
) -> str:
|
|
334
|
+
name = str(instr.get("name") or "instruction")
|
|
335
|
+
params: list[str] = []
|
|
336
|
+
for account in instr.get("accounts") or []:
|
|
337
|
+
if not isinstance(account, dict):
|
|
338
|
+
continue
|
|
339
|
+
param = str(account.get("name") or "account")
|
|
340
|
+
ann = _account_annotation(name, account, accounts, imports)
|
|
341
|
+
suffix = ""
|
|
342
|
+
if ann.startswith(_WRAPPERS):
|
|
343
|
+
suffix = " # type: ignore[name-defined] # noqa: F821"
|
|
344
|
+
params.append(f" {param}: {ann},{suffix}")
|
|
345
|
+
for arg in instr.get("args") or []:
|
|
346
|
+
if not isinstance(arg, dict):
|
|
347
|
+
continue
|
|
348
|
+
param = str(arg.get("name") or "arg")
|
|
349
|
+
params.append(f" {param}: {_annotation(arg.get('type'), types, imports)},")
|
|
350
|
+
lines = ["@instruction", f"def {name}("]
|
|
351
|
+
lines.extend(params)
|
|
352
|
+
lines.append(") -> None:")
|
|
353
|
+
lines.append(f' """TODO: port {name}."""')
|
|
354
|
+
lines.append(" pass")
|
|
355
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""PythSol compiler — front-end and toolchain modules.
|
|
2
|
+
|
|
3
|
+
Pipeline: ``parse → subset → resolve → typecheck`` produce
|
|
4
|
+
:class:`~pythsol.compiler.diagnostics.Diagnostic` objects; ``toolchain`` then
|
|
5
|
+
compiles the generated C (Phase 3) to native SBF with clang + ld.lld.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pythsol.compiler.diagnostics import PS4010, Diagnostic, PysolError
|
|
11
|
+
from pythsol.compiler.front import check_source, check_tree
|
|
12
|
+
from pythsol.compiler.toolchain import (
|
|
13
|
+
ToolchainPaths,
|
|
14
|
+
compile_to_sbf,
|
|
15
|
+
find_cargo_build_sbf,
|
|
16
|
+
find_clang,
|
|
17
|
+
find_lld,
|
|
18
|
+
find_solana_cli,
|
|
19
|
+
locate_platform_tools,
|
|
20
|
+
sbpf_compile_args,
|
|
21
|
+
sbpf_link_args,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"PS4010",
|
|
26
|
+
"Diagnostic",
|
|
27
|
+
"PysolError",
|
|
28
|
+
"ToolchainPaths",
|
|
29
|
+
"check_source",
|
|
30
|
+
"check_tree",
|
|
31
|
+
"compile_to_sbf",
|
|
32
|
+
"find_cargo_build_sbf",
|
|
33
|
+
"find_clang",
|
|
34
|
+
"find_lld",
|
|
35
|
+
"find_solana_cli",
|
|
36
|
+
"locate_platform_tools",
|
|
37
|
+
"sbpf_compile_args",
|
|
38
|
+
"sbpf_link_args",
|
|
39
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Stable diagnostic codes and helpers shared by every compiler stage.
|
|
2
|
+
|
|
3
|
+
Codes follow the pattern used in ``TRD.md`` §5.3:
|
|
4
|
+
|
|
5
|
+
* ``PS1xxx`` — subset/parse
|
|
6
|
+
* ``PS2xxx`` — type check
|
|
7
|
+
* ``PS3xxx`` — semantic/aliasing
|
|
8
|
+
* ``PS9xxx`` — internal compiler error (always a bug: report it)
|
|
9
|
+
|
|
10
|
+
The registry below is written for Phase 2, but Phase 1 already hands back
|
|
11
|
+
structured objects for toolchain errors.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Diagnostic:
|
|
21
|
+
"""One problem with a stable code and 1-based source position.
|
|
22
|
+
|
|
23
|
+
``render()`` produces the clang-style string the CLI prints:
|
|
24
|
+
|
|
25
|
+
PS1003 program.py:4:5: unsupported statement: Try
|
|
26
|
+
|
|
27
|
+
The shape of :meth:`as_dict` is the ``--json`` contract; keep it additive.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
code: str
|
|
31
|
+
message: str
|
|
32
|
+
file: str = "<program>"
|
|
33
|
+
line: int = 1
|
|
34
|
+
col: int = 1
|
|
35
|
+
fix: str | None = None
|
|
36
|
+
|
|
37
|
+
def render(self) -> str:
|
|
38
|
+
text = f"{self.code} {self.file}:{self.line}:{self.col}: {self.message}"
|
|
39
|
+
if self.fix:
|
|
40
|
+
return f"{text}\n fix: {self.fix}"
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
def position_key(self) -> tuple[int, int]:
|
|
44
|
+
return (self.line, self.col)
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> dict[str, str | int | None]:
|
|
47
|
+
return {
|
|
48
|
+
"code": self.code,
|
|
49
|
+
"message": self.message,
|
|
50
|
+
"file": self.file,
|
|
51
|
+
"line": self.line,
|
|
52
|
+
"col": self.col,
|
|
53
|
+
"fix": self.fix,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PysolError(Exception):
|
|
58
|
+
"""A structured compiler/toolchain error raised back to the CLI.
|
|
59
|
+
|
|
60
|
+
Not a dataclass: exceptions must accept ``__traceback__`` writes when
|
|
61
|
+
raised (a frozen dataclass blocks click's re-raise machinery).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, code: str, message: str, fix: str | None = None) -> None:
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self.code = code
|
|
67
|
+
self.message = message
|
|
68
|
+
self.fix = fix
|
|
69
|
+
|
|
70
|
+
def render(self) -> str:
|
|
71
|
+
text = f"{self.code}: {self.message}"
|
|
72
|
+
if self.fix:
|
|
73
|
+
return f"{text}\nsuggested fix: {self.fix}"
|
|
74
|
+
return text
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Subset / parse problems (PS1xxx)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
PS1000 = "PS1000" # unparseable syntax
|
|
81
|
+
PS1001 = "PS1001" # unsupported import
|
|
82
|
+
PS1002 = "PS1002" # floats/complex in program code
|
|
83
|
+
PS1003 = "PS1003" # unsupported statement (try/except, with, yield…)
|
|
84
|
+
PS1004 = "PS1004" # dynamic `import` or `__import__`
|
|
85
|
+
PS1005 = "PS1005" # `*args`/`**kwargs`/defaults on instruction functions are not callable.
|
|
86
|
+
PS1006 = "PS1006" # missing annotation on a parameter/field local.
|
|
87
|
+
PS1007 = "PS1007" # unsupported type annotation (unbounded containers, Any).
|
|
88
|
+
PS1008 = "PS1008" # unsupported attribute access / dynamic attribute assignment.
|
|
89
|
+
PS1009 = "PS1009" # unsupported operator (is, in, not-in on non-enums).
|
|
90
|
+
PS1010 = "PS1010" # unsupported call (dynamic callee).
|
|
91
|
+
PS1011 = "PS1011" # unsupported comprehension/generator.
|
|
92
|
+
PS1012 = "PS1012" # unsupported `class` (only @account/@error/@enum programs).
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Type-check problems (PS2xxx)
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
PS2000 = "PS2000" # missing annotation on anything not inferable.
|
|
98
|
+
PS2001 = "PS2001" # unsupported/unknown type annotation.
|
|
99
|
+
PS2002 = "PS2002" # `float`/`complex` literal in program code.
|
|
100
|
+
PS2003 = "PS2003" # unbounded integers (int) are not allowed.
|
|
101
|
+
PS2004 = "PS2004" # bytes/str/list/tuple/dict/set in a program.
|
|
102
|
+
PS2005 = "PS2005" # unsupported assignment target.
|
|
103
|
+
PS2006 = "PS2006" # TypeError / unexpected operand.
|
|
104
|
+
PS2007 = "PS2007" # int width mismatch/overflow on constant expression.
|
|
105
|
+
PS2008 = "PS2008" # unsupported decorator target.
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Semantic / account-model problems (PS3xxx)
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
PS3000 = "PS3000" # aliasing refusal (mutable data used for a live aliasing reference).
|
|
111
|
+
PS3001 = "PS3001" # unsupported constraint combination.
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Toolchain problems (PS40xx) — see toolchain.py for exact text.
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
PS4010 = "PS4010"
|
|
117
|
+
|
|
118
|
+
# Model/elaboration problems (PS41xx) — raised by model.py when a type-checked
|
|
119
|
+
# program still contains something the emitter model cannot represent.
|
|
120
|
+
PS4001 = "PS4001" # unsupported construct in the emitter model
|
|
121
|
+
|
|
122
|
+
# Emitter problems (PS5xxx) — raised by emit.py while lowering an AST body.
|
|
123
|
+
PS5001 = "PS5001" # unsupported statement in an instruction body
|
|
124
|
+
PS5002 = "PS5002" # unsupported expression in an instruction body
|
|
125
|
+
PS5003 = "PS5003" # Init[...] without a `bump: u8` data parameter / payer signer
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Internal compiler errors (PS9xxx)
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
PS9000 = "PS9000" # internal bug: unhandled construct in emitter.
|