tablambda 0.6.0.post30.dev0__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.
tablambda/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """A pure lambda-calculus interpreter that applies tabling to weak-head reduction, folding self-referential terms into finite cyclic graphs."""
tablambda/_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 tablambda._dsl import Builder, app, curry, lam
23
+ from tablambda._prelude import IS_ZERO, PLUS, PRED, SUCC, Y
24
+ from tablambda._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)
tablambda/_ast.py ADDED
@@ -0,0 +1,289 @@
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 WeakHeadBottom(Enum):
23
+ """The bottom of the weak-head lattice: a term with no weak head normal form (an unproductive cycle)."""
24
+
25
+ BOTTOM = auto()
26
+
27
+
28
+ BOTTOM = WeakHeadBottom.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 | WeakHeadBottom":
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 tablambda._shape import compute_weak_head_normal_form
63
+
64
+ return compute_weak_head_normal_form(self)
65
+
66
+ def __call__(self, *arguments: "Node") -> "Node":
67
+ """Curried application: ``function(a, b, c)`` builds ``make_app(make_app(make_app(function,
68
+ a), b), c)``. Sugar for the nested ``make_app`` chains that applying a node to several
69
+ arguments needs (the node-level counterpart of the HOAS ``app``)."""
70
+ result: Node = self
71
+ for argument in arguments:
72
+ result = make_app(result, argument)
73
+ return result
74
+
75
+
76
+ @final
77
+ @dataclass(kw_only=True, eq=False)
78
+ class Var(Node):
79
+ __slots__ = ("index",)
80
+ index: int
81
+ """de Bruijn index."""
82
+
83
+
84
+ @final
85
+ @dataclass(kw_only=True, eq=False, repr=False)
86
+ class Lam(Node):
87
+ __slots__ = ("body",)
88
+ body: Node
89
+
90
+
91
+ @final
92
+ @dataclass(kw_only=True, eq=False, repr=False)
93
+ class App(Node):
94
+ __slots__ = ("function", "argument")
95
+ function: Node
96
+ argument: Node
97
+
98
+
99
+ # Hash-consing: structurally-equal nodes (with already-interned children) become the SAME
100
+ # object, so node identity is structural identity. This is what makes a cyclic structure a
101
+ # finite set of positions: an Omega contractum, or a repeated stream cell produced by a Y
102
+ # recursion, interns back to an existing node, so the least-fixpoint merge folds it. No
103
+ # recursion binder is needed; Y suffices, since the calculus stays pure.
104
+ #
105
+ # ``FOL_INTERNER_RETAIN`` selects the cache strategy. One knob, three regimes:
106
+ # "inf" (default): a plain strong dict that never frees -- node identity is permanent, the original
107
+ # no-GC interner. Full tabling speed; a large compilation (specializing the whole
108
+ # compiler) retains gigabytes, but that path is gated, so the common case keeps the
109
+ # fast, simple behaviour with no weakref overhead.
110
+ # "0" : a ``WeakValueDictionary`` with no retainer -- a key maps to a node iff it is still
111
+ # alive, so unreferenced reductions are reclaimed by refcounting (minimal memory).
112
+ # Correctness-safe (the weak map never holds two structurally-equal LIVE nodes, so
113
+ # cycle folding's pointer test never sees a duplicate), but a dropped node loses its
114
+ # cached normal form and is recomputed, so tabling speed is lost.
115
+ # N (an int) : the weak map plus a bounded strong LRU of the N most-recently-used nodes. The LRU
116
+ # keeps the hot, frequently-reused nodes (and their cached normal forms) alive so
117
+ # tabling speed is preserved within reuse distance N, while the cold tail is
118
+ # reclaimed -- memory bounded near max(live working set, N). The retainer only ADDS
119
+ # strong refs, so it can never create a duplicate.
120
+ # The LRU is a stdlib ``OrderedDict`` (C-backed move-to-end / popitem); cachetools / lru-dict were
121
+ # considered but add a dependency (and, for lru-dict, a C build under Nix) for no behavioural gain here.
122
+ import os as _os
123
+ import weakref as _weakref
124
+ from collections import OrderedDict as _OrderedDict
125
+
126
+ _INTERNER_RETAIN = _os.environ.get("FOL_INTERNER_RETAIN", "inf")
127
+
128
+ _canonical: "dict[tuple[object, ...], Node] | _weakref.WeakValueDictionary[tuple[object, ...], Node]"
129
+ _retainer: "_OrderedDict[tuple[object, ...], Node] | None"
130
+ if _INTERNER_RETAIN == "inf":
131
+ _canonical = {} # strong: the original no-GC interner
132
+ _retainer = None
133
+ _retain_max = 0
134
+ elif _INTERNER_RETAIN == "0":
135
+ _canonical = _weakref.WeakValueDictionary()
136
+ _retainer = None
137
+ _retain_max = 0
138
+ else:
139
+ _canonical = _weakref.WeakValueDictionary()
140
+ _retainer = _OrderedDict()
141
+ _retain_max = int(_INTERNER_RETAIN)
142
+
143
+
144
+ def _retain(key: tuple[object, ...], node: Node) -> None:
145
+ """Mark ``key -> node`` most-recently-used in the bounded LRU retainer (evicting the oldest if over
146
+ the bound). A canonical-map hit may name a node already evicted from the retainer while it stayed
147
+ alive elsewhere, so an absent key is re-inserted rather than moved (move-to-end would raise)."""
148
+ if _retainer is None:
149
+ return
150
+ if key in _retainer:
151
+ _retainer.move_to_end(key)
152
+ else:
153
+ _retainer[key] = node
154
+ if len(_retainer) > _retain_max:
155
+ _retainer.popitem(last=False)
156
+
157
+
158
+ def _intern_node(key: tuple[object, ...], make: Callable[[], Node]) -> Node:
159
+ existing = _canonical.get(key)
160
+ if existing is not None:
161
+ _retain(key, existing)
162
+ return existing
163
+ node = make()
164
+ _canonical[key] = node
165
+ _retain(key, node)
166
+ return node
167
+
168
+
169
+ def make_var(index: int) -> Var:
170
+ return cast(Var, _intern_node(("Var", index), lambda: Var(index=index)))
171
+
172
+
173
+ def make_lam(body: Node) -> Lam:
174
+ return cast(Lam, _intern_node(("Lam", id(body)), lambda: Lam(body=body)))
175
+
176
+
177
+ def make_app(function: Node, argument: Node) -> App:
178
+ return cast(
179
+ App,
180
+ _intern_node(
181
+ ("App", id(function), id(argument)),
182
+ lambda: App(function=function, argument=argument),
183
+ ),
184
+ )
185
+
186
+
187
+ def _deep_merge(left: "Node | WeakHeadBottom", right: "Node | WeakHeadBottom") -> "Node | WeakHeadBottom":
188
+ """Join two approximations of a node's weak-head/Boehm layer in the approximation order.
189
+
190
+ ``BOTTOM`` is least, so it joins to the other side. Two non-``BOTTOM`` layers with the same
191
+ outermost constructor join structurally (their successors merge); two different constructors (or
192
+ ``Var`` indices) have no upper bound, a conflict that crashes, because a deterministic effect must
193
+ not expose two incompatible layers at one node. Two distinct compiled values (``Closure``/``Thunk``,
194
+ interned in their own pool) fall through to the conflict arm. Because nodes are interned, equal
195
+ layers share identity and the common case short-circuits without recursing.
196
+ """
197
+ return _deep_merge_into(left, right, {})
198
+
199
+
200
+ def _deep_merge_into(
201
+ left: "Node | WeakHeadBottom",
202
+ right: "Node | WeakHeadBottom",
203
+ in_progress: "dict[tuple[int, int], None]",
204
+ ) -> "Node | WeakHeadBottom":
205
+ if right is BOTTOM:
206
+ return left
207
+ if left is BOTTOM:
208
+ return right
209
+ if left is right:
210
+ return left
211
+ pair = (id(left), id(right))
212
+ if pair in in_progress:
213
+ raise ValueError(
214
+ "deep merge of two distinct rational layers would not terminate: a node exposed two "
215
+ "incompatible non-bottom layers across rounds"
216
+ )
217
+ in_progress[pair] = None
218
+ try:
219
+ match (left, right):
220
+ case (Var(index=left_index), Var(index=right_index)):
221
+ if left_index != right_index:
222
+ raise ValueError(f"deep merge conflict: Var {left_index} vs Var {right_index}")
223
+ return left
224
+ case (Lam(body=left_body), Lam(body=right_body)):
225
+ return make_lam(_deep_merge_into(left_body, right_body, in_progress))
226
+ case (
227
+ App(function=left_function, argument=left_argument),
228
+ App(function=right_function, argument=right_argument),
229
+ ):
230
+ return make_app(
231
+ _deep_merge_into(left_function, right_function, in_progress),
232
+ _deep_merge_into(left_argument, right_argument, in_progress),
233
+ )
234
+ case _:
235
+ raise ValueError(
236
+ f"deep merge conflict: {type(left).__name__} vs {type(right).__name__}"
237
+ )
238
+ finally:
239
+ del in_progress[pair]
240
+
241
+
242
+ def _loose_bound(node: Node) -> int:
243
+ match node:
244
+ case Var(index=index):
245
+ return index + 1
246
+ case Lam(body=body):
247
+ return max(0, body.loose_bound - 1)
248
+ case App(function=function, argument=argument):
249
+ return max(function.loose_bound, argument.loose_bound)
250
+ case _:
251
+ raise TypeError(f"Unknown node {node!r}")
252
+
253
+
254
+ def shift(node: Node, *, cutoff: int, amount: int) -> Node:
255
+ """Shift free de Bruijn indices ``>= cutoff`` by ``amount``."""
256
+ if node.loose_bound <= cutoff:
257
+ return node
258
+ match node:
259
+ case Var(index=index):
260
+ return make_var(index + amount)
261
+ case Lam(body=body):
262
+ return make_lam(shift(body, cutoff=cutoff + 1, amount=amount))
263
+ case App(function=function, argument=argument):
264
+ return make_app(
265
+ shift(function, cutoff=cutoff, amount=amount),
266
+ shift(argument, cutoff=cutoff, amount=amount),
267
+ )
268
+ case _:
269
+ raise TypeError(f"Unknown node {node!r}")
270
+
271
+
272
+ def substitute(node: Node, *, depth: int, argument: Node) -> Node:
273
+ """Capture-avoiding de Bruijn substitution of ``argument`` for ``Var(depth)``."""
274
+ if node.loose_bound <= depth:
275
+ return node
276
+ match node:
277
+ case Var(index=index):
278
+ if index == depth:
279
+ return shift(argument, cutoff=0, amount=depth)
280
+ return make_var(index - 1)
281
+ case Lam(body=body):
282
+ return make_lam(substitute(body, depth=depth + 1, argument=argument))
283
+ case App(function=function, argument=app_argument):
284
+ return make_app(
285
+ substitute(function, depth=depth, argument=argument),
286
+ substitute(app_argument, depth=depth, argument=argument),
287
+ )
288
+ case _:
289
+ raise TypeError(f"Unknown node {node!r}")
tablambda/_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 tablambda._dsl import Builder, app, curry, lam, lam_named
21
+ from tablambda._prelude import AND, FALSE, OR, SCOTT_NIL, TRUE, Y
22
+ from tablambda._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
+ )))))