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,1046 @@
|
|
|
1
|
+
"""AST simplification pass + basis_vector-argument collection.
|
|
2
|
+
|
|
3
|
+
Runs after parsing, before codegen. Takes an `ast.Module` and
|
|
4
|
+
returns a simplified `ast.Module` with algebraic rewrites applied.
|
|
5
|
+
|
|
6
|
+
The rewrite set below is deliberately aggressive: Sutra's language
|
|
7
|
+
contract is that `.su` source describes *what* to compute, and the
|
|
8
|
+
compiler is responsible for reducing that to the minimum substrate
|
|
9
|
+
work. Every rewrite here is either an exact algebraic identity or
|
|
10
|
+
a soundness-preserving structural match. No approximate rewrites.
|
|
11
|
+
|
|
12
|
+
### Rewrites applied
|
|
13
|
+
|
|
14
|
+
1. **bundle-of-one elision.** `bundle(v)` → `v`.
|
|
15
|
+
Exact identity for unit-norm inputs; harmless re-normalization of
|
|
16
|
+
non-unit inputs is also algebraically `v / |v|`, still the identity
|
|
17
|
+
after any downstream cosine-based consumption.
|
|
18
|
+
|
|
19
|
+
2. **bundle flattening.** `bundle(bundle(a,b), c, bundle(d,e))` →
|
|
20
|
+
`bundle(a,b,c,d,e)`. Nested bundles are sums-of-sums; flattening
|
|
21
|
+
surfaces all terms so the parallel-scheduling pass sees them as
|
|
22
|
+
independent leaves.
|
|
23
|
+
|
|
24
|
+
3. **compose flattening.** `compose(compose(a,b), c)` → `compose(a,b,c)`.
|
|
25
|
+
Same motivation as bundle flattening: `compose` is associative,
|
|
26
|
+
and nested forms hide parallelism from the scheduling pass.
|
|
27
|
+
|
|
28
|
+
4. **similarity of self.** `similarity(a, a)` → `1.0`. Cosine of a
|
|
29
|
+
vector with itself is 1 for any non-zero vector. The rare
|
|
30
|
+
zero-vector case also agrees with runtime (runtime returns 0 for
|
|
31
|
+
zero norms, but `similarity(zero, zero)` in actual programs means
|
|
32
|
+
"I made a bug"; the rewrite surfaces that earlier).
|
|
33
|
+
|
|
34
|
+
5. **displacement of self.** `displacement(a, a)` → `zero_vector()`.
|
|
35
|
+
`a - a = 0` exactly. Downstream rewrites (6, 7) then absorb the
|
|
36
|
+
zero into surrounding expressions.
|
|
37
|
+
|
|
38
|
+
6. **zero absorption in bundle.** `bundle(..., zero_vector(), ...)` →
|
|
39
|
+
`bundle(...)` (drop the zero arg). If that leaves bundle empty,
|
|
40
|
+
the rewrite emits `zero_vector()` directly.
|
|
41
|
+
|
|
42
|
+
7. **zero absorption in addition.** `x + zero_vector()` → `x`,
|
|
43
|
+
`zero_vector() + x` → `x`. For BinaryOp with `+` or `-`.
|
|
44
|
+
|
|
45
|
+
8. **unbind/bind inverse.** `unbind(R, bind(R, x))` → `x` when the
|
|
46
|
+
two R arguments are structurally-identical Identifier references.
|
|
47
|
+
This is exact: Q.T @ (Q @ x) = x for orthogonal Q. The role
|
|
48
|
+
matrix is recomputed at runtime from the role vector, so
|
|
49
|
+
bit-identical role vectors produce bit-identical Q matrices.
|
|
50
|
+
|
|
51
|
+
9. **bind/unbind inverse.** `bind(R, unbind(R, x))` → `x`. Same
|
|
52
|
+
identity in the other direction.
|
|
53
|
+
|
|
54
|
+
10. **displacement-addition bundle rewrite.** A `bundle(...)` whose
|
|
55
|
+
args include a `displacement(a, b)` and a `b` in adjacent positions
|
|
56
|
+
*could* collapse to `bundle(..., a)`, but this requires reasoning
|
|
57
|
+
about bundle's normalization, which is lossy for repeated terms.
|
|
58
|
+
Not implemented — left as a comment for future work. The
|
|
59
|
+
cartography-style `a - b + c` (= `bundle(displacement(a,b), c)`)
|
|
60
|
+
stays as written.
|
|
61
|
+
|
|
62
|
+
11. **Arithmetic constant folding.** `x + 0` → `x`, `x - 0` → `x`,
|
|
63
|
+
`x * 1` → `x`, `1 * x` → `x`, `x * 0` → `0`, `0 * x` → `0`,
|
|
64
|
+
`x / 1` → `x` for scalar literal operands. Applied to IntLiteral
|
|
65
|
+
and FloatLiteral.
|
|
66
|
+
|
|
67
|
+
12. **bind of zero_vector() absorbs.** `bind(role, zero_vector())`
|
|
68
|
+
→ `zero_vector()`. Q @ 0 = 0 for any orthogonal Q. Independent
|
|
69
|
+
of role. Enables cascading: rule 5's `displacement(a, a)` →
|
|
70
|
+
zero can propagate through bind, and rule 6 then drops it from
|
|
71
|
+
an enclosing bundle.
|
|
72
|
+
|
|
73
|
+
13. **unbind of zero_vector() absorbs.** `unbind(role,
|
|
74
|
+
zero_vector())` → `zero_vector()`. Q^T @ 0 = 0 by the same
|
|
75
|
+
argument.
|
|
76
|
+
|
|
77
|
+
14. **compose with identity_permutation drops.**
|
|
78
|
+
`compose(identity_permutation(), x)` → `x` and
|
|
79
|
+
`compose(x, identity_permutation())` → `x`. `identity_permutation()`
|
|
80
|
+
is the all-ones vector; pointwise multiply by all-ones is the
|
|
81
|
+
identity. If all args are identities, the result is
|
|
82
|
+
`identity_permutation()` itself.
|
|
83
|
+
|
|
84
|
+
15. **argmax_cosine of single-candidate list.** `argmax_cosine(v,
|
|
85
|
+
[x])` → `x`. Only fires when the candidates are a compile-time
|
|
86
|
+
`ArrayLiteral` with exactly one element.
|
|
87
|
+
|
|
88
|
+
16. **Subscript of ArrayLiteral with literal int index.** `[a, b,
|
|
89
|
+
c][1]` → `b`. Compile-time array indexing. Negative indices
|
|
90
|
+
are handled Python-style; out-of-range indices are left
|
|
91
|
+
unsimplified so the runtime IndexError surfaces as a real
|
|
92
|
+
diagnostic.
|
|
93
|
+
|
|
94
|
+
### Rewrites NOT applied (documented non-rewrites)
|
|
95
|
+
|
|
96
|
+
- `bundle(x, x)` → `x` (NOT applied). `bundle` normalizes to unit
|
|
97
|
+
norm, so `bundle(x, x) = (x+x)/|x+x| = x/|x| = x` for unit x.
|
|
98
|
+
True algebraically, but the rewrite requires reasoning about
|
|
99
|
+
norms we don't track statically. Skipped.
|
|
100
|
+
- `bind(R1, bind(R2, x))` → `bind(compose(R1,R2), x)`. The product
|
|
101
|
+
of two Haar rotations is not itself a single cached role, so the
|
|
102
|
+
rewrite would require materializing a composite rotation at
|
|
103
|
+
runtime rather than at compile time. Skipped.
|
|
104
|
+
|
|
105
|
+
### Design invariants
|
|
106
|
+
|
|
107
|
+
- Post-order traversal: children are simplified before the parent
|
|
108
|
+
looks at them. This lets nested rewrites cascade in one pass.
|
|
109
|
+
- No rewrite produces a new node type the codegen doesn't already
|
|
110
|
+
handle. `zero_vector()` is a new builtin emitted via the normal
|
|
111
|
+
`Call(Identifier("zero_vector"), ...)` path.
|
|
112
|
+
- Rewrites are applied unconditionally — there is no user-facing
|
|
113
|
+
flag to disable them. If a rewrite produces wrong output for some
|
|
114
|
+
program, the rewrite is wrong and has to be removed, not hidden
|
|
115
|
+
behind a flag.
|
|
116
|
+
"""
|
|
117
|
+
from __future__ import annotations
|
|
118
|
+
|
|
119
|
+
from typing import Callable, List, Optional
|
|
120
|
+
|
|
121
|
+
from . import ast_nodes as ast
|
|
122
|
+
from .diagnostics import SourceSpan
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Optional tracing hook. When set to a callable, every rewrite that
|
|
126
|
+
# fires invokes `_trace_callback(rule_name, before_node, after_node)`.
|
|
127
|
+
# Used by `sutra_compiler.review` to show step-by-step simplification.
|
|
128
|
+
# None in production — zero overhead when not used.
|
|
129
|
+
_trace_callback: Optional[Callable[[str, object, object], None]] = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def set_trace_callback(cb: Optional[Callable[[str, object, object], None]]) -> None:
|
|
133
|
+
"""Set (or clear with None) the per-rewrite tracing callback.
|
|
134
|
+
|
|
135
|
+
The callback receives (rule_name, before_node, after_node) every
|
|
136
|
+
time a rewrite fires. Intended for the --review compiler mode;
|
|
137
|
+
production code leaves this at None.
|
|
138
|
+
"""
|
|
139
|
+
global _trace_callback
|
|
140
|
+
_trace_callback = cb
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _trace(rule: str, before, after):
|
|
144
|
+
"""Invoke the trace callback if one is registered. Inline helper so
|
|
145
|
+
rewrite sites read as `return _trace("R01", call, result)`."""
|
|
146
|
+
if _trace_callback is not None:
|
|
147
|
+
_trace_callback(rule, before, after)
|
|
148
|
+
return after
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Public entry points
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def simplify_module(module: ast.Module) -> ast.Module:
|
|
157
|
+
"""Apply all simplification passes to a module and return the result.
|
|
158
|
+
|
|
159
|
+
Two passes:
|
|
160
|
+
1. The hand-rolled rule set in this file (R01..R16, structural).
|
|
161
|
+
2. An egglog-driven post-pass that tries to reduce expressions
|
|
162
|
+
the hand-rolled pass leaves intact — primarily cases where
|
|
163
|
+
cascading rewrites need equality saturation rather than the
|
|
164
|
+
fixed-order pattern matching the hand-rolled simplifier does.
|
|
165
|
+
|
|
166
|
+
The egglog pass is conservative: it only replaces a subtree when
|
|
167
|
+
the result lowers to a recognized simpler shape (a literal, an
|
|
168
|
+
identifier, a `zero_vector()` call). Anything else round-trips
|
|
169
|
+
losslessly. If the `egglog` package isn't installed, the post-
|
|
170
|
+
pass is a no-op.
|
|
171
|
+
|
|
172
|
+
The module is mutated in place; the return value is the same
|
|
173
|
+
object, returned for call-chain convenience.
|
|
174
|
+
"""
|
|
175
|
+
for decl in module.items:
|
|
176
|
+
_simplify_top_level(decl)
|
|
177
|
+
_egglog_post_pass(module)
|
|
178
|
+
return module
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _egglog_post_pass(module: ast.Module) -> None:
|
|
182
|
+
"""Walk every expression in the module and try the egglog
|
|
183
|
+
simplifier on it. Replace the expression in its parent if egglog
|
|
184
|
+
found a simpler shape.
|
|
185
|
+
|
|
186
|
+
Conservative — only replaces when the result is structurally
|
|
187
|
+
simpler (a literal, identifier, or zero_vector call). The lift /
|
|
188
|
+
lower bridge in `simplify_egglog` returns the original expression
|
|
189
|
+
unchanged when it can't make progress, so the identity-comparison
|
|
190
|
+
`out is expr` is the signal for "no change."
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
from . import simplify_egglog as _eg
|
|
194
|
+
except ImportError:
|
|
195
|
+
# egglog not installed — post-pass is a no-op.
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
def _try(expr: ast.Expr) -> ast.Expr:
|
|
199
|
+
# Try vec context first; if no progress, try num. Bridge is
|
|
200
|
+
# conservative on both sides — never returns a wrong result.
|
|
201
|
+
out = _eg.simplify_ast_vec(expr)
|
|
202
|
+
if out is not expr:
|
|
203
|
+
return out
|
|
204
|
+
out = _eg.simplify_ast_num(expr)
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
def _walk_expr(expr: ast.Expr) -> ast.Expr:
|
|
208
|
+
"""Bottom-up walk: simplify children first, then the node."""
|
|
209
|
+
if isinstance(expr, ast.Call):
|
|
210
|
+
expr.args = [_walk_expr(a) for a in expr.args]
|
|
211
|
+
elif isinstance(expr, ast.BinaryOp):
|
|
212
|
+
expr.left = _walk_expr(expr.left)
|
|
213
|
+
expr.right = _walk_expr(expr.right)
|
|
214
|
+
elif isinstance(expr, ast.UnaryOp):
|
|
215
|
+
expr.operand = _walk_expr(expr.operand)
|
|
216
|
+
elif isinstance(expr, ast.Parenthesized):
|
|
217
|
+
expr.inner = _walk_expr(expr.inner)
|
|
218
|
+
elif isinstance(expr, ast.ArrayLiteral):
|
|
219
|
+
expr.elements = [_walk_expr(e) for e in expr.elements]
|
|
220
|
+
elif isinstance(expr, ast.CastExpr):
|
|
221
|
+
expr.expr = _walk_expr(expr.expr)
|
|
222
|
+
elif isinstance(expr, ast.UnsafeCastExpr):
|
|
223
|
+
expr.expr = _walk_expr(expr.expr)
|
|
224
|
+
return _try(expr)
|
|
225
|
+
|
|
226
|
+
def _walk_stmt(stmt: ast.Stmt) -> None:
|
|
227
|
+
# Skip if there's no obvious place an expr could be.
|
|
228
|
+
if isinstance(stmt, ast.VarDecl):
|
|
229
|
+
if stmt.initializer is not None:
|
|
230
|
+
stmt.initializer = _walk_expr(stmt.initializer)
|
|
231
|
+
elif isinstance(stmt, ast.ExprStmt):
|
|
232
|
+
stmt.expr = _walk_expr(stmt.expr)
|
|
233
|
+
elif isinstance(stmt, ast.ReturnStmt):
|
|
234
|
+
if stmt.value is not None:
|
|
235
|
+
stmt.value = _walk_expr(stmt.value)
|
|
236
|
+
elif isinstance(stmt, ast.IfStmt):
|
|
237
|
+
stmt.condition = _walk_expr(stmt.condition)
|
|
238
|
+
for s in stmt.then_branch.statements:
|
|
239
|
+
_walk_stmt(s)
|
|
240
|
+
if stmt.else_branch is not None:
|
|
241
|
+
for s in stmt.else_branch.statements:
|
|
242
|
+
_walk_stmt(s)
|
|
243
|
+
elif isinstance(stmt, ast.Block):
|
|
244
|
+
for s in stmt.statements:
|
|
245
|
+
_walk_stmt(s)
|
|
246
|
+
elif isinstance(stmt, ast.WhileStmt):
|
|
247
|
+
stmt.condition = _walk_expr(stmt.condition)
|
|
248
|
+
for s in stmt.body.statements:
|
|
249
|
+
_walk_stmt(s)
|
|
250
|
+
elif isinstance(stmt, ast.LoopStmt):
|
|
251
|
+
for s in stmt.body.statements:
|
|
252
|
+
_walk_stmt(s)
|
|
253
|
+
# Other statement types: walked-through children are out of
|
|
254
|
+
# scope for this post-pass; the hand-written pass already
|
|
255
|
+
# handled them.
|
|
256
|
+
|
|
257
|
+
for decl in module.items:
|
|
258
|
+
if isinstance(decl, ast.VarDecl):
|
|
259
|
+
if decl.initializer is not None:
|
|
260
|
+
decl.initializer = _walk_expr(decl.initializer)
|
|
261
|
+
elif isinstance(decl, ast.FunctionDecl):
|
|
262
|
+
for s in decl.body.statements:
|
|
263
|
+
_walk_stmt(s)
|
|
264
|
+
elif isinstance(decl, ast.MethodDecl):
|
|
265
|
+
for s in decl.body.statements:
|
|
266
|
+
_walk_stmt(s)
|
|
267
|
+
elif isinstance(decl, ast.ClassDecl):
|
|
268
|
+
# Empty bodies for now; nothing to walk inside.
|
|
269
|
+
pass
|
|
270
|
+
elif isinstance(decl, ast.Stmt):
|
|
271
|
+
_walk_stmt(decl)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _auto_embed_var_decl_init(decl: ast.VarDecl) -> None:
|
|
275
|
+
"""Type-directed rewrite of StringLiteral initializers.
|
|
276
|
+
|
|
277
|
+
String literals are interpreted by the type they are being
|
|
278
|
+
assigned into:
|
|
279
|
+
|
|
280
|
+
- `vector v = "foo"` — the string is implicitly embedded. We wrap
|
|
281
|
+
it in `EmbedExpr` so the codegen emits `_VSA.embed("foo")`.
|
|
282
|
+
- `char c = "c"` — the string must be exactly one character; we
|
|
283
|
+
rewrite it to a `CharLiteral`. A longer string is a type error
|
|
284
|
+
the validator will catch separately; for now we leave longer
|
|
285
|
+
strings in place so the program still parses (codegen will
|
|
286
|
+
diagnose).
|
|
287
|
+
- `var x = "foo"` (untyped) — defaults to embed. User direction
|
|
288
|
+
was clear: "var x = 'x' will default embed". The programmer
|
|
289
|
+
writes `var x : string = "foo"` or `string x = "foo"` to keep
|
|
290
|
+
the string as a string.
|
|
291
|
+
- `string x = "foo"` — untouched. Explicit string type keeps the
|
|
292
|
+
string.
|
|
293
|
+
|
|
294
|
+
Only the literal case — `StringLiteral` — is rewritten. Non-literal
|
|
295
|
+
RHS expressions (e.g. `vector v = lookup_name();`) are left to
|
|
296
|
+
whatever the normal expression-typing story produces.
|
|
297
|
+
"""
|
|
298
|
+
if decl.initializer is None:
|
|
299
|
+
return
|
|
300
|
+
if not isinstance(decl.initializer, ast.StringLiteral):
|
|
301
|
+
return
|
|
302
|
+
s_lit: ast.StringLiteral = decl.initializer
|
|
303
|
+
type_name = decl.type_ref.name if decl.type_ref is not None else None
|
|
304
|
+
|
|
305
|
+
# `string x = "foo"` — explicit string, no rewrite.
|
|
306
|
+
if type_name == "string":
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
# `char c = "c"` — fold to CharLiteral at compile time.
|
|
310
|
+
if type_name == "char":
|
|
311
|
+
if len(s_lit.value) == 1:
|
|
312
|
+
decl.initializer = ast.CharLiteral(
|
|
313
|
+
value=ord(s_lit.value), span=s_lit.span,
|
|
314
|
+
)
|
|
315
|
+
# Length != 1 is left alone; the validator / codegen will
|
|
316
|
+
# complain when a string hits a `char` context.
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
# `vector v = "foo"` OR `var v = "foo"` (untyped) — implicit embed.
|
|
320
|
+
if type_name == "vector" or decl.is_var_inferred:
|
|
321
|
+
decl.initializer = ast.EmbedExpr(expr=s_lit, span=s_lit.span)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Other typed contexts (int, float, fuzzy, etc.) — don't rewrite.
|
|
325
|
+
# Fuzzy-with-StringLiteral is a type error the validator catches;
|
|
326
|
+
# int/float with a string is a cast the validator catches too.
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def collect_basis_vector_strings(module: ast.Module) -> list[str]:
|
|
330
|
+
"""Return every string literal that will be embedded at runtime.
|
|
331
|
+
|
|
332
|
+
Covers two sources:
|
|
333
|
+
- `basis_vector("name")` — explicit source-level basis_vector call
|
|
334
|
+
with a string literal argument.
|
|
335
|
+
- `embed(<StringLiteral>)` — the `EmbedExpr` AST node, which the
|
|
336
|
+
auto-embed pass inserts in type-directed contexts (`vector v =
|
|
337
|
+
"foo"`, untyped `var x = "foo"`, etc.) and which may also be
|
|
338
|
+
written explicitly.
|
|
339
|
+
|
|
340
|
+
Used by the codegen to emit a batched Ollama pre-fetch at module
|
|
341
|
+
init: N sequential HTTP round-trips collapse into a single batched
|
|
342
|
+
embed call. Strings are returned in source order, deduplicated
|
|
343
|
+
(first-occurrence order preserved).
|
|
344
|
+
"""
|
|
345
|
+
seen: set[str] = set()
|
|
346
|
+
collected: list[str] = []
|
|
347
|
+
|
|
348
|
+
def record(s: str) -> None:
|
|
349
|
+
if s not in seen:
|
|
350
|
+
seen.add(s)
|
|
351
|
+
collected.append(s)
|
|
352
|
+
|
|
353
|
+
def visit(node) -> None:
|
|
354
|
+
if node is None:
|
|
355
|
+
return
|
|
356
|
+
if isinstance(node, ast.Call):
|
|
357
|
+
if _is_basis_vector_literal_call(node):
|
|
358
|
+
record(node.args[0].value) # type: ignore[attr-defined]
|
|
359
|
+
visit(node.callee)
|
|
360
|
+
for a in node.args:
|
|
361
|
+
visit(a)
|
|
362
|
+
return
|
|
363
|
+
if isinstance(node, ast.EmbedExpr) and isinstance(node.expr, ast.StringLiteral):
|
|
364
|
+
record(node.expr.value)
|
|
365
|
+
return
|
|
366
|
+
for child in _children(node):
|
|
367
|
+
visit(child)
|
|
368
|
+
|
|
369
|
+
for item in module.items:
|
|
370
|
+
visit(item)
|
|
371
|
+
return collected
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
# Internal: AST traversal helpers
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _children(node):
|
|
380
|
+
"""Yield direct child AST nodes of `node`. Covers every expression
|
|
381
|
+
and statement type the simplifier may encounter.
|
|
382
|
+
"""
|
|
383
|
+
if node is None:
|
|
384
|
+
return
|
|
385
|
+
if isinstance(node, ast.BinaryOp):
|
|
386
|
+
yield node.left; yield node.right; return
|
|
387
|
+
if isinstance(node, ast.UnaryOp):
|
|
388
|
+
yield node.operand; return
|
|
389
|
+
if isinstance(node, ast.PostfixOp):
|
|
390
|
+
yield node.operand; return
|
|
391
|
+
if isinstance(node, ast.ArrayLiteral):
|
|
392
|
+
for e in node.elements: yield e
|
|
393
|
+
return
|
|
394
|
+
if isinstance(node, ast.MapLiteral):
|
|
395
|
+
for k in node.keys: yield k
|
|
396
|
+
for v in node.values: yield v
|
|
397
|
+
return
|
|
398
|
+
if isinstance(node, ast.Subscript):
|
|
399
|
+
yield node.target; yield node.index; return
|
|
400
|
+
if isinstance(node, ast.MemberAccess):
|
|
401
|
+
yield node.obj; return
|
|
402
|
+
if isinstance(node, ast.Assignment):
|
|
403
|
+
yield node.target; yield node.value; return
|
|
404
|
+
if isinstance(node, ast.Parenthesized):
|
|
405
|
+
yield node.inner; return
|
|
406
|
+
if isinstance(node, ast.CastExpr):
|
|
407
|
+
yield node.expr; return
|
|
408
|
+
if isinstance(node, ast.UnsafeCastExpr):
|
|
409
|
+
yield node.expr; return
|
|
410
|
+
if isinstance(node, ast.UnsafeOverrideExpr):
|
|
411
|
+
yield node.expr; return
|
|
412
|
+
if isinstance(node, ast.DefuzzyExpr):
|
|
413
|
+
yield node.expr; return
|
|
414
|
+
if isinstance(node, ast.EmbedExpr):
|
|
415
|
+
yield node.expr; return
|
|
416
|
+
if isinstance(node, ast.InterpolatedString):
|
|
417
|
+
for part in node.parts:
|
|
418
|
+
if not isinstance(part, str):
|
|
419
|
+
yield part
|
|
420
|
+
return
|
|
421
|
+
if isinstance(node, ast.VarDecl):
|
|
422
|
+
yield node.initializer; return
|
|
423
|
+
if isinstance(node, ast.FunctionDecl):
|
|
424
|
+
for s in node.body.statements: yield s
|
|
425
|
+
return
|
|
426
|
+
if isinstance(node, ast.ReturnStmt):
|
|
427
|
+
yield node.value; return
|
|
428
|
+
if isinstance(node, ast.ExprStmt):
|
|
429
|
+
yield node.expr; return
|
|
430
|
+
if isinstance(node, ast.IfStmt):
|
|
431
|
+
yield node.condition
|
|
432
|
+
for s in node.then_branch.statements: yield s
|
|
433
|
+
if node.else_branch is not None:
|
|
434
|
+
if isinstance(node.else_branch, ast.IfStmt):
|
|
435
|
+
yield node.else_branch
|
|
436
|
+
else:
|
|
437
|
+
for s in node.else_branch.statements: yield s
|
|
438
|
+
return
|
|
439
|
+
if isinstance(node, ast.WhileStmt):
|
|
440
|
+
yield node.condition
|
|
441
|
+
for s in node.body.statements: yield s
|
|
442
|
+
return
|
|
443
|
+
if isinstance(node, ast.ForStmt):
|
|
444
|
+
yield node.init; yield node.condition; yield node.step
|
|
445
|
+
for s in node.body.statements: yield s
|
|
446
|
+
return
|
|
447
|
+
if isinstance(node, ast.DoWhileStmt):
|
|
448
|
+
yield node.condition
|
|
449
|
+
for s in node.body.statements: yield s
|
|
450
|
+
return
|
|
451
|
+
if isinstance(node, ast.ForeachStmt):
|
|
452
|
+
yield node.iterable
|
|
453
|
+
for s in node.body.statements: yield s
|
|
454
|
+
return
|
|
455
|
+
if isinstance(node, ast.LoopStmt):
|
|
456
|
+
yield node.count; yield node.condition
|
|
457
|
+
for s in node.body.statements: yield s
|
|
458
|
+
return
|
|
459
|
+
if isinstance(node, ast.Block):
|
|
460
|
+
for s in node.statements: yield s
|
|
461
|
+
return
|
|
462
|
+
if isinstance(node, ast.TryStmt):
|
|
463
|
+
for s in node.try_body.statements: yield s
|
|
464
|
+
for s in node.catch_body.statements: yield s
|
|
465
|
+
return
|
|
466
|
+
# Leaf nodes (Identifier, literals, etc.) have no children.
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
# Internal: structural equality on expressions
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _structurally_equal(a, b) -> bool:
|
|
475
|
+
"""Conservative structural equality for expressions.
|
|
476
|
+
|
|
477
|
+
Returns True only when we can prove the two subtrees evaluate to
|
|
478
|
+
the same value — which is exact for literal constants and for
|
|
479
|
+
identifier references to the same name. Pessimistic elsewhere:
|
|
480
|
+
a `MemberAccess`, a `Call`, or anything with side effects compares
|
|
481
|
+
unequal even if textually identical, because a cautious rewriter
|
|
482
|
+
needs to assume they might differ.
|
|
483
|
+
|
|
484
|
+
This is used by the unbind/bind inverse rewrites to decide whether
|
|
485
|
+
the two role arguments are "the same". In practice, roles in .su
|
|
486
|
+
programs are top-level `vector r_foo = basis_vector("...")`
|
|
487
|
+
declarations referenced by identifier — exactly the case this
|
|
488
|
+
function handles exactly.
|
|
489
|
+
"""
|
|
490
|
+
if type(a) is not type(b):
|
|
491
|
+
return False
|
|
492
|
+
if isinstance(a, ast.Identifier):
|
|
493
|
+
return a.name == b.name
|
|
494
|
+
if isinstance(a, ast.IntLiteral):
|
|
495
|
+
return a.value == b.value
|
|
496
|
+
if isinstance(a, ast.FloatLiteral):
|
|
497
|
+
return a.value == b.value
|
|
498
|
+
if isinstance(a, ast.StringLiteral):
|
|
499
|
+
return a.value == b.value
|
|
500
|
+
if isinstance(a, ast.BoolLiteral):
|
|
501
|
+
return a.value == b.value
|
|
502
|
+
# Anything else: be conservative.
|
|
503
|
+
return False
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ---------------------------------------------------------------------------
|
|
507
|
+
# Internal: call-pattern matchers
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _is_call_named(expr, name: str, arity: Optional[int] = None) -> bool:
|
|
512
|
+
if not isinstance(expr, ast.Call):
|
|
513
|
+
return False
|
|
514
|
+
if not isinstance(expr.callee, ast.Identifier):
|
|
515
|
+
return False
|
|
516
|
+
if expr.callee.name != name:
|
|
517
|
+
return False
|
|
518
|
+
if arity is not None and len(expr.args) != arity:
|
|
519
|
+
return False
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _is_zero_vector_call(expr) -> bool:
|
|
524
|
+
"""Match `zero_vector()` — the emitted zero primitive."""
|
|
525
|
+
return _is_call_named(expr, "zero_vector", arity=0)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _is_basis_vector_literal_call(expr) -> bool:
|
|
529
|
+
return (_is_call_named(expr, "basis_vector", arity=1)
|
|
530
|
+
and isinstance(expr.args[0], ast.StringLiteral))
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _mk_zero_vector(span: SourceSpan) -> ast.Call:
|
|
534
|
+
"""Construct a fresh `zero_vector()` call node."""
|
|
535
|
+
return ast.Call(
|
|
536
|
+
span=span,
|
|
537
|
+
callee=ast.Identifier(span=span, name="zero_vector"),
|
|
538
|
+
type_args=[],
|
|
539
|
+
args=[],
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _mk_float_literal(value: float, span: SourceSpan) -> ast.FloatLiteral:
|
|
544
|
+
return ast.FloatLiteral(span=span, value=value)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# ---------------------------------------------------------------------------
|
|
548
|
+
# Internal: statement dispatch
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _simplify_top_level(decl) -> None:
|
|
553
|
+
if isinstance(decl, ast.FunctionDecl):
|
|
554
|
+
_simplify_block(decl.body)
|
|
555
|
+
elif isinstance(decl, ast.VarDecl):
|
|
556
|
+
_auto_embed_var_decl_init(decl)
|
|
557
|
+
if decl.initializer is not None:
|
|
558
|
+
decl.initializer = _simplify_expr(decl.initializer)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _simplify_block(block: ast.Block) -> None:
|
|
562
|
+
for stmt in block.statements:
|
|
563
|
+
_simplify_stmt(stmt)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _simplify_stmt(stmt) -> None:
|
|
567
|
+
if isinstance(stmt, ast.VarDecl):
|
|
568
|
+
_auto_embed_var_decl_init(stmt)
|
|
569
|
+
if stmt.initializer is not None:
|
|
570
|
+
stmt.initializer = _simplify_expr(stmt.initializer)
|
|
571
|
+
return
|
|
572
|
+
if isinstance(stmt, ast.ReturnStmt):
|
|
573
|
+
if stmt.value is not None:
|
|
574
|
+
stmt.value = _simplify_expr(stmt.value)
|
|
575
|
+
return
|
|
576
|
+
if isinstance(stmt, ast.ExprStmt):
|
|
577
|
+
stmt.expr = _simplify_expr(stmt.expr)
|
|
578
|
+
return
|
|
579
|
+
if isinstance(stmt, ast.IfStmt):
|
|
580
|
+
stmt.condition = _simplify_expr(stmt.condition)
|
|
581
|
+
_simplify_block(stmt.then_branch)
|
|
582
|
+
if stmt.else_branch is not None:
|
|
583
|
+
if isinstance(stmt.else_branch, ast.IfStmt):
|
|
584
|
+
_simplify_stmt(stmt.else_branch)
|
|
585
|
+
else:
|
|
586
|
+
_simplify_block(stmt.else_branch)
|
|
587
|
+
return
|
|
588
|
+
if isinstance(stmt, ast.WhileStmt):
|
|
589
|
+
stmt.condition = _simplify_expr(stmt.condition)
|
|
590
|
+
_simplify_block(stmt.body)
|
|
591
|
+
return
|
|
592
|
+
if isinstance(stmt, ast.ForStmt):
|
|
593
|
+
if stmt.init is not None:
|
|
594
|
+
_simplify_stmt(stmt.init)
|
|
595
|
+
if stmt.condition is not None:
|
|
596
|
+
stmt.condition = _simplify_expr(stmt.condition)
|
|
597
|
+
if stmt.step is not None:
|
|
598
|
+
_simplify_stmt(stmt.step)
|
|
599
|
+
_simplify_block(stmt.body)
|
|
600
|
+
return
|
|
601
|
+
if isinstance(stmt, ast.DoWhileStmt):
|
|
602
|
+
_simplify_block(stmt.body)
|
|
603
|
+
stmt.condition = _simplify_expr(stmt.condition)
|
|
604
|
+
return
|
|
605
|
+
if isinstance(stmt, ast.ForeachStmt):
|
|
606
|
+
stmt.iterable = _simplify_expr(stmt.iterable)
|
|
607
|
+
_simplify_block(stmt.body)
|
|
608
|
+
return
|
|
609
|
+
if isinstance(stmt, ast.LoopStmt):
|
|
610
|
+
if stmt.count is not None:
|
|
611
|
+
stmt.count = _simplify_expr(stmt.count)
|
|
612
|
+
if stmt.condition is not None:
|
|
613
|
+
stmt.condition = _simplify_expr(stmt.condition)
|
|
614
|
+
_simplify_block(stmt.body)
|
|
615
|
+
return
|
|
616
|
+
if isinstance(stmt, ast.Block):
|
|
617
|
+
_simplify_block(stmt)
|
|
618
|
+
return
|
|
619
|
+
if isinstance(stmt, ast.TryStmt):
|
|
620
|
+
_simplify_block(stmt.try_body)
|
|
621
|
+
_simplify_block(stmt.catch_body)
|
|
622
|
+
return
|
|
623
|
+
# Unknown statement types pass through untouched.
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# ---------------------------------------------------------------------------
|
|
627
|
+
# Internal: expression simplification (post-order)
|
|
628
|
+
# ---------------------------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _simplify_expr(expr):
|
|
632
|
+
"""Recursively simplify an expression. Post-order traversal: simplify
|
|
633
|
+
children first, then look at the resulting node for rewrite matches.
|
|
634
|
+
Rewrites may compound in a single pass because children are finalized
|
|
635
|
+
before the parent inspects them.
|
|
636
|
+
"""
|
|
637
|
+
if expr is None:
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
if isinstance(expr, ast.Call):
|
|
641
|
+
if not isinstance(expr.callee, ast.Identifier):
|
|
642
|
+
expr.callee = _simplify_expr(expr.callee)
|
|
643
|
+
expr.args = [_simplify_expr(a) for a in expr.args]
|
|
644
|
+
return _rewrite_call(expr)
|
|
645
|
+
|
|
646
|
+
if isinstance(expr, ast.BinaryOp):
|
|
647
|
+
expr.left = _simplify_expr(expr.left)
|
|
648
|
+
expr.right = _simplify_expr(expr.right)
|
|
649
|
+
return _rewrite_binary(expr)
|
|
650
|
+
|
|
651
|
+
if isinstance(expr, ast.UnaryOp):
|
|
652
|
+
expr.operand = _simplify_expr(expr.operand)
|
|
653
|
+
# Unary minus on an imaginary literal folds at compile time
|
|
654
|
+
# (`-5i` → ImaginaryLiteral(-5)). Unary plus is a no-op.
|
|
655
|
+
if expr.op == "-" and isinstance(expr.operand, ast.ImaginaryLiteral):
|
|
656
|
+
return ast.ImaginaryLiteral(
|
|
657
|
+
value=-expr.operand.value, span=expr.span,
|
|
658
|
+
)
|
|
659
|
+
if expr.op == "+" and isinstance(expr.operand, ast.ImaginaryLiteral):
|
|
660
|
+
return expr.operand
|
|
661
|
+
# Unary minus on a numeric literal: `-5` → IntLiteral(-5),
|
|
662
|
+
# `-3.14` → FloatLiteral(-3.14). Matters for inlined
|
|
663
|
+
# polynomials where a `0 - x` form from logical_not on a
|
|
664
|
+
# literal input should collapse.
|
|
665
|
+
if expr.op == "-":
|
|
666
|
+
if isinstance(expr.operand, ast.IntLiteral):
|
|
667
|
+
return ast.IntLiteral(
|
|
668
|
+
value=-expr.operand.value, span=expr.span,
|
|
669
|
+
)
|
|
670
|
+
if isinstance(expr.operand, ast.FloatLiteral):
|
|
671
|
+
return ast.FloatLiteral(
|
|
672
|
+
value=-expr.operand.value, span=expr.span,
|
|
673
|
+
)
|
|
674
|
+
if expr.op == "+":
|
|
675
|
+
if isinstance(expr.operand, (ast.IntLiteral, ast.FloatLiteral)):
|
|
676
|
+
return expr.operand
|
|
677
|
+
return expr
|
|
678
|
+
|
|
679
|
+
if isinstance(expr, ast.ArrayLiteral):
|
|
680
|
+
expr.elements = [_simplify_expr(e) for e in expr.elements]
|
|
681
|
+
return expr
|
|
682
|
+
|
|
683
|
+
if isinstance(expr, ast.MapLiteral):
|
|
684
|
+
expr.keys = [_simplify_expr(k) for k in expr.keys]
|
|
685
|
+
expr.values = [_simplify_expr(v) for v in expr.values]
|
|
686
|
+
return expr
|
|
687
|
+
|
|
688
|
+
if isinstance(expr, ast.Subscript):
|
|
689
|
+
expr.target = _simplify_expr(expr.target)
|
|
690
|
+
expr.index = _simplify_expr(expr.index)
|
|
691
|
+
# Rule 16: Subscript of an ArrayLiteral with a literal int index
|
|
692
|
+
# → the indexed element. Compile-time array indexing. Out-of-
|
|
693
|
+
# range indices are left unsimplified (the runtime IndexError
|
|
694
|
+
# is honest; silent truncation would hide a program bug).
|
|
695
|
+
# Negative indices are handled in Python style (-1 → last).
|
|
696
|
+
if (isinstance(expr.target, ast.ArrayLiteral)
|
|
697
|
+
and isinstance(expr.index, ast.IntLiteral)):
|
|
698
|
+
elements = expr.target.elements
|
|
699
|
+
idx = expr.index.value
|
|
700
|
+
if -len(elements) <= idx < len(elements):
|
|
701
|
+
return elements[idx]
|
|
702
|
+
return expr
|
|
703
|
+
|
|
704
|
+
if isinstance(expr, ast.MemberAccess):
|
|
705
|
+
expr.obj = _simplify_expr(expr.obj)
|
|
706
|
+
return expr
|
|
707
|
+
|
|
708
|
+
if isinstance(expr, ast.Assignment):
|
|
709
|
+
expr.target = _simplify_expr(expr.target)
|
|
710
|
+
expr.value = _simplify_expr(expr.value)
|
|
711
|
+
return expr
|
|
712
|
+
|
|
713
|
+
if isinstance(expr, ast.Parenthesized):
|
|
714
|
+
expr.inner = _simplify_expr(expr.inner)
|
|
715
|
+
return expr
|
|
716
|
+
|
|
717
|
+
if isinstance(expr, ast.CastExpr):
|
|
718
|
+
expr.expr = _simplify_expr(expr.expr)
|
|
719
|
+
return expr
|
|
720
|
+
|
|
721
|
+
if isinstance(expr, ast.UnsafeCastExpr):
|
|
722
|
+
expr.expr = _simplify_expr(expr.expr)
|
|
723
|
+
return expr
|
|
724
|
+
|
|
725
|
+
if isinstance(expr, ast.UnsafeOverrideExpr):
|
|
726
|
+
expr.expr = _simplify_expr(expr.expr)
|
|
727
|
+
return expr
|
|
728
|
+
|
|
729
|
+
if isinstance(expr, ast.DefuzzyExpr):
|
|
730
|
+
expr.expr = _simplify_expr(expr.expr)
|
|
731
|
+
return expr
|
|
732
|
+
|
|
733
|
+
if isinstance(expr, ast.EmbedExpr):
|
|
734
|
+
expr.expr = _simplify_expr(expr.expr)
|
|
735
|
+
return expr
|
|
736
|
+
|
|
737
|
+
if isinstance(expr, ast.InterpolatedString):
|
|
738
|
+
new_parts = []
|
|
739
|
+
for p in expr.parts:
|
|
740
|
+
if isinstance(p, str):
|
|
741
|
+
new_parts.append(p)
|
|
742
|
+
else:
|
|
743
|
+
new_parts.append(_simplify_expr(p))
|
|
744
|
+
expr.parts = new_parts
|
|
745
|
+
return expr
|
|
746
|
+
|
|
747
|
+
# Identifier, IntLiteral, FloatLiteral, CharLiteral, StringLiteral,
|
|
748
|
+
# BoolLiteral, UnknownLiteral, ImaginaryLiteral, ComplexLiteral —
|
|
749
|
+
# ThisExpr — no simplification.
|
|
750
|
+
return expr
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
# ---------------------------------------------------------------------------
|
|
754
|
+
# Internal: call-level rewrites (post children-simplified)
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _rewrite_call(call: ast.Call):
|
|
759
|
+
if not isinstance(call.callee, ast.Identifier):
|
|
760
|
+
return call
|
|
761
|
+
name = call.callee.name
|
|
762
|
+
|
|
763
|
+
# Rule 1: bundle(v) → v (single-arg bundle is identity).
|
|
764
|
+
if name == "bundle" and len(call.args) == 1:
|
|
765
|
+
return _trace("R01 bundle(v) -> v", call, call.args[0])
|
|
766
|
+
|
|
767
|
+
# Rule 2: flatten nested bundles.
|
|
768
|
+
if name == "bundle":
|
|
769
|
+
flattened: List = []
|
|
770
|
+
changed = False
|
|
771
|
+
for a in call.args:
|
|
772
|
+
if _is_call_named(a, "bundle"):
|
|
773
|
+
flattened.extend(a.args)
|
|
774
|
+
changed = True
|
|
775
|
+
else:
|
|
776
|
+
flattened.append(a)
|
|
777
|
+
if changed:
|
|
778
|
+
call.args = flattened
|
|
779
|
+
# Re-check rule 1 after flattening.
|
|
780
|
+
if len(call.args) == 1:
|
|
781
|
+
return call.args[0]
|
|
782
|
+
|
|
783
|
+
# Rule 6: drop zero_vector() arguments from bundle.
|
|
784
|
+
if name == "bundle":
|
|
785
|
+
non_zero = [a for a in call.args if not _is_zero_vector_call(a)]
|
|
786
|
+
if len(non_zero) != len(call.args):
|
|
787
|
+
if not non_zero:
|
|
788
|
+
# bundle(zero, zero, ...) → zero_vector()
|
|
789
|
+
return _mk_zero_vector(call.span)
|
|
790
|
+
call.args = non_zero
|
|
791
|
+
if len(call.args) == 1:
|
|
792
|
+
return call.args[0]
|
|
793
|
+
|
|
794
|
+
# Rule 3: compose(compose(a,b), c) → compose(a,b,c).
|
|
795
|
+
if name == "compose":
|
|
796
|
+
flattened = []
|
|
797
|
+
changed = False
|
|
798
|
+
for a in call.args:
|
|
799
|
+
if _is_call_named(a, "compose"):
|
|
800
|
+
flattened.extend(a.args)
|
|
801
|
+
changed = True
|
|
802
|
+
else:
|
|
803
|
+
flattened.append(a)
|
|
804
|
+
if changed:
|
|
805
|
+
call.args = flattened
|
|
806
|
+
|
|
807
|
+
# Rule 4: similarity(a, a) → 1.0 (structurally equal args only).
|
|
808
|
+
if (name == "similarity"
|
|
809
|
+
and len(call.args) == 2
|
|
810
|
+
and _structurally_equal(call.args[0], call.args[1])):
|
|
811
|
+
return _trace("R04 similarity(a, a) -> 1.0", call,
|
|
812
|
+
_mk_float_literal(1.0, call.span))
|
|
813
|
+
|
|
814
|
+
# Rule 5: displacement(a, a) → zero_vector() (structurally equal args).
|
|
815
|
+
if (name == "displacement"
|
|
816
|
+
and len(call.args) == 2
|
|
817
|
+
and _structurally_equal(call.args[0], call.args[1])):
|
|
818
|
+
return _trace("R05 displacement(a, a) -> zero", call,
|
|
819
|
+
_mk_zero_vector(call.span))
|
|
820
|
+
|
|
821
|
+
# Rule 8: unbind(R, bind(R, x)) → x.
|
|
822
|
+
if name == "unbind" and len(call.args) == 2:
|
|
823
|
+
inner = call.args[1]
|
|
824
|
+
if (_is_call_named(inner, "bind", arity=2)
|
|
825
|
+
and _structurally_equal(call.args[0], inner.args[0])):
|
|
826
|
+
return _trace("R08 unbind(R, bind(R, x)) -> x", call,
|
|
827
|
+
inner.args[1])
|
|
828
|
+
|
|
829
|
+
# Rule 9: bind(R, unbind(R, x)) → x.
|
|
830
|
+
if name == "bind" and len(call.args) == 2:
|
|
831
|
+
inner = call.args[1]
|
|
832
|
+
if (_is_call_named(inner, "unbind", arity=2)
|
|
833
|
+
and _structurally_equal(call.args[0], inner.args[0])):
|
|
834
|
+
return _trace("R09 bind(R, unbind(R, x)) -> x", call,
|
|
835
|
+
inner.args[1])
|
|
836
|
+
|
|
837
|
+
# Rule 12: bind(role, zero_vector()) → zero_vector().
|
|
838
|
+
# Q @ 0 = 0 for any orthogonal Q. The rotation of the zero vector is
|
|
839
|
+
# the zero vector. Independent of role.
|
|
840
|
+
if (name == "bind" and len(call.args) == 2
|
|
841
|
+
and _is_zero_vector_call(call.args[1])):
|
|
842
|
+
return _trace("R12 bind(R, zero) -> zero", call,
|
|
843
|
+
_mk_zero_vector(call.span))
|
|
844
|
+
|
|
845
|
+
# Rule 13: unbind(role, zero_vector()) → zero_vector().
|
|
846
|
+
# Q^T @ 0 = 0 by the same argument. Independent of role.
|
|
847
|
+
if (name == "unbind" and len(call.args) == 2
|
|
848
|
+
and _is_zero_vector_call(call.args[1])):
|
|
849
|
+
return _trace("R13 unbind(R, zero) -> zero", call,
|
|
850
|
+
_mk_zero_vector(call.span))
|
|
851
|
+
|
|
852
|
+
# Rule 14: compose with identity_permutation() on either side →
|
|
853
|
+
# drop the identity. `identity_permutation()` is the all-ones
|
|
854
|
+
# vector and `compose` is elementwise multiply, so multiplying
|
|
855
|
+
# by all-ones is the identity. Works after rule 3 has flattened
|
|
856
|
+
# nested composes.
|
|
857
|
+
if name == "compose" and len(call.args) >= 2:
|
|
858
|
+
non_identity = [
|
|
859
|
+
a for a in call.args if not _is_call_named(a, "identity_permutation", arity=0)
|
|
860
|
+
]
|
|
861
|
+
if len(non_identity) != len(call.args):
|
|
862
|
+
if not non_identity:
|
|
863
|
+
# compose(identity, identity, ...) is identity itself.
|
|
864
|
+
return _mk_identity_permutation(call.span)
|
|
865
|
+
if len(non_identity) == 1:
|
|
866
|
+
# compose(x, identity) → x; compose(identity, x) → x.
|
|
867
|
+
return non_identity[0]
|
|
868
|
+
call.args = non_identity
|
|
869
|
+
|
|
870
|
+
# Rule 15: argmax_cosine(query, [single]) → single.
|
|
871
|
+
# Single-candidate argmax has no choice; the literal element is the
|
|
872
|
+
# result regardless of the query. Only fires when the candidates
|
|
873
|
+
# are an ArrayLiteral with exactly one element (structural match
|
|
874
|
+
# against the compile-time shape).
|
|
875
|
+
if name == "argmax_cosine" and len(call.args) == 2:
|
|
876
|
+
candidates = call.args[1]
|
|
877
|
+
if (isinstance(candidates, ast.ArrayLiteral)
|
|
878
|
+
and len(candidates.elements) == 1):
|
|
879
|
+
return _trace("R15 argmax_cosine(q, [x]) -> x", call,
|
|
880
|
+
candidates.elements[0])
|
|
881
|
+
|
|
882
|
+
return call
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _mk_identity_permutation(span):
|
|
886
|
+
"""Synthesize an `identity_permutation()` call for rule 14's empty
|
|
887
|
+
compose case."""
|
|
888
|
+
return ast.Call(
|
|
889
|
+
callee=ast.Identifier(name="identity_permutation", span=span),
|
|
890
|
+
type_args=[],
|
|
891
|
+
args=[],
|
|
892
|
+
span=span,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
# ---------------------------------------------------------------------------
|
|
897
|
+
# Internal: binary-op rewrites
|
|
898
|
+
# ---------------------------------------------------------------------------
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _rewrite_binary(expr: ast.BinaryOp):
|
|
902
|
+
"""Arithmetic constant folding + zero-vector absorption."""
|
|
903
|
+
op = expr.op
|
|
904
|
+
left = expr.left
|
|
905
|
+
right = expr.right
|
|
906
|
+
|
|
907
|
+
# Rule 7: zero-vector absorption in +/-.
|
|
908
|
+
if op == "+":
|
|
909
|
+
if _is_zero_vector_call(left):
|
|
910
|
+
return _trace("R07 zero + x -> x", expr, right)
|
|
911
|
+
if _is_zero_vector_call(right):
|
|
912
|
+
return _trace("R07 x + zero -> x", expr, left)
|
|
913
|
+
if op == "-":
|
|
914
|
+
if _is_zero_vector_call(right):
|
|
915
|
+
return _trace("R07 x - zero -> x", expr, left)
|
|
916
|
+
# zero - x is not x, so no rewrite on the left side of subtract.
|
|
917
|
+
|
|
918
|
+
# Complex-literal folding: `re ± im·i` at compile time. Turns the
|
|
919
|
+
# two-literal expression into a single ComplexLiteral so the
|
|
920
|
+
# codegen emits one _VSA.make_complex(re, im) allocation instead
|
|
921
|
+
# of a vector-add over two partial literals. Handles int+imag,
|
|
922
|
+
# float+imag, imag+int, imag+float, and the four subtraction
|
|
923
|
+
# variants. Imag+imag collapses to a single ImaginaryLiteral.
|
|
924
|
+
if op in ("+", "-"):
|
|
925
|
+
folded = _fold_complex_literal(left, right, op, expr.span)
|
|
926
|
+
if folded is not None:
|
|
927
|
+
return folded
|
|
928
|
+
|
|
929
|
+
# Rule 11: numeric constant folding for scalar literals.
|
|
930
|
+
l_num = _numeric_value(left)
|
|
931
|
+
r_num = _numeric_value(right)
|
|
932
|
+
|
|
933
|
+
# Identity-element rules first — preserve the original operand
|
|
934
|
+
# node so its type/span stays. (Pre-dates the step-6 full fold
|
|
935
|
+
# and has tests asserting specific node types survive.)
|
|
936
|
+
if op == "+":
|
|
937
|
+
if l_num == 0:
|
|
938
|
+
return right
|
|
939
|
+
if r_num == 0:
|
|
940
|
+
return left
|
|
941
|
+
elif op == "-":
|
|
942
|
+
if r_num == 0:
|
|
943
|
+
return left
|
|
944
|
+
elif op == "*":
|
|
945
|
+
if l_num == 0 or r_num == 0:
|
|
946
|
+
return _mk_float_literal(0.0, expr.span)
|
|
947
|
+
if l_num == 1:
|
|
948
|
+
return right
|
|
949
|
+
if r_num == 1:
|
|
950
|
+
return left
|
|
951
|
+
elif op == "/":
|
|
952
|
+
if r_num == 1:
|
|
953
|
+
return left
|
|
954
|
+
|
|
955
|
+
# Step 6 — full literal-on-literal fold: `2 + 3` → `5`,
|
|
956
|
+
# `0.7 * 0.3` → `0.21`, `(poly on literal args) * 0.5` → single
|
|
957
|
+
# constant. Applies when both operands have a numeric-literal
|
|
958
|
+
# value AND neither is an identity element (caught above). This
|
|
959
|
+
# is the starter for the fusion pass: inlined polynomial bodies
|
|
960
|
+
# where all operands are literals collapse to a single literal
|
|
961
|
+
# at compile time. `logical_and(0.7, 0.3)` inlines to the
|
|
962
|
+
# polynomial on `0.7` / `0.3`; this fold collapses that chain to
|
|
963
|
+
# `FloatLiteral(0.33705)`, so the runtime sees a constant
|
|
964
|
+
# (wrapped in `make_truth` by the fuzzy-literal coercion) rather
|
|
965
|
+
# than a tree of arithmetic ops.
|
|
966
|
+
if l_num is not None and r_num is not None:
|
|
967
|
+
try:
|
|
968
|
+
if op == "+":
|
|
969
|
+
return _trace("R16 fold (lit + lit)", expr,
|
|
970
|
+
_lit_from_num(l_num + r_num, expr.span))
|
|
971
|
+
if op == "-":
|
|
972
|
+
return _trace("R16 fold (lit - lit)", expr,
|
|
973
|
+
_lit_from_num(l_num - r_num, expr.span))
|
|
974
|
+
if op == "*":
|
|
975
|
+
return _trace("R16 fold (lit * lit)", expr,
|
|
976
|
+
_lit_from_num(l_num * r_num, expr.span))
|
|
977
|
+
if op == "/" and r_num != 0:
|
|
978
|
+
return _trace("R16 fold (lit / lit)", expr,
|
|
979
|
+
_lit_from_num(l_num / r_num, expr.span))
|
|
980
|
+
except (ZeroDivisionError, OverflowError):
|
|
981
|
+
pass # fall through; leave expr unchanged
|
|
982
|
+
|
|
983
|
+
return expr
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _lit_from_num(value, span: SourceSpan):
|
|
987
|
+
"""Pick IntLiteral vs FloatLiteral based on the value's type.
|
|
988
|
+
Integer-valued floats that came from a pure int computation become
|
|
989
|
+
IntLiteral for pleasanter emission; anything with a fractional
|
|
990
|
+
component stays FloatLiteral."""
|
|
991
|
+
if isinstance(value, int):
|
|
992
|
+
return ast.IntLiteral(span=span, value=value)
|
|
993
|
+
if isinstance(value, float) and value.is_integer():
|
|
994
|
+
# Keep as float — mixing int and float folds should produce
|
|
995
|
+
# float per language semantics, even if the math happens to
|
|
996
|
+
# land on an integer value.
|
|
997
|
+
return _mk_float_literal(value, span)
|
|
998
|
+
return _mk_float_literal(float(value), span)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _numeric_value(expr):
|
|
1002
|
+
"""Extract the numeric value of a literal node, or None if not a literal.
|
|
1003
|
+
|
|
1004
|
+
Returns an int or float depending on the literal type; callers can
|
|
1005
|
+
compare against 0 or 1 directly. Unwraps `Parenthesized(...)`
|
|
1006
|
+
wrappers so a literal inside parens still folds.
|
|
1007
|
+
"""
|
|
1008
|
+
while isinstance(expr, ast.Parenthesized):
|
|
1009
|
+
expr = expr.inner
|
|
1010
|
+
if isinstance(expr, ast.IntLiteral):
|
|
1011
|
+
return expr.value
|
|
1012
|
+
if isinstance(expr, ast.FloatLiteral):
|
|
1013
|
+
return expr.value
|
|
1014
|
+
return None
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _fold_complex_literal(left, right, op: str, span):
|
|
1018
|
+
"""Compile-time fold of `re ± im·i` into a single ComplexLiteral.
|
|
1019
|
+
|
|
1020
|
+
Recognises the four pairings (number ± imag, imag ± number) and
|
|
1021
|
+
the imag ± imag case. Returns the folded node or None if the
|
|
1022
|
+
operands don't form a complex-literal pattern.
|
|
1023
|
+
"""
|
|
1024
|
+
assert op in ("+", "-")
|
|
1025
|
+
sign = 1.0 if op == "+" else -1.0
|
|
1026
|
+
l_num = _numeric_value(left)
|
|
1027
|
+
r_num = _numeric_value(right)
|
|
1028
|
+
l_is_imag = isinstance(left, ast.ImaginaryLiteral)
|
|
1029
|
+
r_is_imag = isinstance(right, ast.ImaginaryLiteral)
|
|
1030
|
+
|
|
1031
|
+
# number ± imag → Complex(number, ±imag)
|
|
1032
|
+
if l_num is not None and r_is_imag:
|
|
1033
|
+
return ast.ComplexLiteral(
|
|
1034
|
+
re=float(l_num), im=sign * float(right.value), span=span,
|
|
1035
|
+
)
|
|
1036
|
+
# imag ± number → Complex(±number, imag) (because `5i - 3 = -3 + 5i`)
|
|
1037
|
+
if l_is_imag and r_num is not None:
|
|
1038
|
+
return ast.ComplexLiteral(
|
|
1039
|
+
re=sign * float(r_num), im=float(left.value), span=span,
|
|
1040
|
+
)
|
|
1041
|
+
# imag ± imag → ImaginaryLiteral(sum/diff).
|
|
1042
|
+
if l_is_imag and r_is_imag:
|
|
1043
|
+
return ast.ImaginaryLiteral(
|
|
1044
|
+
value=float(left.value) + sign * float(right.value), span=span,
|
|
1045
|
+
)
|
|
1046
|
+
return None
|