curryparty 0.1.0__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.
- curryparty-0.1.0/PKG-INFO +70 -0
- curryparty-0.1.0/README.md +59 -0
- curryparty-0.1.0/pyproject.toml +23 -0
- curryparty-0.1.0/src/curryparty/__init__.py +172 -0
- curryparty-0.1.0/src/curryparty/core.py +128 -0
- curryparty-0.1.0/src/curryparty/display.py +232 -0
- curryparty-0.1.0/src/curryparty/py.typed +0 -0
- curryparty-0.1.0/src/curryparty/utils.py +96 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: curryparty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python playground to learn lambda calculus
|
|
5
|
+
Author: Antonin P
|
|
6
|
+
Author-email: Antonin P <antonin.peronnet@telecom-paris.fr>
|
|
7
|
+
Requires-Dist: polars>=1.36.1
|
|
8
|
+
Requires-Dist: svg-py>=1.9.2
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Curry party
|
|
13
|
+
|
|
14
|
+
`curryparty` is a library created to explore, visualize and teach lambda-calculus concepts.
|
|
15
|
+
|
|
16
|
+
# Install
|
|
17
|
+
|
|
18
|
+
Run `pip install curryparty` or `uv add curryparty` depending on your package manager.
|
|
19
|
+
|
|
20
|
+
# How to use
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from curryparty import L, V
|
|
24
|
+
|
|
25
|
+
# build expressions idiomatically
|
|
26
|
+
identity = L("x")._("x").build()
|
|
27
|
+
|
|
28
|
+
zero = L("f", "x")._("x").build()
|
|
29
|
+
|
|
30
|
+
omega = L("x")._("x").call("x").build()
|
|
31
|
+
|
|
32
|
+
# you can create more complex terms:
|
|
33
|
+
|
|
34
|
+
succ = L("n", "f", "x")._("f").call(
|
|
35
|
+
V("n").call("f").call("x")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# If you try to combine them, nothing will happen:
|
|
39
|
+
bomb = omega(omega)
|
|
40
|
+
one = s(zero)
|
|
41
|
+
|
|
42
|
+
# You need to beta-reduce them:
|
|
43
|
+
bomb.beta()
|
|
44
|
+
one_with_first_reduction = one.beta()
|
|
45
|
+
|
|
46
|
+
# if you want the final form:
|
|
47
|
+
term = s(zero)
|
|
48
|
+
while term is not None:
|
|
49
|
+
term = term.beta()
|
|
50
|
+
print(term)
|
|
51
|
+
|
|
52
|
+
# this is equivalent to:
|
|
53
|
+
for x in term.reduction_chain():
|
|
54
|
+
print(x)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
But the main point of this library is the svg-based display system.
|
|
58
|
+
If you use a notebook such as jupyternotebook or marimo, you will see something like this:
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
You can also use `term.show_reduction` to get an animated version.
|
|
64
|
+
|
|
65
|
+
# How it works
|
|
66
|
+
|
|
67
|
+
Under the wood, all the terms are converted into a list of nodes, that can either be a `lambda`, an `application` (with 2 arguments) or a `variable` (with 0 arguments).
|
|
68
|
+
|
|
69
|
+
For efficiency, they are converted to a [polars](https://github.com/pola-rs/polars) dataframe in prefix traversal order.
|
|
70
|
+
If you want to understand how it works, start by looking at the `.nodes` attribute of any term.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Curry party
|
|
2
|
+
|
|
3
|
+
`curryparty` is a library created to explore, visualize and teach lambda-calculus concepts.
|
|
4
|
+
|
|
5
|
+
# Install
|
|
6
|
+
|
|
7
|
+
Run `pip install curryparty` or `uv add curryparty` depending on your package manager.
|
|
8
|
+
|
|
9
|
+
# How to use
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from curryparty import L, V
|
|
13
|
+
|
|
14
|
+
# build expressions idiomatically
|
|
15
|
+
identity = L("x")._("x").build()
|
|
16
|
+
|
|
17
|
+
zero = L("f", "x")._("x").build()
|
|
18
|
+
|
|
19
|
+
omega = L("x")._("x").call("x").build()
|
|
20
|
+
|
|
21
|
+
# you can create more complex terms:
|
|
22
|
+
|
|
23
|
+
succ = L("n", "f", "x")._("f").call(
|
|
24
|
+
V("n").call("f").call("x")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# If you try to combine them, nothing will happen:
|
|
28
|
+
bomb = omega(omega)
|
|
29
|
+
one = s(zero)
|
|
30
|
+
|
|
31
|
+
# You need to beta-reduce them:
|
|
32
|
+
bomb.beta()
|
|
33
|
+
one_with_first_reduction = one.beta()
|
|
34
|
+
|
|
35
|
+
# if you want the final form:
|
|
36
|
+
term = s(zero)
|
|
37
|
+
while term is not None:
|
|
38
|
+
term = term.beta()
|
|
39
|
+
print(term)
|
|
40
|
+
|
|
41
|
+
# this is equivalent to:
|
|
42
|
+
for x in term.reduction_chain():
|
|
43
|
+
print(x)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
But the main point of this library is the svg-based display system.
|
|
47
|
+
If you use a notebook such as jupyternotebook or marimo, you will see something like this:
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
You can also use `term.show_reduction` to get an animated version.
|
|
53
|
+
|
|
54
|
+
# How it works
|
|
55
|
+
|
|
56
|
+
Under the wood, all the terms are converted into a list of nodes, that can either be a `lambda`, an `application` (with 2 arguments) or a `variable` (with 0 arguments).
|
|
57
|
+
|
|
58
|
+
For efficiency, they are converted to a [polars](https://github.com/pola-rs/polars) dataframe in prefix traversal order.
|
|
59
|
+
If you want to understand how it works, start by looking at the `.nodes` attribute of any term.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "curryparty"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python playground to learn lambda calculus"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Antonin P", email = "antonin.peronnet@telecom-paris.fr" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"polars>=1.36.1",
|
|
12
|
+
"svg-py>=1.9.2",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.9.13,<0.10.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"marimo>=0.18.4",
|
|
22
|
+
"ruff>=0.14.8",
|
|
23
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import Iterable, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
from svg import SVG
|
|
5
|
+
|
|
6
|
+
from .core import SCHEMA, beta_reduce, compose, find_redexes, find_variables, subtree
|
|
7
|
+
from .display import (
|
|
8
|
+
compute_svg_frame_final,
|
|
9
|
+
compute_svg_frame_init,
|
|
10
|
+
compute_svg_frame_phase_a,
|
|
11
|
+
compute_svg_frame_phase_b,
|
|
12
|
+
)
|
|
13
|
+
from .utils import ShapeAnim
|
|
14
|
+
|
|
15
|
+
__all__ = ["L", "V"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Term:
|
|
19
|
+
def __init__(self, nodes: pl.DataFrame):
|
|
20
|
+
assert nodes.schema == SCHEMA, (
|
|
21
|
+
f"{nodes.schema} is different from expected {SCHEMA}"
|
|
22
|
+
)
|
|
23
|
+
self.nodes = nodes
|
|
24
|
+
self.lamb = None
|
|
25
|
+
|
|
26
|
+
def __call__(self, other: "Term") -> "Term":
|
|
27
|
+
return Term(compose(self.nodes, other.nodes))
|
|
28
|
+
|
|
29
|
+
def beta(self) -> Optional["Term"]:
|
|
30
|
+
candidates = find_redexes(self.nodes)
|
|
31
|
+
if len(candidates) == 0:
|
|
32
|
+
return None
|
|
33
|
+
_redex, lamb, b = candidates.row(0)
|
|
34
|
+
self.lamb = lamb
|
|
35
|
+
self.b = b
|
|
36
|
+
reduced = beta_reduce(self.nodes, lamb, b)
|
|
37
|
+
return Term(reduced)
|
|
38
|
+
|
|
39
|
+
def reduction_chain(self) -> Iterable["Term"]:
|
|
40
|
+
term = self
|
|
41
|
+
while True:
|
|
42
|
+
yield term
|
|
43
|
+
term = term.beta()
|
|
44
|
+
if term is None:
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
def show_reduction(self):
|
|
48
|
+
candidates = find_redexes(self.nodes)
|
|
49
|
+
if len(candidates) == 0:
|
|
50
|
+
return None
|
|
51
|
+
_redex, lamb, b = candidates.row(0)
|
|
52
|
+
new_nodes = beta_reduce(self.nodes, lamb, b)
|
|
53
|
+
vars = find_variables(self.nodes, lamb)["id"]
|
|
54
|
+
b_subtree = subtree(self.nodes, b)
|
|
55
|
+
shapes: dict[int, ShapeAnim] = {}
|
|
56
|
+
N_STEPS = 8
|
|
57
|
+
|
|
58
|
+
for t in range(N_STEPS):
|
|
59
|
+
if t < 2:
|
|
60
|
+
items = compute_svg_frame_init(self.nodes)
|
|
61
|
+
elif t == 2 or t == 3:
|
|
62
|
+
items = compute_svg_frame_phase_a(self.nodes, lamb, b_subtree, vars)
|
|
63
|
+
elif t == 4 or t == 5:
|
|
64
|
+
items = compute_svg_frame_phase_b(
|
|
65
|
+
self.nodes, lamb, b_subtree, new_nodes
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
items = compute_svg_frame_final(new_nodes)
|
|
69
|
+
for k, e, attributes in items:
|
|
70
|
+
if k not in shapes:
|
|
71
|
+
shapes[k] = ShapeAnim(e)
|
|
72
|
+
shapes[k].append_frame(t, attributes.items())
|
|
73
|
+
|
|
74
|
+
elements = [x.to_element(N_STEPS) for x in shapes.values()]
|
|
75
|
+
height = 2 + int(0.6 * len(self.nodes))
|
|
76
|
+
return Html(
|
|
77
|
+
SVG(
|
|
78
|
+
xmlns="http://www.w3.org/2000/svg",
|
|
79
|
+
viewBox=f"-10 0 30 {height}", # type: ignore
|
|
80
|
+
height=f"{35 * height}px", # type: ignore
|
|
81
|
+
elements=elements,
|
|
82
|
+
).as_str()
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _repr_html_(self):
|
|
86
|
+
frame = compute_svg_frame_init(self.nodes)
|
|
87
|
+
elements = []
|
|
88
|
+
height = 2 + int(0.6 * len(self.nodes))
|
|
89
|
+
for _, e, attributes in frame:
|
|
90
|
+
for name, v in attributes.items():
|
|
91
|
+
e.__setattr__(name, v)
|
|
92
|
+
elements.append(e)
|
|
93
|
+
return SVG(
|
|
94
|
+
xmlns="http://www.w3.org/2000/svg",
|
|
95
|
+
viewBox=f"-10 0 30 {height}", # type: ignore
|
|
96
|
+
height=f"{35 * height}px", # type: ignore
|
|
97
|
+
elements=elements,
|
|
98
|
+
).as_str()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class L:
|
|
102
|
+
def __init__(self, *lambda_names):
|
|
103
|
+
self.n = len(lambda_names)
|
|
104
|
+
self.lambdas = {x: i for (i, x) in enumerate(lambda_names)}
|
|
105
|
+
self.refs: List[tuple[int, Union[int, str]]] = []
|
|
106
|
+
self.args = []
|
|
107
|
+
self.last_ = None
|
|
108
|
+
|
|
109
|
+
def lamb(self, name: str) -> "L":
|
|
110
|
+
self.n += 1
|
|
111
|
+
self.lambdas[name] = self.n
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def _append_subtree_or_subexpression(self, t: Union[str, "L"]):
|
|
115
|
+
if isinstance(t, L):
|
|
116
|
+
offset = self.n
|
|
117
|
+
for i, x in t.refs:
|
|
118
|
+
self.refs.append((offset + i, t.lambdas.get(x, x)))
|
|
119
|
+
|
|
120
|
+
for i, x in t.args:
|
|
121
|
+
self.args.append((offset + i, offset + x))
|
|
122
|
+
self.n += t.n
|
|
123
|
+
else:
|
|
124
|
+
assert isinstance(t, str)
|
|
125
|
+
self.refs.append((self.n, t))
|
|
126
|
+
self.n += 1
|
|
127
|
+
|
|
128
|
+
def _(self, x: Union[str, "L"]) -> "L":
|
|
129
|
+
self.last_ = self.n
|
|
130
|
+
self._append_subtree_or_subexpression(x)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def call(self, arg: Union[str, "L"]) -> "L":
|
|
134
|
+
assert self.last_ is not None
|
|
135
|
+
self.refs = [(i + 1, x) if i >= self.last_ else (i, x) for (i, x) in self.refs]
|
|
136
|
+
self.args = [
|
|
137
|
+
(i + 1, x + 1) if i >= self.last_ else (i, x) for (i, x) in self.args
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
self.n += 1
|
|
141
|
+
self.args.append((self.last_, self.n))
|
|
142
|
+
self._append_subtree_or_subexpression(arg)
|
|
143
|
+
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def build(self) -> "Term":
|
|
147
|
+
self.refs = [(i, self.lambdas.get(x, x)) for i, x in self.refs]
|
|
148
|
+
ref = pl.from_records(
|
|
149
|
+
self.refs, orient="row", schema={"id": pl.UInt32, "ref": pl.UInt32}
|
|
150
|
+
)
|
|
151
|
+
arg = pl.from_records(
|
|
152
|
+
self.args, orient="row", schema={"id": pl.UInt32, "arg": pl.UInt32}
|
|
153
|
+
)
|
|
154
|
+
data = (
|
|
155
|
+
pl.Series("id", range(self.n), dtype=pl.UInt32)
|
|
156
|
+
.to_frame()
|
|
157
|
+
.join(ref, on="id", how="left")
|
|
158
|
+
.join(arg, on="id", how="left")
|
|
159
|
+
).with_columns(bid=pl.struct(major="id", minor="id"))
|
|
160
|
+
return Term(data)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def V(name: str) -> L:
|
|
164
|
+
return L()._(name)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Html:
|
|
168
|
+
def __init__(self, content: str):
|
|
169
|
+
self.content = content
|
|
170
|
+
|
|
171
|
+
def _repr_html_(self):
|
|
172
|
+
return self.content
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from polars import Schema, UInt32
|
|
3
|
+
from polars.functions.lazy import coalesce
|
|
4
|
+
|
|
5
|
+
SCHEMA = Schema(
|
|
6
|
+
{
|
|
7
|
+
"id": UInt32,
|
|
8
|
+
"ref": UInt32,
|
|
9
|
+
"arg": UInt32,
|
|
10
|
+
"bid": pl.Struct({"major": UInt32, "minor": UInt32}),
|
|
11
|
+
},
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _shift(nodes: pl.DataFrame, offset: int):
|
|
16
|
+
return nodes.with_columns(
|
|
17
|
+
pl.col("id") + offset,
|
|
18
|
+
pl.col("ref") + offset,
|
|
19
|
+
pl.col("arg") + offset,
|
|
20
|
+
bid=None,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compose(f: pl.DataFrame, x: pl.DataFrame):
|
|
25
|
+
n = len(f)
|
|
26
|
+
return pl.concat(
|
|
27
|
+
[
|
|
28
|
+
pl.DataFrame(
|
|
29
|
+
[{"id": 0, "arg": n + 1}],
|
|
30
|
+
schema=SCHEMA,
|
|
31
|
+
),
|
|
32
|
+
_shift(f, 1),
|
|
33
|
+
_shift(x, n + 1),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_redexes(nodes: pl.DataFrame):
|
|
39
|
+
parents = nodes.filter(pl.col("ref").is_null())
|
|
40
|
+
return (
|
|
41
|
+
parents.join(nodes, left_on="id", right_on=pl.col("id") - 1, suffix="_child")
|
|
42
|
+
.filter(
|
|
43
|
+
pl.col("arg").is_not_null(),
|
|
44
|
+
pl.col("ref_child").is_null(),
|
|
45
|
+
pl.col("arg_child").is_null(),
|
|
46
|
+
)
|
|
47
|
+
.select(redex="id", lamb="id_child", b="arg")
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def find_variables(nodes: pl.DataFrame, lamb: int):
|
|
52
|
+
return nodes.filter(pl.col("ref") == lamb).select("id", replaced=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def subtree(nodes: pl.DataFrame, root: int) -> pl.DataFrame:
|
|
56
|
+
rightmost = root
|
|
57
|
+
while True:
|
|
58
|
+
ref = nodes["ref"][rightmost]
|
|
59
|
+
arg = nodes["arg"][rightmost]
|
|
60
|
+
if ref is not None:
|
|
61
|
+
return nodes.filter(pl.col("id").is_between(root, rightmost))
|
|
62
|
+
|
|
63
|
+
rightmost = arg if arg is not None else rightmost + 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _generate_bi_identifier(
|
|
67
|
+
major_name: str, minor_name: str, minor_replacement=pl.lit(None)
|
|
68
|
+
):
|
|
69
|
+
return pl.struct(
|
|
70
|
+
major=pl.col(major_name).fill_null(pl.col(minor_name)),
|
|
71
|
+
minor=minor_replacement.fill_null(pl.col(minor_name)),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def beta_reduce(nodes: pl.DataFrame, lamb: int, b: int) -> pl.DataFrame:
|
|
76
|
+
redex = lamb - 1
|
|
77
|
+
a = lamb + 1
|
|
78
|
+
|
|
79
|
+
vars = find_variables(nodes, lamb)
|
|
80
|
+
b_subtree = subtree(nodes, b)
|
|
81
|
+
|
|
82
|
+
b_subtree_duplicated = b_subtree.join(vars, how="cross", suffix="_major")
|
|
83
|
+
rest_of_nodes = nodes.join(b_subtree, on="id", how="anti").with_columns(
|
|
84
|
+
arg=pl.col("arg").replace(redex, a)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
new_nodes = (
|
|
88
|
+
pl.concat(
|
|
89
|
+
[b_subtree_duplicated, rest_of_nodes],
|
|
90
|
+
how="diagonal_relaxed",
|
|
91
|
+
)
|
|
92
|
+
.join(vars, left_on="id", right_on="id", how="anti")
|
|
93
|
+
.join(vars, left_on="arg", right_on="id", how="left", suffix="_arg")
|
|
94
|
+
.join(vars, left_on="ref", right_on="id", how="left", suffix="_ref")
|
|
95
|
+
.filter(
|
|
96
|
+
~pl.col("id").is_between(redex, lamb),
|
|
97
|
+
)
|
|
98
|
+
.select(
|
|
99
|
+
bid=_generate_bi_identifier("id_major", "id"),
|
|
100
|
+
bid_ref=_generate_bi_identifier("id_major", "ref"),
|
|
101
|
+
bid_ref_fallback=pl.struct(major="ref", minor="ref"),
|
|
102
|
+
bid_arg=_generate_bi_identifier(
|
|
103
|
+
"id_major", "arg", minor_replacement=pl.when("replaced_arg").then(b)
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
.sort("bid")
|
|
107
|
+
.with_row_index("id")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
new_nodes.join(
|
|
112
|
+
new_nodes.select(bid_ref="bid", ref="id"),
|
|
113
|
+
on="bid_ref",
|
|
114
|
+
how="left",
|
|
115
|
+
)
|
|
116
|
+
.join(
|
|
117
|
+
new_nodes.select(bid_ref_fallback="bid", ref_fallback="id"),
|
|
118
|
+
on="bid_ref_fallback",
|
|
119
|
+
how="left",
|
|
120
|
+
)
|
|
121
|
+
.join(
|
|
122
|
+
new_nodes.select(bid_arg="bid", arg="id"),
|
|
123
|
+
on="bid_arg",
|
|
124
|
+
how="left",
|
|
125
|
+
)
|
|
126
|
+
.select("id", ref=pl.coalesce("ref", "ref_fallback"), arg="arg", bid="bid")
|
|
127
|
+
.sort("id")
|
|
128
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from typing import Any, Iterable, Optional, Union
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
import svg
|
|
5
|
+
|
|
6
|
+
from .utils import Interval
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compute_layout(
|
|
10
|
+
nodes: pl.DataFrame, lamb=None, replaced_var_width=1
|
|
11
|
+
) -> tuple[dict[int, int], dict[int, int]]:
|
|
12
|
+
y = {0: Interval((0, 0))}
|
|
13
|
+
x = {}
|
|
14
|
+
for node, ref, arg in nodes.select("id", "ref", "arg").iter_rows():
|
|
15
|
+
if ref is not None:
|
|
16
|
+
continue
|
|
17
|
+
child = node + 1
|
|
18
|
+
if arg is not None:
|
|
19
|
+
y[child] = y[node].shift(0 if nodes["arg"][child] is None else 1)
|
|
20
|
+
y[arg] = y[node].shift(0)
|
|
21
|
+
else:
|
|
22
|
+
y[child] = y[node].shift(1)
|
|
23
|
+
|
|
24
|
+
next_var_x = nodes.select(pl.col("ref").count()).item()
|
|
25
|
+
|
|
26
|
+
for node, ref, arg in (
|
|
27
|
+
nodes.sort("id", descending=True).select("id", "ref", "arg").iter_rows()
|
|
28
|
+
):
|
|
29
|
+
if ref is not None:
|
|
30
|
+
width = replaced_var_width if ref == lamb else 1
|
|
31
|
+
x[node] = Interval((next_var_x - width + 1, next_var_x))
|
|
32
|
+
next_var_x -= width
|
|
33
|
+
x[ref] = x[node] | x.get(ref, Interval(None))
|
|
34
|
+
|
|
35
|
+
else:
|
|
36
|
+
child = node + 1
|
|
37
|
+
x[node] = x[child] | x.get(node, Interval(None))
|
|
38
|
+
y[node] = y[child] | y[node]
|
|
39
|
+
return x, y
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def draw(
|
|
43
|
+
x: dict[Union[int, tuple[int, int]], Interval],
|
|
44
|
+
y: dict[Union[int, tuple[int, int]], Interval],
|
|
45
|
+
i_node: Union[int, tuple[int, int]],
|
|
46
|
+
ref: Optional[int],
|
|
47
|
+
arg: Optional[int],
|
|
48
|
+
key: Any,
|
|
49
|
+
replaced=False,
|
|
50
|
+
removed=False,
|
|
51
|
+
) -> Iterable[tuple[Any, svg.Element, dict]]:
|
|
52
|
+
x_node = x[i_node]
|
|
53
|
+
y_node = y[i_node]
|
|
54
|
+
if True:
|
|
55
|
+
if arg is not None or removed:
|
|
56
|
+
color = "transparent"
|
|
57
|
+
elif replaced or removed:
|
|
58
|
+
color = "green"
|
|
59
|
+
elif ref is not None:
|
|
60
|
+
color = "red"
|
|
61
|
+
else:
|
|
62
|
+
color = "blue"
|
|
63
|
+
|
|
64
|
+
stroke_width = 0.05
|
|
65
|
+
stroke = "gray"
|
|
66
|
+
if arg is not None:
|
|
67
|
+
stroke_width = 0.1
|
|
68
|
+
stroke = "orange"
|
|
69
|
+
r = svg.Rect(
|
|
70
|
+
height=0.8,
|
|
71
|
+
stroke_width=stroke_width,
|
|
72
|
+
stroke=stroke,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
yield (
|
|
76
|
+
("r", key),
|
|
77
|
+
r,
|
|
78
|
+
{
|
|
79
|
+
"x": 0.1 + x_node[0],
|
|
80
|
+
"y": 0.1 + y_node[0],
|
|
81
|
+
"width": 0.8 + x_node[1] - x_node[0],
|
|
82
|
+
"fill_opacity": 1 if arg is None else 0,
|
|
83
|
+
"fill": color,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if ref is not None:
|
|
88
|
+
y_ref = y[ref]
|
|
89
|
+
e = svg.Line(
|
|
90
|
+
stroke_width=0.2,
|
|
91
|
+
stroke="gray",
|
|
92
|
+
)
|
|
93
|
+
yield (
|
|
94
|
+
("l", key),
|
|
95
|
+
e,
|
|
96
|
+
{
|
|
97
|
+
"x1": x_node[0] + 0.5,
|
|
98
|
+
"y1": y_node[0] + 0.1,
|
|
99
|
+
"x2": x_node[0] + 0.5,
|
|
100
|
+
"y2": y_ref[0] + 0.9,
|
|
101
|
+
"stroke": "green" if replaced else "gray",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if arg is not None:
|
|
106
|
+
x_arg = x[arg]
|
|
107
|
+
y_arg = y[arg]
|
|
108
|
+
e1 = svg.Line(
|
|
109
|
+
stroke="black",
|
|
110
|
+
stroke_width=0.05,
|
|
111
|
+
)
|
|
112
|
+
e2 = svg.Circle(fill="black", r=0.1)
|
|
113
|
+
if not removed:
|
|
114
|
+
yield (
|
|
115
|
+
("b", key),
|
|
116
|
+
e1,
|
|
117
|
+
{
|
|
118
|
+
"x1": 0.5 + x_node[1],
|
|
119
|
+
"y1": 0.5 + y_node[0],
|
|
120
|
+
"x2": 0.5 + x_arg[0],
|
|
121
|
+
"y2": 0.5 + y_arg[0],
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
yield (
|
|
125
|
+
("c", key),
|
|
126
|
+
e2,
|
|
127
|
+
{
|
|
128
|
+
"cx": 0.5 + x_node[1],
|
|
129
|
+
"cy": 0.5 + y_node[0],
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def compute_svg_frame_init(
|
|
135
|
+
nodes: pl.DataFrame,
|
|
136
|
+
) -> Iterable[tuple[Any, svg.Element, dict[str, Any]]]:
|
|
137
|
+
x, y = compute_layout(nodes)
|
|
138
|
+
for target_id, ref, arg in (
|
|
139
|
+
nodes.select("id", "ref", "arg").sort("id", descending=True).iter_rows()
|
|
140
|
+
):
|
|
141
|
+
yield from draw(x, y, target_id, ref, arg, target_id)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def compute_svg_frame_phase_a(
|
|
145
|
+
nodes: pl.DataFrame,
|
|
146
|
+
lamb: int,
|
|
147
|
+
b_subtree: pl.DataFrame,
|
|
148
|
+
vars: pl.Series,
|
|
149
|
+
):
|
|
150
|
+
redex = lamb - 1 if lamb is not None else None
|
|
151
|
+
b_width = b_subtree.count()["ref"].item()
|
|
152
|
+
x, y = compute_layout(nodes, lamb=lamb, replaced_var_width=b_width)
|
|
153
|
+
for target_id, ref, arg in (
|
|
154
|
+
nodes.select("id", "ref", "arg").sort("id", descending=True).iter_rows()
|
|
155
|
+
):
|
|
156
|
+
replaced = ref is not None and ref == lamb
|
|
157
|
+
yield from draw(
|
|
158
|
+
x,
|
|
159
|
+
y,
|
|
160
|
+
target_id,
|
|
161
|
+
ref,
|
|
162
|
+
arg,
|
|
163
|
+
target_id,
|
|
164
|
+
replaced=replaced,
|
|
165
|
+
removed=(target_id == lamb or target_id == redex),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
for v in vars:
|
|
169
|
+
for minor, ref, arg in (
|
|
170
|
+
b_subtree.select("id", "ref", "arg").sort("id", descending=True).iter_rows()
|
|
171
|
+
):
|
|
172
|
+
yield from draw(x, y, minor, ref, arg, key=(v, minor))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def compute_svg_frame_phase_b(
|
|
176
|
+
nodes: pl.DataFrame,
|
|
177
|
+
lamb: int,
|
|
178
|
+
b_subtree: pl.DataFrame,
|
|
179
|
+
new_nodes: pl.DataFrame,
|
|
180
|
+
):
|
|
181
|
+
b_width = b_subtree.count()["ref"].item()
|
|
182
|
+
b = b_subtree["id"][0]
|
|
183
|
+
x, y = compute_layout(nodes, lamb=lamb, replaced_var_width=b_width)
|
|
184
|
+
b_x = x[b][0]
|
|
185
|
+
b_y = y[b][0]
|
|
186
|
+
for bid, arg in new_nodes.select("bid", "arg").iter_rows():
|
|
187
|
+
if bid["minor"] != bid["major"]:
|
|
188
|
+
v = bid["major"]
|
|
189
|
+
minor = bid["minor"]
|
|
190
|
+
delta_x = x[v][0] - b_x
|
|
191
|
+
delta_y = y[v][0] - b_y
|
|
192
|
+
x[(v, minor)] = x[minor].shift(delta_x)
|
|
193
|
+
y[(v, minor)] = y[minor].shift(delta_y)
|
|
194
|
+
|
|
195
|
+
for bid, new_ref, new_arg in new_nodes.select("bid", "ref", "arg").iter_rows():
|
|
196
|
+
v = bid["major"]
|
|
197
|
+
minor = bid["minor"]
|
|
198
|
+
if new_ref is None:
|
|
199
|
+
ref = None
|
|
200
|
+
else:
|
|
201
|
+
bid_ref = new_nodes["bid"][new_ref]
|
|
202
|
+
ref = (
|
|
203
|
+
(bid_ref["major"], bid_ref["minor"])
|
|
204
|
+
if bid_ref["major"] != bid_ref["minor"]
|
|
205
|
+
else bid_ref["minor"]
|
|
206
|
+
)
|
|
207
|
+
if new_arg is None:
|
|
208
|
+
arg = None
|
|
209
|
+
else:
|
|
210
|
+
bid_arg = new_nodes["bid"][new_arg]
|
|
211
|
+
arg = (
|
|
212
|
+
(bid_arg["major"], bid_arg["minor"])
|
|
213
|
+
if bid_arg["major"] != bid_arg["minor"]
|
|
214
|
+
else bid_arg["minor"]
|
|
215
|
+
)
|
|
216
|
+
if bid_arg["minor"] == b:
|
|
217
|
+
arg = bid_arg["major"]
|
|
218
|
+
key = (v, minor) if minor != v else minor
|
|
219
|
+
yield from draw(x, y, key, ref, arg, key=key)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def compute_svg_frame_final(reduced: pl.DataFrame):
|
|
223
|
+
x, y = compute_layout(reduced)
|
|
224
|
+
for target_id, bid, ref, arg in (
|
|
225
|
+
reduced.select("id", "bid", "ref", "arg")
|
|
226
|
+
.sort("id", descending=True)
|
|
227
|
+
.iter_rows()
|
|
228
|
+
):
|
|
229
|
+
minor = bid["minor"]
|
|
230
|
+
major = bid["major"]
|
|
231
|
+
key = (major, minor) if minor != major else minor
|
|
232
|
+
yield from draw(x, y, target_id, ref, arg, key)
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import Any, Iterable, Optional
|
|
4
|
+
|
|
5
|
+
from svg import Animate, Element
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Interval:
|
|
10
|
+
values: Optional[tuple[int, int]]
|
|
11
|
+
|
|
12
|
+
def __or__(self: "Interval", other: "Interval") -> "Interval":
|
|
13
|
+
if self.values is None:
|
|
14
|
+
if other.values is None:
|
|
15
|
+
return Interval(None)
|
|
16
|
+
else:
|
|
17
|
+
return other
|
|
18
|
+
else:
|
|
19
|
+
if other.values is None:
|
|
20
|
+
return self
|
|
21
|
+
return Interval(
|
|
22
|
+
(min(self.values[0], other.values[0]), max(self.values[1], other.values[1]))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, index):
|
|
26
|
+
assert self.values is not None, "interval is empty"
|
|
27
|
+
return self.values[index]
|
|
28
|
+
|
|
29
|
+
def shift(self, offset: int) -> "Interval":
|
|
30
|
+
if self.values is None:
|
|
31
|
+
return Interval(None)
|
|
32
|
+
else:
|
|
33
|
+
return Interval((self.values[0] + offset, self.values[1] + offset))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ShapeAnim:
|
|
38
|
+
shape: Element
|
|
39
|
+
attributes: set[str]
|
|
40
|
+
values: dict[tuple[int, str], Any]
|
|
41
|
+
n: int
|
|
42
|
+
duration: int
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
shape: Element,
|
|
47
|
+
duration: int = 7,
|
|
48
|
+
):
|
|
49
|
+
self.shape = shape
|
|
50
|
+
self.attributes = set()
|
|
51
|
+
self.values = {}
|
|
52
|
+
self.duration = duration
|
|
53
|
+
|
|
54
|
+
def append_frame(self, i: int, attributes: Iterable[tuple[str, Any]]):
|
|
55
|
+
for name, v in attributes:
|
|
56
|
+
self.attributes.add(name)
|
|
57
|
+
self.values[i, name] = v
|
|
58
|
+
|
|
59
|
+
def to_element(self, n: int):
|
|
60
|
+
elements = []
|
|
61
|
+
|
|
62
|
+
visible = [
|
|
63
|
+
all((i, name) in self.values for name in self.attributes) for i in range(n)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for name in self.attributes:
|
|
67
|
+
non_nulls = [
|
|
68
|
+
self.values[i, name] for i in range(n) if (i, name) in self.values
|
|
69
|
+
]
|
|
70
|
+
for i in reversed(range(n)):
|
|
71
|
+
if (i, name) in self.values:
|
|
72
|
+
break
|
|
73
|
+
self.values[i, name] = non_nulls[-1]
|
|
74
|
+
for i in range(n):
|
|
75
|
+
if (i, name) not in self.values:
|
|
76
|
+
self.values[i, name] = non_nulls[0]
|
|
77
|
+
elements.append(
|
|
78
|
+
Animate(
|
|
79
|
+
attributeName=name,
|
|
80
|
+
values=";".join(str(self.values[i, name]) for i in range(n)),
|
|
81
|
+
dur=timedelta(seconds=self.duration),
|
|
82
|
+
repeatCount="indefinite",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
elements.append(
|
|
86
|
+
Animate(
|
|
87
|
+
attributeName="opacity",
|
|
88
|
+
values=";".join("1" if v else "0" for v in visible),
|
|
89
|
+
dur=timedelta(seconds=self.duration),
|
|
90
|
+
repeatCount="indefinite",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert not self.shape.elements
|
|
95
|
+
self.shape.elements = elements
|
|
96
|
+
return self.shape
|