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,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
+ ]
@@ -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)