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
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
|
+
]
|
algo2code/algo_parser.py
ADDED
|
@@ -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)."""
|