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.
- co_lambda-0.5.0.post1.dev0/.gitignore +247 -0
- co_lambda-0.5.0.post1.dev0/PKG-INFO +84 -0
- co_lambda-0.5.0.post1.dev0/README.md +68 -0
- co_lambda-0.5.0.post1.dev0/pyproject.toml +43 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/__init__.py +1 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_analysis.py +92 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_ast.py +372 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_binnat.py +170 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_codec.py +147 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_compiler_artifact.py +50 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_defun_codegen.py +321 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_defun_runtime.py +188 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_defunctionalize.py +470 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_dsl.py +148 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/.gitattributes +5 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/__init__.py +1 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py311.py +7586 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py312.py +7586 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_generated/_generated_defun_compiler_py313.py +7586 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_hoas_latex.py +144 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_latex.py +58 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_prelude.py +163 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_pyast.py +418 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_pybuild.py +313 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_reduce.py +145 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_shape.py +224 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_sugar.py +74 -0
- co_lambda-0.5.0.post1.dev0/src/co_lambda/_typecheck.py +370 -0
- co_lambda-0.5.0.post1.dev0/tests/__snapshots__/test_binnat.ambr +8 -0
- co_lambda-0.5.0.post1.dev0/tests/__snapshots__/test_defunctionalize.ambr +76 -0
- co_lambda-0.5.0.post1.dev0/tests/test_arithmetic.py +82 -0
- co_lambda-0.5.0.post1.dev0/tests/test_binnat.py +81 -0
- co_lambda-0.5.0.post1.dev0/tests/test_defunctionalize.py +243 -0
- co_lambda-0.5.0.post1.dev0/tests/test_hoas_latex.py +30 -0
- co_lambda-0.5.0.post1.dev0/tests/test_pyast.py +69 -0
- 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)
|