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 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
+ )))))