sutra-dev 0.2.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.
- sutra_compiler/__init__.py +49 -0
- sutra_compiler/__main__.py +514 -0
- sutra_compiler/ast_nodes.py +553 -0
- sutra_compiler/codegen.py +1811 -0
- sutra_compiler/codegen_base.py +2436 -0
- sutra_compiler/codegen_pytorch.py +1472 -0
- sutra_compiler/diagnostics.py +145 -0
- sutra_compiler/inliner.py +581 -0
- sutra_compiler/lexer.py +821 -0
- sutra_compiler/parser.py +2112 -0
- sutra_compiler/review.py +322 -0
- sutra_compiler/simplify.py +1046 -0
- sutra_compiler/simplify_egglog.py +674 -0
- sutra_compiler/stdlib/axons.su +53 -0
- sutra_compiler/stdlib/embed.su +48 -0
- sutra_compiler/stdlib/javascript_object.su +18 -0
- sutra_compiler/stdlib/logic.su +202 -0
- sutra_compiler/stdlib/math.su +12 -0
- sutra_compiler/stdlib/memory.su +82 -0
- sutra_compiler/stdlib/numbers.su +99 -0
- sutra_compiler/stdlib/rotation.su +83 -0
- sutra_compiler/stdlib/similarity.su +97 -0
- sutra_compiler/stdlib/strings.su +56 -0
- sutra_compiler/stdlib/tensor.su +82 -0
- sutra_compiler/stdlib/vectors.su +119 -0
- sutra_compiler/stdlib_loader.py +219 -0
- sutra_compiler/sutradb_embedded.py +273 -0
- sutra_compiler/trace.py +135 -0
- sutra_compiler/validator.py +552 -0
- sutra_compiler/workspace.py +655 -0
- sutra_dev-0.2.0.dist-info/METADATA +80 -0
- sutra_dev-0.2.0.dist-info/RECORD +36 -0
- sutra_dev-0.2.0.dist-info/WHEEL +5 -0
- sutra_dev-0.2.0.dist-info/entry_points.txt +2 -0
- sutra_dev-0.2.0.dist-info/licenses/LICENSE +201 -0
- sutra_dev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""High-level validator for Sutra source.
|
|
2
|
+
|
|
3
|
+
The validator runs after the parser. It walks the AST and emits
|
|
4
|
+
diagnostics for rules that the syntax-decisions document calls out as
|
|
5
|
+
errors but that aren't enforced by the pure parser.
|
|
6
|
+
|
|
7
|
+
Rules implemented in v0.1:
|
|
8
|
+
|
|
9
|
+
- SUT0103: `var TYPE x` — `var` combined with an explicit type. (The
|
|
10
|
+
parser already emits this; the validator doesn't need to re-check.)
|
|
11
|
+
- SUT0110: `|>` pipe-forward operator is not supported in Sutra.
|
|
12
|
+
- SUT0111: `(vector) "string"` (or any primitive-cast applied to a
|
|
13
|
+
string literal) — per the spec, string→vector must go through
|
|
14
|
+
`embed(...)`, not a cast.
|
|
15
|
+
- SUT0112: modifiers combined in disallowed ways (e.g. both `public`
|
|
16
|
+
and `private`).
|
|
17
|
+
- SUT0113: naming drift — the file uses class names in inconsistent
|
|
18
|
+
casing (a warning, not an error, because both are currently
|
|
19
|
+
accepted in the example code).
|
|
20
|
+
|
|
21
|
+
v0.1 deliberately does NOT do:
|
|
22
|
+
|
|
23
|
+
- Type checking across declarations
|
|
24
|
+
- Name resolution (unknown identifiers)
|
|
25
|
+
- Arity checking on calls
|
|
26
|
+
- Return-statement coverage
|
|
27
|
+
|
|
28
|
+
Those land in v0.2+ once we have a symbol table and cross-module
|
|
29
|
+
resolution.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import List, Optional, Set
|
|
35
|
+
|
|
36
|
+
from . import ast_nodes as ast
|
|
37
|
+
from .diagnostics import (
|
|
38
|
+
DiagnosticBag,
|
|
39
|
+
DiagnosticLevel,
|
|
40
|
+
SourcePosition,
|
|
41
|
+
SourceSpan,
|
|
42
|
+
)
|
|
43
|
+
from .lexer import Lexer, TokenKind
|
|
44
|
+
from .parser import Parser
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _fuzzy_literal_constant(expr: ast.Expr) -> Optional[float]:
|
|
48
|
+
"""Fold a literal (possibly with unary +/-) to a single scalar.
|
|
49
|
+
|
|
50
|
+
Returns None for anything needing runtime evaluation. Used to
|
|
51
|
+
range-check fuzzy-typed literal initializers.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(expr, ast.FloatLiteral):
|
|
54
|
+
return float(expr.value)
|
|
55
|
+
if isinstance(expr, ast.IntLiteral):
|
|
56
|
+
return float(expr.value)
|
|
57
|
+
if isinstance(expr, ast.BoolLiteral):
|
|
58
|
+
return 1.0 if expr.value else -1.0
|
|
59
|
+
if isinstance(expr, ast.UnknownLiteral):
|
|
60
|
+
return 0.0
|
|
61
|
+
if isinstance(expr, ast.UnaryOp) and expr.op in ("-", "+"):
|
|
62
|
+
inner = _fuzzy_literal_constant(expr.operand)
|
|
63
|
+
if inner is None:
|
|
64
|
+
return None
|
|
65
|
+
return -inner if expr.op == "-" else inner
|
|
66
|
+
if isinstance(expr, ast.Parenthesized):
|
|
67
|
+
return _fuzzy_literal_constant(expr.inner)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ============================================================
|
|
72
|
+
# Public entry points
|
|
73
|
+
# ============================================================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_source(
|
|
77
|
+
source: str,
|
|
78
|
+
*,
|
|
79
|
+
file: Optional[str] = None,
|
|
80
|
+
) -> DiagnosticBag:
|
|
81
|
+
"""Lex, parse, and validate a string of Sutra source."""
|
|
82
|
+
lexer = Lexer(source, file=file)
|
|
83
|
+
tokens = lexer.tokenize()
|
|
84
|
+
bag = lexer.diagnostics
|
|
85
|
+
parser = Parser(tokens, file=file, diagnostics=bag)
|
|
86
|
+
module = parser.parse_module()
|
|
87
|
+
_check_pipe_forward(tokens, bag)
|
|
88
|
+
_Walker(bag).visit_module(module)
|
|
89
|
+
return bag
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_file(path: str) -> DiagnosticBag:
|
|
93
|
+
"""Validate a file on disk."""
|
|
94
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
95
|
+
source = f.read()
|
|
96
|
+
return validate_source(source, file=path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ============================================================
|
|
100
|
+
# Token-level checks (before AST walk)
|
|
101
|
+
# ============================================================
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _check_pipe_forward(tokens, bag: DiagnosticBag) -> None:
|
|
105
|
+
"""Flag any `|>` pipe-forward tokens.
|
|
106
|
+
|
|
107
|
+
The spec is explicit: Sutra does not have a pipe operator; use
|
|
108
|
+
nested calls or method chaining instead.
|
|
109
|
+
"""
|
|
110
|
+
for tok in tokens:
|
|
111
|
+
if tok.kind is TokenKind.PIPE_FORWARD:
|
|
112
|
+
bag.error(
|
|
113
|
+
"the `|>` pipe-forward operator is not supported in Sutra",
|
|
114
|
+
tok.span,
|
|
115
|
+
code="SUT0110",
|
|
116
|
+
hint="use nested calls (`Normalize(Blend(a, b))`) or method chaining (`a.Blend(b).Normalize()`)",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================
|
|
121
|
+
# AST walker
|
|
122
|
+
# ============================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _Walker:
|
|
126
|
+
"""AST walker that runs validator rules.
|
|
127
|
+
|
|
128
|
+
Each visit_X method handles one node type. Unhandled nodes fall
|
|
129
|
+
back to a generic child-walk so we always reach every expression.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, diagnostics: DiagnosticBag) -> None:
|
|
133
|
+
self.diagnostics = diagnostics
|
|
134
|
+
self._class_name_usages: Set[str] = set()
|
|
135
|
+
# User-declared classes — name → parent_name. Populated by
|
|
136
|
+
# visit_ClassDecl. Used to walk inheritance chains and to
|
|
137
|
+
# check that user-defined types in type positions actually
|
|
138
|
+
# resolve to a declared class.
|
|
139
|
+
self._class_decls: dict = {}
|
|
140
|
+
# `wait`-declared variables in the *current* function scope.
|
|
141
|
+
# Maps name → declaration span, populated when a `var x = wait;`
|
|
142
|
+
# is seen. Cleared on function entry so wait-tracking is
|
|
143
|
+
# function-local. When the function body finishes, any name
|
|
144
|
+
# still in this dict has never been assigned and gets a
|
|
145
|
+
# SUT0130 error.
|
|
146
|
+
self._wait_declared: dict = {}
|
|
147
|
+
# Names declared at file scope (top-level FunctionDecl,
|
|
148
|
+
# MethodDecl, VarDecl, ClassDecl, LoopFunctionDecl). Populated
|
|
149
|
+
# in visit_module before the per-item walk. Used by the object-
|
|
150
|
+
# encapsulation rule (SUT0144): object methods cannot read
|
|
151
|
+
# file-scope names — see planning/open-questions/
|
|
152
|
+
# function-taxonomy-and-closure.md.
|
|
153
|
+
self._file_scope_names: Set[str] = set()
|
|
154
|
+
# When inside a method body, the set of names locally in
|
|
155
|
+
# scope (params + body var decls). None when not in a method.
|
|
156
|
+
# The encapsulation rule fires on any Identifier whose name is
|
|
157
|
+
# in _file_scope_names AND not in _method_local_names while
|
|
158
|
+
# _method_local_names is non-None.
|
|
159
|
+
self._method_local_names: Optional[Set[str]] = None
|
|
160
|
+
|
|
161
|
+
# ---- module ----------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def visit_module(self, module: ast.Module) -> None:
|
|
164
|
+
# Pre-pass: collect every top-level declaration's name into the
|
|
165
|
+
# file-scope set, EXCEPT class names. Class names are
|
|
166
|
+
# namespace anchors — `Math.log(x)` from inside a method is
|
|
167
|
+
# legitimate access through the class boundary, not a
|
|
168
|
+
# file-scope read. The encapsulation rule (SUT0144) fires on
|
|
169
|
+
# bare references to file-scope free functions, top-level vars,
|
|
170
|
+
# top-level methods, and top-level loop functions.
|
|
171
|
+
for item in module.items:
|
|
172
|
+
if isinstance(item, ast.ClassDecl):
|
|
173
|
+
continue
|
|
174
|
+
name = getattr(item, "name", None)
|
|
175
|
+
if isinstance(name, str) and name:
|
|
176
|
+
self._file_scope_names.add(name)
|
|
177
|
+
for item in module.items:
|
|
178
|
+
self.visit(item)
|
|
179
|
+
self._check_class_casing_drift()
|
|
180
|
+
|
|
181
|
+
# ---- dispatch --------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def visit(self, node) -> None:
|
|
184
|
+
method_name = f"visit_{type(node).__name__}"
|
|
185
|
+
method = getattr(self, method_name, None)
|
|
186
|
+
if method is not None:
|
|
187
|
+
method(node)
|
|
188
|
+
else:
|
|
189
|
+
self._walk_children(node)
|
|
190
|
+
|
|
191
|
+
def _walk_children(self, node) -> None:
|
|
192
|
+
# Generic walk: visit every field that's an AST node or a list
|
|
193
|
+
# of AST nodes. This is enough for the v0.1 validator; richer
|
|
194
|
+
# traversal can come later.
|
|
195
|
+
for attr in vars(node).values():
|
|
196
|
+
if isinstance(attr, (ast.Node, ast.Module)):
|
|
197
|
+
self.visit(attr)
|
|
198
|
+
elif isinstance(attr, list):
|
|
199
|
+
for item in attr:
|
|
200
|
+
if isinstance(item, ast.Node):
|
|
201
|
+
self.visit(item)
|
|
202
|
+
|
|
203
|
+
# ---- declarations ----------------------------------------------
|
|
204
|
+
|
|
205
|
+
def visit_ClassDecl(self, node: ast.ClassDecl) -> None:
|
|
206
|
+
# Detect duplicate class declarations.
|
|
207
|
+
if node.name in self._class_decls:
|
|
208
|
+
self.diagnostics.error(
|
|
209
|
+
f"class `{node.name}` is already declared in this module",
|
|
210
|
+
node.span,
|
|
211
|
+
code="SUT0141",
|
|
212
|
+
)
|
|
213
|
+
# Still walk methods so any in-method diagnostics fire.
|
|
214
|
+
for m in node.methods:
|
|
215
|
+
self.visit(m)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# Walk the would-be inheritance chain to verify it bottoms
|
|
219
|
+
# out at a primitive. The parent must be either a primitive
|
|
220
|
+
# type name or a previously-declared user class. Forward
|
|
221
|
+
# references aren't supported in MVP — declarations are
|
|
222
|
+
# expected to be in dependency order.
|
|
223
|
+
from .lexer import PRIMITIVE_TYPE_NAMES
|
|
224
|
+
|
|
225
|
+
parent = node.parent_name
|
|
226
|
+
if parent in PRIMITIVE_TYPE_NAMES:
|
|
227
|
+
self._class_decls[node.name] = parent
|
|
228
|
+
elif parent not in self._class_decls:
|
|
229
|
+
self.diagnostics.error(
|
|
230
|
+
f"class `{node.name}` extends `{parent}`, which is not a "
|
|
231
|
+
"primitive type and has not been declared earlier in this "
|
|
232
|
+
"module. The MVP class system requires the extends-chain "
|
|
233
|
+
"to bottom out at a primitive (vector / int / float / "
|
|
234
|
+
"fuzzy / etc.) and does not support forward references",
|
|
235
|
+
node.span,
|
|
236
|
+
code="SUT0142",
|
|
237
|
+
hint=f"declare `class {parent} extends <something> {{ }}` "
|
|
238
|
+
"before this declaration, or change `extends` to a "
|
|
239
|
+
"primitive type name",
|
|
240
|
+
)
|
|
241
|
+
# Still register the class so downstream references don't
|
|
242
|
+
# double-error. Mark with a sentinel so we know the chain
|
|
243
|
+
# is broken.
|
|
244
|
+
self._class_decls[node.name] = parent
|
|
245
|
+
else:
|
|
246
|
+
# Walk transitively to confirm the chain ultimately reaches
|
|
247
|
+
# a primitive (it should, by induction, but a malformed
|
|
248
|
+
# earlier decl can poison the chain — we already errored on
|
|
249
|
+
# it, so just treat this one as OK for downstream usage).
|
|
250
|
+
self._class_decls[node.name] = parent
|
|
251
|
+
|
|
252
|
+
# Walk methods declared inside the class body. Each is
|
|
253
|
+
# validator-visited via the existing visit_MethodDecl, which
|
|
254
|
+
# enforces the encapsulation rule (SUT0144) on the body.
|
|
255
|
+
for m in node.methods:
|
|
256
|
+
self.visit(m)
|
|
257
|
+
# Walk loop function declarations declared inside the class
|
|
258
|
+
# body (object loops). Same visitor path as top-level loop
|
|
259
|
+
# function decls.
|
|
260
|
+
for lf in node.loop_functions:
|
|
261
|
+
self.visit(lf)
|
|
262
|
+
|
|
263
|
+
def visit_FunctionDecl(self, node: ast.FunctionDecl) -> None:
|
|
264
|
+
self._check_modifier_conflict(node.modifiers, node.span)
|
|
265
|
+
self._record_type_usage(node.return_type)
|
|
266
|
+
for p in node.params:
|
|
267
|
+
self._record_type_usage(p.type_ref)
|
|
268
|
+
self._enter_function_scope()
|
|
269
|
+
self.visit(node.body)
|
|
270
|
+
self._exit_function_scope()
|
|
271
|
+
|
|
272
|
+
def visit_MethodDecl(self, node: ast.MethodDecl) -> None:
|
|
273
|
+
self._check_modifier_conflict(node.modifiers, node.span)
|
|
274
|
+
self._record_type_usage(node.return_type)
|
|
275
|
+
for p in node.params:
|
|
276
|
+
self._record_type_usage(p.type_ref)
|
|
277
|
+
self._enter_function_scope()
|
|
278
|
+
# Encapsulation rule (SUT0144): walking the method body, any
|
|
279
|
+
# bare Identifier whose name is in _file_scope_names but not in
|
|
280
|
+
# _method_local_names is forbidden. Seed the local set with the
|
|
281
|
+
# method's params; visit_VarDecl extends it as `var x = ...;`
|
|
282
|
+
# decls are seen inside the body.
|
|
283
|
+
saved_method_scope = self._method_local_names
|
|
284
|
+
self._method_local_names = {p.name for p in node.params}
|
|
285
|
+
try:
|
|
286
|
+
self.visit(node.body)
|
|
287
|
+
finally:
|
|
288
|
+
self._method_local_names = saved_method_scope
|
|
289
|
+
self._exit_function_scope()
|
|
290
|
+
|
|
291
|
+
def visit_VarDecl(self, node: ast.VarDecl) -> None:
|
|
292
|
+
if node.type_ref is not None:
|
|
293
|
+
self._record_type_usage(node.type_ref)
|
|
294
|
+
# `wait`-initialized declarations: register the name as a
|
|
295
|
+
# pending wait (assigned-later promise) and do NOT descend into
|
|
296
|
+
# the initializer — `WaitLiteral` is legal here, illegal
|
|
297
|
+
# everywhere else, and the position check below catches the
|
|
298
|
+
# everywhere-else case.
|
|
299
|
+
if isinstance(node.initializer, ast.WaitLiteral):
|
|
300
|
+
if self._method_local_names is not None:
|
|
301
|
+
self._method_local_names.add(node.name)
|
|
302
|
+
# Top-level `wait` has no enclosing function body to
|
|
303
|
+
# assign in — reject it. Use the wait stack as the
|
|
304
|
+
# in-function indicator (it's pushed by _enter_function_scope).
|
|
305
|
+
if not getattr(self, "_wait_stack", []):
|
|
306
|
+
self.diagnostics.error(
|
|
307
|
+
"`wait` is only valid inside a function or method "
|
|
308
|
+
"body — top-level declarations don't have a later "
|
|
309
|
+
"execution flow to assign in",
|
|
310
|
+
node.span,
|
|
311
|
+
code="SUT0133",
|
|
312
|
+
hint="move the declaration into a function body, "
|
|
313
|
+
"or initialize it with a concrete value at the "
|
|
314
|
+
"top level",
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
if node.type_ref is None:
|
|
318
|
+
# `var x = wait;` (inferred) has no type to default the
|
|
319
|
+
# zero-of-type emission to. Require an explicit type.
|
|
320
|
+
self.diagnostics.error(
|
|
321
|
+
"`var x = wait;` (inferred) is not allowed — "
|
|
322
|
+
"`wait` requires an explicit type so the compiler "
|
|
323
|
+
"knows the zero-of-type to allocate at the "
|
|
324
|
+
"declaration site",
|
|
325
|
+
node.span,
|
|
326
|
+
code="SUT0131",
|
|
327
|
+
hint="write `int x = wait;` (or another concrete type) "
|
|
328
|
+
"instead, or use `var x : TYPE;` for the same "
|
|
329
|
+
"uninitialized-slot semantics without the explicit "
|
|
330
|
+
"deferred-init signal",
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
self._wait_declared[node.name] = node.span
|
|
334
|
+
return
|
|
335
|
+
if node.initializer is not None:
|
|
336
|
+
self.visit(node.initializer)
|
|
337
|
+
# After the initializer is checked (so `var x = file_scope_name;`
|
|
338
|
+
# inside a method body still flags the file-scope read), register
|
|
339
|
+
# x as method-local so subsequent references inside the body
|
|
340
|
+
# don't trip the encapsulation rule.
|
|
341
|
+
if self._method_local_names is not None:
|
|
342
|
+
self._method_local_names.add(node.name)
|
|
343
|
+
# Fuzzy / trit literals live on the truth axis which the
|
|
344
|
+
# spec defines over [-1, +1]. A literal outside that range is
|
|
345
|
+
# almost always a mistake; warn (not error) so existing programs
|
|
346
|
+
# don't break while the rule beds in.
|
|
347
|
+
if (node.type_ref is not None
|
|
348
|
+
and node.type_ref.name in ("fuzzy", "trit")
|
|
349
|
+
and node.initializer is not None):
|
|
350
|
+
value = _fuzzy_literal_constant(node.initializer)
|
|
351
|
+
if value is not None and (value < -1.0 or value > 1.0):
|
|
352
|
+
self.diagnostics.warning(
|
|
353
|
+
f"{node.type_ref.name} literal {value!r} is outside "
|
|
354
|
+
"[-1, +1] — the truth axis saturates at ±1. "
|
|
355
|
+
"Did you mean a different type?",
|
|
356
|
+
node.span,
|
|
357
|
+
code="SUT0120",
|
|
358
|
+
hint="use a `scalar` for unbounded values, or clamp the "
|
|
359
|
+
"literal into [-1, +1]",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def visit_Param(self, node: ast.Param) -> None:
|
|
363
|
+
self._record_type_usage(node.type_ref)
|
|
364
|
+
|
|
365
|
+
def visit_Identifier(self, node: ast.Identifier) -> None:
|
|
366
|
+
# Object-encapsulation rule (SUT0144): when we are inside a
|
|
367
|
+
# method body (`_method_local_names` is non-None), any bare
|
|
368
|
+
# identifier whose name is declared at file scope but is not
|
|
369
|
+
# locally bound (param or var decl in the body) is forbidden.
|
|
370
|
+
# Object methods are encapsulated within the class boundary —
|
|
371
|
+
# static methods see the class as namespace; non-static
|
|
372
|
+
# methods see `this` only. File-scope visibility is for free
|
|
373
|
+
# functions only.
|
|
374
|
+
# See planning/open-questions/function-taxonomy-and-closure.md.
|
|
375
|
+
if self._method_local_names is None:
|
|
376
|
+
return
|
|
377
|
+
if node.name in self._method_local_names:
|
|
378
|
+
return
|
|
379
|
+
if node.name in self._file_scope_names:
|
|
380
|
+
self.diagnostics.error(
|
|
381
|
+
f"object methods cannot read file-scope name `{node.name}` — "
|
|
382
|
+
"object methods are encapsulated within the class boundary "
|
|
383
|
+
"(static methods see the class as namespace; non-static "
|
|
384
|
+
"methods see `this` only). File-scope visibility is for "
|
|
385
|
+
"free functions only.",
|
|
386
|
+
node.span,
|
|
387
|
+
code="SUT0144",
|
|
388
|
+
hint=(
|
|
389
|
+
f"if `{node.name}` should be accessible to this method, "
|
|
390
|
+
"either make this a free function (`function` instead of "
|
|
391
|
+
f"`method`), or move `{node.name}` onto a class so the "
|
|
392
|
+
"method can reach it through `this.` or the class as a "
|
|
393
|
+
"namespace."
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# ---- statements ------------------------------------------------
|
|
398
|
+
|
|
399
|
+
def visit_Block(self, node: ast.Block) -> None:
|
|
400
|
+
for s in node.statements:
|
|
401
|
+
self.visit(s)
|
|
402
|
+
|
|
403
|
+
def visit_IfStmt(self, node: ast.IfStmt) -> None:
|
|
404
|
+
self.visit(node.condition)
|
|
405
|
+
self.visit(node.then_branch)
|
|
406
|
+
if node.else_branch is not None:
|
|
407
|
+
self.visit(node.else_branch)
|
|
408
|
+
|
|
409
|
+
def visit_ForStmt(self, node: ast.ForStmt) -> None:
|
|
410
|
+
if node.init is not None:
|
|
411
|
+
self.visit(node.init)
|
|
412
|
+
if node.condition is not None:
|
|
413
|
+
self.visit(node.condition)
|
|
414
|
+
if node.step is not None:
|
|
415
|
+
self.visit(node.step)
|
|
416
|
+
self.visit(node.body)
|
|
417
|
+
|
|
418
|
+
def visit_ForeachStmt(self, node: ast.ForeachStmt) -> None:
|
|
419
|
+
if node.var_type is not None:
|
|
420
|
+
self._record_type_usage(node.var_type)
|
|
421
|
+
self.visit(node.iterable)
|
|
422
|
+
self.visit(node.body)
|
|
423
|
+
|
|
424
|
+
# ---- expressions -----------------------------------------------
|
|
425
|
+
|
|
426
|
+
def visit_CastExpr(self, node: ast.CastExpr) -> None:
|
|
427
|
+
# SUT0111: (TYPE) "string literal" is not allowed. String ->
|
|
428
|
+
# vector must go through embed(), per the spec.
|
|
429
|
+
if isinstance(node.expr, ast.StringLiteral):
|
|
430
|
+
self.diagnostics.error(
|
|
431
|
+
f"cannot cast a string literal to `{node.target_type.name}`; "
|
|
432
|
+
"use `embed(...)` to convert a string into a vector",
|
|
433
|
+
node.span,
|
|
434
|
+
code="SUT0111",
|
|
435
|
+
hint="write `embed(\"...\")` instead of `({}) \"...\"`".format(
|
|
436
|
+
node.target_type.name
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
self._record_type_usage(node.target_type)
|
|
440
|
+
self.visit(node.expr)
|
|
441
|
+
|
|
442
|
+
def visit_UnsafeCastExpr(self, node: ast.UnsafeCastExpr) -> None:
|
|
443
|
+
self._record_type_usage(node.target_type)
|
|
444
|
+
self.visit(node.expr)
|
|
445
|
+
|
|
446
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
447
|
+
for t in node.type_args:
|
|
448
|
+
self._record_type_usage(t)
|
|
449
|
+
self.visit(node.callee)
|
|
450
|
+
for a in node.args:
|
|
451
|
+
self.visit(a)
|
|
452
|
+
|
|
453
|
+
def visit_WaitLiteral(self, node: ast.WaitLiteral) -> None:
|
|
454
|
+
# The only place `wait` is legal is the RHS of a var-decl
|
|
455
|
+
# initializer, which `visit_VarDecl` handles by short-circuiting
|
|
456
|
+
# before descending. If we reach the literal through any other
|
|
457
|
+
# path, it's a position error.
|
|
458
|
+
self.diagnostics.error(
|
|
459
|
+
"`wait` is only valid as a var-decl initializer "
|
|
460
|
+
"(`int i = wait;`); it cannot appear in other expression "
|
|
461
|
+
"positions",
|
|
462
|
+
node.span,
|
|
463
|
+
code="SUT0130",
|
|
464
|
+
hint="if you want a placeholder value, use a typed zero "
|
|
465
|
+
"(`0`, `unknown`, the zero vector); if you want explicit "
|
|
466
|
+
"deferred initialization, move `wait` to a declaration",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def visit_Assignment(self, node: ast.Assignment) -> None:
|
|
470
|
+
# If this assigns to a wait-declared name, mark the wait as
|
|
471
|
+
# satisfied. Single-name targets only — assignments to fields
|
|
472
|
+
# / subscripts don't satisfy a wait on the variable itself.
|
|
473
|
+
if (isinstance(node.target, ast.Identifier)
|
|
474
|
+
and node.target.name in self._wait_declared):
|
|
475
|
+
del self._wait_declared[node.target.name]
|
|
476
|
+
self.visit(node.value)
|
|
477
|
+
|
|
478
|
+
# ---- helpers ---------------------------------------------------
|
|
479
|
+
|
|
480
|
+
def _enter_function_scope(self) -> None:
|
|
481
|
+
# Save the outer wait-tracking state and start a fresh scope.
|
|
482
|
+
# Function definitions can be nested (a method inside a class
|
|
483
|
+
# inside another method-bearing decl, for instance), so we
|
|
484
|
+
# need to restore on exit rather than just clearing.
|
|
485
|
+
self._wait_stack = getattr(self, "_wait_stack", [])
|
|
486
|
+
self._wait_stack.append(self._wait_declared)
|
|
487
|
+
self._wait_declared = {}
|
|
488
|
+
|
|
489
|
+
def _exit_function_scope(self) -> None:
|
|
490
|
+
# Any wait-declared name still pending at function exit was
|
|
491
|
+
# never assigned. Per the wait spec ("if it's not declared at
|
|
492
|
+
# all, it throws an error"), that's an error.
|
|
493
|
+
for name, span in self._wait_declared.items():
|
|
494
|
+
self.diagnostics.error(
|
|
495
|
+
f"variable `{name}` was declared with `wait` but never "
|
|
496
|
+
"assigned in the function body — `wait` is a promise "
|
|
497
|
+
"that an assignment will follow before the value is read",
|
|
498
|
+
span,
|
|
499
|
+
code="SUT0132",
|
|
500
|
+
hint=f"add `{name} = <value>;` somewhere in this function, "
|
|
501
|
+
"or remove the `wait` initializer if you intended "
|
|
502
|
+
"the zero-of-type as the final value",
|
|
503
|
+
)
|
|
504
|
+
self._wait_declared = self._wait_stack.pop() if self._wait_stack else {}
|
|
505
|
+
|
|
506
|
+
def _check_modifier_conflict(
|
|
507
|
+
self, mods: ast.Modifiers, span: SourceSpan
|
|
508
|
+
) -> None:
|
|
509
|
+
if mods.is_public and mods.is_private:
|
|
510
|
+
self.diagnostics.error(
|
|
511
|
+
"a declaration cannot be both `public` and `private`",
|
|
512
|
+
span,
|
|
513
|
+
code="SUT0112",
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def _record_type_usage(self, type_ref: Optional[ast.TypeRef]) -> None:
|
|
517
|
+
if type_ref is None:
|
|
518
|
+
return
|
|
519
|
+
# Only track user-defined types (not primitives) so we can
|
|
520
|
+
# detect casing drift on the same logical name.
|
|
521
|
+
PRIMITIVES = {
|
|
522
|
+
"scalar", "vector", "matrix", "tuple", "string",
|
|
523
|
+
"bool", "fuzzy", "void", "permutation", "map",
|
|
524
|
+
}
|
|
525
|
+
if type_ref.name not in PRIMITIVES:
|
|
526
|
+
self._class_name_usages.add(type_ref.name)
|
|
527
|
+
for arg in type_ref.type_args:
|
|
528
|
+
self._record_type_usage(arg)
|
|
529
|
+
|
|
530
|
+
def _check_class_casing_drift(self) -> None:
|
|
531
|
+
# Detect when the same class name appears in multiple casings
|
|
532
|
+
# within a single file. e.g. `animal` and `Animal`. We don't
|
|
533
|
+
# know which is canonical, so we emit a single warning listing
|
|
534
|
+
# both variants.
|
|
535
|
+
by_lower: dict = {}
|
|
536
|
+
for name in self._class_name_usages:
|
|
537
|
+
by_lower.setdefault(name.lower(), set()).add(name)
|
|
538
|
+
for variants in by_lower.values():
|
|
539
|
+
if len(variants) > 1:
|
|
540
|
+
sorted_variants = sorted(variants)
|
|
541
|
+
joined = ", ".join(f"`{v}`" for v in sorted_variants)
|
|
542
|
+
# Use a zero-length span at position 1,1 since this is
|
|
543
|
+
# a file-level concern. The SUT0113 code makes it
|
|
544
|
+
# editor-filterable.
|
|
545
|
+
pos = SourcePosition(line=1, column=1, offset=0)
|
|
546
|
+
self.diagnostics.warning(
|
|
547
|
+
f"class name appears in multiple casings in the same file: {joined}",
|
|
548
|
+
SourceSpan(start=pos, end=pos),
|
|
549
|
+
code="SUT0113",
|
|
550
|
+
hint="pick one casing and use it consistently — the spec "
|
|
551
|
+
"follows C# naming (PascalCase for class names)",
|
|
552
|
+
)
|