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 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, x0=-10, width=30):
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) + 3
74
- shapes: dict[int, ShapeAnim] = {}
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
- for k, e, attributes in items:
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 shapes.values()
111
+ for x in anims
98
112
  ]
99
113
 
100
114
  anim_elements.append(
101
115
  Rect(
102
116
  id=box_id,
103
- x=str(x0),
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"{x0} 0 {width} {height}",
114
- height=f"{35 * height}px",
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
- f"<div><div style=\"margin:5px\">click to animate, move away and back to reset</div>{anim_svg}</div>"
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, width=30):
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
- for _, e, attributes in frame:
128
- for name, v in attributes.items():
129
- e.__setattr__(name, v)
130
- elements.append(e)
131
- return SVG(
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"{x0} 0 {width} {height}", # type: ignore
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 = [(i + 1, x) if i >= self.last_ else (i, x) for (i, x) in 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
- self.refs = [(i, self.lambdas.get(x, x)) for i, x in self.refs]
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.select(pl.col("ref").count()).item()
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
- ) -> Iterable[tuple[Any, svg.Element, dict]]:
61
+ hide_arg=False,
62
+ ) -> Iterable[ShapeAnimFrame]:
56
63
  x_node = x[i_node]
57
64
  y_node = y[i_node]
58
- if True:
59
- if arg is not None or removed:
60
- color = "transparent"
61
- elif replaced or removed:
62
- color = "green"
63
- elif ref is not None:
64
- color = "red"
65
- else:
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
- stroke_width = 0.05
69
- stroke = "gray"
70
- if arg is not None:
71
- stroke_width = 0.1
72
- stroke = "orange"
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=stroke_width,
76
- stroke=stroke,
96
+ stroke_width=0.1,
97
+ stroke="orange",
77
98
  )
78
99
 
79
- yield (
80
- ("r", key),
81
- r,
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
- ("l", key),
99
- e,
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": y_node[0] + 0.1,
126
+ "y1": y_ref[0] + 0.9,
103
127
  "x2": x_node[0] + 0.5,
104
- "y2": y_ref[0] + 0.9,
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
- ("b", key),
120
- e1,
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 + y_arg[0],
150
+ "y2": 0.5 + y_node[0],
126
151
  },
152
+ zindex=3,
127
153
  )
128
- yield (
129
- ("c", key),
130
- e2,
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[tuple[Any, svg.Element, dict[str, Any]]]:
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
- lamb: int,
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(x, y, key, ref, arg, key=key)
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(reduced: pl.DataFrame):
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
- shape: Element
39
- attributes: set[str]
40
- values: dict[tuple[int, str], Any]
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
- 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
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.shape.__setattr__(name, non_nulls[0])
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.shape.elements
117
- self.shape
118
- self.shape.elements = elements
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.2.2
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.22
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
@@ -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,,