co-lambda 0.5.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.
- co_lambda/__init__.py +1 -0
- co_lambda/_analysis.py +92 -0
- co_lambda/_ast.py +372 -0
- co_lambda/_binnat.py +170 -0
- co_lambda/_codec.py +147 -0
- co_lambda/_compiler_artifact.py +50 -0
- co_lambda/_defun_codegen.py +321 -0
- co_lambda/_defun_runtime.py +188 -0
- co_lambda/_defunctionalize.py +470 -0
- co_lambda/_dsl.py +148 -0
- co_lambda/_generated/.gitattributes +5 -0
- co_lambda/_generated/__init__.py +1 -0
- co_lambda/_generated/_generated_defun_compiler_py311.py +7586 -0
- co_lambda/_generated/_generated_defun_compiler_py312.py +7586 -0
- co_lambda/_generated/_generated_defun_compiler_py313.py +7586 -0
- co_lambda/_hoas_latex.py +144 -0
- co_lambda/_latex.py +58 -0
- co_lambda/_prelude.py +163 -0
- co_lambda/_pyast.py +418 -0
- co_lambda/_pybuild.py +313 -0
- co_lambda/_reduce.py +145 -0
- co_lambda/_shape.py +224 -0
- co_lambda/_sugar.py +74 -0
- co_lambda/_typecheck.py +370 -0
- co_lambda-0.5.0.dist-info/METADATA +84 -0
- co_lambda-0.5.0.dist-info/RECORD +28 -0
- co_lambda-0.5.0.dist-info/WHEEL +4 -0
- co_lambda-0.5.0.dist-info/entry_points.txt +2 -0
co_lambda/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A least-fixpoint first-order-shape-relation interpreter for the lambda-calculus."""
|
co_lambda/_analysis.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Specialization analysis written in the lambda-calculus itself.
|
|
2
|
+
|
|
3
|
+
The analysis that decides which sub-terms to specialize is a pure lambda term, run by the
|
|
4
|
+
interpreter on the quoted program, so the calculus analyzes its own programs: a demonstration that
|
|
5
|
+
tabling-based reduction expresses program analysis, not only evaluation. This module holds the
|
|
6
|
+
closedness and depth measures; richer certificates (typability, fuel-bounded normalization) live in
|
|
7
|
+
``_typecheck`` and ``_reduce`` in the same style.
|
|
8
|
+
|
|
9
|
+
This module is pure lambda calculus: every top-level binding is a ``Builder`` (a ``@curry``-decorated
|
|
10
|
+
``def`` IS a Builder). The Python-side verdict readers live at the boundary (``_specialize``).
|
|
11
|
+
|
|
12
|
+
``LOOSE_BOUND`` is a DEPTH-FREE closedness measure, so the interpreter's interning shares it across
|
|
13
|
+
every position: ``LOOSE_BOUND quoted`` takes no binder depth, so ``app(LOOSE_BOUND, sub)`` is the
|
|
14
|
+
SAME node for an interned sub-term and is tabled once; a whole-tree scan is then linear. It returns
|
|
15
|
+
the number of enclosing binders the sub-term needs (the de Bruijn ``loose_bound``): a variable needs
|
|
16
|
+
index+1, an abstraction discharges one (floored at zero by ``PRED``), an application needs the larger
|
|
17
|
+
of the two. A sub-term is closed exactly when it needs none (``IS_CLOSED``).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from co_lambda._dsl import Builder, app, curry, lam
|
|
23
|
+
from co_lambda._prelude import IS_ZERO, PLUS, PRED, SUCC, Y
|
|
24
|
+
from co_lambda._sugar import ap
|
|
25
|
+
|
|
26
|
+
# Church arithmetic for the measures (truncated subtraction gives the comparisons).
|
|
27
|
+
_SUBTRACT: Builder = lam(lambda a: lam(lambda b: app(app(b, PRED), a))) # a - b, floored at zero
|
|
28
|
+
_AT_MOST: Builder = lam(lambda a: lam(lambda b: app(IS_ZERO, ap(_SUBTRACT, a, b)))) # a <= b
|
|
29
|
+
_MAX: Builder = lam(lambda a: lam(lambda b: ap(_AT_MOST, a, b, b, a))) # a <= b ? b : a
|
|
30
|
+
|
|
31
|
+
LOOSE_BOUND: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
|
|
32
|
+
quoted,
|
|
33
|
+
lam(lambda index: app(SUCC, index)), # QVar index: needs index+1 enclosing binders
|
|
34
|
+
lam(lambda body: app(PRED, app(self_recursion, body))), # QLam body: discharges one binder
|
|
35
|
+
lam(lambda function: lam(lambda argument: ap(
|
|
36
|
+
_MAX, app(self_recursion, function), app(self_recursion, argument),
|
|
37
|
+
))), # QApp f a: the larger of the two
|
|
38
|
+
))))
|
|
39
|
+
|
|
40
|
+
IS_CLOSED: Builder = lam(lambda quoted: app(IS_ZERO, app(LOOSE_BOUND, quoted))) # closed iff needs none
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# DEPTH: the nesting depth of a quoted term (a Church numeral), a cheap path-free measure the interner
|
|
44
|
+
# shares per distinct sub-term. It bounds the simple-typability check: running algorithm-W on a large
|
|
45
|
+
# (deep) closed combinator is expensive and the no-GC interner retains every reduction, so a specializer
|
|
46
|
+
# only certifies an island when the sub-term is shallow enough (``depth_at_most``), leaving a deep closed
|
|
47
|
+
# region reconstructed as an interpreted graph rather than flattened to a strict island. The bound only
|
|
48
|
+
# ever makes the certificate MORE conservative (fewer islands), never unsound.
|
|
49
|
+
DEPTH: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
|
|
50
|
+
quoted,
|
|
51
|
+
lam(lambda index: lam(lambda s: lam(lambda z: z))), # QVar: a leaf (depth zero)
|
|
52
|
+
lam(lambda body: app(SUCC, app(self_recursion, body))), # QLam: one deeper
|
|
53
|
+
lam(lambda function: lam(lambda argument: app(SUCC, ap(
|
|
54
|
+
_MAX, app(self_recursion, function), app(self_recursion, argument),
|
|
55
|
+
)))), # QApp: one past the deeper side
|
|
56
|
+
))))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@curry
|
|
60
|
+
def depth_at_most(bound: Builder, quoted: Builder) -> Builder:
|
|
61
|
+
"""``DEPTH quoted <= bound`` (a Church boolean): the shallow-enough certificate."""
|
|
62
|
+
return ap(_AT_MOST, app(DEPTH, quoted), bound)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# NODE_COUNT: the number of Var/Lam/App nodes in a quoted term (a Church numeral), a path-free
|
|
66
|
+
# catamorphism the interner shares per distinct sub-term -- same shape as DEPTH but summing (PLUS) the
|
|
67
|
+
# children instead of taking their MAX. It bounds how big a region may be de-tabled (host-compiled to
|
|
68
|
+
# call-by-need): a small region (<= a bound) loses cross-location tabling at most a constant factor, never
|
|
69
|
+
# exponentially, so the local-call-by-need optimization stays bounded and measurable.
|
|
70
|
+
_ZERO: Builder = lam(lambda s: lam(lambda z: z)) # church 0, the leaf base for the count
|
|
71
|
+
|
|
72
|
+
NODE_COUNT: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
|
|
73
|
+
quoted,
|
|
74
|
+
lam(lambda index: app(SUCC, _ZERO)), # QVar: one node
|
|
75
|
+
lam(lambda body: app(SUCC, app(self_recursion, body))), # QLam: one + body
|
|
76
|
+
lam(lambda function: lam(lambda argument: app(SUCC, ap(
|
|
77
|
+
PLUS, app(self_recursion, function), app(self_recursion, argument),
|
|
78
|
+
)))), # QApp: one + function + argument
|
|
79
|
+
))))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@curry
|
|
83
|
+
def node_count_at_most(bound: Builder, quoted: Builder) -> Builder:
|
|
84
|
+
"""``NODE_COUNT quoted <= bound`` (a Church boolean): the small-enough-to-de-table certificate."""
|
|
85
|
+
return ap(_AT_MOST, app(NODE_COUNT, quoted), bound)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@curry
|
|
89
|
+
def loose_bound_at_most(bound: Builder, quoted: Builder) -> Builder:
|
|
90
|
+
"""``LOOSE_BOUND quoted <= bound`` (a Church boolean): the few-free-variables certificate (an open
|
|
91
|
+
region with at most ``bound`` free de Bruijn variables, so its host island is an arity-``bound`` native)."""
|
|
92
|
+
return ap(_AT_MOST, app(LOOSE_BOUND, quoted), bound)
|
co_lambda/_ast.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""The lambda-term graph: a first-order de Bruijn tree.
|
|
2
|
+
|
|
3
|
+
Nodes are identity objects (``eq=False``): node identity is object identity, which the
|
|
4
|
+
paper uses as the visited set. The AST is a finite tree; the only source of genuine
|
|
5
|
+
sharing / cycles is the ``Mu`` recursion binder, which the interpreter resolves to the
|
|
6
|
+
same node object at reduction time.
|
|
7
|
+
|
|
8
|
+
``substitute`` is the load-bearing function for the copy-vs-share distinction: it copies
|
|
9
|
+
the redex-body spine into fresh nodes and inserts the argument by reference.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
from typing import Callable, cast, final
|
|
18
|
+
|
|
19
|
+
from fixpoints._core import fixpoint_cached_property, fixpoint_slotted
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ShapeBottom(Enum):
|
|
23
|
+
"""The bottom of the shape lattice: no weak-head shape (bottom, an unproductive cycle)."""
|
|
24
|
+
|
|
25
|
+
BOTTOM = auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
BOTTOM = ShapeBottom.BOTTOM
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@fixpoint_slotted
|
|
32
|
+
@dataclass(kw_only=True, eq=False)
|
|
33
|
+
class Node(ABC):
|
|
34
|
+
"""A lambda-term-graph node. Identity is object identity (``eq=False``)."""
|
|
35
|
+
|
|
36
|
+
__slots__ = ()
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"<{type(self).__name__} 0x{id(self):x}>"
|
|
40
|
+
|
|
41
|
+
@fixpoint_cached_property(bottom=lambda: 0)
|
|
42
|
+
def loose_bound(self) -> int:
|
|
43
|
+
"""One past the largest free de Bruijn index (``0`` iff the node is closed)."""
|
|
44
|
+
return _loose_bound(self)
|
|
45
|
+
|
|
46
|
+
@fixpoint_cached_property(
|
|
47
|
+
bottom=lambda: BOTTOM, merge=lambda left, right: _deep_merge(left, right)
|
|
48
|
+
)
|
|
49
|
+
def weak_head_normal_form(self) -> "Node | ShapeBottom":
|
|
50
|
+
"""The weak head normal form: the outermost constructor after weak head reduction, a least
|
|
51
|
+
fixpoint.
|
|
52
|
+
|
|
53
|
+
Single-valued (a deterministic calculus exposes one constructor), so not a set. The least
|
|
54
|
+
fixpoint of the weak-head-normalization recurrence, computed from ``BOTTOM`` upward by deep
|
|
55
|
+
merge in the approximation order (``fixpoints``): each round's freshly computed layer is joined
|
|
56
|
+
into the running approximation rather than overwriting it, so the iteration respects the order
|
|
57
|
+
and two incompatible non-``BOTTOM`` layers crash as a conflict. Because nodes are interned, a
|
|
58
|
+
node reached again during its own computation is caught by a pointer test. An unproductive cycle
|
|
59
|
+
(a re-entry with no constructor exposed, as in ``Omega`` or ``Y (lambda x. x)``) stabilizes at
|
|
60
|
+
``BOTTOM``.
|
|
61
|
+
"""
|
|
62
|
+
from co_lambda._shape import compute_weak_head_normal_form
|
|
63
|
+
|
|
64
|
+
return compute_weak_head_normal_form(self)
|
|
65
|
+
|
|
66
|
+
@fixpoint_cached_property(
|
|
67
|
+
bottom=lambda: BOTTOM, merge=lambda left, right: _deep_merge(left, right)
|
|
68
|
+
)
|
|
69
|
+
def head_normal_form(self) -> "Node | ShapeBottom":
|
|
70
|
+
"""The head normal form (the Boehm reading): the outermost constructor after head reduction,
|
|
71
|
+
a least fixpoint.
|
|
72
|
+
|
|
73
|
+
Identical to ``weak_head_normal_form`` except that a ``lambda`` whose body has no head normal
|
|
74
|
+
form is itself ``BOTTOM`` here (head reduction continues under the ``lambda``), so the readout
|
|
75
|
+
is the Boehm tree rather than Levy-Longo.
|
|
76
|
+
"""
|
|
77
|
+
from co_lambda._shape import compute_head_normal_form
|
|
78
|
+
|
|
79
|
+
return compute_head_normal_form(self)
|
|
80
|
+
|
|
81
|
+
def __call__(self, *arguments: "Node") -> "Node":
|
|
82
|
+
"""Curried application: ``function(a, b, c)`` builds ``make_app(make_app(make_app(function,
|
|
83
|
+
a), b), c)``. Sugar for the nested ``make_app`` chains that applying a node to several
|
|
84
|
+
arguments needs (the node-level counterpart of the HOAS ``app``)."""
|
|
85
|
+
result: Node = self
|
|
86
|
+
for argument in arguments:
|
|
87
|
+
result = make_app(result, argument)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@final
|
|
92
|
+
@dataclass(kw_only=True, eq=False)
|
|
93
|
+
class Var(Node):
|
|
94
|
+
__slots__ = ("index",)
|
|
95
|
+
index: int
|
|
96
|
+
"""de Bruijn index."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@final
|
|
100
|
+
@dataclass(kw_only=True, eq=False, repr=False)
|
|
101
|
+
class Lam(Node):
|
|
102
|
+
__slots__ = ("body",)
|
|
103
|
+
body: Node
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@final
|
|
107
|
+
@dataclass(kw_only=True, eq=False, repr=False)
|
|
108
|
+
class App(Node):
|
|
109
|
+
__slots__ = ("function", "argument")
|
|
110
|
+
function: Node
|
|
111
|
+
argument: Node
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@final
|
|
115
|
+
@dataclass(kw_only=True, eq=False, repr=False)
|
|
116
|
+
class Native(Node):
|
|
117
|
+
"""A foreign-function node: a compiled Python callable embedded in the term graph (the FFI).
|
|
118
|
+
|
|
119
|
+
``run`` takes ``arity`` argument ``Node``s and returns a result ``Node``; the Node graph is the
|
|
120
|
+
lingua franca, so a compiled island interoperates with the interpreter by consuming and producing
|
|
121
|
+
nodes. A closed island is ``arity == 0`` (``run()`` builds its result node). The interpreter
|
|
122
|
+
drives it: a saturated native fires ``run`` and continues normalizing the returned node, so a
|
|
123
|
+
compiled island sits inside an otherwise interpreted (folding) graph.
|
|
124
|
+
|
|
125
|
+
``collected`` holds the arguments gathered so far while the native is under-applied (empty for a
|
|
126
|
+
bare native): an ``App`` spine over a native is read back as a ``Native`` whose ``collected`` grows
|
|
127
|
+
one argument at a time until it reaches ``arity`` and fires. A bare native is closed, but a partial
|
|
128
|
+
application's ``loose_bound`` is that of its collected arguments.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
__slots__ = ("run", "arity", "collected")
|
|
132
|
+
run: "Callable[..., Node]"
|
|
133
|
+
arity: int
|
|
134
|
+
collected: "tuple[Node, ...]"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Hash-consing: structurally-equal nodes (with already-interned children) become the SAME
|
|
138
|
+
# object, so node identity is structural identity. This is what makes a cyclic structure a
|
|
139
|
+
# finite set of positions: an Omega contractum, or a repeated stream cell produced by a Y
|
|
140
|
+
# recursion, interns back to an existing node, so the least-fixpoint merge folds it. No
|
|
141
|
+
# recursion binder is needed; Y suffices, since the calculus stays pure.
|
|
142
|
+
#
|
|
143
|
+
# ``FOL_INTERNER_RETAIN`` selects the cache strategy. One knob, three regimes:
|
|
144
|
+
# "inf" (default): a plain strong dict that never frees -- node identity is permanent, the original
|
|
145
|
+
# no-GC interner. Full tabling speed; a large compilation (specializing the whole
|
|
146
|
+
# compiler) retains gigabytes, but that path is gated, so the common case keeps the
|
|
147
|
+
# fast, simple behaviour with no weakref overhead.
|
|
148
|
+
# "0" : a ``WeakValueDictionary`` with no retainer -- a key maps to a node iff it is still
|
|
149
|
+
# alive, so unreferenced reductions are reclaimed by refcounting (minimal memory).
|
|
150
|
+
# Correctness-safe (the weak map never holds two structurally-equal LIVE nodes, so
|
|
151
|
+
# cycle folding's pointer test never sees a duplicate), but a dropped node loses its
|
|
152
|
+
# cached normal form and is recomputed, so tabling speed is lost.
|
|
153
|
+
# N (an int) : the weak map plus a bounded strong LRU of the N most-recently-used nodes. The LRU
|
|
154
|
+
# keeps the hot, frequently-reused nodes (and their cached normal forms) alive so
|
|
155
|
+
# tabling speed is preserved within reuse distance N, while the cold tail is
|
|
156
|
+
# reclaimed -- memory bounded near max(live working set, N). The retainer only ADDS
|
|
157
|
+
# strong refs, so it can never create a duplicate.
|
|
158
|
+
# The LRU is a stdlib ``OrderedDict`` (C-backed move-to-end / popitem); cachetools / lru-dict were
|
|
159
|
+
# considered but add a dependency (and, for lru-dict, a C build under Nix) for no behavioural gain here.
|
|
160
|
+
import os as _os
|
|
161
|
+
import weakref as _weakref
|
|
162
|
+
from collections import OrderedDict as _OrderedDict
|
|
163
|
+
|
|
164
|
+
_INTERNER_RETAIN = _os.environ.get("FOL_INTERNER_RETAIN", "inf")
|
|
165
|
+
|
|
166
|
+
_canonical: "dict[tuple[object, ...], Node] | _weakref.WeakValueDictionary[tuple[object, ...], Node]"
|
|
167
|
+
_retainer: "_OrderedDict[tuple[object, ...], Node] | None"
|
|
168
|
+
if _INTERNER_RETAIN == "inf":
|
|
169
|
+
_canonical = {} # strong: the original no-GC interner
|
|
170
|
+
_retainer = None
|
|
171
|
+
_retain_max = 0
|
|
172
|
+
elif _INTERNER_RETAIN == "0":
|
|
173
|
+
_canonical = _weakref.WeakValueDictionary()
|
|
174
|
+
_retainer = None
|
|
175
|
+
_retain_max = 0
|
|
176
|
+
else:
|
|
177
|
+
_canonical = _weakref.WeakValueDictionary()
|
|
178
|
+
_retainer = _OrderedDict()
|
|
179
|
+
_retain_max = int(_INTERNER_RETAIN)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _retain(key: tuple[object, ...], node: Node) -> None:
|
|
183
|
+
"""Mark ``key -> node`` most-recently-used in the bounded LRU retainer (evicting the oldest if over
|
|
184
|
+
the bound). A canonical-map hit may name a node already evicted from the retainer while it stayed
|
|
185
|
+
alive elsewhere, so an absent key is re-inserted rather than moved (move-to-end would raise)."""
|
|
186
|
+
if _retainer is None:
|
|
187
|
+
return
|
|
188
|
+
if key in _retainer:
|
|
189
|
+
_retainer.move_to_end(key)
|
|
190
|
+
else:
|
|
191
|
+
_retainer[key] = node
|
|
192
|
+
if len(_retainer) > _retain_max:
|
|
193
|
+
_retainer.popitem(last=False)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _intern_node(key: tuple[object, ...], make: Callable[[], Node]) -> Node:
|
|
197
|
+
existing = _canonical.get(key)
|
|
198
|
+
if existing is not None:
|
|
199
|
+
_retain(key, existing)
|
|
200
|
+
return existing
|
|
201
|
+
node = make()
|
|
202
|
+
_canonical[key] = node
|
|
203
|
+
_retain(key, node)
|
|
204
|
+
return node
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def make_var(index: int) -> Var:
|
|
208
|
+
return cast(Var, _intern_node(("Var", index), lambda: Var(index=index)))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def make_lam(body: Node) -> Lam:
|
|
212
|
+
return cast(Lam, _intern_node(("Lam", id(body)), lambda: Lam(body=body)))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def make_app(function: Node, argument: Node) -> App:
|
|
216
|
+
return cast(
|
|
217
|
+
App,
|
|
218
|
+
_intern_node(
|
|
219
|
+
("App", id(function), id(argument)),
|
|
220
|
+
lambda: App(function=function, argument=argument),
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def make_native(
|
|
226
|
+
run: "Callable[..., Node]", arity: int, collected: "tuple[Node, ...]" = ()
|
|
227
|
+
) -> Native:
|
|
228
|
+
if arity < 0:
|
|
229
|
+
raise ValueError("native arity must be nonnegative")
|
|
230
|
+
return cast(
|
|
231
|
+
Native,
|
|
232
|
+
_intern_node(
|
|
233
|
+
("Native", id(run), arity, tuple(id(node) for node in collected)),
|
|
234
|
+
lambda: Native(run=run, arity=arity, collected=collected),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _deep_merge(left: "Node | ShapeBottom", right: "Node | ShapeBottom") -> "Node | ShapeBottom":
|
|
240
|
+
"""Join two approximations of a node's weak-head/Boehm layer in the approximation order.
|
|
241
|
+
|
|
242
|
+
``BOTTOM`` is least, so it joins to the other side. Two non-``BOTTOM`` layers with the same
|
|
243
|
+
outermost constructor join structurally (their successors merge); two different constructors (or
|
|
244
|
+
``Var`` indices, or native run/arity) have no upper bound, a conflict that crashes, because a
|
|
245
|
+
deterministic effect must not expose two incompatible layers at one node. Because nodes are
|
|
246
|
+
interned, equal layers share identity and the common case short-circuits without recursing.
|
|
247
|
+
"""
|
|
248
|
+
return _deep_merge_into(left, right, {})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _deep_merge_into(
|
|
252
|
+
left: "Node | ShapeBottom",
|
|
253
|
+
right: "Node | ShapeBottom",
|
|
254
|
+
in_progress: "dict[tuple[int, int], None]",
|
|
255
|
+
) -> "Node | ShapeBottom":
|
|
256
|
+
if right is BOTTOM:
|
|
257
|
+
return left
|
|
258
|
+
if left is BOTTOM:
|
|
259
|
+
return right
|
|
260
|
+
if left is right:
|
|
261
|
+
return left
|
|
262
|
+
pair = (id(left), id(right))
|
|
263
|
+
if pair in in_progress:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
"deep merge of two distinct rational layers would not terminate: a node exposed two "
|
|
266
|
+
"incompatible non-bottom layers across rounds"
|
|
267
|
+
)
|
|
268
|
+
in_progress[pair] = None
|
|
269
|
+
try:
|
|
270
|
+
match (left, right):
|
|
271
|
+
case (Var(index=left_index), Var(index=right_index)):
|
|
272
|
+
if left_index != right_index:
|
|
273
|
+
raise ValueError(f"deep merge conflict: Var {left_index} vs Var {right_index}")
|
|
274
|
+
return left
|
|
275
|
+
case (Lam(body=left_body), Lam(body=right_body)):
|
|
276
|
+
return make_lam(_deep_merge_into(left_body, right_body, in_progress))
|
|
277
|
+
case (
|
|
278
|
+
App(function=left_function, argument=left_argument),
|
|
279
|
+
App(function=right_function, argument=right_argument),
|
|
280
|
+
):
|
|
281
|
+
return make_app(
|
|
282
|
+
_deep_merge_into(left_function, right_function, in_progress),
|
|
283
|
+
_deep_merge_into(left_argument, right_argument, in_progress),
|
|
284
|
+
)
|
|
285
|
+
case (
|
|
286
|
+
Native(run=left_run, arity=left_arity, collected=left_collected),
|
|
287
|
+
Native(run=right_run, arity=right_arity, collected=right_collected),
|
|
288
|
+
):
|
|
289
|
+
if (
|
|
290
|
+
left_run is not right_run
|
|
291
|
+
or left_arity != right_arity
|
|
292
|
+
or len(left_collected) != len(right_collected)
|
|
293
|
+
):
|
|
294
|
+
raise ValueError("deep merge conflict: incompatible natives")
|
|
295
|
+
merged_collected = tuple(
|
|
296
|
+
_deep_merge_into(left_child, right_child, in_progress)
|
|
297
|
+
for left_child, right_child in zip(left_collected, right_collected)
|
|
298
|
+
)
|
|
299
|
+
return make_native(left_run, left_arity, merged_collected)
|
|
300
|
+
case _:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"deep merge conflict: {type(left).__name__} vs {type(right).__name__}"
|
|
303
|
+
)
|
|
304
|
+
finally:
|
|
305
|
+
del in_progress[pair]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _loose_bound(node: Node) -> int:
|
|
309
|
+
match node:
|
|
310
|
+
case Var(index=index):
|
|
311
|
+
return index + 1
|
|
312
|
+
case Lam(body=body):
|
|
313
|
+
return max(0, body.loose_bound - 1)
|
|
314
|
+
case App(function=function, argument=argument):
|
|
315
|
+
return max(function.loose_bound, argument.loose_bound)
|
|
316
|
+
case Native(collected=collected):
|
|
317
|
+
return max((argument.loose_bound for argument in collected), default=0)
|
|
318
|
+
case _:
|
|
319
|
+
raise TypeError(f"Unknown node {node!r}")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def shift(node: Node, *, cutoff: int, amount: int) -> Node:
|
|
323
|
+
"""Shift free de Bruijn indices ``>= cutoff`` by ``amount``."""
|
|
324
|
+
if node.loose_bound <= cutoff:
|
|
325
|
+
return node
|
|
326
|
+
match node:
|
|
327
|
+
case Var(index=index):
|
|
328
|
+
return make_var(index + amount)
|
|
329
|
+
case Lam(body=body):
|
|
330
|
+
return make_lam(shift(body, cutoff=cutoff + 1, amount=amount))
|
|
331
|
+
case App(function=function, argument=argument):
|
|
332
|
+
return make_app(
|
|
333
|
+
shift(function, cutoff=cutoff, amount=amount),
|
|
334
|
+
shift(argument, cutoff=cutoff, amount=amount),
|
|
335
|
+
)
|
|
336
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
337
|
+
return make_native(
|
|
338
|
+
run,
|
|
339
|
+
arity,
|
|
340
|
+
tuple(shift(argument, cutoff=cutoff, amount=amount) for argument in collected),
|
|
341
|
+
)
|
|
342
|
+
case _:
|
|
343
|
+
raise TypeError(f"Unknown node {node!r}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def substitute(node: Node, *, depth: int, argument: Node) -> Node:
|
|
347
|
+
"""Capture-avoiding de Bruijn substitution of ``argument`` for ``Var(depth)``."""
|
|
348
|
+
if node.loose_bound <= depth:
|
|
349
|
+
return node
|
|
350
|
+
match node:
|
|
351
|
+
case Var(index=index):
|
|
352
|
+
if index == depth:
|
|
353
|
+
return shift(argument, cutoff=0, amount=depth)
|
|
354
|
+
return make_var(index - 1)
|
|
355
|
+
case Lam(body=body):
|
|
356
|
+
return make_lam(substitute(body, depth=depth + 1, argument=argument))
|
|
357
|
+
case App(function=function, argument=app_argument):
|
|
358
|
+
return make_app(
|
|
359
|
+
substitute(function, depth=depth, argument=argument),
|
|
360
|
+
substitute(app_argument, depth=depth, argument=argument),
|
|
361
|
+
)
|
|
362
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
363
|
+
return make_native(
|
|
364
|
+
run,
|
|
365
|
+
arity,
|
|
366
|
+
tuple(
|
|
367
|
+
substitute(collected_argument, depth=depth, argument=argument)
|
|
368
|
+
for collected_argument in collected
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
case _:
|
|
372
|
+
raise TypeError(f"Unknown node {node!r}")
|
co_lambda/_binnat.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Binary naturals (BinNat): an LSB-first lambda-calculus encoding of the naturals, with arithmetic.
|
|
2
|
+
|
|
3
|
+
A BinNat is a Scott-encoded linked list of booleans (bits), least-significant bit first: its value is
|
|
4
|
+
``sum(bit_i << i)``. A bit is a Scott boolean (``TRUE`` = 1, ``FALSE`` = 0). Trailing zero bits are
|
|
5
|
+
harmless, so a value has many representations; the operations are correct on all of them. Unlike a
|
|
6
|
+
Church numeral, whose every operation is O(value) (it is unary), a BinNat is O(log value) in size, so
|
|
7
|
+
addition, comparison, and multiplication are polynomial in the number of digits.
|
|
8
|
+
|
|
9
|
+
This module is pure lambda calculus: every top-level binding is a ``Builder`` (a ``@curry``-decorated
|
|
10
|
+
``def`` IS a Builder, an object-level abstraction applied with ``app``). The Python-int encodings
|
|
11
|
+
(``int_to_binnat``, ``binnat_list``) and readouts (``binnat_to_int``, ``binnat_list_to_identifier``)
|
|
12
|
+
live in ``_codec``.
|
|
13
|
+
|
|
14
|
+
BinNat is also the type checker's type-variable id type: fresh ids get O(log) ``BIN_EQUAL`` comparison
|
|
15
|
+
during unification, where Church-id arithmetic would be O(id).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from co_lambda._dsl import Builder, app, curry, lam, lam_named
|
|
21
|
+
from co_lambda._prelude import AND, FALSE, OR, SCOTT_NIL, TRUE, Y
|
|
22
|
+
from co_lambda._sugar import ap, cons
|
|
23
|
+
|
|
24
|
+
# A Scott boolean selects between two branches (``bit then else``); a Scott list is eliminated by
|
|
25
|
+
# ``list on_cons on_nil``. The recursions thread a carry (addition), a borrow (subtraction), or a
|
|
26
|
+
# comparison verdict from the high bits down, with ``Y`` for the structural recursion over the digits.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@curry
|
|
30
|
+
def _not(bit: Builder) -> Builder:
|
|
31
|
+
return ap(bit, FALSE, TRUE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@curry
|
|
35
|
+
def _xor(left: Builder, right: Builder) -> Builder:
|
|
36
|
+
return ap(left, app(_not, right), right) # left ? not right : right
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@curry
|
|
40
|
+
def _majority(first: Builder, second: Builder, third: Builder) -> Builder:
|
|
41
|
+
return ap(
|
|
42
|
+
OR,
|
|
43
|
+
ap(AND, first, second),
|
|
44
|
+
ap(OR, ap(AND, first, third), ap(AND, second, third)),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@curry
|
|
49
|
+
def _bit_equal(left: Builder, right: Builder) -> Builder:
|
|
50
|
+
return ap(left, right, app(_not, right)) # left ? right : not right
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
BIN_ZERO: Builder = SCOTT_NIL
|
|
54
|
+
BIN_ONE: Builder = cons(TRUE, SCOTT_NIL)
|
|
55
|
+
|
|
56
|
+
# add carry a b: ripple-carry addition, both lists LSB-first, treating a missing digit as 0.
|
|
57
|
+
_ADD_CARRY: Builder = app(Y, lam_named("addc", lambda add: lam(lambda carry: lam(lambda a: lam(lambda b: ap(
|
|
58
|
+
a,
|
|
59
|
+
lam(lambda x: lam(lambda xs: ap(
|
|
60
|
+
b,
|
|
61
|
+
lam(lambda y: lam(lambda ys: cons(
|
|
62
|
+
ap(_xor, ap(_xor, x, y), carry),
|
|
63
|
+
ap(add, ap(_majority, x, y, carry), xs, ys),
|
|
64
|
+
))),
|
|
65
|
+
cons(ap(_xor, x, carry), ap(add, ap(AND, x, carry), xs, SCOTT_NIL)),
|
|
66
|
+
))),
|
|
67
|
+
ap(
|
|
68
|
+
b,
|
|
69
|
+
lam(lambda y: lam(lambda ys: cons(
|
|
70
|
+
ap(_xor, y, carry),
|
|
71
|
+
ap(add, ap(AND, y, carry), SCOTT_NIL, ys),
|
|
72
|
+
))),
|
|
73
|
+
ap(carry, BIN_ONE, SCOTT_NIL), # both empty: a final carry is the leading 1
|
|
74
|
+
),
|
|
75
|
+
))))))
|
|
76
|
+
|
|
77
|
+
BIN_ADD: Builder = lam(lambda a: lam(lambda b: ap(_ADD_CARRY, FALSE, a, b)))
|
|
78
|
+
BIN_SUCC: Builder = lam(lambda n: ap(BIN_ADD, n, BIN_ONE))
|
|
79
|
+
|
|
80
|
+
# pred n: truncated decrement (pred 0 = 0). bit 1 clears to 0; bit 0 borrows from the next digit.
|
|
81
|
+
BIN_PRED: Builder = app(Y, lam(lambda pred: lam(lambda n: ap(
|
|
82
|
+
n,
|
|
83
|
+
lam(lambda bit: lam(lambda rest: ap(
|
|
84
|
+
bit,
|
|
85
|
+
cons(FALSE, rest),
|
|
86
|
+
cons(TRUE, app(pred, rest)),
|
|
87
|
+
))),
|
|
88
|
+
SCOTT_NIL,
|
|
89
|
+
))))
|
|
90
|
+
|
|
91
|
+
# sub borrow a b: truncated subtraction (a - b is 0 when a < b). Borrow out is the majority of
|
|
92
|
+
# (not x), y, borrow; a exhausted means the rest underflows, truncated to 0.
|
|
93
|
+
_SUB_BORROW: Builder = app(Y, lam(lambda sub: lam(lambda borrow: lam(lambda a: lam(lambda b: ap(
|
|
94
|
+
a,
|
|
95
|
+
lam(lambda x: lam(lambda xs: ap(
|
|
96
|
+
b,
|
|
97
|
+
lam(lambda y: lam(lambda ys: cons(
|
|
98
|
+
ap(_xor, ap(_xor, x, y), borrow),
|
|
99
|
+
ap(sub, ap(_majority, app(_not, x), y, borrow), xs, ys),
|
|
100
|
+
))),
|
|
101
|
+
cons(ap(_xor, x, borrow), ap(sub, ap(AND, app(_not, x), borrow), xs, SCOTT_NIL)),
|
|
102
|
+
))),
|
|
103
|
+
SCOTT_NIL,
|
|
104
|
+
))))))
|
|
105
|
+
|
|
106
|
+
# is_zero n: every digit is 0 (or the list is empty).
|
|
107
|
+
BIN_IS_ZERO: Builder = app(Y, lam_named("iszero", lambda is_zero: lam(lambda n: ap(
|
|
108
|
+
n,
|
|
109
|
+
lam(lambda bit: lam(lambda rest: ap(bit, FALSE, app(is_zero, rest)))),
|
|
110
|
+
TRUE,
|
|
111
|
+
))))
|
|
112
|
+
|
|
113
|
+
# A comparison verdict is a three-way selector ``verdict less equal greater``.
|
|
114
|
+
_LESS: Builder = lam(lambda less: lam(lambda equal: lam(lambda greater: less)))
|
|
115
|
+
_EQUAL: Builder = lam(lambda less: lam(lambda equal: lam(lambda greater: equal)))
|
|
116
|
+
_GREATER: Builder = lam(lambda less: lam(lambda equal: lam(lambda greater: greater)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@curry
|
|
120
|
+
def _bit_compare(x: Builder, y: Builder) -> Builder:
|
|
121
|
+
# equal bits compare equal; otherwise x = 1 means greater (1 > 0), x = 0 means less (0 < 1).
|
|
122
|
+
return ap(ap(_bit_equal, x, y), _EQUAL, ap(x, _GREATER, _LESS))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# cmp a b: the verdict for a versus b. The high bits dominate, so recurse on the tails first; if they
|
|
126
|
+
# are equal the current bit decides, otherwise the tail verdict stands. A missing tail compares as 0.
|
|
127
|
+
BIN_CMP: Builder = app(Y, lam(lambda cmp: lam(lambda a: lam(lambda b: ap(
|
|
128
|
+
a,
|
|
129
|
+
lam(lambda x: lam(lambda xs: ap(
|
|
130
|
+
b,
|
|
131
|
+
lam(lambda y: lam(lambda ys: ap(
|
|
132
|
+
ap(cmp, xs, ys),
|
|
133
|
+
_LESS,
|
|
134
|
+
ap(_bit_compare, x, y),
|
|
135
|
+
_GREATER,
|
|
136
|
+
))),
|
|
137
|
+
ap(ap(BIN_IS_ZERO, cons(x, xs)), _EQUAL, _GREATER), # a vs 0
|
|
138
|
+
))),
|
|
139
|
+
ap(
|
|
140
|
+
b,
|
|
141
|
+
lam(lambda y: lam(lambda ys: ap(ap(BIN_IS_ZERO, cons(y, ys)), _EQUAL, _LESS))), # 0 vs b
|
|
142
|
+
_EQUAL, # both empty
|
|
143
|
+
),
|
|
144
|
+
)))))
|
|
145
|
+
|
|
146
|
+
BIN_LESS: Builder = lam(lambda a: lam(lambda b: ap(BIN_CMP, a, b, TRUE, FALSE, FALSE)))
|
|
147
|
+
BIN_EQUAL: Builder = lam(lambda a: lam(lambda b: ap(BIN_CMP, a, b, FALSE, TRUE, FALSE)))
|
|
148
|
+
BIN_MIN: Builder = lam(lambda a: lam(lambda b: ap(BIN_CMP, a, b, a, a, b)))
|
|
149
|
+
BIN_MAX: Builder = lam(lambda a: lam(lambda b: ap(BIN_CMP, a, b, b, a, a)))
|
|
150
|
+
|
|
151
|
+
# sub a b: truncated subtraction. The borrow subtraction is correct only when a >= b (it emits low
|
|
152
|
+
# digits before it could detect an underflow), so the verdict gates it: a <= b gives 0, a > b the
|
|
153
|
+
# borrow subtraction.
|
|
154
|
+
BIN_SUB: Builder = lam(lambda a: lam(lambda b: ap(
|
|
155
|
+
BIN_CMP, a, b,
|
|
156
|
+
BIN_ZERO,
|
|
157
|
+
BIN_ZERO,
|
|
158
|
+
ap(_SUB_BORROW, FALSE, a, b),
|
|
159
|
+
)))
|
|
160
|
+
|
|
161
|
+
# mul a b: shift-and-add. b = bit0 + 2 * rest, so a * b = (bit0 ? a : 0) + (2a) * rest.
|
|
162
|
+
BIN_MUL: Builder = app(Y, lam(lambda mul: lam(lambda a: lam(lambda b: ap(
|
|
163
|
+
b,
|
|
164
|
+
lam(lambda bit: lam(lambda rest: ap(
|
|
165
|
+
BIN_ADD,
|
|
166
|
+
ap(bit, a, SCOTT_NIL),
|
|
167
|
+
ap(mul, cons(FALSE, a), rest),
|
|
168
|
+
))),
|
|
169
|
+
SCOTT_NIL,
|
|
170
|
+
)))))
|