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/_sugar.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared HOAS transcription sugar: fixed-shape notation for writing lambda terms.
|
|
2
|
+
|
|
3
|
+
This module is one of the four strictly separated kinds (codec / sugar / runtime / pure-lambda
|
|
4
|
+
compiler source). Everything here is a one-to-one notational transcription: parameters are Builders
|
|
5
|
+
(or Python callables standing for object-language binders), the produced shape is literal at the
|
|
6
|
+
call site, and nothing is computed from data. The pure-lambda source modules import their writing
|
|
7
|
+
notation from here (and from ``_dsl``/``_pybuild``), so no Python helper definitions live next to
|
|
8
|
+
the lambda terms themselves.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from co_lambda._dsl import Builder, app, lam
|
|
14
|
+
from co_lambda._prelude import FALSE, MAP, SCOTT_CONS, SCOTT_NIL, TRUE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ap(function: Builder, *arguments: Builder) -> Builder:
|
|
18
|
+
"""Left-folded application: ``ap(f, x, y, z)`` is ``((f x) y) z``. A thin alias for calling the
|
|
19
|
+
builder directly (``function(*arguments)``), kept for the existing call sites."""
|
|
20
|
+
return function(*arguments)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def let(value: Builder, body) -> Builder:
|
|
24
|
+
"""Bind ``value`` for ``body`` (a Python ``lambda`` over the bound Builder)."""
|
|
25
|
+
return app(lam(body), value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def split_pair(pair_value: Builder, body) -> Builder:
|
|
29
|
+
"""Destructure a continuation pair (``pair k = k first second``) for ``body`` (a Python
|
|
30
|
+
``lambda`` over the two bound Builders)."""
|
|
31
|
+
return app(pair_value, lam(lambda first: lam(lambda second: body(first, second))))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pair(first: Builder, second: Builder) -> Builder:
|
|
35
|
+
"""The continuation pair: ``lambda k. k first second``."""
|
|
36
|
+
return lam(lambda consume: ap(consume, first, second))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def split_triple(triple_value: Builder, body) -> Builder:
|
|
40
|
+
"""Destructure a right-nested triple ``pair(a, pair(b, c))`` for ``body(a, b, c)``."""
|
|
41
|
+
return split_pair(triple_value, lambda first, rest: split_pair(
|
|
42
|
+
rest, lambda second, third: body(first, second, third),
|
|
43
|
+
))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def split_quad(quad_value: Builder, body) -> Builder:
|
|
47
|
+
"""Destructure a right-nested quadruple ``pair(a, pair(b, pair(c, d)))`` for ``body(a, b, c, d)``."""
|
|
48
|
+
return split_pair(quad_value, lambda first, rest: split_triple(
|
|
49
|
+
rest, lambda second, third, fourth: body(first, second, third, fourth),
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pair_first(pair_value: Builder) -> Builder:
|
|
54
|
+
return app(pair_value, TRUE)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pair_second(pair_value: Builder) -> Builder:
|
|
58
|
+
return app(pair_value, FALSE)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cons(head: Builder, tail: Builder) -> Builder:
|
|
62
|
+
return ap(SCOTT_CONS, head, tail)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def one(element: Builder) -> Builder:
|
|
66
|
+
return cons(element, SCOTT_NIL)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def two(first: Builder, second: Builder) -> Builder:
|
|
70
|
+
return cons(first, cons(second, SCOTT_NIL))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def map_list(function: Builder, source: Builder) -> Builder:
|
|
74
|
+
return ap(MAP, function, source)
|
co_lambda/_typecheck.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Simple-typability as a lambda term: algorithm-W (STLC) written in the pure calculus.
|
|
2
|
+
|
|
3
|
+
This is the soundness certificate the specializer needs. ``TYPABLE`` consumes a quoted term (the
|
|
4
|
+
``QVar``/``QLam``/``QApp`` Scott value ``quote`` produces) and returns a Church boolean: whether the
|
|
5
|
+
term is simply typable, which is a sound certificate of strong normalization (so the strict
|
|
6
|
+
call-by-value runtime is safe). The Python ``_specialize._Inference`` is the specification this
|
|
7
|
+
ports and the test oracle it is checked against; it is not on the compile path.
|
|
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 (``is_typable_lambda``, ``typable_bu_lambda``)
|
|
11
|
+
live at the boundary (``_specialize``).
|
|
12
|
+
|
|
13
|
+
The encoding is monomorphic algorithm-W (one fresh monotype per binder, no generalization), threading
|
|
14
|
+
a state of ``(next-fresh-id, substitution, failed-flag)`` purely:
|
|
15
|
+
|
|
16
|
+
* Types are a two-constructor Scott value: ``TVAR id`` (``id`` a BinNat, so allocating and comparing
|
|
17
|
+
type variables is O(log id)) and ``TARROW l r``.
|
|
18
|
+
* The substitution is a function ``id -> Option Type`` (the empty map is ``lambda id. NONE``; extension
|
|
19
|
+
shadows by id-equality), so ``resolve`` follows the chain and ``occurs`` walks the resolved type.
|
|
20
|
+
* ``unify`` threads ``(substitution, failed)`` and sets ``failed`` when the occurs check fires, which is
|
|
21
|
+
exactly why the self-application ``x x`` (constraint ``alpha = alpha -> beta``) is untypable.
|
|
22
|
+
* ``infer`` threads the whole state and returns ``(state, type)``; a binder extends the typing context
|
|
23
|
+
(a Scott list indexed by de Bruijn index) with a fresh monotype. It short-circuits once failed, so an
|
|
24
|
+
untypable term is rejected as soon as the first occurs check fires.
|
|
25
|
+
|
|
26
|
+
Termination: ``infer`` recurses structurally on the term; the occurs check keeps the substitution
|
|
27
|
+
acyclic, so ``resolve``/``occurs``/``unify`` recurse on finite type trees. The verdict is therefore a
|
|
28
|
+
normal-form Church boolean.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from co_lambda._binnat import BIN_ADD, BIN_EQUAL, BIN_SUCC, BIN_ZERO
|
|
34
|
+
from co_lambda._dsl import Builder, app, lam
|
|
35
|
+
from co_lambda._prelude import FALSE, IS_ZERO, OR, PRED, TRUE, Y
|
|
36
|
+
from co_lambda._sugar import ap, let, pair, split_pair, split_quad, split_triple
|
|
37
|
+
|
|
38
|
+
# --- booleans -------------------------------------------------------------------------------------
|
|
39
|
+
_NOT: Builder = lam(lambda boolean: ap(boolean, FALSE, TRUE))
|
|
40
|
+
|
|
41
|
+
# Option: SOME carries a value, NONE is empty; consumed as ``option some_handler none_value``.
|
|
42
|
+
_SOME: Builder = lam(lambda value: lam(lambda some_handler: lam(lambda none_value: app(some_handler, value))))
|
|
43
|
+
_NONE: Builder = lam(lambda some_handler: lam(lambda none_value: none_value))
|
|
44
|
+
|
|
45
|
+
# Scott list: consumed as ``list nil_value cons_handler``.
|
|
46
|
+
_NIL: Builder = lam(lambda nil_value: lam(lambda cons_handler: nil_value))
|
|
47
|
+
_CONS: Builder = lam(lambda head: lam(lambda tail: lam(lambda nil_value: lam(lambda cons_handler: ap(cons_handler, head, tail)))))
|
|
48
|
+
|
|
49
|
+
# --- types: TVAR id | TARROW left right (consumed as ``type var_handler arrow_handler``) ----------
|
|
50
|
+
_TVAR: Builder = lam(lambda identifier: lam(lambda var_handler: lam(lambda arrow_handler: app(var_handler, identifier))))
|
|
51
|
+
_TARROW: Builder = lam(lambda left: lam(lambda right: lam(lambda var_handler: lam(lambda arrow_handler: ap(arrow_handler, left, right)))))
|
|
52
|
+
|
|
53
|
+
# --- substitution: a function id -> Option Type --------------------------------------------------
|
|
54
|
+
_EMPTY_SUBST: Builder = lam(lambda identifier: _NONE)
|
|
55
|
+
# extend subst id type = a new map shadowing ``id`` with ``SOME type``.
|
|
56
|
+
_EXTEND: Builder = lam(lambda subst: lam(lambda identifier: lam(lambda bound: lam(lambda lookup: ap(
|
|
57
|
+
ap(BIN_EQUAL, lookup, identifier),
|
|
58
|
+
app(_SOME, bound),
|
|
59
|
+
app(subst, lookup),
|
|
60
|
+
)))))
|
|
61
|
+
|
|
62
|
+
# resolve subst type: follow the substitution chain to the representative type.
|
|
63
|
+
_RESOLVE: Builder = app(Y, lam(lambda self_recursion: lam(lambda subst: lam(lambda type_: ap(
|
|
64
|
+
type_,
|
|
65
|
+
lam(lambda identifier: ap(
|
|
66
|
+
app(subst, identifier),
|
|
67
|
+
lam(lambda found: ap(self_recursion, subst, found)),
|
|
68
|
+
app(_TVAR, identifier),
|
|
69
|
+
)),
|
|
70
|
+
lam(lambda left: lam(lambda right: ap(_TARROW, left, right))),
|
|
71
|
+
)))))
|
|
72
|
+
|
|
73
|
+
# occurs subst id type: whether ``id`` occurs in the resolved ``type`` (the occurs check).
|
|
74
|
+
_OCCURS: Builder = app(Y, lam(lambda self_recursion: lam(lambda subst: lam(lambda identifier: lam(lambda type_: let(
|
|
75
|
+
ap(_RESOLVE, subst, type_),
|
|
76
|
+
lambda resolved: ap(
|
|
77
|
+
resolved,
|
|
78
|
+
lam(lambda other: ap(BIN_EQUAL, other, identifier)),
|
|
79
|
+
lam(lambda left: lam(lambda right: ap(
|
|
80
|
+
OR,
|
|
81
|
+
ap(self_recursion, subst, identifier, left),
|
|
82
|
+
ap(self_recursion, subst, identifier, right),
|
|
83
|
+
))),
|
|
84
|
+
),
|
|
85
|
+
))))))
|
|
86
|
+
|
|
87
|
+
# bind subst id type: if the occurs check fires, fail; else extend the substitution. The result is a
|
|
88
|
+
# pair (substitution, failed).
|
|
89
|
+
_BIND: Builder = lam(lambda subst: lam(lambda identifier: lam(lambda bound: ap(
|
|
90
|
+
ap(_OCCURS, subst, identifier, bound),
|
|
91
|
+
pair(subst, TRUE),
|
|
92
|
+
pair(ap(_EXTEND, subst, identifier, bound), FALSE),
|
|
93
|
+
))))
|
|
94
|
+
|
|
95
|
+
# unify state a b, with state = (substitution, failed): unify the two types, threading the state.
|
|
96
|
+
# Short-circuits when already failed; otherwise resolves both sides and matches the four shape cases:
|
|
97
|
+
# var/var (equal: nothing; else bind), var/arrow and arrow/var (bind after occurs check), arrow/arrow
|
|
98
|
+
# (unify the components left to right).
|
|
99
|
+
_UNIFY: Builder = app(Y, lam(lambda self_recursion: lam(lambda state: lam(lambda left_type: lam(lambda right_type: split_pair(
|
|
100
|
+
state,
|
|
101
|
+
lambda subst, failed: ap(
|
|
102
|
+
failed,
|
|
103
|
+
state,
|
|
104
|
+
let(ap(_RESOLVE, subst, left_type), lambda left: let(ap(_RESOLVE, subst, right_type), lambda right: ap(
|
|
105
|
+
left,
|
|
106
|
+
lam(lambda left_id: ap(
|
|
107
|
+
right,
|
|
108
|
+
lam(lambda right_id: ap(ap(BIN_EQUAL, left_id, right_id), state, ap(_BIND, subst, left_id, right))),
|
|
109
|
+
lam(lambda right_left: lam(lambda right_right: ap(_BIND, subst, left_id, right))),
|
|
110
|
+
)),
|
|
111
|
+
lam(lambda left_left: lam(lambda left_right: ap(
|
|
112
|
+
right,
|
|
113
|
+
lam(lambda right_id: ap(_BIND, subst, right_id, left)),
|
|
114
|
+
lam(lambda right_left: lam(lambda right_right: ap(
|
|
115
|
+
self_recursion,
|
|
116
|
+
ap(self_recursion, state, left_left, right_left),
|
|
117
|
+
left_right,
|
|
118
|
+
right_right,
|
|
119
|
+
))),
|
|
120
|
+
))),
|
|
121
|
+
))),
|
|
122
|
+
),
|
|
123
|
+
))))))
|
|
124
|
+
|
|
125
|
+
# --- inference state: (next-fresh-id, (substitution, failed)) ------------------------------------
|
|
126
|
+
# The fresh-id counter is a BinNat, so allocating and comparing type variables is O(log id).
|
|
127
|
+
_INITIAL_STATE: Builder = pair(BIN_ZERO, pair(_EMPTY_SUBST, FALSE))
|
|
128
|
+
|
|
129
|
+
# fresh state = ((next+1, subst, failed), TVAR next): the new state and the fresh type variable.
|
|
130
|
+
_FRESH: Builder = lam(lambda state: split_triple(
|
|
131
|
+
state,
|
|
132
|
+
lambda next_id, subst, failed: pair(
|
|
133
|
+
pair(app(BIN_SUCC, next_id), pair(subst, failed)),
|
|
134
|
+
app(_TVAR, next_id),
|
|
135
|
+
),
|
|
136
|
+
))
|
|
137
|
+
|
|
138
|
+
# unify_state state a b: apply unify to the (substitution, failed) part of the inference state.
|
|
139
|
+
_UNIFY_STATE: Builder = lam(lambda state: lam(lambda left_type: lam(lambda right_type: split_triple(
|
|
140
|
+
state,
|
|
141
|
+
lambda next_id, subst, failed: split_pair(
|
|
142
|
+
ap(_UNIFY, pair(subst, failed), left_type, right_type),
|
|
143
|
+
lambda new_subst, new_failed: pair(next_id, pair(new_subst, new_failed)),
|
|
144
|
+
),
|
|
145
|
+
))))
|
|
146
|
+
|
|
147
|
+
# lookup context index: the type at de Bruijn ``index`` in the context, as an Option.
|
|
148
|
+
_LOOKUP: Builder = app(Y, lam(lambda self_recursion: lam(lambda context: lam(lambda index: ap(
|
|
149
|
+
context,
|
|
150
|
+
_NONE,
|
|
151
|
+
lam(lambda head: lam(lambda tail: ap(
|
|
152
|
+
app(IS_ZERO, index),
|
|
153
|
+
app(_SOME, head),
|
|
154
|
+
ap(self_recursion, tail, app(PRED, index)),
|
|
155
|
+
))),
|
|
156
|
+
)))))
|
|
157
|
+
|
|
158
|
+
# infer state context node: infer the node's type, threading the state; returns (state, type).
|
|
159
|
+
# Once the state has failed (an occurs check fired), inference short-circuits: it returns a dummy type
|
|
160
|
+
# without recursing, so an untypable term (the compiler's Y, factorial, ...) is rejected as soon as the
|
|
161
|
+
# first self-application fails rather than building the whole constraint tree. This mirrors the Python
|
|
162
|
+
# ``_Inference.infer`` early return and is what keeps the certificate fast on the large untypable terms.
|
|
163
|
+
_INFER: Builder = app(Y, lam(lambda self_recursion: lam(lambda state: lam(lambda context: lam(lambda node: split_triple(
|
|
164
|
+
state,
|
|
165
|
+
lambda next_id, subst, failed: ap(
|
|
166
|
+
failed,
|
|
167
|
+
pair(state, app(_TVAR, next_id)), # already failed: short-circuit with a dummy type
|
|
168
|
+
ap(
|
|
169
|
+
node,
|
|
170
|
+
lam(lambda index: ap( # QVar index
|
|
171
|
+
ap(_LOOKUP, context, index),
|
|
172
|
+
lam(lambda found: pair(state, found)), # bound: its context type
|
|
173
|
+
app(_FRESH, state), # free: a fresh type variable
|
|
174
|
+
)),
|
|
175
|
+
lam(lambda body: split_pair( # QLam body
|
|
176
|
+
app(_FRESH, state),
|
|
177
|
+
lambda state_after_fresh, parameter: split_pair(
|
|
178
|
+
ap(self_recursion, state_after_fresh, ap(_CONS, parameter, context), body),
|
|
179
|
+
lambda state_after_body, result: pair(state_after_body, ap(_TARROW, parameter, result)),
|
|
180
|
+
),
|
|
181
|
+
)),
|
|
182
|
+
lam(lambda function: lam(lambda argument: split_pair( # QApp function argument
|
|
183
|
+
ap(self_recursion, state, context, function),
|
|
184
|
+
lambda state_after_function, function_type: split_pair(
|
|
185
|
+
ap(self_recursion, state_after_function, context, argument),
|
|
186
|
+
lambda state_after_argument, argument_type: split_pair(
|
|
187
|
+
app(_FRESH, state_after_argument),
|
|
188
|
+
lambda state_after_fresh, result: pair(
|
|
189
|
+
ap(_UNIFY_STATE, state_after_fresh, function_type, ap(_TARROW, argument_type, result)),
|
|
190
|
+
result,
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
))),
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
))))))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# TYPABLE quoted = run inference from the initial state on the empty context, read the failed flag.
|
|
201
|
+
# A closed term is simply typable iff inference does not fail (no occurs-check violation).
|
|
202
|
+
TYPABLE: Builder = lam(lambda quoted: split_pair(
|
|
203
|
+
ap(_INFER, _INITIAL_STATE, _NIL, quoted),
|
|
204
|
+
lambda final_state, _type: split_triple(
|
|
205
|
+
final_state,
|
|
206
|
+
lambda next_id, subst, failed: app(_NOT, failed),
|
|
207
|
+
),
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# === Bottom-up principal typing: one path-free fold the interpreter tables per distinct sub-term ===
|
|
212
|
+
# ``TYPABLE`` above threads a fresh-id/substitution state, so the interpreter cannot share its
|
|
213
|
+
# per-sub-term inference (the state differs at every position) and the substitution is an O(chain)
|
|
214
|
+
# function. ``PRINCIPAL`` is PATH-FREE -- it takes only the node -- so ``app(PRINCIPAL, sub)`` is the
|
|
215
|
+
# same interned node wherever ``sub`` occurs and the interpreter tables it ONCE per distinct sub-term
|
|
216
|
+
# (the compiler's combinators reuse sub-combinators heavily, so this is the win). Each ``App`` unifies
|
|
217
|
+
# in a FRESH LOCAL substitution, resolved and discarded, so there is no global chain. Type-variable
|
|
218
|
+
# ids are BinNats (O(log) equality); de Bruijn indices stay Church (small, bounded by depth) and are
|
|
219
|
+
# converted to BinNat only where they seed a type-variable id.
|
|
220
|
+
#
|
|
221
|
+
# A result is ``(next-fresh-id, context, type, failed)``: ``context`` is a Scott list of types indexed
|
|
222
|
+
# by de Bruijn index (a fresh type per binder so siblings constrain them independently); ``type`` is
|
|
223
|
+
# the sub-term's type with the local substitution applied; ``next-fresh-id`` (a BinNat) bounds the
|
|
224
|
+
# type-variable ids used, so a sibling is renamed apart by adding it; ``failed`` is the occurs-check
|
|
225
|
+
# verdict for the whole sub-tree.
|
|
226
|
+
|
|
227
|
+
# Convert a Church numeral (a de Bruijn index from ``quote``) to a BinNat type-variable id.
|
|
228
|
+
_CHURCH_TO_BINNAT: Builder = lam(lambda church_value: ap(church_value, BIN_SUCC, BIN_ZERO))
|
|
229
|
+
|
|
230
|
+
# build_vars count = [TVAR 0, TVAR 1, ..., TVAR (count-1)]: the fresh context for a variable at de
|
|
231
|
+
# Bruijn index count-1, one distinct fresh type per enclosing binder (BinNat ids).
|
|
232
|
+
_BUILD_VARS_GO: Builder = app(Y, lam(lambda self_recursion: lam(lambda current: lam(lambda count: ap(
|
|
233
|
+
ap(BIN_EQUAL, current, count),
|
|
234
|
+
_NIL,
|
|
235
|
+
ap(_CONS, app(_TVAR, current), ap(self_recursion, app(BIN_SUCC, current), count)),
|
|
236
|
+
)))))
|
|
237
|
+
|
|
238
|
+
_BUILD_VARS: Builder = lam(lambda count: ap(_BUILD_VARS_GO, BIN_ZERO, count))
|
|
239
|
+
|
|
240
|
+
# shift_type offset type: add ``offset`` to every type-variable id (rename a whole type apart).
|
|
241
|
+
_SHIFT_TYPE: Builder = app(Y, lam(lambda self_recursion: lam(lambda offset: lam(lambda type_: ap(
|
|
242
|
+
type_,
|
|
243
|
+
lam(lambda identifier: app(_TVAR, ap(BIN_ADD, offset, identifier))),
|
|
244
|
+
lam(lambda left: lam(lambda right: ap(
|
|
245
|
+
_TARROW,
|
|
246
|
+
ap(self_recursion, offset, left), ap(self_recursion, offset, right),
|
|
247
|
+
))),
|
|
248
|
+
)))))
|
|
249
|
+
|
|
250
|
+
# shift_context offset context: rename every type in a context apart by ``offset``.
|
|
251
|
+
_SHIFT_CONTEXT: Builder = app(Y, lam(lambda self_recursion: lam(lambda offset: lam(lambda context: ap(
|
|
252
|
+
context,
|
|
253
|
+
_NIL,
|
|
254
|
+
lam(lambda head: lam(lambda tail: ap(
|
|
255
|
+
_CONS,
|
|
256
|
+
ap(_SHIFT_TYPE, offset, head), ap(self_recursion, offset, tail),
|
|
257
|
+
))),
|
|
258
|
+
)))))
|
|
259
|
+
|
|
260
|
+
# apply_subst subst type: resolve ``type`` deeply, so the result carries no residual substitution.
|
|
261
|
+
_APPLY_SUBST: Builder = app(Y, lam(lambda self_recursion: lam(lambda subst: lam(lambda type_: ap(
|
|
262
|
+
ap(_RESOLVE, subst, type_),
|
|
263
|
+
lam(lambda identifier: app(_TVAR, identifier)),
|
|
264
|
+
lam(lambda left: lam(lambda right: ap(
|
|
265
|
+
_TARROW,
|
|
266
|
+
ap(self_recursion, subst, left), ap(self_recursion, subst, right),
|
|
267
|
+
))),
|
|
268
|
+
)))))
|
|
269
|
+
|
|
270
|
+
_APPLY_SUBST_CONTEXT: Builder = app(Y, lam(lambda self_recursion: lam(lambda subst: lam(lambda context: ap(
|
|
271
|
+
context,
|
|
272
|
+
_NIL,
|
|
273
|
+
lam(lambda head: lam(lambda tail: ap(
|
|
274
|
+
_CONS,
|
|
275
|
+
ap(_APPLY_SUBST, subst, head), ap(self_recursion, subst, tail),
|
|
276
|
+
))),
|
|
277
|
+
)))))
|
|
278
|
+
|
|
279
|
+
# merge state a b, state = (subst, failed): unify the shared prefix of two contexts (same de Bruijn
|
|
280
|
+
# indices) and keep the tail of the longer; returns (state, merged-context).
|
|
281
|
+
_MERGE: Builder = app(Y, lam(lambda self_recursion: lam(lambda state: lam(lambda a: lam(lambda b: ap(
|
|
282
|
+
a,
|
|
283
|
+
pair(state, b), # a is nil: the merge is b
|
|
284
|
+
lam(lambda head_a: lam(lambda tail_a: ap(
|
|
285
|
+
b,
|
|
286
|
+
pair(state, ap(_CONS, head_a, tail_a)), # b is nil: the merge is a
|
|
287
|
+
lam(lambda head_b: lam(lambda tail_b: split_pair(
|
|
288
|
+
ap(self_recursion, ap(_UNIFY, state, head_a, head_b), tail_a, tail_b),
|
|
289
|
+
lambda merged_state, merged_tail: pair(merged_state, ap(_CONS, head_a, merged_tail)),
|
|
290
|
+
))),
|
|
291
|
+
))),
|
|
292
|
+
))))))
|
|
293
|
+
|
|
294
|
+
_INITIAL_PAIR: Builder = pair(_EMPTY_SUBST, FALSE)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# principal node: the bottom-up principal typing of a quoted term, the path-free fold described above.
|
|
298
|
+
PRINCIPAL: Builder = app(Y, lam(lambda self_recursion: lam(lambda node: ap(
|
|
299
|
+
node,
|
|
300
|
+
# QVar index: context [TVAR 0 .. TVAR index], type TVAR index (ids as BinNat).
|
|
301
|
+
lam(lambda index: let(app(_CHURCH_TO_BINNAT, index), lambda binnat_index: pair(
|
|
302
|
+
app(BIN_SUCC, binnat_index),
|
|
303
|
+
pair(
|
|
304
|
+
app(_BUILD_VARS, app(BIN_SUCC, binnat_index)),
|
|
305
|
+
pair(app(_TVAR, binnat_index), FALSE),
|
|
306
|
+
),
|
|
307
|
+
))),
|
|
308
|
+
# QLam body: discharge de Bruijn index 0 (the binder's type) from the body's context.
|
|
309
|
+
lam(lambda body: split_quad(
|
|
310
|
+
app(self_recursion, body),
|
|
311
|
+
lambda next_body, context_body, type_body, failed_body: ap(
|
|
312
|
+
context_body,
|
|
313
|
+
# body uses no enclosing binder: the parameter is a fresh, unconstrained type.
|
|
314
|
+
pair(
|
|
315
|
+
app(BIN_SUCC, next_body),
|
|
316
|
+
pair(_NIL, pair(ap(_TARROW, app(_TVAR, next_body), type_body), failed_body)),
|
|
317
|
+
),
|
|
318
|
+
lam(lambda parameter: lam(lambda rest: pair(
|
|
319
|
+
next_body,
|
|
320
|
+
pair(rest, pair(ap(_TARROW, parameter, type_body), failed_body)),
|
|
321
|
+
))),
|
|
322
|
+
),
|
|
323
|
+
)),
|
|
324
|
+
# QApp function argument: rename the argument's type-var band apart, merge the shared context,
|
|
325
|
+
# then unify the function's type with (argument-type -> fresh result).
|
|
326
|
+
lam(lambda function: lam(lambda argument: split_quad(
|
|
327
|
+
app(self_recursion, function),
|
|
328
|
+
lambda next_f, context_f, type_f, failed_f: split_quad(
|
|
329
|
+
app(self_recursion, argument),
|
|
330
|
+
lambda next_a, context_a, type_a, failed_a: ap(
|
|
331
|
+
ap(OR, failed_f, failed_a),
|
|
332
|
+
pair(
|
|
333
|
+
app(BIN_SUCC, ap(BIN_ADD, next_f, next_a)),
|
|
334
|
+
pair(_NIL, pair(app(_TVAR, BIN_ZERO), TRUE)),
|
|
335
|
+
),
|
|
336
|
+
let(ap(BIN_ADD, next_f, next_a), lambda total: let(
|
|
337
|
+
ap(_SHIFT_CONTEXT, next_f, context_a), lambda context_a_shifted: let(
|
|
338
|
+
ap(_SHIFT_TYPE, next_f, type_a), lambda type_a_shifted: let(
|
|
339
|
+
app(_TVAR, total), lambda result_type: split_pair(
|
|
340
|
+
ap(_MERGE, _INITIAL_PAIR, context_f, context_a_shifted),
|
|
341
|
+
lambda merged_state, merged_context: split_pair(
|
|
342
|
+
ap(_UNIFY, merged_state, type_f, ap(_TARROW, type_a_shifted, result_type)),
|
|
343
|
+
lambda final_subst, final_failed: ap(
|
|
344
|
+
final_failed,
|
|
345
|
+
pair(
|
|
346
|
+
app(BIN_SUCC, total),
|
|
347
|
+
pair(_NIL, pair(app(_TVAR, BIN_ZERO), TRUE)),
|
|
348
|
+
),
|
|
349
|
+
pair(
|
|
350
|
+
app(BIN_SUCC, total),
|
|
351
|
+
pair(
|
|
352
|
+
ap(_APPLY_SUBST_CONTEXT, final_subst, merged_context),
|
|
353
|
+
pair(ap(_APPLY_SUBST, final_subst, result_type), FALSE),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
),
|
|
358
|
+
)))),
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
))),
|
|
363
|
+
))))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# TYPABLE_BU quoted: simply typable iff the bottom-up principal typing has no occurs-check failure.
|
|
367
|
+
TYPABLE_BU: Builder = lam(lambda quoted: split_quad(
|
|
368
|
+
app(PRINCIPAL, quoted),
|
|
369
|
+
lambda next_id, context, type_, failed: app(_NOT, failed),
|
|
370
|
+
))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: co-lambda
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A least-fixpoint first-order-shape-relation interpreter for the lambda-calculus
|
|
5
|
+
Project-URL: Repository, https://github.com/Atry/MIXINv2
|
|
6
|
+
Author-email: "Yang, Bo" <yang-bo@yang-bo.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: fixpoints
|
|
14
|
+
Requires-Dist: typing-extensions>=4.1.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# co_lambda
|
|
18
|
+
|
|
19
|
+
A first-order-shape-relation interpreter for the lambda-calculus, realizing the
|
|
20
|
+
semantics of the paper `papers/co-lambda/first-order.tex`, depending on `fixpoints`.
|
|
21
|
+
|
|
22
|
+
A lambda-term's tree is the readout of a single first-order weak-head **shape relation** `Sh`
|
|
23
|
+
over term positions. The shape at a position is single-valued, so there is no set to aggregate.
|
|
24
|
+
`readout(node)` resolves each position's head via its `Sh` and descends.
|
|
25
|
+
|
|
26
|
+
Because positions are **interned** (structurally-equal positions are one object, identity is a
|
|
27
|
+
pointer test), a cyclic structure has finitely many positions and the least-fixpoint reading
|
|
28
|
+
folds it into a finite rational tree where head reduction would unfold forever. So the readout
|
|
29
|
+
terminates on every rational tree, and decides an unproductive cycle as the meaningless leaf in
|
|
30
|
+
finite time, where head reduction diverges. Interning is the *finest* instance of a pluggable
|
|
31
|
+
**position congruence** (see below); a coarser sound congruence folds more.
|
|
32
|
+
|
|
33
|
+
`readout` has two re-entry policies:
|
|
34
|
+
|
|
35
|
+
- `fold_cycles=True` (default) is the least fixpoint `lfp`, the denotation: a guarded cycle
|
|
36
|
+
folds into a finite rational graph (`render` prints it with `#N` back-references); the only
|
|
37
|
+
leaves are variables and the meaningless `⊥`.
|
|
38
|
+
- `fold_cycles=False` is the finite-budget first-iteration reading `T↑1`: a re-entered guarded
|
|
39
|
+
cycle is cut to the distinct guarded-cut leaf `∅` (the hole where the budget stopped on a
|
|
40
|
+
productive cycle), kept separate from the meaningless `⊥` (an unproductive cycle, a position
|
|
41
|
+
with no shape). `∅` never appears in the least fixpoint.
|
|
42
|
+
|
|
43
|
+
The calculus is **pure** (`Var`/`Lam`/`App`): no recursion binder is needed. The `Y` combinator
|
|
44
|
+
produces the structural repetition that interning folds, so:
|
|
45
|
+
|
|
46
|
+
- `Y (cons 0)` (the cyclic stream `r = cons 0 r`) folds to a finite rational tree.
|
|
47
|
+
- `Ω = (λx.xx)(λx.xx)` and `Y (λx.x)` (i.e. `letrec x = x`) are unproductive cycles: they read
|
|
48
|
+
out as `⊥` under both readings.
|
|
49
|
+
|
|
50
|
+
The fold/cut is taken only at **closed** positions, so a folded back-reference never misreads a
|
|
51
|
+
free de Bruijn variable.
|
|
52
|
+
|
|
53
|
+
## Position congruence (a second parameter)
|
|
54
|
+
|
|
55
|
+
*Which* positions count as "the same" when the readout folds is a parameter, a **position
|
|
56
|
+
congruence** (`Definition def:congruence` in the paper). `readout(node, congruence=...)` keys the
|
|
57
|
+
fold on `congruence.key(node)` instead of raw object identity. A congruence is **sound** when it
|
|
58
|
+
is contained in tree equality (it never folds positions with different trees); stated over the
|
|
59
|
+
full signature, soundness is the congruence law read coinductively, so a well-formed congruence
|
|
60
|
+
folds without changing the denotation. `_congruence.py` provides four instances, from finest to
|
|
61
|
+
coarser:
|
|
62
|
+
|
|
63
|
+
- `IdentityCongruence` (the default): syntactic de Bruijn identity, `key = id(node)`, a pointer
|
|
64
|
+
test. The finest instance; reproduces the pure-interning readout exactly.
|
|
65
|
+
- `EGraphCongruence`: a union-find with congruence closure over the *syntactic* constructors. The
|
|
66
|
+
caller asserts sound (tree-equal) merges with `merge(a, b)`; closure propagates them to
|
|
67
|
+
`App`/`Lam` parents, and the readout shares the merged positions. The inductive
|
|
68
|
+
(least-fixpoint) family: it folds whatever finitely many tree-equal pairs generate.
|
|
69
|
+
- `PositionEGraphCongruence`: the faithful `def:congruence`, keyed on the *demanded descent* (the
|
|
70
|
+
shape tree) rather than the syntax. It auto-folds any two positions bisimilar under `Sh` with
|
|
71
|
+
no asserted merge (a redex and its reduct collapse on sight), folding exactly the rational
|
|
72
|
+
fragment. It cannot finitize an infinitely-presented shape graph, so the `Y F 0` witness below
|
|
73
|
+
still diverges: bisimulation alone does not rescue a dead-argument cycle.
|
|
74
|
+
- `DeadSubtermCongruence(rules=...)`: equality up to dead subterms, the key being the syntax with
|
|
75
|
+
every dead-argument slot erased to a canonical placeholder (a tree-preserving map, not a
|
|
76
|
+
congruence closure). The dead slots are recognised by a **library of sound rules**; the caller
|
|
77
|
+
enables a subset. `RecursionArgumentRule` folds the paper's `Y F 0` witness, where a
|
|
78
|
+
constant-headed recursion carries an index it never inspects; `UnusedParameterRule` erases the
|
|
79
|
+
argument of a function that discards it. This is the one reading that folds the witness, which
|
|
80
|
+
neither e-graph can.
|
|
81
|
+
|
|
82
|
+
No parser is provided. Build terms in Python with the HOAS DSL in `_dsl.py` (`lam`, `app`,
|
|
83
|
+
`build`), which compiles to a first-order de Bruijn AST; `_prelude.py` collects example terms
|
|
84
|
+
(combinators, Scott-encoded lists, Church numerals with Peano arithmetic).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
co_lambda/__init__.py,sha256=jlsjd6vC85uaCQcvtQdTczXIb_U1ggnan7Zj9okwme0,87
|
|
2
|
+
co_lambda/_analysis.py,sha256=jK7hM3Ec5-xgxtg6Xk8X3S_XZMKRBYIm11gfFq0R0kc,5224
|
|
3
|
+
co_lambda/_ast.py,sha256=sbjz-OgsjAbiTZwBWZoUFpHWblplYJcI4bmsyljvpDs,15467
|
|
4
|
+
co_lambda/_binnat.py,sha256=6JZk6AKoMiH1QaJEAkOHHehKAtm3ymXGR1UBEYf6sDs,6589
|
|
5
|
+
co_lambda/_codec.py,sha256=apgi6NqK_BfyeJbYs_gROCLIlefQIbmaAeuhgTK8Fmo,5979
|
|
6
|
+
co_lambda/_compiler_artifact.py,sha256=aTknT4uPoJ27NluVMueRFoEcdXtTgZz5K0lxl9pnWGQ,1828
|
|
7
|
+
co_lambda/_defun_codegen.py,sha256=0Lwn6Z7V71hfhjLmnXev5AekzPPszKgtR3lLnClNrbQ,12942
|
|
8
|
+
co_lambda/_defun_runtime.py,sha256=t9yXzG4qUPI423d5oBXCvxxJVrIIykgaTSyvHrqmxKM,6624
|
|
9
|
+
co_lambda/_defunctionalize.py,sha256=cNTLvZmcrvE0FB2CmpFEC0C1SHX5TveRd4eIC_Bpp_w,18511
|
|
10
|
+
co_lambda/_dsl.py,sha256=CGPRtfobNboS49Bf38rT98v5pyWJP10gHnrl66IoRz8,5901
|
|
11
|
+
co_lambda/_hoas_latex.py,sha256=fa71h7ZE7npe7viISb8mKY70gTExQpgnJ3dOscprlKs,6205
|
|
12
|
+
co_lambda/_latex.py,sha256=mSKFx-LOs1aNPViB5CHHVjwfUHz92NRL5LIIjlXlHkc,2378
|
|
13
|
+
co_lambda/_prelude.py,sha256=QzgCnnEio2vqqmaWDlAzuqEZNNvtWTmuWluZRUV1gvg,6694
|
|
14
|
+
co_lambda/_pyast.py,sha256=l_5kzE4A5xIpfW-m3O1FguOIubp1d5o15aeAn62bQp4,18526
|
|
15
|
+
co_lambda/_pybuild.py,sha256=kDO5e2oF71EBij6-y_GAYW7NnsyAoa1-ofEhHwVPJ_g,12898
|
|
16
|
+
co_lambda/_reduce.py,sha256=38sp9EcbDAaj2qbU3zwfgl9VbteHb3cRBJsTB7125n8,7794
|
|
17
|
+
co_lambda/_shape.py,sha256=uSE65a1028s7dGMLFxvXoN2Sx8COorcb2uIEOWPweqQ,9991
|
|
18
|
+
co_lambda/_sugar.py,sha256=zpqMOEjaPjKXZ9QCKQaJjeOYHjQXmQBdyB40rzPU09Q,2804
|
|
19
|
+
co_lambda/_typecheck.py,sha256=E2b8CRNLprqwkD6REgeTsmDjD4XI3DONqviWoG9Fv3E,18451
|
|
20
|
+
co_lambda/_generated/.gitattributes,sha256=wKvWc-ybPwrbJlEkT0CjI34N0mlTFIt_Gs634YupU6U,319
|
|
21
|
+
co_lambda/_generated/__init__.py,sha256=_XA-qAGrK7esywkAOlTnKDF01A8c3MVFCK4FToC8Y6U,80
|
|
22
|
+
co_lambda/_generated/_generated_defun_compiler_py311.py,sha256=vwGvCqaUszUXKoRkfVeJG-P1TBKlp3wc49ew05aE64o,221357
|
|
23
|
+
co_lambda/_generated/_generated_defun_compiler_py312.py,sha256=jY2a0we8bxa4kj9r3bO0Bc1kC6pcxW4Hct0oUExHl5A,221417
|
|
24
|
+
co_lambda/_generated/_generated_defun_compiler_py313.py,sha256=_rY7M5lWz-HhGKpsd4vA-CkBD7AD1vG2wzswQ7Fwgus,221417
|
|
25
|
+
co_lambda-0.5.0.dist-info/METADATA,sha256=DNF8Xz81YqNQKhAqEMZmDbEzoi9ruqoUzb4XOQ3jLrM,5179
|
|
26
|
+
co_lambda-0.5.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
27
|
+
co_lambda-0.5.0.dist-info/entry_points.txt,sha256=6aVuCcnD39-E3dCVplkt_qG4Grc_x_q_wTPSS9ppcck,79
|
|
28
|
+
co_lambda-0.5.0.dist-info/RECORD,,
|