plan-kernel 0.1.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.
@@ -0,0 +1,3 @@
1
+ """plan-kernel — a Jupyter kernel for the PLAN virtual machine."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,14 @@
1
+ """``python -m plan_kernel`` entry point.
2
+
3
+ With no args (or with ``-f <connection_file>`` supplied by Jupyter) this
4
+ launches the kernel. With the ``install`` subcommand it registers the
5
+ kernelspec.
6
+ """
7
+
8
+ import sys
9
+
10
+ from .kernel import _cli_main
11
+
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(_cli_main(sys.argv))
@@ -0,0 +1,305 @@
1
+ """Cell-level driver.
2
+
3
+ ``PlanKernelEvaluator`` ties parser → expander → runtime together with a
4
+ notebook-scoped ``Env``. ``eval_cell(source)`` returns a ``CellResult`` that
5
+ the kernel layer (phase 8) translates into a Jupyter MIME bundle.
6
+
7
+ Pipeline per cell: parse leading magics → apply them → parse_many on the
8
+ remaining body → for each form, macroexpand → thunk → backend evaluator.
9
+ Bind-only and trailing-bind cells produce ``bind <name>`` summary lines;
10
+ expression cells render the last form's value via ``plan_kernel.render``. Per-
11
+ stage exceptions become structured error envelopes; Python's recursion
12
+ limit is bumped to 200K so user-level recursion gets a fair shot.
13
+
14
+ Magic semantics:
15
+ - ``%backend NAME`` is per-cell — the backend reverts at end-of-cell.
16
+ - ``%reset`` is persistent — cleared bindings stay cleared.
17
+ - ``%env`` is read-only.
18
+
19
+ Deferred to phase 7: BPLAN op prelude auto-loading. ``%reset`` will
20
+ preserve the prelude once that lands.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import sys
26
+ from dataclasses import dataclass
27
+
28
+ from .expander import Env, MacroError, macroexpand, thunk
29
+ from .magics import MagicDirective, MagicError, parse_magics
30
+ from .parser import ParseError, parse_many
31
+ from .prelude import load_prelude
32
+ from .render import render_value
33
+ from .runtime.plan import (
34
+ is_nat,
35
+ nat_str, str_nat,
36
+ evaluate,
37
+ _unapp,
38
+ )
39
+
40
+
41
+ __all__ = ["CellResult", "PlanKernelEvaluator"]
42
+
43
+
44
+ _BIND_NAT = str_nat("#bind")
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # CellResult — public output shape (mirrors gallowglass's kernel).
49
+ # ---------------------------------------------------------------------------
50
+
51
+ @dataclass
52
+ class CellResult:
53
+ """Result of evaluating one cell.
54
+
55
+ Exactly one of ``value_text`` / ``error`` is populated for non-silent
56
+ cells. ``decls_only=True`` indicates a silent cell (empty source, or all
57
+ forms were silent assignments with no displayable summary).
58
+ """
59
+
60
+ value_text: str | None = None
61
+ value_html: str | None = None
62
+ error: dict | None = None
63
+ decls_only: bool = False
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Internal error envelope — same shape as gallowglass's ``_error_envelope``.
68
+ # ---------------------------------------------------------------------------
69
+
70
+ def _error_envelope(stage: str, err: Exception) -> dict:
71
+ loc = None
72
+ if isinstance(err, ParseError):
73
+ loc = {"file": None, "line": err.line, "col": err.col}
74
+ return {
75
+ "stage": stage,
76
+ "message": str(err),
77
+ "type": type(err).__name__,
78
+ "loc": loc,
79
+ }
80
+
81
+
82
+ def _stage_for(err: Exception) -> str:
83
+ if isinstance(err, ParseError):
84
+ return "parse"
85
+ if isinstance(err, MacroError):
86
+ return "expand"
87
+ if isinstance(err, MagicError):
88
+ return "magic"
89
+ if isinstance(err, RecursionError):
90
+ return "runtime"
91
+ return "internal"
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Helpers.
96
+ # ---------------------------------------------------------------------------
97
+
98
+ def _pretty_nat(n) -> str:
99
+ # ``n`` may be a Marduk ``Val`` (post Phase E swap) or a raw int
100
+ # (legacy callers). Normalize to int via ``.nat`` when needed.
101
+ if hasattr(n, "type") and n.type == "nat":
102
+ n = n.nat
103
+ if not isinstance(n, int):
104
+ return repr(n)
105
+ s = nat_str(n)
106
+ if s and not s.startswith("<nat:"):
107
+ return s
108
+ return str(n)
109
+
110
+
111
+ def _is_bind_form(form) -> bool:
112
+ """Structurally check whether ``form`` is a ``(#bind ...)`` macro call."""
113
+ parts = _unapp(form)
114
+ return (
115
+ len(parts) >= 2
116
+ and is_nat(parts[0]) and parts[0].nat == 0
117
+ and is_nat(parts[1]) and parts[1].nat == _BIND_NAT
118
+ )
119
+
120
+
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # PlanKernelEvaluator.
125
+ # ---------------------------------------------------------------------------
126
+
127
+ class PlanKernelEvaluator:
128
+ """Stateful plan-kernel evaluator.
129
+
130
+ Owns a notebook-scoped ``Env``. ``eval_cell(source)`` parses, expands,
131
+ and evaluates one cell, returning a ``CellResult``. Side-effects from
132
+ ``#bind`` accumulate across cells.
133
+
134
+ Parameters
135
+ ----------
136
+ env
137
+ A pre-existing ``Env`` to use; if ``None``, a fresh empty one is
138
+ created. Useful when the kernel needs to share an env across
139
+ instances.
140
+ backend
141
+ Either ``"evaluate"`` (formal Python evaluator, default) or
142
+ ``"bevaluate"`` (jet-aware, faster for arithmetic). The
143
+ ``%backend`` magic switches this at cell granularity.
144
+ prelude
145
+ If ``True`` (default), load the BPLAN op prelude into ``env`` so
146
+ ``(Add 2 3)`` and friends work in cell 1. ``%reset`` reloads the
147
+ prelude. Pass ``False`` for a fully empty env.
148
+ """
149
+
150
+ def __init__(self, env: Env | None = None, backend: str = "evaluate",
151
+ prelude: bool = True):
152
+ self.env = env if env is not None else Env()
153
+ self._set_backend(backend)
154
+ self._prelude_loaded = bool(prelude)
155
+ self._prelude_names: set[int] = set()
156
+ if prelude:
157
+ self._prelude_names = load_prelude(self.env)
158
+
159
+ # ------------------------------------------------------------------
160
+ # Public entry points.
161
+ # ------------------------------------------------------------------
162
+
163
+ def eval_cell(self, source: str) -> CellResult:
164
+ if not source.strip():
165
+ return CellResult(decls_only=True)
166
+
167
+ directives, body = parse_magics(source)
168
+
169
+ # Save backend state for cell-scoped %backend reverts. (env/reset
170
+ # changes deliberately persist.)
171
+ saved_backend = self._backend
172
+ saved_backend_name = self._backend_name
173
+ try:
174
+ magic_outputs: list[str] = []
175
+ try:
176
+ for d in directives:
177
+ out = self._apply_magic(d)
178
+ if out is not None:
179
+ magic_outputs.append(out)
180
+ except MagicError as e:
181
+ return CellResult(error=_error_envelope("magic", e))
182
+
183
+ return self._eval_body(body, magic_outputs)
184
+ finally:
185
+ self._backend = saved_backend
186
+ self._backend_name = saved_backend_name
187
+
188
+ def _eval_body(self, body: str, magic_outputs: list[str]) -> CellResult:
189
+ if not body.strip():
190
+ if magic_outputs:
191
+ return CellResult(value_text="\n".join(magic_outputs))
192
+ return CellResult(decls_only=True)
193
+
194
+ try:
195
+ forms = parse_many(body)
196
+ except ParseError as e:
197
+ return CellResult(error=_error_envelope("parse", e))
198
+ except Exception as e: # pragma: no cover — defensive
199
+ return CellResult(error=_error_envelope("internal", e))
200
+
201
+ if not forms:
202
+ if magic_outputs:
203
+ return CellResult(value_text="\n".join(magic_outputs))
204
+ return CellResult(decls_only=True)
205
+
206
+ old_limit = sys.getrecursionlimit()
207
+ sys.setrecursionlimit(max(old_limit, 200_000))
208
+ try:
209
+ result = self._evaluate_forms(forms)
210
+ except (MacroError, RecursionError) as e:
211
+ return CellResult(error=_error_envelope(_stage_for(e), e))
212
+ except Exception as e:
213
+ return CellResult(error=_error_envelope("internal", e))
214
+ finally:
215
+ sys.setrecursionlimit(old_limit)
216
+
217
+ if magic_outputs and result.value_text is not None:
218
+ result.value_text = "\n".join(magic_outputs) + "\n" + result.value_text
219
+ return result
220
+
221
+ def reset(self) -> None:
222
+ """Clear all user bindings. The prelude (if loaded) is reloaded."""
223
+ self.env.reset()
224
+ if self._prelude_loaded:
225
+ self._prelude_names = load_prelude(self.env)
226
+
227
+ @property
228
+ def backend_name(self) -> str:
229
+ return self._backend_name
230
+
231
+ # ------------------------------------------------------------------
232
+ # Internals.
233
+ # ------------------------------------------------------------------
234
+
235
+ def _set_backend(self, name: str) -> None:
236
+ if name == "evaluate":
237
+ self._backend = evaluate
238
+ self._backend_name = name
239
+ return
240
+ if name == "bevaluate":
241
+ # Resolved on first use (lazy import — bplan pulls in jet
242
+ # registration code we don't need until selected).
243
+ from .runtime.bplan import bevaluate
244
+ self._backend = bevaluate
245
+ self._backend_name = name
246
+ return
247
+ raise ValueError(
248
+ f"unknown backend: {name!r} (expected 'evaluate' or 'bevaluate')"
249
+ )
250
+
251
+ def _apply_magic(self, d: MagicDirective) -> str | None:
252
+ """Apply one magic directive. Returns optional display output."""
253
+ if d.name == "backend":
254
+ if len(d.args) != 1:
255
+ raise MagicError(f"{d.line}: %backend takes exactly one argument")
256
+ try:
257
+ self._set_backend(d.args[0])
258
+ except ValueError as e:
259
+ raise MagicError(f"{d.line}: {e}") from e
260
+ return None
261
+ if d.name == "reset":
262
+ if d.args:
263
+ raise MagicError(f"{d.line}: %reset takes no arguments")
264
+ self.reset()
265
+ return None
266
+ if d.name == "env":
267
+ if d.args:
268
+ raise MagicError(f"{d.line}: %env takes no arguments")
269
+ user_names = sorted(
270
+ _pretty_nat(n)
271
+ for n in self.env.names()
272
+ if n not in self._prelude_names
273
+ )
274
+ return ", ".join(user_names) if user_names else "(env empty)"
275
+ raise MagicError(f"unknown magic: %{d.name}")
276
+
277
+ def _eval_one(self, form):
278
+ """Run one form through expand → thunk → backend."""
279
+ expanded = macroexpand(form, self.env)
280
+ thunked = thunk(expanded, self.env)
281
+ return self._backend(thunked)
282
+
283
+ def _evaluate_forms(self, forms) -> CellResult:
284
+ bind_summaries: list[str] = []
285
+ last_value = None
286
+ last_was_bind = False
287
+
288
+ for form in forms:
289
+ is_bind = _is_bind_form(form)
290
+ result = self._eval_one(form)
291
+ if is_bind:
292
+ bind_summaries.append(f"bind {_pretty_nat(result)}")
293
+ else:
294
+ last_value = result
295
+ last_was_bind = is_bind
296
+
297
+ # Trailing-bind cells (and all-binds cells) display the bind summary
298
+ # block; expression-cells display their final value.
299
+ if last_was_bind:
300
+ if not bind_summaries:
301
+ return CellResult(decls_only=True) # pragma: no cover
302
+ return CellResult(value_text="\n".join(bind_summaries))
303
+
304
+ text, html = render_value(last_value)
305
+ return CellResult(value_text=text, value_html=html)
@@ -0,0 +1,23 @@
1
+ """Macro expander — compatibility shim.
2
+
3
+ Lifted to :mod:`marduk.asm.expander` as part of the Phase E Marduk swap.
4
+ This module re-exports the same API for callers that still import via
5
+ ``plan_kernel.expander``. New code should import from ``marduk.asm``.
6
+ """
7
+
8
+ from marduk.asm.expander import (
9
+ Env,
10
+ MacroError,
11
+ macroexpand,
12
+ thunk,
13
+ eval_form,
14
+ )
15
+
16
+
17
+ __all__ = [
18
+ "Env",
19
+ "MacroError",
20
+ "macroexpand",
21
+ "thunk",
22
+ "eval_form",
23
+ ]
plan_kernel/kernel.py ADDED
@@ -0,0 +1,219 @@
1
+ """Jupyter kernel and CLI entry points.
2
+
3
+ Two surface areas:
4
+
5
+ - :func:`_kernel_main` — invoked via ``python -m plan_kernel`` (i.e.
6
+ ``plan_kernel/__main__.py``). Lazy-imports ``ipykernel`` and launches the
7
+ kernel via ``IPKernelApp.launch_instance``.
8
+
9
+ - :func:`_install_kernelspec` — registers ``PLAN`` with Jupyter's
10
+ ``KernelSpecManager`` so the kernel picker shows it. The kernel.json's
11
+ ``argv`` is ``[sys.executable, '-m', 'plan_kernel', '-f', '{connection_file}']``,
12
+ so launching the kernel reuses the Python interpreter that ran ``install``.
13
+
14
+ The ``PlanKernel`` class is defined inside :func:`_kernel_main` so that
15
+ importing this module doesn't pull in ``ipykernel`` — that keeps test
16
+ environments without ``ipykernel`` (and the kernel-startup path itself)
17
+ fast and importable.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+ import tempfile
27
+ import traceback
28
+
29
+
30
+ __all__ = [
31
+ "_install_kernelspec",
32
+ "_kernel_main",
33
+ "_cli_main",
34
+ ]
35
+
36
+
37
+ _KERNEL_NAME = "plan_kernel"
38
+ _DISPLAY_NAME = "PLAN"
39
+ _IMPLEMENTATION = "plan-kernel"
40
+
41
+
42
+ def _kernel_main() -> None:
43
+ """Launch the kernel under ``ipykernel.IPKernelApp.launch_instance``."""
44
+ from ipykernel.kernelapp import IPKernelApp
45
+ from ipykernel.kernelbase import Kernel
46
+
47
+ from . import __version__
48
+ from .evaluator import PlanKernelEvaluator
49
+
50
+ class PlanKernel(Kernel):
51
+ implementation = _IMPLEMENTATION
52
+ implementation_version = __version__
53
+ language_info = {
54
+ "name": "plan",
55
+ "mimetype": "text/x-plan",
56
+ "file_extension": ".plan",
57
+ "pygments_lexer": "lisp",
58
+ }
59
+ banner = (
60
+ "PLAN kernel (plan-kernel) — type Plan Asm in a cell, see the reduced "
61
+ "PLAN value back. The BPLAN op prelude is auto-loaded; %backend, "
62
+ "%reset, %env are available as cell magics."
63
+ )
64
+
65
+ def __init__(self, **kwargs):
66
+ super().__init__(**kwargs)
67
+ self._evaluator = PlanKernelEvaluator()
68
+
69
+ def do_execute(self, code, silent, store_history=True,
70
+ user_expressions=None, allow_stdin=False, *,
71
+ cell_id=None):
72
+ try:
73
+ result = self._evaluator.eval_cell(code)
74
+ except Exception as e: # noqa: BLE001 — last-resort guard
75
+ tb = traceback.format_exception(type(e), e, e.__traceback__)
76
+ if not silent:
77
+ self.send_response(self.iopub_socket, "stream", {
78
+ "name": "stderr",
79
+ "text": "".join(tb),
80
+ })
81
+ return {
82
+ "status": "error",
83
+ "execution_count": self.execution_count,
84
+ "ename": type(e).__name__,
85
+ "evalue": str(e),
86
+ "traceback": tb,
87
+ }
88
+
89
+ if result.error is not None:
90
+ error_text = _format_error(result.error)
91
+ if not silent:
92
+ self.send_response(self.iopub_socket, "stream", {
93
+ "name": "stderr",
94
+ "text": error_text,
95
+ })
96
+ return {
97
+ "status": "error",
98
+ "execution_count": self.execution_count,
99
+ "ename": result.error.get("type", "Error"),
100
+ "evalue": result.error.get("message", ""),
101
+ "traceback": [error_text],
102
+ }
103
+
104
+ if result.value_text is not None and not silent:
105
+ data = {"text/plain": result.value_text}
106
+ if result.value_html is not None:
107
+ data["text/html"] = result.value_html
108
+ self.send_response(self.iopub_socket, "execute_result", {
109
+ "execution_count": self.execution_count,
110
+ "data": data,
111
+ "metadata": {},
112
+ })
113
+
114
+ return {
115
+ "status": "ok",
116
+ "execution_count": self.execution_count,
117
+ "payload": [],
118
+ "user_expressions": {},
119
+ }
120
+
121
+ IPKernelApp.launch_instance(kernel_class=PlanKernel)
122
+
123
+
124
+ def _format_error(envelope: dict) -> str:
125
+ """Render an error envelope for the Jupyter ``stream`` channel.
126
+
127
+ Format: ``[file:line:col: ]<stage> error: <message>``. The location
128
+ block is dropped when no ``loc`` is present (most expand/runtime
129
+ errors).
130
+ """
131
+ stage = envelope.get("stage", "error")
132
+ msg = envelope.get("message", "")
133
+ loc = envelope.get("loc")
134
+ if loc is not None:
135
+ file_part = loc.get("file") or "<cell>"
136
+ prefix = f"{file_part}:{loc['line']}:{loc['col']}: "
137
+ else:
138
+ prefix = ""
139
+ return f"{prefix}{stage} error: {msg}\n"
140
+
141
+
142
+ def _install_kernelspec(user: bool = True, prefix: str | None = None) -> str:
143
+ """Register the plan-kernel kernelspec with Jupyter.
144
+
145
+ Returns the install path (the directory containing ``kernel.json``).
146
+
147
+ Parameters
148
+ ----------
149
+ user
150
+ Install to the per-user kernels directory (default). Set to
151
+ ``False`` together with a ``prefix`` for an isolated install.
152
+ prefix
153
+ Optional prefix path. When set, the kernelspec is written under
154
+ ``<prefix>/share/jupyter/kernels/plan-kernel/``. Useful for venv-scoped
155
+ installs (``--prefix .venv``).
156
+ """
157
+ from jupyter_client.kernelspec import KernelSpecManager
158
+
159
+ spec = {
160
+ "argv": [
161
+ sys.executable,
162
+ "-m",
163
+ "plan_kernel",
164
+ "-f",
165
+ "{connection_file}",
166
+ ],
167
+ "display_name": _DISPLAY_NAME,
168
+ "language": "plan",
169
+ "metadata": {
170
+ "description": "plan-kernel — Jupyter kernel for the PLAN virtual machine",
171
+ },
172
+ }
173
+
174
+ with tempfile.TemporaryDirectory() as td:
175
+ spec_path = os.path.join(td, "kernel.json")
176
+ with open(spec_path, "w") as f:
177
+ json.dump(spec, f, indent=2)
178
+ ksm = KernelSpecManager()
179
+ installed = ksm.install_kernel_spec(
180
+ td,
181
+ kernel_name=_KERNEL_NAME,
182
+ user=user,
183
+ prefix=prefix,
184
+ )
185
+ return installed
186
+
187
+
188
+ def _cli_main(argv: list[str]) -> int:
189
+ """CLI entry point. Returns process exit code.
190
+
191
+ Subcommands:
192
+ - ``plan-kernel install [--prefix DIR] [--system]`` registers the
193
+ kernelspec and prints the install path.
194
+ - Any other invocation (typically ``-f <connection_file>`` from
195
+ Jupyter) falls through to :func:`_kernel_main`.
196
+ """
197
+ if len(argv) >= 2 and argv[1] == "install":
198
+ parser = argparse.ArgumentParser(
199
+ prog="plan-kernel install",
200
+ description="Register the plan-kernel kernelspec with Jupyter.",
201
+ )
202
+ parser.add_argument(
203
+ "--prefix",
204
+ default=None,
205
+ help="install under PREFIX/share/jupyter/kernels/plan-kernel (overrides --user)",
206
+ )
207
+ parser.add_argument(
208
+ "--system",
209
+ action="store_true",
210
+ help="install system-wide instead of per-user (the default)",
211
+ )
212
+ args = parser.parse_args(argv[2:])
213
+ user = not args.system and args.prefix is None
214
+ installed = _install_kernelspec(user=user, prefix=args.prefix)
215
+ print(f"Installed plan-kernel kernelspec at {installed}")
216
+ return 0
217
+
218
+ _kernel_main()
219
+ return 0
plan_kernel/magics.py ADDED
@@ -0,0 +1,72 @@
1
+ """Cell-leading ``%magic`` parsing.
2
+
3
+ A cell's *magic block* is its run of leading ``%``-prefixed lines, with
4
+ blank lines between them allowed once at least one magic has been seen.
5
+ Anything else — comments, source forms, blank cells — ends the magic
6
+ block, and the rest is the cell's source body.
7
+
8
+ Three magics are recognized by the evaluator:
9
+
10
+ - ``%backend evaluate | bevaluate`` — pick the runtime evaluator. The
11
+ evaluator scopes this to the current cell only.
12
+ - ``%reset`` — clear all bindings from the env. (Persistent.)
13
+ - ``%env`` — display the names currently bound. (Read-only.)
14
+
15
+ This module just *parses* the magic block; the evaluator dispatches each
16
+ directive (and decides what counts as a per-cell vs. persistent action).
17
+ Unknown directives are returned as-is — the dispatcher raises
18
+ ``MagicError`` if it can't handle one.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+
25
+
26
+ __all__ = ["MagicDirective", "MagicError", "parse_magics"]
27
+
28
+
29
+ class MagicError(Exception):
30
+ """Raised when a magic directive is malformed, unsupported, or its
31
+ arguments don't validate."""
32
+
33
+
34
+ @dataclass
35
+ class MagicDirective:
36
+ name: str # e.g. 'backend', 'reset', 'env'
37
+ args: list[str] = field(default_factory=list)
38
+
39
+ @property
40
+ def line(self) -> str:
41
+ if not self.args:
42
+ return f"%{self.name}"
43
+ return f"%{self.name} {' '.join(self.args)}"
44
+
45
+
46
+ def parse_magics(source: str) -> tuple[list[MagicDirective], str]:
47
+ """Split a cell's leading ``%magic`` lines from its source body.
48
+
49
+ The body retains its original indentation/whitespace — only the
50
+ consumed magic lines (and any blank lines between them) are removed.
51
+ """
52
+ lines = source.splitlines(keepends=True)
53
+ directives: list[MagicDirective] = []
54
+ i = 0
55
+ while i < len(lines):
56
+ stripped = lines[i].lstrip()
57
+ if stripped.startswith("%"):
58
+ tokens = stripped[1:].split()
59
+ if not tokens:
60
+ # Bare `%` line — not a magic; treat as body so the user
61
+ # gets a clear parse error rather than a silent skip.
62
+ break
63
+ directives.append(MagicDirective(name=tokens[0], args=tokens[1:]))
64
+ i += 1
65
+ continue
66
+ if stripped == "" and directives:
67
+ # Blank line *between* magics — absorb so a multi-magic block
68
+ # can be visually separated without breaking the parse.
69
+ i += 1
70
+ continue
71
+ break
72
+ return directives, "".join(lines[i:])
plan_kernel/parser.py ADDED
@@ -0,0 +1,11 @@
1
+ """Plan Asm reader — compatibility shim.
2
+
3
+ Lifted to :mod:`marduk.asm.reader` as part of the Phase E Marduk swap.
4
+ This module re-exports the same API for callers that still import via
5
+ ``plan_kernel.parser``. New code should import from ``marduk.asm``.
6
+ """
7
+
8
+ from marduk.asm.reader import ParseError, parse, parse_many
9
+
10
+
11
+ __all__ = ["ParseError", "parse", "parse_many"]