co-lambda 0.5.0.post1.dev0__tar.gz

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.
Files changed (36) hide show
  1. co_lambda-0.5.0.post1.dev0/.gitignore +247 -0
  2. co_lambda-0.5.0.post1.dev0/PKG-INFO +84 -0
  3. co_lambda-0.5.0.post1.dev0/README.md +68 -0
  4. co_lambda-0.5.0.post1.dev0/pyproject.toml +43 -0
  5. co_lambda-0.5.0.post1.dev0/src/co_lambda/__init__.py +1 -0
  6. co_lambda-0.5.0.post1.dev0/src/co_lambda/_analysis.py +92 -0
  7. co_lambda-0.5.0.post1.dev0/src/co_lambda/_ast.py +372 -0
  8. co_lambda-0.5.0.post1.dev0/src/co_lambda/_binnat.py +170 -0
  9. co_lambda-0.5.0.post1.dev0/src/co_lambda/_codec.py +147 -0
  10. co_lambda-0.5.0.post1.dev0/src/co_lambda/_compiler_artifact.py +50 -0
  11. co_lambda-0.5.0.post1.dev0/src/co_lambda/_defun_codegen.py +321 -0
  12. co_lambda-0.5.0.post1.dev0/src/co_lambda/_defun_runtime.py +188 -0
  13. co_lambda-0.5.0.post1.dev0/src/co_lambda/_defunctionalize.py +470 -0
  14. co_lambda-0.5.0.post1.dev0/src/co_lambda/_dsl.py +148 -0
  15. co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/.gitattributes +5 -0
  16. co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/__init__.py +1 -0
  17. co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py311.py +7586 -0
  18. co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py312.py +7586 -0
  19. co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py313.py +7586 -0
  20. co_lambda-0.5.0.post1.dev0/src/co_lambda/_hoas_latex.py +144 -0
  21. co_lambda-0.5.0.post1.dev0/src/co_lambda/_latex.py +58 -0
  22. co_lambda-0.5.0.post1.dev0/src/co_lambda/_prelude.py +163 -0
  23. co_lambda-0.5.0.post1.dev0/src/co_lambda/_pyast.py +418 -0
  24. co_lambda-0.5.0.post1.dev0/src/co_lambda/_pybuild.py +313 -0
  25. co_lambda-0.5.0.post1.dev0/src/co_lambda/_reduce.py +145 -0
  26. co_lambda-0.5.0.post1.dev0/src/co_lambda/_shape.py +224 -0
  27. co_lambda-0.5.0.post1.dev0/src/co_lambda/_sugar.py +74 -0
  28. co_lambda-0.5.0.post1.dev0/src/co_lambda/_typecheck.py +370 -0
  29. co_lambda-0.5.0.post1.dev0/tests/__snapshots__/test_binnat.ambr +8 -0
  30. co_lambda-0.5.0.post1.dev0/tests/__snapshots__/test_defunctionalize.ambr +76 -0
  31. co_lambda-0.5.0.post1.dev0/tests/test_arithmetic.py +82 -0
  32. co_lambda-0.5.0.post1.dev0/tests/test_binnat.py +81 -0
  33. co_lambda-0.5.0.post1.dev0/tests/test_defunctionalize.py +243 -0
  34. co_lambda-0.5.0.post1.dev0/tests/test_hoas_latex.py +30 -0
  35. co_lambda-0.5.0.post1.dev0/tests/test_pyast.py +69 -0
  36. co_lambda-0.5.0.post1.dev0/tests/test_pybuild.py +108 -0
