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/_pybuild.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Lambda-term builders for the generic ``_pyast`` Scott encoding of a Python AST.
|
|
2
|
+
|
|
3
|
+
The compiler's target is a real Python ``ast`` Scott-encoded by ``_pyast`` (reflection-derived from the
|
|
4
|
+
``ast`` node classes). To let the ``COMPILE`` lambda term EMIT that generic encoding directly, this
|
|
5
|
+
module gives lambda-term "smart constructors", one per ``ast`` node the compiler produces, that fill in
|
|
6
|
+
the boilerplate fields the generic ``_pyast.decode`` reads (``decorator_list=[]``, ``returns=None``,
|
|
7
|
+
``ctx=Load()``, ...). Each is a thin wrapper over ``_pyast``'s own ``_ctor`` (the n-ary Scott
|
|
8
|
+
constructor), ``_kind`` (the field kind-tag pair), and ``_scott_list``, so the values these build are
|
|
9
|
+
exactly what ``_pyast.decode`` expects: ``_pyast.decode(build(<smart ctor>)) == <the ast node>``.
|
|
10
|
+
|
|
11
|
+
A node is ``_ctor(tag, fields)`` where ``tag`` is the class's index in ``_pyast.SUPPORTED`` and each
|
|
12
|
+
field is ``_kind(kind, payload)``; a list field's payload is a Scott list whose elements are themselves
|
|
13
|
+
kind-tagged fields (so a list of nodes is a Scott list of ``_field_node`` values).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
|
|
20
|
+
from co_lambda._codec import char_codes, church
|
|
21
|
+
from co_lambda._dsl import Builder, app, lam
|
|
22
|
+
from co_lambda._prelude import SCOTT_NIL
|
|
23
|
+
from co_lambda._sugar import ap, cons, map_list, one, two
|
|
24
|
+
from co_lambda._pyast import (
|
|
25
|
+
_K_BOOL,
|
|
26
|
+
_K_GENSYM,
|
|
27
|
+
_K_IDENT,
|
|
28
|
+
_K_INT,
|
|
29
|
+
_K_LIST,
|
|
30
|
+
_K_NODE,
|
|
31
|
+
_K_NONE,
|
|
32
|
+
_K_STR,
|
|
33
|
+
_TAG,
|
|
34
|
+
_ctor,
|
|
35
|
+
_kind,
|
|
36
|
+
_scott_list,
|
|
37
|
+
encode,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# --- field constructors (a field is a <kind, payload> pair the decoder dispatches on) -----------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _node(cls: "type[ast.AST]", fields: "list[Builder]") -> Builder:
|
|
44
|
+
"""The Scott value for an ``ast`` node of class ``cls`` with the given (kind-tagged) fields."""
|
|
45
|
+
return _ctor(_TAG[cls], fields)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def field_node(child: Builder) -> Builder:
|
|
49
|
+
return _kind(_K_NODE, child)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def field_list(elements: Builder) -> Builder:
|
|
53
|
+
"""A list field; ``elements`` is a Scott list whose items are themselves kind-tagged fields."""
|
|
54
|
+
return _kind(_K_LIST, elements)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def field_int(nat: Builder) -> Builder:
|
|
58
|
+
return _kind(_K_INT, nat)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def field_str(char_codes: Builder) -> Builder:
|
|
62
|
+
"""A string field; ``char_codes`` is a Scott list of Nat character codes."""
|
|
63
|
+
return _kind(_K_STR, char_codes)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def field_ident(path: Builder) -> Builder:
|
|
67
|
+
"""An identifier field; ``path`` is a Scott list of Nats (an AST path). The single ``_pyast``
|
|
68
|
+
decoder renders it ``v_<int>_<int>...`` for every runtime, so the lambda compiler emits only the
|
|
69
|
+
path, never a rendered string."""
|
|
70
|
+
return _kind(_K_IDENT, path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def field_bool(nat: Builder) -> Builder:
|
|
74
|
+
return _kind(_K_BOOL, nat)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def field_none() -> Builder:
|
|
78
|
+
return _kind(_K_NONE, church(0))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def field_node_list(node_fields: Builder) -> Builder:
|
|
82
|
+
"""Convenience: a list field over a Scott list whose elements are already ``field_node`` values."""
|
|
83
|
+
return field_list(node_fields)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --- smart constructors, one per ast node the compiler emits -------------------------------------
|
|
87
|
+
# A list-valued argument is a Scott list of ALREADY kind-tagged fields (``field_node`` of each node, or
|
|
88
|
+
# ``field_str`` of each name), matching what the decoder's ``_K_LIST`` case feeds back to ``_decode_field``.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def py_load() -> Builder:
|
|
92
|
+
return _node(ast.Load, [])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def py_store() -> Builder:
|
|
96
|
+
return _node(ast.Store, [])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def py_is() -> Builder:
|
|
100
|
+
return _node(ast.Is, [])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def py_name(name_field: Builder, ctx: Builder) -> Builder:
|
|
104
|
+
"""``ast.Name(id=<name>, ctx=<ctx>)``; ``name_field`` an already-kind-tagged name field
|
|
105
|
+
(``field_str`` for a fixed name, ``field_ident`` for a variable's AST path)."""
|
|
106
|
+
return _node(ast.Name, [name_field, field_node(ctx)])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def py_arg(name_field: Builder) -> Builder:
|
|
110
|
+
"""``ast.arg(arg=<name>, annotation=None, type_comment=None)``; ``name_field`` a name field."""
|
|
111
|
+
return _node(ast.arg, [name_field, field_none(), field_none()])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def py_arguments(arg_fields: Builder) -> Builder:
|
|
115
|
+
"""``ast.arguments`` with only positional ``args`` populated; ``arg_fields`` a Scott list of
|
|
116
|
+
``field_node(arg)``. Order: posonlyargs, args, vararg, kwonlyargs, kw_defaults, kwarg, defaults."""
|
|
117
|
+
return _node(
|
|
118
|
+
ast.arguments,
|
|
119
|
+
[
|
|
120
|
+
field_list(SCOTT_NIL),
|
|
121
|
+
field_list(arg_fields),
|
|
122
|
+
field_none(),
|
|
123
|
+
field_list(SCOTT_NIL),
|
|
124
|
+
field_list(SCOTT_NIL),
|
|
125
|
+
field_none(),
|
|
126
|
+
field_list(SCOTT_NIL),
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def py_lambda(arg_field: Builder, body: Builder) -> Builder:
|
|
132
|
+
"""``lambda <arg>: <body>`` with a single positional parameter; ``arg_field`` a name field."""
|
|
133
|
+
args = py_arguments(_scott_list([field_node(py_arg(arg_field))]))
|
|
134
|
+
return _node(ast.Lambda, [field_node(args), field_node(body)])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def py_lambda0(body: Builder) -> Builder:
|
|
138
|
+
"""``lambda: <body>`` with no parameters (for a call-by-name ``Thunk(lambda: e)``)."""
|
|
139
|
+
return _node(ast.Lambda, [field_node(py_arguments(SCOTT_NIL)), field_node(body)])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def py_call(func: Builder, arg_fields: Builder) -> Builder:
|
|
143
|
+
"""``<func>(<args...>)``; ``arg_fields`` a Scott list of ``field_node(arg)``; no keywords."""
|
|
144
|
+
return _node(ast.Call, [field_node(func), field_list(arg_fields), field_list(SCOTT_NIL)])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def py_function_def(name_field: Builder, args_node: Builder, body_fields: Builder) -> Builder:
|
|
148
|
+
"""``def <name>(<args>): <body>`` with no decorators/returns/type comment; ``body_fields`` a Scott
|
|
149
|
+
list of ``field_node(stmt)``.
|
|
150
|
+
|
|
151
|
+
The fields are keyed by name and ordered by the RUNNING ``ast.FunctionDef._fields``, so the emitted
|
|
152
|
+
node matches the host Python version: Python 3.12+ added ``type_params`` (an empty list here), which
|
|
153
|
+
the generic decoder reflects, so the call-by-need target round-trips on 3.11 and on 3.12+ alike.
|
|
154
|
+
"""
|
|
155
|
+
by_name = {
|
|
156
|
+
"name": name_field,
|
|
157
|
+
"args": field_node(args_node),
|
|
158
|
+
"body": field_list(body_fields),
|
|
159
|
+
"decorator_list": field_list(SCOTT_NIL),
|
|
160
|
+
"returns": field_none(),
|
|
161
|
+
"type_comment": field_none(),
|
|
162
|
+
"type_params": field_list(SCOTT_NIL),
|
|
163
|
+
}
|
|
164
|
+
return _node(ast.FunctionDef, [by_name[name] for name in ast.FunctionDef._fields])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def py_assign(target: Builder, value: Builder) -> Builder:
|
|
168
|
+
"""``<target> = <value>`` with a single target. Order: targets, value, type_comment."""
|
|
169
|
+
return _node(ast.Assign, [field_list(_scott_list([field_node(target)])), field_node(value), field_none()])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def py_nonlocal(name_fields: Builder) -> Builder:
|
|
173
|
+
"""``nonlocal <names...>``; ``name_fields`` a Scott list of ``field_str(codes)``."""
|
|
174
|
+
return _node(ast.Nonlocal, [field_list(name_fields)])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def py_if(test: Builder, body_fields: Builder) -> Builder:
|
|
178
|
+
"""``if <test>: <body>`` with no else; ``body_fields`` a Scott list of ``field_node(stmt)``."""
|
|
179
|
+
return _node(ast.If, [field_node(test), field_list(body_fields), field_list(SCOTT_NIL)])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def py_return(value: Builder) -> Builder:
|
|
183
|
+
return _node(ast.Return, [field_node(value)])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def py_compare_is(left: Builder, right: Builder) -> Builder:
|
|
187
|
+
"""``<left> is <right>``. Order: left, ops, comparators."""
|
|
188
|
+
return _node(
|
|
189
|
+
ast.Compare,
|
|
190
|
+
[
|
|
191
|
+
field_node(left),
|
|
192
|
+
field_list(_scott_list([field_node(py_is())])),
|
|
193
|
+
field_list(_scott_list([field_node(right)])),
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def py_module(stmt_fields: Builder) -> Builder:
|
|
199
|
+
"""``ast.Module``; ``stmt_fields`` a Scott list of ``field_node(stmt)``. Order: body, type_ignores."""
|
|
200
|
+
return _node(ast.Module, [field_list(stmt_fields), field_list(SCOTT_NIL)])
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def py_constant_int(nat: Builder) -> Builder:
|
|
204
|
+
"""``ast.Constant(value=<int>, kind=None)`` with an integer (Nat) value."""
|
|
205
|
+
return _node(ast.Constant, [field_int(nat), field_none()])
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def py_subscript(value: Builder, index: Builder) -> Builder:
|
|
209
|
+
"""``<value>[<index>]`` (Load). Order: value, slice, ctx."""
|
|
210
|
+
return _node(ast.Subscript, [field_node(value), field_node(index), field_node(py_load())])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def py_tuple(element_fields: Builder) -> Builder:
|
|
214
|
+
"""``(<elements...>,)`` (Load); ``element_fields`` a Scott list of ``field_node(elt)``. Order: elts, ctx."""
|
|
215
|
+
return _node(ast.Tuple, [field_list(element_fields), field_node(py_load())])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --- if-expression AST dispatch (the constructor fast path with an interpret fallback) ------------
|
|
219
|
+
# The compiler emits ``<body> if isinstance(n, Var) else ...`` to dispatch on a runtime lambda-AST
|
|
220
|
+
# node's constructor without Scott reduction (so it interns nothing). An IfExp is an EXPRESSION (it
|
|
221
|
+
# fits CODEGEN's expression target and runs on CPython and PyPy alike), unlike a match statement.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def py_attribute(value: Builder, attr_field: Builder) -> Builder:
|
|
225
|
+
"""``<value>.<attr>`` (Load); ``attr_field`` a name field (e.g. ``field_str(char_codes("index"))``).
|
|
226
|
+
Order: value, attr, ctx."""
|
|
227
|
+
return _node(ast.Attribute, [field_node(value), attr_field, field_node(py_load())])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def py_ifexp(test: Builder, body: Builder, orelse: Builder) -> Builder:
|
|
231
|
+
"""``<body> if <test> else <orelse>`` (``ast.IfExp``). Order: test, body, orelse."""
|
|
232
|
+
return _node(ast.IfExp, [field_node(test), field_node(body), field_node(orelse)])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def py_isinstance(value: Builder, class_name_field: Builder) -> Builder:
|
|
236
|
+
"""``isinstance(<value>, <Class>)``; ``class_name_field`` a name field naming the class (``Var`` /
|
|
237
|
+
``Lam`` / ``App``, bound in the generated module header)."""
|
|
238
|
+
return py_call(ex_name(field_str(char_codes("isinstance"))), two_nodes(value, ex_name(class_name_field)))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- emission notation: fixed-shape statement/expression/identifier helpers ----------------------
|
|
242
|
+
# Builder-only transcription sugar used by the CODEGEN / CODEGEN_NEED lambda terms; shapes are
|
|
243
|
+
# literal at the call site, parameters are Builders.
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def name_gensym_field(role: Builder, quoted: Builder) -> Builder:
|
|
247
|
+
"""A PATH-FREE, DEPTH-FREE name field for a call-by-need memo cell/thunk, identified by its ``role``
|
|
248
|
+
(cell / thunk / function) and the ``quoted`` sub-term it belongs to. The payload is interned, so the
|
|
249
|
+
TABLED recursion yields the SAME node for the same (role, quoted) -- the decoder's ``_K_GENSYM`` case
|
|
250
|
+
then assigns one fresh ``vg_<n>`` per distinct node, consistent across the cell's definition and uses,
|
|
251
|
+
distinct across different cells. Lambda-lifted call-by-need accesses binders positionally through the
|
|
252
|
+
environment, so a sub-term's compiled code does not depend on its binder depth; keying on ``quoted``
|
|
253
|
+
alone (not ``(depth, quoted)``) means a sub-term shared across DIFFERENT depths compiles once, not
|
|
254
|
+
once per depth (the depth-keyed scheme recompiled COMPILE's combinators once per nesting depth)."""
|
|
255
|
+
return _kind(_K_GENSYM, cons(role, quoted))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def ex_name(name_field: Builder) -> Builder:
|
|
259
|
+
return py_name(name_field, py_load())
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def stmt(node: Builder) -> Builder:
|
|
263
|
+
"""Wrap a statement node as a field so it can sit in a Scott list of statements."""
|
|
264
|
+
return field_node(node)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def st_func_def(name_field: Builder, parameter_fields: Builder, body_fields: Builder) -> Builder:
|
|
268
|
+
arguments = py_arguments(
|
|
269
|
+
map_list(lam(lambda field: field_node(py_arg(field))), parameter_fields),
|
|
270
|
+
)
|
|
271
|
+
return py_function_def(name_field, arguments, body_fields)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def st_assign(target_field: Builder, value: Builder) -> Builder:
|
|
275
|
+
return py_assign(py_name(target_field, py_store()), value)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def st_return(value: Builder) -> Builder:
|
|
279
|
+
return py_return(value)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def two_nodes(first: Builder, second: Builder) -> Builder:
|
|
283
|
+
"""A two-element Scott argument list of node fields."""
|
|
284
|
+
return two(field_node(first), field_node(second))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# --- ClassDef / AnnAssign smart constructors for defunctionalization --------------------------------
|
|
288
|
+
# Mechanical boilerplate fillers for the class-based defunctionalized output. These encode no
|
|
289
|
+
# compilation decisions; they fill the ``ast`` fields the generic decoder expects.
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def py_classdef(name_field: Builder, decorator_fields: Builder, body_fields: Builder) -> Builder:
|
|
293
|
+
"""``class <name>: <body>`` with decorators, no bases/keywords/type_params.
|
|
294
|
+
|
|
295
|
+
Field order follows ``ast.ClassDef._fields`` on the running Python version.
|
|
296
|
+
"""
|
|
297
|
+
by_name = {
|
|
298
|
+
"name": name_field,
|
|
299
|
+
"bases": field_list(SCOTT_NIL),
|
|
300
|
+
"keywords": field_list(SCOTT_NIL),
|
|
301
|
+
"body": field_list(body_fields),
|
|
302
|
+
"decorator_list": field_list(decorator_fields),
|
|
303
|
+
"type_params": field_list(SCOTT_NIL),
|
|
304
|
+
}
|
|
305
|
+
return _node(ast.ClassDef, [by_name[name] for name in ast.ClassDef._fields])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def py_annassign(target: Builder, annotation: Builder) -> Builder:
|
|
309
|
+
"""``<target>: <annotation>`` with no value, ``simple=1``.
|
|
310
|
+
|
|
311
|
+
Order: target, annotation, value, simple.
|
|
312
|
+
"""
|
|
313
|
+
return _node(ast.AnnAssign, [field_node(target), field_node(annotation), field_none(), field_int(church(1))])
|
co_lambda/_reduce.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""The fold oracle as a lambda term: does a quoted term have a finite normal form?
|
|
2
|
+
|
|
3
|
+
``CHOOSE_RUNTIME`` needs to know whether a term normalizes to a finite normal form (so call-by-need,
|
|
4
|
+
which never folds, reaches the same value) or only the interpreter's fold handles it (a cyclic, rational
|
|
5
|
+
behaviour). The Python ``_specialize.needs_folding`` reads this out of the interpreter with bounds; this
|
|
6
|
+
module is the pure-lambda port and the verdict that actually drives specialization.
|
|
7
|
+
|
|
8
|
+
It is a fuel-bounded normalizer written in the calculus. ``EVAL`` denotes a quoted de Bruijn term as a
|
|
9
|
+
value in a three-constructor semantic domain: ``VLAM`` (a fuel-aware host function), ``VNEU`` (a neutral:
|
|
10
|
+
a variable applied to value arguments), and ``VBOTTOM`` (the fuel ran out). Argument evaluation is lazy
|
|
11
|
+
because the interpreter running ``EVAL`` is itself call-by-name, so a normalizing term whose strict
|
|
12
|
+
reduction would diverge (``factorial`` through ``Y``) still reaches its normal form. A **BinNat fuel**
|
|
13
|
+
threads through every evaluation and walk step and decreases at each one, so a genuinely diverging head
|
|
14
|
+
reduction (``Omega``) hits ``VBOTTOM`` rather than looping. ``WALK`` forces the value to full normal form,
|
|
15
|
+
going under binders (applying a ``VLAM`` to a fresh neutral) and down neutral spines, threading the fuel
|
|
16
|
+
as a SINGLE sequential budget (the argument is walked with whatever fuel the head left), so total work is
|
|
17
|
+
linear in the fuel rather than exponential. ``NORMALIZES`` is whether the walk completes before the fuel
|
|
18
|
+
runs out: completion means a finite normal form was positively observed (call-by-need safe); exhaustion
|
|
19
|
+
is read conservatively as needs-fold (interpret), which is always sound. This mirrors ``needs_folding``.
|
|
20
|
+
|
|
21
|
+
This module is pure lambda calculus: every top-level binding is a ``Builder``. The verdict reader
|
|
22
|
+
(``normalizes_lambda``) and the fuel policy live at the boundary (``_specialize``); the large-stack
|
|
23
|
+
host machinery lives in ``_runtime``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from co_lambda._binnat import BIN_IS_ZERO, BIN_PRED, BIN_SUCC, BIN_ZERO
|
|
29
|
+
from co_lambda._dsl import Builder, app, lam
|
|
30
|
+
from co_lambda._prelude import FALSE, IS_ZERO, PRED, TRUE, Y
|
|
31
|
+
from co_lambda._sugar import ap
|
|
32
|
+
|
|
33
|
+
# Option: SOME remaining-fuel | NONE (exhausted); consumed as ``option some_handler none_value``.
|
|
34
|
+
_SOME: Builder = lam(lambda value: lam(lambda some_handler: lam(lambda none_value: app(some_handler, value))))
|
|
35
|
+
_NONE: Builder = lam(lambda some_handler: lam(lambda none_value: none_value))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- environment as a Scott list ----------------------------------------------------------------
|
|
39
|
+
_NIL: Builder = lam(lambda nil_value: lam(lambda cons_handler: nil_value))
|
|
40
|
+
_CONS: Builder = lam(lambda head: lam(lambda tail: lam(lambda nil_value: lam(lambda cons_handler: ap(cons_handler, head, tail)))))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- the semantic domain: VLAM function | VNEU neutral | VBOTTOM (consumed with three handlers) ---
|
|
44
|
+
# A neutral is NVAR level | NAPP neutral value.
|
|
45
|
+
_VLAM: Builder = lam(lambda function: lam(lambda on_lam: lam(lambda on_neu: lam(lambda on_bottom: app(on_lam, function)))))
|
|
46
|
+
_VNEU: Builder = lam(lambda neutral: lam(lambda on_lam: lam(lambda on_neu: lam(lambda on_bottom: app(on_neu, neutral)))))
|
|
47
|
+
_VBOTTOM: Builder = lam(lambda on_lam: lam(lambda on_neu: lam(lambda on_bottom: on_bottom)))
|
|
48
|
+
|
|
49
|
+
_NVAR: Builder = lam(lambda level: lam(lambda on_var: lam(lambda on_app: app(on_var, level))))
|
|
50
|
+
_NAPP: Builder = lam(lambda neutral: lam(lambda argument: lam(lambda on_var: lam(lambda on_app: ap(on_app, neutral, argument)))))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# nth env index: the index-th environment value (de Bruijn lookup); a free variable (empty environment)
|
|
54
|
+
# reads as a neutral, so an open term is handled and a closed one never reaches it. The index is a
|
|
55
|
+
# CHURCH numeral (quote emits ``q_var(church(i))``), so it is compared with Church ``IS_ZERO``/``PRED``;
|
|
56
|
+
# only the fuel and the binder level are BinNats.
|
|
57
|
+
_NTH: Builder = app(Y, lam(lambda nth: lam(lambda env: lam(lambda index: ap(
|
|
58
|
+
env,
|
|
59
|
+
app(_VNEU, app(_NVAR, index)),
|
|
60
|
+
lam(lambda head: lam(lambda tail: ap(
|
|
61
|
+
app(IS_ZERO, index),
|
|
62
|
+
head,
|
|
63
|
+
ap(nth, tail, app(PRED, index)),
|
|
64
|
+
))),
|
|
65
|
+
)))))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# apply fuel value argument: semantic application. A VLAM fires (passing the fuel on to its body
|
|
69
|
+
# evaluation); a VNEU grows its spine; VBOTTOM stays bottom.
|
|
70
|
+
_APPLY: Builder = lam(lambda fuel: lam(lambda value: lam(lambda argument: ap(
|
|
71
|
+
value,
|
|
72
|
+
lam(lambda function: ap(function, fuel, argument)), # VLAM: invoke the fuel-aware closure
|
|
73
|
+
lam(lambda neutral: app(_VNEU, ap(_NAPP, neutral, argument))), # VNEU: extend the spine
|
|
74
|
+
_VBOTTOM, # VBOTTOM: absorbing
|
|
75
|
+
))))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# eval fuel env quoted: denote the quoted de Bruijn term as a value, threading the fuel.
|
|
79
|
+
# Argument evaluation (the second eval in the QApp case) is left lazy by the call-by-name interpreter,
|
|
80
|
+
# so a normalizing term whose strict reduction would diverge still reaches its value.
|
|
81
|
+
_EVAL: Builder = app(Y, lam(lambda eval_recursion: lam(lambda fuel: lam(lambda env: lam(lambda quoted: ap(
|
|
82
|
+
BIN_IS_ZERO, fuel,
|
|
83
|
+
_VBOTTOM,
|
|
84
|
+
ap(
|
|
85
|
+
quoted,
|
|
86
|
+
lam(lambda index: ap(_NTH, env, index)), # QVar: look up the value
|
|
87
|
+
lam(lambda body: app(_VLAM, lam(lambda inner_fuel: lam(lambda argument: ap(
|
|
88
|
+
eval_recursion, inner_fuel, ap(_CONS, argument, env), body,
|
|
89
|
+
))))), # QLam: a fuel-aware closure
|
|
90
|
+
lam(lambda function: lam(lambda argument: ap(
|
|
91
|
+
_APPLY,
|
|
92
|
+
app(BIN_PRED, fuel),
|
|
93
|
+
ap(eval_recursion, app(BIN_PRED, fuel), env, function),
|
|
94
|
+
ap(eval_recursion, app(BIN_PRED, fuel), env, argument),
|
|
95
|
+
))), # QApp: apply (the argument value stays lazy)
|
|
96
|
+
),
|
|
97
|
+
))))))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# walk_neutral walk fuel level neutral: force a neutral's spine, threading the fuel as a single budget
|
|
101
|
+
# (the argument is walked with whatever fuel the head left), so total work is linear in the fuel rather
|
|
102
|
+
# than exponential. Returns SOME remaining-fuel on completion or NONE on exhaustion.
|
|
103
|
+
_WALK_NEUTRAL: Builder = app(Y, lam(lambda walk_neutral: lam(lambda walk: lam(lambda fuel: lam(lambda level: lam(lambda neutral: ap(
|
|
104
|
+
BIN_IS_ZERO, fuel,
|
|
105
|
+
_NONE,
|
|
106
|
+
ap(
|
|
107
|
+
neutral,
|
|
108
|
+
lam(lambda variable_level: app(_SOME, app(BIN_PRED, fuel))), # NVAR: a leaf, the spine ends
|
|
109
|
+
lam(lambda inner_neutral: lam(lambda argument: ap(
|
|
110
|
+
ap(walk_neutral, walk, app(BIN_PRED, fuel), level, inner_neutral),
|
|
111
|
+
lam(lambda fuel_left: ap(walk, fuel_left, level, argument)), # head done: walk the argument
|
|
112
|
+
_NONE, # head exhausted: propagate
|
|
113
|
+
))),
|
|
114
|
+
),
|
|
115
|
+
)))))))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# walk fuel level value: force the value to its full normal form, threading the fuel as a single budget;
|
|
119
|
+
# returns SOME remaining-fuel on completion or NONE on exhaustion. Going under a VLAM applies it to a
|
|
120
|
+
# fresh neutral (NVAR level) and recurses at level+1; a VNEU walks its spine; VBOTTOM (the evaluator ran
|
|
121
|
+
# out) is not a normal form.
|
|
122
|
+
_WALK: Builder = app(Y, lam(lambda walk: lam(lambda fuel: lam(lambda level: lam(lambda value: ap(
|
|
123
|
+
BIN_IS_ZERO, fuel,
|
|
124
|
+
_NONE,
|
|
125
|
+
ap(
|
|
126
|
+
value,
|
|
127
|
+
lam(lambda function: ap(
|
|
128
|
+
walk,
|
|
129
|
+
app(BIN_PRED, fuel),
|
|
130
|
+
app(BIN_SUCC, level),
|
|
131
|
+
ap(function, app(BIN_PRED, fuel), app(_VNEU, app(_NVAR, level))),
|
|
132
|
+
)),
|
|
133
|
+
lam(lambda neutral: ap(_WALK_NEUTRAL, walk, app(BIN_PRED, fuel), level, neutral)),
|
|
134
|
+
_NONE, # VBOTTOM
|
|
135
|
+
),
|
|
136
|
+
))))))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# normalizes fuel quoted: whether walking the value of the quoted term to normal form completes within
|
|
140
|
+
# the fuel. SOME (completed) -> True (a finite normal form); NONE (exhausted) -> False (needs-fold).
|
|
141
|
+
NORMALIZES: Builder = lam(lambda fuel: lam(lambda quoted: ap(
|
|
142
|
+
ap(_WALK, fuel, BIN_ZERO, ap(_EVAL, fuel, _NIL, quoted)),
|
|
143
|
+
lam(lambda fuel_left: TRUE),
|
|
144
|
+
FALSE,
|
|
145
|
+
)))
|
co_lambda/_shape.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Weak head normalization: the structure map ``out`` of the lambda-calculus coalgebra.
|
|
2
|
+
|
|
3
|
+
``weak_head_normalize`` exposes a node's outermost constructor after weak head reduction (it stops
|
|
4
|
+
at the outermost constructor and does not reduce under ``lambda``). A deterministic calculus
|
|
5
|
+
exposes exactly one constructor at a node, so the value is a single node
|
|
6
|
+
(``Var``/``Lam``/``App``/``Native``) or ``BOTTOM`` (no constructor), never a set.
|
|
7
|
+
``compute_weak_head_normal_form`` is the per-node clause body; ``Node.weak_head_normal_form`` wraps
|
|
8
|
+
it in a ``fixpoint_cached_property`` resolved as a least fixpoint from ``BOTTOM`` upward. Because
|
|
9
|
+
nodes are interned, a node reached again during its own computation is caught by a pointer test; an
|
|
10
|
+
unproductive cycle (a re-entry with no constructor exposed, as in ``Omega`` or ``Y (lambda x. x)``)
|
|
11
|
+
stabilizes at ``BOTTOM``.
|
|
12
|
+
|
|
13
|
+
A reduction budget (a context variable) bounds beta-reduction so a genuinely non-rational reduction
|
|
14
|
+
surfaces as ``ReductionBudgetExceeded`` instead of hanging.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from contextvars import ContextVar
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Iterator, cast, final
|
|
23
|
+
|
|
24
|
+
from co_lambda._ast import (
|
|
25
|
+
BOTTOM,
|
|
26
|
+
App,
|
|
27
|
+
Lam,
|
|
28
|
+
Native,
|
|
29
|
+
Node,
|
|
30
|
+
ShapeBottom,
|
|
31
|
+
Var,
|
|
32
|
+
make_native,
|
|
33
|
+
substitute,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReductionBudgetExceeded(RuntimeError):
|
|
38
|
+
"""Raised when a bounded reduction runs out of beta-steps (a divergent term)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
@dataclass(kw_only=True, slots=True, weakref_slot=True)
|
|
43
|
+
class _Budget:
|
|
44
|
+
remaining: int = field(default=0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_reduction_budget: ContextVar[_Budget | None] = ContextVar(
|
|
48
|
+
"co_lambda._reduction_budget", default=None
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@contextmanager
|
|
53
|
+
def reduction_budget(steps: int) -> Iterator[None]:
|
|
54
|
+
"""Bound beta-reduction to ``steps`` head redexes within this context."""
|
|
55
|
+
if steps <= 0:
|
|
56
|
+
raise ValueError("reduction budget must be positive")
|
|
57
|
+
token = _reduction_budget.set(_Budget(remaining=steps))
|
|
58
|
+
try:
|
|
59
|
+
yield
|
|
60
|
+
finally:
|
|
61
|
+
_reduction_budget.reset(token)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _consume_redex() -> None:
|
|
65
|
+
budget = _reduction_budget.get()
|
|
66
|
+
if budget is None:
|
|
67
|
+
return
|
|
68
|
+
if budget.remaining <= 0:
|
|
69
|
+
raise ReductionBudgetExceeded("reduction budget exhausted")
|
|
70
|
+
budget.remaining -= 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def weak_head_normalize(node: Node) -> Node | ShapeBottom:
|
|
74
|
+
"""The weak head normal form of ``node``: its outermost constructor, or ``BOTTOM`` (none).
|
|
75
|
+
|
|
76
|
+
Typed via ``Node.weak_head_normal_form`` (a ``fixpoint_cached_property`` typed as ``object``).
|
|
77
|
+
"""
|
|
78
|
+
return cast("Node | ShapeBottom", node.weak_head_normal_form)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compute_weak_head_normal_form(node: Node) -> Node | ShapeBottom:
|
|
82
|
+
"""The per-node clause body of weak head normalization; single-valued, no aggregate."""
|
|
83
|
+
match node:
|
|
84
|
+
case Var():
|
|
85
|
+
return node
|
|
86
|
+
case Lam():
|
|
87
|
+
return node
|
|
88
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
89
|
+
if len(collected) == arity:
|
|
90
|
+
return weak_head_normalize(run(*collected))
|
|
91
|
+
return node
|
|
92
|
+
case App(function=function, argument=argument):
|
|
93
|
+
head = weak_head_normalize(function)
|
|
94
|
+
match head:
|
|
95
|
+
case Lam(body=lambda_body):
|
|
96
|
+
_consume_redex()
|
|
97
|
+
return weak_head_normalize(substitute(lambda_body, depth=0, argument=argument))
|
|
98
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
99
|
+
saturated = (*collected, argument)
|
|
100
|
+
if len(saturated) == arity:
|
|
101
|
+
return weak_head_normalize(run(*saturated))
|
|
102
|
+
return make_native(run, arity, saturated)
|
|
103
|
+
case Var() | App():
|
|
104
|
+
return node
|
|
105
|
+
case ShapeBottom.BOTTOM:
|
|
106
|
+
return BOTTOM
|
|
107
|
+
case _:
|
|
108
|
+
raise TypeError(f"Unknown head {head!r}")
|
|
109
|
+
case _:
|
|
110
|
+
raise TypeError(f"Unknown node {node!r}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def head_normalize(node: Node) -> Node | ShapeBottom:
|
|
114
|
+
"""The head normal form of ``node`` (the Boehm reading): its outermost constructor after head
|
|
115
|
+
reduction, which reduces under ``lambda`` to expose the head, or ``BOTTOM`` (no head normal form).
|
|
116
|
+
|
|
117
|
+
Typed via ``Node.head_normal_form`` (a ``fixpoint_cached_property`` typed as ``object``).
|
|
118
|
+
"""
|
|
119
|
+
return cast("Node | ShapeBottom", node.head_normal_form)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def compute_head_normal_form(node: Node) -> Node | ShapeBottom:
|
|
123
|
+
"""The per-node clause body of head normalization (the Boehm reading).
|
|
124
|
+
|
|
125
|
+
The only difference from weak head normalization is the ``Lam`` clause: a ``lambda`` whose body
|
|
126
|
+
has no head normal form is itself meaningless (``BOTTOM``), because head reduction continues under
|
|
127
|
+
the ``lambda``. The ``App`` clause is identical (a head redex fires on the weak head of the
|
|
128
|
+
function, whether or not its body has a head normal form).
|
|
129
|
+
"""
|
|
130
|
+
match node:
|
|
131
|
+
case Var():
|
|
132
|
+
return node
|
|
133
|
+
case Lam(body=body):
|
|
134
|
+
if head_normalize(body) is BOTTOM:
|
|
135
|
+
return BOTTOM
|
|
136
|
+
return node
|
|
137
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
138
|
+
if len(collected) == arity:
|
|
139
|
+
return head_normalize(run(*collected))
|
|
140
|
+
return node
|
|
141
|
+
case App(function=function, argument=argument):
|
|
142
|
+
head = weak_head_normalize(function)
|
|
143
|
+
match head:
|
|
144
|
+
case Lam(body=lambda_body):
|
|
145
|
+
_consume_redex()
|
|
146
|
+
return head_normalize(substitute(lambda_body, depth=0, argument=argument))
|
|
147
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
148
|
+
saturated = (*collected, argument)
|
|
149
|
+
if len(saturated) == arity:
|
|
150
|
+
return head_normalize(run(*saturated))
|
|
151
|
+
return make_native(run, arity, saturated)
|
|
152
|
+
case Var() | App():
|
|
153
|
+
return node
|
|
154
|
+
case ShapeBottom.BOTTOM:
|
|
155
|
+
return BOTTOM
|
|
156
|
+
case _:
|
|
157
|
+
raise TypeError(f"Unknown head {head!r}")
|
|
158
|
+
case _:
|
|
159
|
+
raise TypeError(f"Unknown node {node!r}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def normalize_to_depth(node: Node, depth: int) -> Node | ShapeBottom:
|
|
163
|
+
"""Depth-bounded call-by-name beta normalization: the compiler's reference semantics.
|
|
164
|
+
|
|
165
|
+
This is the fusion of weak head normalization and combinatory (SK) reduction. From weak head
|
|
166
|
+
normalization it keeps call-by-name beta, where the argument is inserted by reference (not
|
|
167
|
+
reduced), so the caller's tabling folds both an unproductive cycle (``Omega`` to bottom) and a
|
|
168
|
+
productive one (``Y (cons 0)`` to a finite cyclic graph); a fully lazy SK reduction cannot fold,
|
|
169
|
+
because its ``S`` rule grows the argument into suspensions that never re-form the cyclic node, and
|
|
170
|
+
reducing them first (call by value) diverges on a productive cycle. From combinatory reduction it
|
|
171
|
+
keeps the motivation to avoid copying the whole tree: it fires at most ``depth`` beta contractions
|
|
172
|
+
per application position and leaves a still-unfired redex ``App(Lam(body), argument)`` (the
|
|
173
|
+
let-stub ``(\\a. body) argument``) as a guarded value, rather than substituting an unbounded tree.
|
|
174
|
+
|
|
175
|
+
A ``depth`` large enough reproduces the Levy-Longo weak head normal form, ``depth == 1`` is the
|
|
176
|
+
one-layer reading, and ``depth == 0`` reads the term raw. It never consults the cached
|
|
177
|
+
``weak_head_normal_form`` (the unbounded least fixpoint), so the cached semantics is untouched;
|
|
178
|
+
each contraction goes through ``substitute`` (which returns closed subterms by reference and builds
|
|
179
|
+
with the interning ``make_*``), so closed cyclic data is shared and structurally identical results
|
|
180
|
+
fold at every reduced layer, the per-layer tabling guarantee that lets a cycle closing within the
|
|
181
|
+
depth fold and halt. ``depth`` bounds firings, so every head reduction terminates regardless of
|
|
182
|
+
rationality; the readout's tree is folded by the caller (``render`` tables re-entrant closed
|
|
183
|
+
nodes), so a rational behaviour whose cycle closes within the depth reads as a finite cyclic graph.
|
|
184
|
+
"""
|
|
185
|
+
match node:
|
|
186
|
+
case Var():
|
|
187
|
+
return node
|
|
188
|
+
case Lam():
|
|
189
|
+
return node
|
|
190
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
191
|
+
if len(collected) == arity:
|
|
192
|
+
return normalize_to_depth(run(*collected), depth)
|
|
193
|
+
return node
|
|
194
|
+
case App(function=function, argument=argument):
|
|
195
|
+
head = normalize_to_depth(function, depth)
|
|
196
|
+
match head:
|
|
197
|
+
case Lam(body=lambda_body):
|
|
198
|
+
if depth <= 0:
|
|
199
|
+
return node
|
|
200
|
+
fired = substitute(lambda_body, depth=0, argument=argument)
|
|
201
|
+
return normalize_to_depth(fired, depth - 1)
|
|
202
|
+
case Native(run=run, arity=arity, collected=collected):
|
|
203
|
+
saturated = (*collected, argument)
|
|
204
|
+
if len(saturated) == arity:
|
|
205
|
+
return normalize_to_depth(run(*saturated), depth)
|
|
206
|
+
return make_native(run, arity, saturated)
|
|
207
|
+
case Var() | App():
|
|
208
|
+
return node
|
|
209
|
+
case ShapeBottom.BOTTOM:
|
|
210
|
+
return BOTTOM
|
|
211
|
+
case _:
|
|
212
|
+
raise TypeError(f"Unknown head {head!r}")
|
|
213
|
+
case _:
|
|
214
|
+
raise TypeError(f"Unknown node {node!r}")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def one_layer_normalize(node: Node) -> Node | ShapeBottom:
|
|
218
|
+
"""The one-layer-beta structure map: one contraction per application position (``depth == 1``).
|
|
219
|
+
|
|
220
|
+
A distinct denotational variant beside ``weak_head_normalize`` (Levy-Longo) and ``head_normalize``
|
|
221
|
+
(Boehm): where weak head normalization fires the head spine to a constructor, this fires a single
|
|
222
|
+
redex per position and leaves any remaining redex as a guarded let-stub.
|
|
223
|
+
"""
|
|
224
|
+
return normalize_to_depth(node, 1)
|