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 ADDED
@@ -0,0 +1,72 @@
1
+ """algo2code — Transpile LaTeX algorithm boxes (algpseudocode) to executable code.
2
+
3
+ Targets: Taichi (MVP), NumPy, C/PETSc.
4
+
5
+ Usage:
6
+ from algo2code import transpile
7
+
8
+ taichi_code = transpile(latex_source, backend='taichi')
9
+ """
10
+
11
+ __version__ = "0.2.1"
12
+
13
+ from .algo_parser import parse_algorithm
14
+ from .ast_nodes import Algorithm, VarType
15
+ from .backends.taichi_codegen import (
16
+ RUNTIME_INLINE,
17
+ RUNTIME_TI_RUNTIME,
18
+ generate_taichi,
19
+ )
20
+ from .errors import Algo2CodeError, UnsupportedConstructError
21
+ from .expr_parser import parse_latex_expr
22
+ from .library import PCG_ALGORITHM_LATEX, get_pcg_algorithm_latex
23
+ from .type_inference import infer_types
24
+
25
+
26
+ def transpile(source: str, backend: str = "taichi", runtime: str = "inline") -> str:
27
+ """
28
+ Full pipeline: LaTeX source → parsed AST → type inference → code generation.
29
+
30
+ Parameters
31
+ ----------
32
+ source : str
33
+ LaTeX source containing \\begin{algorithmic} ... \\end{algorithmic}
34
+ with optional % directive comments.
35
+ backend : str
36
+ Target backend. Currently only 'taichi' is supported.
37
+ runtime : str
38
+ Import mode for the Taichi backend. ``"inline"`` (default) emits
39
+ private ``@ti.kernel`` definitions inline. ``"ti_runtime"`` emits
40
+ ``from ti_runtime import vector_ops as _v`` and calls the shared
41
+ primitives for dot/norm/copy. algo2code itself never imports
42
+ ti_runtime regardless of this setting.
43
+
44
+ Returns
45
+ -------
46
+ str
47
+ Generated source code.
48
+ """
49
+ algo = parse_algorithm(source)
50
+ infer_types(algo)
51
+
52
+ if backend == "taichi":
53
+ return generate_taichi(algo, runtime=runtime)
54
+ else:
55
+ raise ValueError(f"Unknown backend: {backend!r}. Supported: 'taichi'")
56
+
57
+
58
+ __all__ = [
59
+ "PCG_ALGORITHM_LATEX",
60
+ "RUNTIME_INLINE",
61
+ "RUNTIME_TI_RUNTIME",
62
+ "Algo2CodeError",
63
+ "Algorithm",
64
+ "UnsupportedConstructError",
65
+ "VarType",
66
+ "generate_taichi",
67
+ "get_pcg_algorithm_latex",
68
+ "infer_types",
69
+ "parse_algorithm",
70
+ "parse_latex_expr",
71
+ "transpile",
72
+ ]
@@ -0,0 +1,458 @@
1
+ r"""
2
+ Parser for LaTeX `algpseudocode` environments.
3
+
4
+ Recognises:
5
+ \begin{algorithmic} ... \end{algorithmic}
6
+ \State $lhs = rhs$ % optional_comment
7
+ \For{$k = 0, 1, \ldots, N$} ... \EndFor
8
+ \While{$cond$} ... \EndWhile
9
+ \If{$cond$} ... \ElsIf{$cond$} ... \Else ... \EndIf
10
+ \Return $expr$
11
+ \State \textbf{break}
12
+
13
+ Directive comments (lines starting with `%` OUTSIDE algorithmic):
14
+ % algorithm <name>
15
+ % backend <taichi|numpy|petsc>
16
+ % args <name>:<type>, ...
17
+ % type <varname> <scalar|vector|matrix|callable>
18
+
19
+ The parser delegates all $...$ math fragments to expr_parser.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+
26
+ from .ast_nodes import (
27
+ Algorithm,
28
+ Assign,
29
+ Branch,
30
+ Break,
31
+ ForLoop,
32
+ Return,
33
+ Stmt,
34
+ Var,
35
+ VarType,
36
+ WhileLoop,
37
+ )
38
+ from .expr_parser import parse_assignment, parse_condition, parse_latex_expr
39
+
40
+ # ── Directive parsing ────────────────────────────────────────────────────────
41
+
42
+ _TYPE_MAP = {
43
+ "scalar": VarType.SCALAR,
44
+ "vector": VarType.VECTOR,
45
+ "matrix": VarType.MATRIX,
46
+ "callable": VarType.CALLABLE,
47
+ "matvec": VarType.MATRIX, # alias
48
+ }
49
+
50
+
51
+ def _parse_directives(lines: list[str]) -> dict:
52
+ """Parse % directive comments before the algorithmic block."""
53
+ directives: dict = {
54
+ "name": "algorithm",
55
+ "backend": "taichi",
56
+ "args": [],
57
+ "types": {},
58
+ }
59
+
60
+ for line in lines:
61
+ line = line.strip()
62
+ if not line.startswith("%"):
63
+ continue
64
+ line = line[1:].strip()
65
+
66
+ if line.startswith("algorithm "):
67
+ directives["name"] = line.split(None, 1)[1].strip()
68
+
69
+ elif line.startswith("backend "):
70
+ directives["backend"] = line.split(None, 1)[1].strip()
71
+
72
+ elif line.startswith("args "):
73
+ arg_str = line.split(None, 1)[1]
74
+ for arg in arg_str.split(","):
75
+ arg = arg.strip()
76
+ if ":" in arg:
77
+ name, typ = arg.split(":", 1)
78
+ directives["args"].append(
79
+ (name.strip(), _TYPE_MAP.get(typ.strip(), VarType.UNKNOWN))
80
+ )
81
+ else:
82
+ directives["args"].append((arg, VarType.UNKNOWN))
83
+
84
+ elif line.startswith("type "):
85
+ parts = line.split()
86
+ if len(parts) >= 3:
87
+ varname = parts[1]
88
+ vtype = _TYPE_MAP.get(parts[2], VarType.UNKNOWN)
89
+ directives["types"][varname] = vtype
90
+
91
+ return directives
92
+
93
+
94
+ # ── Main parser ──────────────────────────────────────────────────────────────
95
+
96
+
97
+ class AlgPseudocodeParser:
98
+ """
99
+ Parse a complete LaTeX source containing an algorithmic environment.
100
+
101
+ Usage:
102
+ algo = AlgPseudocodeParser(latex_string).parse()
103
+ """
104
+
105
+ def __init__(self, source: str):
106
+ self.source = source
107
+ self.lines: list[str] = []
108
+ self.pos = 0
109
+
110
+ def parse(self) -> Algorithm:
111
+ """Parse the full source and return an Algorithm AST."""
112
+ pre_lines, body_lines = self._split_sections()
113
+ directives = _parse_directives(pre_lines)
114
+
115
+ type_annotations = dict(directives["types"])
116
+ self._collect_inline_types(body_lines, type_annotations)
117
+
118
+ self.lines = body_lines
119
+ self.pos = 0
120
+ stmts = self._parse_block(terminators=[])
121
+
122
+ return Algorithm(
123
+ name=directives["name"],
124
+ backend=directives["backend"],
125
+ args=directives["args"],
126
+ body=stmts,
127
+ type_annotations=type_annotations,
128
+ )
129
+
130
+ def _split_sections(self) -> tuple[list[str], list[str]]:
131
+ """Split source into pre-algorithmic lines and body lines."""
132
+ all_lines = self.source.split("\n")
133
+ pre_lines = []
134
+ body_lines = []
135
+ in_body = False
136
+
137
+ for line in all_lines:
138
+ stripped = line.strip()
139
+
140
+ if re.match(r"\\begin\{algorithmic\}", stripped):
141
+ in_body = True
142
+ continue
143
+ if re.match(r"\\end\{algorithmic\}", stripped):
144
+ in_body = False
145
+ continue
146
+ if re.match(r"\\begin\{algorithm\}", stripped):
147
+ continue
148
+ if re.match(r"\\end\{algorithm\}", stripped):
149
+ continue
150
+ if re.match(r"\\caption\{", stripped):
151
+ continue
152
+
153
+ if in_body:
154
+ if stripped: # skip blank lines
155
+ body_lines.append(line)
156
+ else:
157
+ pre_lines.append(line)
158
+
159
+ return pre_lines, body_lines
160
+
161
+ def _collect_inline_types(self, lines: list[str], types: dict):
162
+ """Extract inline type annotations from % comments on \\State lines."""
163
+ for line in lines:
164
+ m = re.search(r"%\s*(\w+)\s*$", line)
165
+ if m:
166
+ hint = m.group(1).lower()
167
+ if hint in _TYPE_MAP:
168
+ lhs_match = re.search(r"\\State\s+\$\s*([^=$]+?)\s*=", line)
169
+ if lhs_match:
170
+ var_name = self._extract_var_name(lhs_match.group(1))
171
+ if var_name:
172
+ types[var_name] = _TYPE_MAP[hint]
173
+
174
+ def _extract_var_name(self, lhs_latex: str) -> str | None:
175
+ """Extract a clean variable name from a LaTeX LHS fragment."""
176
+ lhs = re.sub(r"\\(?:mathbf|boldsymbol|bm)\{([^}]*)\}", r"\1", lhs_latex)
177
+ lhs = re.sub(r"_\{\\text\{([^}]*)\}\}", r"_\1", lhs)
178
+ lhs = re.sub(r"\\(\w+)", r"\1", lhs)
179
+ lhs = lhs.strip().replace(" ", "")
180
+ return lhs if lhs else None
181
+
182
+ # ── Statement parsing ────────────────────────────────────────────────
183
+
184
+ def _current_line(self) -> str | None:
185
+ if self.pos < len(self.lines):
186
+ return self.lines[self.pos].strip()
187
+ return None
188
+
189
+ def _advance(self):
190
+ self.pos += 1
191
+
192
+ def _parse_block(self, terminators: list[str]) -> list[Stmt]:
193
+ """Parse statements until a terminator command is found."""
194
+ stmts = []
195
+ while self.pos < len(self.lines):
196
+ line = self._current_line()
197
+ if line is None:
198
+ break
199
+
200
+ stripped = self._strip_comment(line)
201
+ if any(stripped.startswith(t) for t in terminators):
202
+ break
203
+
204
+ stmt = self._parse_statement()
205
+ if stmt is not None:
206
+ stmts.append(stmt)
207
+
208
+ return stmts
209
+
210
+ def _strip_comment(self, line: str) -> str:
211
+ """Remove trailing % comment but preserve % inside $...$."""
212
+ in_math = False
213
+ for i, ch in enumerate(line):
214
+ if ch == "$":
215
+ in_math = not in_math
216
+ elif ch == "%" and not in_math:
217
+ return line[:i].strip()
218
+ return line.strip()
219
+
220
+ def _extract_inline_comment(self, line: str) -> str:
221
+ """Extract the % comment portion."""
222
+ in_math = False
223
+ for i, ch in enumerate(line):
224
+ if ch == "$":
225
+ in_math = not in_math
226
+ elif ch == "%" and not in_math:
227
+ return line[i + 1 :].strip()
228
+ return ""
229
+
230
+ def _parse_statement(self) -> Stmt | None:
231
+ """Parse a single statement from the current line."""
232
+ line = self._current_line()
233
+ if line is None:
234
+ return None
235
+
236
+ stripped = self._strip_comment(line)
237
+ comment = self._extract_inline_comment(line)
238
+
239
+ if stripped.startswith("\\For"):
240
+ return self._parse_for(stripped)
241
+
242
+ if stripped.startswith("\\While"):
243
+ return self._parse_while(stripped)
244
+
245
+ if stripped.startswith("\\If"):
246
+ return self._parse_if(stripped)
247
+
248
+ if stripped.startswith("\\Return") or stripped.startswith("\\State \\Return"):
249
+ self._advance()
250
+ return self._parse_return(stripped)
251
+
252
+ if re.search(r"\\textbf\{break\}|\\Break|\\textbf\{Break\}", stripped):
253
+ self._advance()
254
+ return Break()
255
+
256
+ if stripped.startswith("\\State"):
257
+ self._advance()
258
+ return self._parse_state(stripped, comment)
259
+
260
+ # Skip unrecognized lines
261
+ self._advance()
262
+ return None
263
+
264
+ def _extract_math(self, text: str) -> str:
265
+ """Extract content between $ delimiters."""
266
+ m = re.search(r"\$(.+?)\$", text)
267
+ if m:
268
+ return m.group(1).strip()
269
+ return text.strip()
270
+
271
+ def _extract_brace_arg(self, text: str, command: str) -> str:
272
+ """Extract the {argument} after a \\Command."""
273
+ idx = text.find(command)
274
+ if idx < 0:
275
+ return ""
276
+ rest = text[idx + len(command) :]
277
+
278
+ brace_start = rest.find("{")
279
+ if brace_start < 0:
280
+ return ""
281
+
282
+ depth = 0
283
+ start = brace_start
284
+ for i in range(brace_start, len(rest)):
285
+ if rest[i] == "{":
286
+ depth += 1
287
+ elif rest[i] == "}":
288
+ depth -= 1
289
+ if depth == 0:
290
+ return rest[start + 1 : i].strip()
291
+ return rest[start + 1 :].strip()
292
+
293
+ # ── For loop ─────────────────────────────────────────────────────────
294
+
295
+ def _parse_for(self, line: str) -> ForLoop:
296
+ r"""Parse \\For{$k = 0, 1, \\ldots, N$} body \\EndFor"""
297
+ arg = self._extract_brace_arg(line, "\\For")
298
+ arg = arg.strip("$ ")
299
+
300
+ var, start, end_expr = self._parse_for_range(arg)
301
+
302
+ self._advance() # past the \For line
303
+ body = self._parse_block(terminators=["\\EndFor"])
304
+
305
+ cur_line = self._current_line()
306
+ if cur_line and self._strip_comment(cur_line).startswith("\\EndFor"):
307
+ self._advance()
308
+
309
+ return ForLoop(var=var, start=start, end_expr=end_expr, body=body)
310
+
311
+ def _parse_for_range(self, arg: str) -> tuple[str, int, str]:
312
+ r"""
313
+ Parse for-loop range specifications:
314
+ k = 0, 1, ..., N → var='k', start=0, end='N'
315
+ k = 0, 1, 2, ... → var='k', start=0, end=''
316
+ k = 1 to N → var='k', start=1, end='N'
317
+ """
318
+ m = re.match(
319
+ r"([a-zA-Z]\w*)\s*=\s*(\d+)\s*,\s*\d+\s*,?\s*"
320
+ r"(?:\\ldots|\\dots|\\cdots|\.\.\.)\s*(?:,\s*)?"
321
+ r"(?:\\(?:text|mathrm)\{(\w+)\}|([a-zA-Z]\w*))?",
322
+ arg,
323
+ )
324
+ if m:
325
+ var = m.group(1)
326
+ start = int(m.group(2))
327
+ end_expr = m.group(3) or m.group(4) or ""
328
+ return var, start, end_expr
329
+
330
+ m = re.match(r"([a-zA-Z]\w*)\s*=\s*(\d+)\s+(?:to|\\to)\s+(\w+)", arg)
331
+ if m:
332
+ return m.group(1), int(m.group(2)), m.group(3)
333
+
334
+ m = re.match(r"([a-zA-Z]\w*)", arg)
335
+ var = m.group(1) if m else "k"
336
+ return var, 0, ""
337
+
338
+ # ── While loop ───────────────────────────────────────────────────────
339
+
340
+ def _parse_while(self, line: str) -> WhileLoop:
341
+ arg = self._extract_brace_arg(line, "\\While")
342
+ arg = arg.strip("$ ")
343
+ condition = parse_condition(arg)
344
+
345
+ self._advance()
346
+ body = self._parse_block(terminators=["\\EndWhile"])
347
+
348
+ cur_line = self._current_line()
349
+ if cur_line and self._strip_comment(cur_line).startswith("\\EndWhile"):
350
+ self._advance()
351
+
352
+ return WhileLoop(condition=condition, body=body)
353
+
354
+ # ── If / ElsIf / Else ────────────────────────────────────────────────
355
+
356
+ def _parse_if(self, line: str) -> Branch:
357
+ arg = self._extract_brace_arg(line, "\\If")
358
+ arg = arg.strip("$ ")
359
+ condition = parse_condition(arg)
360
+
361
+ self._advance()
362
+ if_body = self._parse_block(terminators=["\\EndIf", "\\ElsIf", "\\Else"])
363
+
364
+ elif_branches = []
365
+ else_body: list[Stmt] = []
366
+
367
+ while True:
368
+ cur_line = self._current_line()
369
+ if not cur_line:
370
+ break
371
+ cur = self._strip_comment(cur_line)
372
+ if cur.startswith("\\ElsIf"):
373
+ elif_arg = self._extract_brace_arg(cur, "\\ElsIf")
374
+ elif_arg = elif_arg.strip("$ ")
375
+ elif_cond = parse_condition(elif_arg)
376
+ self._advance()
377
+ elif_body = self._parse_block(terminators=["\\EndIf", "\\ElsIf", "\\Else"])
378
+ elif_branches.append((elif_cond, elif_body))
379
+ elif cur.startswith("\\Else"):
380
+ self._advance()
381
+ else_body = self._parse_block(terminators=["\\EndIf"])
382
+ break
383
+ else:
384
+ break
385
+
386
+ cur_line = self._current_line()
387
+ if cur_line and self._strip_comment(cur_line).startswith("\\EndIf"):
388
+ self._advance()
389
+
390
+ return Branch(
391
+ condition=condition,
392
+ if_body=if_body,
393
+ elif_branches=elif_branches,
394
+ else_body=else_body,
395
+ )
396
+
397
+ # ── Return ───────────────────────────────────────────────────────────
398
+
399
+ def _parse_return(self, line: str) -> Return:
400
+ m = re.search(r"\\Return\s*(.*)", self._strip_comment(line))
401
+ if not m:
402
+ return Return(values=[])
403
+
404
+ rest = m.group(1).strip().strip("$").strip()
405
+ if not rest:
406
+ return Return(values=[])
407
+
408
+ values = []
409
+ for part in self._split_top_level(rest, ","):
410
+ part = part.strip()
411
+ if part:
412
+ values.append(parse_latex_expr(part))
413
+
414
+ return Return(values=values)
415
+
416
+ def _split_top_level(self, text: str, sep: str) -> list[str]:
417
+ """Split text by separator, respecting brace depth."""
418
+ parts = []
419
+ depth = 0
420
+ current: list[str] = []
421
+ for ch in text:
422
+ if ch == "{":
423
+ depth += 1
424
+ elif ch == "}":
425
+ depth -= 1
426
+ elif ch == sep and depth == 0:
427
+ parts.append("".join(current))
428
+ current = []
429
+ continue
430
+ current.append(ch)
431
+ parts.append("".join(current))
432
+ return parts
433
+
434
+ # ── State (assignment) ───────────────────────────────────────────────
435
+
436
+ def _parse_state(self, line: str, comment: str) -> Stmt | None:
437
+ r"""Parse \\State $lhs = rhs$"""
438
+ stripped = self._strip_comment(line)
439
+ stripped = re.sub(r"^\\State\s*", "", stripped).strip()
440
+ math = self._extract_math(stripped)
441
+ if not math:
442
+ return None
443
+
444
+ result = parse_assignment(math)
445
+ if result is None:
446
+ expr = parse_latex_expr(math)
447
+ return Assign(target=Var(name="_"), value=expr, comment=comment)
448
+
449
+ target, value = result
450
+ return Assign(target=target, value=value, comment=comment)
451
+
452
+
453
+ # ── Public API ───────────────────────────────────────────────────────────────
454
+
455
+
456
+ def parse_algorithm(source: str) -> Algorithm:
457
+ """Parse a LaTeX source containing an algorithmic environment."""
458
+ return AlgPseudocodeParser(source).parse()
algo2code/ast_nodes.py ADDED
@@ -0,0 +1,171 @@
1
+ """
2
+ AST node definitions for algorithmic pseudocode → code transpiler.
3
+
4
+ Two-level AST:
5
+ 1. Control-flow nodes (ForLoop, WhileLoop, Branch, Assign, Return, Break)
6
+ 2. Expression nodes (BinOp, UnaryOp, FuncCall, Var, Scalar, Norm, Transpose)
7
+
8
+ The expression nodes are *typed* (scalar / vector / matrix / callable)
9
+ to drive correct code generation for Taichi kernels vs. Python-scope code.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum, auto
16
+
17
+ # ── Type system ──────────────────────────────────────────────────────────────
18
+
19
+
20
+ class VarType(Enum):
21
+ SCALAR = auto()
22
+ VECTOR = auto()
23
+ MATRIX = auto()
24
+ CALLABLE = auto() # e.g. preconditioner M^{-1}
25
+ UNKNOWN = auto()
26
+
27
+
28
+ # ── Expression nodes ─────────────────────────────────────────────────────────
29
+
30
+
31
+ @dataclass
32
+ class Expr:
33
+ """Base class for all expression nodes."""
34
+
35
+ inferred_type: VarType = field(default=VarType.UNKNOWN, repr=False)
36
+
37
+
38
+ @dataclass
39
+ class Var(Expr):
40
+ """A named variable: x, r, A, \\alpha, \\rho_{\\text{new}}, etc."""
41
+
42
+ name: str = ""
43
+ subscript: str | None = None # e.g. "new" from \rho_{\text{new}}
44
+
45
+ def __post_init__(self):
46
+ super().__init__()
47
+
48
+ @property
49
+ def display(self) -> str:
50
+ if self.subscript:
51
+ return f"{self.name}_{self.subscript}"
52
+ return self.name
53
+
54
+
55
+ @dataclass
56
+ class Number(Expr):
57
+ """A literal number."""
58
+
59
+ value: float = 0.0
60
+
61
+ def __post_init__(self):
62
+ super().__init__(inferred_type=VarType.SCALAR)
63
+
64
+
65
+ @dataclass
66
+ class BinOp(Expr):
67
+ """Binary operation: a + b, A*p, r^T z, a/b, etc."""
68
+
69
+ op: str = "" # '+', '-', '*', '/', 'dot', 'matvec', 'matmul'
70
+ left: Expr = field(default_factory=Expr)
71
+ right: Expr = field(default_factory=Expr)
72
+
73
+ def __post_init__(self):
74
+ super().__init__()
75
+
76
+
77
+ @dataclass
78
+ class UnaryOp(Expr):
79
+ """Unary operation: -x, ‖r‖, etc."""
80
+
81
+ op: str = "" # 'neg', 'norm', 'transpose'
82
+ operand: Expr = field(default_factory=Expr)
83
+
84
+ def __post_init__(self):
85
+ super().__init__()
86
+
87
+
88
+ @dataclass
89
+ class FuncCall(Expr):
90
+ """Function application: M^{-1}(r), f(x,y), etc."""
91
+
92
+ func: Expr = field(default_factory=Expr)
93
+ args: list[Expr] = field(default_factory=list)
94
+
95
+ def __post_init__(self):
96
+ super().__init__()
97
+
98
+
99
+ # ── Control-flow nodes ───────────────────────────────────────────────────────
100
+
101
+
102
+ @dataclass
103
+ class Stmt:
104
+ """Base class for all statement nodes."""
105
+
106
+ pass
107
+
108
+
109
+ @dataclass
110
+ class Assign(Stmt):
111
+ """Assignment: x = expr"""
112
+
113
+ target: Var = field(default_factory=Var)
114
+ value: Expr = field(default_factory=Expr)
115
+ comment: str = "" # inline annotation: "% vector", "% matvec"
116
+
117
+
118
+ @dataclass
119
+ class ForLoop(Stmt):
120
+ """For loop: \\For{k = 0, 1, ..., maxiter} body \\EndFor"""
121
+
122
+ var: str = ""
123
+ start: int = 0
124
+ end_expr: str = "" # e.g. "maxiter", or "" for indefinite
125
+ body: list[Stmt] = field(default_factory=list)
126
+
127
+
128
+ @dataclass
129
+ class WhileLoop(Stmt):
130
+ """While loop: \\While{cond} body \\EndWhile"""
131
+
132
+ condition: Expr = field(default_factory=Expr)
133
+ body: list[Stmt] = field(default_factory=list)
134
+
135
+
136
+ @dataclass
137
+ class Branch(Stmt):
138
+ """If / ElsIf / Else: \\If{cond} body \\EndIf"""
139
+
140
+ condition: Expr = field(default_factory=Expr)
141
+ if_body: list[Stmt] = field(default_factory=list)
142
+ elif_branches: list[tuple[Expr, list[Stmt]]] = field(default_factory=list)
143
+ else_body: list[Stmt] = field(default_factory=list)
144
+
145
+
146
+ @dataclass
147
+ class Return(Stmt):
148
+ """Return statement: \\Return expr"""
149
+
150
+ values: list[Expr] = field(default_factory=list)
151
+
152
+
153
+ @dataclass
154
+ class Break(Stmt):
155
+ """Break statement."""
156
+
157
+ pass
158
+
159
+
160
+ # ── Top-level algorithm ──────────────────────────────────────────────────────
161
+
162
+
163
+ @dataclass
164
+ class Algorithm:
165
+ """A complete parsed algorithm."""
166
+
167
+ name: str = ""
168
+ backend: str = "taichi"
169
+ args: list[tuple[str, VarType]] = field(default_factory=list)
170
+ body: list[Stmt] = field(default_factory=list)
171
+ type_annotations: dict[str, VarType] = field(default_factory=dict) # var_name -> type
@@ -0,0 +1 @@
1
+ """Code generation backends."""
@@ -0,0 +1 @@
1
+ """C/PETSc backend (planned)."""