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/_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)