diraclang 0.1.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.
- dirac/__init__.py +48 -0
- dirac/cli.py +39 -0
- dirac/runtime/__init__.py +0 -0
- dirac/runtime/braket_parser.py +281 -0
- dirac/runtime/interpreter.py +134 -0
- dirac/runtime/parser.py +51 -0
- dirac/runtime/session.py +293 -0
- dirac/shell.py +1103 -0
- dirac/tags/__init__.py +0 -0
- dirac/tags/assign.py +55 -0
- dirac/tags/break_tag.py +10 -0
- dirac/tags/call.py +161 -0
- dirac/tags/defvar.py +45 -0
- dirac/tags/eval_tag.py +81 -0
- dirac/tags/foreach.py +45 -0
- dirac/tags/if_tag.py +123 -0
- dirac/tags/import_tag.py +91 -0
- dirac/tags/input_tag.py +57 -0
- dirac/tags/llm_tag.py +311 -0
- dirac/tags/loop.py +51 -0
- dirac/tags/ollama.py +58 -0
- dirac/tags/output.py +20 -0
- dirac/tags/parameters_tag.py +51 -0
- dirac/tags/return_tag.py +41 -0
- dirac/tags/subroutine.py +62 -0
- dirac/tags/system.py +53 -0
- dirac/tags/test_if.py +87 -0
- dirac/tags/variable.py +26 -0
- dirac/types.py +91 -0
- diraclang-0.1.0.dist-info/METADATA +317 -0
- diraclang-0.1.0.dist-info/RECORD +34 -0
- diraclang-0.1.0.dist-info/WHEEL +5 -0
- diraclang-0.1.0.dist-info/entry_points.txt +5 -0
- diraclang-0.1.0.dist-info/top_level.txt +1 -0
dirac/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DIRAC Python Runtime.
|
|
3
|
+
|
|
4
|
+
A Python-native port of the DIRAC language runtime (reference: dirac-lang on
|
|
5
|
+
npm / the Node.js implementation in the sibling `dirac` repo).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from dirac import execute
|
|
9
|
+
output = execute('<dirac><output>Hello, DIRAC!</output></dirac>')
|
|
10
|
+
|
|
11
|
+
# Bra-ket notation is also supported:
|
|
12
|
+
output = execute('|output>Hello, DIRAC!', fmt="braket")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .runtime.braket_parser import BraKetParser
|
|
16
|
+
from .runtime.interpreter import integrate
|
|
17
|
+
from .runtime.parser import DiracParser
|
|
18
|
+
from .runtime.session import create_session, get_output
|
|
19
|
+
from .types import DiracElement, DiracSession
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"execute",
|
|
23
|
+
"DiracParser",
|
|
24
|
+
"BraKetParser",
|
|
25
|
+
"DiracSession",
|
|
26
|
+
"DiracElement",
|
|
27
|
+
"create_session",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def execute(source: str, debug: bool = False, fmt: str = "xml") -> str:
|
|
32
|
+
"""
|
|
33
|
+
Parse and execute DIRAC source code, returning the captured output.
|
|
34
|
+
|
|
35
|
+
fmt: "xml" (default, `.di` syntax) or "braket" (`.bk` syntax).
|
|
36
|
+
"""
|
|
37
|
+
if fmt == "braket":
|
|
38
|
+
source = BraKetParser().parse(source)
|
|
39
|
+
elif fmt != "xml":
|
|
40
|
+
raise ValueError(f"Unknown format: {fmt!r}. Use 'xml' or 'braket'.")
|
|
41
|
+
|
|
42
|
+
parser = DiracParser()
|
|
43
|
+
session = create_session(debug=debug)
|
|
44
|
+
|
|
45
|
+
ast = parser.parse(source)
|
|
46
|
+
integrate(session, ast)
|
|
47
|
+
|
|
48
|
+
return get_output(session)
|
dirac/cli.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for the DIRAC Python runtime.
|
|
3
|
+
Mirrors dirac/src/cli.ts (single-file execution mode only, for now).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import execute
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="paul", description="Run a DIRAC (.di or .bk) script.")
|
|
14
|
+
parser.add_argument("file", help="Path to a .di (XML) or .bk (bra-ket) file to execute")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--format",
|
|
17
|
+
choices=["xml", "braket"],
|
|
18
|
+
default=None,
|
|
19
|
+
help="Force the source format instead of auto-detecting from the file extension",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
25
|
+
source = f.read()
|
|
26
|
+
|
|
27
|
+
fmt = args.format or ("braket" if args.file.endswith(".bk") else "xml")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
output = execute(source, debug=args.debug, fmt=fmt)
|
|
31
|
+
except Exception as exc: # noqa: BLE001 - surface runtime errors to the CLI user
|
|
32
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
print(output, end="")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bra-Ket Parser - converts bra-ket notation to XML.
|
|
3
|
+
Mirrors dirac/src/runtime/braket-parser.ts from the Node.js reference
|
|
4
|
+
implementation.
|
|
5
|
+
|
|
6
|
+
Syntax:
|
|
7
|
+
- Bra (subroutine): <name| ... defines a subroutine
|
|
8
|
+
- Ket (everything else): |tag attrs> ... can have children/content
|
|
9
|
+
- Indentation defines scope (2 spaces per level), same convention as
|
|
10
|
+
Python itself - which is the whole point of porting this to Python:
|
|
11
|
+
a Python code block embedded in a ket's body (e.g. inside <eval>)
|
|
12
|
+
keeps its own relative indentation intact, because each line's
|
|
13
|
+
indentation is measured in absolute spaces and re-expressed as
|
|
14
|
+
2 * (spaces // 2) - an identity transform for any evenly-indented
|
|
15
|
+
code (2/4/8-space Python or JS, which is effectively all real code).
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
|output>Hello World -> <output>Hello World</output>
|
|
19
|
+
|variable name=x> -> <variable name="x"/>
|
|
20
|
+
<add| -> <subroutine name="add">
|
|
21
|
+
|output>test -> <output>test</output>
|
|
22
|
+
-> </subroutine>
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
_BRA_TAG_RE = re.compile(r"^<([a-zA-Z_][a-zA-Z0-9_-]*)\s*")
|
|
30
|
+
_KET_RE = re.compile(r"^\|([a-zA-Z_][a-zA-Z0-9_-]*)\s*([^>]*?)>\s*(.*)")
|
|
31
|
+
_INLINE_KET_RE = re.compile(r"\|([a-zA-Z_][a-zA-Z0-9_-]*)\s*([^>]*?)>")
|
|
32
|
+
_ATTR_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_-]*)=(.+)$")
|
|
33
|
+
|
|
34
|
+
_RESERVED_BRA_ATTRS = {"description", "extends", "visible", "lang"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class _Line:
|
|
39
|
+
indent: int
|
|
40
|
+
type: str # 'bra' | 'ket' | 'text' | 'empty'
|
|
41
|
+
tag: Optional[str] = None
|
|
42
|
+
attrs: Optional[str] = None
|
|
43
|
+
text: Optional[str] = None
|
|
44
|
+
raw: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BraKetParser:
|
|
48
|
+
"""Parses bra-ket notation and compiles it to a DIRAC-ROOT-free XML string."""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._lines: list = []
|
|
52
|
+
self._current_line = 0
|
|
53
|
+
|
|
54
|
+
def parse(self, source: str) -> str:
|
|
55
|
+
self._lines = source.split("\n")
|
|
56
|
+
self._current_line = 0
|
|
57
|
+
|
|
58
|
+
xml = ["<dirac>"]
|
|
59
|
+
self._parse_block(xml, -1)
|
|
60
|
+
xml.append("</dirac>")
|
|
61
|
+
|
|
62
|
+
return "\n".join(xml)
|
|
63
|
+
|
|
64
|
+
# -- Block/line parsing ------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _parse_block(self, output: list, parent_indent: int) -> None:
|
|
67
|
+
while self._current_line < len(self._lines):
|
|
68
|
+
line = self._parse_line(self._lines[self._current_line])
|
|
69
|
+
|
|
70
|
+
if line.type == "empty":
|
|
71
|
+
self._current_line += 1
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if line.indent <= parent_indent:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
if line.type == "bra":
|
|
78
|
+
attrs = f" {self._convert_bra_attributes(line.attrs)}" if line.attrs else ""
|
|
79
|
+
output.append(f"{' ' * line.indent}<subroutine name=\"{line.tag}\"{attrs}>")
|
|
80
|
+
self._current_line += 1
|
|
81
|
+
self._parse_block(output, line.indent)
|
|
82
|
+
output.append(f"{' ' * line.indent}</subroutine>")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if line.type == "ket":
|
|
86
|
+
indent_str = " " * line.indent
|
|
87
|
+
attrs = f" {self._convert_ket_attributes(line.attrs, line.tag or '')}" if line.attrs else ""
|
|
88
|
+
|
|
89
|
+
next_line = (
|
|
90
|
+
self._parse_line(self._lines[self._current_line + 1])
|
|
91
|
+
if self._current_line + 1 < len(self._lines)
|
|
92
|
+
else None
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if next_line is not None and next_line.indent > line.indent and next_line.type != "empty":
|
|
96
|
+
output.append(f"{indent_str}<{line.tag}{attrs}>")
|
|
97
|
+
self._current_line += 1
|
|
98
|
+
self._parse_block(output, line.indent)
|
|
99
|
+
output.append(f"{indent_str}</{line.tag}>")
|
|
100
|
+
else:
|
|
101
|
+
if line.text:
|
|
102
|
+
content = self._convert_inline_kets(line.text)
|
|
103
|
+
output.append(f"{indent_str}<{line.tag}{attrs}>{content}</{line.tag}>")
|
|
104
|
+
else:
|
|
105
|
+
output.append(f"{indent_str}<{line.tag}{attrs}/>")
|
|
106
|
+
self._current_line += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if line.type == "text":
|
|
110
|
+
indent_str = " " * line.indent
|
|
111
|
+
content = self._convert_inline_kets(line.text or "")
|
|
112
|
+
output.append(f"{indent_str}{content}")
|
|
113
|
+
self._current_line += 1
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
def _parse_line(self, raw: str) -> _Line:
|
|
117
|
+
match = re.match(r"^(\s*)(.*)", raw)
|
|
118
|
+
indent = len(match.group(1)) // 2 if match else 0
|
|
119
|
+
content = match.group(2) if match else ""
|
|
120
|
+
|
|
121
|
+
if not content.strip():
|
|
122
|
+
return _Line(indent=indent, type="empty", raw=raw)
|
|
123
|
+
|
|
124
|
+
if content.startswith("#"):
|
|
125
|
+
return _Line(indent=indent, type="empty", raw=raw)
|
|
126
|
+
|
|
127
|
+
# Bra: <name| or <name attrs|
|
|
128
|
+
if content.startswith("<") and content.endswith("|"):
|
|
129
|
+
tag_match = _BRA_TAG_RE.match(content)
|
|
130
|
+
if tag_match:
|
|
131
|
+
tag_name = tag_match.group(1)
|
|
132
|
+
after_tag = content[tag_match.end() : -1] # strip trailing |
|
|
133
|
+
return _Line(
|
|
134
|
+
indent=indent,
|
|
135
|
+
type="bra",
|
|
136
|
+
tag=tag_name,
|
|
137
|
+
attrs=after_tag.strip() or None,
|
|
138
|
+
raw=raw,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Ket: |tag> or |tag attrs> or |tag>text
|
|
142
|
+
ket_match = _KET_RE.match(content)
|
|
143
|
+
if ket_match:
|
|
144
|
+
return _Line(
|
|
145
|
+
indent=indent,
|
|
146
|
+
type="ket",
|
|
147
|
+
tag=ket_match.group(1),
|
|
148
|
+
attrs=ket_match.group(2).strip() or None,
|
|
149
|
+
text=ket_match.group(3) or None,
|
|
150
|
+
raw=raw,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return _Line(indent=indent, type="text", text=content, raw=raw)
|
|
154
|
+
|
|
155
|
+
# -- Attribute conversion ------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def _parse_attribute_parts(self, attrs: str) -> list:
|
|
158
|
+
parts: list = []
|
|
159
|
+
current = ""
|
|
160
|
+
in_quotes = False
|
|
161
|
+
quote_char = ""
|
|
162
|
+
|
|
163
|
+
for i, char in enumerate(attrs):
|
|
164
|
+
if char in ("\"", "'") and (i == 0 or attrs[i - 1] != "\\"):
|
|
165
|
+
if not in_quotes:
|
|
166
|
+
in_quotes = True
|
|
167
|
+
quote_char = char
|
|
168
|
+
current += char
|
|
169
|
+
elif char == quote_char:
|
|
170
|
+
in_quotes = False
|
|
171
|
+
current += char
|
|
172
|
+
else:
|
|
173
|
+
current += char
|
|
174
|
+
elif char == " " and not in_quotes:
|
|
175
|
+
if current.strip():
|
|
176
|
+
parts.append(current.strip())
|
|
177
|
+
current = ""
|
|
178
|
+
else:
|
|
179
|
+
current += char
|
|
180
|
+
|
|
181
|
+
if current.strip():
|
|
182
|
+
parts.append(current.strip())
|
|
183
|
+
|
|
184
|
+
return parts
|
|
185
|
+
|
|
186
|
+
def _quote_value(self, value: str) -> str:
|
|
187
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
188
|
+
value.startswith("'") and value.endswith("'")
|
|
189
|
+
):
|
|
190
|
+
return value
|
|
191
|
+
return f'"{value}"'
|
|
192
|
+
|
|
193
|
+
def _convert_attributes(self, attrs: str) -> str:
|
|
194
|
+
if not attrs:
|
|
195
|
+
return ""
|
|
196
|
+
|
|
197
|
+
parts = self._parse_attribute_parts(attrs)
|
|
198
|
+
converted = []
|
|
199
|
+
for part in parts:
|
|
200
|
+
match = _ATTR_RE.match(part)
|
|
201
|
+
if not match:
|
|
202
|
+
converted.append(part)
|
|
203
|
+
continue
|
|
204
|
+
name, value = match.group(1), match.group(2)
|
|
205
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
206
|
+
value.startswith("'") and value.endswith("'")
|
|
207
|
+
):
|
|
208
|
+
converted.append(f"{name}={value}")
|
|
209
|
+
else:
|
|
210
|
+
converted.append(f'{name}="{value}"')
|
|
211
|
+
return " ".join(converted)
|
|
212
|
+
|
|
213
|
+
def _convert_bra_attributes(self, attrs: Optional[str]) -> str:
|
|
214
|
+
if not attrs:
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
parts = self._parse_attribute_parts(attrs)
|
|
218
|
+
converted = []
|
|
219
|
+
for part in parts:
|
|
220
|
+
match = _ATTR_RE.match(part)
|
|
221
|
+
if not match:
|
|
222
|
+
converted.append(part)
|
|
223
|
+
continue
|
|
224
|
+
name, value = match.group(1), match.group(2)
|
|
225
|
+
is_reserved = name in _RESERVED_BRA_ATTRS
|
|
226
|
+
attr_name = name if is_reserved else f"param-{name}"
|
|
227
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
|
228
|
+
value.startswith("'") and value.endswith("'")
|
|
229
|
+
):
|
|
230
|
+
converted.append(f"{attr_name}={value}")
|
|
231
|
+
else:
|
|
232
|
+
converted.append(f'{attr_name}="{value}"')
|
|
233
|
+
return " ".join(converted)
|
|
234
|
+
|
|
235
|
+
def _convert_ket_attributes(self, attrs: Optional[str], tag_name: str) -> str:
|
|
236
|
+
if not attrs:
|
|
237
|
+
return ""
|
|
238
|
+
|
|
239
|
+
parts = self._parse_attribute_parts(attrs)
|
|
240
|
+
has_positional = any("=" not in part for part in parts)
|
|
241
|
+
|
|
242
|
+
if not has_positional:
|
|
243
|
+
return self._convert_attributes(attrs)
|
|
244
|
+
|
|
245
|
+
positional_index = 0
|
|
246
|
+
converted = []
|
|
247
|
+
for part in parts:
|
|
248
|
+
match = _ATTR_RE.match(part)
|
|
249
|
+
if match:
|
|
250
|
+
name, value = match.group(1), match.group(2)
|
|
251
|
+
converted.append(f"{name}={self._quote_value(value)}")
|
|
252
|
+
else:
|
|
253
|
+
converted.append(f"_positional-{positional_index}={self._quote_value(part)}")
|
|
254
|
+
positional_index += 1
|
|
255
|
+
return " ".join(converted)
|
|
256
|
+
|
|
257
|
+
def _escape_xml(self, text: str) -> str:
|
|
258
|
+
return (
|
|
259
|
+
text.replace("&", "&") # must be first
|
|
260
|
+
.replace("<", "<")
|
|
261
|
+
.replace(">", ">")
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _convert_inline_kets(self, text: str) -> str:
|
|
265
|
+
parts = []
|
|
266
|
+
last_index = 0
|
|
267
|
+
|
|
268
|
+
for match in _INLINE_KET_RE.finditer(text):
|
|
269
|
+
if match.start() > last_index:
|
|
270
|
+
parts.append(self._escape_xml(text[last_index : match.start()]))
|
|
271
|
+
|
|
272
|
+
tag, attrs = match.group(1), match.group(2)
|
|
273
|
+
attr_str = f" {self._convert_attributes(attrs.strip())}" if attrs.strip() else ""
|
|
274
|
+
parts.append(f"<{tag}{attr_str}/>")
|
|
275
|
+
|
|
276
|
+
last_index = match.end()
|
|
277
|
+
|
|
278
|
+
if last_index < len(text):
|
|
279
|
+
parts.append(self._escape_xml(text[last_index:]))
|
|
280
|
+
|
|
281
|
+
return "".join(parts)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core interpreter - dispatches DiracElement nodes to tag handlers.
|
|
3
|
+
Mirrors dirac/src/runtime/interpreter.ts from the Node.js reference implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ..types import DiracElement, DiracSession
|
|
7
|
+
from .session import emit, substitute_attribute
|
|
8
|
+
from ..tags.assign import execute_assign
|
|
9
|
+
from ..tags.break_tag import execute_break
|
|
10
|
+
from ..tags.call import execute_call
|
|
11
|
+
from ..tags.defvar import execute_defvar
|
|
12
|
+
from ..tags.eval_tag import execute_eval
|
|
13
|
+
from ..tags.foreach import execute_foreach
|
|
14
|
+
from ..tags.if_tag import execute_if
|
|
15
|
+
from ..tags.import_tag import execute_import
|
|
16
|
+
from ..tags.input_tag import execute_input
|
|
17
|
+
from ..tags.llm_tag import execute_llm
|
|
18
|
+
from ..tags.loop import execute_loop
|
|
19
|
+
from ..tags.output import execute_output
|
|
20
|
+
from ..tags.parameters_tag import execute_parameters
|
|
21
|
+
from ..tags.return_tag import execute_return
|
|
22
|
+
from ..tags.subroutine import execute_subroutine
|
|
23
|
+
from ..tags.system import execute_system
|
|
24
|
+
from ..tags.test_if import execute_test_if
|
|
25
|
+
from ..tags.variable import execute_variable
|
|
26
|
+
|
|
27
|
+
# Tags whose body is a direct-call to a registered subroutine (anything not
|
|
28
|
+
# in this set of built-in tag names).
|
|
29
|
+
_BUILTIN_TAGS = {
|
|
30
|
+
"dirac-root",
|
|
31
|
+
"dirac",
|
|
32
|
+
"defvar",
|
|
33
|
+
"variable",
|
|
34
|
+
"assign",
|
|
35
|
+
"output",
|
|
36
|
+
"subroutine",
|
|
37
|
+
"call",
|
|
38
|
+
"parameters",
|
|
39
|
+
"loop",
|
|
40
|
+
"foreach",
|
|
41
|
+
"break",
|
|
42
|
+
"if",
|
|
43
|
+
"test-if",
|
|
44
|
+
"eval",
|
|
45
|
+
"python",
|
|
46
|
+
"system",
|
|
47
|
+
"input",
|
|
48
|
+
"return",
|
|
49
|
+
"import",
|
|
50
|
+
"llm",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def integrate(session: DiracSession, element: DiracElement) -> None:
|
|
55
|
+
"""Execute a single DiracElement node."""
|
|
56
|
+
# Text nodes
|
|
57
|
+
if element.text and not element.tag:
|
|
58
|
+
emit(session, substitute_attribute(session, element.text))
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Control flow: stop execution if return or break has been signaled.
|
|
62
|
+
if session.is_return or session.is_break:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
tag = element.tag.lower()
|
|
66
|
+
|
|
67
|
+
if tag in ("dirac-root", "dirac"):
|
|
68
|
+
integrate_children(session, element)
|
|
69
|
+
return
|
|
70
|
+
if tag == "defvar":
|
|
71
|
+
execute_defvar(session, element)
|
|
72
|
+
return
|
|
73
|
+
if tag == "variable":
|
|
74
|
+
execute_variable(session, element)
|
|
75
|
+
return
|
|
76
|
+
if tag == "assign":
|
|
77
|
+
execute_assign(session, element)
|
|
78
|
+
return
|
|
79
|
+
if tag == "output":
|
|
80
|
+
execute_output(session, element)
|
|
81
|
+
return
|
|
82
|
+
if tag == "subroutine":
|
|
83
|
+
execute_subroutine(session, element)
|
|
84
|
+
return
|
|
85
|
+
if tag == "call":
|
|
86
|
+
execute_call(session, element)
|
|
87
|
+
return
|
|
88
|
+
if tag == "parameters":
|
|
89
|
+
execute_parameters(session, element)
|
|
90
|
+
return
|
|
91
|
+
if tag == "loop":
|
|
92
|
+
execute_loop(session, element)
|
|
93
|
+
return
|
|
94
|
+
if tag == "foreach":
|
|
95
|
+
execute_foreach(session, element)
|
|
96
|
+
return
|
|
97
|
+
if tag == "break":
|
|
98
|
+
execute_break(session, element)
|
|
99
|
+
return
|
|
100
|
+
if tag == "if":
|
|
101
|
+
execute_if(session, element)
|
|
102
|
+
return
|
|
103
|
+
if tag == "test-if":
|
|
104
|
+
execute_test_if(session, element)
|
|
105
|
+
return
|
|
106
|
+
if tag in ("eval", "python"):
|
|
107
|
+
execute_eval(session, element)
|
|
108
|
+
return
|
|
109
|
+
if tag == "system":
|
|
110
|
+
execute_system(session, element)
|
|
111
|
+
return
|
|
112
|
+
if tag == "input":
|
|
113
|
+
execute_input(session, element)
|
|
114
|
+
return
|
|
115
|
+
if tag == "return":
|
|
116
|
+
execute_return(session, element)
|
|
117
|
+
return
|
|
118
|
+
if tag == "import":
|
|
119
|
+
execute_import(session, element)
|
|
120
|
+
return
|
|
121
|
+
if tag == "llm":
|
|
122
|
+
execute_llm(session, element)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Not a built-in tag - treat as a direct subroutine call, e.g. <greet name="Alice" />
|
|
126
|
+
execute_call(session, element)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def integrate_children(session: DiracSession, element: DiracElement) -> None:
|
|
130
|
+
"""Execute all children of an element in order."""
|
|
131
|
+
for child in element.children:
|
|
132
|
+
integrate(session, child)
|
|
133
|
+
if session.is_return or session.is_break:
|
|
134
|
+
break
|
dirac/runtime/parser.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
XML Parser for DIRAC (.di files).
|
|
3
|
+
Mirrors dirac/src/runtime/parser.ts (which uses fast-xml-parser) using the
|
|
4
|
+
Python standard library's xml.etree.ElementTree.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
|
|
10
|
+
from ..types import DiracElement
|
|
11
|
+
|
|
12
|
+
_SHEBANG_RE = re.compile(r"^#!.*\n")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DiracParser:
|
|
16
|
+
"""Parses DIRAC XML source into a DiracElement tree."""
|
|
17
|
+
|
|
18
|
+
def parse(self, source: str) -> DiracElement:
|
|
19
|
+
# Strip shebang line if present
|
|
20
|
+
if source.startswith("#!"):
|
|
21
|
+
source = _SHEBANG_RE.sub("", source, count=1)
|
|
22
|
+
|
|
23
|
+
# Always wrap in DIRAC-ROOT to ensure valid XML with a single root.
|
|
24
|
+
# This allows files with comments, multiple top-level elements, or no
|
|
25
|
+
# root element at all (matches the Node.js parser's behavior).
|
|
26
|
+
wrapped = f"<DIRAC-ROOT>\n{source}\n</DIRAC-ROOT>"
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
root = ET.fromstring(wrapped)
|
|
30
|
+
except ET.ParseError as exc:
|
|
31
|
+
raise ValueError(f"Failed to parse DIRAC XML: {exc}") from exc
|
|
32
|
+
|
|
33
|
+
return self._convert(root)
|
|
34
|
+
|
|
35
|
+
def _convert(self, node: ET.Element) -> DiracElement:
|
|
36
|
+
element = DiracElement(tag=node.tag, attributes=dict(node.attrib), children=[])
|
|
37
|
+
|
|
38
|
+
# Leading text (before the first child, if any)
|
|
39
|
+
if node.text:
|
|
40
|
+
element.children.append(DiracElement(tag="", text=node.text))
|
|
41
|
+
element.text = node.text
|
|
42
|
+
|
|
43
|
+
for child in node:
|
|
44
|
+
element.children.append(self._convert(child))
|
|
45
|
+
|
|
46
|
+
# Tail text (text following this child's closing tag)
|
|
47
|
+
if child.tail:
|
|
48
|
+
element.children.append(DiracElement(tag="", text=child.tail))
|
|
49
|
+
element.text = (element.text or "") + child.tail
|
|
50
|
+
|
|
51
|
+
return element
|