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 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("&", "&amp;") # must be first
260
+ .replace("<", "&lt;")
261
+ .replace(">", "&gt;")
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
@@ -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