curryparty 0.2.2__py3-none-any.whl → 0.3.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.
- curryparty/__init__.py +83 -35
- curryparty/display.py +95 -60
- curryparty/utils.py +67 -20
- {curryparty-0.2.2.dist-info → curryparty-0.3.0.dist-info}/METADATA +6 -1
- curryparty-0.3.0.dist-info/RECORD +8 -0
- {curryparty-0.2.2.dist-info → curryparty-0.3.0.dist-info}/WHEEL +2 -2
- curryparty-0.2.2.dist-info/RECORD +0 -8
curryparty/__init__.py
CHANGED
|
@@ -6,9 +6,10 @@ except ImportError:
|
|
|
6
6
|
raise ImportError(
|
|
7
7
|
"curryparty needs the `polars` library. \n Please install it, typically with `pip install polars`"
|
|
8
8
|
)
|
|
9
|
-
from svg import SVG, Rect
|
|
10
9
|
import uuid
|
|
11
10
|
|
|
11
|
+
from svg import SVG, Rect
|
|
12
|
+
|
|
12
13
|
from .core import SCHEMA, beta_reduce, compose, find_redexes, find_variables, subtree
|
|
13
14
|
from .display import (
|
|
14
15
|
compute_height,
|
|
@@ -16,12 +17,21 @@ from .display import (
|
|
|
16
17
|
compute_svg_frame_init,
|
|
17
18
|
compute_svg_frame_phase_a,
|
|
18
19
|
compute_svg_frame_phase_b,
|
|
20
|
+
count_variables,
|
|
19
21
|
)
|
|
20
|
-
from .utils import ShapeAnim
|
|
22
|
+
from .utils import ShapeAnim, ShapeAnimFrame
|
|
21
23
|
|
|
22
24
|
__all__ = ["L", "V"]
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def log2(n):
|
|
28
|
+
if n <= 0:
|
|
29
|
+
raise ValueError(f"log2 of negative number {n}")
|
|
30
|
+
elif n == 1:
|
|
31
|
+
return 0
|
|
32
|
+
return 1 + log2(n // 2)
|
|
33
|
+
|
|
34
|
+
|
|
25
35
|
class Term:
|
|
26
36
|
def __init__(self, nodes: pl.DataFrame):
|
|
27
37
|
assert nodes.schema == SCHEMA, (
|
|
@@ -57,7 +67,7 @@ class Term:
|
|
|
57
67
|
if term is None:
|
|
58
68
|
break
|
|
59
69
|
|
|
60
|
-
def show_beta(self,
|
|
70
|
+
def show_beta(self, duration=7):
|
|
61
71
|
"""
|
|
62
72
|
Generates an HTML representation that toggles visibility between
|
|
63
73
|
a static state and a SMIL animation on hover using pure CSS.
|
|
@@ -70,37 +80,41 @@ class Term:
|
|
|
70
80
|
new_nodes = beta_reduce(self.nodes, lamb, b)
|
|
71
81
|
vars = find_variables(self.nodes, lamb)["id"]
|
|
72
82
|
b_subtree = subtree(self.nodes, b)
|
|
73
|
-
height = compute_height(self.nodes)
|
|
74
|
-
|
|
83
|
+
height = min(compute_height(self.nodes), compute_height(new_nodes)) * 2
|
|
84
|
+
if count_variables(self.nodes) == 0:
|
|
85
|
+
return "no width"
|
|
86
|
+
raw_width = max(count_variables(self.nodes), count_variables(new_nodes))
|
|
87
|
+
width = 1 << (1 + log2(raw_width))
|
|
88
|
+
frame_data: list[ShapeAnimFrame] = []
|
|
75
89
|
N_STEPS = 6
|
|
76
90
|
|
|
77
91
|
for t in range(N_STEPS):
|
|
78
92
|
if t == 0:
|
|
79
|
-
items = compute_svg_frame_init(self.nodes)
|
|
93
|
+
items = compute_svg_frame_init(self.nodes, t)
|
|
80
94
|
elif t == 1 or t == 2:
|
|
81
|
-
items = compute_svg_frame_phase_a(self.nodes, lamb, b_subtree, vars)
|
|
95
|
+
items = compute_svg_frame_phase_a(self.nodes, lamb, b_subtree, vars, t)
|
|
82
96
|
elif t == 3 or t == 4:
|
|
83
97
|
items = compute_svg_frame_phase_b(
|
|
84
|
-
self.nodes, lamb, b_subtree, new_nodes
|
|
98
|
+
self.nodes, lamb, b_subtree, new_nodes, t
|
|
85
99
|
)
|
|
86
100
|
else:
|
|
87
|
-
items = compute_svg_frame_final(new_nodes)
|
|
88
|
-
|
|
89
|
-
if k not in shapes:
|
|
90
|
-
shapes[k] = ShapeAnim(e)
|
|
91
|
-
shapes[k].append_frame(t, attributes.items())
|
|
101
|
+
items = compute_svg_frame_final(new_nodes, t)
|
|
102
|
+
frame_data.extend(items)
|
|
92
103
|
|
|
93
104
|
figure_id = uuid.uuid4()
|
|
94
105
|
box_id = f"lambda_box_{figure_id}".replace("-", "")
|
|
106
|
+
grouped = ShapeAnim.group_by_key(frame_data)
|
|
107
|
+
anims = [ShapeAnim.from_frames(frames, duration) for frames in grouped.values()]
|
|
108
|
+
anims.sort(key=lambda a: a.zindex)
|
|
95
109
|
anim_elements = [
|
|
96
110
|
x.to_element(N_STEPS, begin=f"{box_id}.click", reset=f"{box_id}.mouseover")
|
|
97
|
-
for x in
|
|
111
|
+
for x in anims
|
|
98
112
|
]
|
|
99
113
|
|
|
100
114
|
anim_elements.append(
|
|
101
115
|
Rect(
|
|
102
116
|
id=box_id,
|
|
103
|
-
x=
|
|
117
|
+
x=f"{-width}",
|
|
104
118
|
y="0",
|
|
105
119
|
width="100%",
|
|
106
120
|
height="100%",
|
|
@@ -108,32 +122,49 @@ class Term:
|
|
|
108
122
|
)
|
|
109
123
|
)
|
|
110
124
|
|
|
125
|
+
# prefered size in pixels
|
|
126
|
+
H = height * 40
|
|
111
127
|
anim_svg = SVG(
|
|
112
128
|
xmlns="http://www.w3.org/2000/svg",
|
|
113
|
-
viewBox=f"{
|
|
114
|
-
|
|
129
|
+
viewBox=f"{-width} 0 {2 * width} {height}",
|
|
130
|
+
style=f"max-height:{H}px",
|
|
115
131
|
elements=anim_elements,
|
|
116
132
|
).as_str()
|
|
117
133
|
|
|
118
134
|
return Html(
|
|
119
|
-
|
|
135
|
+
'<div style="width:100%">'
|
|
136
|
+
'<div style="margin-bottom:30px">'
|
|
137
|
+
"click to animate, move away and back to reset"
|
|
138
|
+
"</div>"
|
|
139
|
+
f"{anim_svg}"
|
|
140
|
+
"</div>"
|
|
120
141
|
)
|
|
121
142
|
|
|
122
|
-
def _repr_html_(self, x0=-10
|
|
123
|
-
frame = compute_svg_frame_init(self.nodes)
|
|
124
|
-
elements = []
|
|
143
|
+
def _repr_html_(self, x0=-10):
|
|
144
|
+
frame = sorted(compute_svg_frame_init(self.nodes), key=lambda x: x.zindex)
|
|
125
145
|
|
|
146
|
+
width = (1 << (1 + log2(count_variables(self.nodes)))) + 4
|
|
126
147
|
height = compute_height(self.nodes) + 1
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
|
|
149
|
+
elements = [ShapeAnim.from_single_frame(x) for x in frame]
|
|
150
|
+
|
|
151
|
+
# prefered size in pixels
|
|
152
|
+
H = height * 40
|
|
153
|
+
W = width * 40
|
|
154
|
+
|
|
155
|
+
svg_element = SVG(
|
|
132
156
|
xmlns="http://www.w3.org/2000/svg",
|
|
133
|
-
viewBox=f"{
|
|
134
|
-
height=f"{35 * height}px", # type: ignore
|
|
157
|
+
viewBox=f"{-1} 0 {width} {height}", # type: ignore
|
|
135
158
|
elements=elements,
|
|
159
|
+
style=f"max-height:{H}px; margin-left: clamp(0px, calc(100% - {W}px), 100px)",
|
|
136
160
|
).as_str()
|
|
161
|
+
return f"<div>{svg_element}</div>"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def offset_var(x: Union[int, str], offset: int) -> Union[int, str]:
|
|
165
|
+
if isinstance(x, int):
|
|
166
|
+
return x + offset
|
|
167
|
+
return x
|
|
137
168
|
|
|
138
169
|
|
|
139
170
|
class L:
|
|
@@ -149,28 +180,37 @@ class L:
|
|
|
149
180
|
self.lambdas[name] = self.n
|
|
150
181
|
return self
|
|
151
182
|
|
|
152
|
-
def _append_subtree_or_subexpression(self, t: Union[str, "L"]):
|
|
183
|
+
def _append_subtree_or_subexpression(self, t: Union[str, "L", Term]):
|
|
184
|
+
offset = self.n
|
|
153
185
|
if isinstance(t, L):
|
|
154
|
-
offset = self.n
|
|
155
186
|
for i, x in t.refs:
|
|
156
|
-
self.refs.append((offset + i, t.lambdas.get(x, x)))
|
|
187
|
+
self.refs.append((offset + i, offset_var(t.lambdas.get(x, x), offset)))
|
|
157
188
|
|
|
158
189
|
for i, x in t.args:
|
|
159
190
|
self.args.append((offset + i, offset + x))
|
|
160
191
|
self.n += t.n
|
|
192
|
+
elif isinstance(t, Term):
|
|
193
|
+
for i, x in t.nodes.select("id", "ref").drop_nulls().iter_rows():
|
|
194
|
+
self.refs.append((offset + i, offset + x))
|
|
195
|
+
for i, x in t.nodes.select("id", "arg").drop_nulls().iter_rows():
|
|
196
|
+
self.args.append((offset + i, offset + x))
|
|
197
|
+
self.n += len(t.nodes)
|
|
161
198
|
else:
|
|
162
199
|
assert isinstance(t, str)
|
|
163
200
|
self.refs.append((self.n, t))
|
|
164
201
|
self.n += 1
|
|
165
202
|
|
|
166
|
-
def _(self, x: Union[str, "L"]) -> "L":
|
|
203
|
+
def _(self, x: Union[str, "L", Term]) -> "L":
|
|
167
204
|
self.last_ = self.n
|
|
168
205
|
self._append_subtree_or_subexpression(x)
|
|
169
206
|
return self
|
|
170
207
|
|
|
171
|
-
def call(self, arg: Union[str, "L"]) -> "L":
|
|
208
|
+
def call(self, arg: Union[str, "L", Term]) -> "L":
|
|
172
209
|
assert self.last_ is not None
|
|
173
|
-
self.refs = [
|
|
210
|
+
self.refs = [
|
|
211
|
+
(i + 1, offset_var(x, 1)) if i >= self.last_ else (i, x)
|
|
212
|
+
for (i, x) in self.refs
|
|
213
|
+
]
|
|
174
214
|
self.args = [
|
|
175
215
|
(i + 1, x + 1) if i >= self.last_ else (i, x) for (i, x) in self.args
|
|
176
216
|
]
|
|
@@ -182,7 +222,15 @@ class L:
|
|
|
182
222
|
return self
|
|
183
223
|
|
|
184
224
|
def build(self) -> "Term":
|
|
185
|
-
|
|
225
|
+
def bind_var(x: Union[str, int]) -> int:
|
|
226
|
+
# check that all remaining unbound variables are bound to this lambda
|
|
227
|
+
if isinstance(x, int):
|
|
228
|
+
return x
|
|
229
|
+
if x not in self.lambdas:
|
|
230
|
+
raise ValueError(f"variable {x} is not bound to any lambda")
|
|
231
|
+
return self.lambdas[x]
|
|
232
|
+
|
|
233
|
+
self.refs = [(i, bind_var(x)) for i, x in self.refs]
|
|
186
234
|
ref = pl.from_records(
|
|
187
235
|
self.refs, orient="row", schema={"id": pl.UInt32, "ref": pl.UInt32}
|
|
188
236
|
)
|
curryparty/display.py
CHANGED
|
@@ -3,7 +3,16 @@ from typing import Any, Iterable, Optional, Union
|
|
|
3
3
|
import polars as pl
|
|
4
4
|
import svg
|
|
5
5
|
|
|
6
|
-
from .utils import Interval
|
|
6
|
+
from .utils import Interval, ShapeAnimFrame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compute_height(nodes: pl.DataFrame):
|
|
10
|
+
_, y = compute_layout(nodes)
|
|
11
|
+
return max(interval[1] for interval in y.values() if interval) + 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def count_variables(nodes: pl.DataFrame):
|
|
15
|
+
return nodes["ref"].count()
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
def compute_layout(
|
|
@@ -21,7 +30,7 @@ def compute_layout(
|
|
|
21
30
|
else:
|
|
22
31
|
y[child] = y[node].shift(1)
|
|
23
32
|
|
|
24
|
-
next_var_x = nodes
|
|
33
|
+
next_var_x = count_variables(nodes) - 1
|
|
25
34
|
|
|
26
35
|
for node, ref, arg in (
|
|
27
36
|
nodes.sort("id", descending=True).select("id", "ref", "arg").iter_rows()
|
|
@@ -38,10 +47,6 @@ def compute_layout(
|
|
|
38
47
|
y[node] = y[child] | y[node]
|
|
39
48
|
return x, y
|
|
40
49
|
|
|
41
|
-
def compute_height(nodes: pl.DataFrame):
|
|
42
|
-
_, y = compute_layout(nodes)
|
|
43
|
-
return max(interval[1] for interval in y.values() if interval)
|
|
44
|
-
|
|
45
50
|
|
|
46
51
|
def draw(
|
|
47
52
|
x: dict[Union[int, tuple[int, int]], Interval],
|
|
@@ -50,42 +55,60 @@ def draw(
|
|
|
50
55
|
ref: Optional[int],
|
|
51
56
|
arg: Optional[int],
|
|
52
57
|
key: Any,
|
|
58
|
+
idx: int,
|
|
53
59
|
replaced=False,
|
|
54
60
|
removed=False,
|
|
55
|
-
|
|
61
|
+
hide_arg=False,
|
|
62
|
+
) -> Iterable[ShapeAnimFrame]:
|
|
56
63
|
x_node = x[i_node]
|
|
57
64
|
y_node = y[i_node]
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
color = "blue"
|
|
65
|
+
if arg is not None or removed:
|
|
66
|
+
color = "transparent"
|
|
67
|
+
elif replaced or removed:
|
|
68
|
+
color = "green"
|
|
69
|
+
elif ref is not None:
|
|
70
|
+
color = "red"
|
|
71
|
+
else:
|
|
72
|
+
color = "blue"
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
r = svg.Rect(
|
|
75
|
+
height=0.8,
|
|
76
|
+
stroke_width=0.05,
|
|
77
|
+
stroke="gray",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
yield ShapeAnimFrame(
|
|
81
|
+
element=r,
|
|
82
|
+
key=("r", key),
|
|
83
|
+
idx=idx,
|
|
84
|
+
attrs={
|
|
85
|
+
"x": 0.1 + x_node[0],
|
|
86
|
+
"y": 0.1 + y_node[0] + (1 if replaced else 0),
|
|
87
|
+
"width": 0.8 + x_node[1] - x_node[0],
|
|
88
|
+
"fill_opacity": 1 if arg is None else 0,
|
|
89
|
+
"fill": color,
|
|
90
|
+
},
|
|
91
|
+
zindex=0,
|
|
92
|
+
)
|
|
93
|
+
if arg is not None and not hide_arg:
|
|
73
94
|
r = svg.Rect(
|
|
74
95
|
height=0.8,
|
|
75
|
-
stroke_width=
|
|
76
|
-
stroke=
|
|
96
|
+
stroke_width=0.1,
|
|
97
|
+
stroke="orange",
|
|
77
98
|
)
|
|
78
99
|
|
|
79
|
-
yield (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
yield ShapeAnimFrame(
|
|
101
|
+
element=r,
|
|
102
|
+
key=("a", key),
|
|
103
|
+
idx=idx,
|
|
104
|
+
attrs={
|
|
83
105
|
"x": 0.1 + x_node[0],
|
|
84
106
|
"y": 0.1 + y_node[0],
|
|
85
107
|
"width": 0.8 + x_node[1] - x_node[0],
|
|
86
108
|
"fill_opacity": 1 if arg is None else 0,
|
|
87
109
|
"fill": color,
|
|
88
110
|
},
|
|
111
|
+
zindex=1,
|
|
89
112
|
)
|
|
90
113
|
|
|
91
114
|
if ref is not None:
|
|
@@ -94,63 +117,65 @@ def draw(
|
|
|
94
117
|
stroke_width=0.2,
|
|
95
118
|
stroke="gray",
|
|
96
119
|
)
|
|
97
|
-
yield (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
yield ShapeAnimFrame(
|
|
121
|
+
element=e,
|
|
122
|
+
key=("l", key),
|
|
123
|
+
idx=idx,
|
|
124
|
+
attrs={
|
|
101
125
|
"x1": x_node[0] + 0.5,
|
|
102
|
-
"y1":
|
|
126
|
+
"y1": y_ref[0] + 0.9,
|
|
103
127
|
"x2": x_node[0] + 0.5,
|
|
104
|
-
"y2":
|
|
128
|
+
"y2": y_node[0] + 0.1 + (1 if replaced else 0),
|
|
105
129
|
"stroke": "green" if replaced else "gray",
|
|
106
130
|
},
|
|
131
|
+
zindex=2,
|
|
107
132
|
)
|
|
108
133
|
|
|
109
134
|
if arg is not None:
|
|
110
135
|
x_arg = x[arg]
|
|
111
|
-
y_arg = y[arg]
|
|
112
136
|
e1 = svg.Line(
|
|
113
137
|
stroke="black",
|
|
114
138
|
stroke_width=0.05,
|
|
115
139
|
)
|
|
116
140
|
e2 = svg.Circle(fill="black", r=0.1)
|
|
117
141
|
if not removed:
|
|
118
|
-
yield (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
yield ShapeAnimFrame(
|
|
143
|
+
element=e1,
|
|
144
|
+
key=("b", key),
|
|
145
|
+
idx=idx,
|
|
146
|
+
attrs={
|
|
122
147
|
"x1": 0.5 + x_node[1],
|
|
123
148
|
"y1": 0.5 + y_node[0],
|
|
124
149
|
"x2": 0.5 + x_arg[0],
|
|
125
|
-
"y2": 0.5 +
|
|
150
|
+
"y2": 0.5 + y_node[0],
|
|
126
151
|
},
|
|
152
|
+
zindex=3,
|
|
127
153
|
)
|
|
128
|
-
yield (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
yield ShapeAnimFrame(
|
|
155
|
+
element=e2,
|
|
156
|
+
key=("c", key),
|
|
157
|
+
idx=idx,
|
|
158
|
+
attrs={
|
|
132
159
|
"cx": 0.5 + x_node[1],
|
|
133
160
|
"cy": 0.5 + y_node[0],
|
|
134
161
|
},
|
|
162
|
+
zindex=3,
|
|
135
163
|
)
|
|
136
164
|
|
|
137
165
|
|
|
138
166
|
def compute_svg_frame_init(
|
|
139
|
-
nodes: pl.DataFrame,
|
|
140
|
-
) -> Iterable[
|
|
167
|
+
nodes: pl.DataFrame, idx: int = 0
|
|
168
|
+
) -> Iterable[ShapeAnimFrame]:
|
|
141
169
|
x, y = compute_layout(nodes)
|
|
142
170
|
for target_id, ref, arg in (
|
|
143
171
|
nodes.select("id", "ref", "arg").sort("id", descending=True).iter_rows()
|
|
144
172
|
):
|
|
145
|
-
yield from draw(x, y, target_id, ref, arg, target_id)
|
|
173
|
+
yield from draw(x, y, target_id, ref, arg, key=target_id, idx=idx)
|
|
146
174
|
|
|
147
175
|
|
|
148
176
|
def compute_svg_frame_phase_a(
|
|
149
|
-
nodes: pl.DataFrame,
|
|
150
|
-
|
|
151
|
-
b_subtree: pl.DataFrame,
|
|
152
|
-
vars: pl.Series,
|
|
153
|
-
):
|
|
177
|
+
nodes: pl.DataFrame, lamb: int, b_subtree: pl.DataFrame, vars: pl.Series, idx: int
|
|
178
|
+
) -> Iterable[ShapeAnimFrame]:
|
|
154
179
|
redex = lamb - 1 if lamb is not None else None
|
|
155
180
|
b_width = b_subtree.count()["ref"].item()
|
|
156
181
|
x, y = compute_layout(nodes, lamb=lamb, replaced_var_width=b_width)
|
|
@@ -164,7 +189,8 @@ def compute_svg_frame_phase_a(
|
|
|
164
189
|
target_id,
|
|
165
190
|
ref,
|
|
166
191
|
arg,
|
|
167
|
-
target_id,
|
|
192
|
+
key=target_id,
|
|
193
|
+
idx=idx,
|
|
168
194
|
replaced=replaced,
|
|
169
195
|
removed=(target_id == lamb or target_id == redex),
|
|
170
196
|
)
|
|
@@ -173,7 +199,7 @@ def compute_svg_frame_phase_a(
|
|
|
173
199
|
for minor, ref, arg in (
|
|
174
200
|
b_subtree.select("id", "ref", "arg").sort("id", descending=True).iter_rows()
|
|
175
201
|
):
|
|
176
|
-
yield from draw(x, y, minor, ref, arg, key=(v, minor))
|
|
202
|
+
yield from draw(x, y, minor, ref, arg, key=(v, minor), idx=idx)
|
|
177
203
|
|
|
178
204
|
|
|
179
205
|
def compute_svg_frame_phase_b(
|
|
@@ -181,7 +207,8 @@ def compute_svg_frame_phase_b(
|
|
|
181
207
|
lamb: int,
|
|
182
208
|
b_subtree: pl.DataFrame,
|
|
183
209
|
new_nodes: pl.DataFrame,
|
|
184
|
-
|
|
210
|
+
idx: int,
|
|
211
|
+
) -> Iterable[ShapeAnimFrame]:
|
|
185
212
|
b_width = b_subtree.count()["ref"].item()
|
|
186
213
|
b = b_subtree["id"][0]
|
|
187
214
|
x, y = compute_layout(nodes, lamb=lamb, replaced_var_width=b_width)
|
|
@@ -192,7 +219,7 @@ def compute_svg_frame_phase_b(
|
|
|
192
219
|
v = bid["major"]
|
|
193
220
|
minor = bid["minor"]
|
|
194
221
|
delta_x = x[v][0] - b_x
|
|
195
|
-
delta_y = y[v][0] - b_y
|
|
222
|
+
delta_y = y[v][0] - b_y + 1
|
|
196
223
|
x[(v, minor)] = x[minor].shift(delta_x)
|
|
197
224
|
y[(v, minor)] = y[minor].shift(delta_y)
|
|
198
225
|
|
|
@@ -217,13 +244,21 @@ def compute_svg_frame_phase_b(
|
|
|
217
244
|
if bid_arg["major"] != bid_arg["minor"]
|
|
218
245
|
else bid_arg["minor"]
|
|
219
246
|
)
|
|
220
|
-
if bid_arg["minor"] == b:
|
|
221
|
-
arg = bid_arg["major"]
|
|
222
247
|
key = (v, minor) if minor != v else minor
|
|
223
|
-
yield from draw(
|
|
248
|
+
yield from draw(
|
|
249
|
+
x,
|
|
250
|
+
y,
|
|
251
|
+
key,
|
|
252
|
+
ref,
|
|
253
|
+
arg,
|
|
254
|
+
key=key,
|
|
255
|
+
idx=idx,
|
|
256
|
+
)
|
|
224
257
|
|
|
225
258
|
|
|
226
|
-
def compute_svg_frame_final(
|
|
259
|
+
def compute_svg_frame_final(
|
|
260
|
+
reduced: pl.DataFrame, idx: int
|
|
261
|
+
) -> Iterable[ShapeAnimFrame]:
|
|
227
262
|
x, y = compute_layout(reduced)
|
|
228
263
|
for target_id, bid, ref, arg in (
|
|
229
264
|
reduced.select("id", "bid", "ref", "arg")
|
|
@@ -233,4 +268,4 @@ def compute_svg_frame_final(reduced: pl.DataFrame):
|
|
|
233
268
|
minor = bid["minor"]
|
|
234
269
|
major = bid["major"]
|
|
235
270
|
key = (major, minor) if minor != major else minor
|
|
236
|
-
yield from draw(x, y, target_id, ref, arg, key)
|
|
271
|
+
yield from draw(x, y, target_id, ref, arg, key, idx=idx)
|
curryparty/utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from datetime import timedelta
|
|
3
3
|
from typing import Any, Iterable, Optional
|
|
4
4
|
|
|
@@ -33,30 +33,78 @@ class Interval:
|
|
|
33
33
|
return Interval((self.values[0] + offset, self.values[1] + offset))
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@dataclass
|
|
37
|
+
class ShapeAnimFrame:
|
|
38
|
+
element: Element
|
|
39
|
+
key: Any
|
|
40
|
+
idx: int
|
|
41
|
+
attrs: dict[str, Any]
|
|
42
|
+
zindex: int = 0
|
|
43
|
+
|
|
44
|
+
def apply_attributes(self):
|
|
45
|
+
for name, v in self.attrs.items():
|
|
46
|
+
self.element.__setattr__(name, v)
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
@dataclass
|
|
37
50
|
class ShapeAnim:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
n: int
|
|
51
|
+
key: Any
|
|
52
|
+
element: Element
|
|
53
|
+
zindex: int
|
|
42
54
|
duration: int
|
|
55
|
+
attributes: set[str] = field(default_factory=set)
|
|
56
|
+
values: dict[tuple[int, str], Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def from_single_frame(frame: ShapeAnimFrame) -> Element:
|
|
60
|
+
frame.apply_attributes()
|
|
61
|
+
return frame.element
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def from_frames(frames: list[ShapeAnimFrame], duration: int = 7) -> "ShapeAnim":
|
|
65
|
+
if not frames:
|
|
66
|
+
raise ValueError("frames list cannot be empty")
|
|
67
|
+
|
|
68
|
+
frames.sort(key=lambda f: f.idx)
|
|
69
|
+
|
|
70
|
+
zindex = frames[0].zindex
|
|
71
|
+
for f in frames[1:]:
|
|
72
|
+
if f.zindex != zindex:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"zindex mismatch for key {frames[0].key}: got {f.zindex}, expected {zindex}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
anim = ShapeAnim(
|
|
78
|
+
key=frames[0].key,
|
|
79
|
+
element=frames[0].element,
|
|
80
|
+
zindex=zindex,
|
|
81
|
+
duration=duration,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for f in frames:
|
|
85
|
+
for name, v in f.attrs.items():
|
|
86
|
+
anim.attributes.add(name)
|
|
87
|
+
anim.values[f.idx, name] = v
|
|
88
|
+
|
|
89
|
+
return anim
|
|
43
90
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
@staticmethod
|
|
92
|
+
def group_by_key(
|
|
93
|
+
frames: Iterable[ShapeAnimFrame],
|
|
94
|
+
) -> dict[Any, list[ShapeAnimFrame]]:
|
|
95
|
+
groups: dict[Any, list[ShapeAnimFrame]] = {}
|
|
96
|
+
for frame in frames:
|
|
97
|
+
if frame.key not in groups:
|
|
98
|
+
groups[frame.key] = []
|
|
99
|
+
groups[frame.key].append(frame)
|
|
100
|
+
return groups
|
|
53
101
|
|
|
54
102
|
def append_frame(self, i: int, attributes: Iterable[tuple[str, Any]]):
|
|
55
103
|
for name, v in attributes:
|
|
56
104
|
self.attributes.add(name)
|
|
57
105
|
self.values[i, name] = v
|
|
58
106
|
|
|
59
|
-
def to_element(self, n: int, begin: str, reset: str):
|
|
107
|
+
def to_element(self, n: int, begin: str, reset: str) -> Element:
|
|
60
108
|
elements = []
|
|
61
109
|
|
|
62
110
|
visible = [
|
|
@@ -74,7 +122,7 @@ class ShapeAnim:
|
|
|
74
122
|
for i in range(n):
|
|
75
123
|
if (i, name) not in self.values:
|
|
76
124
|
self.values[i, name] = non_nulls[0]
|
|
77
|
-
self.
|
|
125
|
+
self.element.__setattr__(name, non_nulls[0])
|
|
78
126
|
|
|
79
127
|
elements.append(
|
|
80
128
|
Animate(
|
|
@@ -113,7 +161,6 @@ class ShapeAnim:
|
|
|
113
161
|
)
|
|
114
162
|
)
|
|
115
163
|
|
|
116
|
-
assert not self.
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
119
|
-
return self.shape
|
|
164
|
+
assert not self.element.elements
|
|
165
|
+
self.element.elements = elements
|
|
166
|
+
return self.element
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: curryparty
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python playground to learn lambda calculus
|
|
5
5
|
Author: Antonin P
|
|
6
6
|
Author-email: Antonin P <antonin.peronnet@telecom-paris.fr>
|
|
@@ -8,6 +8,11 @@ Requires-Dist: svg-py>=1.9.2
|
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://github.com/rambip/curryparty/blob/main/logo.svg?raw=true" width="500px"/>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
# Curry party
|
|
12
17
|
|
|
13
18
|
`curryparty` is a library created to explore, visualize and teach lambda-calculus concepts.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
curryparty/__init__.py,sha256=tRRIyfo2D0dywpX0xgzW_IejlatOW5t-c8KIOEpc3yc,8187
|
|
2
|
+
curryparty/core.py,sha256=9vQJlUeYXJ71SkRV6tUiw1RSu7gUJu4f_e6w2Zt-tLQ,3665
|
|
3
|
+
curryparty/display.py,sha256=fstY7lCPjQjvSgaTJGnoYOqrbXHbXytNTqPJ5EZ2h1E,7833
|
|
4
|
+
curryparty/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
curryparty/utils.py,sha256=56rRpMiDXbEQjFEwoKxuGb3Nch4qJd8OgwmNxkfl-oE,4937
|
|
6
|
+
curryparty-0.3.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
7
|
+
curryparty-0.3.0.dist-info/METADATA,sha256=40Yfvx7_H20iYQy6RxNlttEB9r5oYKQcYLH1Yx7dbpI,2259
|
|
8
|
+
curryparty-0.3.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
curryparty/__init__.py,sha256=UU7e1gFjhOB53kKea6k9hQkOVQzglNtUYpV2FarvcKo,6522
|
|
2
|
-
curryparty/core.py,sha256=9vQJlUeYXJ71SkRV6tUiw1RSu7gUJu4f_e6w2Zt-tLQ,3665
|
|
3
|
-
curryparty/display.py,sha256=3u80YvTYBe3mfexng2fiIiNwNAb_S7eYE5afY08f49M,6925
|
|
4
|
-
curryparty/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
curryparty/utils.py,sha256=XvOGyYiIuMcmi-bnBNLdgSQQ69vT0pJ3qJXgwmfhK28,3494
|
|
6
|
-
curryparty-0.2.2.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
|
|
7
|
-
curryparty-0.2.2.dist-info/METADATA,sha256=bdAGKJiQhTP3iEc7NCfz--FHiF7Gy0X9w9MDzuHE2Z8,2139
|
|
8
|
-
curryparty-0.2.2.dist-info/RECORD,,
|