@@ -0,0 +1,247 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ **/docs/_build/
73
+ **/docs/api/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+ .jupyter_ystore.db
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+ #poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ #pdm.lock
118
+ #pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ #pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .venv
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ #.idea/
178
+
179
+ # Abstra
180
+ # Abstra is an AI-powered process automation framework.
181
+ # Ignore directories containing user credentials, local state, and settings.
182
+ # Learn more at https://abstra.io/docs
183
+ .abstra/
184
+
185
+ # Visual Studio Code
186
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
187
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
188
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
189
+ # you could uncomment the following to ignore the entire vscode folder
190
+ # .vscode/
191
+
192
+ # Ruff stuff:
193
+ .ruff_cache/
194
+
195
+ # PyPI configuration file
196
+ .pypirc
197
+
198
+ # Cursor
199
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
200
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
201
+ # refer to https://docs.cursor.com/context/ignore-files
202
+ .cursorignore
203
+ .cursorindexingignore
204
+
205
+ # Marimo
206
+ marimo/_static/
207
+ marimo/_lsp/
208
+ __marimo__/
209
+
210
+ .direnv/
211
+ .devenv/
212
+ result
213
+
214
+ # devenv/nixago scaffolding regenerated by running `nix develop` in a paper subdirectory
215
+ # (the canonical copies live at the repository root)
216
+ /papers/co-lambda/.envrc
217
+ /papers/co-lambda/.gitattributes
218
+ /papers/co-lambda/.vscode/
219
+
220
+ # LaTeX
221
+ *.pdf
222
+ *.aux
223
+ *.fls
224
+ *.fdb_latexmk
225
+ *.synctex.gz
226
+ *.bbl
227
+ *.blg
228
+ *.out
229
+ *.dvi
230
+ *.xcp
231
+
232
+ # Local data
233
+ data/
234
+ trajectory/
235
+ experiment_results.db
236
+
237
+ .playwright-mcp/
238
+ *.local.*
239
+ .envrc.private
240
+ .pre-commit-config.yaml
241
+
242
+ # nixago: ignore-linked-files
243
+ /.vscode/extensions.json
244
+
245
+ arxiv-submission.tar.gz
246
+ node_modules/
247
+ comment.cut
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: co-lambda
3
+ Version: 0.5.0.post1.dev0
4
+ Summary: A least-fixpoint first-order-shape-relation interpreter for the lambda-calculus
5
+ Project-URL: Repository, https://github.com/Atry/MIXINv2
6
+ Author-email: "Yang, Bo" <yang-bo@yang-bo.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: fixpoints
14
+ Requires-Dist: typing-extensions>=4.1.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # co_lambda
18
+
19
+ A first-order-shape-relation interpreter for the lambda-calculus, realizing the
20
+ semantics of the paper `papers/co-lambda/first-order.tex`, depending on `fixpoints`.
21
+
22
+ A lambda-term's tree is the readout of a single first-order weak-head **shape relation** `Sh`
23
+ over term positions. The shape at a position is single-valued, so there is no set to aggregate.
24
+ `readout(node)` resolves each position's head via its `Sh` and descends.
25
+
26
+ Because positions are **interned** (structurally-equal positions are one object, identity is a
27
+ pointer test), a cyclic structure has finitely many positions and the least-fixpoint reading
28
+ folds it into a finite rational tree where head reduction would unfold forever. So the readout
29
+ terminates on every rational tree, and decides an unproductive cycle as the meaningless leaf in
30
+ finite time, where head reduction diverges. Interning is the *finest* instance of a pluggable
31
+ **position congruence** (see below); a coarser sound congruence folds more.
32
+
33
+ `readout` has two re-entry policies:
34
+
35
+ - `fold_cycles=True` (default) is the least fixpoint `lfp`, the denotation: a guarded cycle
36
+ folds into a finite rational graph (`render` prints it with `#N` back-references); the only
37
+ leaves are variables and the meaningless `⊥`.
38
+ - `fold_cycles=False` is the finite-budget first-iteration reading `T↑1`: a re-entered guarded
39
+ cycle is cut to the distinct guarded-cut leaf `∅` (the hole where the budget stopped on a
40
+ productive cycle), kept separate from the meaningless `⊥` (an unproductive cycle, a position
41
+ with no shape). `∅` never appears in the least fixpoint.
42
+
43
+ The calculus is **pure** (`Var`/`Lam`/`App`): no recursion binder is needed. The `Y` combinator
44
+ produces the structural repetition that interning folds, so:
45
+
46
+ - `Y (cons 0)` (the cyclic stream `r = cons 0 r`) folds to a finite rational tree.
47
+ - `Ω = (λx.xx)(λx.xx)` and `Y (λx.x)` (i.e. `letrec x = x`) are unproductive cycles: they read
48
+ out as `⊥` under both readings.
49
+
50
+ The fold/cut is taken only at **closed** positions, so a folded back-reference never misreads a
51
+ free de Bruijn variable.
52
+
53
+ ## Position congruence (a second parameter)
54
+
55
+ *Which* positions count as "the same" when the readout folds is a parameter, a **position
56
+ congruence** (`Definition def:congruence` in the paper). `readout(node, congruence=...)` keys the
57
+ fold on `congruence.key(node)` instead of raw object identity. A congruence is **sound** when it
58
+ is contained in tree equality (it never folds positions with different trees); stated over the
59
+ full signature, soundness is the congruence law read coinductively, so a well-formed congruence
60
+ folds without changing the denotation. `_congruence.py` provides four instances, from finest to
61
+ coarser:
62
+
63
+ - `IdentityCongruence` (the default): syntactic de Bruijn identity, `key = id(node)`, a pointer
64
+ test. The finest instance; reproduces the pure-interning readout exactly.
65
+ - `EGraphCongruence`: a union-find with congruence closure over the *syntactic* constructors. The
66
+ caller asserts sound (tree-equal) merges with `merge(a, b)`; closure propagates them to
67
+ `App`/`Lam` parents, and the readout shares the merged positions. The inductive
68
+ (least-fixpoint) family: it folds whatever finitely many tree-equal pairs generate.
69
+ - `PositionEGraphCongruence`: the faithful `def:congruence`, keyed on the *demanded descent* (the
70
+ shape tree) rather than the syntax. It auto-folds any two positions bisimilar under `Sh` with
71
+ no asserted merge (a redex and its reduct collapse on sight), folding exactly the rational
72
+ fragment. It cannot finitize an infinitely-presented shape graph, so the `Y F 0` witness below
73
+ still diverges: bisimulation alone does not rescue a dead-argument cycle.
74
+ - `DeadSubtermCongruence(rules=...)`: equality up to dead subterms, the key being the syntax with
75
+ every dead-argument slot erased to a canonical placeholder (a tree-preserving map, not a
76
+ congruence closure). The dead slots are recognised by a **library of sound rules**; the caller
77
+ enables a subset. `RecursionArgumentRule` folds the paper's `Y F 0` witness, where a
78
+ constant-headed recursion carries an index it never inspects; `UnusedParameterRule` erases the
79
+ argument of a function that discards it. This is the one reading that folds the witness, which
80
+ neither e-graph can.
81
+
82
+ No parser is provided. Build terms in Python with the HOAS DSL in `_dsl.py` (`lam`, `app`,
83
+ `build`), which compiles to a first-order de Bruijn AST; `_prelude.py` collects example terms
84
+ (combinators, Scott-encoded lists, Church numerals with Peano arithmetic).
@@ -0,0 +1,68 @@
1
+ # co_lambda
2
+
3
+ A first-order-shape-relation interpreter for the lambda-calculus, realizing the
4
+ semantics of the paper `papers/co-lambda/first-order.tex`, depending on `fixpoints`.
5
+
6
+ A lambda-term's tree is the readout of a single first-order weak-head **shape relation** `Sh`
7
+ over term positions. The shape at a position is single-valued, so there is no set to aggregate.
8
+ `readout(node)` resolves each position's head via its `Sh` and descends.
9
+
10
+ Because positions are **interned** (structurally-equal positions are one object, identity is a
11
+ pointer test), a cyclic structure has finitely many positions and the least-fixpoint reading
12
+ folds it into a finite rational tree where head reduction would unfold forever. So the readout
13
+ terminates on every rational tree, and decides an unproductive cycle as the meaningless leaf in
14
+ finite time, where head reduction diverges. Interning is the *finest* instance of a pluggable
15
+ **position congruence** (see below); a coarser sound congruence folds more.
16
+
17
+ `readout` has two re-entry policies:
18
+
19
+ - `fold_cycles=True` (default) is the least fixpoint `lfp`, the denotation: a guarded cycle
20
+ folds into a finite rational graph (`render` prints it with `#N` back-references); the only
21
+ leaves are variables and the meaningless `⊥`.
22
+ - `fold_cycles=False` is the finite-budget first-iteration reading `T↑1`: a re-entered guarded
23
+ cycle is cut to the distinct guarded-cut leaf `∅` (the hole where the budget stopped on a
24
+ productive cycle), kept separate from the meaningless `⊥` (an unproductive cycle, a position
25
+ with no shape). `∅` never appears in the least fixpoint.
26
+
27
+ The calculus is **pure** (`Var`/`Lam`/`App`): no recursion binder is needed. The `Y` combinator
28
+ produces the structural repetition that interning folds, so:
29
+
30
+ - `Y (cons 0)` (the cyclic stream `r = cons 0 r`) folds to a finite rational tree.
31
+ - `Ω = (λx.xx)(λx.xx)` and `Y (λx.x)` (i.e. `letrec x = x`) are unproductive cycles: they read
32
+ out as `⊥` under both readings.
33
+
34
+ The fold/cut is taken only at **closed** positions, so a folded back-reference never misreads a
35
+ free de Bruijn variable.
36
+
37
+ ## Position congruence (a second parameter)
38
+
39
+ *Which* positions count as "the same" when the readout folds is a parameter, a **position
40
+ congruence** (`Definition def:congruence` in the paper). `readout(node, congruence=...)` keys the
41
+ fold on `congruence.key(node)` instead of raw object identity. A congruence is **sound** when it
42
+ is contained in tree equality (it never folds positions with different trees); stated over the
43
+ full signature, soundness is the congruence law read coinductively, so a well-formed congruence
44
+ folds without changing the denotation. `_congruence.py` provides four instances, from finest to
45
+ coarser:
46
+
47
+ - `IdentityCongruence` (the default): syntactic de Bruijn identity, `key = id(node)`, a pointer
48
+ test. The finest instance; reproduces the pure-interning readout exactly.
49
+ - `EGraphCongruence`: a union-find with congruence closure over the *syntactic* constructors. The
50
+ caller asserts sound (tree-equal) merges with `merge(a, b)`; closure propagates them to
51
+ `App`/`Lam` parents, and the readout shares the merged positions. The inductive
52
+ (least-fixpoint) family: it folds whatever finitely many tree-equal pairs generate.
53
+ - `PositionEGraphCongruence`: the faithful `def:congruence`, keyed on the *demanded descent* (the
54
+ shape tree) rather than the syntax. It auto-folds any two positions bisimilar under `Sh` with
55
+ no asserted merge (a redex and its reduct collapse on sight), folding exactly the rational
56
+ fragment. It cannot finitize an infinitely-presented shape graph, so the `Y F 0` witness below
57
+ still diverges: bisimulation alone does not rescue a dead-argument cycle.
58
+ - `DeadSubtermCongruence(rules=...)`: equality up to dead subterms, the key being the syntax with
59
+ every dead-argument slot erased to a canonical placeholder (a tree-preserving map, not a
60
+ congruence closure). The dead slots are recognised by a **library of sound rules**; the caller
61
+ enables a subset. `RecursionArgumentRule` folds the paper's `Y F 0` witness, where a
62
+ constant-headed recursion carries an index it never inspects; `UnusedParameterRule` erases the
63
+ argument of a function that discards it. This is the one reading that folds the witness, which
64
+ neither e-graph can.
65
+
66
+ No parser is provided. Build terms in Python with the HOAS DSL in `_dsl.py` (`lam`, `app`,
67
+ `build`), which compiles to a first-order de Bruijn AST; `_prelude.py` collects example terms
68
+ (combinators, Scott-encoded lists, Church numerals with Peano arithmetic).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling", "uv-dynamic-versioning>=0.7.0", "editables"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.version]
6
+ source = "uv-dynamic-versioning"
7
+
8
+ [tool.uv-dynamic-versioning]
9
+ vcs = "git"
10
+ style = "pep440"
11
+ bump = false
12
+ fallback-version = "0.0.0.dev0"
13
+ metadata = false
14
+
15
+ [project]
16
+ name = "co-lambda"
17
+ dynamic = ["version"]
18
+ description = "A least-fixpoint first-order-shape-relation interpreter for the lambda-calculus"
19
+ readme = "README.md"
20
+ license = "MIT"
21
+ requires-python = ">=3.11"
22
+ authors = [{ name = "Yang, Bo", email = "yang-bo@yang-bo.com" }]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ ]
29
+ dependencies = ["fixpoints", "typing-extensions>=4.1.0"]
30
+
31
+ [project.scripts]
32
+ co-lambda-regen-compiler = "co_lambda._compiler_artifact:main"
33
+
34
+ [tool.uv]
35
+ sources = { fixpoints = { workspace = true } }
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/co_lambda"]
39
+ only-include = ["src/co_lambda"]
40
+ sources = {"src" = ""}
41
+
42
+ [project.urls]
43
+ Repository = "https://github.com/Atry/MIXINv2"
@@ -0,0 +1 @@
1
+ """A least-fixpoint first-order-shape-relation interpreter for the lambda-calculus."""
@@ -0,0 +1,92 @@
1
+ """Specialization analysis written in the lambda-calculus itself.
2
+
3
+ The analysis that decides which sub-terms to specialize is a pure lambda term, run by the
4
+ interpreter on the quoted program, so the calculus analyzes its own programs: a demonstration that
5
+ tabling-based reduction expresses program analysis, not only evaluation. This module holds the
6
+ closedness and depth measures; richer certificates (typability, fuel-bounded normalization) live in
7
+ ``_typecheck`` and ``_reduce`` in the same style.
8
+
9
+ This module is pure lambda calculus: every top-level binding is a ``Builder`` (a ``@curry``-decorated
10
+ ``def`` IS a Builder). The Python-side verdict readers live at the boundary (``_specialize``).
11
+
12
+ ``LOOSE_BOUND`` is a DEPTH-FREE closedness measure, so the interpreter's interning shares it across
13
+ every position: ``LOOSE_BOUND quoted`` takes no binder depth, so ``app(LOOSE_BOUND, sub)`` is the
14
+ SAME node for an interned sub-term and is tabled once; a whole-tree scan is then linear. It returns
15
+ the number of enclosing binders the sub-term needs (the de Bruijn ``loose_bound``): a variable needs
16
+ index+1, an abstraction discharges one (floored at zero by ``PRED``), an application needs the larger
17
+ of the two. A sub-term is closed exactly when it needs none (``IS_CLOSED``).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from co_lambda._dsl import Builder, app, curry, lam
23
+ from co_lambda._prelude import IS_ZERO, PLUS, PRED, SUCC, Y
24
+ from co_lambda._sugar import ap
25
+
26
+ # Church arithmetic for the measures (truncated subtraction gives the comparisons).
27
+ _SUBTRACT: Builder = lam(lambda a: lam(lambda b: app(app(b, PRED), a))) # a - b, floored at zero
28
+ _AT_MOST: Builder = lam(lambda a: lam(lambda b: app(IS_ZERO, ap(_SUBTRACT, a, b)))) # a <= b
29
+ _MAX: Builder = lam(lambda a: lam(lambda b: ap(_AT_MOST, a, b, b, a))) # a <= b ? b : a
30
+
31
+ LOOSE_BOUND: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
32
+ quoted,
33
+ lam(lambda index: app(SUCC, index)), # QVar index: needs index+1 enclosing binders
34
+ lam(lambda body: app(PRED, app(self_recursion, body))), # QLam body: discharges one binder
35
+ lam(lambda function: lam(lambda argument: ap(
36
+ _MAX, app(self_recursion, function), app(self_recursion, argument),
37
+ ))), # QApp f a: the larger of the two
38
+ ))))
39
+
40
+ IS_CLOSED: Builder = lam(lambda quoted: app(IS_ZERO, app(LOOSE_BOUND, quoted))) # closed iff needs none
41
+
42
+
43
+ # DEPTH: the nesting depth of a quoted term (a Church numeral), a cheap path-free measure the interner
44
+ # shares per distinct sub-term. It bounds the simple-typability check: running algorithm-W on a large
45
+ # (deep) closed combinator is expensive and the no-GC interner retains every reduction, so a specializer
46
+ # only certifies an island when the sub-term is shallow enough (``depth_at_most``), leaving a deep closed
47
+ # region reconstructed as an interpreted graph rather than flattened to a strict island. The bound only
48
+ # ever makes the certificate MORE conservative (fewer islands), never unsound.
49
+ DEPTH: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
50
+ quoted,
51
+ lam(lambda index: lam(lambda s: lam(lambda z: z))), # QVar: a leaf (depth zero)
52
+ lam(lambda body: app(SUCC, app(self_recursion, body))), # QLam: one deeper
53
+ lam(lambda function: lam(lambda argument: app(SUCC, ap(
54
+ _MAX, app(self_recursion, function), app(self_recursion, argument),
55
+ )))), # QApp: one past the deeper side
56
+ ))))
57
+
58
+
59
+ @curry
60
+ def depth_at_most(bound: Builder, quoted: Builder) -> Builder:
61
+ """``DEPTH quoted <= bound`` (a Church boolean): the shallow-enough certificate."""
62
+ return ap(_AT_MOST, app(DEPTH, quoted), bound)
63
+
64
+
65
+ # NODE_COUNT: the number of Var/Lam/App nodes in a quoted term (a Church numeral), a path-free
66
+ # catamorphism the interner shares per distinct sub-term -- same shape as DEPTH but summing (PLUS) the
67
+ # children instead of taking their MAX. It bounds how big a region may be de-tabled (host-compiled to
68
+ # call-by-need): a small region (<= a bound) loses cross-location tabling at most a constant factor, never
69
+ # exponentially, so the local-call-by-need optimization stays bounded and measurable.
70
+ _ZERO: Builder = lam(lambda s: lam(lambda z: z)) # church 0, the leaf base for the count
71
+
72
+ NODE_COUNT: Builder = app(Y, lam(lambda self_recursion: lam(lambda quoted: ap(
73
+ quoted,
74
+ lam(lambda index: app(SUCC, _ZERO)), # QVar: one node
75
+ lam(lambda body: app(SUCC, app(self_recursion, body))), # QLam: one + body
76
+ lam(lambda function: lam(lambda argument: app(SUCC, ap(
77
+ PLUS, app(self_recursion, function), app(self_recursion, argument),
78
+ )))), # QApp: one + function + argument
79
+ ))))
80
+
81
+
82
+ @curry
83
+ def node_count_at_most(bound: Builder, quoted: Builder) -> Builder:
84
+ """``NODE_COUNT quoted <= bound`` (a Church boolean): the small-enough-to-de-table certificate."""
85
+ return ap(_AT_MOST, app(NODE_COUNT, quoted), bound)
86
+
87
+
88
+ @curry
89
+ def loose_bound_at_most(bound: Builder, quoted: Builder) -> Builder:
90
+ """``LOOSE_BOUND quoted <= bound`` (a Church boolean): the few-free-variables certificate (an open
91
+ region with at most ``bound`` free de Bruijn variables, so its host island is an arity-``bound`` native)."""
92
+ return ap(_AT_MOST, app(LOOSE_BOUND, quoted), bound)