algo2code 0.2.1__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.
- algo2code/__init__.py +72 -0
- algo2code/algo_parser.py +458 -0
- algo2code/ast_nodes.py +171 -0
- algo2code/backends/__init__.py +1 -0
- algo2code/backends/c_petsc_codegen.py +1 -0
- algo2code/backends/numpy_codegen.py +1 -0
- algo2code/backends/taichi_codegen.py +859 -0
- algo2code/errors.py +27 -0
- algo2code/expr_parser.py +598 -0
- algo2code/library/.gitkeep +0 -0
- algo2code/library/__init__.py +19 -0
- algo2code/library/pcg.py +127 -0
- algo2code/library/radial_return_j2.py +90 -0
- algo2code/library/radial_return_j2_kinematic.py +89 -0
- algo2code/library/radial_return_j2_mixed.py +91 -0
- algo2code/type_inference.py +219 -0
- algo2code-0.2.1.dist-info/METADATA +30 -0
- algo2code-0.2.1.dist-info/RECORD +20 -0
- algo2code-0.2.1.dist-info/WHEEL +4 -0
- algo2code-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Taichi code generator.
|
|
3
|
+
|
|
4
|
+
Takes a typed Algorithm AST and emits:
|
|
5
|
+
1. Taichi kernel functions for vector/matrix operations (matvec, dot, axpy, norm)
|
|
6
|
+
2. A Python driver function that calls the kernels in the right order
|
|
7
|
+
|
|
8
|
+
Design decisions:
|
|
9
|
+
- Vector/matrix variables are allocated as Taichi fields inside the driver
|
|
10
|
+
- The loop driver stays in Python scope (Taichi doesn't support
|
|
11
|
+
dynamic convergence checks inside kernels well)
|
|
12
|
+
- Each heavy linear-algebra op gets its own @ti.kernel
|
|
13
|
+
- Scalar arithmetic stays in the driver
|
|
14
|
+
- Vector assignment (p = z) emits _copy(z, p), not Python reference aliasing
|
|
15
|
+
|
|
16
|
+
Import modes (``runtime`` parameter of :func:`generate_taichi`):
|
|
17
|
+
- ``"inline"`` (default): emit private ``@ti.kernel`` definitions inline in
|
|
18
|
+
each generated file — backward-compatible, no runtime dependency.
|
|
19
|
+
- ``"ti_runtime"``: emit ``from ti_runtime import vector_ops as _v`` and call
|
|
20
|
+
the shared primitives for ``dot``/``norm``/``copy``/``vec_add``; only
|
|
21
|
+
``_matvec`` remains inlined (the matrix-free operator seam is P2-2's
|
|
22
|
+
concern). algo2code itself never imports ``ti_runtime`` — it only *emits*
|
|
23
|
+
the import line.
|
|
24
|
+
|
|
25
|
+
Compatibility note (``"ti_runtime"`` mode):
|
|
26
|
+
``ti_runtime.vector_ops.dot`` uses ``x[I].dot(y[I])`` which requires
|
|
27
|
+
``ti.Vector.field`` arguments. algo2code emits ``ti.field(ti.f64, shape=n)``
|
|
28
|
+
scalar fields, so ``_v.dot``/``_v.norm2`` will raise at Taichi JIT time if
|
|
29
|
+
the consumer passes scalar fields. Callers that use vector fields (e.g.
|
|
30
|
+
mechdsl FEM drivers) are unaffected. This is a known field-model mismatch;
|
|
31
|
+
resolving it (either by changing the emitted field model to ``ti.Vector.field``
|
|
32
|
+
or by adding scalar variants to ti_runtime) is out of scope for P2-1.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import contextlib
|
|
38
|
+
|
|
39
|
+
from ..ast_nodes import (
|
|
40
|
+
Algorithm,
|
|
41
|
+
Assign,
|
|
42
|
+
BinOp,
|
|
43
|
+
Branch,
|
|
44
|
+
Break,
|
|
45
|
+
Expr,
|
|
46
|
+
ForLoop,
|
|
47
|
+
FuncCall,
|
|
48
|
+
Number,
|
|
49
|
+
Return,
|
|
50
|
+
Stmt,
|
|
51
|
+
UnaryOp,
|
|
52
|
+
Var,
|
|
53
|
+
VarType,
|
|
54
|
+
WhileLoop,
|
|
55
|
+
)
|
|
56
|
+
from ..errors import UnsupportedConstructError
|
|
57
|
+
|
|
58
|
+
# Literal sentinel values for the ``runtime`` parameter.
|
|
59
|
+
RUNTIME_INLINE = "inline"
|
|
60
|
+
RUNTIME_TI_RUNTIME = "ti_runtime"
|
|
61
|
+
|
|
62
|
+
# ── Variable name sanitization ───────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
_GREEK_MAP = {
|
|
65
|
+
"alpha": "alpha",
|
|
66
|
+
"beta": "beta",
|
|
67
|
+
"gamma": "gamma",
|
|
68
|
+
"delta": "delta",
|
|
69
|
+
"epsilon": "eps",
|
|
70
|
+
"varepsilon": "eps",
|
|
71
|
+
"zeta": "zeta",
|
|
72
|
+
"eta": "eta",
|
|
73
|
+
"theta": "theta",
|
|
74
|
+
"iota": "iota",
|
|
75
|
+
"kappa": "kappa",
|
|
76
|
+
"lambda": "lam",
|
|
77
|
+
"mu": "mu",
|
|
78
|
+
"nu": "nu",
|
|
79
|
+
"xi": "xi",
|
|
80
|
+
"pi": "pi_val",
|
|
81
|
+
"rho": "rho",
|
|
82
|
+
"sigma": "sigma",
|
|
83
|
+
"tau": "tau",
|
|
84
|
+
"phi": "phi",
|
|
85
|
+
"varphi": "phi",
|
|
86
|
+
"chi": "chi",
|
|
87
|
+
"psi": "psi",
|
|
88
|
+
"omega": "omega",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sanitize(name: str) -> str:
|
|
93
|
+
"""Convert a LaTeX variable name to a valid Python identifier."""
|
|
94
|
+
if name in _GREEK_MAP:
|
|
95
|
+
return _GREEK_MAP[name]
|
|
96
|
+
name = name.replace("{", "").replace("}", "").replace("\\", "")
|
|
97
|
+
name = name.replace(" ", "_").replace("-", "_")
|
|
98
|
+
if name in ("lambda", "in", "is", "not", "and", "or", "from", "import"):
|
|
99
|
+
name = name + "_"
|
|
100
|
+
return name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _var_name(v: Var) -> str:
|
|
104
|
+
"""Get the Python name for a Var node."""
|
|
105
|
+
base = _sanitize(v.name)
|
|
106
|
+
if v.subscript:
|
|
107
|
+
sub = _sanitize(v.subscript)
|
|
108
|
+
return f"{base}_{sub}"
|
|
109
|
+
return base
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Kernel collector ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class KernelCollector:
|
|
116
|
+
"""Walk the AST and identify which standard kernels are needed."""
|
|
117
|
+
|
|
118
|
+
def __init__(self):
|
|
119
|
+
self.needed_kernels: set[str] = set()
|
|
120
|
+
|
|
121
|
+
def scan(self, stmts: list[Stmt]):
|
|
122
|
+
for stmt in stmts:
|
|
123
|
+
self._scan_stmt(stmt)
|
|
124
|
+
|
|
125
|
+
def _scan_stmt(self, stmt: Stmt):
|
|
126
|
+
if isinstance(stmt, Assign):
|
|
127
|
+
self._scan_expr(stmt.value)
|
|
128
|
+
if (
|
|
129
|
+
stmt.target.inferred_type == VarType.VECTOR
|
|
130
|
+
and isinstance(stmt.value, Var)
|
|
131
|
+
and stmt.value.inferred_type == VarType.VECTOR
|
|
132
|
+
):
|
|
133
|
+
self.needed_kernels.add("copy")
|
|
134
|
+
elif isinstance(stmt, ForLoop):
|
|
135
|
+
self.scan(stmt.body)
|
|
136
|
+
elif isinstance(stmt, WhileLoop):
|
|
137
|
+
self._scan_expr(stmt.condition)
|
|
138
|
+
self.scan(stmt.body)
|
|
139
|
+
elif isinstance(stmt, Branch):
|
|
140
|
+
self._scan_expr(stmt.condition)
|
|
141
|
+
self.scan(stmt.if_body)
|
|
142
|
+
for cond, body in stmt.elif_branches:
|
|
143
|
+
self._scan_expr(cond)
|
|
144
|
+
self.scan(body)
|
|
145
|
+
self.scan(stmt.else_body)
|
|
146
|
+
elif isinstance(stmt, Return):
|
|
147
|
+
for v in stmt.values:
|
|
148
|
+
self._scan_expr(v)
|
|
149
|
+
|
|
150
|
+
def _scan_expr(self, expr: Expr):
|
|
151
|
+
if isinstance(expr, BinOp):
|
|
152
|
+
self._scan_expr(expr.left)
|
|
153
|
+
self._scan_expr(expr.right)
|
|
154
|
+
if expr.op == "dot":
|
|
155
|
+
self.needed_kernels.add("dot")
|
|
156
|
+
elif expr.op == "matvec":
|
|
157
|
+
# A matrix-free callable operator (`% type A callable`, §8.3)
|
|
158
|
+
# lowers `A · p` to an in-place call A(out, p); it needs no
|
|
159
|
+
# dense `_matvec` kernel. Only a stored MATRIX operand does.
|
|
160
|
+
if expr.left.inferred_type != VarType.CALLABLE:
|
|
161
|
+
self.needed_kernels.add("matvec")
|
|
162
|
+
elif expr.op == "scale" or (
|
|
163
|
+
expr.op in ("+", "-") and expr.inferred_type == VarType.VECTOR
|
|
164
|
+
):
|
|
165
|
+
self.needed_kernels.add("vec_add")
|
|
166
|
+
elif isinstance(expr, UnaryOp):
|
|
167
|
+
self._scan_expr(expr.operand)
|
|
168
|
+
if expr.op == "norm":
|
|
169
|
+
self.needed_kernels.add("norm")
|
|
170
|
+
elif isinstance(expr, FuncCall):
|
|
171
|
+
for arg in expr.args:
|
|
172
|
+
self._scan_expr(arg)
|
|
173
|
+
|
|
174
|
+
def emit_kernels(self, runtime: str = RUNTIME_INLINE) -> str:
|
|
175
|
+
"""Emit kernel definitions (inline mode) or nothing (ti_runtime mode).
|
|
176
|
+
|
|
177
|
+
In ``"ti_runtime"`` mode the shared primitives (dot/norm/copy/vec_add)
|
|
178
|
+
are called via the ``_v`` alias imported from ``ti_runtime.vector_ops``.
|
|
179
|
+
Only ``_matvec`` has no ti_runtime equivalent and remains inlined
|
|
180
|
+
(the matrix-free operator seam is P2-2's concern).
|
|
181
|
+
"""
|
|
182
|
+
lines = []
|
|
183
|
+
if runtime == RUNTIME_TI_RUNTIME:
|
|
184
|
+
# dot/norm/copy/vec_add are routed to ti_runtime; only emit _matvec.
|
|
185
|
+
if "matvec" in self.needed_kernels:
|
|
186
|
+
lines.append(_K_MATVEC)
|
|
187
|
+
else:
|
|
188
|
+
if "dot" in self.needed_kernels:
|
|
189
|
+
lines.append(_K_DOT)
|
|
190
|
+
if "norm" in self.needed_kernels:
|
|
191
|
+
lines.append(_K_NORM)
|
|
192
|
+
if "matvec" in self.needed_kernels:
|
|
193
|
+
lines.append(_K_MATVEC)
|
|
194
|
+
if "vec_add" in self.needed_kernels:
|
|
195
|
+
lines.append(_K_VEC_ADD)
|
|
196
|
+
if "copy" in self.needed_kernels:
|
|
197
|
+
lines.append(_K_COPY)
|
|
198
|
+
return "\n\n".join(lines)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── Kernel templates ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
_K_DOT = """@ti.kernel
|
|
204
|
+
def _dot(a: ti.template(), b: ti.template()) -> ti.f64:
|
|
205
|
+
result = 0.0
|
|
206
|
+
for i in a:
|
|
207
|
+
result += a[i] * b[i]
|
|
208
|
+
return result"""
|
|
209
|
+
|
|
210
|
+
_K_NORM = """@ti.kernel
|
|
211
|
+
def _norm(a: ti.template()) -> ti.f64:
|
|
212
|
+
result = 0.0
|
|
213
|
+
for i in a:
|
|
214
|
+
result += a[i] * a[i]
|
|
215
|
+
return ti.sqrt(result)"""
|
|
216
|
+
|
|
217
|
+
_K_MATVEC = """@ti.kernel
|
|
218
|
+
def _matvec(A: ti.template(), x: ti.template(), out: ti.template()):
|
|
219
|
+
for i in out:
|
|
220
|
+
s = 0.0
|
|
221
|
+
for j in range(x.shape[0]):
|
|
222
|
+
s += A[i, j] * x[j]
|
|
223
|
+
out[i] = s"""
|
|
224
|
+
|
|
225
|
+
_K_VEC_ADD = '''@ti.kernel
|
|
226
|
+
def _vec_add(alpha: ti.f64, x: ti.template(), beta: ti.f64,
|
|
227
|
+
y: ti.template(), out: ti.template()):
|
|
228
|
+
"""out[i] = alpha*x[i] + beta*y[i]"""
|
|
229
|
+
for i in out:
|
|
230
|
+
out[i] = alpha * x[i] + beta * y[i]'''
|
|
231
|
+
|
|
232
|
+
_K_COPY = """@ti.kernel
|
|
233
|
+
def _copy(src: ti.template(), dst: ti.template()):
|
|
234
|
+
for i in dst:
|
|
235
|
+
dst[i] = src[i]"""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ── Variable scanner ─────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _collect_vector_vars(algo: Algorithm) -> set[str]:
|
|
242
|
+
"""Find all vector-typed variable names that need field allocation."""
|
|
243
|
+
vecs = set()
|
|
244
|
+
for name, vtype in algo.type_annotations.items():
|
|
245
|
+
if vtype == VarType.VECTOR:
|
|
246
|
+
vecs.add(_sanitize(name))
|
|
247
|
+
return vecs
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _collect_arg_names(algo: Algorithm) -> set[str]:
|
|
251
|
+
return {name for name, _ in algo.args}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ── Code emitter ─────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TaichiEmitter:
|
|
258
|
+
"""Emit complete Taichi code from a typed Algorithm AST.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
algo:
|
|
263
|
+
Typed Algorithm AST to emit.
|
|
264
|
+
runtime:
|
|
265
|
+
Import mode — ``"inline"`` (default) or ``"ti_runtime"``. See module
|
|
266
|
+
docstring for the full description of each mode.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(self, algo: Algorithm, runtime: str = RUNTIME_INLINE):
|
|
270
|
+
self.algo = algo
|
|
271
|
+
self.runtime = runtime
|
|
272
|
+
self.lines: list[str] = []
|
|
273
|
+
self._indent = 0
|
|
274
|
+
self._temp_counter = 0
|
|
275
|
+
self._needed_temp_count = 0
|
|
276
|
+
self._peak_temps = 0
|
|
277
|
+
self._arg_names: set[str] = set()
|
|
278
|
+
self._vector_vars: set[str] = set()
|
|
279
|
+
|
|
280
|
+
def emit(self) -> str:
|
|
281
|
+
"""Generate the complete Taichi source."""
|
|
282
|
+
self._arg_names = _collect_arg_names(self.algo)
|
|
283
|
+
self._vector_vars = _collect_vector_vars(self.algo)
|
|
284
|
+
|
|
285
|
+
collector = KernelCollector()
|
|
286
|
+
collector.scan(self.algo.body)
|
|
287
|
+
|
|
288
|
+
self._pre_scan_temps(self.algo.body)
|
|
289
|
+
|
|
290
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
291
|
+
self._check_runtime_mode_supported(collector)
|
|
292
|
+
|
|
293
|
+
parts = []
|
|
294
|
+
parts.append("import taichi as ti\n")
|
|
295
|
+
parts.append("ti.init(arch=ti.gpu, default_fp=ti.f64)\n")
|
|
296
|
+
|
|
297
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
298
|
+
parts.append("from ti_runtime import vector_ops as _v\n")
|
|
299
|
+
|
|
300
|
+
kernels = collector.emit_kernels(runtime=self.runtime)
|
|
301
|
+
if kernels:
|
|
302
|
+
parts.append("# ── Taichi kernels " + "─" * 56)
|
|
303
|
+
parts.append(kernels)
|
|
304
|
+
parts.append("")
|
|
305
|
+
|
|
306
|
+
parts.append("# ── Solver driver " + "─" * 57)
|
|
307
|
+
parts.append(self._emit_driver())
|
|
308
|
+
|
|
309
|
+
return "\n\n".join(parts) + "\n"
|
|
310
|
+
|
|
311
|
+
def _pre_scan_temps(self, stmts: list[Stmt]):
|
|
312
|
+
"""Discover how many temporary vector fields the driver needs.
|
|
313
|
+
|
|
314
|
+
Rather than re-deriving the count from a parallel traversal (which drifts
|
|
315
|
+
from the real lowering — the F1 root cause was exactly such a mismatch),
|
|
316
|
+
we *dry-run* the actual body emission into a throwaway buffer. Every
|
|
317
|
+
``_get_temp`` call updates ``_peak_temps``; the temp counter resets per
|
|
318
|
+
statement (temps are scratch within one statement and reused across
|
|
319
|
+
statements), so the peak is the number of fields to allocate once.
|
|
320
|
+
``_emit_driver`` overwrites ``lines``/``indent``/``temp_counter`` for the
|
|
321
|
+
real pass, so no save/restore is needed here.
|
|
322
|
+
"""
|
|
323
|
+
self.lines = []
|
|
324
|
+
self._indent = 0
|
|
325
|
+
self._temp_counter = 0
|
|
326
|
+
self._peak_temps = 0
|
|
327
|
+
# A genuinely un-lowerable construct will raise again — and be reported —
|
|
328
|
+
# during the real emission pass, so don't let the dry run fail here.
|
|
329
|
+
with contextlib.suppress(UnsupportedConstructError):
|
|
330
|
+
self._emit_block(stmts)
|
|
331
|
+
self._needed_temp_count = self._peak_temps
|
|
332
|
+
|
|
333
|
+
def _emit_driver(self) -> str:
|
|
334
|
+
self.lines = []
|
|
335
|
+
self._indent = 0
|
|
336
|
+
self._temp_counter = 0
|
|
337
|
+
|
|
338
|
+
args = self._build_arg_list()
|
|
339
|
+
self._write(f"def {self.algo.name}({args}):")
|
|
340
|
+
self._indent += 1
|
|
341
|
+
|
|
342
|
+
self._write('"""')
|
|
343
|
+
self._write("Auto-generated from LaTeX algorithmic environment.")
|
|
344
|
+
self._write("Backend: Taichi (GPU)")
|
|
345
|
+
self._write('"""')
|
|
346
|
+
|
|
347
|
+
vec_arg = self._first_vector_arg()
|
|
348
|
+
local_vecs = self._vector_vars - self._arg_names
|
|
349
|
+
requires_vector_length = bool(local_vecs) or self._needed_temp_count > 0
|
|
350
|
+
needs_n = requires_vector_length or vec_arg is not None
|
|
351
|
+
if needs_n:
|
|
352
|
+
if vec_arg:
|
|
353
|
+
self._write(f"n = {vec_arg}.shape[0]")
|
|
354
|
+
elif requires_vector_length:
|
|
355
|
+
self._write(
|
|
356
|
+
'raise ValueError("Cannot infer vector length n for this generated Taichi driver: '
|
|
357
|
+
"the algorithm allocates local vectors or temporary vector fields, but no vector "
|
|
358
|
+
'argument is available to determine n.")'
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
# Emitting ``n = b.shape[0]`` unconditionally would break scalar/matrix-only
|
|
362
|
+
# algorithms with no vector argument; keep a harmless marker only when no
|
|
363
|
+
# vector-sized allocation depends on n.
|
|
364
|
+
self._write("n = 0 # no vector arg present; scalar/matrix-only algorithm")
|
|
365
|
+
|
|
366
|
+
if local_vecs or self._needed_temp_count > 0:
|
|
367
|
+
self._write("")
|
|
368
|
+
self._write("# Allocate working vectors")
|
|
369
|
+
alloc = self._vector_field_alloc(vec_arg)
|
|
370
|
+
for name in sorted(local_vecs):
|
|
371
|
+
self._write(f"{name} = {alloc}")
|
|
372
|
+
for i in range(self._needed_temp_count):
|
|
373
|
+
self._write(f"_tmp{i} = {alloc}")
|
|
374
|
+
|
|
375
|
+
self._write("")
|
|
376
|
+
|
|
377
|
+
self._emit_block(self.algo.body)
|
|
378
|
+
|
|
379
|
+
self._indent -= 1
|
|
380
|
+
return "\n".join(self.lines)
|
|
381
|
+
|
|
382
|
+
def _first_vector_arg(self) -> str | None:
|
|
383
|
+
for name, vtype in self.algo.args:
|
|
384
|
+
if vtype == VarType.VECTOR:
|
|
385
|
+
return name
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _vector_field_alloc(self, vec_arg: str | None) -> str:
|
|
389
|
+
"""Return the field-allocation expression for a local/temp working vector.
|
|
390
|
+
|
|
391
|
+
Inline mode keeps the historical scalar layout ``ti.field(ti.f64,
|
|
392
|
+
shape=n)`` — byte-stable, so existing consumers and goldens are
|
|
393
|
+
unaffected. ``ti_runtime`` mode instead allocates ``ti.Vector.field``
|
|
394
|
+
locals matching the *vector argument's* component layout (``<arg>.n``):
|
|
395
|
+
the ``ti_runtime`` ``dot``/``norm2`` primitives use ``x[I].dot(y[I])``,
|
|
396
|
+
which requires vector fields. This is what lets the matrix-free seam
|
|
397
|
+
(P2-2) run end-to-end against ``ti_runtime`` (resolving the field-model
|
|
398
|
+
mismatch P2-1 documented). With no vector argument the component count
|
|
399
|
+
is unknown, so fall back to the scalar layout.
|
|
400
|
+
"""
|
|
401
|
+
if self.runtime == RUNTIME_TI_RUNTIME and vec_arg is not None:
|
|
402
|
+
return f"ti.Vector.field({vec_arg}.n, ti.f64, shape=n)"
|
|
403
|
+
return "ti.field(ti.f64, shape=n)"
|
|
404
|
+
|
|
405
|
+
def _check_runtime_mode_supported(self, collector: KernelCollector) -> None:
|
|
406
|
+
"""Fail loud on ``ti_runtime``-mode inputs that would emit non-runnable code.
|
|
407
|
+
|
|
408
|
+
Runtime mode targets the matrix-free seam over ``ti.Vector.field`` DOF
|
|
409
|
+
vectors, so two preconditions must hold (otherwise the emitter would
|
|
410
|
+
silently produce code that crashes at Taichi JIT):
|
|
411
|
+
|
|
412
|
+
* **callable operator** — a stored ``MATRIX`` operand lowers to a dense,
|
|
413
|
+
scalar-indexed ``_matvec``, incompatible with the ``ti.Vector.field``
|
|
414
|
+
locals and the ``ti_runtime`` ``dot``/``norm2`` reductions
|
|
415
|
+
(``x[I].dot(y[I])``). Declare the operator ``callable``
|
|
416
|
+
(e.g. ``% type A callable``) to use the in-place ``A(out, x)`` seam.
|
|
417
|
+
* **a vector argument** — needed to size the ``ti.Vector.field`` locals
|
|
418
|
+
(``<arg>.n``). Without one the locals fall back to the scalar layout,
|
|
419
|
+
on which ``_v.dot``/``_v.norm2`` cannot run.
|
|
420
|
+
|
|
421
|
+
Both are rejected here, before emission, with the specific reason.
|
|
422
|
+
"""
|
|
423
|
+
if "matvec" in collector.needed_kernels:
|
|
424
|
+
raise UnsupportedConstructError(
|
|
425
|
+
"runtime='ti_runtime' requires a matrix-free callable operator: "
|
|
426
|
+
"declare the system operator callable (e.g. `% type A callable`). "
|
|
427
|
+
"A matrix-typed operator lowers to a dense scalar-indexed matvec, "
|
|
428
|
+
"incompatible with the ti.Vector.field layout used in runtime mode."
|
|
429
|
+
)
|
|
430
|
+
uses_vectors = (
|
|
431
|
+
bool(self._vector_vars - self._arg_names)
|
|
432
|
+
or self._needed_temp_count > 0
|
|
433
|
+
or bool({"dot", "copy", "vec_add"} & collector.needed_kernels)
|
|
434
|
+
)
|
|
435
|
+
if uses_vectors and self._first_vector_arg() is None:
|
|
436
|
+
raise UnsupportedConstructError(
|
|
437
|
+
"runtime='ti_runtime' needs a vector argument to size its "
|
|
438
|
+
"ti.Vector.field working vectors (`<arg>.n`); none was declared."
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _build_arg_list(self) -> str:
|
|
442
|
+
if self.algo.args:
|
|
443
|
+
return ", ".join(name for name, _ in self.algo.args)
|
|
444
|
+
return "A, b, x, M_inv=None, tol=1e-10, maxiter=1000"
|
|
445
|
+
|
|
446
|
+
def _write(self, text: str):
|
|
447
|
+
self.lines.append(" " * self._indent + text)
|
|
448
|
+
|
|
449
|
+
def _emit_block(self, stmts: list[Stmt]):
|
|
450
|
+
for stmt in stmts:
|
|
451
|
+
self._emit_stmt(stmt)
|
|
452
|
+
|
|
453
|
+
def _emit_stmt(self, stmt: Stmt):
|
|
454
|
+
# Temps are scratch within a single statement; reset so they are reused
|
|
455
|
+
# across statements (only the per-statement peak is allocated).
|
|
456
|
+
self._temp_counter = 0
|
|
457
|
+
if isinstance(stmt, Assign):
|
|
458
|
+
self._emit_assign(stmt)
|
|
459
|
+
elif isinstance(stmt, ForLoop):
|
|
460
|
+
self._emit_for(stmt)
|
|
461
|
+
elif isinstance(stmt, WhileLoop):
|
|
462
|
+
self._emit_while(stmt)
|
|
463
|
+
elif isinstance(stmt, Branch):
|
|
464
|
+
self._emit_branch(stmt)
|
|
465
|
+
elif isinstance(stmt, Return):
|
|
466
|
+
self._emit_return(stmt)
|
|
467
|
+
elif isinstance(stmt, Break):
|
|
468
|
+
self._write("break")
|
|
469
|
+
|
|
470
|
+
# ── Assignment ───────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
def _emit_assign(self, stmt: Assign):
|
|
473
|
+
target = _var_name(stmt.target)
|
|
474
|
+
target_type = self.algo.type_annotations.get(stmt.target.display, stmt.target.inferred_type)
|
|
475
|
+
|
|
476
|
+
# Vector ← simple vector: copy
|
|
477
|
+
if (
|
|
478
|
+
target_type == VarType.VECTOR
|
|
479
|
+
and isinstance(stmt.value, Var)
|
|
480
|
+
and stmt.value.inferred_type == VarType.VECTOR
|
|
481
|
+
):
|
|
482
|
+
src = _var_name(stmt.value)
|
|
483
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
484
|
+
# ti_runtime.vector_ops.copy(dst, src) — reversed arg order
|
|
485
|
+
self._write(f"_v.copy({target}, {src})")
|
|
486
|
+
else:
|
|
487
|
+
self._write(f"_copy({src}, {target})")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# Vector ← callable(args): in-place e.g. M_inv(r, z) not z = M_inv(r)
|
|
491
|
+
if target_type == VarType.VECTOR and isinstance(stmt.value, FuncCall):
|
|
492
|
+
func_code = self._emit_func_call_name(stmt.value)
|
|
493
|
+
args = ", ".join(self._emit_expr_str(a) for a in stmt.value.args)
|
|
494
|
+
self._write(f"{func_code}({args}, {target})")
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
value_code = self._emit_expr(stmt.value, target_var=target)
|
|
498
|
+
if value_code is not None:
|
|
499
|
+
self._write(f"{target} = {value_code}")
|
|
500
|
+
|
|
501
|
+
# ── Expression emission ──────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
def _emit_expr_str(self, expr: Expr) -> str:
|
|
504
|
+
"""Like _emit_expr but asserts a value is returned (no target_var side-effect path)."""
|
|
505
|
+
result = self._emit_expr(expr)
|
|
506
|
+
assert result is not None, f"expression emitted no value: {expr!r}"
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
def _emit_expr(self, expr: Expr, target_var: str | None = None) -> str | None:
|
|
510
|
+
if isinstance(expr, Number):
|
|
511
|
+
v = expr.value
|
|
512
|
+
return str(int(v)) if v == int(v) else str(v)
|
|
513
|
+
if isinstance(expr, Var):
|
|
514
|
+
return _var_name(expr)
|
|
515
|
+
if isinstance(expr, UnaryOp):
|
|
516
|
+
# A vector-valued negation (w = -v) must lower to a kernel call, not
|
|
517
|
+
# Python field arithmetic. Scalar/transpose/norm stay inline.
|
|
518
|
+
if expr.inferred_type == VarType.VECTOR and expr.op == "neg":
|
|
519
|
+
result = self._lower_vector_expr(expr, dest=target_var)
|
|
520
|
+
return None if target_var is not None else result
|
|
521
|
+
return self._emit_unary(expr)
|
|
522
|
+
if isinstance(expr, BinOp):
|
|
523
|
+
return self._emit_binop(expr, target_var)
|
|
524
|
+
if isinstance(expr, FuncCall):
|
|
525
|
+
return self._emit_func_call(expr)
|
|
526
|
+
raise UnsupportedConstructError(
|
|
527
|
+
f"cannot emit code for expression node {type(expr).__name__}: {expr!r}"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def _emit_unary(self, expr: UnaryOp) -> str:
|
|
531
|
+
inner = self._emit_expr_str(expr.operand)
|
|
532
|
+
if expr.op == "neg":
|
|
533
|
+
return f"(-{inner})"
|
|
534
|
+
if expr.op == "norm":
|
|
535
|
+
# |scalar| is absolute value; ||vector|| is the Euclidean norm kernel.
|
|
536
|
+
if expr.operand.inferred_type == VarType.SCALAR:
|
|
537
|
+
return f"abs({inner})"
|
|
538
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
539
|
+
return f"_v.norm2({inner})"
|
|
540
|
+
return f"_norm({inner})"
|
|
541
|
+
if expr.op in ("transpose", "inverse"):
|
|
542
|
+
return inner # semantic, handled at binop / funccall level
|
|
543
|
+
raise UnsupportedConstructError(
|
|
544
|
+
f"cannot emit code for unary operator {expr.op!r} on {expr.operand!r}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def _emit_binop(self, expr: BinOp, target_var: str | None) -> str | None:
|
|
548
|
+
if expr.op == "dot":
|
|
549
|
+
return self._emit_dot(expr)
|
|
550
|
+
# All vector-valued binary ops (matvec, scale, +, -) lower through the
|
|
551
|
+
# SSA pass: every emitted op is a single kernel call writing a field,
|
|
552
|
+
# so arbitrarily nested RHS like `r + beta*(p - omega*v)` is faithful.
|
|
553
|
+
# When a target field is given the result is written there and None is
|
|
554
|
+
# returned; otherwise a fresh temp field name is.
|
|
555
|
+
if expr.inferred_type == VarType.VECTOR and expr.op in (
|
|
556
|
+
"matvec",
|
|
557
|
+
"scale",
|
|
558
|
+
"*",
|
|
559
|
+
"+",
|
|
560
|
+
"-",
|
|
561
|
+
):
|
|
562
|
+
result = self._lower_vector_expr(expr, dest=target_var)
|
|
563
|
+
return None if target_var is not None else result
|
|
564
|
+
|
|
565
|
+
# Scalar
|
|
566
|
+
left = self._emit_expr(expr.left)
|
|
567
|
+
right = self._emit_expr(expr.right)
|
|
568
|
+
op_map = {
|
|
569
|
+
"+": "+",
|
|
570
|
+
"-": "-",
|
|
571
|
+
"*": "*",
|
|
572
|
+
"/": "/",
|
|
573
|
+
"pow": "**",
|
|
574
|
+
"<": "<",
|
|
575
|
+
">": ">",
|
|
576
|
+
"<=": "<=",
|
|
577
|
+
">=": ">=",
|
|
578
|
+
"==": "==",
|
|
579
|
+
"!=": "!=",
|
|
580
|
+
}
|
|
581
|
+
op = op_map.get(expr.op, expr.op)
|
|
582
|
+
return f"({left} {op} {right})"
|
|
583
|
+
|
|
584
|
+
def _emit_dot(self, expr: BinOp) -> str:
|
|
585
|
+
left = expr.left
|
|
586
|
+
if isinstance(left, UnaryOp) and left.op == "transpose":
|
|
587
|
+
left = left.operand
|
|
588
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
589
|
+
return f"_v.dot({self._emit_expr(left)}, {self._emit_expr(expr.right)})"
|
|
590
|
+
return f"_dot({self._emit_expr(left)}, {self._emit_expr(expr.right)})"
|
|
591
|
+
|
|
592
|
+
# ── SSA vector lowering ──────────────────────────────────────────────
|
|
593
|
+
#
|
|
594
|
+
# Any vector-valued expression is lowered to a chain of single-kernel-call
|
|
595
|
+
# operations, each writing a fresh temporary field, with the final op
|
|
596
|
+
# writing the destination. One-level decomposition would emit invalid
|
|
597
|
+
# ``ti.field`` Python arithmetic for nested RHS such as
|
|
598
|
+
# ``r + beta*(p - omega*v)``. ``scalar * vector`` factors fuse into the
|
|
599
|
+
# parent axpy coefficient, so the common solver updates stay a single
|
|
600
|
+
# ``_vec_add`` and only genuinely nested sub-expressions cost a temp.
|
|
601
|
+
|
|
602
|
+
def _lower_vector_expr(self, expr: Expr, dest: str | None) -> str:
|
|
603
|
+
"""Lower a vector expression to kernel calls; return the result field.
|
|
604
|
+
|
|
605
|
+
``dest`` is the destination field name, or None to materialise into a
|
|
606
|
+
fresh temporary. Every branch emits at most one kernel call plus the
|
|
607
|
+
calls from its recursively-lowered operands.
|
|
608
|
+
"""
|
|
609
|
+
if isinstance(expr, Var):
|
|
610
|
+
name = _var_name(expr)
|
|
611
|
+
if dest is not None and dest != name:
|
|
612
|
+
# ti_runtime.vector_ops.copy(dst, src) — note arg order is
|
|
613
|
+
# reversed from the inlined _copy(src, dst) kernel.
|
|
614
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
615
|
+
self._write(f"_v.copy({dest}, {name})")
|
|
616
|
+
else:
|
|
617
|
+
self._write(f"_copy({name}, {dest})")
|
|
618
|
+
return dest
|
|
619
|
+
return name
|
|
620
|
+
|
|
621
|
+
if isinstance(expr, FuncCall):
|
|
622
|
+
# In-place callable: M_inv(r, out) — out is the last argument.
|
|
623
|
+
out = dest if dest is not None else self._get_temp()
|
|
624
|
+
func = self._emit_func_call_name(expr)
|
|
625
|
+
args = ", ".join(self._emit_expr_str(a) for a in expr.args)
|
|
626
|
+
self._write(f"{func}({args}, {out})")
|
|
627
|
+
return out
|
|
628
|
+
|
|
629
|
+
if isinstance(expr, UnaryOp) and expr.op == "neg":
|
|
630
|
+
vec = self._lower_to_field(expr.operand)
|
|
631
|
+
out = dest if dest is not None else self._get_temp()
|
|
632
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
633
|
+
# ti_runtime.vec_add(out, a, x, b, y) — note arg order differs
|
|
634
|
+
# from the inlined _vec_add(alpha, x, beta, y, out)
|
|
635
|
+
self._write(f"_v.vec_add({out}, -1.0, {vec}, 0.0, {vec})")
|
|
636
|
+
else:
|
|
637
|
+
self._write(f"_vec_add(-1.0, {vec}, 0.0, {vec}, {out})")
|
|
638
|
+
return out
|
|
639
|
+
|
|
640
|
+
if isinstance(expr, BinOp):
|
|
641
|
+
if expr.op == "matvec":
|
|
642
|
+
out = dest if dest is not None else self._get_temp()
|
|
643
|
+
vec = self._lower_to_field(expr.right)
|
|
644
|
+
# Matrix-free operator seam (11-ALGO2CODE §8.3): a CALLABLE
|
|
645
|
+
# operand `A` is applied in place — `A(out, vec)`, matching the
|
|
646
|
+
# ti_runtime `apply_A(out, x)` contract — instead of a dense
|
|
647
|
+
# `_matvec` over a stored matrix field. The op name comes from
|
|
648
|
+
# the operator Var the same way an `M_inv(...)` callable does.
|
|
649
|
+
if expr.left.inferred_type == VarType.CALLABLE:
|
|
650
|
+
op_name = self._emit_expr_str(expr.left)
|
|
651
|
+
self._write(f"{op_name}({out}, {vec})")
|
|
652
|
+
return out
|
|
653
|
+
mat = self._emit_expr_str(expr.left)
|
|
654
|
+
self._write(f"_matvec({mat}, {vec}, {out})")
|
|
655
|
+
return out
|
|
656
|
+
|
|
657
|
+
if expr.op in ("scale", "*"):
|
|
658
|
+
scalar, vec_expr = self._split_scale(expr)
|
|
659
|
+
if scalar is not None and vec_expr is not None:
|
|
660
|
+
vec = self._lower_to_field(vec_expr)
|
|
661
|
+
out = dest if dest is not None else self._get_temp()
|
|
662
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
663
|
+
self._write(f"_v.vec_add({out}, {scalar}, {vec}, 0.0, {vec})")
|
|
664
|
+
else:
|
|
665
|
+
self._write(f"_vec_add({scalar}, {vec}, 0.0, {vec}, {out})")
|
|
666
|
+
return out
|
|
667
|
+
|
|
668
|
+
if expr.op in ("+", "-"):
|
|
669
|
+
left = self._lower_to_field(expr.left)
|
|
670
|
+
coeff, right = self._lower_addend(expr.right)
|
|
671
|
+
beta = self._negate(coeff) if expr.op == "-" else coeff
|
|
672
|
+
out = dest if dest is not None else self._get_temp()
|
|
673
|
+
if self.runtime == RUNTIME_TI_RUNTIME:
|
|
674
|
+
self._write(f"_v.vec_add({out}, 1.0, {left}, {beta}, {right})")
|
|
675
|
+
else:
|
|
676
|
+
self._write(f"_vec_add(1.0, {left}, {beta}, {right}, {out})")
|
|
677
|
+
return out
|
|
678
|
+
|
|
679
|
+
raise UnsupportedConstructError(
|
|
680
|
+
f"cannot lower vector expression to kernel ops: {expr!r}. "
|
|
681
|
+
f"Supported vector forms are copy, callable application, matvec, "
|
|
682
|
+
f"scalar*vector, and vector +/- vector."
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
def _lower_to_field(self, expr: Expr) -> str:
|
|
686
|
+
"""Lower a vector expression into some field (existing var or a temp)."""
|
|
687
|
+
return self._lower_vector_expr(expr, dest=None)
|
|
688
|
+
|
|
689
|
+
def _split_scale(self, expr: BinOp) -> tuple[str | None, Expr | None]:
|
|
690
|
+
"""Split a scalar*vector product into (scalar_code, vector_expr).
|
|
691
|
+
|
|
692
|
+
The vector operand is whichever side is typed VECTOR; the other side is
|
|
693
|
+
the scalar coefficient. Treating "not VECTOR" (rather than strictly
|
|
694
|
+
SCALAR) as the coefficient keeps lowering robust when a scalar is only
|
|
695
|
+
weakly typed (e.g. an undeclared Greek coefficient inferred UNKNOWN).
|
|
696
|
+
"""
|
|
697
|
+
lt, rt = expr.left.inferred_type, expr.right.inferred_type
|
|
698
|
+
if rt == VarType.VECTOR and lt != VarType.VECTOR:
|
|
699
|
+
return self._emit_expr_str(expr.left), expr.right
|
|
700
|
+
if lt == VarType.VECTOR and rt != VarType.VECTOR:
|
|
701
|
+
return self._emit_expr_str(expr.right), expr.left
|
|
702
|
+
return None, None
|
|
703
|
+
|
|
704
|
+
def _lower_addend(self, expr: Expr) -> tuple[str, str]:
|
|
705
|
+
"""Express an addend as (coefficient_code, vector_field).
|
|
706
|
+
|
|
707
|
+
A ``scalar*vector`` addend fuses its coefficient into the parent axpy;
|
|
708
|
+
anything else gets coefficient ``1.0`` and is lowered into a field.
|
|
709
|
+
"""
|
|
710
|
+
if isinstance(expr, BinOp) and expr.op in ("scale", "*"):
|
|
711
|
+
scalar, vec_expr = self._split_scale(expr)
|
|
712
|
+
if scalar is not None and vec_expr is not None:
|
|
713
|
+
return scalar, self._lower_to_field(vec_expr)
|
|
714
|
+
return "1.0", self._lower_to_field(expr)
|
|
715
|
+
|
|
716
|
+
@staticmethod
|
|
717
|
+
def _has_top_level_addsub(coeff: str) -> bool:
|
|
718
|
+
"""True if ``coeff`` has a *binary* ``+``/``-`` at the top paren level.
|
|
719
|
+
|
|
720
|
+
Emitted coefficient code spaces its binary operators (``a + b``,
|
|
721
|
+
``a - b``) while a leading unary minus (``-alpha``) and float exponents
|
|
722
|
+
(``1e-3``) do not — so a space-surrounded ``+``/``-`` at paren depth 0
|
|
723
|
+
unambiguously marks a top-level sum/difference. ``(a + b)`` (depth 1)
|
|
724
|
+
and ``a * b`` are *not* compound for negation purposes (unary minus
|
|
725
|
+
already binds tighter than ``*`` / ``/`` and the parens already group).
|
|
726
|
+
"""
|
|
727
|
+
depth = 0
|
|
728
|
+
for i, ch in enumerate(coeff):
|
|
729
|
+
if ch == "(":
|
|
730
|
+
depth += 1
|
|
731
|
+
elif ch == ")":
|
|
732
|
+
depth -= 1
|
|
733
|
+
elif (
|
|
734
|
+
depth == 0
|
|
735
|
+
and ch in "+-"
|
|
736
|
+
and 0 < i < len(coeff) - 1
|
|
737
|
+
and coeff[i - 1] == " "
|
|
738
|
+
and coeff[i + 1] == " "
|
|
739
|
+
):
|
|
740
|
+
return True
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
@staticmethod
|
|
744
|
+
def _negate(coeff: str) -> str:
|
|
745
|
+
"""Negate a coefficient string, keeping the output tidy.
|
|
746
|
+
|
|
747
|
+
A compound coefficient (a top-level ``+``/``-``) must be parenthesised
|
|
748
|
+
so the unary minus binds the *whole* expression: ``-(a + b)``, not the
|
|
749
|
+
precedence-wrong ``-a + b`` (gemini MED / WI-4). This also covers a
|
|
750
|
+
compound *leading*-minus (``-a + b``) correctly — wrapping yields
|
|
751
|
+
``-(-a + b)`` (== ``a - b``), whereas the bare ``coeff[1:]`` strip would
|
|
752
|
+
have produced the wrong ``a + b``. Atoms and products keep the tidy
|
|
753
|
+
strip-or-prepend form.
|
|
754
|
+
"""
|
|
755
|
+
if TaichiEmitter._has_top_level_addsub(coeff):
|
|
756
|
+
return f"-({coeff})"
|
|
757
|
+
return coeff[1:] if coeff.startswith("-") else f"-{coeff}"
|
|
758
|
+
|
|
759
|
+
def _emit_func_call(self, expr: FuncCall) -> str:
|
|
760
|
+
func_name = self._emit_func_call_name(expr)
|
|
761
|
+
args = ", ".join(self._emit_expr_str(a) for a in expr.args)
|
|
762
|
+
return f"{func_name}({args})"
|
|
763
|
+
|
|
764
|
+
def _emit_func_call_name(self, expr: FuncCall) -> str:
|
|
765
|
+
"""Extract the function name string from a FuncCall node."""
|
|
766
|
+
if isinstance(expr.func, UnaryOp) and expr.func.op == "inverse":
|
|
767
|
+
base = expr.func.operand
|
|
768
|
+
return (
|
|
769
|
+
_var_name(base) if isinstance(base, Var) else self._emit_expr_str(base)
|
|
770
|
+
) + "_inv"
|
|
771
|
+
elif isinstance(expr.func, Var):
|
|
772
|
+
name = _var_name(expr.func)
|
|
773
|
+
return "ti.sqrt" if name == "sqrt" else name
|
|
774
|
+
else:
|
|
775
|
+
return self._emit_expr_str(expr.func)
|
|
776
|
+
|
|
777
|
+
# ── Control flow ─────────────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
def _emit_for(self, stmt: ForLoop):
|
|
780
|
+
# Explicit terminal `\ldots, N` is inclusive (k = 1, 2, ..., N), so the
|
|
781
|
+
# Python range upper bound is N + 1 — without the +1 the loop would run one
|
|
782
|
+
# fewer iteration than written. An open-ended `0, 1, 2, ...` uses `maxiter`
|
|
783
|
+
# as a safety cap (exactly `maxiter` iterations), not an inclusive terminal.
|
|
784
|
+
end = f"{_sanitize(stmt.end_expr)} + 1" if stmt.end_expr else "maxiter"
|
|
785
|
+
self._write(f"for {stmt.var} in range({stmt.start}, {end}):")
|
|
786
|
+
self._indent += 1
|
|
787
|
+
self._emit_block(stmt.body)
|
|
788
|
+
self._indent -= 1
|
|
789
|
+
|
|
790
|
+
def _emit_while(self, stmt: WhileLoop):
|
|
791
|
+
cond = self._emit_expr(stmt.condition)
|
|
792
|
+
self._write(f"while {cond}:")
|
|
793
|
+
self._indent += 1
|
|
794
|
+
self._emit_block(stmt.body)
|
|
795
|
+
self._indent -= 1
|
|
796
|
+
|
|
797
|
+
def _emit_branch(self, stmt: Branch):
|
|
798
|
+
cond = self._emit_expr(stmt.condition)
|
|
799
|
+
self._write(f"if {cond}:")
|
|
800
|
+
self._indent += 1
|
|
801
|
+
self._emit_block(stmt.if_body)
|
|
802
|
+
self._indent -= 1
|
|
803
|
+
for elif_cond, elif_body in stmt.elif_branches:
|
|
804
|
+
self._write(f"elif {self._emit_expr(elif_cond)}:")
|
|
805
|
+
self._indent += 1
|
|
806
|
+
self._emit_block(elif_body)
|
|
807
|
+
self._indent -= 1
|
|
808
|
+
if stmt.else_body:
|
|
809
|
+
self._write("else:")
|
|
810
|
+
self._indent += 1
|
|
811
|
+
self._emit_block(stmt.else_body)
|
|
812
|
+
self._indent -= 1
|
|
813
|
+
|
|
814
|
+
def _emit_return(self, stmt: Return):
|
|
815
|
+
if not stmt.values:
|
|
816
|
+
self._write("return")
|
|
817
|
+
else:
|
|
818
|
+
vals = ", ".join(self._emit_expr_str(v) for v in stmt.values)
|
|
819
|
+
self._write(f"return {vals}")
|
|
820
|
+
|
|
821
|
+
def _get_temp(self) -> str:
|
|
822
|
+
name = f"_tmp{self._temp_counter}"
|
|
823
|
+
self._temp_counter += 1
|
|
824
|
+
if self._temp_counter > self._peak_temps:
|
|
825
|
+
self._peak_temps = self._temp_counter
|
|
826
|
+
return name
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
# ── Public API ───────────────────────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def generate_taichi(algo: Algorithm, runtime: str = RUNTIME_INLINE) -> str:
|
|
833
|
+
"""Generate Taichi source code from a typed Algorithm AST.
|
|
834
|
+
|
|
835
|
+
Parameters
|
|
836
|
+
----------
|
|
837
|
+
algo:
|
|
838
|
+
Typed Algorithm AST (output of :func:`algo2code.type_inference.infer_types`).
|
|
839
|
+
runtime:
|
|
840
|
+
Import mode. One of:
|
|
841
|
+
|
|
842
|
+
``"inline"`` (default)
|
|
843
|
+
Emit private ``@ti.kernel`` definitions inline in the generated
|
|
844
|
+
file. Zero external runtime dependencies. All existing consumers
|
|
845
|
+
use this mode; it is the backward-compatible default.
|
|
846
|
+
|
|
847
|
+
``"ti_runtime"``
|
|
848
|
+
Emit ``from ti_runtime import vector_ops as _v`` and call the
|
|
849
|
+
shared primitives for dot/norm/copy/vec_add. Only ``_matvec``
|
|
850
|
+
remains inlined (the matrix-free operator seam is P2-2's concern).
|
|
851
|
+
algo2code itself never imports ti_runtime — it only emits the
|
|
852
|
+
import line.
|
|
853
|
+
|
|
854
|
+
Returns
|
|
855
|
+
-------
|
|
856
|
+
str
|
|
857
|
+
Generated Taichi Python source.
|
|
858
|
+
"""
|
|
859
|
+
return TaichiEmitter(algo, runtime=runtime).emit()
|