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 +1 -0
- tablambda/_analysis.py +92 -0
- tablambda/_ast.py +289 -0
- tablambda/_binnat.py +170 -0
- tablambda/_codec.py +147 -0
- tablambda/_compiler_artifact.py +50 -0
- tablambda/_defun_codegen.py +324 -0
- tablambda/_defun_runtime.py +207 -0
- tablambda/_defunctionalize.py +455 -0
- tablambda/_dsl.py +148 -0
- tablambda/_generated/.gitattributes +5 -0
- tablambda/_generated/__init__.py +1 -0
- tablambda/_generated/_generated_defun_compiler_py311.py +7579 -0
- tablambda/_generated/_generated_defun_compiler_py312.py +7579 -0
- tablambda/_generated/_generated_defun_compiler_py313.py +7579 -0
- tablambda/_hoas_latex.py +144 -0
- tablambda/_latex.py +56 -0
- tablambda/_prelude.py +163 -0
- tablambda/_pyast.py +416 -0
- tablambda/_pybuild.py +315 -0
- tablambda/_reduce.py +145 -0
- tablambda/_shape.py +129 -0
- tablambda/_sugar.py +74 -0
- tablambda/_typecheck.py +370 -0
- tablambda-0.6.0.post30.dev0.dist-info/METADATA +45 -0
- tablambda-0.6.0.post30.dev0.dist-info/RECORD +28 -0
- tablambda-0.6.0.post30.dev0.dist-info/WHEEL +4 -0
- tablambda-0.6.0.post30.dev0.dist-info/entry_points.txt +2 -0
tablambda/_pybuild.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
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 tablambda._codec import char_codes, church
|
|
21
|
+
from tablambda._dsl import Builder, app, lam
|
|
22
|
+
from tablambda._prelude import SCOTT_NIL
|
|
23
|
+
from tablambda._sugar import ap, cons, map_list, one, two
|
|
24
|
+
from tablambda._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(
|
|
293
|
+
name_field: Builder, bases_fields: Builder, decorator_fields: Builder, body_fields: Builder
|
|
294
|
+
) -> Builder:
|
|
295
|
+
"""``class <name>(<bases>): <body>`` with decorators, no keywords/type_params.
|
|
296
|
+
|
|
297
|
+
Field order follows ``ast.ClassDef._fields`` on the running Python version.
|
|
298
|
+
"""
|
|
299
|
+
by_name = {
|
|
300
|
+
"name": name_field,
|
|
301
|
+
"bases": field_list(bases_fields),
|
|
302
|
+
"keywords": field_list(SCOTT_NIL),
|
|
303
|
+
"body": field_list(body_fields),
|
|
304
|
+
"decorator_list": field_list(decorator_fields),
|
|
305
|
+
"type_params": field_list(SCOTT_NIL),
|
|
306
|
+
}
|
|
307
|
+
return _node(ast.ClassDef, [by_name[name] for name in ast.ClassDef._fields])
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def py_annassign(target: Builder, annotation: Builder) -> Builder:
|
|
311
|
+
"""``<target>: <annotation>`` with no value, ``simple=1``.
|
|
312
|
+
|
|
313
|
+
Order: target, annotation, value, simple.
|
|
314
|
+
"""
|
|
315
|
+
return _node(ast.AnnAssign, [field_node(target), field_node(annotation), field_none(), field_int(church(1))])
|
tablambda/_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 tablambda._binnat import BIN_IS_ZERO, BIN_PRED, BIN_SUCC, BIN_ZERO
|
|
29
|
+
from tablambda._dsl import Builder, app, lam
|
|
30
|
+
from tablambda._prelude import FALSE, IS_ZERO, PRED, TRUE, Y
|
|
31
|
+
from tablambda._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
|
+
)))
|
tablambda/_shape.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Weak head normalization: a term's outermost constructor, computed as a least fixpoint by tabling.
|
|
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 (an interpreter
|
|
6
|
+
``Var``/``Lam``/``App`` or a compiled ``Closure``) or ``BOTTOM`` (no constructor), never a set. A
|
|
7
|
+
compiled ``Thunk`` (a redex) forces through ``apply_value``, the application path shared with ``App``.
|
|
8
|
+
``compute_weak_head_normal_form`` is the per-node clause body; ``Node.weak_head_normal_form`` wraps
|
|
9
|
+
it in a ``fixpoint_cached_property`` resolved as a least fixpoint from ``BOTTOM`` upward. Because
|
|
10
|
+
nodes are interned, a node reached again during its own computation is caught by a pointer test; an
|
|
11
|
+
unproductive cycle (a re-entry with no constructor exposed, as in ``Omega`` or ``Y (lambda x. x)``)
|
|
12
|
+
stabilizes at ``BOTTOM``.
|
|
13
|
+
|
|
14
|
+
A reduction budget (a context variable) bounds beta-reduction so a genuinely non-rational reduction
|
|
15
|
+
surfaces as ``ReductionBudgetExceeded`` instead of hanging.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from contextvars import ContextVar
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Iterator, cast, final
|
|
24
|
+
|
|
25
|
+
from tablambda._ast import (
|
|
26
|
+
BOTTOM,
|
|
27
|
+
App,
|
|
28
|
+
Lam,
|
|
29
|
+
Node,
|
|
30
|
+
WeakHeadBottom,
|
|
31
|
+
Var,
|
|
32
|
+
make_app,
|
|
33
|
+
substitute,
|
|
34
|
+
)
|
|
35
|
+
from tablambda._defun_runtime import Closure, Thunk
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ReductionBudgetExceeded(RuntimeError):
|
|
39
|
+
"""Raised when a bounded reduction runs out of beta-steps (a divergent term)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
@dataclass(kw_only=True, slots=True, weakref_slot=True)
|
|
44
|
+
class _Budget:
|
|
45
|
+
remaining: int = field(default=0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_reduction_budget: ContextVar[_Budget | None] = ContextVar(
|
|
49
|
+
"tablambda._reduction_budget", default=None
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def reduction_budget(steps: int) -> Iterator[None]:
|
|
55
|
+
"""Bound beta-reduction to ``steps`` head redexes within this context."""
|
|
56
|
+
if steps <= 0:
|
|
57
|
+
raise ValueError("reduction budget must be positive")
|
|
58
|
+
token = _reduction_budget.set(_Budget(remaining=steps))
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
_reduction_budget.reset(token)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _consume_redex() -> None:
|
|
66
|
+
budget = _reduction_budget.get()
|
|
67
|
+
if budget is None:
|
|
68
|
+
return
|
|
69
|
+
if budget.remaining <= 0:
|
|
70
|
+
raise ReductionBudgetExceeded("reduction budget exhausted")
|
|
71
|
+
budget.remaining -= 1
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def weak_head_normalize(node: Node) -> Node | WeakHeadBottom:
|
|
75
|
+
"""The weak head normal form of ``node``: its outermost constructor, or ``BOTTOM`` (none).
|
|
76
|
+
|
|
77
|
+
Typed via ``Node.weak_head_normal_form`` (a ``fixpoint_cached_property`` typed as ``object``).
|
|
78
|
+
"""
|
|
79
|
+
return cast("Node | WeakHeadBottom", node.weak_head_normal_form)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def apply_value(head: Node, argument: Node) -> Node | WeakHeadBottom:
|
|
83
|
+
"""Apply a weak-head VALUE ``head`` to ``argument``, returning the node to continue normalizing
|
|
84
|
+
(or ``BOTTOM``). ``head`` must already be in weak head normal form, so it is never a ``Thunk``.
|
|
85
|
+
|
|
86
|
+
This is the one application path shared by the interpreter's ``App`` clause and the compiled
|
|
87
|
+
``Thunk.force``, so an interpreter ``Lam`` and a compiled ``Closure`` apply uniformly and the two
|
|
88
|
+
worlds run mixed: firing a ``Lam`` substitutes (consuming the reduction budget), calling a
|
|
89
|
+
``Closure`` runs the compiled body, and a neutral head builds the application node.
|
|
90
|
+
"""
|
|
91
|
+
match head:
|
|
92
|
+
case Lam(body=lambda_body):
|
|
93
|
+
_consume_redex()
|
|
94
|
+
return substitute(lambda_body, depth=0, argument=argument)
|
|
95
|
+
case Closure():
|
|
96
|
+
return head(argument)
|
|
97
|
+
case Var() | App():
|
|
98
|
+
return make_app(head, argument)
|
|
99
|
+
case WeakHeadBottom.BOTTOM:
|
|
100
|
+
return BOTTOM
|
|
101
|
+
case _:
|
|
102
|
+
raise TypeError(f"Cannot apply non-value {head!r}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def compute_weak_head_normal_form(node: Node) -> Node | WeakHeadBottom:
|
|
106
|
+
"""The per-node clause body of weak head normalization; single-valued, no aggregate."""
|
|
107
|
+
match node:
|
|
108
|
+
case Var():
|
|
109
|
+
return node
|
|
110
|
+
case Lam():
|
|
111
|
+
return node
|
|
112
|
+
case Closure():
|
|
113
|
+
return node
|
|
114
|
+
case Thunk():
|
|
115
|
+
return node.force()
|
|
116
|
+
case App(function=function, argument=argument):
|
|
117
|
+
head = weak_head_normalize(function)
|
|
118
|
+
match head:
|
|
119
|
+
case Var() | App():
|
|
120
|
+
return node
|
|
121
|
+
case WeakHeadBottom.BOTTOM:
|
|
122
|
+
return BOTTOM
|
|
123
|
+
case _:
|
|
124
|
+
applied = apply_value(head, argument)
|
|
125
|
+
if applied is BOTTOM:
|
|
126
|
+
return BOTTOM
|
|
127
|
+
return weak_head_normalize(applied)
|
|
128
|
+
case _:
|
|
129
|
+
raise TypeError(f"Unknown node {node!r}")
|
tablambda/_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 tablambda._dsl import Builder, app, lam
|
|
14
|
+
from tablambda._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)
|