curryparty 0.3.1__py3-none-any.whl → 0.3.3__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 +49 -47
- curryparty/core.py +292 -102
- curryparty/display.py +140 -106
- {curryparty-0.3.1.dist-info → curryparty-0.3.3.dist-info}/METADATA +1 -1
- curryparty-0.3.3.dist-info/RECORD +8 -0
- {curryparty-0.3.1.dist-info → curryparty-0.3.3.dist-info}/WHEEL +1 -1
- curryparty-0.3.1.dist-info/RECORD +0 -8
curryparty/__init__.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""The `curryparty` library, a playground to learn lambda-calculus.
|
|
2
|
+
|
|
3
|
+
This library is intended to be used in an interactive
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
from typing import Iterable, List, Optional, Union
|
|
2
7
|
|
|
3
8
|
try:
|
|
@@ -8,9 +13,9 @@ except ImportError:
|
|
|
8
13
|
)
|
|
9
14
|
import uuid
|
|
10
15
|
|
|
11
|
-
from svg import SVG, Rect
|
|
16
|
+
from svg import SVG, Length, Rect, ViewBoxSpec
|
|
12
17
|
|
|
13
|
-
from .core import
|
|
18
|
+
from .core import AbstractTerm
|
|
14
19
|
from .display import (
|
|
15
20
|
compute_height,
|
|
16
21
|
compute_svg_frame_final,
|
|
@@ -33,24 +38,18 @@ def log2(n):
|
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class Term:
|
|
36
|
-
def __init__(self,
|
|
37
|
-
|
|
38
|
-
f"{nodes.schema} is different from expected {SCHEMA}"
|
|
39
|
-
)
|
|
40
|
-
self.nodes = nodes
|
|
41
|
-
self.lamb = None
|
|
41
|
+
def __init__(self, data: AbstractTerm):
|
|
42
|
+
self.data = data
|
|
42
43
|
|
|
43
44
|
def __call__(self, other: "Term") -> "Term":
|
|
44
|
-
return Term(
|
|
45
|
+
return Term((self.data)(other.data))
|
|
45
46
|
|
|
46
47
|
def beta(self) -> Optional["Term"]:
|
|
47
|
-
candidates =
|
|
48
|
-
|
|
48
|
+
candidates = self.data.find_redexes()
|
|
49
|
+
redex = next(candidates, None)
|
|
50
|
+
if redex is None:
|
|
49
51
|
return None
|
|
50
|
-
|
|
51
|
-
self.lamb = lamb
|
|
52
|
-
self.b = b
|
|
53
|
-
reduced = beta_reduce(self.nodes, lamb, b)
|
|
52
|
+
reduced = self.data.beta_reduce(redex)
|
|
54
53
|
return Term(reduced)
|
|
55
54
|
|
|
56
55
|
def reduce(self):
|
|
@@ -61,41 +60,44 @@ class Term:
|
|
|
61
60
|
|
|
62
61
|
def reduction_chain(self) -> Iterable["Term"]:
|
|
63
62
|
term = self
|
|
64
|
-
while
|
|
63
|
+
while term is not None:
|
|
65
64
|
yield term
|
|
66
65
|
term = term.beta()
|
|
67
|
-
if term is None:
|
|
68
|
-
break
|
|
69
66
|
|
|
70
67
|
def show_beta(self, duration=7):
|
|
71
68
|
"""
|
|
72
69
|
Generates an HTML representation that toggles visibility between
|
|
73
70
|
a static state and a SMIL animation on hover using pure CSS.
|
|
74
71
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
|
|
73
|
+
candidates = self.data.find_redexes()
|
|
74
|
+
redex = next(candidates, None)
|
|
75
|
+
if redex is None:
|
|
77
76
|
return self._repr_html_()
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
lamb = self.data.node(redex).get_left()
|
|
79
|
+
assert lamb is not None
|
|
80
|
+
b = self.data.node(redex).get_arg()
|
|
81
|
+
assert b is not None
|
|
82
|
+
new_nodes = self.data.beta_reduce(redex)
|
|
83
|
+
vars = list(self.data.find_variables(lamb))
|
|
84
|
+
b_subtree = list(self.data.get_subtree(b))
|
|
85
|
+
height = min(compute_height(self.data), compute_height(new_nodes)) * 2
|
|
86
|
+
if count_variables(self.data) == 0:
|
|
85
87
|
return "no width"
|
|
86
|
-
raw_width = max(count_variables(self.
|
|
88
|
+
raw_width = max(count_variables(self.data), count_variables(new_nodes))
|
|
87
89
|
width = 1 << (1 + log2(raw_width))
|
|
88
90
|
frame_data: list[ShapeAnimFrame] = []
|
|
89
91
|
N_STEPS = 6
|
|
90
92
|
|
|
91
93
|
for t in range(N_STEPS):
|
|
92
94
|
if t == 0:
|
|
93
|
-
items = compute_svg_frame_init(self.
|
|
95
|
+
items = compute_svg_frame_init(self.data, t)
|
|
94
96
|
elif t == 1 or t == 2:
|
|
95
|
-
items = compute_svg_frame_phase_a(self.
|
|
97
|
+
items = compute_svg_frame_phase_a(self.data, redex, b_subtree, vars, t)
|
|
96
98
|
elif t == 3 or t == 4:
|
|
97
99
|
items = compute_svg_frame_phase_b(
|
|
98
|
-
self.
|
|
100
|
+
self.data, redex, b_subtree, new_nodes, t
|
|
99
101
|
)
|
|
100
102
|
else:
|
|
101
103
|
items = compute_svg_frame_final(new_nodes, t)
|
|
@@ -114,10 +116,10 @@ class Term:
|
|
|
114
116
|
anim_elements.append(
|
|
115
117
|
Rect(
|
|
116
118
|
id=box_id,
|
|
117
|
-
x
|
|
118
|
-
y=
|
|
119
|
-
width="
|
|
120
|
-
height="
|
|
119
|
+
x=-width,
|
|
120
|
+
y=0,
|
|
121
|
+
width=Length(100, "%"),
|
|
122
|
+
height=Length(100, "%"),
|
|
121
123
|
fill="transparent",
|
|
122
124
|
)
|
|
123
125
|
)
|
|
@@ -126,7 +128,7 @@ class Term:
|
|
|
126
128
|
H = height * 40
|
|
127
129
|
anim_svg = SVG(
|
|
128
130
|
xmlns="http://www.w3.org/2000/svg",
|
|
129
|
-
viewBox=
|
|
131
|
+
viewBox=ViewBoxSpec(-width, 0, 2 * width, height),
|
|
130
132
|
style=f"max-height:{H}px",
|
|
131
133
|
elements=anim_elements,
|
|
132
134
|
).as_str()
|
|
@@ -140,11 +142,11 @@ class Term:
|
|
|
140
142
|
"</div>"
|
|
141
143
|
)
|
|
142
144
|
|
|
143
|
-
def _repr_html_(self
|
|
144
|
-
frame = sorted(compute_svg_frame_init(self.
|
|
145
|
+
def _repr_html_(self):
|
|
146
|
+
frame = sorted(compute_svg_frame_init(self.data), key=lambda x: x.zindex)
|
|
145
147
|
|
|
146
|
-
width = (1 << (1 + log2(count_variables(self.
|
|
147
|
-
height = compute_height(self.
|
|
148
|
+
width = (1 << (1 + log2(count_variables(self.data)))) + 4
|
|
149
|
+
height = compute_height(self.data) + 1
|
|
148
150
|
|
|
149
151
|
elements = [ShapeAnim.from_single_frame(x) for x in frame]
|
|
150
152
|
|
|
@@ -152,13 +154,12 @@ class Term:
|
|
|
152
154
|
H = height * 40
|
|
153
155
|
W = width * 40
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
return SVG(
|
|
156
158
|
xmlns="http://www.w3.org/2000/svg",
|
|
157
|
-
viewBox=
|
|
159
|
+
viewBox=ViewBoxSpec(-1, 0, width, height),
|
|
158
160
|
elements=elements,
|
|
159
161
|
style=f"max-height:{H}px; margin-left: clamp(0px, calc(100% - {W}px), 100px)",
|
|
160
162
|
).as_str()
|
|
161
|
-
return f"<div>{svg_element}</div>"
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def offset_var(x: Union[int, str], offset: int) -> Union[int, str]:
|
|
@@ -190,11 +191,12 @@ class L:
|
|
|
190
191
|
self.args.append((offset + i, offset + x))
|
|
191
192
|
self.n += t.n
|
|
192
193
|
elif isinstance(t, Term):
|
|
193
|
-
|
|
194
|
+
# fixme: encapsulate
|
|
195
|
+
for i, x in t.data.nodes.select("id", "ref").drop_nulls().iter_rows():
|
|
194
196
|
self.refs.append((offset + i, offset + x))
|
|
195
|
-
for i, x in t.nodes.select("id", "arg").drop_nulls().iter_rows():
|
|
197
|
+
for i, x in t.data.nodes.select("id", "arg").drop_nulls().iter_rows():
|
|
196
198
|
self.args.append((offset + i, offset + x))
|
|
197
|
-
self.n += len(t.nodes)
|
|
199
|
+
self.n += len(t.data.nodes)
|
|
198
200
|
else:
|
|
199
201
|
assert isinstance(t, str)
|
|
200
202
|
self.refs.append((self.n, t))
|
|
@@ -242,8 +244,8 @@ class L:
|
|
|
242
244
|
.to_frame()
|
|
243
245
|
.join(ref, on="id", how="left")
|
|
244
246
|
.join(arg, on="id", how="left")
|
|
245
|
-
).with_columns(
|
|
246
|
-
return Term(data)
|
|
247
|
+
).with_columns(prev=None)
|
|
248
|
+
return Term(AbstractTerm(data))
|
|
247
249
|
|
|
248
250
|
|
|
249
251
|
def V(name: str) -> L:
|
curryparty/core.py
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
|
+
"""Core engine of lambda-calculus.
|
|
2
|
+
|
|
3
|
+
This file defines 2 main types:
|
|
4
|
+
- `AbstractTerm`, a Lambda-calculus term
|
|
5
|
+
- `NodeId`, a node in the term.
|
|
6
|
+
|
|
7
|
+
Because this is the most complex module of the library, it is useful to define some terms.
|
|
8
|
+
I will refer to these terms in the above comments for concision.
|
|
9
|
+
|
|
10
|
+
# 1. Term
|
|
11
|
+
|
|
12
|
+
A `Term` is a complete Lambda-calculus expression.
|
|
13
|
+
|
|
14
|
+
# 2. Node
|
|
15
|
+
|
|
16
|
+
A `Node` is what constitutes a `Term`. There are 3 types of Nodes.
|
|
17
|
+
|
|
18
|
+
2.1 There is `Lambda` Node, that has a single children
|
|
19
|
+
2.2 There is a `Variable` Node. It has no children and refers to a `Lambda` Node.
|
|
20
|
+
We say the variable is `bound` to the corresponding Lambda.
|
|
21
|
+
2.3 There is an `Application` Node. It has 2 children.
|
|
22
|
+
The first children is the function and the second children is the argument.
|
|
23
|
+
|
|
24
|
+
# 3. Subtrees
|
|
25
|
+
|
|
26
|
+
Each `Node` in the `Term` has a subtree. It is made up of itself, his children, his grandchildren ... and so on.
|
|
27
|
+
The **strict** subtree of a node is its subtree but without the node itself.
|
|
28
|
+
|
|
29
|
+
# 4. Redex
|
|
30
|
+
|
|
31
|
+
A "Redex Node" is a `Node` in the `Term` that we can reduce. It must have the following properties:
|
|
32
|
+
|
|
33
|
+
4.1 A "Redex Node" is a `Node` of type "Application". The subtree of the "Redex node" is called simply the "Redex".
|
|
34
|
+
4.2 The first children of the Redex node must be a lambda. The trunk node is the child of the lambda node.
|
|
35
|
+
The "Trunk" is defined as the strict subtree of the lambda node (i.e the subtree of the trunk).
|
|
36
|
+
4.3 The variables that are bound to the "trunk node" are called the "Stumps".
|
|
37
|
+
4.4 The second children of the Redex node can be anything. We call it the **Substitute node** of the Redex.
|
|
38
|
+
The "Subsitute" is simply the subtree of the "substitute node" of the Redex.
|
|
39
|
+
|
|
40
|
+
# 5. Beta-reduction
|
|
41
|
+
|
|
42
|
+
The operation of "beta-reduction" transforms a Redex inside the Term, leaving the rest of the term unchanged.
|
|
43
|
+
The beta-reduction of a Term on a Redex R consists in 2 steps:
|
|
44
|
+
|
|
45
|
+
5.1 Beta-reduction replaces all stumps by a copy of the substitute
|
|
46
|
+
5.1.1 If an application had a stump node as one of its children, the stump node is replaced by a copy of the substitute.
|
|
47
|
+
5.1.2 All variables in the substitute are now bound to the corresponding duplicated lambda in their duplicated substitute.
|
|
48
|
+
But if a variable was bound to a lambda higher in the tree (not in the substitute), the bound remains
|
|
49
|
+
5.2 Beta-reduction removes the redex node and the lambda node
|
|
50
|
+
5.3 Beta-reduction removes the substitute (it has already been duplicated for each stump)
|
|
51
|
+
5.4 All we have left is the trunk, and it is placed where the original redex was.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
from dataclasses import dataclass
|
|
58
|
+
from typing import Generator, List, NewType, Optional
|
|
59
|
+
|
|
1
60
|
import polars as pl
|
|
2
61
|
from polars import Schema, UInt32
|
|
3
62
|
|
|
63
|
+
__all__ = ["AbstractTerm", "NodeId"]
|
|
64
|
+
|
|
65
|
+
# See `Node`
|
|
66
|
+
PREV_ID_SCHEMA = {"stump": UInt32, "local": UInt32}
|
|
67
|
+
|
|
4
68
|
SCHEMA = Schema(
|
|
5
|
-
{
|
|
6
|
-
"id": UInt32,
|
|
7
|
-
"ref": UInt32,
|
|
8
|
-
"arg": UInt32,
|
|
9
|
-
"bid": pl.Struct({"major": UInt32, "minor": UInt32}),
|
|
10
|
-
},
|
|
69
|
+
{"id": UInt32, "ref": UInt32, "arg": UInt32, "prev": pl.Struct(PREV_ID_SCHEMA)},
|
|
11
70
|
)
|
|
12
71
|
|
|
13
72
|
|
|
@@ -16,121 +75,252 @@ def _shift(nodes: pl.DataFrame, offset: int):
|
|
|
16
75
|
pl.col("id") + offset,
|
|
17
76
|
pl.col("ref") + offset,
|
|
18
77
|
pl.col("arg") + offset,
|
|
19
|
-
|
|
78
|
+
prev=None,
|
|
20
79
|
)
|
|
21
80
|
|
|
22
81
|
|
|
23
|
-
|
|
24
|
-
n = len(f)
|
|
25
|
-
return pl.concat(
|
|
26
|
-
[
|
|
27
|
-
pl.DataFrame(
|
|
28
|
-
[{"id": 0, "arg": n + 1}],
|
|
29
|
-
schema=SCHEMA,
|
|
30
|
-
),
|
|
31
|
-
_shift(f, 1),
|
|
32
|
-
_shift(x, n + 1),
|
|
33
|
-
],
|
|
34
|
-
)
|
|
82
|
+
NodeId = NewType("NodeId", int)
|
|
35
83
|
|
|
36
84
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
pl.col("arg").is_not_null(),
|
|
42
|
-
pl.col("arg").shift(-1).is_null(),
|
|
43
|
-
pl.col("ref").shift(-1).is_null(),
|
|
44
|
-
)
|
|
45
|
-
.select("id", lamb=pl.col("id") + 1, arg="arg")
|
|
46
|
-
)
|
|
85
|
+
@dataclass
|
|
86
|
+
class Node[NodeId]:
|
|
87
|
+
"""
|
|
88
|
+
Represents a node in the lambda calculus abstract syntax tree.
|
|
47
89
|
|
|
90
|
+
Attributes:
|
|
91
|
+
ref:
|
|
92
|
+
The lambda this variable is bound to, or None if this node is not a variable.
|
|
93
|
+
children:
|
|
94
|
+
Tuple containing either:
|
|
48
95
|
|
|
49
|
-
|
|
50
|
-
|
|
96
|
+
- Single id: child of lambda node
|
|
97
|
+
- Two ids: function and argument of application node
|
|
98
|
+
- Empty tuple: no children (in this case, this is a variable)
|
|
51
99
|
|
|
100
|
+
previous:
|
|
101
|
+
If this NodeId comes from a beta-reduction, corresponds to the id of the node in the term before reduction.
|
|
102
|
+
If the node has been created by the reduction, `previous` is the id in the "Substitute" (see 5.1)
|
|
52
103
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rightmost = root
|
|
57
|
-
while True:
|
|
58
|
-
ref = refs[rightmost]
|
|
59
|
-
if ref is not None:
|
|
60
|
-
return nodes.lazy().filter(pl.col("id").is_between(root, rightmost))
|
|
104
|
+
previous_stump:
|
|
105
|
+
If the node has been created by a beta-reduction, the `Stump` it originates from (see 4.3)
|
|
106
|
+
"""
|
|
61
107
|
|
|
62
|
-
|
|
63
|
-
|
|
108
|
+
ref: Optional[NodeId]
|
|
109
|
+
children: List[NodeId]
|
|
110
|
+
previous_local: NodeId
|
|
111
|
+
previous_stump: NodeId
|
|
64
112
|
|
|
113
|
+
def get_arg(self) -> Optional[NodeId]:
|
|
114
|
+
if len(self.children) == 2:
|
|
115
|
+
return self.children[1]
|
|
116
|
+
return None
|
|
65
117
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
major=pl.col(major_name).fill_null(pl.col(minor_name)),
|
|
71
|
-
minor=minor_replacement.fill_null(pl.col(minor_name)),
|
|
72
|
-
)
|
|
118
|
+
def get_left(self) -> Optional[NodeId]:
|
|
119
|
+
if len(self.children) == 0:
|
|
120
|
+
return None
|
|
121
|
+
return self.children[0]
|
|
73
122
|
|
|
123
|
+
def previous(self):
|
|
124
|
+
return (self.previous_local, self.previous_stump)
|
|
74
125
|
|
|
75
|
-
def beta_reduce(nodes: pl.DataFrame, lamb: int, b: int) -> pl.DataFrame:
|
|
76
|
-
redex = lamb - 1
|
|
77
|
-
a = lamb + 1
|
|
78
126
|
|
|
79
|
-
|
|
127
|
+
class AbstractTerm:
|
|
128
|
+
nodes: pl.DataFrame
|
|
80
129
|
|
|
81
|
-
|
|
130
|
+
def __init__(self, nodes: pl.DataFrame | pl.LazyFrame):
|
|
131
|
+
nodes = nodes.match_to_schema(SCHEMA)
|
|
132
|
+
if isinstance(nodes, pl.LazyFrame):
|
|
133
|
+
self.nodes = nodes.collect()
|
|
134
|
+
else:
|
|
135
|
+
self.nodes = nodes
|
|
82
136
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
nodes.lazy()
|
|
86
|
-
.join(b_subtree, on="id", how="anti")
|
|
87
|
-
.with_columns(arg=pl.col("arg").replace(redex, a))
|
|
88
|
-
)
|
|
137
|
+
def root(self) -> NodeId:
|
|
138
|
+
return NodeId(0)
|
|
89
139
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"id_major",
|
|
108
|
-
"arg",
|
|
109
|
-
minor_replacement=pl.when("replaced_arg").then(b),
|
|
110
|
-
),
|
|
140
|
+
def node(self, node_id: NodeId):
|
|
141
|
+
"""
|
|
142
|
+
Get a node in the expression tree by id
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
id, ref, arg, prev = self.nodes.row(node_id)
|
|
146
|
+
children = []
|
|
147
|
+
if ref is None:
|
|
148
|
+
children.append(id + 1)
|
|
149
|
+
if arg is not None:
|
|
150
|
+
children.append(arg)
|
|
151
|
+
|
|
152
|
+
return Node(
|
|
153
|
+
ref=ref,
|
|
154
|
+
children=children,
|
|
155
|
+
previous_local=prev["local"] if prev else None,
|
|
156
|
+
previous_stump=prev["stump"] if prev else None,
|
|
111
157
|
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
|
|
159
|
+
def find_variables(self, lamb: NodeId) -> Generator[NodeId]:
|
|
160
|
+
"""
|
|
161
|
+
Find all variables bound to a specific lambda.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
lamb: the id of the lambda to consider
|
|
165
|
+
"""
|
|
166
|
+
return self.nodes.filter(pl.col("ref") == lamb)["id"].__iter__()
|
|
167
|
+
|
|
168
|
+
def find_redexes(self) -> Generator[NodeId]:
|
|
169
|
+
"""
|
|
170
|
+
Find all candidate redex nodes (see 4.)
|
|
171
|
+
|
|
172
|
+
The first redex must be the leftmost-outermost redex.
|
|
173
|
+
"""
|
|
174
|
+
return self.nodes.filter(
|
|
175
|
+
pl.col("arg").is_not_null(),
|
|
176
|
+
pl.col("arg").shift(-1).is_null(),
|
|
177
|
+
pl.col("ref").shift(-1).is_null(),
|
|
178
|
+
)["id"].__iter__()
|
|
179
|
+
|
|
180
|
+
def _get_subtree(self, root: NodeId) -> range:
|
|
181
|
+
"""
|
|
182
|
+
Get the subtree of the node in a postfix left -> right order (see 3.)
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
root: the ancestor to consider
|
|
186
|
+
"""
|
|
187
|
+
refs = self.nodes["ref"]
|
|
188
|
+
args = self.nodes["arg"]
|
|
189
|
+
rightmost = root
|
|
190
|
+
while True:
|
|
191
|
+
ref = refs[rightmost]
|
|
192
|
+
if ref is not None:
|
|
193
|
+
return range(NodeId(root), NodeId(rightmost + 1))
|
|
194
|
+
|
|
195
|
+
arg = args[rightmost]
|
|
196
|
+
rightmost = arg if arg is not None else rightmost + 1
|
|
197
|
+
|
|
198
|
+
def get_subtree(self, root: NodeId) -> Generator[NodeId]:
|
|
199
|
+
return (NodeId(x) for x in self._get_subtree(root))
|
|
200
|
+
|
|
201
|
+
def __call__(self, other: AbstractTerm) -> AbstractTerm:
|
|
202
|
+
r"""
|
|
203
|
+
Compose this term with another one with an application.
|
|
204
|
+
```
|
|
205
|
+
application
|
|
206
|
+
/ \
|
|
207
|
+
/ \
|
|
208
|
+
self other
|
|
209
|
+
```
|
|
210
|
+
"""
|
|
211
|
+
n = len(self.nodes)
|
|
212
|
+
return AbstractTerm(
|
|
213
|
+
pl.concat(
|
|
214
|
+
[
|
|
215
|
+
pl.DataFrame(
|
|
216
|
+
[{"id": 0, "arg": n + 1}],
|
|
217
|
+
schema=SCHEMA,
|
|
218
|
+
),
|
|
219
|
+
_shift(self.nodes, 1),
|
|
220
|
+
_shift(other.nodes, n + 1),
|
|
221
|
+
],
|
|
222
|
+
)
|
|
122
223
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
224
|
+
|
|
225
|
+
def beta_reduce(self, redex: NodeId) -> AbstractTerm:
|
|
226
|
+
"""
|
|
227
|
+
Compute the beta-reduction of this term on this redex.
|
|
228
|
+
"""
|
|
229
|
+
lamb = redex + 1
|
|
230
|
+
a = redex + 2
|
|
231
|
+
id_subst = self.node(redex).get_arg()
|
|
232
|
+
assert id_subst is not None
|
|
233
|
+
|
|
234
|
+
# see 4.3
|
|
235
|
+
stumps = self.nodes.lazy().filter(pl.col("ref") == lamb).select("id")
|
|
236
|
+
|
|
237
|
+
# see 4.4
|
|
238
|
+
subst_range = self._get_subtree(id_subst)
|
|
239
|
+
subst = self.nodes.lazy().filter(
|
|
240
|
+
pl.col("id").is_between(subst_range.start, subst_range.stop, closed="left")
|
|
128
241
|
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
242
|
+
|
|
243
|
+
# we start duplicating
|
|
244
|
+
new_trunks = subst.join(stumps, how="cross", suffix="_stump").select(
|
|
245
|
+
"id",
|
|
246
|
+
"id_stump",
|
|
247
|
+
prev=pl.struct(stump="id_stump", local="id"),
|
|
248
|
+
prev_arg=pl.struct(stump="id_stump", local="arg"),
|
|
249
|
+
prev_ref=pl.struct(stump="id_stump", local="ref"),
|
|
250
|
+
# TODO: explain the logic for variables that are not bound inside the redex
|
|
251
|
+
prev_ref_unbound=pl.struct(stump=None, local="ref", schema=PREV_ID_SCHEMA),
|
|
134
252
|
)
|
|
135
|
-
|
|
136
|
-
|
|
253
|
+
|
|
254
|
+
new_nodes = (
|
|
255
|
+
self.nodes.lazy()
|
|
256
|
+
# remove the information about the previous iteration
|
|
257
|
+
.select(pl.exclude("prev"))
|
|
258
|
+
# See 5.2
|
|
259
|
+
.filter(
|
|
260
|
+
~(pl.col("id").eq(redex) | pl.col("id").eq(lamb)),
|
|
261
|
+
)
|
|
262
|
+
# See 5.3
|
|
263
|
+
.join(subst, left_on="id", right_on="id", how="anti")
|
|
264
|
+
# See 5.4
|
|
265
|
+
.with_columns(arg=pl.col("arg").replace(redex, a))
|
|
266
|
+
.join(
|
|
267
|
+
# 5.1.1 we check if the argument is a stump
|
|
268
|
+
stumps.select("id", arg_is_stump=True),
|
|
269
|
+
left_on="arg",
|
|
270
|
+
right_on="id",
|
|
271
|
+
how="left",
|
|
272
|
+
maintain_order="left",
|
|
273
|
+
)
|
|
274
|
+
# See 5.1
|
|
275
|
+
.join(
|
|
276
|
+
new_trunks,
|
|
277
|
+
left_on="id",
|
|
278
|
+
right_on="id_stump",
|
|
279
|
+
how="left",
|
|
280
|
+
maintain_order="left",
|
|
281
|
+
)
|
|
282
|
+
.select(
|
|
283
|
+
pl.col("prev").fill_null(pl.struct(stump=None, local="id")),
|
|
284
|
+
pl.col("prev_ref").fill_null(pl.struct(stump=None, local="ref")),
|
|
285
|
+
pl.col("prev_ref_unbound"),
|
|
286
|
+
# 5.1.1
|
|
287
|
+
pl.col("prev_arg").fill_null(
|
|
288
|
+
pl.when(pl.col("arg_is_stump"))
|
|
289
|
+
.then(pl.struct(stump="arg", local=id_subst))
|
|
290
|
+
.otherwise(pl.struct(stump=None, local="arg"))
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
.with_row_index("id")
|
|
294
|
+
).cache()
|
|
295
|
+
|
|
296
|
+
# renumbering step: we replace each "previous location" with the new id
|
|
297
|
+
out = (
|
|
298
|
+
new_nodes.join(
|
|
299
|
+
new_nodes.select(prev_ref="prev", ref="id"),
|
|
300
|
+
on="prev_ref",
|
|
301
|
+
how="left",
|
|
302
|
+
maintain_order="left",
|
|
303
|
+
nulls_equal=True,
|
|
304
|
+
)
|
|
305
|
+
.join(
|
|
306
|
+
new_nodes.select(prev_ref_unbound="prev", ref_unbound="id"),
|
|
307
|
+
on="prev_ref_unbound",
|
|
308
|
+
how="left",
|
|
309
|
+
maintain_order="left",
|
|
310
|
+
nulls_equal=True,
|
|
311
|
+
)
|
|
312
|
+
.join(
|
|
313
|
+
new_nodes.select(prev_arg="prev", arg="id"),
|
|
314
|
+
on="prev_arg",
|
|
315
|
+
how="left",
|
|
316
|
+
maintain_order="left",
|
|
317
|
+
nulls_equal=True,
|
|
318
|
+
)
|
|
319
|
+
.select(
|
|
320
|
+
"id",
|
|
321
|
+
ref=pl.coalesce("ref", "ref_unbound"), # 5.1.2
|
|
322
|
+
arg="arg",
|
|
323
|
+
prev="prev",
|
|
324
|
+
)
|
|
325
|
+
).collect()
|
|
326
|
+
return AbstractTerm(out)
|
curryparty/display.py
CHANGED
|
@@ -1,60 +1,73 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Iterable, List, Optional, Tuple, TypeAlias
|
|
2
2
|
|
|
3
|
-
import polars as pl
|
|
4
3
|
import svg
|
|
5
4
|
|
|
5
|
+
from .core import AbstractTerm, NodeId
|
|
6
6
|
from .utils import Interval, ShapeAnimFrame
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def compute_height(
|
|
10
|
-
_, y = compute_layout(
|
|
9
|
+
def compute_height(term: AbstractTerm):
|
|
10
|
+
_, y = compute_layout(term)
|
|
11
11
|
return max(interval[1] for interval in y.values() if interval) + 1
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def count_variables(
|
|
15
|
-
return
|
|
14
|
+
def count_variables(term: AbstractTerm):
|
|
15
|
+
return sum(1 for x in term.get_subtree(term.root()) if term.node(x).ref is not None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Loc: TypeAlias = Tuple[NodeId, Optional[NodeId]]
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def compute_layout(
|
|
19
|
-
|
|
20
|
-
) -> tuple[dict[
|
|
21
|
-
y = {
|
|
22
|
-
x = {}
|
|
23
|
-
|
|
22
|
+
term: AbstractTerm, lamb: Optional[NodeId] = None, replaced_var_width=1
|
|
23
|
+
) -> tuple[dict[Loc, Interval], dict[Loc, Interval]]:
|
|
24
|
+
y: dict[Loc, Interval] = {(term.root(), None): Interval((0, 0))}
|
|
25
|
+
x: dict[Loc, Interval] = {}
|
|
26
|
+
nodes = list(term.get_subtree(term.root()))
|
|
27
|
+
for node_id in nodes:
|
|
28
|
+
node = term.node(node_id)
|
|
29
|
+
ref = node.ref
|
|
30
|
+
arg = node.get_arg()
|
|
24
31
|
if ref is not None:
|
|
25
32
|
continue
|
|
26
|
-
child = node
|
|
33
|
+
child = term.node(node_id).get_left()
|
|
34
|
+
assert child is not None
|
|
27
35
|
if arg is not None:
|
|
28
|
-
y[child] = y[
|
|
29
|
-
|
|
36
|
+
y[child, None] = y[node_id, None].shift(
|
|
37
|
+
0 if term.node(child).get_arg() is None else 1
|
|
38
|
+
)
|
|
39
|
+
y[arg, None] = y[node_id, None].shift(0)
|
|
30
40
|
else:
|
|
31
|
-
y[child] = y[
|
|
41
|
+
y[child, None] = y[node_id, None].shift(1)
|
|
32
42
|
|
|
33
|
-
next_var_x = count_variables(
|
|
43
|
+
next_var_x = count_variables(term) - 1
|
|
34
44
|
|
|
35
|
-
for
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
for node_id in reversed(nodes):
|
|
46
|
+
node = term.node(node_id)
|
|
47
|
+
ref = node.ref
|
|
48
|
+
arg = node.get_arg()
|
|
38
49
|
if ref is not None:
|
|
39
50
|
width = replaced_var_width if ref == lamb else 1
|
|
40
|
-
x[
|
|
51
|
+
x[node_id, None] = Interval((next_var_x - width + 1, next_var_x))
|
|
41
52
|
next_var_x -= width
|
|
42
|
-
x[ref] = x[
|
|
53
|
+
x[ref, None] = x[node_id, None] | x.get((ref, None), Interval(None))
|
|
43
54
|
|
|
44
55
|
else:
|
|
45
|
-
child = node
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
child = term.node(node_id).get_left()
|
|
57
|
+
assert child is not None
|
|
58
|
+
x[node_id, None] = x[child, None] | x.get((node_id, None), Interval(None))
|
|
59
|
+
y[node_id, None] = y[child, None] | y[node_id, None]
|
|
60
|
+
|
|
48
61
|
return x, y
|
|
49
62
|
|
|
50
63
|
|
|
51
64
|
def draw(
|
|
52
|
-
x: dict[
|
|
53
|
-
y: dict[
|
|
54
|
-
i_node:
|
|
55
|
-
ref: Optional[
|
|
56
|
-
arg: Optional[
|
|
57
|
-
key:
|
|
65
|
+
x: dict[Loc, Interval],
|
|
66
|
+
y: dict[Loc, Interval],
|
|
67
|
+
i_node: Loc,
|
|
68
|
+
ref: Optional[Loc],
|
|
69
|
+
arg: Optional[Loc],
|
|
70
|
+
key: Loc,
|
|
58
71
|
idx: int,
|
|
59
72
|
replaced=False,
|
|
60
73
|
removed=False,
|
|
@@ -164,108 +177,129 @@ def draw(
|
|
|
164
177
|
|
|
165
178
|
|
|
166
179
|
def compute_svg_frame_init(
|
|
167
|
-
|
|
180
|
+
term: AbstractTerm, idx: int = 0
|
|
168
181
|
) -> Iterable[ShapeAnimFrame]:
|
|
169
|
-
x, y = compute_layout(
|
|
170
|
-
for
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
x, y = compute_layout(term)
|
|
183
|
+
for node_id in term.get_subtree(term.root()):
|
|
184
|
+
node = term.node(node_id)
|
|
185
|
+
ref = node.ref
|
|
186
|
+
arg = node.get_arg()
|
|
187
|
+
yield from draw(
|
|
188
|
+
x,
|
|
189
|
+
y,
|
|
190
|
+
(node_id, None),
|
|
191
|
+
(ref, None) if ref is not None else None,
|
|
192
|
+
(arg, None) if arg is not None else None,
|
|
193
|
+
key=(node_id, None),
|
|
194
|
+
idx=idx,
|
|
195
|
+
)
|
|
174
196
|
|
|
175
197
|
|
|
176
198
|
def compute_svg_frame_phase_a(
|
|
177
|
-
|
|
199
|
+
term: AbstractTerm,
|
|
200
|
+
redex: NodeId,
|
|
201
|
+
b_subtree: List[NodeId],
|
|
202
|
+
vars: List[NodeId],
|
|
203
|
+
idx: int,
|
|
178
204
|
) -> Iterable[ShapeAnimFrame]:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
x
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
lamb = term.node(redex).get_left()
|
|
206
|
+
assert lamb is not None
|
|
207
|
+
b_width = sum(1 for x in b_subtree if term.node(x).ref is not None)
|
|
208
|
+
x, y = compute_layout(term, lamb=lamb, replaced_var_width=b_width)
|
|
209
|
+
for node_id in term.get_subtree(term.root()):
|
|
210
|
+
if node_id in b_subtree:
|
|
211
|
+
continue
|
|
212
|
+
node = term.node(node_id)
|
|
213
|
+
ref = node.ref
|
|
214
|
+
arg = node.get_arg()
|
|
215
|
+
replaced = ref == lamb
|
|
186
216
|
yield from draw(
|
|
187
217
|
x,
|
|
188
218
|
y,
|
|
189
|
-
|
|
190
|
-
ref,
|
|
191
|
-
arg,
|
|
192
|
-
key=
|
|
219
|
+
(node_id, None),
|
|
220
|
+
(ref, None) if ref is not None else None,
|
|
221
|
+
(arg, None) if arg is not None else None,
|
|
222
|
+
key=(node_id, None),
|
|
193
223
|
idx=idx,
|
|
194
224
|
replaced=replaced,
|
|
195
|
-
removed=(
|
|
225
|
+
removed=(node_id == lamb or node_id == redex),
|
|
196
226
|
)
|
|
197
227
|
|
|
198
|
-
for
|
|
199
|
-
for
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
228
|
+
for stump in vars:
|
|
229
|
+
for local_id in b_subtree:
|
|
230
|
+
local_node = term.node(local_id)
|
|
231
|
+
ref = local_node.ref
|
|
232
|
+
arg = local_node.get_arg()
|
|
233
|
+
key = (local_id, stump)
|
|
234
|
+
yield from draw(
|
|
235
|
+
x,
|
|
236
|
+
y,
|
|
237
|
+
(local_id, None),
|
|
238
|
+
(ref, None) if ref is not None else None,
|
|
239
|
+
(arg, None) if arg is not None else None,
|
|
240
|
+
key=key,
|
|
241
|
+
idx=idx,
|
|
242
|
+
)
|
|
203
243
|
|
|
204
244
|
|
|
205
245
|
def compute_svg_frame_phase_b(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
b_subtree:
|
|
209
|
-
|
|
246
|
+
term: AbstractTerm,
|
|
247
|
+
redex: NodeId,
|
|
248
|
+
b_subtree: List[NodeId],
|
|
249
|
+
reduced: AbstractTerm,
|
|
210
250
|
idx: int,
|
|
211
251
|
) -> Iterable[ShapeAnimFrame]:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
x
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
if new_arg is None:
|
|
239
|
-
arg = None
|
|
240
|
-
else:
|
|
241
|
-
bid_arg = new_nodes["bid"][new_arg]
|
|
242
|
-
arg = (
|
|
243
|
-
(bid_arg["major"], bid_arg["minor"])
|
|
244
|
-
if bid_arg["major"] != bid_arg["minor"]
|
|
245
|
-
else bid_arg["minor"]
|
|
246
|
-
)
|
|
247
|
-
key = (v, minor) if minor != v else minor
|
|
252
|
+
lamb = term.node(redex).get_left()
|
|
253
|
+
assert lamb is not None
|
|
254
|
+
b_width = sum(1 for x in b_subtree if term.node(x).ref is not None)
|
|
255
|
+
b = term.node(redex).get_arg()
|
|
256
|
+
assert b is not None
|
|
257
|
+
x, y = compute_layout(term, lamb=lamb, replaced_var_width=b_width)
|
|
258
|
+
b_x = x[b, None][0]
|
|
259
|
+
b_y = y[b, None][0]
|
|
260
|
+
for node_id in reduced.get_subtree(reduced.root()):
|
|
261
|
+
node = reduced.node(node_id)
|
|
262
|
+
local = node.previous_local
|
|
263
|
+
stump = node.previous_stump
|
|
264
|
+
if stump is not None:
|
|
265
|
+
delta_x = x[stump, None][0] - b_x
|
|
266
|
+
delta_y = y[stump, None][0] - b_y + 1
|
|
267
|
+
x[local, stump] = x[local, None].shift(delta_x)
|
|
268
|
+
y[local, stump] = y[local, None].shift(delta_y)
|
|
269
|
+
|
|
270
|
+
for node_id in reduced.get_subtree(reduced.root()):
|
|
271
|
+
node = reduced.node(node_id)
|
|
272
|
+
new_ref = node.ref
|
|
273
|
+
new_arg = node.get_arg()
|
|
274
|
+
|
|
275
|
+
key = node.previous()
|
|
276
|
+
|
|
248
277
|
yield from draw(
|
|
249
278
|
x,
|
|
250
279
|
y,
|
|
251
280
|
key,
|
|
252
|
-
ref,
|
|
253
|
-
arg,
|
|
281
|
+
ref=reduced.node(new_ref).previous() if new_ref else None,
|
|
282
|
+
arg=reduced.node(new_arg).previous() if new_arg else None,
|
|
254
283
|
key=key,
|
|
255
284
|
idx=idx,
|
|
256
285
|
)
|
|
257
286
|
|
|
258
287
|
|
|
259
288
|
def compute_svg_frame_final(
|
|
260
|
-
reduced:
|
|
289
|
+
reduced: AbstractTerm, idx: int
|
|
261
290
|
) -> Iterable[ShapeAnimFrame]:
|
|
262
291
|
x, y = compute_layout(reduced)
|
|
263
|
-
for
|
|
264
|
-
reduced.
|
|
265
|
-
|
|
266
|
-
.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
for node_id in reduced.get_subtree(reduced.root()):
|
|
293
|
+
node = reduced.node(node_id)
|
|
294
|
+
ref = node.ref
|
|
295
|
+
arg = node.get_arg()
|
|
296
|
+
key = node.previous()
|
|
297
|
+
yield from draw(
|
|
298
|
+
x,
|
|
299
|
+
y,
|
|
300
|
+
(node_id, None),
|
|
301
|
+
(ref, None) if ref is not None else None,
|
|
302
|
+
(arg, None) if arg is not None else None,
|
|
303
|
+
key,
|
|
304
|
+
idx=idx,
|
|
305
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
curryparty/__init__.py,sha256=uxHe9q_6L8yqQcgwZpm8WMLCdroBNliGJWRwcheFi3k,8152
|
|
2
|
+
curryparty/core.py,sha256=CkJi2MpolY22WisJ-0DBmaM5Y25Kt2kfYmaiAX5Ykdk,10972
|
|
3
|
+
curryparty/display.py,sha256=21pqOwembQ1Mx-OjT3woy1UFpVlnZK02E5f6Zz7Yd4c,8809
|
|
4
|
+
curryparty/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
curryparty/utils.py,sha256=56rRpMiDXbEQjFEwoKxuGb3Nch4qJd8OgwmNxkfl-oE,4937
|
|
6
|
+
curryparty-0.3.3.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
7
|
+
curryparty-0.3.3.dist-info/METADATA,sha256=EOM6X4YoY0cYRhFsF0O5UclSdawkbvVJFgVU3OtDkMo,2259
|
|
8
|
+
curryparty-0.3.3.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
curryparty/__init__.py,sha256=_55oknmsS2Z8BRfs_OYD_rwj_INJ7FEBfDhiSpYnAtk,8243
|
|
2
|
-
curryparty/core.py,sha256=nqHU0ZntZdymudC6jR6WYck80xFwcvymwjwUfh0UIWo,3852
|
|
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.1.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
|
|
7
|
-
curryparty-0.3.1.dist-info/METADATA,sha256=ChdqiQ4zzQtr_i8ey_EUxanWhF0WlJM173j_YqhFs88,2259
|
|
8
|
-
curryparty-0.3.1.dist-info/RECORD,,
|