diff --git a/bin/widish b/bin/widish new file mode 100755 index 0000000..8d1fb99 --- /dev/null +++ b/bin/widish @@ -0,0 +1,2 @@ +#!/bin/sh +exec python -m widip "$@" diff --git a/bin/widish.png b/bin/widish.png new file mode 100644 index 0000000..dd106b1 Binary files /dev/null and b/bin/widish.png differ diff --git a/bin/widish.svg b/bin/widish.svg new file mode 100644 index 0000000..d15aa63 --- /dev/null +++ b/bin/widish.svg @@ -0,0 +1,612 @@ + + + + + + + + 2026-01-19T13:41:27.540701 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/yaml/python.yaml b/bin/yaml/python.yaml new file mode 100755 index 0000000..6dd6fdf --- /dev/null +++ b/bin/yaml/python.yaml @@ -0,0 +1,2 @@ +#!bin/widish +!python diff --git a/bin/yaml/shell.png b/bin/yaml/shell.png new file mode 100644 index 0000000..0b8389c Binary files /dev/null and b/bin/yaml/shell.png differ diff --git a/bin/yaml/shell.svg b/bin/yaml/shell.svg new file mode 100644 index 0000000..4c68cb0 --- /dev/null +++ b/bin/yaml/shell.svg @@ -0,0 +1,192 @@ + + + + + + + + 2026-01-19T14:23:08.160262 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/README.md b/examples/README.md index ac92f6a..68ca981 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,7 @@ $ python -m widip examples/hello-world.yaml Hello world! ``` -![](hello-world.jpg) +![](hello-world.svg) ## Script @@ -19,7 +19,7 @@ $ python -m widip examples/shell.yaml ? !tail -2 ``` -![IMG](shell.jpg) +![IMG](shell.svg) # Working with the CLI diff --git a/examples/aoc2025/1-1.jpg b/examples/aoc2025/1-1.jpg index 3ef213f..5a9bf95 100644 Binary files a/examples/aoc2025/1-1.jpg and b/examples/aoc2025/1-1.jpg differ diff --git a/examples/hello-world.jpg b/examples/hello-world.jpg deleted file mode 100644 index 382ad43..0000000 Binary files a/examples/hello-world.jpg and /dev/null differ diff --git a/examples/hello-world.png b/examples/hello-world.png new file mode 100644 index 0000000..6e3474f Binary files /dev/null and b/examples/hello-world.png differ diff --git a/examples/hello-world.svg b/examples/hello-world.svg new file mode 100644 index 0000000..442f7fe --- /dev/null +++ b/examples/hello-world.svg @@ -0,0 +1,333 @@ + + + + + + + + 2026-01-19T12:38:40.632095 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/mascarpone/crack-then-beat.png b/examples/mascarpone/crack-then-beat.png new file mode 100644 index 0000000..7c38706 Binary files /dev/null and b/examples/mascarpone/crack-then-beat.png differ diff --git a/examples/mascarpone/crack-then-beat.svg b/examples/mascarpone/crack-then-beat.svg new file mode 100644 index 0000000..b6ff70e --- /dev/null +++ b/examples/mascarpone/crack-then-beat.svg @@ -0,0 +1,1671 @@ + + + + + + + + 2026-01-19T12:41:50.492635 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/shell.jpg b/examples/shell.jpg deleted file mode 100644 index da94402..0000000 Binary files a/examples/shell.jpg and /dev/null differ diff --git a/examples/shell.png b/examples/shell.png new file mode 100644 index 0000000..bcd3e6d Binary files /dev/null and b/examples/shell.png differ diff --git a/examples/shell.svg b/examples/shell.svg new file mode 100644 index 0000000..965a746 --- /dev/null +++ b/examples/shell.svg @@ -0,0 +1,1316 @@ + + + + + + + + 2026-01-19T13:27:56.577278 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/test_script.sh b/examples/test_script.sh new file mode 100755 index 0000000..b0cf654 --- /dev/null +++ b/examples/test_script.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Hello from Bash Script!" diff --git a/pyproject.toml b/pyproject.toml index 12389de..ae62060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,9 @@ name = "widip" version = "0.1.0" description = "Widip is an interactive environment for computing with wiring diagrams in modern systems" dependencies = [ - "discopy>=1.2.1", "pyyaml>=6.0.1", "watchdog>=4.0.1", "nx-yaml==0.3.0", + "discopy>=1.2.2", "pyyaml>=6.0.1", "watchdog>=4.0.1", "nx-yaml==0.4.1", ] [project.urls] "Source" = "https://github.com/colltoaction/widip" + diff --git a/widip/composing.py b/widip/composing.py deleted file mode 100644 index 7464cc5..0000000 --- a/widip/composing.py +++ /dev/null @@ -1,145 +0,0 @@ -from discopy.closed import Id, Ty, Diagram, Functor, Box - - -def adapt_to_interface(diagram, box): - return - """adapts a diagram open ports to fit in the box""" - left = Id(box.dom) - right = Id(box.cod) - return adapter_hypergraph(left, diagram) >> \ - diagram >> \ - adapter_hypergraph(diagram, right) - -def adapter_hypergraph(left, right): - return - mid = Ty().tensor(*set(left.cod + right.dom)) - mid_to_left_ports = { - t: tuple(i for i, lt in enumerate(left.cod) if lt == t) - for t in mid} - mid_to_right_ports = { - t: tuple(i + len(left.cod) for i, lt in enumerate(right.dom) if lt == t) - for t in mid} - boxes = tuple( - Id(Ty().tensor(*(t for _ in range(len(mid_to_left_ports[t]))))) - if len(mid_to_left_ports[t]) == len(mid_to_right_ports[t]) else - Spider( - len(mid_to_left_ports[t]), - len(mid_to_right_ports[t]), - t) - for t in mid) - g = H( - dom=left.cod, cod=right.dom, - boxes=boxes, - wires=( - tuple(i for i in range(len(left.cod))), - tuple( - (mid_to_left_ports[t], mid_to_right_ports[t]) - for t in mid), - tuple(i + len(left.cod) for i in range(len(right.dom))), - ), - ) - return g.to_diagram() - -def glue_diagrams(left, right): - return - """a diagram connecting equal objects within each type""" - """glues two diagrams sequentially with closed generators""" - if left.cod == right.dom: - return left >> right - l_dom, l_cod, r_dom, r_cod = left.dom, left.cod, right.dom, right.cod - dw_l = { - t - for t in l_cod - if t not in r_dom} - dw_r = { - t - for t in r_dom - if t not in l_cod} - cw_l = { - t - for t in l_cod - if t in r_dom} - cw_r = { - t - for t in r_dom - if t in l_cod} - # TODO convention for repeated in both sides - mid_names = tuple({t for t in l_cod + r_dom}) - dom_wires = l_dom_wires = tuple( - i - for i in range(len(l_dom) + len(dw_r)) - ) - l_cod_wires = tuple( - (mid_names.index(t) - + len(l_dom) + len(dw_r)) - for t in l_cod) + \ - tuple( - (mid_names.index(t) + len(l_dom) + len(dw_r)) - for t in dw_r - ) - r_dom_wires = tuple( - (mid_names.index(t) + len(l_dom) + len(dw_r)) - for t in dw_l) + \ - tuple( - (mid_names.index(t) - + len(l_dom) + len(dw_r)) - for t in r_dom - ) - cod_wires = r_cod_wires = tuple( - i - + len(l_dom) + len(dw_r) - + len(mid_names) - for i in range(len(dw_l) + len(r_cod)) - ) - glued = H( - dom=l_dom @ Ty().tensor(*dw_r), - cod=Ty().tensor(*dw_l) @ r_cod, - boxes=( - left @ Ty().tensor(*dw_r), - Ty().tensor(*dw_l) @ right, - ), - wires=( - dom_wires, - ( - (l_dom_wires, l_cod_wires), - (r_dom_wires, r_cod_wires), - ), - cod_wires, - ), - ).to_diagram() - return glued - -def replace_id_f(name): - return Functor( - lambda ob: replace_id_ty(ob, name), - lambda ar: replace_id_box(ar, name),) - -def replace_id_box(box, name): - return Box( - box.name, - replace_id_ty(box.dom, name), - replace_id_ty(box.cod, name)) - -def replace_id_ty(ty, name): - return Ty().tensor(*(Ty("") if t == Ty(name) else t for t in ty)) - -def close_ty_f(name): - return Functor( - lambda ob: ob,#close_ty(ob, name), - lambda ar: close_ty_box(ar, name),) - -def close_ty_box(box, name): - l = Ty().tensor(*( - t for t in box.dom - if t != Ty(name))) - r = Ty().tensor(*( - t for t in box.cod - if t != Ty(name))) - # box.draw() - box.draw() - closed = adapt_to_interface(box, Box("", l, r)) - closed.draw() - return closed - -def close_ty(ty, name): - return Ty() if ty == Ty(name) else ty \ No newline at end of file diff --git a/widip/discopy_to_hif.py b/widip/discopy_to_hif.py new file mode 100644 index 0000000..3db6049 --- /dev/null +++ b/widip/discopy_to_hif.py @@ -0,0 +1,44 @@ +from discopy.hypergraph import Hypergraph +from nx_hif.hif import hif_create, hif_new_node, hif_new_edge, hif_add_incidence + + +def discopy_to_hif(hg: Hypergraph): + hif_hg = hif_create() + + # Create spiders (edges) + spider_to_eid = {} + for i in range(hg.n_spiders): + eid = hif_new_edge(hif_hg, kind="spider") + spider_to_eid[i] = eid + + # Create boundary + boundary_id = hif_new_node(hif_hg, kind="boundary") + + # Connect boundary + dom_wires = hg.wires[0] + for i, spider_idx in enumerate(dom_wires): + eid = spider_to_eid[spider_idx] + hif_add_incidence(hif_hg, eid, boundary_id, role="dom", index=i, key=None) + + cod_wires = hg.wires[2] + for i, spider_idx in enumerate(cod_wires): + eid = spider_to_eid[spider_idx] + hif_add_incidence(hif_hg, eid, boundary_id, role="cod", index=i, key=None) + + # Create boxes + box_wires = hg.wires[1] + for i, box in enumerate(hg.boxes): + data = box.data.copy() if box.data else {} + data["kind"] = box.name + nid = hif_new_node(hif_hg, **data) + + ins, outs = box_wires[i] + for idx, spider_idx in enumerate(ins): + eid = spider_to_eid[spider_idx] + hif_add_incidence(hif_hg, eid, nid, role="dom", index=idx, key=None) + + for idx, spider_idx in enumerate(outs): + eid = spider_to_eid[spider_idx] + hif_add_incidence(hif_hg, eid, nid, role="cod", index=idx, key=None) + + return hif_hg diff --git a/widip/drawing.py b/widip/drawing.py new file mode 100644 index 0000000..f180d8a --- /dev/null +++ b/widip/drawing.py @@ -0,0 +1,98 @@ +import math, sys +from pathlib import Path +from discopy import monoidal, closed + +class ComplexityProfile: + def __init__(self, rw, rh, rn, d_width, d_height): + self.max_cpw, self.max_lpl, self.rn, self.d_width, self.d_height = rw, rh, rn, d_width, d_height + + def get_layout_params(self): + fsize = max(10, min(24, 80 / math.pow(max(1, self.rn), 0.15))) + w_mult = (max(3, self.max_cpw) * fsize) / 72.0 + 0.6 + h_mult = (self.max_lpl * fsize * 1.5) / 72.0 + 0.2 + width, height = self.d_width * w_mult, self.d_height * h_mult + if self.d_width == 1 and self.d_height > 3 and height > width * 2.5: + height = self.d_height * ((width * 2.5) / self.d_height) + return {"figsize": (width, height), "fontsize": int(fsize), "textpad": (0.1, 0.15), "fontsize_types": max(12, int(fsize * 0.4))} + +def get_recursive_stats(d, visited=None): + if visited is None: visited = set() + max_cpw, max_lpl, box_count = 1.0, 1.0, 0 + + def walk(obj): + nonlocal max_cpw, max_lpl, box_count + if id(obj) in visited: return + visited.add(id(obj)) + if isinstance(obj, monoidal.Bubble): return walk(obj.inside) + if hasattr(obj, "boxes") and len(obj.boxes) == 1 and obj.boxes[0] is obj: + label = str(getattr(obj, "drawing_name", getattr(obj, "name", ""))) or "" + lines, wires = label.split('\n'), max(1, len(getattr(obj, "dom", [])), len(getattr(obj, "cod", []))) + max_cpw, max_lpl, box_count = max(max_cpw, max(len(l) for l in lines) / wires), max(max_lpl, len(lines)), box_count + 1 + return + if hasattr(obj, "__iter__"): [walk(b) for layer in obj if hasattr(layer, "boxes") for b in layer.boxes] + elif hasattr(obj, "inside"): walk(obj.inside) + elif hasattr(obj, "boxes"): [walk(b) for b in obj.boxes] + walk(d) + return max_cpw, max_lpl, box_count + +STYLES = { + "Scalar": {"color": "#ffffff", "spider": True, "fmt": lambda n, b: f"{getattr(b, 'tag', '')} {getattr(b, 'value', '')}".strip() or ""}, + "Alias": {"color": "#3498db", "spider": True, "fmt": lambda n, b: f"*{getattr(b, 'name', n.lstrip('*'))}"}, + "Anchor": {"color": "#2980b9", "spider": True, "fmt": lambda n, b: f"&{getattr(b, 'name', n.lstrip('&'))}"}, + "Label": {"color": "#ffffff"}, "Data": {"color": "#fff9c4"}, "Eval": {"color": "#ffccbc", "name": "exec"}, + "Curry": {"color": "#d1c4e9", "name": "Λ"}, "Program": {"color": "#ffffff"}, + "Copy": {"color": "#2ecc71", "spider": True, "name": "Δ"}, "Merge": {"color": "#27ae60", "spider": True, "name": "μ"}, + "Discard": {"color": "#e74c3c", "spider": True, "name": "ε", "check": lambda b: b.dom.name != ""}, + "Swap": {"color": "#f1c40f", "swap": True, "name": ""}, + "Sequence": {"color": "#ffffff", "bubble": True, "fmt": lambda n, b: f"[{getattr(b, 'tag', '')}]" if getattr(b, 'tag', '') else ""}, + "Mapping": {"color": "#ffffff", "bubble": True, "fmt": lambda n, b: f"{{{getattr(b, 'tag', '')}}}" if getattr(b, 'tag', '') else ""}, +} + +def get_style(box): + cls, name = type(box).__name__, str(getattr(box, "drawing_name", getattr(box, "name", ""))) + key = next((k for k in STYLES if cls == k or name.startswith(k) or (k == "Eval" and name=="eval") or (k == "Curry" and name=="curry") or (k=="Data" and name.startswith("⌜"))), None) + style = STYLES.get(key, {}) + final_name = style.get("name", name) + if "fmt" in style: final_name = style["fmt"](name, box) + if "check" in style and not style["check"](box): final_name, style = "", {"color": "#ffffff", "spider": True} + return final_name, style.get("color", "#f0f0f0"), style.get("spider", False), style.get("swap", False), style.get("bubble", False) + +def diagram_draw(path: Path, fd): + m_cpw, m_lpl, rn = get_recursive_stats(fd) + params = ComplexityProfile(m_cpw, m_lpl, rn, getattr(fd, "width", len(getattr(fd, "dom", [])) or 1), len(fd) if hasattr(fd, "__iter__") else 1).get_layout_params() + out_svg, out_png = (str(path), None) if path.suffix.lower() in ['.png', '.jpg', '.jpeg'] else (str(path.with_suffix(".svg")), str(path.with_suffix(".png"))) + + def map_ob(ob): return monoidal.Ty(*[getattr(o, "name", str(o)) for o in ob.inside]) + + def map_ar(box): + name, color, spider, swap, bubble = get_style(box) + dom, cod = map_ob(box.dom), map_ob(box.cod) + if spider: res = monoidal.Box(name, dom, cod, drawing_name=name) + else: + padded = "\n".join([l.ljust(int(m_cpw * max(1, len(dom), len(cod)))) for l in str(name).split('\n')]) + if bubble or (isinstance(box, monoidal.Bubble) and not spider): + inside = standardize(box.inside) if hasattr(box, 'inside') else monoidal.Box(padded, dom, cod) + res = monoidal.Bubble(inside, dom, cod, drawing_name=padded) + else: + res = monoidal.Box(padded, dom, cod, drawing_name=padded) + if not swap: res.nodesize = (1, 1) + res.color, res.draw_as_spider, res.draw_as_swap = color, spider, swap + if spider: res.shape, res.nodesize = "circle", (1.5, 1.5) + return res + + def standardize(diag): + if not hasattr(diag, "boxes_and_offsets"): return diag + m_dom, curr, inside = map_ob(diag.dom), map_ob(diag.dom), [] + for box, off in diag.boxes_and_offsets: + mbox = map_ar(box) + layer = monoidal.Layer(monoidal.Id(curr[:off]), mbox, monoidal.Id(curr[off + len(mbox.dom):])) + inside.append(layer) + curr = layer.cod + return monoidal.Diagram(inside, m_dom, curr) + + fd_draw = standardize(fd) + draw_params = {"aspect": "auto", "figsize": params["figsize"], "fontsize": params["fontsize"], "fontfamily": "monospace", "textpad": params["textpad"], "fontsize_types": params["fontsize_types"]} + fd_draw.draw(path=out_svg, **draw_params) + if out_png: + try: fd_draw.draw(path=out_png, **draw_params) + except: pass diff --git a/widip/files.py b/widip/files.py index be33b80..b8ff844 100644 --- a/widip/files.py +++ b/widip/files.py @@ -24,9 +24,7 @@ def file_diagram(file_name) -> Diagram: return fd def diagram_draw(path, fd): - fd.draw(path=str(path.with_suffix(".jpg")), - textpad=(0.3, 0.1), - fontsize=12, - fontsize_types=8) + from .drawing import diagram_draw as draw_svg + draw_svg(path, fd) files_f = Functor(lambda x: Ty(""), files_ar) diff --git a/widip/loader.py b/widip/loader.py index 912e6cc..7c0ac5f 100644 --- a/widip/loader.py +++ b/widip/loader.py @@ -1,144 +1,83 @@ -from itertools import batched -from nx_yaml import nx_compose_all, nx_serialize_all -from nx_hif.hif import * - -from discopy.markov import Id, Ty, Box, Eval -P = Ty("io") >> Ty("io") - -from .composing import glue_diagrams - - -def repl_read(stream): - incidences = nx_compose_all(stream) - diagrams = incidences_to_diagram(incidences) - return diagrams - -def incidences_to_diagram(node: HyperGraph): - # TODO properly skip stream and document start - diagram = _incidences_to_diagram(node, 0) - return diagram - -def _incidences_to_diagram(node: HyperGraph, index): - """ - Takes an nx_yaml rooted bipartite graph - and returns an equivalent string diagram - """ - tag = (hif_node(node, index).get("tag") or "")[1:] - kind = hif_node(node, index)["kind"] - - match kind: - - case "stream": - ob = load_stream(node, index) - case "document": - ob = load_document(node, index) - case "scalar": - ob = load_scalar(node, index, tag) - case "sequence": - ob = load_sequence(node, index, tag) - case "mapping": - ob = load_mapping(node, index, tag) - case _: - raise Exception(f"Kind \"{kind}\" doesn't match any.") - - return ob - - -def load_scalar(node, index, tag): - v = hif_node(node, index)["value"] - if tag and v: - return Box("G", Ty(tag) @ Ty(v), Ty() << Ty("")) - elif tag: - return Box("G", Ty(tag), Ty() << Ty("")) - elif v: - return Box("⌜−⌝", Ty(v), Ty() << Ty("")) - else: - return Box("⌜−⌝", Ty(), Ty() << Ty("")) - -def load_mapping(node, index, tag): - ob = Id() - i = 0 - nxt = tuple(hif_node_incidences(node, index, key="next")) - while True: - if not nxt: - break - ((k_edge, _, _, _), ) = nxt - ((_, k, _, _), ) = hif_edge_incidences(node, k_edge, key="start") - ((v_edge, _, _, _), ) = hif_node_incidences(node, k, key="forward") - ((_, v, _, _), ) = hif_edge_incidences(node, v_edge, key="start") - key = _incidences_to_diagram(node, k) - value = _incidences_to_diagram(node, v) - - kv = key @ value - - if i==0: - ob = kv - else: - ob = ob @ kv - - i += 1 - nxt = tuple(hif_node_incidences(node, v, key="forward")) - bases = Ty().tensor(*map(lambda x: x.inside[0].base, ob.cod[0::2])) - exps = Ty().tensor(*map(lambda x: x.inside[0].exponent, ob.cod[1::2])) - par_box = Box("(||)", ob.cod, exps << bases) - ob = ob >> par_box - if tag: - ob = (ob @ bases>> Eval(exps << bases)) - ob = Ty(tag) @ ob >> Box("G", Ty(tag) @ ob.cod, Ty("") << Ty("")) - return ob - -def load_sequence(node, index, tag): - ob = Id() - i = 0 - nxt = tuple(hif_node_incidences(node, index, key="next")) - while True: - if not nxt: - break - ((v_edge, _, _, _), ) = nxt - ((_, v, _, _), ) = hif_edge_incidences(node, v_edge, key="start") - value = _incidences_to_diagram(node, v) - if i==0: - ob = value - else: - ob = ob @ value - bases = ob.cod[0].inside[0].exponent - exps = value.cod[0].inside[0].base - ob = ob >> Box("(;)", ob.cod, bases >> exps) - - i += 1 - nxt = tuple(hif_node_incidences(node, v, key="forward")) - if tag: - bases = Ty().tensor(*map(lambda x: x.inside[0].exponent, ob.cod)) - exps = Ty().tensor(*map(lambda x: x.inside[0].base, ob.cod)) - ob = (bases @ ob >> Eval(bases >> exps)) - ob = Ty(tag) @ ob >> Box("G", Ty(tag) @ ob.cod, Ty() >> Ty(tag)) - return ob - -def load_document(node, index): - nxt = tuple(hif_node_incidences(node, index, key="next")) - ob = Id() - if nxt: - ((root_e, _, _, _), ) = nxt - ((_, root, _, _), ) = hif_edge_incidences(node, root_e, key="start") - ob = _incidences_to_diagram(node, root) - return ob - -def load_stream(node, index): - ob = Id() - nxt = tuple(hif_node_incidences(node, index, key="next")) - while True: - if not nxt: - break - ((nxt_edge, _, _, _), ) = nxt - starts = tuple(hif_edge_incidences(node, nxt_edge, key="start")) - if not starts: - break - ((_, nxt_node, _, _), ) = starts - doc = _incidences_to_diagram(node, nxt_node) - if ob == Id(): - ob = doc - else: - ob = glue_diagrams(ob, doc) - - nxt = tuple(hif_node_incidences(node, nxt_node, key="forward")) - return ob +from nx_yaml import nx_compose_all +from nx_hif.hif import * +from discopy.closed import Id, Ty, Box, Eval + +P = Ty() << Ty("") + +def iter_linked_list(node, index): + edges = tuple(hif_node_incidences(node, index, key="next")) + while edges: + ((edge, _, _, _), ) = edges + ((_, target, _, _), ) = hif_edge_incidences(node, edge, key="start") + yield target + edges = tuple(hif_node_incidences(node, target, key="forward")) + +def repl_read(stream): + return _load_node(nx_compose_all(stream), 0) + +def _load_node(node, index, tag=None): + if tag is None: + tag = (hif_node(node, index).get("tag") or "")[1:] + kind = hif_node(node, index)["kind"] + loaders = {"stream": _load_stream, "document": _load_document, + "scalar": _load_scalar, "sequence": _load_sequence, "mapping": _load_mapping} + if kind not in loaders: + raise Exception(f"Unknown kind: {kind}") + return loaders[kind](node, index, tag) + +def _load_scalar(node, index, tag): + v = hif_node(node, index)["value"] + if tag == "fix" and v: + return Box("Ω", Ty(), Ty(v) << P) @ P >> Eval(Ty(v) << P) >> Box("e", Ty(v), Ty(v)) + if tag and v: return Box(tag, Ty(v), Ty(tag) >> Ty(tag)) + if tag: return Box(tag, Ty(v) if v else Ty(), Ty(tag) >> Ty(tag)) + if v: return Box("⌜−⌝", Ty(v), Ty() >> Ty(v)) + return Box("⌜−⌝", Ty(), Ty() >> Ty(v)) + +def _load_mapping(node, index, tag): + ob, edges = Id(), tuple(hif_node_incidences(node, index, key="next")) + while edges: + ((k_edge, _, _, _), ) = edges + ((_, k, _, _), ) = hif_edge_incidences(node, k_edge, key="start") + ((v_edge, _, _, _), ) = hif_node_incidences(node, k, key="forward") + ((_, v, _, _), ) = hif_edge_incidences(node, v_edge, key="start") + key, value = _load_node(node, k, None), _load_node(node, v, None) + exps = Ty().tensor(*map(lambda x: x.inside[0].exponent, key.cod)) + bases = Ty().tensor(*map(lambda x: x.inside[0].base, value.cod)) + kv = key @ value >> Box("(;)", key.cod @ value.cod, exps >> bases) + ob = kv if ob == Id() else ob @ kv + edges = tuple(hif_node_incidences(node, v, key="forward")) + exps = Ty().tensor(*map(lambda x: x.inside[0].exponent, ob.cod)) + bases = Ty().tensor(*map(lambda x: x.inside[0].base, ob.cod)) + ob = ob >> Box("(||)", ob.cod, exps >> bases) + if tag: + ob = ob @ exps >> Eval(exps >> bases) >> Box(tag, ob.cod, Ty(tag) >> Ty(tag)) + return ob + +def _load_sequence(node, index, tag): + ob = Id() + for i, v in enumerate(iter_linked_list(node, index)): + value = _load_node(node, v, None) + if i == 0: ob = value + else: + bases, exps = ob.cod[0].inside[0].exponent, value.cod[0].inside[0].base + ob = ob @ value >> Box("(;)", ob.cod, bases >> exps) + if tag: + bases = Ty().tensor(*map(lambda x: x.inside[0].exponent, ob.cod)) + exps = Ty().tensor(*map(lambda x: x.inside[0].base, ob.cod)) + ob = bases @ ob >> Eval(bases >> exps) >> Box(tag, ob.cod, Ty() >> Ty(tag)) + return ob + +def _load_document(node, index, _): + nxt = tuple(hif_node_incidences(node, index, key="next")) + if not nxt: return Id() + ((root_e, _, _, _), ) = nxt + ((_, root, _, _), ) = hif_edge_incidences(node, root_e, key="start") + return _load_node(node, root, None) + +def _load_stream(node, index, _): + ob = Id() + for nxt_node in iter_linked_list(node, index): + doc = _load_node(node, nxt_node, None) + ob = doc if ob == Id() else ob @ doc + return ob diff --git a/widip/test_discopy_to_hif.py b/widip/test_discopy_to_hif.py new file mode 100644 index 0000000..d4c291b --- /dev/null +++ b/widip/test_discopy_to_hif.py @@ -0,0 +1,14 @@ +from discopy.symmetric import Box, Ty +from nx_hif.readwrite import encode_hif_data +from widip.discopy_to_hif import discopy_to_hif + + +def test_discopy_to_hif(): + x, y, z = Ty('x'), Ty('y'), Ty('z') + f = Box("f", x, y @ z, data={"foo": "bar"}) + g = Box("g", y @ z, x, data={"baz": 42}) + + discopy_hg = (f >> g).to_hypergraph() + hif_hg = discopy_to_hif(discopy_hg) + data = encode_hif_data(hif_hg) + assert data == {'incidences': [{'edge': 0, 'node': 0, 'attrs': {'key': 0, 'role': 'dom', 'index': 0}}, {'edge': 0, 'node': 1, 'attrs': {'key': 0, 'role': 'dom', 'index': 0}}, {'edge': 1, 'node': 1, 'attrs': {'key': 0, 'role': 'cod', 'index': 0}}, {'edge': 1, 'node': 2, 'attrs': {'key': 0, 'role': 'dom', 'index': 0}}, {'edge': 2, 'node': 1, 'attrs': {'key': 0, 'role': 'cod', 'index': 1}}, {'edge': 2, 'node': 2, 'attrs': {'key': 0, 'role': 'dom', 'index': 1}}, {'edge': 3, 'node': 0, 'attrs': {'key': 0, 'role': 'cod', 'index': 0}}, {'edge': 3, 'node': 2, 'attrs': {'key': 0, 'role': 'cod', 'index': 0}}], 'edges': [{'edge': 0, 'attrs': {'kind': 'spider'}}, {'edge': 1, 'attrs': {'kind': 'spider'}}, {'edge': 2, 'attrs': {'kind': 'spider'}}, {'edge': 3, 'attrs': {'kind': 'spider'}}], 'nodes': [{'node': 0, 'attrs': {'kind': 'boundary'}}, {'node': 1, 'attrs': {'foo': 'bar', 'kind': 'f'}}, {'node': 2, 'attrs': {'baz': 42, 'kind': 'g'}}]} diff --git a/widip/test_files.py b/widip/test_files.py deleted file mode 100644 index a5ffc60..0000000 --- a/widip/test_files.py +++ /dev/null @@ -1,37 +0,0 @@ -from discopy.closed import Box, Ty, Diagram, Id - -from .files import stream_diagram - - -def test_single_wires(): - a = Id("a") - a0 = stream_diagram("a") - a1 = stream_diagram("- a") - with Diagram.hypergraph_equality: - assert a == a0 - assert a0 == a1 - -def test_id_boxes(): - a = Box("a", Ty(""), Ty("")) - a0 = stream_diagram("!a") - a1 = stream_diagram("!a :") - a2 = stream_diagram("- !a") - with Diagram.hypergraph_equality: - assert a == a0 - assert a == a1 - assert a == a2 - -def test_the_empty_value(): - a0 = stream_diagram("") - a1 = stream_diagram("\"\":") - a2 = stream_diagram("\"\": a") - a3 = stream_diagram("a:") - a4 = stream_diagram("!a :") - a5 = stream_diagram("\"\": !a") - with Diagram.hypergraph_equality: - assert a0 == Id() - assert a1 == Id("") - assert a2 == Box("map", Ty(""), Ty("a")) - assert a3 == Id("a") - assert a4 == Box("a", Ty(""), Ty("")) - assert a5 == Box("map", Ty(""), Ty("")) >> a4 diff --git a/widip/test_loader.py b/widip/test_loader.py deleted file mode 100644 index b98664e..0000000 --- a/widip/test_loader.py +++ /dev/null @@ -1,49 +0,0 @@ -from discopy.closed import Box, Ty, Diagram, Spider, Id, Spider - -from .loader import compose_all - - -id_box = lambda i: Box("!", Ty(i), Ty(i)) - -def test_tagged(): - a0 = compose_all("!a") - a1 = compose_all("!a :") - a2 = compose_all("--- !a") - a3 = compose_all("--- !a\n--- !b") - a4 = compose_all("\"\": !a") - a5 = compose_all("? !a") - with Diagram.hypergraph_equality: - assert a0 == Box("a", Ty(""), Ty("")) - assert a1 == a0 - assert a2 == a0 - assert a3 == a0 @ Box("b", Ty(""), Ty("")) - assert a4 == Box("map", Ty(""), Ty("")) >> a0 - assert a5 == a0 - -def test_untagged(): - a0 = compose_all("") - a1 = compose_all("\"\":") - a2 = compose_all("\"\": a") - a3 = compose_all("a:") - a4 = compose_all("? a") - with Diagram.hypergraph_equality: - assert a0 == Id() - assert a1 == Id("") - assert a2 == Box("map", Ty(""), Ty("a")) - assert a3 == Id("a") - assert a4 == a3 - -def test_bool(): - d = Id("true") @ Id("false") - t = compose_all(open("src/data/bool.yaml")) - with Diagram.hypergraph_equality: - assert t == d - -# u = Ty("unit") -# m = Ty("monoid") - -# def test_monoid(): -# d = Box(u.name, Ty(), m) @ Box("product", m @ m, m) -# t = compose_all(open("src/data/monoid.yaml")) -# with Diagram.hypergraph_equality: -# assert t == d diff --git a/widip/watch.py b/widip/watch.py index c46ffd2..061c633 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -3,62 +3,34 @@ from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer from yaml import YAMLError - -from discopy.closed import Id, Ty, Box from discopy.utils import tuplify, untuplify - from .loader import repl_read from .files import diagram_draw, file_diagram -from .widish import SHELL_RUNNER, compile_shell_program - - -# TODO watch functor ?? +from .widish import SHELL_RUNNER class ShellHandler(FileSystemEventHandler): - """Reload the shell on change.""" def on_modified(self, event): if event.src_path.endswith(".yaml"): print(f"reloading {event.src_path}") try: fd = file_diagram(str(event.src_path)) diagram_draw(Path(event.src_path), fd) - diagram_draw(Path(event.src_path+".2"), fd) except YAMLError as e: print(e) -def watch_main(): - """the process manager for the reader and """ - # TODO watch this path to reload changed files, - # returning an IO as always and maintaining the contract. - print(f"watching for changes in current path") - observer = Observer() - shell_handler = ShellHandler() - observer.schedule(shell_handler, ".", recursive=True) - observer.start() - return observer - def shell_main(file_name): try: while True: - observer = watch_main() + print("watching for changes in current path") + observer = Observer() + observer.schedule(ShellHandler(), ".", recursive=True) + observer.start() try: - prompt = f"--- !{file_name}\n" - source = input(prompt) + source = input(f"--- !{file_name}\n") source_d = repl_read(source) - # source_d.draw( - # textpad=(0.3, 0.1), - # fontsize=12, - # fontsize_types=8) - path = Path(file_name) - diagram_draw(path, source_d) - # source_d = compile_shell_program(source_d) - # diagram_draw(Path(file_name+".2"), source_d) - # source_d = Spider(0, len(source_d.dom), Ty("io")) \ - # >> source_d \ - # >> Spider(len(source_d.cod), 1, Ty("io")) - # diagram_draw(path, source_d) - result_ev = SHELL_RUNNER(source_d)() - print(result_ev) + diagram_draw(Path(file_name), source_d) + constants = tuple(x.name for x in source_d.dom) + print(SHELL_RUNNER(source_d)(*constants)("")) except KeyboardInterrupt: print() except YAMLError as e: @@ -67,14 +39,11 @@ def shell_main(file_name): observer.stop() except EOFError: print("⌁") - exit(0) -def widish_main(file_name, *shell_program_args: str): +def widish_main(file_name, *args): fd = file_diagram(file_name) - path = Path(file_name) - diagram_draw(path, fd) + diagram_draw(Path(file_name), fd) constants = tuple(x.name for x in fd.dom) runner = SHELL_RUNNER(fd)(*constants) - # TODO pass stdin - run_res = runner and runner("") - print(*(tuple(x.rstrip() for x in tuplify(untuplify(run_res)) if x)), sep="\n") + result = runner("") if sys.stdin.isatty() else runner(sys.stdin.read()) + print(*(x.rstrip() for x in tuplify(untuplify(result)) if x), sep="\n") diff --git a/widip/widish.py b/widip/widish.py index 997a6b4..c08f96e 100644 --- a/widip/widish.py +++ b/widip/widish.py @@ -1,79 +1,42 @@ from functools import partial -from itertools import batched -from subprocess import CalledProcessError, run - -from discopy.closed import Category, Functor, Ty, Box, Eval +from subprocess import run +import sys from discopy.utils import tuplify, untuplify -from discopy import python +from discopy import closed, python +def _run_subprocess(args, input_str=None): + return run(args, check=True, text=True, capture_output=True, input=input_str).stdout.rstrip("\n") -io_ty = Ty("io") +def _split_args(ar, args): + n = len(ar.dom) + return args[:n], args[n:] -def run_native_subprocess(ar, *b): - def run_native_subprocess_constant(*params): - if not params: - return "" if ar.dom == Ty() else ar.dom.name - return untuplify(params) - def run_native_subprocess_map(*params): - # TODO cat then copy to two - # but formal is to pass through - mapped = [] - start = 0 - for (dk, k), (dv, v) in batched(zip(ar.dom, b), 2): - # note that the key cod and value dom might be different - b0 = k(*tuplify(params)) - res = untuplify(v(*tuplify(b0))) - mapped.append(untuplify(res)) - - return untuplify(tuple(mapped)) - def run_native_subprocess_seq(*params): - b0 = b[0](*untuplify(params)) - res = untuplify(b[1](*tuplify(b0))) - return res - def run_native_subprocess_inside(*params): - try: - io_result = run( - b, - check=True, text=True, capture_output=True, - input="\n".join(params) if params else None, - ) - res = io_result.stdout.rstrip("\n") - return res - except CalledProcessError as e: - return e.stderr - if ar.name == "⌜−⌝": - return run_native_subprocess_constant - if ar.name == "(||)": - return run_native_subprocess_map - if ar.name == "(;)": - return run_native_subprocess_seq - if ar.name == "g": - res = run_native_subprocess_inside(*b) - return res - if ar.name == "G": - return run_native_subprocess_inside +def _run_constant(ar, *args): + _, params = _split_args(ar, args) + return untuplify(params) if params else ("" if ar.dom == closed.Ty() else ar.dom.name) -SHELL_RUNNER = Functor( - lambda ob: str, - lambda ar: partial(run_native_subprocess, ar), - cod=Category(python.Ty, python.Function)) +def _run_map(ar, *args): + b, params = _split_args(ar, args) + return untuplify(tuple(untuplify(kv(*tuplify(params))) for kv in b)) +def _run_seq(ar, *args): + b, params = _split_args(ar, args) + return untuplify(b[1](*tuplify(b[0](*tuplify(params))))) -SHELL_COMPILER = Functor( - # lambda ob: Ty() if ob == Ty("io") else ob, - lambda ob: ob, - lambda ar: { - # "ls": ar.curry().uncurry() - }.get(ar.name, ar),) - # TODO remove .inside[0] workaround - # lambda ar: ar) +def _run_default(ar, *args): + b, params = _split_args(ar, args) + return _run_subprocess((ar.name,) + b, "\n".join(params) if params else None) +def _run_widip(ar, *args): + b, params = _split_args(ar, args) + return _run_subprocess((sys.executable, "-m", "widip") + b, "\n".join(params) if params else None) -def compile_shell_program(diagram): - """ - close input parameters (constants) - drop outputs matching input parameters - all boxes are io->[io]""" - # TODO compile sequences and parallels to evals - diagram = SHELL_COMPILER(diagram) - return diagram +SHELL_RUNNER = closed.Functor( + lambda ob: str, + lambda ar: { + "widip": partial(partial, _run_widip, ar), + "⌜−⌝": partial(partial, _run_constant, ar), + "(||)": partial(partial, _run_map, ar), + "(;)": partial(partial, _run_seq, ar), + }.get(ar.name, partial(partial, _run_default, ar)), + cod=closed.Category(python.Ty, python.Function))