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.
- plan_kernel/__init__.py +3 -0
- plan_kernel/__main__.py +14 -0
- plan_kernel/evaluator.py +305 -0
- plan_kernel/expander.py +23 -0
- plan_kernel/kernel.py +219 -0
- plan_kernel/magics.py +72 -0
- plan_kernel/parser.py +11 -0
- plan_kernel/prelude.py +77 -0
- plan_kernel/render.py +161 -0
- plan_kernel/runtime/__init__.py +1 -0
- plan_kernel/runtime/bplan.py +20 -0
- plan_kernel/runtime/bplan_deps.py +115 -0
- plan_kernel/runtime/plan.py +133 -0
- plan_kernel-0.1.1.dist-info/METADATA +219 -0
- plan_kernel-0.1.1.dist-info/RECORD +17 -0
- plan_kernel-0.1.1.dist-info/WHEEL +5 -0
- plan_kernel-0.1.1.dist-info/top_level.txt +1 -0
plan_kernel/__init__.py
ADDED
plan_kernel/__main__.py
ADDED
|
@@ -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))
|
plan_kernel/evaluator.py
ADDED
|
@@ -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)
|
plan_kernel/expander.py
ADDED
|
@@ -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"]
|