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/prelude.py ADDED
@@ -0,0 +1,77 @@
1
+ """BPLAN op prelude — boot.plan-style wrappers auto-loaded on kernel start.
2
+
3
+ For each ``(name, arity)`` in ``runtime.bplan_deps.ALL_DEPS``, this module
4
+ constructs the boot.plan-style wrapper:
5
+
6
+ .. code-block:: text
7
+
8
+ (#bind Name
9
+ (#pin
10
+ (#law "Name" (Name a b ...)
11
+ ((#pin "B") ("Name" a b ...)))))
12
+
13
+ and evaluates it against the supplied env. After loading, expressions like
14
+ ``(Add 2 3)``, ``(Inc 41)``, ``(Eq x y)`` work in any cell.
15
+
16
+ Under Phase E (the Marduk swap), the runtime is :mod:`marduk.runtime` and
17
+ the BPLAN op table is :mod:`marduk.runtime.bplan`. Wrappers for op names
18
+ in ``ALL_DEPS`` that aren't yet implemented in Marduk's op table still
19
+ *bind* successfully — the wrapper Law is constructed without evaluating
20
+ its body — but calling them at runtime will raise NotImplementedError
21
+ from the BPLAN dispatcher. Add the op upstream rather than working
22
+ around it here.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from marduk.asm import Env, eval_form, parse_many
28
+ from marduk.runtime.strnat import str_nat
29
+
30
+ from .runtime.bplan_deps import ALL_DEPS
31
+
32
+
33
+ __all__ = ["PRELUDE_NAMES", "load_prelude"]
34
+
35
+
36
+ # Single-letter argument names. ``ALL_DEPS`` arities cap at 6 (``Elim``);
37
+ # 26 letters is plenty.
38
+ _ARG_LETTERS = "abcdefghijklmnopqrstuvwxyz"
39
+
40
+
41
+ def _arg_names(arity: int) -> str:
42
+ if arity > len(_ARG_LETTERS):
43
+ raise ValueError(
44
+ f"prelude: arity {arity} exceeds supported letters ({len(_ARG_LETTERS)})"
45
+ )
46
+ return " ".join(_ARG_LETTERS[i] for i in range(arity))
47
+
48
+
49
+ def _wrapper_source(name: str, arity: int) -> str:
50
+ args = _arg_names(arity)
51
+ sig = f"{name} {args}"
52
+ body = f'((#pin "B") ("{name}" {args}))'
53
+ return f'(#bind {name} (#pin (#law "{name}" ({sig}) {body})))'
54
+
55
+
56
+ def load_prelude(env: Env) -> set[int]:
57
+ """Load every BPLAN op wrapper into ``env``.
58
+
59
+ Returns the set of name nats that were bound — useful for the
60
+ evaluator's ``%env`` magic to filter prelude noise from user bindings.
61
+ """
62
+ bound: set[int] = set()
63
+ for name, arity in ALL_DEPS.items():
64
+ if arity < 1:
65
+ continue
66
+ src = _wrapper_source(name, arity)
67
+ for form in parse_many(src):
68
+ eval_form(form, env)
69
+ bound.add(str_nat(name))
70
+ return bound
71
+
72
+
73
+ # Precomputed set of all prelude name nats. Equivalent to the return value
74
+ # of ``load_prelude(...)`` but available without performing the load.
75
+ PRELUDE_NAMES: frozenset[int] = frozenset(
76
+ str_nat(name) for name, arity in ALL_DEPS.items() if arity >= 1
77
+ )
plan_kernel/render.py ADDED
@@ -0,0 +1,161 @@
1
+ """Structural value renderer.
2
+
3
+ ``render_value(val)`` returns a ``(text/plain, text/html)`` pair the
4
+ kernel emits as a Jupyter MIME bundle. Both forms are depth-bounded so
5
+ a pathological structure (a malformed App tree, a Pin loop) can't blow
6
+ the stack while we're trying to *show* it.
7
+
8
+ Format:
9
+
10
+ - Nat ``n`` → ``"42"`` (decimal).
11
+ - Pin ``v`` → ``"<v>"``.
12
+ - Pin ``Law`` → ``"<{'name'…}>"`` when ``pretty=True`` (default);
13
+ otherwise expanded as ``"<{name arity body}>"``.
14
+ - Law ``{a n b}`` → ``"{'name' arity body}"``; ``name`` is decoded
15
+ via ``nat_str`` and shown single-quoted (or
16
+ decimal if the nat doesn't decode to printable
17
+ UTF-8).
18
+ - App ``f x`` → ``"(f x)"``.
19
+
20
+ The HTML output uses inline ``style="…"`` (Jupyter strips ``<style>``
21
+ blocks) and a cross-theme palette that reads on both light and dark
22
+ backgrounds.
23
+
24
+ Phase E note: this module now reads Marduk's ``Val`` accessors —
25
+ ``.head``/``.tail`` for App, ``.item`` for Pin, ``.args`` for Law's
26
+ arity (a ``Val``, not an int), ``.name`` (also a ``Val``). The
27
+ predicates ``is_app``/``is_law``/etc. come from the runtime shim,
28
+ which dispatches on ``val.type``.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import html
34
+
35
+ from marduk.runtime.strnat import nat_str
36
+
37
+ from .runtime.plan import is_app, is_law, is_nat, is_pin
38
+
39
+
40
+ __all__ = ["render_value"]
41
+
42
+
43
+ _DEFAULT_DEPTH = 32
44
+
45
+ # Cross-theme-friendly palette.
46
+ _NAT = "color:#0097a7" # cyan
47
+ _LAW = "color:#e65100;font-style:italic" # orange italic
48
+ _NAME = "color:#388e3c" # green (decoded name)
49
+ _MUTED = "color:#999" # gray (brackets)
50
+
51
+
52
+ def render_value(val, *, pretty: bool = True,
53
+ max_depth: int = _DEFAULT_DEPTH) -> tuple[str, str]:
54
+ """Return ``(text, html)`` for ``val``.
55
+
56
+ Parameters
57
+ ----------
58
+ pretty
59
+ Collapse ``Pin(Law(...))`` to ``<{name…}>`` for terse display
60
+ (default). Set to ``False`` to fully expand into
61
+ ``<{name arity body}>``.
62
+ max_depth
63
+ Recursion bound. Past this depth the renderer emits an ellipsis.
64
+ """
65
+ text = _text(val, depth=0, pretty=pretty, max_depth=max_depth)
66
+ body = _html(val, depth=0, pretty=pretty, max_depth=max_depth)
67
+ full_html = (
68
+ '<code style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;'
69
+ f'font-size:0.9em">{body}</code>'
70
+ )
71
+ return text, full_html
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # text/plain
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def _text(val, *, depth: int, pretty: bool, max_depth: int) -> str:
79
+ if depth >= max_depth:
80
+ return "..."
81
+ if is_nat(val):
82
+ return str(val.nat)
83
+ if is_pin(val):
84
+ if pretty and is_law(val.item):
85
+ return f"<{{{_law_name_text(val.item)}…}}>"
86
+ inner = _text(val.item, depth=depth + 1, pretty=pretty, max_depth=max_depth)
87
+ return f"<{inner}>"
88
+ if is_law(val):
89
+ body = _text(val.body, depth=depth + 1, pretty=pretty, max_depth=max_depth)
90
+ return f"{{{_law_name_text(val)} {val.args.nat} {body}}}"
91
+ if is_app(val):
92
+ f = _text(val.head, depth=depth + 1, pretty=pretty, max_depth=max_depth)
93
+ a = _text(val.tail, depth=depth + 1, pretty=pretty, max_depth=max_depth)
94
+ return f"({f} {a})"
95
+ return repr(val)
96
+
97
+
98
+ def _law_name_text(law) -> str:
99
+ """Decode a law's ``name`` via ``nat_str`` for display.
100
+
101
+ Returns ``'identifier'`` (single-quoted) for printable UTF-8 nats, or
102
+ decimal for nats that don't decode. Non-nat names recurse through the
103
+ text renderer with a small depth bound.
104
+ """
105
+ if is_nat(law.name):
106
+ s = nat_str(law.name.nat)
107
+ if s and not s.startswith("<nat:"):
108
+ return f"'{s}'"
109
+ return str(law.name.nat)
110
+ return _text(law.name, depth=0, pretty=True, max_depth=4)
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # text/html
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def _html(val, *, depth: int, pretty: bool, max_depth: int) -> str:
118
+ if depth >= max_depth:
119
+ return f'<span style="{_MUTED}">…</span>'
120
+ if is_nat(val):
121
+ return f'<span style="{_NAT}">{val.nat}</span>'
122
+ if is_pin(val):
123
+ if pretty and is_law(val.item):
124
+ return (
125
+ f'<span style="{_MUTED}">&lt;{{</span>'
126
+ f"{_law_name_html(val.item)}"
127
+ f'<span style="{_MUTED}">…}}&gt;</span>'
128
+ )
129
+ inner = _html(val.item, depth=depth + 1, pretty=pretty, max_depth=max_depth)
130
+ return (
131
+ f'<span style="{_MUTED}">&lt;</span>'
132
+ f"{inner}"
133
+ f'<span style="{_MUTED}">&gt;</span>'
134
+ )
135
+ if is_law(val):
136
+ body = _html(val.body, depth=depth + 1, pretty=pretty, max_depth=max_depth)
137
+ return (
138
+ f'<span style="{_MUTED}">{{</span>'
139
+ f"{_law_name_html(val)} "
140
+ f'<span style="{_NAT}">{val.args.nat}</span> '
141
+ f"{body}"
142
+ f'<span style="{_MUTED}">}}</span>'
143
+ )
144
+ if is_app(val):
145
+ f = _html(val.head, depth=depth + 1, pretty=pretty, max_depth=max_depth)
146
+ a = _html(val.tail, depth=depth + 1, pretty=pretty, max_depth=max_depth)
147
+ return (
148
+ f'<span style="{_MUTED}">(</span>'
149
+ f"{f} {a}"
150
+ f'<span style="{_MUTED}">)</span>'
151
+ )
152
+ return html.escape(repr(val), quote=False)
153
+
154
+
155
+ def _law_name_html(law) -> str:
156
+ if is_nat(law.name):
157
+ s = nat_str(law.name.nat)
158
+ if s and not s.startswith("<nat:"):
159
+ return f'<span style="{_NAME}">\'{html.escape(s)}\'</span>'
160
+ return f'<span style="{_NAT}">{law.name.nat}</span>'
161
+ return f'<span style="{_LAW}">{html.escape(repr(law.name))}</span>'
@@ -0,0 +1 @@
1
+ """Vendored PLAN runtime — see VENDOR.md for source provenance and sync policy."""
@@ -0,0 +1,20 @@
1
+ """BPLAN harness — compatibility shim.
2
+
3
+ The legacy plan-kernel BPLAN harness implemented jet-based dispatch on
4
+ top of the old runtime. Phase E moves the canonical BPLAN op table into
5
+ :mod:`marduk.runtime.bplan`, where dispatch is integrated with the
6
+ spec-faithful core. This module re-exports the new table so callers
7
+ that import from ``plan_kernel.runtime.bplan`` continue to resolve.
8
+
9
+ Note: the legacy file used to define a separate ``bevaluate`` driver
10
+ distinct from ``evaluate``. With Marduk both modes share the same
11
+ evaluator (the spec-faithful core does the right thing without a jet
12
+ overlay), so the historic distinction has gone away — ``bevaluate``
13
+ here is an alias for ``evaluate``.
14
+ """
15
+
16
+ from marduk.runtime import evaluate as bevaluate
17
+ from marduk.runtime.bplan import OPS, dispatch
18
+
19
+
20
+ __all__ = ["OPS", "dispatch", "bevaluate"]
@@ -0,0 +1,115 @@
1
+ """
2
+ BPLAN named-op dependencies of the Gallowglass codegen.
3
+
4
+ Per `DECISIONS.md §"Upstream PLAN authority"`, the canonical PLAN spec is
5
+ `vendor/reaver/src/hs/Plan.hs`. PLAN proper and the Plan Asm text format are
6
+ frozen; the BPLAN named-op set may drift over time.
7
+
8
+ This module enumerates every BPLAN intrinsic that gallowglass-emitted code
9
+ relies on, by name and arity. `tests/sanity/test_bplan_deps.py` greps the
10
+ pinned `vendor/reaver/src/hs/Plan.hs` and asserts presence at the right arity.
11
+ This is the canary that fires when `vendor.lock` is bumped to a Reaver SHA
12
+ that has renamed, removed, or rearity'd one of our deps.
13
+
14
+ Each entry: `name -> arity`.
15
+
16
+ Categories:
17
+ Core primitives — needed by codegen for opcode-equivalent operations.
18
+ Arithmetic/bytes — needed by Core.Nat / Core.Text / Core.Bytes prelude.
19
+ RPLAN — needed for I/O (deferred; see Phase G).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+
25
+ # Core primitives gallowglass emits directly from codegen.
26
+ # Replaces the legacy P(N(0))..P(N(4)) opcode-pin shape with BPLAN named-Law
27
+ # references that delegate to (P("B")) ("Name" args).
28
+ CORE_PRIMITIVES: dict[str, int] = {
29
+ 'Pin': 1, # was P(N(0))
30
+ 'Law': 3, # was P(N(1)) / MkLaw
31
+ 'Inc': 1, # was P(N(2))
32
+ 'Elim': 6, # was P(N(3)) / Case_ — canonical 6-arity dispatch
33
+ 'Force': 1, # was P(N(4))
34
+ }
35
+
36
+
37
+ # Prelude arithmetic / introspection — used by the BPLAN harness as Python
38
+ # fast-path jets, but their existence in Reaver is what makes gallowglass-
39
+ # emitted prelude code actually run on Reaver at all.
40
+ PRELUDE_INTRINSICS: dict[str, int] = {
41
+ # Arithmetic
42
+ 'Add': 2,
43
+ 'Sub': 2,
44
+ 'Mul': 2,
45
+ 'Div': 2,
46
+ 'Mod': 2,
47
+ 'Dec': 1,
48
+ # Comparison — full set
49
+ 'Eq': 2,
50
+ 'Ne': 2,
51
+ 'Lt': 2,
52
+ 'Le': 2,
53
+ 'Gt': 2,
54
+ 'Ge': 2,
55
+ 'Cmp': 2,
56
+ # Boolean / control
57
+ 'Truth': 1,
58
+ 'Or': 2,
59
+ 'And': 2,
60
+ 'If': 3,
61
+ 'Ifz': 3,
62
+ # Bit ops (used by bytes encoding)
63
+ 'Lsh': 2,
64
+ 'Rsh': 2,
65
+ 'Bex': 1, # 2^n — used for bytesBar high-bit decoding in REPL demos
66
+ # Introspection (used in the harness; future codegen may emit these)
67
+ 'Type': 1,
68
+ 'IsPin': 1,
69
+ 'IsLaw': 1,
70
+ 'IsApp': 1,
71
+ 'IsNat': 1,
72
+ 'Hd': 1,
73
+ 'Sz': 1,
74
+ 'Unpin': 1,
75
+ 'Arity': 1,
76
+ 'Name': 1,
77
+ 'Body': 1,
78
+ # Sequencing
79
+ 'Seq': 2,
80
+ # Diagnostics
81
+ 'Trace': 2,
82
+ # Small Case dispatchers — Case<N> takes one nat plus N-1 indexed
83
+ # branches plus a fallback, so total arity is N+1.
84
+ 'Case2': 3,
85
+ 'Case3': 4,
86
+ 'Case4': 5,
87
+ 'Case5': 6,
88
+ 'Case6': 7,
89
+ 'Case7': 8,
90
+ 'Case8': 9,
91
+ 'Case9': 10,
92
+ 'Case10': 11,
93
+ 'Case11': 12,
94
+ 'Case12': 13,
95
+ 'Case13': 14,
96
+ 'Case14': 15,
97
+ 'Case15': 16,
98
+ 'Case16': 17,
99
+ }
100
+
101
+
102
+ # All BPLAN deps as a single dict, suitable for the sanity test.
103
+ ALL_DEPS: dict[str, int] = {
104
+ **CORE_PRIMITIVES,
105
+ **PRELUDE_INTRINSICS,
106
+ }
107
+
108
+
109
+ # strNat encoding helper — packs UTF-8 bytes little-endian into a Python int.
110
+ # Mirrors `bootstrap.codegen.encode_name`. Defined here so `bplan_deps`
111
+ # stays free of bootstrap dependencies and can be imported from tests.
112
+ def str_nat(s: str) -> int:
113
+ """Encode s as a little-endian nat (Reaver's `strNat`)."""
114
+ raw = s.encode('utf-8')
115
+ return int.from_bytes(raw, 'little')
@@ -0,0 +1,133 @@
1
+ """PLAN runtime — compatibility shim re-exporting Marduk's runtime under
2
+ the legacy plan-kernel API names.
3
+
4
+ Plan-kernel originally vendored a copy of the gallowglass harness at
5
+ ``runtime/plan.py``. As part of the Phase E swap, the runtime is now
6
+ provided by :mod:`marduk.runtime`; this file maps the old names
7
+ (``A``, ``L``, ``N``, ``P``, ``is_app``/``is_law``/``is_nat``/``is_pin``,
8
+ ``_unapp``, ``evaluate``, ``str_nat``, ``nat_str``) onto the new API so
9
+ existing callers don't have to change.
10
+
11
+ A few semantics differ from the legacy runtime that matter for callers:
12
+
13
+ * ``L(arity, name, body)`` constructs a Marduk law via the smart
14
+ constructor — it forces the body's let-binding spine via ``B`` (per
15
+ spec), so callers shouldn't expect deferred evaluation of binding
16
+ values.
17
+ * ``N(n)`` returns a ``Val`` (not a raw int). Comparisons of the form
18
+ ``some_val == 0`` no longer work — use ``some_val.nat == 0``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from marduk.runtime import (
24
+ Val, Hol,
25
+ Nat as _Nat,
26
+ Pin as _Pin,
27
+ App as _App,
28
+ Law as _Law,
29
+ evaluate as _evaluate,
30
+ force,
31
+ PlanError,
32
+ PlanLoop,
33
+ )
34
+ from marduk.runtime.strnat import str_nat, nat_str
35
+
36
+
37
+ __all__ = [
38
+ # constructors (legacy short names)
39
+ "A", "L", "N", "P", "Hol",
40
+ # new long names also exposed for forward-compat
41
+ "App", "Law", "Nat", "Pin", "Val",
42
+ # type predicates
43
+ "is_app", "is_law", "is_nat", "is_pin",
44
+ # spine flatten
45
+ "_unapp", "unapp",
46
+ # drivers
47
+ "evaluate", "force",
48
+ # str-nat helpers
49
+ "str_nat", "nat_str",
50
+ # exceptions
51
+ "PlanError", "PlanLoop",
52
+ ]
53
+
54
+
55
+ # ---- New-API aliases -------------------------------------------------------
56
+
57
+ App = _App
58
+ Law = _Law
59
+ Nat = _Nat
60
+ Pin = _Pin
61
+ evaluate = _evaluate
62
+
63
+
64
+ # ---- Legacy short-name aliases --------------------------------------------
65
+
66
+ def _coerce(x):
67
+ """Coerce a Python int to a Marduk ``Val``; pass other ``Val`` through.
68
+
69
+ Plan-kernel's legacy runtime treated raw Python ints as nats, so call
70
+ sites like ``P(5)`` meant "pin a nat 5". Marduk has no raw-int
71
+ representation for nats — every nat is a ``Val``. The legacy
72
+ constructor shims auto-coerce so existing code keeps working.
73
+ """
74
+ if isinstance(x, Val):
75
+ return x
76
+ if isinstance(x, int):
77
+ return _Nat(x)
78
+ return x
79
+
80
+
81
+ def A(f, x):
82
+ """Legacy alias for :func:`marduk.runtime.App`."""
83
+ return _App(_coerce(f), _coerce(x))
84
+
85
+
86
+ def L(arity, name, body):
87
+ """Legacy law constructor: ``L(arity, name, body)``.
88
+
89
+ Plan-kernel's ``class L`` was ``__init__(self, arity, name, body)``.
90
+ Marduk's smart constructor uses ``Law(name, arity, body)``; this
91
+ shim flips the order back, wraps int arity in a ``Nat``, and
92
+ coerces int name/body args.
93
+ """
94
+ arity_val = arity if isinstance(arity, Val) else _Nat(arity)
95
+ return _Law(_coerce(name), arity_val, _coerce(body))
96
+
97
+
98
+ def N(n):
99
+ """Legacy alias for :func:`marduk.runtime.Nat`. Wraps an int as a Val."""
100
+ return _Nat(n)
101
+
102
+
103
+ def P(item):
104
+ """Legacy alias for :func:`marduk.runtime.Pin`."""
105
+ return _Pin(_coerce(item))
106
+
107
+
108
+ # ---- Type predicates -------------------------------------------------------
109
+
110
+ def is_app(v):
111
+ return isinstance(v, Val) and v.type == "app"
112
+
113
+
114
+ def is_law(v):
115
+ return isinstance(v, Val) and v.type == "law"
116
+
117
+
118
+ def is_nat(v):
119
+ return isinstance(v, Val) and v.type == "nat"
120
+
121
+
122
+ def is_pin(v):
123
+ return isinstance(v, Val) and v.type == "pin"
124
+
125
+
126
+ # ---- Spine flatten ---------------------------------------------------------
127
+
128
+ def unapp(v):
129
+ """Public alias for :attr:`marduk.runtime.Val.spine`."""
130
+ return v.spine
131
+
132
+
133
+ _unapp = unapp