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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Front-end entry point — run parse → subset → resolve → typecheck.
|
|
2
|
+
|
|
3
|
+
``check_source`` is the API the CLI and tests build on. It never raises for
|
|
4
|
+
malformed *user input*: every failure becomes a diagnostic. Only genuine
|
|
5
|
+
compiler bugs escape as ``PS9000`` diagnostics (still not exceptions), which
|
|
6
|
+
is what the random-AST fuzzer relies on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
|
|
14
|
+
from pythsol.compiler import resolve, subset, typecheck
|
|
15
|
+
from pythsol.compiler.diagnostics import PS9000, Diagnostic
|
|
16
|
+
from pythsol.compiler.modules import load_program
|
|
17
|
+
|
|
18
|
+
__all__ = ["check_source", "check_tree"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_source(
|
|
22
|
+
source: str,
|
|
23
|
+
file: str = "<program>",
|
|
24
|
+
*,
|
|
25
|
+
modules: Mapping[str, str] | None = None,
|
|
26
|
+
module_files: Mapping[str, str] | None = None,
|
|
27
|
+
) -> list[Diagnostic]:
|
|
28
|
+
"""Check a program from text; returns all diagnostics.
|
|
29
|
+
|
|
30
|
+
``modules`` maps a sibling module name to source text. The compiler parses
|
|
31
|
+
those strings. It does not import or execute them. Omit ``modules`` for a
|
|
32
|
+
single file; a foreign import is then rejected exactly as before.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
program, diagnostics = load_program(source, file, modules, module_files)
|
|
36
|
+
except Exception as err:
|
|
37
|
+
return _internal(file, "parse", err)
|
|
38
|
+
if program is None or diagnostics:
|
|
39
|
+
return diagnostics
|
|
40
|
+
try:
|
|
41
|
+
diagnostics.extend(typecheck.check_program(program, file))
|
|
42
|
+
except Exception as err:
|
|
43
|
+
return diagnostics + _internal(file, "typecheck", err)
|
|
44
|
+
return diagnostics
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_tree(tree: ast.Module, file: str = "<program>") -> list[Diagnostic]:
|
|
48
|
+
"""Check an already-parsed module tree (parse diagnostics must be empty)."""
|
|
49
|
+
diagnostics: list[Diagnostic] = []
|
|
50
|
+
try:
|
|
51
|
+
subset_diags = subset.scan_tree(tree, file)
|
|
52
|
+
except Exception as err:
|
|
53
|
+
return _internal(file, "subset", err)
|
|
54
|
+
diagnostics.extend(subset_diags)
|
|
55
|
+
if subset_diags:
|
|
56
|
+
return diagnostics # structural errors: resolve/typecheck results are noise
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
program, resolve_diags = resolve.collect(tree, file)
|
|
60
|
+
except Exception as err:
|
|
61
|
+
return diagnostics + _internal(file, "resolve", err)
|
|
62
|
+
diagnostics.extend(resolve_diags)
|
|
63
|
+
if resolve_diags:
|
|
64
|
+
return diagnostics
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
diagnostics.extend(typecheck.check_program(program, file))
|
|
68
|
+
except Exception as err:
|
|
69
|
+
return diagnostics + _internal(file, "typecheck", err)
|
|
70
|
+
return diagnostics
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _internal(file: str, stage: str, err: BaseException) -> list[Diagnostic]:
|
|
74
|
+
"""A raised exception from a stage is a compiler bug: report it as PS9000."""
|
|
75
|
+
return [
|
|
76
|
+
Diagnostic(
|
|
77
|
+
code=PS9000,
|
|
78
|
+
message=f"internal compiler error in {stage}: {err!r}",
|
|
79
|
+
file=file,
|
|
80
|
+
line=1,
|
|
81
|
+
col=1,
|
|
82
|
+
fix="please report this at github.com/PythSol/pythsol/issues",
|
|
83
|
+
)
|
|
84
|
+
]
|
pythsol/compiler/idl.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Anchor-compatible IDL generation (legacy shape, ``spec 0.1.0``).
|
|
2
|
+
|
|
3
|
+
The JSON mirrors what the Anchor TS client accepted as legacy IDL:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"name": "counter", "version": "0.1.0", "spec": "0.1.0",
|
|
8
|
+
"metadata": {...},
|
|
9
|
+
"instructions": [
|
|
10
|
+
{"name": "increment", "discriminator": [...8], "accounts": [...], "args": [...]}
|
|
11
|
+
],
|
|
12
|
+
"accounts": [
|
|
13
|
+
{"name": "Counter", "discriminator": [...8],
|
|
14
|
+
"type": {"kind": "struct", "fields": [...]}}
|
|
15
|
+
],
|
|
16
|
+
"types": [{"name": "Status", "type": {"kind": "enum", "variants": [...]}}],
|
|
17
|
+
"errors": [{"code": 6000, "name": "Unauthorized", "msg": "..."}]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
``validate_idl`` is a dependency-free structural validator strong enough for the
|
|
22
|
+
golden tests and the build gate; full Anchor schema conformance is a Phase 5
|
|
23
|
+
welcome add-on. Output is deterministic (sorted keys).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from pythsol.compiler.model import (
|
|
32
|
+
DataParamModel,
|
|
33
|
+
FieldModel,
|
|
34
|
+
ProgramModel,
|
|
35
|
+
fixed_string_type_name,
|
|
36
|
+
option_type_name,
|
|
37
|
+
snake_case,
|
|
38
|
+
vec_type_name,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = ["build_idl", "idl_json", "validate_idl"]
|
|
42
|
+
|
|
43
|
+
_TYPE_NAMES: dict[str, str] = {
|
|
44
|
+
"u8": "u8",
|
|
45
|
+
"u16": "u16",
|
|
46
|
+
"u32": "u32",
|
|
47
|
+
"u64": "u64",
|
|
48
|
+
"u128": "u128",
|
|
49
|
+
"i8": "i8",
|
|
50
|
+
"i16": "i16",
|
|
51
|
+
"i32": "i32",
|
|
52
|
+
"i64": "i64",
|
|
53
|
+
"i128": "i128",
|
|
54
|
+
"uint8_t": "u8",
|
|
55
|
+
"uint16_t": "u16",
|
|
56
|
+
"uint32_t": "u32",
|
|
57
|
+
"uint64_t": "u64",
|
|
58
|
+
"uint128_t": "u128",
|
|
59
|
+
"int8_t": "i8",
|
|
60
|
+
"int16_t": "i16",
|
|
61
|
+
"int32_t": "i32",
|
|
62
|
+
"int64_t": "i64",
|
|
63
|
+
"int128_t": "i128",
|
|
64
|
+
"bool": "bool",
|
|
65
|
+
"SolPubkey": "publicKey",
|
|
66
|
+
"Pubkey": "publicKey",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_VERSION = "0.1.0"
|
|
70
|
+
_IDL_SPEC = "0.1.0"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _stored_fields(model: ProgramModel) -> list[FieldModel]:
|
|
74
|
+
"""Account fields and ``@struct`` fields. Options and strings live in both."""
|
|
75
|
+
found: list[FieldModel] = []
|
|
76
|
+
for account in model.accounts:
|
|
77
|
+
found.extend(account.fields)
|
|
78
|
+
for struct in model.structs:
|
|
79
|
+
found.extend(struct.fields)
|
|
80
|
+
return found
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _idl_scalar(c_type: str, defined: str) -> str | dict[str, Any]:
|
|
84
|
+
if defined:
|
|
85
|
+
return {"defined": defined}
|
|
86
|
+
return _TYPE_NAMES[c_type]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _idl_type(field: FieldModel) -> str | dict[str, Any]:
|
|
90
|
+
"""Anchor legacy type for one field (scalar, array, fixed option, or fixed string)."""
|
|
91
|
+
if field.shape == "struct":
|
|
92
|
+
return {"defined": field.defined}
|
|
93
|
+
if field.shape == "option":
|
|
94
|
+
return {"defined": option_type_name(field.c_type, field.defined)}
|
|
95
|
+
if field.shape == "string":
|
|
96
|
+
return {"defined": fixed_string_type_name(field.length)}
|
|
97
|
+
if field.shape == "vec":
|
|
98
|
+
return {"defined": vec_type_name(field.c_type, field.length, field.defined)}
|
|
99
|
+
base = _idl_scalar(field.c_type, field.defined)
|
|
100
|
+
if field.shape == "array":
|
|
101
|
+
return {"array": [base, field.length]}
|
|
102
|
+
return base
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _idl_account(param: Any) -> dict[str, Any]:
|
|
106
|
+
"""One instruction account. Constraint keys are omitted when unused."""
|
|
107
|
+
entry: dict[str, Any] = {
|
|
108
|
+
"name": param.name,
|
|
109
|
+
"isMut": param.writable,
|
|
110
|
+
"isSigner": param.kind == "signer",
|
|
111
|
+
"docs": [],
|
|
112
|
+
}
|
|
113
|
+
constraints: list[dict[str, str]] = []
|
|
114
|
+
if param.seed:
|
|
115
|
+
constraints.append({"kind": "seeds", "seed": param.seed})
|
|
116
|
+
if param.has_one:
|
|
117
|
+
constraints.append({"kind": "has_one", "field": param.has_one})
|
|
118
|
+
entry["relations"] = [param.has_one]
|
|
119
|
+
if param.owner_of:
|
|
120
|
+
constraints.append({"kind": "owner", "account": param.owner_of})
|
|
121
|
+
if param.address_of:
|
|
122
|
+
constraints.append({"kind": "address", "pubkey": param.address_of})
|
|
123
|
+
if param.close_to:
|
|
124
|
+
constraints.append({"kind": "close", "account": param.close_to})
|
|
125
|
+
if constraints:
|
|
126
|
+
entry["constraints"] = constraints
|
|
127
|
+
return entry
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _idl_arg_type(param: DataParamModel) -> str | dict[str, Any]:
|
|
131
|
+
if param.shape == "option":
|
|
132
|
+
return {"defined": option_type_name(param.c_type, param.defined)}
|
|
133
|
+
if param.shape == "string":
|
|
134
|
+
return {"defined": fixed_string_type_name(param.length)}
|
|
135
|
+
return _idl_scalar(param.c_type, param.defined)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _user_structs(model: ProgramModel) -> list[dict[str, Any]]:
|
|
139
|
+
"""``@struct`` values, in declaration order, as Anchor struct types."""
|
|
140
|
+
out: list[dict[str, Any]] = []
|
|
141
|
+
for struct in model.structs:
|
|
142
|
+
out.append(
|
|
143
|
+
{
|
|
144
|
+
"name": struct.name,
|
|
145
|
+
"type": {
|
|
146
|
+
"kind": "struct",
|
|
147
|
+
"fields": [
|
|
148
|
+
{"name": field.name, "type": _idl_type(field)}
|
|
149
|
+
for field in struct.fields
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _option_structs(model: ProgramModel) -> list[dict[str, Any]]:
|
|
158
|
+
"""One defined struct per distinct fixed-width option (tag byte + reserved value)."""
|
|
159
|
+
found: list[tuple[str, str, str]] = []
|
|
160
|
+
seen: set[str] = set()
|
|
161
|
+
|
|
162
|
+
def add(c_type: str, defined: str) -> None:
|
|
163
|
+
name = option_type_name(c_type, defined)
|
|
164
|
+
if name in seen:
|
|
165
|
+
return
|
|
166
|
+
seen.add(name)
|
|
167
|
+
found.append((name, c_type, defined))
|
|
168
|
+
|
|
169
|
+
for field in _stored_fields(model):
|
|
170
|
+
if field.shape == "option":
|
|
171
|
+
add(field.c_type, field.defined)
|
|
172
|
+
for instr in model.instructions:
|
|
173
|
+
for param in instr.data_params:
|
|
174
|
+
if param.shape == "option":
|
|
175
|
+
add(param.c_type, param.defined)
|
|
176
|
+
structs: list[dict[str, Any]] = []
|
|
177
|
+
for name, c_type, defined in found:
|
|
178
|
+
structs.append(
|
|
179
|
+
{
|
|
180
|
+
"name": name,
|
|
181
|
+
"type": {
|
|
182
|
+
"kind": "struct",
|
|
183
|
+
"fields": [
|
|
184
|
+
{"name": "tag", "type": "u8"},
|
|
185
|
+
{"name": "value", "type": _idl_scalar(c_type, defined)},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
return structs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _string_structs(model: ProgramModel) -> list[dict[str, Any]]:
|
|
194
|
+
"""One defined struct per distinct ``FixedString[N]`` (u32 length + N bytes)."""
|
|
195
|
+
found: list[int] = []
|
|
196
|
+
seen: set[int] = set()
|
|
197
|
+
|
|
198
|
+
def add(length: int) -> None:
|
|
199
|
+
if length in seen:
|
|
200
|
+
return
|
|
201
|
+
seen.add(length)
|
|
202
|
+
found.append(length)
|
|
203
|
+
|
|
204
|
+
for field in _stored_fields(model):
|
|
205
|
+
if field.shape == "string":
|
|
206
|
+
add(field.length)
|
|
207
|
+
for instr in model.instructions:
|
|
208
|
+
for param in instr.data_params:
|
|
209
|
+
if param.shape == "string":
|
|
210
|
+
add(param.length)
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
"name": fixed_string_type_name(length),
|
|
214
|
+
"type": {
|
|
215
|
+
"kind": "struct",
|
|
216
|
+
"fields": [
|
|
217
|
+
{"name": "len", "type": "u32"},
|
|
218
|
+
{"name": "data", "type": {"array": ["u8", length]}},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
for length in found
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _vec_structs(model: ProgramModel) -> list[dict[str, Any]]:
|
|
227
|
+
"""One defined struct per distinct ``Vec[T, N]`` (u32 length plus N elements)."""
|
|
228
|
+
found: list[tuple[str, str, str, int]] = []
|
|
229
|
+
seen: set[str] = set()
|
|
230
|
+
|
|
231
|
+
def add(c_type: str, defined: str, length: int) -> None:
|
|
232
|
+
name = vec_type_name(c_type, length, defined)
|
|
233
|
+
if name in seen:
|
|
234
|
+
return
|
|
235
|
+
seen.add(name)
|
|
236
|
+
found.append((name, c_type, defined, length))
|
|
237
|
+
|
|
238
|
+
for field in _stored_fields(model):
|
|
239
|
+
if field.shape == "vec":
|
|
240
|
+
add(field.c_type, field.defined, field.length)
|
|
241
|
+
structs: list[dict[str, Any]] = []
|
|
242
|
+
for name, c_type, defined, length in found:
|
|
243
|
+
inner = _idl_scalar(c_type, defined)
|
|
244
|
+
structs.append(
|
|
245
|
+
{
|
|
246
|
+
"name": name,
|
|
247
|
+
"type": {
|
|
248
|
+
"kind": "struct",
|
|
249
|
+
"fields": [
|
|
250
|
+
{"name": "len", "type": "u32"},
|
|
251
|
+
{"name": "data", "type": {"array": [inner, length]}},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
return structs
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def build_idl(model: ProgramModel, address: str | None = None) -> dict[str, Any]:
|
|
260
|
+
"""Build the legacy-IDL dictionary for ``model`` (deterministic)."""
|
|
261
|
+
instructions = []
|
|
262
|
+
cpi: dict[str, dict[str, bool]] = {}
|
|
263
|
+
for instr in model.instructions:
|
|
264
|
+
accounts = [_idl_account(param) for param in instr.account_params]
|
|
265
|
+
args = [
|
|
266
|
+
{"name": param.name, "type": _idl_arg_type(param)} for param in instr.data_params
|
|
267
|
+
]
|
|
268
|
+
pays = any(param.is_init for param in instr.account_params)
|
|
269
|
+
cpi[instr.snake_name] = {
|
|
270
|
+
"ata": instr.needs_ata,
|
|
271
|
+
"pays": pays,
|
|
272
|
+
"system": pays or instr.needs_system,
|
|
273
|
+
"token": instr.needs_token,
|
|
274
|
+
"token2022": instr.needs_token2022,
|
|
275
|
+
}
|
|
276
|
+
instructions.append(
|
|
277
|
+
{
|
|
278
|
+
"name": instr.snake_name,
|
|
279
|
+
"discriminator": list(instr.discriminator),
|
|
280
|
+
"accounts": accounts,
|
|
281
|
+
"args": args,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
account_types = []
|
|
286
|
+
for account in model.accounts:
|
|
287
|
+
fields = [{"name": f.name, "type": _idl_type(f)} for f in account.fields]
|
|
288
|
+
account_types.append(
|
|
289
|
+
{
|
|
290
|
+
"name": account.name,
|
|
291
|
+
"discriminator": list(account.discriminator),
|
|
292
|
+
"type": {"kind": "struct", "fields": fields},
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
errors = [{"code": err.code, "name": err.name, "msg": err.name} for err in model.errors]
|
|
297
|
+
events = [
|
|
298
|
+
{
|
|
299
|
+
"name": event.name,
|
|
300
|
+
"discriminator": list(event.discriminator),
|
|
301
|
+
"fields": [
|
|
302
|
+
{"name": field.name, "type": _idl_type(field), "index": False}
|
|
303
|
+
for field in event.fields
|
|
304
|
+
],
|
|
305
|
+
}
|
|
306
|
+
for event in model.events
|
|
307
|
+
]
|
|
308
|
+
types = [
|
|
309
|
+
{
|
|
310
|
+
"name": enum.name,
|
|
311
|
+
"type": {
|
|
312
|
+
"kind": "enum",
|
|
313
|
+
"variants": [
|
|
314
|
+
{"name": variant}
|
|
315
|
+
if variant_tag not in enum.payload_tags
|
|
316
|
+
else {
|
|
317
|
+
"name": variant,
|
|
318
|
+
"fields": [{"name": "value", "type": _TYPE_NAMES[enum.payload_c]}],
|
|
319
|
+
}
|
|
320
|
+
for variant, variant_tag in enum.variants
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
for enum in model.enums
|
|
325
|
+
]
|
|
326
|
+
types.extend(_user_structs(model))
|
|
327
|
+
types.extend(_option_structs(model))
|
|
328
|
+
types.extend(_string_structs(model))
|
|
329
|
+
types.extend(_vec_structs(model))
|
|
330
|
+
|
|
331
|
+
idl: dict[str, Any] = {
|
|
332
|
+
"version": _VERSION,
|
|
333
|
+
# Top-level spec mirrors Anchor's legacy IDL shape (0.1.0); the
|
|
334
|
+
# metadata copy is for clients that only read `metadata`.
|
|
335
|
+
"spec": _IDL_SPEC,
|
|
336
|
+
"name": snake_case(model.name),
|
|
337
|
+
"metadata": {
|
|
338
|
+
"name": snake_case(model.name),
|
|
339
|
+
"version": _VERSION,
|
|
340
|
+
"spec": "0.1.0",
|
|
341
|
+
"description": f"{model.name} program, generated by PythSol",
|
|
342
|
+
# Which programs the client must append. Account metas stay free of
|
|
343
|
+
# this: unconstrained accounts keep the same keys as before.
|
|
344
|
+
"pythsol": {"cpi": cpi},
|
|
345
|
+
},
|
|
346
|
+
"instructions": instructions,
|
|
347
|
+
"accounts": account_types,
|
|
348
|
+
"types": types,
|
|
349
|
+
"events": events,
|
|
350
|
+
"errors": errors,
|
|
351
|
+
}
|
|
352
|
+
if address is not None:
|
|
353
|
+
idl["address"] = address
|
|
354
|
+
return idl
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def idl_json(model: ProgramModel, address: str | None = None) -> str:
|
|
358
|
+
"""Deterministic JSON text (sorted keys, 2-space indent, trailing newline)."""
|
|
359
|
+
return json.dumps(build_idl(model, address), indent=2, sort_keys=True) + "\n"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def validate_idl(idl: dict[str, Any]) -> list[str]:
|
|
363
|
+
"""Structural validation; the list is empty when the IDL is well-formed."""
|
|
364
|
+
problems: list[str] = []
|
|
365
|
+
for key in ("name", "version", "instructions"):
|
|
366
|
+
if key not in idl:
|
|
367
|
+
problems.append(f"missing required key `{key}`")
|
|
368
|
+
if not isinstance(idl.get("name"), str) or not idl.get("name"):
|
|
369
|
+
problems.append("`name` must be a non-empty string")
|
|
370
|
+
if not isinstance(idl.get("instructions"), list):
|
|
371
|
+
problems.append("`instructions` must be a list")
|
|
372
|
+
return problems
|
|
373
|
+
seen: set[bytes] = set()
|
|
374
|
+
for instr in idl["instructions"]:
|
|
375
|
+
if "name" not in instr or "discriminator" not in instr:
|
|
376
|
+
problems.append("instruction missing name/discriminator")
|
|
377
|
+
continue
|
|
378
|
+
disc = instr.get("discriminator")
|
|
379
|
+
if not isinstance(disc, list) or len(disc) != 8:
|
|
380
|
+
problems.append(f"instruction `{instr.get('name')}`: discriminator must be 8 bytes")
|
|
381
|
+
continue
|
|
382
|
+
key_bytes = bytes(element & 0xFF for element in disc)
|
|
383
|
+
if key_bytes in seen:
|
|
384
|
+
problems.append(f"instruction `{instr.get('name')}`: duplicate discriminator")
|
|
385
|
+
seen.add(key_bytes)
|
|
386
|
+
for account in instr.get("accounts", []):
|
|
387
|
+
if "name" not in account:
|
|
388
|
+
problems.append(f"instruction `{instr.get('name')}`: account without name")
|
|
389
|
+
for arg in instr.get("args", []):
|
|
390
|
+
if "name" not in arg or "type" not in arg:
|
|
391
|
+
problems.append(f"instruction `{instr.get('name')}`: arg missing name/type")
|
|
392
|
+
for account in idl.get("accounts", []):
|
|
393
|
+
if "name" not in account:
|
|
394
|
+
problems.append("account without name")
|
|
395
|
+
t = account.get("type", {})
|
|
396
|
+
if t.get("kind") != "struct" or not isinstance(t.get("fields"), list):
|
|
397
|
+
problems.append(f"account `{account.get('name')}`: type must be a struct with fields")
|
|
398
|
+
for err in idl.get("errors", []):
|
|
399
|
+
if "code" not in err:
|
|
400
|
+
problems.append("error without code")
|
|
401
|
+
declared = {
|
|
402
|
+
entry.get("name")
|
|
403
|
+
for entry in idl.get("types", [])
|
|
404
|
+
if isinstance(entry, dict) and isinstance(entry.get("name"), str)
|
|
405
|
+
}
|
|
406
|
+
seen_types: set[str] = set()
|
|
407
|
+
for entry in idl.get("types", []):
|
|
408
|
+
if not isinstance(entry, dict) or not entry.get("name"):
|
|
409
|
+
problems.append("type without name")
|
|
410
|
+
continue
|
|
411
|
+
type_name = entry.get("name")
|
|
412
|
+
if isinstance(type_name, str):
|
|
413
|
+
if type_name in seen_types:
|
|
414
|
+
problems.append(f"duplicate type `{type_name}`")
|
|
415
|
+
seen_types.add(type_name)
|
|
416
|
+
body = entry.get("type", {})
|
|
417
|
+
if body.get("kind") == "enum" and not isinstance(body.get("variants"), list):
|
|
418
|
+
problems.append(f"type `{entry.get('name')}`: enum needs variants")
|
|
419
|
+
if body.get("kind") == "struct" and isinstance(body.get("fields"), list):
|
|
420
|
+
for field in body["fields"]:
|
|
421
|
+
if isinstance(field, dict):
|
|
422
|
+
_check_defined(
|
|
423
|
+
field.get("type"),
|
|
424
|
+
declared,
|
|
425
|
+
f"type `{entry.get('name')}`",
|
|
426
|
+
problems,
|
|
427
|
+
)
|
|
428
|
+
for account in idl.get("accounts", []):
|
|
429
|
+
fields = account.get("type", {}).get("fields", [])
|
|
430
|
+
if isinstance(fields, list):
|
|
431
|
+
for field in fields:
|
|
432
|
+
_check_defined(
|
|
433
|
+
field.get("type"),
|
|
434
|
+
declared,
|
|
435
|
+
f"account `{account.get('name')}`",
|
|
436
|
+
problems,
|
|
437
|
+
)
|
|
438
|
+
for instr in idl.get("instructions", []):
|
|
439
|
+
if not isinstance(instr, dict):
|
|
440
|
+
continue
|
|
441
|
+
for arg in instr.get("args", []):
|
|
442
|
+
if isinstance(arg, dict):
|
|
443
|
+
_check_defined(
|
|
444
|
+
arg.get("type"),
|
|
445
|
+
declared,
|
|
446
|
+
f"instruction `{instr.get('name')}`",
|
|
447
|
+
problems,
|
|
448
|
+
)
|
|
449
|
+
return problems
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _check_defined(
|
|
453
|
+
ty: object,
|
|
454
|
+
declared: set[object],
|
|
455
|
+
where: str,
|
|
456
|
+
problems: list[str],
|
|
457
|
+
) -> None:
|
|
458
|
+
if not isinstance(ty, dict):
|
|
459
|
+
return
|
|
460
|
+
name = ty.get("defined")
|
|
461
|
+
if isinstance(name, str) and name not in declared:
|
|
462
|
+
problems.append(f"{where}: unknown defined type `{name}`")
|
|
463
|
+
array = ty.get("array")
|
|
464
|
+
if isinstance(array, list) and array:
|
|
465
|
+
_check_defined(array[0], declared, where, problems)
|