From 67aafaad2a5b88c404e6cec297e84de0a71ff5aa Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sun, 15 Mar 2026 14:13:34 +0000 Subject: [PATCH 01/17] metaprog: 6.2.2 --- tests/test_metaprog.py | 37 +++++++++++----- widip/computer.py | 19 ++++++--- widip/metaprog.py | 96 +++++++++++++++++++++++++++--------------- widip/widish.py | 39 ++++++++--------- 4 files changed, 120 insertions(+), 71 deletions(-) diff --git a/tests/test_metaprog.py b/tests/test_metaprog.py index b81cd8a..0aa3ae0 100644 --- a/tests/test_metaprog.py +++ b/tests/test_metaprog.py @@ -27,16 +27,31 @@ def after_each_test(request): prog.draw(path=svg_path(f"{test_name}_prog.svg")) mprog.draw(path=svg_path(f"{test_name}_mprog.svg")) -def test_fig_6_1_program_and_metaprogram(request): + +A, B, X = Ty("A"), Ty("B"), Ty("X") +H_ty, L_ty = ProgramTy("H"), ProgramTy("L") +h_ev = ComputableFunction("{H}", X, A, B) +l_ev = ComputableFunction("{L}", X, A, B) +H_to_L = ProgramComputation("H", L_ty, X, A, B) +L_to_H = ProgramComputation("L", H_ty, X, A, B) + +def test_sec_6_2_2(request): + """ + Sec. 6.2.2 {H}L = h, {L}H = l + """ + assert H_to_L.universal_ev() == h_ev + assert L_to_H.universal_ev() == l_ev + request.node.draw_objects = (h_ev, l_ev, H_to_L) + +def test_fig_6_3_eq_0(request): """ - Fig. 6.1 program and metaprogram + Fig. 6.3 {X}H y = {H}L(X, y) """ - A, B, X = Ty("A"), Ty("B"), Ty("X") - comp = Computation("f", X, A, B) - prog = Program(comp) - mprog = Metaprogram(prog) - right = MetaprogramFunctor()(mprog) - assert right == prog - right = ProgramFunctor()(right) - assert right == comp - request.node.draw_objects = (comp, prog, mprog) + # comp = ComputableFunction("f", X, A, B) + # prog = Program("f", L_ty, X) + # mprog = Metaprogram("F", L_ty) + # right = MetaprogramFunctor()(mprog) + # assert right == prog + # right = ProgramFunctor()(right) + # assert right == comp + # request.node.draw_objects = (comp, prog, mprog) diff --git a/widip/computer.py b/widip/computer.py index b9e4e50..4adf289 100644 --- a/widip/computer.py +++ b/widip/computer.py @@ -30,21 +30,31 @@ def tensor(self, *others): @factory -class Diagram(markov.Diagram): +class Diagram(markov.Diagram, closed.Diagram): ty_factory = Ty def to_drawing(self): # Fix exponential type drawing recursion. return markov.Diagram.to_drawing(self, functor_factory=closed.Functor) + +class Category(markov.Category, closed.Diagram): + """2.5.1: A monoidal computer is a (symmetric, strict) monoidal category""" + ob, ar = Ty, Diagram + + class Functor(markov.Functor, closed.Functor): """ Preserves markov, closed, and computer boxes. """ + dom = cod = Category(Ty, Diagram) + def __call__(self, other): if isinstance(other, Diagram): return other - return Functor.__call__(self, other) + if isinstance(other, (closed.Diagram, closed.Ty)): + return closed.Functor.__call__(self, other) + return markov.Functor.__call__(self, other) class Box(markov.Box, closed.Box, Diagram): @@ -104,11 +114,6 @@ def __init__(self, box, A, B): Box.__init__(self, f"uncurry({box.name})", dom, cod) -class Category(markov.Category): - """2.5.1: A monoidal computer is a (symmetric, strict) monoidal category""" - ob, ar = Ty, Diagram - - def Id(x=Ty()): """Identity diagram over widip.computer.Ty (defaults to Ty()).""" return Diagram.id(x) diff --git a/widip/metaprog.py b/widip/metaprog.py index 1673367..8776c19 100644 --- a/widip/metaprog.py +++ b/widip/metaprog.py @@ -2,53 +2,81 @@ Chapter 6. Computing programs. Metaprograms are programs that compute programs. """ -from discopy.monoidal import Ty - from .computer import * -class Computation(Box): +class Metaprogram(Box): + """ + a metaprogram, presented as a cartesian function G:I⊸P + """ + def __init__(self, name, P: ProgramTy): + Box.__init__(self, name, dom=Ty(), cod=P) + + +class ProgramComputation(Diagram): """ - An eval box with distinguished X-indexing + Section 3.1: a function is computable when it is programmable. + Fig 6.1: A computation f encoded such that f = {F}. """ - def __init__(self, name, X, A, B): - self.name, self.X, self.A, self.B = name, X, A, B - super().__init__(name, X @ A, B) + def __init__(self, name, P: ProgramTy, X: Ty, A: Ty, B: Ty): + """ + Running a given program is a routine operation. + Every function is computable, in the sense that there is a program for it. + """ + self.universal_ev_diagram = ComputableFunction("{"+name+"}", X, A, B) + diagram = ( + Program(name, P, X) @ A, + Computer(P, A, B)) + # TODO review .inside + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + + def universal_ev(self): + return self.universal_ev_diagram - def specialize(self): - return Eval(self.B << self.X @ self.A) +class MetaprogramComputation(Diagram): + """ + Fig 6.1: A program F encoded such that F = {ℱ}. + """ + def __init__(self, name, P: ProgramTy, PP: ProgramTy, X: Ty, A: Ty, B: Ty): + self.partial_ev_diagram = ( + ProgramComputation(name, PP, Ty(), X, P) @ A + >> Computer(P, A, B) + ) + diagram = ( + Program(name, PP, Ty()) @ X @ A, + Computer(PP, X, P) @ A, + Computer(P, A, B) + ) + # TODO review .inside + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + def partial_ev(self): + return self.partial_ev_diagram -class Program(monoidal.Bubble, Box): +class Interpreter(Program): """ - Fig 6.1: F:f→f - A computation f encoded such that f = {F}. + Sec 2.2: The program evaluators are computable functions representing typed interpreters. The type P is the set of program expressions. + Ex 2.5.2: C_P's program evaluators correspond to P's interpreters. + Sec 6.2.2: programs that implement universal evaluators are called interpreters. """ - def __init__(self, f: Computation): - self.f = f - arg = ( - Box(f.name, f.X, f.B << f.A) @ f.A - >> Eval(f.B << f.A)) - monoidal.Bubble.__init__(self, arg, dom=f.X @ f.A, cod=f.cod) - def specialize(self): - return self.f -class Metaprogram(monoidal.Bubble, Box): +class Specializer(Metaprogram): """ - Fig 6.1: ℱ:I->(F->F) - A program F encoded such that F = {ℱ}. + Ex 2.5.2: C_P's partial evaluators correspond to P's specializers. + Sec 6.2.2: metaprograms that implement partial evaluators """ - def __init__(self, F: Program): - self.F, f = F, F.f - arg = ( - Box(f.name, Ty(), f.B << f.A << f.X) @ f.X @ f.A - >> Eval(f.B << f.A << f.X) @ f.A - >> Eval(f.B << f.A)) - monoidal.Bubble.__init__(self, arg, dom=f.X @ f.A, cod=f.cod) + def partial_ev(self, X): + raise NotImplementedError() - def specialize(self): - return self.F +class Compiler(Metaprogram): + """Fig 6.4: partially evaluating a specializer on an interpreter gives a compiler.""" + def __init__(self, P: Specializer, X: Interpreter): + """""" + def partial_ev(self): + """""" class ProgramFunctor(Functor): """ @@ -56,7 +84,7 @@ class ProgramFunctor(Functor): Preserves computer boxes and metaprograms. """ def __call__(self, other): - if isinstance(other, Program): + if isinstance(other, Program) and callable(getattr(other, "specialize", None)): other = other.specialize() return Functor.__call__(self, other) @@ -67,6 +95,6 @@ class MetaprogramFunctor(Functor): Preserves computer boxes and programs. """ def __call__(self, other): - if isinstance(other, Metaprogram): + if isinstance(other, Metaprogram) and callable(getattr(other, "specialize", None)): return other.specialize() return Functor.__call__(self, other) diff --git a/widip/widish.py b/widip/widish.py index 3b6a0e3..3d2ec50 100644 --- a/widip/widish.py +++ b/widip/widish.py @@ -1,9 +1,12 @@ from functools import partial from subprocess import CalledProcessError, run +import subprocess from discopy.utils import tuplify, untuplify from discopy import closed, python +from . import computer + io_ty = closed.Ty("io") @@ -52,26 +55,24 @@ def ar_mapping(ar): return partial(partial, run_native_subprocess_seq, ar) return partial(partial, run_native_subprocess_default, ar) -SHELL_RUNNER = closed.Functor( - lambda ob: partial, - ar_mapping, - cod=closed.Category(python.Ty, python.Function)) - -SHELL_COMPILER = closed.Functor( - lambda ob: ob, - lambda ar: { - # "ls": ar.curry().uncurry() - }.get(ar.name, ar),) - # TODO remove .inside[0] workaround - # lambda ar: ar) -def compile_shell_program(diagram): +class ShellRunner(computer.Functor): """ - 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 + Transforms a computer diagram into a runnable Python function of a subprocess pipeline. + Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P) for each pair of computer.types A, B. + """ + def __init__(self): + computer.Functor.__init__( + self, + lambda _: python.Ty, + ar_mapping, + cod=computer.Category(python.Ty, python.Function)) + + def __call__(self, other): + """subprocess.run(cmd=X, *A): B is the natural family of surjections.""" + constants = tuple(x.name for x in other.dom) + return python.Function( + partial(subprocess.run, *constants), + dom=python.Ty, cod=python.Ty) From f0aa9a5ade7bd8d171a48363100855a03a989ae2 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sun, 15 Mar 2026 18:28:15 +0000 Subject: [PATCH 02/17] wip: ch 7 stateful computing --- tests/test_state.py | 84 +++++++++++++++++++++++++++++++++++ widip/state.py | 105 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/test_state.py create mode 100644 widip/state.py diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..8a20bea --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,84 @@ +from widip.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty +from widip.state import ( + Execution, + MonoidalComputer, + Process, + ProgramClosedCategory, + execute, + fixed_state, + out, + simulate, + sta, +) + + +X, Y, A, B = Ty("X"), Ty("Y"), Ty("A"), Ty("B") +P = ProgramTy("P") +H_ty, L_ty = ProgramTy("H"), ProgramTy("L") + + +def test_eq_7_1_process_is_a_pair_of_functions(): + q = Process("q", X, A, B) + expected = Copy(X @ A) >> sta(q) @ out(q) + + assert sta(q).dom == X @ A + assert sta(q).cod == X + assert out(q).dom == X @ A + assert out(q).cod == B + assert q == expected + + +def test_fig_7_2_simulation_is_postcomposition_with_state_map(): + q = Process("q", X, A, B) + s = Box("s", X, Y) + + simulated = simulate(q, s) + + assert simulated == q >> s @ B + assert simulated.dom == X @ A + assert simulated.cod == Y @ B + + +def test_sec_7_3_program_execution_is_stateful(): + execution = Execution(P, A, B) + + assert execution.dom == P @ A + assert execution.cod == P @ B + assert execution.universal_ev() == Computer(P, A, P @ B) + assert sta(execution).cod == P + assert out(execution).cod == B + + +def test_sec_7_4_fixed_state_lifts_a_function_to_a_process(): + g = ComputableFunction("g", X, A, B) + hat_g = fixed_state(g) + + assert hat_g == (Copy(X) @ A >> X @ g) + assert hat_g.dom == X @ A + assert hat_g.cod == X @ B + + +def test_sec_7_4_execute_uses_stateful_execution(): + Q = Program("Q", P, X) + q = execute(Q, A, B) + + assert q == Q @ A >> Execution(P, A, B) + assert q.dom == X @ A + assert q.cod == P @ B + + +def test_sec_8_3_program_closed_category_chooses_a_language_type(): + computer_category = MonoidalComputer() + high_level = ProgramClosedCategory(H_ty) + low_level = ProgramClosedCategory(L_ty) + + assert isinstance(high_level, MonoidalComputer) + assert isinstance(low_level, MonoidalComputer) + assert high_level.program_ty == H_ty + assert low_level.program_ty == L_ty + assert high_level.evaluator(A, B) == Computer(H_ty, A, B) + assert low_level.evaluator(A, B) == Computer(L_ty, A, B) + assert high_level.execution(A, B).universal_ev() == Computer(H_ty, A, H_ty @ B) + assert low_level.execution(A, B).universal_ev() == Computer(L_ty, A, L_ty @ B) + assert computer_category.ob == high_level.ob == low_level.ob + assert computer_category.ar == high_level.ar == low_level.ar diff --git a/widip/state.py b/widip/state.py new file mode 100644 index 0000000..233c84c --- /dev/null +++ b/widip/state.py @@ -0,0 +1,105 @@ +"""Chapter 7: Stateful computing.""" + +from . import computer + + +class Process(computer.Diagram): + """ + Eq. 7.1: a process q : X x A -> X x B is paired from state update and output. + """ + + def __init__(self, name, X: computer.Ty, A: computer.Ty, B: computer.Ty): + self.name, self.X, self.A, self.B = name, X, A, B + self.state_update_diagram = computer.Box(f"sta({name})", X @ A, X) + self.output_diagram = computer.Box(f"out({name})", X @ A, B) + + diagram = ( + computer.Copy(X @ A), + self.state_update_diagram @ self.output_diagram, + ) + inside = sum((d.inside for d in diagram), ()) + computer.Diagram.__init__(self, inside, X @ A, X @ B) + + def sta(self): + return self.state_update_diagram + + def out(self): + return self.output_diagram + + +class Execution(Process): + """ + Sec. 7.3: program execution is a process P x A -> P x B. + """ + + def __init__(self, P: computer.ProgramTy, A: computer.Ty, B: computer.Ty): + self.universal_ev_diagram = computer.Computer(P, A, P @ B) + Process.__init__(self, "{}", P, A, B) + + def universal_ev(self): + """ + Eq. 7.3: program execution is the evaluator with output type P x B. + """ + return self.universal_ev_diagram + + def specialize(self): + return self.universal_ev() + + +class MonoidalComputer(computer.Category): + """ + The ambient computer category may contain more than one program language type. + """ + + +class ProgramClosedCategory(MonoidalComputer): + """ + Sec. 8.3: a program-closed category chooses one distinguished program type. + """ + + def __init__(self, program_ty: computer.ProgramTy): + self.program_ty = program_ty + MonoidalComputer.__init__(self) + + def evaluator(self, A: computer.Ty, B: computer.Ty): + return computer.Computer(self.program_ty, A, B) + + def execution(self, A: computer.Ty, B: computer.Ty): + return Execution(self.program_ty, A, B) + + +def sta(q): + """Projection to the state-update component of a process.""" + if hasattr(q, "sta") and callable(q.sta): + return q.sta() + raise TypeError("sta expects a state.Process") + + +def out(q): + """Projection to the output component of a process.""" + if hasattr(q, "out") and callable(q.out): + return q.out() + raise TypeError("out expects a state.Process") + + +def simulate(q: Process, s: computer.Diagram): + """ + Fig. 7.2: a simulation along s is postcomposition with s x id_B. + """ + return q >> s @ q.B + + +def execute(Q: computer.Diagram, A: computer.Ty, B: computer.Ty): + """ + Sec. 7.3: execute an X-parameterized program as a stateful process. + """ + return Q @ A >> Execution(Q.cod, A, B) + + +def fixed_state(g: computer.Diagram): + """ + Sec. 7.4 proof b: lift g : X x A -> B to the fixed-state process X x A -> X x B. + """ + X = g.dom[:1] + A = g.dom[1:] + return computer.Copy(X) @ A >> X @ g From cf0227e58cf77ac7f882f700e4d787dd076b948c Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sun, 15 Mar 2026 20:20:43 +0000 Subject: [PATCH 03/17] wip: hif --- widip/hif.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 widip/hif.py diff --git a/widip/hif.py b/widip/hif.py new file mode 100644 index 0000000..a2386f4 --- /dev/null +++ b/widip/hif.py @@ -0,0 +1,78 @@ +"""Helpers for traversing YAML hypergraphs produced by nx_yaml.""" + +from nx_hif.hif import HyperGraph, hif_edge_incidences, hif_node, hif_node_incidences + + +def node_data(graph: HyperGraph, node): + """Return the attribute dict of a hypergraph node.""" + return hif_node(graph, node) + + +def node_kind(graph: HyperGraph, node): + """Return the YAML node kind for a hypergraph node.""" + return node_data(graph, node)["kind"] + + +def node_tag(graph: HyperGraph, node): + """Return the YAML tag without the leading !, if present.""" + return (node_data(graph, node).get("tag") or "")[1:] + + +def node_value(graph: HyperGraph, node): + """Return the scalar payload, defaulting to the empty string.""" + return node_data(graph, node).get("value", "") + + +def node_edges(graph: HyperGraph, node, *, key="next", direction="head"): + """Return incidences leaving a node for the requested key/direction.""" + return tuple(hif_node_incidences(graph, node, key=key, direction=direction)) + + +def edge_nodes(graph: HyperGraph, edge, *, key="start", direction="head"): + """Return incidences from an edge to its incident nodes.""" + return tuple(hif_edge_incidences(graph, edge, key=key, direction=direction)) + + +def successor_nodes(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """ + Yield nodes reached by following incidences of type edge_key then node_key. + """ + for edge, _, _, _ in node_edges(graph, node, key=edge_key): + for _, target, _, _ in edge_nodes(graph, edge, key=node_key): + yield target + + +def first_successor_node(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """Return the first successor node for a given incidence pattern, if any.""" + return next(iter(successor_nodes(graph, node, edge_key=edge_key, node_key=node_key)), None) + + +def stream_document_nodes(graph: HyperGraph, stream=0): + """Yield document nodes contained in a YAML stream node.""" + yield from successor_nodes(graph, stream, edge_key="next", node_key="start") + + +def document_root_node(graph: HyperGraph, document): + """Return the root YAML node for a document node, if any.""" + return first_successor_node(graph, document, edge_key="next", node_key="start") + + +def sequence_item_nodes(graph: HyperGraph, sequence): + """Yield sequence items in order by following next/forward links.""" + current = first_successor_node(graph, sequence, edge_key="next", node_key="start") + while current is not None: + yield current + current = first_successor_node(graph, current, edge_key="forward", node_key="start") + + +def mapping_entry_nodes(graph: HyperGraph, mapping): + """ + Yield `(key_node, value_node)` pairs in order for a YAML mapping node. + """ + key_node = first_successor_node(graph, mapping, edge_key="next", node_key="start") + while key_node is not None: + value_node = first_successor_node(graph, key_node, edge_key="forward", node_key="start") + if value_node is None: + break + yield key_node, value_node + key_node = first_successor_node(graph, value_node, edge_key="forward", node_key="start") From 4cf8130dab4c39ac4b578b3f225d2ee8dfdce9da Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Mon, 16 Mar 2026 15:43:54 +0000 Subject: [PATCH 04/17] hyperwip: AI madness --- widip/comput/__init__.py | 1 + widip/{lang.py => comput/boxes.py} | 99 ++-------- widip/comput/compile.py | 25 +++ widip/comput/computer.py | 66 +++++++ widip/comput/equations.py | 51 +++++ widip/comput/loader.py | 51 +++++ widip/comput/python.py | 40 ++++ widip/comput/widish.py | 87 ++++++++ widip/computer.py | 119 ----------- widip/files.py | 7 +- widip/loader.py | 274 +++++++++++--------------- widip/metaprog.py | 100 ---------- widip/metaprog/__init__.py | 116 +++++++++++ widip/metaprog/loader.py | 50 +++++ widip/metaprog/widish.py | 189 ++++++++++++++++++ widip/{state.py => state/__init__.py} | 58 +++++- widip/state/loader.py | 62 ++++++ widip/state/widish.py | 31 +++ widip/to_py.py | 32 --- widip/watch.py | 14 +- widip/widish.py | 78 -------- widip/wire/__init__.py | 1 + widip/wire/functions.py | 22 +++ widip/wire/loader.py | 75 +++++++ widip/wire/services.py | 33 ++++ widip/wire/types.py | 21 ++ widip/wire/widish.py | 45 +++++ 27 files changed, 1154 insertions(+), 593 deletions(-) create mode 100644 widip/comput/__init__.py rename widip/{lang.py => comput/boxes.py} (50%) create mode 100644 widip/comput/compile.py create mode 100644 widip/comput/computer.py create mode 100644 widip/comput/equations.py create mode 100644 widip/comput/loader.py create mode 100644 widip/comput/python.py create mode 100644 widip/comput/widish.py delete mode 100644 widip/computer.py delete mode 100644 widip/metaprog.py create mode 100644 widip/metaprog/__init__.py create mode 100644 widip/metaprog/loader.py create mode 100644 widip/metaprog/widish.py rename widip/{state.py => state/__init__.py} (66%) create mode 100644 widip/state/loader.py create mode 100644 widip/state/widish.py delete mode 100644 widip/to_py.py delete mode 100644 widip/widish.py create mode 100644 widip/wire/__init__.py create mode 100644 widip/wire/functions.py create mode 100644 widip/wire/loader.py create mode 100644 widip/wire/services.py create mode 100644 widip/wire/types.py create mode 100644 widip/wire/widish.py diff --git a/widip/comput/__init__.py b/widip/comput/__init__.py new file mode 100644 index 0000000..fef23b2 --- /dev/null +++ b/widip/comput/__init__.py @@ -0,0 +1 @@ +"""Textbook Chapter 2 elements for the Run language.""" diff --git a/widip/lang.py b/widip/comput/boxes.py similarity index 50% rename from widip/lang.py rename to widip/comput/boxes.py index 44c4226..42a105c 100644 --- a/widip/lang.py +++ b/widip/comput/boxes.py @@ -1,6 +1,6 @@ -"""Textbook compilation transformations for the Run language.""" +"""Chapter 2 bubble constructors for the Run language.""" -from discopy import closed, markov, monoidal +from discopy import monoidal from . import computer @@ -11,6 +11,7 @@ class Partial(monoidal.Bubble, computer.Box): A partial evaluator is a (P×Y)-indexed program satisfying {[Γ]y}a = {Γ}(y,a). X=P×Y and g:P×Y×A→B """ + def __init__(self, gamma): self.gamma = gamma self.X, self.A = gamma.cod.exponent @@ -23,13 +24,10 @@ def __init__(self, gamma): ) monoidal.Bubble.__init__(self, arg, dom=arg.dom, cod=arg.cod) - + def specialize(self): """Fig. 2.5: compile partial-evaluator box as operator + eval.""" - return ( - self.gamma @ self.X @ self.A - >> computer.Eval(self.B << self.X @ self.A) - ) + return self.gamma @ self.X @ self.A >> computer.Eval(self.B << self.X @ self.A) class Sequential(monoidal.Bubble, computer.Box): @@ -37,6 +35,7 @@ class Sequential(monoidal.Bubble, computer.Box): Sec. 2.2.3. (;)_ABC:P×P⊸P A -{F;G}→ C = A -{F}→ B -{G}→ C """ + def __init__(self, F, G): self.F, self.G = F, G A = F.cod.exponent @@ -55,9 +54,9 @@ def specialize(self): B = F.cod.base C = G.cod.base - F_Eval = computer.Eval(B << A) - G_Eval = computer.Eval(C << B) - return G @ F @ A >> (C << B) @ F_Eval >> G_Eval + F_eval = computer.Eval(B << A) + G_eval = computer.Eval(C << B) + return G @ F @ A >> (C << B) @ F_eval >> G_eval class Parallel(monoidal.Bubble, computer.Box): @@ -65,6 +64,7 @@ class Parallel(monoidal.Bubble, computer.Box): Sec. 2.2.3. (||)_AUBV:P×P⊸P A×U -{F||H}→ B×V = A -{F}→ B × U-{T}→ V """ + def __init__(self, F, T): self.F, self.T = F, T A, B = F.cod.exponent, F.cod.base @@ -94,86 +94,15 @@ class Data(monoidal.Bubble, computer.Box): ⌜a⌝: P {⌜a⌝} = a """ + def __init__(self, A): self.A = A if isinstance(A, computer.Ty) else computer.Ty(A) arg = ( - computer.Box("⌜−⌝", self.A, self.A << computer.Ty()) >> - computer.Eval(self.A << computer.Ty())) + computer.Box("⌜−⌝", self.A, self.A << computer.Ty()) + >> computer.Eval(self.A << computer.Ty()) + ) monoidal.Bubble.__init__(self, arg, dom=self.A, cod=self.A) - # computer.Box.__init__(self, "⌜−⌝", P, self.A) def specialize(self): """Eq. 2.8: compile quoted data using idempotent quote/eval structure.""" return computer.Id(self.A) - - - -class Compile(closed.Functor, markov.Functor): - """Pure diagram compilation of custom boxes into closed+markov structure.""" - - dom = computer.Category() - cod = computer.Category() - - def __init__(self): - super().__init__(ob=lambda ob: ob, ar=self.ar_map) - - def __call__(self, box): - if isinstance(box, (Sequential, Parallel, Partial, Data)): - return box.specialize() - return box - - def ar_map(self, box): - assert not isinstance(box, computer.Box) - return box - - -### TODO -### recover equations below - -def run(G: computer.Diagram, A: computer.Ty, B: computer.Ty): - """Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P) for each pair of computer.types A, B.""" - del G - return computer.Eval(A, B) - - -def eval_f(G: computer.Diagram): - """Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P) for each pair of computer.types A, B.""" - return computer.Eval(G.dom, G.cod) - - -def parametrize(g: computer.Diagram): - """ - Eq. 2.2: an X-parametrized program, presented as a cartesian function G:X⊸P that evaluates to g. - g(x, a) = {Gx}a. - """ - G = g.curry(left=False) - A = g.dom[1:] - return G >> computer.Eval(G.cod @ A >> g.cod) - - -def reparametrize(g: computer.Diagram, s: computer.Diagram): - """ - Fig. 2.3 Reparametring x along s:Y⊸X leads to the family g_s(y):A→B parametrized by y:Y and implemented by the reparametrization Gs=(Y -s⊸ X -G⊸ P) of the program G for g. - """ - A = g.dom[1:] - Gs = s @ A >> g.curry(left=False) - return Gs >> computer.Eval(Gs.cod @ A >> g.cod) - - -def substitute(g: computer.Diagram, s: computer.Diagram): - """ - Fig. 2.3 Substituting for a along t:C→A leads to the familiy h_X=(C -t→ A -g_x→ B), still parametrized over X, but requiring a program H:X⊸P) of the program G for g. - """ - A = g.dom[1:] - Gs = s @ A >> g.curry(left=False) - return Gs >> computer.Eval(Gs.cod @ A >> g.cod) - - -def constant_a(f: computer.Diagram): - """Sec. 2.2.1.3 a) f:I×A→B. f(a) = {Φ_a}().""" - return f.curry(0, left=False) - - -def constant_b(f: computer.Diagram): - """Sec. 2.2.1.3 b) f: A×I→B. f(a) = {F}(a).""" - return f.curry(1, left=False) diff --git a/widip/comput/compile.py b/widip/comput/compile.py new file mode 100644 index 0000000..73a5872 --- /dev/null +++ b/widip/comput/compile.py @@ -0,0 +1,25 @@ +"""Chapter 2 compiler for custom Run-language bubbles.""" + +from discopy import closed, markov + +from . import computer +from .boxes import Data, Parallel, Partial, Sequential + + +class Compile(closed.Functor, markov.Functor): + """Pure diagram compilation of custom boxes into closed+markov structure.""" + + dom = computer.Category() + cod = computer.Category() + + def __init__(self): + super().__init__(ob=lambda ob: ob, ar=self.ar_map) + + def __call__(self, box): + if isinstance(box, (Sequential, Parallel, Partial, Data)): + return box.specialize() + return box + + def ar_map(self, box): + assert not isinstance(box, computer.Box) + return box diff --git a/widip/comput/computer.py b/widip/comput/computer.py new file mode 100644 index 0000000..88566b1 --- /dev/null +++ b/widip/comput/computer.py @@ -0,0 +1,66 @@ +"""Monoidal-computer core layered over the Chapter 1 wire calculus.""" + +from discopy import cat, monoidal + +from ..wire.functions import Box, Category, Functor +from ..wire.services import Copy, Delete, Swap +from ..wire.types import Diagram, Id, Ty + + +class ProgramOb(cat.Ob): + """Distinguished atomic object for program wires.""" + + +class ProgramTy(Ty): + """Distinguished type constructor for program objects.""" + + def __init__(self, name): + Ty.__init__(self, ProgramOb(name)) + + +class ComputableFunction(Box): + """ + An X-parametrized computation XxA→B. + X: state, A: input, B: output. + Fig 6.1: program encoding. + Figure 2: Computation as a conversation. + """ + def __init__(self, name, X, A, B): + Box.__init__(self, name, X @ A, B) + + +class Program(Box): + """ + Eq. 2.2: an X-parametrized program, + presented as a cartesian function G:X⊸P + """ + def __init__(self, name, P: ProgramTy, X: Ty): + Box.__init__(self, name, dom=X, cod=P) + + +class Computer(Box): + """ + The program evaluators are computable functions, representing typed interpreters. + 2.2.1.1 Program evaluators are universal parametrized functions + 2.5.1 c) Program evaluator {}:P×A→B (default type P) + """ + def __init__(self, P: ProgramTy, A: Ty, B: Ty): + self.P, self.A, self.B = P, A, B + Box.__init__(self, "{}", P @ A, B) + + +class Uncurry(monoidal.Bubble, Box): + """ + Fig. 2.7 right-hand-side syntax: a composition-program box followed by eval. + + - `Uncurry((;), A, B, C)` stands for `((;) @ A) >> {}_{A,C}` + with type `P×P×A⊸C`. + - `Uncurry((||), A, U, B, V)` stands for `((||) @ A×U) >> {}_{A×U,B×V}` + with type `P×P×A×U⊸B×V`. + """ + def __init__(self, box, A, B): + dom, cod = box.dom @ A, B + # Keep uncurry as a typed layered diagram, analogous to closed.Curry. + arg = box.bubble(dom=dom, cod=cod) + monoidal.Bubble.__init__(self, arg, dom=dom, cod=cod, drawing_name="$\\Lambda^{-1}$") + Box.__init__(self, f"uncurry({box.name})", dom, cod) diff --git a/widip/comput/equations.py b/widip/comput/equations.py new file mode 100644 index 0000000..908ee57 --- /dev/null +++ b/widip/comput/equations.py @@ -0,0 +1,51 @@ +"""Chapter 2 helper equations for the Run language.""" + +from . import computer + + +def run(G: computer.Diagram, A: computer.Ty, B: computer.Ty): + """Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P).""" + del G + return computer.Eval(A, B) + + +def eval_f(G: computer.Diagram): + """Eq. 2.15: evaluators as surjections from programs to computations.""" + return computer.Eval(G.dom, G.cod) + + +def parametrize(g: computer.Diagram): + """ + Eq. 2.2: present an X-parametrized computation as a program G:X⊸P. + """ + G = g.curry(left=False) + A = g.dom[1:] + return G >> computer.Eval(G.cod @ A >> g.cod) + + +def reparametrize(g: computer.Diagram, s: computer.Diagram): + """ + Fig. 2.3: reparametrize x along s:Y⊸X to obtain a Y-indexed family. + """ + A = g.dom[1:] + Gs = s @ A >> g.curry(left=False) + return Gs >> computer.Eval(Gs.cod @ A >> g.cod) + + +def substitute(g: computer.Diagram, s: computer.Diagram): + """ + Fig. 2.3: substitute for a along t:C→A while keeping the same parameter space. + """ + A = g.dom[1:] + Gs = s @ A >> g.curry(left=False) + return Gs >> computer.Eval(Gs.cod @ A >> g.cod) + + +def constant_a(f: computer.Diagram): + """Sec. 2.2.1.3 a) f:I×A→B. f(a) = {Φ_a}().""" + return f.curry(0, left=False) + + +def constant_b(f: computer.Diagram): + """Sec. 2.2.1.3 b) f:A×I→B. f(a) = {F}(a).""" + return f.curry(1, left=False) diff --git a/widip/comput/loader.py b/widip/comput/loader.py new file mode 100644 index 0000000..89d987e --- /dev/null +++ b/widip/comput/loader.py @@ -0,0 +1,51 @@ +"""Loader-language program constants.""" + +import shlex + +from . import computer + + +loader_program_ty = computer.ProgramTy("yaml") + + +class LoaderProgram(computer.Program): + """Closed program in the YAML loader language.""" + + def __init__(self, name: str): + computer.Program.__init__(self, name, loader_program_ty, computer.Ty()) + + +class LoaderScalar(LoaderProgram): + """Closed loader program representing scalar content.""" + + def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": + raise NotImplementedError + + +class LoaderEmpty(LoaderScalar): + """Empty scalar in the loader language.""" + + def __init__(self): + LoaderScalar.__init__(self, repr("")) + + def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": + return LoaderCommand(program.argv) + + +class LoaderLiteral(LoaderScalar): + """Literal scalar in the loader language.""" + + def __init__(self, text: str): + self.text = text + LoaderScalar.__init__(self, repr(text)) + + def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": + return LoaderCommand(program.argv + (self.text,)) + + +class LoaderCommand(LoaderProgram): + """Loader-language command program before backend interpretation.""" + + def __init__(self, argv): + self.argv = tuple(argv) + LoaderProgram.__init__(self, shlex.join(self.argv)) diff --git a/widip/comput/python.py b/widip/comput/python.py new file mode 100644 index 0000000..4c535c7 --- /dev/null +++ b/widip/comput/python.py @@ -0,0 +1,40 @@ +from discopy import closed, markov, monoidal + +from . import computer + + +class PythonComputationCategory(closed.Category, markov.Category): + """""" + + +class PythonComputationFunctor(monoidal.Functor): + """ + Transforms computer diagrams into lower-level runnable diagrams. + Preserves computations including closed and markov boxes. + """ + + def __init__(self): + monoidal.Functor.__init__( + self, + self.ob_map, + self.ar_map, + dom=computer.Category(), + cod=PythonComputationCategory(), + ) + + def __call__(self, other): + if hasattr(other, "partial_ev") and callable(other.partial_ev) and hasattr(other, "universal_ev") and callable(other.universal_ev): + arg = other.universal_ev() + return closed.Curry(arg, len(arg.dom)) + if hasattr(other, "universal_ev") and callable(other.universal_ev): + return other.universal_ev() + return other + + def ob_map(self, ob): + return ob + + def ar_map(self, ar): + return ar + + +to_py = PythonComputationFunctor() diff --git a/widip/comput/widish.py b/widip/comput/widish.py new file mode 100644 index 0000000..d7c5353 --- /dev/null +++ b/widip/comput/widish.py @@ -0,0 +1,87 @@ +"""Shell-language program constants.""" + +import shlex +import subprocess + +from . import computer +from ..wire.widish import io_ty + + +shell_program_ty = computer.ProgramTy("sh") + + +class ShellProgram(computer.Program): + """Closed shell program constant.""" + + def __init__(self, name: str): + computer.Program.__init__(self, name, shell_program_ty, computer.Ty()) + + def run(self, stdin: str) -> str: + raise NotImplementedError + + +class ScalarProgram(ShellProgram): + """Closed shell program representing scalar content.""" + + def partial_apply(self, program: "Command") -> "Command": + raise NotImplementedError + + +class Empty(ScalarProgram): + """Closed empty scalar program, acting as the identity on streams.""" + + def __init__(self): + ScalarProgram.__init__(self, repr("")) + + def run(self, stdin: str) -> str: + return stdin + + def partial_apply(self, program: "Command") -> "Command": + return Command(program.argv) + + +class Literal(ScalarProgram): + """Closed literal shell program.""" + + def __init__(self, text: str): + self.text = text + ShellProgram.__init__(self, repr(text)) + + def run(self, stdin: str) -> str: + del stdin + return self.text + + def partial_apply(self, program: "Command") -> "Command": + return Command(program.argv + (self.text,)) + + +class Command(ShellProgram): + """Closed POSIX command shell program.""" + + def __init__(self, argv): + self.argv = tuple(argv) + ShellProgram.__init__(self, shlex.join(self.argv)) + + def run(self, stdin: str) -> str: + completed = subprocess.run( + self.argv, + input=stdin, + text=True, + capture_output=True, + check=True, + ) + return completed.stdout + + +class ShellStateUpdate(computer.Box): + """State projection box for shell execution.""" + + def __init__(self): + computer.Box.__init__(self, "sta(shell)", shell_program_ty @ io_ty, shell_program_ty) + + +class ShellOutput(computer.Box): + """Observable-output box for shell execution.""" + + def __init__(self): + computer.Box.__init__(self, "out(shell)", shell_program_ty @ io_ty, io_ty) diff --git a/widip/computer.py b/widip/computer.py deleted file mode 100644 index 4adf289..0000000 --- a/widip/computer.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Section 2.5 monoidal-computer core and Run-language primitives.""" - -from discopy import closed, markov, monoidal -from discopy.utils import factory - - -@factory -class Ty(closed.Ty): - def __init__(self, *inside): - # Normalization for casts coming from DisCoPy internals: - # `Ty(closed.Ty(...))` should denote the same wire tuple, not a nested - # atomic object containing a whole type. - if len(inside) == 1 and isinstance(inside[0], closed.Ty): - inside = inside[0].inside - closed.Ty.__init__(self, *inside) - - def tensor(self, *others): - # Preserve distinguished subtypes when tensoring with the monoidal unit. - if len(others) == 1: - other = others[0] - if len(self) == 0: - return other - if len(other) == 0: - return self - return closed.Ty.tensor(self, *others) - - -# factory = closed.Ty so discopy's Curry/Eval can use closed.Ty objects. -Ty.factory = closed.Ty - - -@factory -class Diagram(markov.Diagram, closed.Diagram): - ty_factory = Ty - - def to_drawing(self): - # Fix exponential type drawing recursion. - return markov.Diagram.to_drawing(self, functor_factory=closed.Functor) - - -class Category(markov.Category, closed.Diagram): - """2.5.1: A monoidal computer is a (symmetric, strict) monoidal category""" - ob, ar = Ty, Diagram - - -class Functor(markov.Functor, closed.Functor): - """ - Preserves markov, closed, and computer boxes. - """ - dom = cod = Category(Ty, Diagram) - - def __call__(self, other): - if isinstance(other, Diagram): - return other - if isinstance(other, (closed.Diagram, closed.Ty)): - return closed.Functor.__call__(self, other) - return markov.Functor.__call__(self, other) - - -class Box(markov.Box, closed.Box, Diagram): - """""" - - -class Copy(Box, markov.Copy): - """ - 1.2, 2.5.1 a) Copying data service: ∆:A→A×A. - """ - def __init__(self, A): - if A: - markov.Copy.__init__(self, A, 2) - else: - # ∆I = (I-id→I). - markov.Box.__init__(self, "∆", Ty(), Ty()) - - -class Delete(Box, markov.Discard): - """ - 1.2, 2.5.1 a) Deleting data service: A⊸I. - """ - def __init__(self, A): - if A: - markov.Discard.__init__(self, A) - else: - # ⊸I = (I-id→I). - markov.Box.__init__(self, "⊸", Ty(), Ty()) - - -class Swap(Box, markov.Swap): - """1.2""" - - -class Eval(Box, closed.Eval): - """ - The program evaluators are computable functions, representing typed interpreters. - 2.2.1.1 - 2.5.1 c) Program evaluator {}:P×A→B - """ - - -class Uncurry(monoidal.Bubble, Box): - """ - Fig. 2.7 right-hand-side syntax: a composition-program box followed by eval. - - - `Uncurry((;), A, B, C)` stands for `((;) @ A) >> {}_{A,C}` - with type `P×P×A⊸C`. - - `Uncurry((||), A, U, B, V)` stands for `((||) @ A×U) >> {}_{A×U,B×V}` - with type `P×P×A×U⊸B×V`. - """ - def __init__(self, box, A, B): - dom, cod = box.dom @ A, B - # Keep uncurry as a typed layered diagram, analogous to closed.Curry. - arg = box.bubble(dom=dom, cod=cod) - monoidal.Bubble.__init__(self, arg, dom=dom, cod=cod, drawing_name="$\\Lambda^{-1}$") - Box.__init__(self, f"uncurry({box.name})", dom, cod) - - -def Id(x=Ty()): - """Identity diagram over widip.computer.Ty (defaults to Ty()).""" - return Diagram.id(x) diff --git a/widip/files.py b/widip/files.py index be33b80..e5cbff2 100644 --- a/widip/files.py +++ b/widip/files.py @@ -1,7 +1,6 @@ import pathlib -from discopy.closed import Ty, Diagram, Box, Id, Functor - +from .comput.computer import Box, Diagram from .loader import repl_read @@ -19,8 +18,6 @@ def files_ar(ar: Box) -> Diagram: def file_diagram(file_name) -> Diagram: path = pathlib.Path(file_name) fd = repl_read(path.open()) - # TODO TypeError: Expected closed.Diagram, got monoidal.Diagram instead - # fd = replace_id_f(path.stem)(fd) return fd def diagram_draw(path, fd): @@ -28,5 +25,3 @@ def diagram_draw(path, fd): textpad=(0.3, 0.1), fontsize=12, fontsize_types=8) - -files_f = Functor(lambda x: Ty(""), files_ar) diff --git a/widip/loader.py b/widip/loader.py index 40ed488..642c91a 100644 --- a/widip/loader.py +++ b/widip/loader.py @@ -1,155 +1,119 @@ -from itertools import batched -from nx_yaml import nx_compose_all, nx_serialize_all -from nx_hif.hif import * - -from discopy.closed import Eval, Curry - -from .computer import Box, Id, Ty - - -P = Ty("io") >> Ty("io") - - -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): - """ - 2.3.1 (Sec:retracts): encode data as programs via retraction-style embedding. - Fig. 2.3 (Sec:uev): reparametrization acts on program parameters, not raw inputs. - """ - v = hif_node(node, index)["value"] - if not tag and not v: - return Curry(Id(Ty() << Ty()), n=1) - X = Ty(tag) if tag else Ty() - A = Ty(v) # != Ty() - if not tag: - return Curry(Eval(A << Ty(), n=0)) - return Curry(Eval(Ty() << X @ A), n=2) - -def load_mapping(node, index, tag): - """2.2.3 (Sec:compos-prog) Build keyed computations using composed programs.""" - 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) - - 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_box = Box("(;)", key.cod @ value.cod, exps >> bases) - kv = key @ value >> kv_box - - if i==0: - ob = kv - else: - ob = ob @ kv - - i += 1 - nxt = 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)) - par_box = Box("(||)", ob.cod, bases << exps) - ob = ob >> par_box - if tag: - ob = (ob @ exps >> Eval(bases << exps)) - box = Box(tag, ob.cod, Ty(tag) << Ty(tag)) - ob = ob >> box - return ob - -def load_sequence(node, index, tag): - """2.2.3 (Sec:compos-prog) Fold a YAML sequence into repeated (;) composition.""" - 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 = ob >> Box(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 = ob @ doc - - nxt = tuple(hif_node_incidences(node, nxt_node, key="forward")) - return ob +"""Load YAML hypergraphs into loader diagrams and compile them to shell.""" + +from nx_yaml import nx_compose_all + +from .comput.loader import LoaderCommand, LoaderEmpty, LoaderLiteral +from .hif import ( + HyperGraph, + document_root_node, + mapping_entry_nodes, + node_kind, + node_tag, + node_value, + sequence_item_nodes, + stream_document_nodes, +) +from .metaprog import LOADER_TO_SHELL +from .state.loader import run_loader_program +from .wire.loader import LoaderMapping, LoaderSequence, loader_id, pipeline + + +def repl_read(stream): + """Parse a YAML stream and compile it to the shell backend.""" + incidences = nx_compose_all(stream) + return LOADER_TO_SHELL(incidences_to_program(incidences)) + + +def incidences_to_program(node: HyperGraph): + """Turn an ``nx_yaml`` hypergraph into a loader-language diagram.""" + return _incidences_to_program(node, 0) + + +def _incidences_to_program(node: HyperGraph, index): + kind = node_kind(node, index) + tag = node_tag(node, index) + + match kind: + case "stream": + diagram = load_stream(node, index) + case "document": + diagram = load_document(node, index) + case "scalar": + return load_scalar(node, index, tag) + case "sequence": + if tag: + return load_tagged_sequence(node, index, tag) + diagram = load_sequence(node, index) + case "mapping": + if tag: + return load_tagged_mapping(node, index, tag) + diagram = load_mapping(node, index) + case _: + raise ValueError(f"unsupported YAML node kind: {kind!r}") + + if tag: + diagram = diagram >> run_loader_program(LoaderCommand((tag,))) + return diagram + + +def load_scalar(node, index, tag): + """Scalars lower directly to runnable loader diagrams.""" + value = node_value(node, index) + if not value and not tag: + return loader_id() + + scalar = LoaderLiteral(value) if value else LoaderEmpty() + if tag: + scalar = scalar.partial_apply(LoaderCommand((tag,))) + return run_loader_program(scalar) + + +def load_mapping(node, index): + """Mappings denote parallel branches in the loader language.""" + branches = [] + for key_node, value_node in mapping_entry_nodes(node, index): + key = _incidences_to_program(node, key_node) + value = _incidences_to_program(node, value_node) + branches.append(pipeline((key, value))) + return LoaderMapping(branches) + + +def load_sequence(node, index): + """Sequences denote shell-style pipelines in the loader language.""" + stages = tuple(_incidences_to_program(node, child) for child in sequence_item_nodes(node, index)) + return LoaderSequence(stages) + + +def argv_item(diagram): + """Normalize one loader child diagram as one newline-delimited argv item.""" + return diagram >> run_loader_program(LoaderCommand(("xargs", "printf", "%s\n"))) + + +def load_tagged_mapping(node, index, tag): + """Tagged mappings treat each branch as an argv-supplying process.""" + branches = [] + for key_node, value_node in mapping_entry_nodes(node, index): + key = _incidences_to_program(node, key_node) + value = _incidences_to_program(node, value_node) + branches.append(argv_item(pipeline((key, value)))) + return LoaderMapping(branches) >> run_loader_program(LoaderCommand(("xargs", tag))) + + +def load_tagged_sequence(node, index, tag): + """Tagged sequences treat each item as an argv-supplying process.""" + branches = tuple(argv_item(_incidences_to_program(node, child)) for child in sequence_item_nodes(node, index)) + return LoaderMapping(branches) >> run_loader_program(LoaderCommand(("xargs", tag))) + + +def load_document(node, index): + """A document is its root node, if present.""" + root = document_root_node(node, index) + if root is None: + return loader_id() + return _incidences_to_program(node, root) + + +def load_stream(node, index): + """A stream is the pipeline of its documents.""" + documents = tuple(_incidences_to_program(node, child) for child in stream_document_nodes(node, index)) + return pipeline(documents) diff --git a/widip/metaprog.py b/widip/metaprog.py deleted file mode 100644 index 8776c19..0000000 --- a/widip/metaprog.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Chapter 6. Computing programs. -Metaprograms are programs that compute programs. -""" -from .computer import * - - -class Metaprogram(Box): - """ - a metaprogram, presented as a cartesian function G:I⊸P - """ - def __init__(self, name, P: ProgramTy): - Box.__init__(self, name, dom=Ty(), cod=P) - - -class ProgramComputation(Diagram): - """ - Section 3.1: a function is computable when it is programmable. - Fig 6.1: A computation f encoded such that f = {F}. - """ - def __init__(self, name, P: ProgramTy, X: Ty, A: Ty, B: Ty): - """ - Running a given program is a routine operation. - Every function is computable, in the sense that there is a program for it. - """ - self.universal_ev_diagram = ComputableFunction("{"+name+"}", X, A, B) - diagram = ( - Program(name, P, X) @ A, - Computer(P, A, B)) - # TODO review .inside - inside = sum(map(lambda d: d.inside, diagram), ()) - Diagram.__init__(self, inside, X @ A, B) - - def universal_ev(self): - return self.universal_ev_diagram - -class MetaprogramComputation(Diagram): - """ - Fig 6.1: A program F encoded such that F = {ℱ}. - """ - def __init__(self, name, P: ProgramTy, PP: ProgramTy, X: Ty, A: Ty, B: Ty): - self.partial_ev_diagram = ( - ProgramComputation(name, PP, Ty(), X, P) @ A - >> Computer(P, A, B) - ) - diagram = ( - Program(name, PP, Ty()) @ X @ A, - Computer(PP, X, P) @ A, - Computer(P, A, B) - ) - # TODO review .inside - inside = sum(map(lambda d: d.inside, diagram), ()) - Diagram.__init__(self, inside, X @ A, B) - - def partial_ev(self): - return self.partial_ev_diagram - -class Interpreter(Program): - """ - Sec 2.2: The program evaluators are computable functions representing typed interpreters. The type P is the set of program expressions. - Ex 2.5.2: C_P's program evaluators correspond to P's interpreters. - Sec 6.2.2: programs that implement universal evaluators are called interpreters. - """ - - -class Specializer(Metaprogram): - """ - Ex 2.5.2: C_P's partial evaluators correspond to P's specializers. - Sec 6.2.2: metaprograms that implement partial evaluators - """ - def partial_ev(self, X): - raise NotImplementedError() - -class Compiler(Metaprogram): - """Fig 6.4: partially evaluating a specializer on an interpreter gives a compiler.""" - def __init__(self, P: Specializer, X: Interpreter): - """""" - def partial_ev(self): - """""" - -class ProgramFunctor(Functor): - """ - Evaluates programs. - Preserves computer boxes and metaprograms. - """ - def __call__(self, other): - if isinstance(other, Program) and callable(getattr(other, "specialize", None)): - other = other.specialize() - return Functor.__call__(self, other) - - -class MetaprogramFunctor(Functor): - """ - Evaluates metaprograms. - Preserves computer boxes and programs. - """ - def __call__(self, other): - if isinstance(other, Metaprogram) and callable(getattr(other, "specialize", None)): - return other.specialize() - return Functor.__call__(self, other) diff --git a/widip/metaprog/__init__.py b/widip/metaprog/__init__.py new file mode 100644 index 0000000..3fd5b33 --- /dev/null +++ b/widip/metaprog/__init__.py @@ -0,0 +1,116 @@ +""" +Chapter 6. Computing programs. +Metaprograms are programs that compute programs. +""" + +from ..comput.computer import Box, Computer, ComputableFunction, Diagram, Program, ProgramTy, Ty + + +class Metaprogram(Box): + """ + a metaprogram, presented as a cartesian function G:I⊸P + """ + + def __init__(self, name, P: ProgramTy): + Box.__init__(self, name, dom=Ty(), cod=P) + + +class ProgramComputation(Diagram): + """ + Section 3.1: a function is computable when it is programmable. + Fig 6.1: A computation f encoded such that f = {F}. + """ + + def __init__(self, name, P: ProgramTy, X: Ty, A: Ty, B: Ty): + """ + Running a given program is a routine operation. + Every function is computable, in the sense that there is a program for it. + """ + self.universal_ev_diagram = ComputableFunction("{" + name + "}", X, A, B) + diagram = ( + Program(name, P, X) @ A, + Computer(P, A, B), + ) + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + + def universal_ev(self): + return self.universal_ev_diagram + + def specialize(self): + """Pure wiring rewrite from a program layer to its encoded computation.""" + return self.universal_ev() + + +class MetaprogramComputation(Diagram): + """ + Fig 6.1: A program F encoded such that F = {ℱ}. + """ + + def __init__(self, name, P: ProgramTy, PP: ProgramTy, X: Ty, A: Ty, B: Ty): + self.universal_ev_diagram = ComputableFunction("{{" + name + "}}", X, A, B) + self.partial_ev_diagram = ( + ProgramComputation("{" + name + "}", PP, Ty(), X, P) @ A + >> Computer(P, A, B) + ) + diagram = ( + Program(name, PP, Ty()) @ X @ A, + Computer(PP, X, P) @ A, + Computer(P, A, B), + ) + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + + def universal_ev(self): + return self.universal_ev_diagram + + def partial_ev(self): + return self.partial_ev_diagram + + def specialize(self): + """Pure wiring rewrite from a metaprogram layer to partial evaluation.""" + return self.partial_ev() + + +class ProgramFunctor: + """ + Pure wiring transformation that strips one program-evaluation layer. + """ + + def __call__(self, other): + if isinstance(other, ProgramComputation): + return other.specialize() + return other + + +class MetaprogramFunctor: + """ + Pure wiring transformation that strips one metaprogram-evaluation layer. + """ + + def __call__(self, other): + if isinstance(other, MetaprogramComputation): + return other.specialize() + return other + + +def __getattr__(name): + if name == "SHELL_SPECIALIZER": + from .widish import ShellSpecializer + + value = ShellSpecializer() + globals()[name] = value + return value + if name == "SHELL_RUNNER": + from .widish import ShellRunner + + value = ShellRunner() + globals()[name] = value + return value + if name == "LOADER_TO_SHELL": + from .loader import LoaderToShell + + value = LoaderToShell() + globals()[name] = value + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/widip/metaprog/loader.py b/widip/metaprog/loader.py new file mode 100644 index 0000000..8bd2a96 --- /dev/null +++ b/widip/metaprog/loader.py @@ -0,0 +1,50 @@ +"""Loader-specific program transformations.""" + +from ..comput import computer +from ..comput import loader as loader_lang +from ..comput import widish as shell_lang +from ..state import loader as loader_state +from ..wire import loader as loader_wire +from .widish import Parallel, Pipeline + + +class LoaderToShell(computer.Functor): + """Compile loader programs and execution boxes into shell diagrams.""" + + def __init__(self): + computer.Functor.__init__( + self, + self.ob_map, + self.ar_map, + dom=computer.Category(), + cod=computer.Category(), + ) + + @staticmethod + def ob_map(ob): + if ob == loader_lang.loader_program_ty: + return shell_lang.shell_program_ty + if ob == loader_wire.loader_stream_ty: + return shell_lang.io_ty + return ob + + def __call__(self, other): + if isinstance(other, loader_wire.LoaderSequence): + return Pipeline(tuple(self(stage) for stage in other.stages)) + if isinstance(other, loader_wire.LoaderMapping): + return Parallel(tuple(self(branch) for branch in other.branches)) + return computer.Functor.__call__(self, other) + + @staticmethod + def ar_map(ar): + if isinstance(ar, loader_lang.LoaderEmpty): + return shell_lang.Empty() + if isinstance(ar, loader_lang.LoaderLiteral): + return shell_lang.Literal(ar.text) + if isinstance(ar, loader_lang.LoaderCommand): + return shell_lang.Command(ar.argv) + if isinstance(ar, loader_state.LoaderStateUpdate): + return shell_lang.ShellStateUpdate() + if isinstance(ar, loader_state.LoaderOutput): + return shell_lang.ShellOutput() + return ar diff --git a/widip/metaprog/widish.py b/widip/metaprog/widish.py new file mode 100644 index 0000000..96dfddb --- /dev/null +++ b/widip/metaprog/widish.py @@ -0,0 +1,189 @@ +"""Shell-specific program transformations and interpreters.""" + +from discopy import monoidal, python + +from ..comput import computer +from ..comput import widish as shell_lang +from ..wire import widish as shell_wire + + +def _pipeline_diagram(stages): + """Compose shell stages from top to bottom, skipping identities.""" + result = shell_wire.shell_id() + identity = shell_wire.shell_id() + for stage in stages: + if stage == identity: + continue + result = stage if result == identity else result >> stage + return result + + +def _tensor_all(diagrams): + """Tensor shell stages left-to-right for bubble drawing.""" + diagrams = tuple(diagrams) + if not diagrams: + return computer.Id() + result = diagrams[0] + for diagram in diagrams[1:]: + result = result @ diagram + return result + + +def _shell_stage(program): + """Run a primitive shell program through the standard shell evaluator.""" + return program @ shell_lang.io_ty >> shell_lang.ShellOutput() + + +def _parallel_diagram(branches): + """Lower a parallel bubble to compact shell wiring.""" + branches = tuple(branches) + if not branches: + return shell_wire.shell_id() + if len(branches) == 1: + return branches[0] + return shell_wire.Copy(len(branches)) >> _tensor_all(branches) >> shell_wire.Merge(len(branches)) + + +class Pipeline(monoidal.Bubble, computer.Box): + """Bubble grouping shell stages in sequence.""" + + def __init__(self, stages): + self.stages = tuple(stages) + arg = _pipeline_diagram(self.stages) + monoidal.Bubble.__init__( + self, + arg, + dom=shell_lang.io_ty, + cod=shell_lang.io_ty, + draw_vertically=True, + drawing_name="seq", + ) + + def specialize(self): + return _pipeline_diagram(tuple(specialize_shell(stage) for stage in self.stages)) + + +class Parallel(monoidal.Bubble, computer.Box): + """Bubble grouping parallel shell branches.""" + + def __init__(self, branches): + self.branches = tuple(branches) + arg = _tensor_all(self.branches) if self.branches else shell_wire.shell_id() + monoidal.Bubble.__init__( + self, + arg, + dom=shell_lang.io_ty, + cod=shell_lang.io_ty, + drawing_name="map", + ) + + def specialize(self): + return _parallel_diagram(tuple(specialize_shell(branch) for branch in self.branches)) + + +Sequence = Pipeline +Mapping = Parallel + + +def pipeline(stages): + """Build a shell pipeline bubble.""" + stages = tuple(stages) + if not stages: + return shell_wire.shell_id() + return Pipeline(stages) + + +def parallel(branches): + """Build a shell parallel bubble.""" + branches = tuple(branches) + if not branches: + return shell_wire.shell_id() + return Parallel(branches) + + +class ShellSpecializer(computer.Functor): + """Lower shell bubbles to their executable wiring.""" + + def __init__(self): + computer.Functor.__init__(self, self.ob_map, self.ar_map, dom=computer.Category(), cod=computer.Category()) + + @staticmethod + def ob_map(ob): + return ob + + def __call__(self, other): + if isinstance(other, Pipeline): + return _pipeline_diagram(tuple(self(stage) for stage in other.stages)) + if isinstance(other, Parallel): + return _parallel_diagram(tuple(self(branch) for branch in other.branches)) + if isinstance(other, monoidal.Bubble): + return self(other.arg) + return computer.Functor.__call__(self, other) + + @staticmethod + def ar_map(ar): + return ar + + +def specialize_shell(diagram: computer.Diagram) -> computer.Diagram: + """Recursively lower shell bubbles to plain shell wiring.""" + return ShellSpecializer()(diagram) + + +class ShellRunner(monoidal.Functor): + """Interpret shell diagrams as Python callables over stateful text streams.""" + + def __init__(self): + monoidal.Functor.__init__( + self, + ob=self.ob_map, + ar=self.ar_map, + dom=computer.Category(), + cod=python.Category(), + ) + + @staticmethod + def ob_map(ob): + if ( + isinstance(ob, computer.Ty) + and len(ob) == 1 + and isinstance(ob.inside[0], computer.ProgramOb) + ): + return shell_lang.ShellProgram + return str + + def __call__(self, box): + if isinstance(box, Pipeline): + return self(box.specialize()) + if isinstance(box, Parallel): + return self(box.specialize()) + if isinstance(box, monoidal.Bubble): + return self(box.arg) + return monoidal.Functor.__call__(self, box) + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + + if isinstance(box, shell_lang.ShellProgram): + return python.Function(lambda: box, dom, cod) + if isinstance(box, shell_lang.ShellStateUpdate): + return python.Function(lambda program, _stdin: program, dom, cod) + if isinstance(box, shell_lang.ShellOutput): + return python.Function(lambda program, stdin: program.run(stdin), dom, cod) + if isinstance(box, computer.Copy): + return python.Function.copy(dom, n=2) + if isinstance(box, shell_wire.Merge): + return python.Function(lambda *parts: "".join(parts), dom, cod) + if isinstance(box, computer.Delete): + return python.Function.discard(dom) + if isinstance(box, computer.Swap): + return python.Function.swap(self(box.left), self(box.right)) + + raise TypeError(f"unsupported shell box: {box!r}") + + +def compile_shell_program(diagram: computer.Diagram) -> python.Function: + """Compile a shell diagram into an executable Python function.""" + from . import SHELL_RUNNER + + return SHELL_RUNNER(diagram) diff --git a/widip/state.py b/widip/state/__init__.py similarity index 66% rename from widip/state.py rename to widip/state/__init__.py index 233c84c..db467e5 100644 --- a/widip/state.py +++ b/widip/state/__init__.py @@ -1,6 +1,6 @@ """Chapter 7: Stateful computing.""" -from . import computer +from ..comput import computer class Process(computer.Diagram): @@ -8,10 +8,26 @@ class Process(computer.Diagram): Eq. 7.1: a process q : X x A -> X x B is paired from state update and output. """ - def __init__(self, name, X: computer.Ty, A: computer.Ty, B: computer.Ty): + def __init__( + self, + name, + X: computer.Ty, + A: computer.Ty, + B: computer.Ty, + state_update_diagram=None, + output_diagram=None, + ): self.name, self.X, self.A, self.B = name, X, A, B - self.state_update_diagram = computer.Box(f"sta({name})", X @ A, X) - self.output_diagram = computer.Box(f"out({name})", X @ A, B) + self.state_update_diagram = ( + state_update_diagram + if state_update_diagram is not None + else computer.Box(f"sta({name})", X @ A, X) + ) + self.output_diagram = ( + output_diagram + if output_diagram is not None + else computer.Box(f"out({name})", X @ A, B) + ) diagram = ( computer.Copy(X @ A), @@ -32,9 +48,29 @@ class Execution(Process): Sec. 7.3: program execution is a process P x A -> P x B. """ - def __init__(self, P: computer.ProgramTy, A: computer.Ty, B: computer.Ty): - self.universal_ev_diagram = computer.Computer(P, A, P @ B) - Process.__init__(self, "{}", P, A, B) + def __init__( + self, + P: computer.ProgramTy, + A: computer.Ty, + B: computer.Ty, + universal_ev_diagram=None, + state_update_diagram=None, + output_diagram=None, + ): + self.universal_ev_diagram = ( + universal_ev_diagram + if universal_ev_diagram is not None + else computer.Computer(P, A, P @ B) + ) + Process.__init__( + self, + "{}", + P, + A, + B, + state_update_diagram=state_update_diagram, + output_diagram=output_diagram, + ) def universal_ev(self): """ @@ -103,3 +139,11 @@ def fixed_state(g: computer.Diagram): X = g.dom[:1] A = g.dom[1:] return computer.Copy(X) @ A >> X @ g + + +from .loader import LoaderLanguage +from .widish import ShellLanguage + + +LOADER = LoaderLanguage() +SHELL = ShellLanguage() diff --git a/widip/state/loader.py b/widip/state/loader.py new file mode 100644 index 0000000..3570494 --- /dev/null +++ b/widip/state/loader.py @@ -0,0 +1,62 @@ +"""Loader-specific stateful execution.""" + +from ..comput import computer +from ..comput.loader import loader_program_ty +from ..wire.loader import loader_stream_ty +from . import Execution, ProgramClosedCategory, out + + +class LoaderStateUpdate(computer.Box): + """State projection box for loader execution.""" + + def __init__(self): + computer.Box.__init__(self, "sta(loader)", loader_program_ty @ loader_stream_ty, loader_program_ty) + + +class LoaderOutput(computer.Box): + """Observable-output box for loader execution.""" + + def __init__(self): + computer.Box.__init__(self, "out(loader)", loader_program_ty @ loader_stream_ty, loader_stream_ty) + + +class LoaderExecution(Execution): + """Stateful execution process for loader programs.""" + + def __init__(self): + Execution.__init__( + self, + loader_program_ty, + loader_stream_ty, + loader_stream_ty, + universal_ev_diagram=computer.Computer( + loader_program_ty, + loader_stream_ty, + loader_program_ty @ loader_stream_ty, + ), + state_update_diagram=LoaderStateUpdate(), + output_diagram=LoaderOutput(), + ) + + +class LoaderLanguage(ProgramClosedCategory): + """Program-closed category for the YAML loader language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, loader_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + return LoaderExecution() + + +def loader_execution() -> computer.Diagram: + """Projection to observable output of loader execution.""" + from . import LOADER + + return out(LOADER.execution(loader_stream_ty, loader_stream_ty)) + + +def run_loader_program(program: computer.Diagram) -> computer.Diagram: + """Execute a closed loader program on one loader stream wire.""" + return program @ loader_stream_ty >> loader_execution() diff --git a/widip/state/widish.py b/widip/state/widish.py new file mode 100644 index 0000000..bf7600a --- /dev/null +++ b/widip/state/widish.py @@ -0,0 +1,31 @@ +"""Shell-specific stateful execution.""" + +from ..comput import computer +from ..comput.widish import ShellOutput, ShellStateUpdate, io_ty, shell_program_ty +from . import Execution, ProgramClosedCategory + + +class ShellExecution(Execution): + """Stateful shell evaluator P x io -> P x io.""" + + def __init__(self): + Execution.__init__( + self, + shell_program_ty, + io_ty, + io_ty, + universal_ev_diagram=computer.Computer(shell_program_ty, io_ty, shell_program_ty @ io_ty), + state_update_diagram=ShellStateUpdate(), + output_diagram=ShellOutput(), + ) + + +class ShellLanguage(ProgramClosedCategory): + """Program-closed category with the shell as distinguished language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, shell_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + return ShellExecution() diff --git a/widip/to_py.py b/widip/to_py.py deleted file mode 100644 index 896e096..0000000 --- a/widip/to_py.py +++ /dev/null @@ -1,32 +0,0 @@ -from functools import partial -from itertools import repeat -import operator - -from discopy import python, symmetric -from discopy.utils import tuplify as discopy_tuplify -from .lang import * -from .computer import * - - -tuplify = partial(operator.call, discopy_tuplify) -partial_const = partial(next, repeat(partial)) -empty_tuple = partial(next, repeat(())) -tuple_type = partial(operator.mul, (partial, )) -copy_builder = partial(operator.call, python.Function.copy) -delete_builder = partial(operator.call, python.Function.discard) - -def to_py_ar(ar): - if not ar.dom: - return empty_tuple - dom = tuple_type(len(ar.dom)) - if isinstance(ar, Copy): - return copy_builder(dom) - if isinstance(ar, Delete): - return delete_builder(dom) - assert not ar - -to_py = symmetric.Functor( - partial_const, - to_py_ar, - dom=computer.Category(), - cod=python.Category()) diff --git a/widip/watch.py b/widip/watch.py index c196dfd..a301f18 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -4,12 +4,11 @@ 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 +from .metaprog.widish import compile_shell_program # TODO watch functor ?? @@ -53,13 +52,7 @@ def shell_main(file_name, draw=True): if draw: 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)() + result_ev = compile_shell_program(source_d)("") print(result_ev) except KeyboardInterrupt: print() @@ -76,8 +69,7 @@ def widish_main(file_name, draw): path = Path(file_name) if draw: diagram_draw(path, fd) - constants = tuple(x.name for x in fd.dom) - runner = SHELL_RUNNER(fd)(*constants) + runner = compile_shell_program(fd) run_res = runner("") if sys.stdin.isatty() else runner(sys.stdin.read()) diff --git a/widip/widish.py b/widip/widish.py deleted file mode 100644 index 3d2ec50..0000000 --- a/widip/widish.py +++ /dev/null @@ -1,78 +0,0 @@ -from functools import partial -from subprocess import CalledProcessError, run -import subprocess - -from discopy.utils import tuplify, untuplify -from discopy import closed, python - -from . import computer - - -io_ty = closed.Ty("io") - -def split_args(ar, args): - n = len(ar.dom) - return args[:n], args[n:] - -def run_native_subprocess_constant(ar, *args): - b, params = split_args(ar, args) - if not params: - return "" if ar.dom == closed.Ty() else ar.dom.name - return untuplify(params) - -def run_native_subprocess_map(ar, *args): - b, params = split_args(ar, args) - mapped = [] - for kv in b: - res = kv(*tuplify(params)) - mapped.append(untuplify(res)) - return untuplify(tuple(mapped)) - -def run_native_subprocess_seq(ar, *args): - b, params = split_args(ar, args) - b0 = b[0](*tuplify(params)) - b1 = b[1](*tuplify(b0)) - return untuplify(b1) - -def run_native_subprocess_default(ar, *args): - """ - 7.4 Universality of program execution: A function {}:P×A→B is universal when any function g:X×A→B has an implementation G:X⊸P evaluated by {}. - We choose `subprocess.run` where X is the command name. - """ - b, params = split_args(ar, args) - -def ar_mapping(ar): - """ - 2.5.3 (Sec:surj) Realize the run-surjection mapping into executable arrows. - 7.4 Universality of program execution: A function : P × A B is universal when any function g:X×A→B has an implementation G:X⊸P evaluated by {}. - """ - # implementar gamma - if isinstance(ar, closed.Curry) or ar.name == "⌜−⌝": - return partial(partial, run_native_subprocess_constant, ar) - if ar.name == "(||)": - return partial(partial, run_native_subprocess_map, ar) - if ar.name == "(;)": - return partial(partial, run_native_subprocess_seq, ar) - return partial(partial, run_native_subprocess_default, ar) - - - - -class ShellRunner(computer.Functor): - """ - Transforms a computer diagram into a runnable Python function of a subprocess pipeline. - Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P) for each pair of computer.types A, B. - """ - def __init__(self): - computer.Functor.__init__( - self, - lambda _: python.Ty, - ar_mapping, - cod=computer.Category(python.Ty, python.Function)) - - def __call__(self, other): - """subprocess.run(cmd=X, *A): B is the natural family of surjections.""" - constants = tuple(x.name for x in other.dom) - return python.Function( - partial(subprocess.run, *constants), - dom=python.Ty, cod=python.Ty) diff --git a/widip/wire/__init__.py b/widip/wire/__init__.py new file mode 100644 index 0000000..f06c462 --- /dev/null +++ b/widip/wire/__init__.py @@ -0,0 +1 @@ +"""Chapter 1 wire calculus used by the later computer and language layers.""" diff --git a/widip/wire/functions.py b/widip/wire/functions.py new file mode 100644 index 0000000..95c1b05 --- /dev/null +++ b/widip/wire/functions.py @@ -0,0 +1,22 @@ +"""Chapter 1 categorical structure for wire diagrams.""" + +from discopy import monoidal + +from .types import Diagram, Ty + + +class Category(monoidal.Category): + """Strict symmetric monoidal category of wire diagrams.""" + + ob, ar = Ty, Diagram + + +class Functor(monoidal.Functor): + """Functor between wire-diagram categories.""" + + dom = Category() + cod = Category() + + +class Box(monoidal.Box, Diagram): + """Atomic box attached to input and output wires.""" diff --git a/widip/wire/loader.py b/widip/wire/loader.py new file mode 100644 index 0000000..fb7fd01 --- /dev/null +++ b/widip/wire/loader.py @@ -0,0 +1,75 @@ +"""Loader-specific wire combinators and structural boxes.""" + +from discopy import monoidal + +from .functions import Box +from .services import Copy as CopyService, Delete +from .types import Id, Ty + + +loader_stream_ty = Ty("yaml_stream") + + +class LoaderSequence(monoidal.Bubble, Box): + """Bubble grouping loader stages in sequence.""" + + def __init__(self, stages): + self.stages = tuple(stages) + monoidal.Bubble.__init__( + self, + pipeline(self.stages), + dom=loader_stream_ty, + cod=loader_stream_ty, + draw_vertically=True, + drawing_name="seq", + ) + + +class LoaderMapping(monoidal.Bubble, Box): + """Bubble grouping loader branches in parallel.""" + + def __init__(self, branches): + self.branches = tuple(branches) + arg = tensor_all(self.branches) if self.branches else loader_id() + monoidal.Bubble.__init__( + self, + arg, + dom=loader_stream_ty, + cod=loader_stream_ty, + drawing_name="map", + ) + + +def loader_id(): + """Identity diagram over the loader stream wire.""" + return Id(loader_stream_ty) + + +def pipeline(diagrams): + """Compose loader stages from top to bottom, skipping identities.""" + result = loader_id() + identity = loader_id() + for diagram in diagrams: + if diagram == identity: + continue + result = diagram if result == identity else result >> diagram + return result + + +def tensor_all(diagrams): + """Tensor loader stages left-to-right.""" + diagrams = tuple(diagrams) + if not diagrams: + return Id() + result = diagrams[0] + for diagram in diagrams[1:]: + result = result @ diagram + return result + + +def stream_wires(n: int): + """Return the monoidal product of ``n`` loader stream wires.""" + wires = Ty() + for _ in range(n): + wires = wires @ loader_stream_ty + return wires diff --git a/widip/wire/services.py b/widip/wire/services.py new file mode 100644 index 0000000..3bcdc0f --- /dev/null +++ b/widip/wire/services.py @@ -0,0 +1,33 @@ +"""Chapter 1 wire services: copying, deleting, and swapping.""" + +from .functions import Box +from .types import Ty + + +class Copy(Box): + """Copying data service: ``A -> A @ A``.""" + + def __init__(self, A): + Box.__init__(self, "∆", A, A @ A, draw_as_spider=True, drawing_name="") + + +class Delete(Box): + """Deleting data service: ``A -> I``.""" + + def __init__(self, A): + Box.__init__(self, "⊸", A, Ty(), draw_as_spider=True, drawing_name="") + + +class Swap(Box): + """Symmetry isomorphism swapping adjacent wires.""" + + def __init__(self, left, right): + self.left, self.right = left, right + Box.__init__( + self, + "Swap", + left @ right, + right @ left, + draw_as_wires=True, + drawing_name="", + ) diff --git a/widip/wire/types.py b/widip/wire/types.py new file mode 100644 index 0000000..d5f822e --- /dev/null +++ b/widip/wire/types.py @@ -0,0 +1,21 @@ +"""Chapter 1 wire types and diagrams.""" + +from discopy import monoidal +from discopy.utils import factory + + +@factory +class Ty(monoidal.Ty): + """Wire types presented as strings in diagrams.""" + + +@factory +class Diagram(monoidal.Diagram): + """Typed wire diagrams.""" + + ty_factory = Ty + + +def Id(x=Ty()): + """Identity diagram over ``Ty`` (defaults to the monoidal unit).""" + return Diagram.id(x) diff --git a/widip/wire/widish.py b/widip/wire/widish.py new file mode 100644 index 0000000..1bfe320 --- /dev/null +++ b/widip/wire/widish.py @@ -0,0 +1,45 @@ +"""Shell-specific wire combinators and structural boxes.""" + +from .functions import Box +from .services import Copy as CopyService, Delete +from .types import Id, Ty + + +io_ty = Ty("io") + + +def io_wires(n: int): + """Return the monoidal product of ``n`` shell-stream wires.""" + wires = Ty() + for _ in range(n): + wires = wires @ io_ty + return wires + + +def shell_id(): + """Identity shell process on one stream.""" + return Id(io_ty) + + +def Copy(n: int): + """N-ary stream fan-out built from the cartesian copy service.""" + if n < 0: + raise ValueError("copy arity must be non-negative") + if n == 0: + return Delete(io_ty) + if n == 1: + return shell_id() + result = CopyService(io_ty) + for copies in range(2, n): + result = result >> io_wires(copies - 1) @ CopyService(io_ty) + return result + + +class Merge(Box): + """N-ary stream fan-in for the shell case study.""" + + def __init__(self, n: int): + if n <= 0: + raise ValueError("merge arity must be positive") + self.n = n + Box.__init__(self, f"merge[{n}]", io_wires(n), io_ty) From 0695764f26ee61a5c59ae154710398147f5505c8 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Wed, 18 Mar 2026 20:21:13 +0000 Subject: [PATCH 05/17] megawip --- debug.py | 64 + debug.svg | 2233 ++ discorun.tex | 214 + examples/hello-world-map.jpg | Bin 0 -> 78090 bytes examples/shell.jpg | Bin 60977 -> 65235 bytes examples/shell.svg | 1021 + tests/shell.yaml | 4 + tests/svg/test_fig_6_3_eq_0_comp.svg | 262 + tests/svg/test_fig_6_3_eq_0_mprog.svg | 266 + tests/svg/test_fig_6_3_eq_0_prog.svg | 262 + tests/svg/test_fig_6_3_eq_1_comp.svg | 325 + tests/svg/test_fig_6_3_eq_1_mprog.svg | 292 + tests/svg/test_fig_6_3_eq_1_prog.svg | 325 + tests/svg/test_sec_6_2_2_comp.svg | 246 + tests/svg/test_sec_6_2_2_mprog.svg | 283 + tests/svg/test_sec_6_2_2_prog.svg | 240 + ...lizer_has_unit_metaprogram_domain_comp.svg | 246 + ...izer_has_unit_metaprogram_domain_mprog.svg | 283 + ...lizer_has_unit_metaprogram_domain_prog.svg | 240 + ...aprograms_with_partial_evaluators_comp.svg | 246 + ...programs_with_partial_evaluators_mprog.svg | 283 + ...aprograms_with_partial_evaluators_prog.svg | 240 + tests/test_compiler.py | 6 +- tests/test_hif.py | 122 + tests/test_interpreter.py | 18 + tests/test_lang.py | 223 +- tests/test_loader.py | 150 +- tests/test_metaprog.py | 60 +- tests/test_runner.py | 74 +- tests/test_state.py | 126 +- tests/test_wire.py | 36 + tests/widish/0.in | 0 tests/widish/0.mprog.svg | 1114 + tests/widish/0.out | 4 + tests/widish/0.prog.svg | 1776 ++ tests/widish/0.yaml | 4 + tests/widish/01.in | 1 + tests/widish/01.mprog.svg | 258 + tests/widish/01.out | 1 + tests/widish/01.prog.svg | 95 + tests/widish/01.yaml | 0 tests/widish/02.in | 0 tests/widish/02.mprog.svg | 330 + tests/widish/02.out | 1 + tests/widish/02.prog.svg | 398 + tests/widish/02.yaml | 1 + tests/widish/03.in | 1 + tests/widish/03.mprog.svg | 303 + tests/widish/03.out | 1 + tests/widish/03.prog.svg | 95 + tests/widish/03.yaml | 1 + tests/widish/04.in | 0 tests/widish/04.mprog.svg | 474 + tests/widish/04.out | 1 + tests/widish/04.prog.svg | 398 + tests/widish/04.yaml | 1 + tests/widish/05.in | 0 tests/widish/05.mprog.svg | 438 + tests/widish/05.out | 1 + tests/widish/05.prog.svg | 398 + tests/widish/05.yaml | 1 + tests/widish/06.in | 1 + tests/widish/06.mprog.svg | 334 + tests/widish/06.out | 1 + tests/widish/06.prog.svg | 390 + tests/widish/06.yaml | 1 + tests/widish/07.in | 0 tests/widish/07.mprog.svg | 375 + tests/widish/07.out | 1 + tests/widish/07.prog.svg | 358 + tests/widish/07.yaml | 1 + tests/widish/08.in | 0 tests/widish/08.mprog.svg | 433 + tests/widish/08.out | 1 + tests/widish/08.prog.svg | 486 + tests/widish/08.yaml | 1 + tests/widish/09.in | 0 tests/widish/09.mprog.svg | 392 + tests/widish/09.out | 1 + tests/widish/09.prog.svg | 418 + tests/widish/09.yaml | 1 + tests/widish/10.in | 0 tests/widish/10.mprog.svg | 532 + tests/widish/10.out | 1 + tests/widish/10.prog.svg | 478 + tests/widish/10.yaml | 2 + tests/widish/11.in | 0 tests/widish/11.mprog.svg | 523 + tests/widish/11.out | 1 + tests/widish/11.prog.svg | 488 + tests/widish/11.yaml | 2 + tests/widish/14.in | 4 + tests/widish/14.mprog.svg | 585 + tests/widish/14.out | 1 + tests/widish/14.prog.svg | 540 + tests/widish/14.yaml | 2 + tests/widish/15.in | 0 tests/widish/15.mprog.svg | 616 + tests/widish/15.out | 1 + tests/widish/15.prog.svg | 1268 ++ tests/widish/15.yaml | 2 + tests/widish/16.in | 0 tests/widish/16.mprog.svg | 689 + tests/widish/16.out | 1 + tests/widish/16.prog.svg | 1322 ++ tests/widish/16.yaml | 2 + tests/widish/17.in | 1 + tests/widish/17.mprog.svg | 7399 +++++++ tests/widish/17.out | 1 + tests/widish/17.prog.svg | 17813 ++++++++++++++++ tests/widish/17.yaml | 92 + widip/comput/__init__.py | 62 +- widip/comput/loader.py | 31 +- widip/comput/python.py | 3 + widip/comput/widish.py | 41 +- widip/files.py | 2 +- widip/hif.py | 78 - widip/loader.py | 119 - widip/metaprog/__init__.py | 66 +- widip/metaprog/hif.py | 73 + widip/metaprog/loader.py | 39 +- widip/metaprog/widish.py | 167 +- widip/state/__init__.py | 163 +- widip/state/core.py | 173 + widip/state/hif.py | 47 + widip/state/loader.py | 48 +- widip/state/widish.py | 133 +- widip/watch.py | 7 +- widip/wire/hif.py | 11 + widip/wire/loader.py | 24 +- widip/wire/widish.py | 11 - 131 files changed, 50555 insertions(+), 790 deletions(-) create mode 100644 debug.py create mode 100644 debug.svg create mode 100644 discorun.tex create mode 100644 examples/hello-world-map.jpg create mode 100644 examples/shell.svg create mode 100644 tests/shell.yaml create mode 100644 tests/svg/test_fig_6_3_eq_0_comp.svg create mode 100644 tests/svg/test_fig_6_3_eq_0_mprog.svg create mode 100644 tests/svg/test_fig_6_3_eq_0_prog.svg create mode 100644 tests/svg/test_fig_6_3_eq_1_comp.svg create mode 100644 tests/svg/test_fig_6_3_eq_1_mprog.svg create mode 100644 tests/svg/test_fig_6_3_eq_1_prog.svg create mode 100644 tests/svg/test_sec_6_2_2_comp.svg create mode 100644 tests/svg/test_sec_6_2_2_mprog.svg create mode 100644 tests/svg/test_sec_6_2_2_prog.svg create mode 100644 tests/svg/test_specializer_has_unit_metaprogram_domain_comp.svg create mode 100644 tests/svg/test_specializer_has_unit_metaprogram_domain_mprog.svg create mode 100644 tests/svg/test_specializer_has_unit_metaprogram_domain_prog.svg create mode 100644 tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg create mode 100644 tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg create mode 100644 tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg create mode 100644 tests/test_hif.py create mode 100644 tests/test_interpreter.py create mode 100644 tests/test_wire.py create mode 100644 tests/widish/0.in create mode 100644 tests/widish/0.mprog.svg create mode 100644 tests/widish/0.out create mode 100644 tests/widish/0.prog.svg create mode 100644 tests/widish/0.yaml create mode 100644 tests/widish/01.in create mode 100644 tests/widish/01.mprog.svg create mode 100644 tests/widish/01.out create mode 100644 tests/widish/01.prog.svg create mode 100644 tests/widish/01.yaml create mode 100644 tests/widish/02.in create mode 100644 tests/widish/02.mprog.svg create mode 100644 tests/widish/02.out create mode 100644 tests/widish/02.prog.svg create mode 100644 tests/widish/02.yaml create mode 100644 tests/widish/03.in create mode 100644 tests/widish/03.mprog.svg create mode 100644 tests/widish/03.out create mode 100644 tests/widish/03.prog.svg create mode 100644 tests/widish/03.yaml create mode 100644 tests/widish/04.in create mode 100644 tests/widish/04.mprog.svg create mode 100644 tests/widish/04.out create mode 100644 tests/widish/04.prog.svg create mode 100644 tests/widish/04.yaml create mode 100644 tests/widish/05.in create mode 100644 tests/widish/05.mprog.svg create mode 100644 tests/widish/05.out create mode 100644 tests/widish/05.prog.svg create mode 100644 tests/widish/05.yaml create mode 100644 tests/widish/06.in create mode 100644 tests/widish/06.mprog.svg create mode 100644 tests/widish/06.out create mode 100644 tests/widish/06.prog.svg create mode 100644 tests/widish/06.yaml create mode 100644 tests/widish/07.in create mode 100644 tests/widish/07.mprog.svg create mode 100644 tests/widish/07.out create mode 100644 tests/widish/07.prog.svg create mode 100644 tests/widish/07.yaml create mode 100644 tests/widish/08.in create mode 100644 tests/widish/08.mprog.svg create mode 100644 tests/widish/08.out create mode 100644 tests/widish/08.prog.svg create mode 100644 tests/widish/08.yaml create mode 100644 tests/widish/09.in create mode 100644 tests/widish/09.mprog.svg create mode 100644 tests/widish/09.out create mode 100644 tests/widish/09.prog.svg create mode 100644 tests/widish/09.yaml create mode 100644 tests/widish/10.in create mode 100644 tests/widish/10.mprog.svg create mode 100644 tests/widish/10.out create mode 100644 tests/widish/10.prog.svg create mode 100644 tests/widish/10.yaml create mode 100644 tests/widish/11.in create mode 100644 tests/widish/11.mprog.svg create mode 100644 tests/widish/11.out create mode 100644 tests/widish/11.prog.svg create mode 100644 tests/widish/11.yaml create mode 100644 tests/widish/14.in create mode 100644 tests/widish/14.mprog.svg create mode 100644 tests/widish/14.out create mode 100644 tests/widish/14.prog.svg create mode 100644 tests/widish/14.yaml create mode 100644 tests/widish/15.in create mode 100644 tests/widish/15.mprog.svg create mode 100644 tests/widish/15.out create mode 100644 tests/widish/15.prog.svg create mode 100644 tests/widish/15.yaml create mode 100644 tests/widish/16.in create mode 100644 tests/widish/16.mprog.svg create mode 100644 tests/widish/16.out create mode 100644 tests/widish/16.prog.svg create mode 100644 tests/widish/16.yaml create mode 100644 tests/widish/17.in create mode 100644 tests/widish/17.mprog.svg create mode 100644 tests/widish/17.out create mode 100644 tests/widish/17.prog.svg create mode 100644 tests/widish/17.yaml delete mode 100644 widip/hif.py delete mode 100644 widip/loader.py create mode 100644 widip/metaprog/hif.py create mode 100644 widip/state/core.py create mode 100644 widip/state/hif.py create mode 100644 widip/wire/hif.py diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..d855e96 --- /dev/null +++ b/debug.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from widip.computer import Box, ComputableFunction, ProgramTy, Ty +from widip.metaprog import ( + MetaprogramComputation, + MetaprogramFunctor, + ProgramComputation, + ProgramFunctor, +) +from widip.state import Process, ProgramClosedCategory, fixed_state, simulate + + +def large_diagram(): + X, A, B, C, D = Ty("X"), Ty("A"), Ty("B"), Ty("C"), Ty("D") + H_ty, L_ty = ProgramTy("H"), ProgramTy("L") + + high_level = ProgramClosedCategory(H_ty) + low_level = ProgramClosedCategory(L_ty) + + # Stateful execution in the same language composed for several rounds. + stateful_chain = ( + high_level.execution(A, B) + >> high_level.execution(B, C) + >> high_level.execution(C, D) + ) + + # One execution step followed by an interpreted high-level computation. + interpreter_chain = ( + high_level.execution(A, B) + >> ProgramFunctor()(ProgramComputation("H", L_ty, H_ty, B, C)) + ) + + # One execution step followed by a metaprogram rewrite / partial evaluation. + compiler_chain = ( + high_level.execution(A, B) + >> MetaprogramFunctor()(MetaprogramComputation("H", L_ty, L_ty, H_ty, B, C)) + ) + + # A plain computation lifted to a process, then simulated in the high-level + # program state space, and executed once more. + process_chain = ( + fixed_state(ComputableFunction("worker", X, A, B)) + >> simulate(Process("machine", X, B, C), Box("embed", X, H_ty)) + >> high_level.execution(C, D) + ) + + # A separate low-level execution pipeline for contrast with the H-language. + low_level_chain = low_level.execution(A, B) >> low_level.execution(B, C) + + # Tensor the pipelines together so the rendered figure shows several linked + # Chapter 6/7 stories in one large diagram. + return ( + stateful_chain + @ interpreter_chain + @ compiler_chain + @ process_chain + @ low_level_chain + ) + + +if __name__ == "__main__": + output_path = Path("debug.svg") + large_diagram().draw(path=str(output_path)) + print(output_path) diff --git a/debug.svg b/debug.svg new file mode 100644 index 0000000..5053df7 --- /dev/null +++ b/debug.svg @@ -0,0 +1,2233 @@ + + + + + + + + 2026-03-15T18:26:09.422355 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/discorun.tex b/discorun.tex new file mode 100644 index 0000000..86af2d0 --- /dev/null +++ b/discorun.tex @@ -0,0 +1,214 @@ +% This is samplepaper.tex, a sample chapter demonstrating the +% LLNCS macro package for Springer Computer Science proceedings; +% Version 2.21 of 2022/01/12 +% +\documentclass[runningheads]{llncs} +% +\usepackage{amsmath} +% +\usepackage[T1]{fontenc} +% T1 fonts will be used to generate the final print and online PDFs, +% so please use T1 fonts in your manuscript whenever possible. +% Other font encondings may result in incorrect characters. +% +\usepackage{graphicx} +\usepackage{caption} +\usepackage{subcaption} +\usepackage{minted} +% Used for displaying a sample figure. If possible, figure files should +% be included in EPS format. +% +% If you use the hyperref package, please uncomment the following two lines +% to display URLs in blue roman font according to Springer's eBook style: +%\usepackage{color} +%\renewcommand\UrlFont{\color{blue}\rmfamily} +%\urlstyle{rm} +% +\usepackage[inkscapelatex=false]{svg} +% +% \usepackage[backend=biber,style=splncs04]{biblatex} +% \bibliographystyle{splncs04} +\usepackage[backend=biber]{biblatex} +\bibliography{references} +% +% +\begin{document} +% +\title{DisCoRun: An implementation of the Monoidal Computer and Run language in DisCoPy} +% +\titlerunning{DisCoRun} +% If the paper title is too long for the running head, you can set +% an abbreviated paper title here +% +\author{Mart\'in Coll\inst{1}\orcidID{0009-0009-0146-7132}} +% +\authorrunning{M. Coll.} +% First names are abbreviated in the running head. +% If there are more than two authors, 'et al.' is used. +% +\institute{Department of Computer Science, University of Buenos Aires, \email{mcoll@dc.uba.ar}, CABA, C1428EGA, Argentina} +% +\maketitle % typeset the header of the contribution +% +\begin{abstract} +% The abstract should briefly summarize the contents of the paper in +% 150--250 words. + +We present DisCoRun, an implementation of the Monoidal Computer and Run language in DisCoPy~\cite{toumi2023discopyhierarchygraphicallanguages, de_Felice_2021}, released as the \texttt{discopy-run} package. Our design follows \emph{Programs as Diagrams}~\cite{pavlovic2023programsdiagramscategoricalcomputability}: programs are typed string diagrams equipped with cartesian data services (copy and delete), a distinguished program type with decidable equality, and a universal evaluator family. +In our case study, we instantiate this evaluator for the POSIX Shell Command Language~\cite{posix-2024} using \texttt{subprocess.run}. In this interpretation, sequential composition (\texttt{>>}) realizes shell-style pipelines and monoidal tensor (\texttt{@}) models independent process branches. \texttt{Copy}, \texttt{Merge}, and \texttt{Trace} arise naturally from the monoidal computer representation, extending the Shell Command Language with primitive fan-out, fan-in, and feedback operators. The resulting system highlights the practical applications of categorical computability: developers can construct programs diagrammatically, analyze them compositionally, and translate them to a chosen target language. + +% We present an implementation of the Run language in DisCoPy, along side a practical universal evaluator for Operating System processes. +% Following the theory of Programs as Diagrams, we write and publish a package 'discopy-computer', with the Computer, Program and Execution categories, Run morphism, and Compiler functor to move from an abstract program definition to a callable object in the category of Python functions provided by the framework. + +% treating programs as topological string diagrams captures both composition and correctness. + +% The Compiler functor maps morphisms to operating-system subprocesses through a functor into \texttt{discopy.python.Function}, where \texttt{subprocess.run} acts as a universal evaluator. In this setting, vertical composition (\texttt{>>}) models Unix-style pipelines, while the monoidal tensor (\texttt{@}) models parallel subprocess execution. +% Symmetry and trace operators encode feedback and cyclic pipelines, and explicit fan-out/fan-in combinators realize practical IO multiplexing via \texttt{tee} and \texttt{cat}. + +% The result is a compact bridge between the Monoidal Computer theory and the execution of Python programs using DisCoPy. + +% By leveraging DisCoPy's diagrammatic tooling, the approach combines formal clarity with + +% : developers can design, reason about programs graphically their behavior, and execute them with standard OS primitives. practical subprocess orchestration for high-concurrency pipeline design. + + + +\keywords{Categorical computability \and String diagram.} +\end{abstract} +% +% +% + +\section{Motivation} + +\paragraph{Graphical calculus} +String diagrams provide a rigorous graphical calculus for monoidal categories~\cite{Selinger_2010}. Sequential composition is represented by vertical wiring, while tensor product is represented by horizontal juxtaposition. Additional structural features, such as symmetries and traces, are encoded topologically. Diagram deformations that preserve connectivity correspond to sound equational reasoning. + +\paragraph{Computing with string diagrams} +This perspective motivates treating programs as diagrams~\cite{pavlovic2023programsdiagramscategoricalcomputability}. The universal evaluator is the central primitive from which sequential and parallel composition, and partial evaluation. +DisCoRun operationalizes this approach on top of DisCoPy, the Python toolkit for monoidal categories. In this setting, diagrams are typed program objects that support composition, rewriting, and functorial interpretation into executable backends (e.g., Python functions). We instantiate this machinery with the monoidal computer interface of \emph{Programs as Diagrams}: \texttt{RUN} is the single primitive induced by the running surjection, while richer behaviors are derived by composing diagrams. + + +% TODO "intentions" column on the left +% with eq and diagram +% \begin{figure} +% \centering +% \begin{subfigure}[b]{0.4\textwidth} +% \centering +% \begin{minted}{python} +% Copy(3) >> ( +% Command(["cat"]), +% Command(["grep", "-c", "x"]), +% Command(["wc", "-l"]), +% ) >> Merge(3) +% \end{minted} +% \caption{The program equation.} +% \end{subfigure} +% \hfill +% \begin{subfigure}[b]{0.55\textwidth} +% \centering +% \includesvg[width=\textwidth]{merge-diagram.svg} +% \caption{The program diagram.} +% \end{subfigure} +% \caption{Diagrams imbue visual intuition with formal guarantees.} +% \label{fig1} +% \end{figure} +\begin{figure}[htbp] + \centering + % LEFT COLUMN: Stacked vertically + \begin{minipage}[c]{0.48\textwidth} + \begin{subfigure}[b]{\textwidth} + \centering + \begin{minted}{python} +Copy(3) +>> ( + Command(["cat"]), + Command(["grep", "-c", "x"]), + Command(["wc", "-l"]), +) +>> Merge(3) + \end{minted} + \caption{Program equation.} + \end{subfigure} + + \vspace{0.5cm} % Vertical gap between the stacked items + + \begin{subfigure}{0.5\textwidth} + \centering + \includesvg[width=\textwidth]{parallel-diagram.abstract.svg} + \caption{Abstract program diagram.} + \label{fig:bottom_left} + \end{subfigure} + \end{minipage} + \hfill + % RIGHT COLUMN: Single tall item + \begin{minipage}[c]{0.45\textwidth} + \begin{subfigure}{\textwidth} + \centering + % Adjust height to match the combined height of the left column + \includesvg[width=\textwidth]{parallel-diagram.svg} + \caption{Runnable diagram} + \label{fig:right_side} + \end{subfigure} + \end{minipage} + + \caption{Diagrams imbue visual intuition with formal guarantees.} + \label{fig:example1} +\end{figure} + +\section{POSIX Shell Command Language Case Study} + +\subsection{Shell Evaluator} +This case study instantiates the evaluator on \texttt{IO} wires using \texttt{subprocess.run}. Each command box denotes a POSIX command invocation interpreted as an \texttt{IO} transformer. + +\paragraph{Operators} +Vertical composition (\texttt{>>}) models shell pipeline composition: the stdout stream of one command becomes the stdin stream of the next command. +Monoidal tensor (\texttt{@}) denotes independent command branches at the diagram level, with the \texttt{fork} command as a close translation. The language also exposes three explicit structural operators: \texttt{Copy} for fan-out, \texttt{Merge} for fan-in, and \texttt{Trace} for feedback. These are diagrammatic extensions rather than native POSIX Shell Command Language constructs. + +\begin{table} +\caption{Mapping of Categorical Operations to POSIX Shell Command Language Concepts}\label{tab1} +\centering +\begin{tabular}{|l|l|} +\hline +Categorical Operation & POSIX Shell Command Language Concept \\ +\hline +Vertical Composition ($>>$) & Command pipelining (\texttt{|}) \\ +Monoidal Tensor ($@$) & Independent branches (e.g., \texttt{fork}) \\ +Copy & Stream fan-out (e.g., \texttt{tee}) \\ +Merge & Stream fan-in (e.g., \texttt{cat}) \\ +Trace & Feedback/cyclic wiring (outside POSIX grammar) \\ +\hline +\end{tabular} +\end{table} + +\begin{credits} +\subsubsection{\ackname} +M. Coll acknowledges public, tuition-free, and high-quality higher education, Ley 24.521. + +% \subsubsection{\discintname} +% It is now necessary to declare any competing interests or to specifically +% state that the authors have no competing interests. Please place the +% statement with a bold run-in heading in small font size beneath the +% (optional) acknowledgments\footnote{If EquinOCS, our proceedings submission +% system, is used, then the disclaimer can be provided directly in the system.}, +% for example: The authors have no competing interests to declare that are +% relevant to the content of this article. Or: Author A has received research +% grants from Company W. Author B has received a speaker honorarium from +% Company X and owns stock in Company Y. Author C is a member of committee Z. +\subsubsection{\discintname} +The authors have no competing interests to declare that are +relevant to the content of this article. +\end{credits} +% +% ---- Bibliography ---- +% +% BibTeX users should specify bibliography style 'splncs04'. +% References will then be sorted and formatted in the correct style. +% +% \bibliographystyle{splncs04} +% \bibliography{mybibliography} +% +\printbibliography + + +\end{document} diff --git a/examples/hello-world-map.jpg b/examples/hello-world-map.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7cbe074f845dbfa4f1e5f62d59128871ceca13db GIT binary patch literal 78090 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 P??3cZzkfc%Qv3e_boZmD literal 60977 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 Y*H-Ka^@B6<$32(Q`HwFrNjIMV4 + + + + + + + 2026-03-15T19:48:36.878537 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/shell.yaml b/tests/shell.yaml new file mode 100644 index 0000000..24b3df6 --- /dev/null +++ b/tests/shell.yaml @@ -0,0 +1,4 @@ +!cat examples/shell.yaml: + ? !wc -c + ? !grep grep: !wc -c + ? !tail -2 diff --git a/tests/svg/test_fig_6_3_eq_0_comp.svg b/tests/svg/test_fig_6_3_eq_0_comp.svg new file mode 100644 index 0000000..2d9c329 --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_0_comp.svg @@ -0,0 +1,262 @@ + + + + + + + + 2026-03-18T16:04:25.237369 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_fig_6_3_eq_0_mprog.svg b/tests/svg/test_fig_6_3_eq_0_mprog.svg new file mode 100644 index 0000000..62b42df --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_0_mprog.svg @@ -0,0 +1,266 @@ + + + + + + + + 2026-03-18T16:04:25.276445 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_fig_6_3_eq_0_prog.svg b/tests/svg/test_fig_6_3_eq_0_prog.svg new file mode 100644 index 0000000..1758882 --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_0_prog.svg @@ -0,0 +1,262 @@ + + + + + + + + 2026-03-18T16:04:25.258172 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_fig_6_3_eq_1_comp.svg b/tests/svg/test_fig_6_3_eq_1_comp.svg new file mode 100644 index 0000000..cbf0b2a --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_1_comp.svg @@ -0,0 +1,325 @@ + + + + + + + + 2026-03-18T16:04:25.306843 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_fig_6_3_eq_1_mprog.svg b/tests/svg/test_fig_6_3_eq_1_mprog.svg new file mode 100644 index 0000000..2b89e91 --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_1_mprog.svg @@ -0,0 +1,292 @@ + + + + + + + + 2026-03-18T16:04:25.361541 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_fig_6_3_eq_1_prog.svg b/tests/svg/test_fig_6_3_eq_1_prog.svg new file mode 100644 index 0000000..51e4dca --- /dev/null +++ b/tests/svg/test_fig_6_3_eq_1_prog.svg @@ -0,0 +1,325 @@ + + + + + + + + 2026-03-18T16:04:25.334234 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_sec_6_2_2_comp.svg b/tests/svg/test_sec_6_2_2_comp.svg new file mode 100644 index 0000000..5c92a49 --- /dev/null +++ b/tests/svg/test_sec_6_2_2_comp.svg @@ -0,0 +1,246 @@ + + + + + + + + 2026-03-18T16:04:25.190487 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_sec_6_2_2_mprog.svg b/tests/svg/test_sec_6_2_2_mprog.svg new file mode 100644 index 0000000..e074c9c --- /dev/null +++ b/tests/svg/test_sec_6_2_2_mprog.svg @@ -0,0 +1,283 @@ + + + + + + + + 2026-03-18T16:04:25.220451 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_sec_6_2_2_prog.svg b/tests/svg/test_sec_6_2_2_prog.svg new file mode 100644 index 0000000..d6d33dd --- /dev/null +++ b/tests/svg/test_sec_6_2_2_prog.svg @@ -0,0 +1,240 @@ + + + + + + + + 2026-03-18T16:04:25.203748 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializer_has_unit_metaprogram_domain_comp.svg b/tests/svg/test_specializer_has_unit_metaprogram_domain_comp.svg new file mode 100644 index 0000000..7c65738 --- /dev/null +++ b/tests/svg/test_specializer_has_unit_metaprogram_domain_comp.svg @@ -0,0 +1,246 @@ + + + + + + + + 2026-03-18T14:48:32.145686 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializer_has_unit_metaprogram_domain_mprog.svg b/tests/svg/test_specializer_has_unit_metaprogram_domain_mprog.svg new file mode 100644 index 0000000..b223c2e --- /dev/null +++ b/tests/svg/test_specializer_has_unit_metaprogram_domain_mprog.svg @@ -0,0 +1,283 @@ + + + + + + + + 2026-03-18T14:48:32.172182 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializer_has_unit_metaprogram_domain_prog.svg b/tests/svg/test_specializer_has_unit_metaprogram_domain_prog.svg new file mode 100644 index 0000000..1745a2e --- /dev/null +++ b/tests/svg/test_specializer_has_unit_metaprogram_domain_prog.svg @@ -0,0 +1,240 @@ + + + + + + + + 2026-03-18T14:48:32.157485 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg new file mode 100644 index 0000000..1c8d245 --- /dev/null +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg @@ -0,0 +1,246 @@ + + + + + + + + 2026-03-18T16:04:25.384331 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg new file mode 100644 index 0000000..4b795f3 --- /dev/null +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg @@ -0,0 +1,283 @@ + + + + + + + + 2026-03-18T16:04:25.415906 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg new file mode 100644 index 0000000..47e3bf1 --- /dev/null +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg @@ -0,0 +1,240 @@ + + + + + + + + 2026-03-18T16:04:25.397835 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_compiler.py b/tests/test_compiler.py index c885efa..05342c1 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,7 +1,8 @@ import pytest -from widip.computer import * -from widip.lang import * +from widip.comput.computer import * +from widip.comput.boxes import Data, Parallel, Partial, Sequential +from widip.comput.compile import Compile from os import path SVG_ROOT_PATH = path.join("tests", "svg") @@ -84,4 +85,3 @@ def test_eq_2_5_compile_partial_is_eval(request): compiled = compiler(right) assert compiled == left request.node.draw_objects = (left, right) - diff --git a/tests/test_hif.py b/tests/test_hif.py new file mode 100644 index 0000000..6f18c32 --- /dev/null +++ b/tests/test_hif.py @@ -0,0 +1,122 @@ +from pathlib import Path + +from nx_yaml import nx_compose_all + +from widip.metaprog.hif import HIFSpecializer +from widip.state.hif import ( + document_root_node, + mapping_entry_nodes, + sequence_item_nodes, + stream_document_nodes, +) +from widip.wire.hif import hif_node + + +class ShapeSpecializer(HIFSpecializer): + def node_map(self, graph, node, kind, value, tag): + del graph, node + return (kind, tag, value) + + +def test_document_root_node_for_scalar_yaml(): + graph = nx_compose_all("a") + + documents = tuple(stream_document_nodes(graph)) + assert documents == (1,) + + root = document_root_node(graph, documents[0]) + assert hif_node(graph, root)["kind"] == "scalar" + assert hif_node(graph, root).get("tag", "") == "" + assert hif_node(graph, root).get("value", "") == "a" + + +def test_sequence_item_nodes_follow_next_and_forward_links(): + graph = nx_compose_all("- a\n- !echo b\n- c\n") + document = tuple(stream_document_nodes(graph))[0] + root = document_root_node(graph, document) + + items = tuple(sequence_item_nodes(graph, root)) + + assert hif_node(graph, root)["kind"] == "sequence" + assert [hif_node(graph, item).get("value", "") for item in items] == ["a", "b", "c"] + assert [(hif_node(graph, item).get("tag") or "")[1:] for item in items] == ["", "echo", ""] + + +def test_mapping_entry_nodes_cover_the_shell_case_study(): + graph = nx_compose_all(Path("examples/shell.yaml").read_text()) + document = tuple(stream_document_nodes(graph))[0] + outer_mapping = document_root_node(graph, document) + + outer_entries = tuple(mapping_entry_nodes(graph, outer_mapping)) + assert len(outer_entries) == 1 + + outer_key, inner_mapping = outer_entries[0] + assert (hif_node(graph, outer_key).get("tag") or "")[1:] == "cat" + assert hif_node(graph, outer_key).get("value", "") == "examples/shell.yaml" + assert hif_node(graph, inner_mapping)["kind"] == "mapping" + + inner_entries = tuple(mapping_entry_nodes(graph, inner_mapping)) + assert len(inner_entries) == 3 + + first_key, first_value = inner_entries[0] + assert ((hif_node(graph, first_key).get("tag") or "")[1:], hif_node(graph, first_key).get("value", "")) == ( + "wc", + "-c", + ) + assert hif_node(graph, first_value).get("value", "") == "" + + second_key, second_value = inner_entries[1] + assert hif_node(graph, second_key)["kind"] == "mapping" + assert hif_node(graph, second_value).get("value", "") == "" + + nested_entries = tuple(mapping_entry_nodes(graph, second_key)) + assert len(nested_entries) == 1 + nested_key, nested_value = nested_entries[0] + assert ((hif_node(graph, nested_key).get("tag") or "")[1:], hif_node(graph, nested_key).get("value", "")) == ( + "grep", + "grep", + ) + assert ( + (hif_node(graph, nested_value).get("tag") or "")[1:], + hif_node(graph, nested_value).get("value", ""), + ) == ("wc", "-c") + + third_key, third_value = inner_entries[2] + assert ((hif_node(graph, third_key).get("tag") or "")[1:], hif_node(graph, third_key).get("value", "")) == ( + "tail", + "-2", + ) + assert hif_node(graph, third_value).get("value", "") == "" + + +def test_fold_hif_uses_actual_yaml_node_kinds_for_scalar_documents(): + folded = ShapeSpecializer()(nx_compose_all("!echo a")) + + assert folded == ("stream", None, (("document", None, ("scalar", "echo", "a")),)) + + +def test_fold_hif_preserves_tagged_mapping_structure(): + folded = ShapeSpecializer()(nx_compose_all("!echo\n? a\n")) + + assert folded[0] == "stream" + assert len(folded[2]) == 1 + + document = folded[2][0] + assert document[0] == "document" + + mapping = document[2] + assert mapping[0] == "mapping" + assert mapping[1] == "echo" + assert len(mapping[2]) == 1 + assert mapping[2][0][0] == ("scalar", None, "a") + assert mapping[2][0][1] == ("scalar", None, "") + + +def test_fold_hif_exposes_multi_document_streams_directly(): + folded = ShapeSpecializer()(nx_compose_all("--- a\n--- b\n")) + + assert folded[0] == "stream" + assert [document[2] for document in folded[2]] == [ + ("scalar", None, "a"), + ("scalar", None, "b"), + ] diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py new file mode 100644 index 0000000..0ea655c --- /dev/null +++ b/tests/test_interpreter.py @@ -0,0 +1,18 @@ +from widip.compiler import Eval_H, Eval_L, H_ty, L_ty +from widip.comput.computer import Computer, Ty + + +def test_high_level_interpreter_is_typed_evaluator(): + A, B = Ty("A"), Ty("B") + evaluator = Eval_H(A, B) + assert isinstance(evaluator, Computer) + assert evaluator.dom == Computer(H_ty, A, B).dom + assert evaluator.cod == Computer(H_ty, A, B).cod + + +def test_low_level_interpreter_is_typed_evaluator(): + A, B = Ty("A"), Ty("B") + evaluator = Eval_L(A, B) + assert isinstance(evaluator, Computer) + assert evaluator.dom == Computer(L_ty, A, B).dom + assert evaluator.cod == Computer(L_ty, A, B).cod diff --git a/tests/test_lang.py b/tests/test_lang.py index 2c24a7d..53a8e31 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,78 +1,145 @@ -from functools import partial - -import pytest - -from widip.computer import * -from widip.to_py import to_py -from widip.lang import * -from discopy import closed, python -from os import path - - -X, A = Ty("X"), Ty("A") - -SVG_ROOT_PATH = path.join("tests", "svg") - -def svg_path(filename): - return path.join(SVG_ROOT_PATH, filename) - - -def test_distinguished_program_type(): - assert ProgramTy() != closed.Ty() - assert ProgramTy() != closed.Ty("P") - assert ProgramOb() != cat.Ob() - -def test_fig_2_13(): - """Fig 2.13: {} induces the X-indexed family runAB_X""" - G = Id() - assert run(G, G.dom[1:], G.cod) == run(G, G.dom[:1], G.cod) - -def test_fig_2_14(): - """Fig 2.14: naturality requirement for runAB""" - G = Id() - s = Id() - assert run(G, G.dom[1:], G.cod) >> s == run(G >> s, G.dom[:1], G.cod) - -def test_eq_2_15(): - """Eq 2.15""" - G = Id() - assert run(G, G.dom[1:], G.cod) == eval_f(G) - -def test_fig_2_16(): - """Fig 2.16: {} == runAB_P(id)""" - G = Id(ProgramTy()) - assert Eval(Ty() << Ty()) == run(G, Ty(), Ty()) - -def test_fig_7_2(): - """Eq 2.2: g = (G × A) ; {} with G : X⊸P and g : X×A→B.""" - X, A, B = Ty("X"), Ty("A"), Ty("B") - G = Box("G", X, B << A) - g = (G @ A) >> run(G, A, B) - assert g == (G @ A) >> Eval(A, B) - assert g.dom == X @ A - assert g.cod == B - -### -# Python-based axiom checks -### - -@pytest.mark.parametrize(["diagram_left", "diagram_right", "expected"],[ - (Copy(A) >> Copy(A) @ A, Copy(A) >> A @ Copy(A), ("s0", "s0", "s0")), - (Id(A), Copy(A) >> Delete(A) @ A, "s0"), - (Id(A), Copy(A) >> A @ Delete(A), "s0"), - (Copy(A), Copy(A) >> Swap(A, A), ("s0", "s0")), - (Id(), Copy(Ty()), ()), - (Id(), Delete(Ty()), ()), - ], -) -def test_cartesian_data_services(diagram_left, diagram_right, expected): - """Eq 1.2""" - left_f = to_py(diagram_left) - right_f = to_py(diagram_right) - assert len(diagram_left.dom) == len(diagram_right.dom) - assert len(diagram_left.cod) == len(diagram_right.cod) - inputs = tuple(f"s{i}" for i in range(len(left_f.dom))) - with python.Function.no_type_checking: - left = left_f(*inputs) - assert left == expected - assert left == right_f(*inputs) +from widip.comput import SHELL +from widip.comput.computer import Ty +from widip.comput.widish import Command, Literal, ShellProgram, io_ty, shell_program_ty +from widip.metaprog import SHELL_TO_PYTHON +from widip.metaprog.widish import Parallel, Pipeline, ShellSpecializer, parallel +from widip.state.widish import ShellExecution +from widip.wire.widish import Copy + + +def box_names(diagram): + return tuple(layer[1].name for layer in diagram.inside) + + +def test_command_programs_have_shell_program_type(): + command = Command(["echo", "hello"]) + assert isinstance(command, ShellProgram) + assert command.dom == Ty() + assert command.cod == shell_program_ty + + +def test_commands_run_through_stateful_execution(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Command(["echo", "hello"]) + runnable = program @ io_ty >> execution + assert runnable.dom == io_ty + assert runnable.cod == io_ty + + +def test_sh_command_runs_through_shell_runner(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Command(["sh", "-c", "read line; printf 'shell:%s' \"$line\"", "sh"]) @ io_ty >> execution + + assert SHELL_TO_PYTHON(program)("world\n") == "shell:world" + + +def test_shell_language_chooses_shell_program_type_and_execution(): + execution = SHELL.execution(io_ty, io_ty) + assert SHELL.program_ty == shell_program_ty + assert isinstance(execution, ShellExecution) + assert execution.dom == shell_program_ty @ io_ty + assert execution.cod == shell_program_ty @ io_ty + + +def test_copy_has_expected_shell_types(): + assert Copy(3).dom == io_ty + assert Copy(3).cod == io_ty @ io_ty @ io_ty + + +def test_parallel_helper_builds_parallel_bubble(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + branches = ( + Literal("a") @ io_ty >> execution, + Literal("b") @ io_ty >> execution, + Literal("c") @ io_ty >> execution, + ) + assert parallel(branches) == Parallel(branches) + + +def test_shell_bubbles_are_lowered_by_shell_specializer(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + pipeline = Pipeline((Literal("a") @ io_ty >> execution,)) + parallel_bubble = Parallel((Literal("a") @ io_ty >> execution,)) + + assert ShellSpecializer()(pipeline) == pipeline.specialize() + assert ShellSpecializer()(parallel_bubble) == parallel_bubble.specialize() + + +def test_sequence_bubble_specializes_to_pipeline(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + first = Literal("a") @ io_ty >> execution + second = Literal("b") @ io_ty >> execution + bubble = Pipeline((first, second)) + + assert bubble.specialize() == first >> second + + +def test_mapping_bubble_specializes_to_primitive_command_diagram(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + branches = ( + Literal("a") @ io_ty >> execution, + Literal("b") @ io_ty >> execution, + Literal("c") @ io_ty >> execution, + ) + bubble = Parallel(branches) + specialized = bubble.specialize() + names = box_names(specialized) + + assert specialized.dom == io_ty + assert specialized.cod == io_ty + assert any(name.startswith("('tee', '/tmp/widip-") for name in names) + assert any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert "merge[3]" not in names + assert "∆" not in names + + +def test_discorun_parallel_example_runs(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Parallel( + ( + Command(["cat"]) @ io_ty >> execution, + Command(["grep", "-c", "x"]) @ io_ty >> execution, + Command(["wc", "-l"]) @ io_ty >> execution, + ) + ) + assert SHELL_TO_PYTHON(program)("a\nx\n") == "a\nx\n1\n2\n" + + +def test_parallel_preserves_argv_literals_without_shell_reparsing(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Parallel( + ( + Command(["printf", "%s", "a|b"]) @ io_ty >> execution, + Command(["printf", "%s", "c&d"]) @ io_ty >> execution, + ) + ) + + assert SHELL_TO_PYTHON(program)("") == "a|bc&d" + + +def test_parallel_specializer_inlines_native_command_diagram(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Parallel( + ( + Command(["printf", "%s", "left"]) @ io_ty >> execution, + Command(["printf", "%s", "right"]) @ io_ty >> execution, + ) + ) + specialized = ShellSpecializer()(program) + names = box_names(specialized) + + assert any(name.startswith("('tee', '/tmp/widip-") for name in names) + assert any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert "merge[2]" not in names + assert "∆" not in names + assert SHELL_TO_PYTHON(program)("") == "leftright" + + +def test_stateful_shell_execution_preserves_program_state(): + program = Command(["printf", "hello"]) + runner = SHELL_TO_PYTHON(program @ io_ty >> SHELL.execution(io_ty, io_ty)) + state, output = runner("") + + assert callable(state) + assert state("") == "hello" + assert output == "hello" diff --git a/tests/test_loader.py b/tests/test_loader.py index bc5f826..cce545e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,38 +1,112 @@ -import pytest -from discopy.closed import Curry, Eval - -from widip.computer import Ty, Id -from widip.loader import repl_read - - -@pytest.mark.parametrize(["path", "yaml_text", "expected"], [ - [ - "empty_content.svg", - "", - Id(), - ], - [ - "scalar_only.svg", - "a", - Curry(Eval(Ty() >> Ty("a")), n=0, left=False), - ], - [ - "tagged_scalar.svg", - "!X a", - Curry(Eval(Ty("X") @ Ty("a") >> Ty()), n=2, left=False), - ], - [ - # implicit empty string - "tag_only.svg", - "!X", - Curry(Eval(Ty("X") @ Ty("") >> Ty()), n=2, left=False), - ], - [ - "empty_string.svg", - "''", - Curry(Id(Ty() >> Ty()), n=1, left=True), - ], -]) -def test_loader_encoding(path, yaml_text, expected): - actual = repl_read(yaml_text) - assert actual == expected +from pathlib import Path + +from nx_yaml import nx_compose_all + +from widip.comput import SHELL +from widip.comput.computer import Ty +from widip.comput.loader import LoaderLiteral, loader_program_ty +from widip.comput.widish import Command, Literal, io_ty, shell_program_ty +from widip.metaprog import HIF_TO_LOADER, LOADER_TO_SHELL, incidences_to_program, repl_read +from widip.metaprog.widish import Parallel, Pipeline +from widip.state import loader_output, loader_state_update +from widip.state.core import InputOutputMap, StateUpdateMap +from widip.wire.hif import HyperGraph +from widip.wire.loader import LoaderMapping, LoaderScalar, loader_stream_ty +from widip.wire.widish import shell_id + + +def test_loader_empty_stream_is_identity(): + assert repl_read("") == shell_id() + + +def test_loader_scalar_program_is_functorial(): + program = LoaderLiteral("scalar") + compiled = LOADER_TO_SHELL(program) + + assert program.dom == Ty() + assert program.cod == loader_program_ty + assert compiled == Literal("scalar") + + +def test_loader_translation_preserves_tagged_mapping_nodes(): + graph = nx_compose_all("!echo\n? scalar\n") + program = incidences_to_program(graph) + + assert isinstance(program, LoaderMapping) + assert program.tag == "echo" + assert len(program.branches) == 1 + + +def test_loader_translation_uses_hif_metaprogram(): + graph: HyperGraph = nx_compose_all("!echo scalar") + + assert incidences_to_program(graph) == HIF_TO_LOADER(graph) + + +def test_loader_tagged_scalar_stays_loader_node_until_compiled(): + program = incidences_to_program(nx_compose_all("!echo scalar")) + compiled = LOADER_TO_SHELL(program) + execution = SHELL.execution(io_ty, io_ty).output_diagram() + + assert isinstance(program, LoaderScalar) + assert program.cod == loader_stream_ty + assert compiled == Command(["echo", "scalar"]) @ io_ty >> execution + + +def test_loader_scalar_literal(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + assert repl_read("scalar") == Literal("scalar") @ io_ty >> execution + + +def test_loader_state_projections_are_transported_by_state_layer(): + assert LOADER_TO_SHELL(loader_state_update()) == StateUpdateMap("loader", shell_program_ty, io_ty) + assert LOADER_TO_SHELL(loader_output()) == InputOutputMap("loader", shell_program_ty, io_ty, io_ty) + + +def test_loader_empty_scalar_is_identity(): + assert repl_read("''") == shell_id() + + +def test_loader_tagged_scalar_is_command(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + assert repl_read("!echo scalar") == Command(["echo", "scalar"]) @ io_ty >> execution + + +def test_loader_tag_only_is_command_with_no_scalar_argument(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + assert repl_read("!cat") == Command(["cat"]) @ io_ty >> execution + + +def test_loader_sequence_is_pipeline(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + expected = ( + (Command(["grep", "grep"]) @ io_ty >> execution) + >> (Command(["wc", "-c"]) @ io_ty >> execution) + ) + diagram = repl_read("- !grep grep\n- !wc -c\n") + assert isinstance(diagram, Pipeline) + assert diagram.specialize() == expected + + +def test_loader_shell_case_study_is_mapping_bubble(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + expected = Parallel( + ( + (Command(["cat", "examples/shell.yaml"]) @ io_ty >> execution) + >> Parallel( + ( + (Command(["wc", "-c"]) @ io_ty >> execution), + Parallel( + ( + (Command(["grep", "grep"]) @ io_ty >> execution) + >> (Command(["wc", "-c"]) @ io_ty >> execution), + ) + ), + (Command(["tail", "-2"]) @ io_ty >> execution), + ) + ), + ) + ) + diagram = repl_read(Path("examples/shell.yaml").read_text()) + assert isinstance(diagram, Parallel) + assert diagram == expected diff --git a/tests/test_metaprog.py b/tests/test_metaprog.py index 0aa3ae0..7b5252d 100644 --- a/tests/test_metaprog.py +++ b/tests/test_metaprog.py @@ -1,6 +1,7 @@ import pytest +from nx_yaml import nx_compose_all -from widip.computer import * +from widip.comput.computer import * from widip.metaprog import * from os import path @@ -35,23 +36,58 @@ def after_each_test(request): H_to_L = ProgramComputation("H", L_ty, X, A, B) L_to_H = ProgramComputation("L", H_ty, X, A, B) + def test_sec_6_2_2(request): """ Sec. 6.2.2 {H}L = h, {L}H = l """ - assert H_to_L.universal_ev() == h_ev - assert L_to_H.universal_ev() == l_ev + program_f = ProgramFunctor() request.node.draw_objects = (h_ev, l_ev, H_to_L) + assert program_f(H_to_L) == h_ev + assert program_f(L_to_H) == l_ev def test_fig_6_3_eq_0(request): """ - Fig. 6.3 {X}H y = {H}L(X, y) + Fig. 6.3 right-hand side: {H}L(X, y) + """ + x_program = Program("X", H_ty, Ty()) + interpreter = ProgramComputation("H", L_ty, H_ty, A, B) + + transformed = x_program @ A >> ProgramFunctor()(interpreter) + expected = x_program @ A >> ComputableFunction("{H}", H_ty, A, B) + + request.node.draw_objects = (expected, transformed, interpreter) + assert transformed == expected + + +def test_fig_6_3_eq_1(request): + """ + Fig. 6.3 right-hand side after one metaprogram rewrite: {pev(H)L X}L y """ - # comp = ComputableFunction("f", X, A, B) - # prog = Program("f", L_ty, X) - # mprog = Metaprogram("F", L_ty) - # right = MetaprogramFunctor()(mprog) - # assert right == prog - # right = ProgramFunctor()(right) - # assert right == comp - # request.node.draw_objects = (comp, prog, mprog) + x_program = Program("X", H_ty, Ty()) + compiler = MetaprogramComputation("H", L_ty, L_ty, H_ty, A, B) + + transformed = x_program @ A >> MetaprogramFunctor()(compiler) + expected = ( + x_program @ A + >> ProgramComputation("{H}", L_ty, Ty(), H_ty, L_ty) @ A + >> Computer(L_ty, A, B) + ) + + request.node.draw_objects = (expected, transformed, compiler) + assert MetaprogramFunctor()(compiler) == compiler.partial_ev() + assert transformed == expected + + +def test_specializers_are_unit_metaprograms_with_partial_evaluators(request): + request.node.draw_objects = (h_ev, l_ev, H_to_L) + + graph = nx_compose_all("a") + + assert Specializer.metaprogram_dom() == Ty() + assert HIFToLoader.metaprogram_dom() == Ty() + assert LoaderToShell.metaprogram_dom() == Ty() + assert ShellSpecializer.metaprogram_dom() == Ty() + assert isinstance(LOADER_TO_SHELL, Specializer) + assert isinstance(SHELL_SPECIALIZER, Specializer) + assert HIFToLoader().specialize(graph) == HIF_TO_LOADER(graph) diff --git a/tests/test_runner.py b/tests/test_runner.py index 7d6a649..83e999a 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,18 +1,60 @@ +import os +import re +from pathlib import Path + import pytest +from nx_yaml import nx_compose_all + +from widip.metaprog import LOADER_TO_SHELL, SHELL_TO_PYTHON, incidences_to_program +from widip.metaprog.widish import ShellSpecializer + + +FIXTURE_DIR = Path("tests/widish") + +os.environ.setdefault("MPLCONFIGDIR", "/tmp/widip-mpl") + + +def case_paths(): + return tuple(sorted(path.with_suffix("") for path in FIXTURE_DIR.glob("*.yaml"))) + + +def normalize_svg(svg_text: str) -> str: + """Strip volatile Matplotlib metadata from golden SVGs.""" + svg_text = re.sub(r".*?\s*", "", svg_text, flags=re.DOTALL) + svg_text = re.sub(r'id="[^"]*[0-9a-f]{8,}[^"]*"', 'id="SVG_ID"', svg_text) + svg_text = re.sub(r'url\(#([^)]*[0-9a-f]{8,}[^)]*)\)', 'url(#SVG_ID)', svg_text) + svg_text = re.sub(r'xlink:href="#[^"]*[0-9a-f]{8,}[^"]*"', 'xlink:href="#SVG_ID"', svg_text) + svg_text = re.sub(r"/tmp/widip-[^<\" ]+\.tmp", "/tmp/WIDIP_TMP", svg_text) + marker_use_re = re.compile(r'^\s*\s*$', re.MULTILINE) + marker_uses = iter(sorted(match.group(0).strip() for match in marker_use_re.finditer(svg_text))) + svg_text = marker_use_re.sub(lambda _match: next(marker_uses), svg_text) + return svg_text.strip() + +@pytest.mark.parametrize("path", case_paths(), ids=lambda path: path.name) +def test_shell_runner_files(path, tmp_path): + yaml_path = path.with_suffix(".yaml") + stdin_path = path.with_suffix(".in") + stdout_path = path.with_suffix(".out") + prog_svg_path = path.with_suffix(".prog.svg") + mprog_svg_path = path.with_suffix(".mprog.svg") + + assert yaml_path.exists() + assert stdin_path.exists() + assert stdout_path.exists() + assert prog_svg_path.exists() + assert mprog_svg_path.exists() + + yaml_text = yaml_path.read_text() + mprog = incidences_to_program(nx_compose_all(yaml_text)) + prog = ShellSpecializer()(LOADER_TO_SHELL(mprog)) + + actual_mprog_svg_path = tmp_path / f"{path.name}.mprog.svg" + actual_prog_svg_path = tmp_path / f"{path.name}.prog.svg" + mprog.draw(path=str(actual_mprog_svg_path)) + prog.draw(path=str(actual_prog_svg_path)) + + program = SHELL_TO_PYTHON(prog) -from widip.loader import repl_read -from widip.widish import SHELL_RUNNER - - -@pytest.mark.parametrize(["yaml_text", "stdin", "expected"], [ - ["scalar", "", "scalar"], - ["? scalar", "", "scalar"], - ["- scalar", "", "scalar"], - ["!printf scalar", "", "scalar"], - ["!echo scalar", "", "scalar\n"], -]) -def test_shell_runner(yaml_text, stdin, expected): - # TODO deduplicate with widish_main - fd = repl_read(yaml_text) - constants = tuple(x.name for x in fd.dom) - assert SHELL_RUNNER(fd)(*constants)(stdin) == expected + assert program(stdin_path.read_text()) == stdout_path.read_text() + assert normalize_svg(actual_prog_svg_path.read_text()) == normalize_svg(prog_svg_path.read_text()) + assert normalize_svg(actual_mprog_svg_path.read_text()) == normalize_svg(mprog_svg_path.read_text()) diff --git a/tests/test_state.py b/tests/test_state.py index 8a20bea..a9f5758 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,15 +1,24 @@ -from widip.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty +from discopy import python + +from widip.comput import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL +from widip.comput import computer +from widip.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty +from widip.comput.widish import io_ty from widip.state import ( Execution, - MonoidalComputer, + InputOutputMap, Process, - ProgramClosedCategory, + StateUpdateMap, execute, fixed_state, - out, + loader_output, + loader_state_update, + shell_output, + shell_state_update, simulate, - sta, ) +from widip.state.core import ProcessRunner +from widip.wire.loader import loader_stream_ty X, Y, A, B = Ty("X"), Ty("Y"), Ty("A"), Ty("B") @@ -19,12 +28,14 @@ def test_eq_7_1_process_is_a_pair_of_functions(): q = Process("q", X, A, B) - expected = Copy(X @ A) >> sta(q) @ out(q) - - assert sta(q).dom == X @ A - assert sta(q).cod == X - assert out(q).dom == X @ A - assert out(q).cod == B + expected = Copy(X @ A) >> q.state_update_diagram() @ q.output_diagram() + + assert isinstance(q.state_update_diagram(), StateUpdateMap) + assert isinstance(q.output_diagram(), InputOutputMap) + assert q.state_update_diagram().dom == X @ A + assert q.state_update_diagram().cod == X + assert q.output_diagram().dom == X @ A + assert q.output_diagram().cod == B assert q == expected @@ -39,14 +50,94 @@ def test_fig_7_2_simulation_is_postcomposition_with_state_map(): assert simulated.cod == Y @ B +def test_process_maps_are_overrideable_methods(): + state_update = Box("u", X @ A, X) + output = Box("v", X @ A, B) + + class CustomProcess(Process): + def state_update_diagram(self): + return state_update + + def output_diagram(self): + return output + + q = CustomProcess("q", X, A, B) + + assert q.state_update_diagram() == state_update + assert q.output_diagram() == output + assert q == Copy(X @ A) >> state_update @ output + + def test_sec_7_3_program_execution_is_stateful(): - execution = Execution(P, A, B) + execution = Execution( + "{}", + P, + A, + B, + ) assert execution.dom == P @ A assert execution.cod == P @ B assert execution.universal_ev() == Computer(P, A, P @ B) - assert sta(execution).cod == P - assert out(execution).cod == B + assert execution.state_update_diagram() == StateUpdateMap("{}", P, A) + assert execution.output_diagram() == InputOutputMap("{}", P, A, B) + assert execution.state_update_diagram().cod == P + assert execution.output_diagram().cod == B + + +def test_execution_universal_evaluator_is_overrideable_method(): + universal_ev = Box("ev", P @ A, P @ B) + + class CustomExecution(Execution): + def universal_ev(self): + return universal_ev + + execution = CustomExecution("q", P, A, B) + + assert execution.universal_ev() == universal_ev + assert execution.specialize() == universal_ev + + +def test_process_runner_interprets_generic_state_projections(): + class DummyRunner(ProcessRunner): + def __init__(self): + ProcessRunner.__init__(self, lambda _ob: object) + + def process_ar_map(self, box, dom, cod): + return python.Function(lambda *_xs: None, dom, cod) + + runner = DummyRunner() + state_update = runner.ar_map(StateUpdateMap("q", X, A)) + output = runner.ar_map(InputOutputMap("q", X, A, B)) + + assert state_update("state", "input") == "state" + assert output(lambda value: f"out:{value}", "input") == "out:input" + + +def test_process_runner_interprets_generic_structural_boxes(): + class DummyRunner(ProcessRunner): + def __init__(self): + ProcessRunner.__init__(self, lambda _ob: object) + + def process_ar_map(self, box, dom, cod): + return python.Function(lambda *_xs: None, dom, cod) + + runner = DummyRunner() + + assert runner.ar_map(Copy(X))("value") == ("value", "value") + assert runner.ar_map(computer.Delete(X))("value") == () + assert runner.ar_map(computer.Swap(X, Y))("left", "right") == ("right", "left") + + +def test_loader_and_shell_projections_live_in_state(): + assert loader_state_update() == LOADER.execution( + loader_stream_ty, loader_stream_ty + ).state_update_diagram() + assert loader_output() == LOADER.execution( + loader_stream_ty, loader_stream_ty + ).output_diagram() + assert shell_state_update() == SHELL.execution(io_ty, io_ty).state_update_diagram() + assert shell_output() == SHELL.execution(io_ty, io_ty).output_diagram() def test_sec_7_4_fixed_state_lifts_a_function_to_a_process(): @@ -62,7 +153,12 @@ def test_sec_7_4_execute_uses_stateful_execution(): Q = Program("Q", P, X) q = execute(Q, A, B) - assert q == Q @ A >> Execution(P, A, B) + assert q == Q @ A >> Execution( + "{}", + P, + A, + B, + ) assert q.dom == X @ A assert q.cod == P @ B diff --git a/tests/test_wire.py b/tests/test_wire.py new file mode 100644 index 0000000..42e7aa5 --- /dev/null +++ b/tests/test_wire.py @@ -0,0 +1,36 @@ +from widip.comput.computer import Box as ComputerBox +from widip.comput.computer import Copy as ComputerCopy +from widip.comput.computer import Ty as ComputerTy +from widip.comput.widish import io_ty as shell_io_ty +from widip.wire.functions import Box +from widip.wire.loader import loader_id, loader_stream_ty +from widip.wire.services import Copy, Delete, Swap +from widip.wire.types import Diagram, Id, Ty +from widip.wire.widish import Copy as ShellCopy, shell_id + + +def test_wire_exports_chapter_one_primitives(): + A, B = Ty("A"), Ty("B") + + assert Ty is ComputerTy + assert Box is ComputerBox + assert Copy is ComputerCopy + assert isinstance(Id(A), Diagram) + assert Copy(A).dom == A + assert Copy(A).cod == A @ A + assert Delete(A).dom == A + assert Delete(A).cod == Ty() + assert Swap(A, B).dom == A @ B + assert Swap(A, B).cod == B @ A + + +def test_loader_wire_module_exports_loader_specific_wiring(): + assert loader_id().dom == loader_stream_ty + assert loader_id().cod == loader_stream_ty + + +def test_widish_wire_module_exports_shell_specific_wiring(): + assert shell_id().dom == shell_io_ty + assert shell_id().cod == shell_io_ty + assert ShellCopy(3).dom == shell_io_ty + assert ShellCopy(3).cod == shell_io_ty @ shell_io_ty @ shell_io_ty diff --git a/tests/widish/0.in b/tests/widish/0.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/0.mprog.svg b/tests/widish/0.mprog.svg new file mode 100644 index 0000000..518dabc --- /dev/null +++ b/tests/widish/0.mprog.svg @@ -0,0 +1,1114 @@ + + + + + + + + 2026-03-17T13:58:47.021520 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/0.out b/tests/widish/0.out new file mode 100644 index 0000000..cbaf305 --- /dev/null +++ b/tests/widish/0.out @@ -0,0 +1,4 @@ +73 +23 + ? !grep grep: !wc -c + ? !tail -2 diff --git a/tests/widish/0.prog.svg b/tests/widish/0.prog.svg new file mode 100644 index 0000000..e2193f0 --- /dev/null +++ b/tests/widish/0.prog.svg @@ -0,0 +1,1776 @@ + + + + + + + + 2026-03-17T14:36:22.984310 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/0.yaml b/tests/widish/0.yaml new file mode 100644 index 0000000..24b3df6 --- /dev/null +++ b/tests/widish/0.yaml @@ -0,0 +1,4 @@ +!cat examples/shell.yaml: + ? !wc -c + ? !grep grep: !wc -c + ? !tail -2 diff --git a/tests/widish/01.in b/tests/widish/01.in new file mode 100644 index 0000000..8378831 --- /dev/null +++ b/tests/widish/01.in @@ -0,0 +1 @@ +pass through diff --git a/tests/widish/01.mprog.svg b/tests/widish/01.mprog.svg new file mode 100644 index 0000000..4ad594c --- /dev/null +++ b/tests/widish/01.mprog.svg @@ -0,0 +1,258 @@ + + + + + + + + 2026-03-17T13:58:47.059330 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/01.out b/tests/widish/01.out new file mode 100644 index 0000000..8378831 --- /dev/null +++ b/tests/widish/01.out @@ -0,0 +1 @@ +pass through diff --git a/tests/widish/01.prog.svg b/tests/widish/01.prog.svg new file mode 100644 index 0000000..f077d58 --- /dev/null +++ b/tests/widish/01.prog.svg @@ -0,0 +1,95 @@ + + + + + + + + 2026-03-17T14:36:23.040356 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/01.yaml b/tests/widish/01.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/02.in b/tests/widish/02.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/02.mprog.svg b/tests/widish/02.mprog.svg new file mode 100644 index 0000000..ce21e38 --- /dev/null +++ b/tests/widish/02.mprog.svg @@ -0,0 +1,330 @@ + + + + + + + + 2026-03-17T13:58:47.070287 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/02.out b/tests/widish/02.out new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/02.out @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/02.prog.svg b/tests/widish/02.prog.svg new file mode 100644 index 0000000..a468c86 --- /dev/null +++ b/tests/widish/02.prog.svg @@ -0,0 +1,398 @@ + + + + + + + + 2026-03-17T14:36:23.051559 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/02.yaml b/tests/widish/02.yaml new file mode 100644 index 0000000..e8df7ca --- /dev/null +++ b/tests/widish/02.yaml @@ -0,0 +1 @@ +scalar diff --git a/tests/widish/03.in b/tests/widish/03.in new file mode 100644 index 0000000..5537770 --- /dev/null +++ b/tests/widish/03.in @@ -0,0 +1 @@ +ignored \ No newline at end of file diff --git a/tests/widish/03.mprog.svg b/tests/widish/03.mprog.svg new file mode 100644 index 0000000..b0d2273 --- /dev/null +++ b/tests/widish/03.mprog.svg @@ -0,0 +1,303 @@ + + + + + + + + 2026-03-17T13:58:47.081092 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/03.out b/tests/widish/03.out new file mode 100644 index 0000000..5537770 --- /dev/null +++ b/tests/widish/03.out @@ -0,0 +1 @@ +ignored \ No newline at end of file diff --git a/tests/widish/03.prog.svg b/tests/widish/03.prog.svg new file mode 100644 index 0000000..0baa382 --- /dev/null +++ b/tests/widish/03.prog.svg @@ -0,0 +1,95 @@ + + + + + + + + 2026-03-17T14:36:23.063483 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/03.yaml b/tests/widish/03.yaml new file mode 100644 index 0000000..a614936 --- /dev/null +++ b/tests/widish/03.yaml @@ -0,0 +1 @@ +'' diff --git a/tests/widish/04.in b/tests/widish/04.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/04.mprog.svg b/tests/widish/04.mprog.svg new file mode 100644 index 0000000..d321ed3 --- /dev/null +++ b/tests/widish/04.mprog.svg @@ -0,0 +1,474 @@ + + + + + + + + 2026-03-17T13:58:47.103262 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/04.out b/tests/widish/04.out new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/04.out @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/04.prog.svg b/tests/widish/04.prog.svg new file mode 100644 index 0000000..ae6bfdb --- /dev/null +++ b/tests/widish/04.prog.svg @@ -0,0 +1,398 @@ + + + + + + + + 2026-03-17T14:36:23.076195 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/04.yaml b/tests/widish/04.yaml new file mode 100644 index 0000000..2f091e8 --- /dev/null +++ b/tests/widish/04.yaml @@ -0,0 +1 @@ +? scalar diff --git a/tests/widish/05.in b/tests/widish/05.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/05.mprog.svg b/tests/widish/05.mprog.svg new file mode 100644 index 0000000..6b95b82 --- /dev/null +++ b/tests/widish/05.mprog.svg @@ -0,0 +1,438 @@ + + + + + + + + 2026-03-17T13:58:47.130084 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/05.out b/tests/widish/05.out new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/05.out @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/05.prog.svg b/tests/widish/05.prog.svg new file mode 100644 index 0000000..d535278 --- /dev/null +++ b/tests/widish/05.prog.svg @@ -0,0 +1,398 @@ + + + + + + + + 2026-03-17T14:36:23.096056 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/05.yaml b/tests/widish/05.yaml new file mode 100644 index 0000000..88442d8 --- /dev/null +++ b/tests/widish/05.yaml @@ -0,0 +1 @@ +- scalar diff --git a/tests/widish/06.in b/tests/widish/06.in new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/06.in @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/06.mprog.svg b/tests/widish/06.mprog.svg new file mode 100644 index 0000000..9390c10 --- /dev/null +++ b/tests/widish/06.mprog.svg @@ -0,0 +1,334 @@ + + + + + + + + 2026-03-17T13:58:47.144880 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/06.out b/tests/widish/06.out new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/06.out @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/06.prog.svg b/tests/widish/06.prog.svg new file mode 100644 index 0000000..6cd9dfa --- /dev/null +++ b/tests/widish/06.prog.svg @@ -0,0 +1,390 @@ + + + + + + + + 2026-03-17T14:36:23.115452 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/06.yaml b/tests/widish/06.yaml new file mode 100644 index 0000000..557df60 --- /dev/null +++ b/tests/widish/06.yaml @@ -0,0 +1 @@ +!cat diff --git a/tests/widish/07.in b/tests/widish/07.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/07.mprog.svg b/tests/widish/07.mprog.svg new file mode 100644 index 0000000..b1eaf43 --- /dev/null +++ b/tests/widish/07.mprog.svg @@ -0,0 +1,375 @@ + + + + + + + + 2026-03-17T13:58:47.158047 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/07.out b/tests/widish/07.out new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/widish/07.out @@ -0,0 +1 @@ + diff --git a/tests/widish/07.prog.svg b/tests/widish/07.prog.svg new file mode 100644 index 0000000..e024551 --- /dev/null +++ b/tests/widish/07.prog.svg @@ -0,0 +1,358 @@ + + + + + + + + 2026-03-17T14:36:23.166704 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/07.yaml b/tests/widish/07.yaml new file mode 100644 index 0000000..78ec3f1 --- /dev/null +++ b/tests/widish/07.yaml @@ -0,0 +1 @@ +!echo diff --git a/tests/widish/08.in b/tests/widish/08.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/08.mprog.svg b/tests/widish/08.mprog.svg new file mode 100644 index 0000000..d52e6a8 --- /dev/null +++ b/tests/widish/08.mprog.svg @@ -0,0 +1,433 @@ + + + + + + + + 2026-03-17T13:58:47.168290 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/08.out b/tests/widish/08.out new file mode 100644 index 0000000..3c97385 --- /dev/null +++ b/tests/widish/08.out @@ -0,0 +1 @@ +scalar \ No newline at end of file diff --git a/tests/widish/08.prog.svg b/tests/widish/08.prog.svg new file mode 100644 index 0000000..bf5cab6 --- /dev/null +++ b/tests/widish/08.prog.svg @@ -0,0 +1,486 @@ + + + + + + + + 2026-03-17T14:36:23.183030 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/08.yaml b/tests/widish/08.yaml new file mode 100644 index 0000000..9e175cc --- /dev/null +++ b/tests/widish/08.yaml @@ -0,0 +1 @@ +!printf scalar diff --git a/tests/widish/09.in b/tests/widish/09.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/09.mprog.svg b/tests/widish/09.mprog.svg new file mode 100644 index 0000000..f230216 --- /dev/null +++ b/tests/widish/09.mprog.svg @@ -0,0 +1,392 @@ + + + + + + + + 2026-03-17T13:58:47.179137 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/09.out b/tests/widish/09.out new file mode 100644 index 0000000..e8df7ca --- /dev/null +++ b/tests/widish/09.out @@ -0,0 +1 @@ +scalar diff --git a/tests/widish/09.prog.svg b/tests/widish/09.prog.svg new file mode 100644 index 0000000..c437846 --- /dev/null +++ b/tests/widish/09.prog.svg @@ -0,0 +1,418 @@ + + + + + + + + 2026-03-17T14:36:23.199555 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/09.yaml b/tests/widish/09.yaml new file mode 100644 index 0000000..9c442de --- /dev/null +++ b/tests/widish/09.yaml @@ -0,0 +1 @@ +!echo scalar diff --git a/tests/widish/10.in b/tests/widish/10.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/10.mprog.svg b/tests/widish/10.mprog.svg new file mode 100644 index 0000000..4f945ea --- /dev/null +++ b/tests/widish/10.mprog.svg @@ -0,0 +1,532 @@ + + + + + + + + 2026-03-17T13:58:47.223334 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/10.out b/tests/widish/10.out new file mode 100644 index 0000000..1910281 --- /dev/null +++ b/tests/widish/10.out @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/tests/widish/10.prog.svg b/tests/widish/10.prog.svg new file mode 100644 index 0000000..1dc664d --- /dev/null +++ b/tests/widish/10.prog.svg @@ -0,0 +1,478 @@ + + + + + + + + 2026-03-17T14:36:23.221296 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/10.yaml b/tests/widish/10.yaml new file mode 100644 index 0000000..95e8a57 --- /dev/null +++ b/tests/widish/10.yaml @@ -0,0 +1,2 @@ +- foo +- !cat diff --git a/tests/widish/11.in b/tests/widish/11.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/11.mprog.svg b/tests/widish/11.mprog.svg new file mode 100644 index 0000000..988cd8f --- /dev/null +++ b/tests/widish/11.mprog.svg @@ -0,0 +1,523 @@ + + + + + + + + 2026-03-17T13:58:47.247611 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/11.out b/tests/widish/11.out new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/tests/widish/11.out @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/tests/widish/11.prog.svg b/tests/widish/11.prog.svg new file mode 100644 index 0000000..97734e4 --- /dev/null +++ b/tests/widish/11.prog.svg @@ -0,0 +1,488 @@ + + + + + + + + 2026-03-17T14:36:23.242760 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/11.yaml b/tests/widish/11.yaml new file mode 100644 index 0000000..59121da --- /dev/null +++ b/tests/widish/11.yaml @@ -0,0 +1,2 @@ +- foo +- bar diff --git a/tests/widish/14.in b/tests/widish/14.in new file mode 100644 index 0000000..24b3df6 --- /dev/null +++ b/tests/widish/14.in @@ -0,0 +1,4 @@ +!cat examples/shell.yaml: + ? !wc -c + ? !grep grep: !wc -c + ? !tail -2 diff --git a/tests/widish/14.mprog.svg b/tests/widish/14.mprog.svg new file mode 100644 index 0000000..1b47aa7 --- /dev/null +++ b/tests/widish/14.mprog.svg @@ -0,0 +1,585 @@ + + + + + + + + 2026-03-17T13:58:47.344184 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/14.out b/tests/widish/14.out new file mode 100644 index 0000000..4099407 --- /dev/null +++ b/tests/widish/14.out @@ -0,0 +1 @@ +23 diff --git a/tests/widish/14.prog.svg b/tests/widish/14.prog.svg new file mode 100644 index 0000000..e3aeef8 --- /dev/null +++ b/tests/widish/14.prog.svg @@ -0,0 +1,540 @@ + + + + + + + + 2026-03-17T14:36:23.334904 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/14.yaml b/tests/widish/14.yaml new file mode 100644 index 0000000..308b86a --- /dev/null +++ b/tests/widish/14.yaml @@ -0,0 +1,2 @@ +- !grep grep +- !wc -c diff --git a/tests/widish/15.in b/tests/widish/15.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/15.mprog.svg b/tests/widish/15.mprog.svg new file mode 100644 index 0000000..7187fa6 --- /dev/null +++ b/tests/widish/15.mprog.svg @@ -0,0 +1,616 @@ + + + + + + + + 2026-03-17T13:58:47.381517 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/15.out b/tests/widish/15.out new file mode 100644 index 0000000..f6ea049 --- /dev/null +++ b/tests/widish/15.out @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/tests/widish/15.prog.svg b/tests/widish/15.prog.svg new file mode 100644 index 0000000..b8c30a6 --- /dev/null +++ b/tests/widish/15.prog.svg @@ -0,0 +1,1268 @@ + + + + + + + + 2026-03-17T14:36:23.406356 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/15.yaml b/tests/widish/15.yaml new file mode 100644 index 0000000..702cf84 --- /dev/null +++ b/tests/widish/15.yaml @@ -0,0 +1,2 @@ +? foo +? bar diff --git a/tests/widish/16.in b/tests/widish/16.in new file mode 100644 index 0000000..e69de29 diff --git a/tests/widish/16.mprog.svg b/tests/widish/16.mprog.svg new file mode 100644 index 0000000..098143c --- /dev/null +++ b/tests/widish/16.mprog.svg @@ -0,0 +1,689 @@ + + + + + + + + 2026-03-17T13:58:47.422711 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/16.out b/tests/widish/16.out new file mode 100644 index 0000000..fa5b8eb --- /dev/null +++ b/tests/widish/16.out @@ -0,0 +1 @@ +leftright \ No newline at end of file diff --git a/tests/widish/16.prog.svg b/tests/widish/16.prog.svg new file mode 100644 index 0000000..1be424d --- /dev/null +++ b/tests/widish/16.prog.svg @@ -0,0 +1,1322 @@ + + + + + + + + 2026-03-17T14:36:23.496097 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/16.yaml b/tests/widish/16.yaml new file mode 100644 index 0000000..4f152c6 --- /dev/null +++ b/tests/widish/16.yaml @@ -0,0 +1,2 @@ +? !printf left +? !printf right diff --git a/tests/widish/17.in b/tests/widish/17.in new file mode 100644 index 0000000..e31de1f --- /dev/null +++ b/tests/widish/17.in @@ -0,0 +1 @@ +seed diff --git a/tests/widish/17.mprog.svg b/tests/widish/17.mprog.svg new file mode 100644 index 0000000..c56bb39 --- /dev/null +++ b/tests/widish/17.mprog.svg @@ -0,0 +1,7399 @@ + + + + + + + + 2026-03-17T13:58:48.901517 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/17.out b/tests/widish/17.out new file mode 100644 index 0000000..64cece0 --- /dev/null +++ b/tests/widish/17.out @@ -0,0 +1 @@ +alphabetagammadeltaepsilonzetaetathetaiotakappalambdamunuxiomicronpirhosigmatauupsilonphichipsiomegaapexblazecrestdriftemberflare \ No newline at end of file diff --git a/tests/widish/17.prog.svg b/tests/widish/17.prog.svg new file mode 100644 index 0000000..a3dbaa9 --- /dev/null +++ b/tests/widish/17.prog.svg @@ -0,0 +1,17813 @@ + + + + + + + + 2026-03-17T14:36:43.311784 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/17.yaml b/tests/widish/17.yaml new file mode 100644 index 0000000..0bdfa7c --- /dev/null +++ b/tests/widish/17.yaml @@ -0,0 +1,92 @@ +- ? + ? !printf alpha + ? + - !printf beta + - !cat + ? + ? !printf gamma + ? !printf delta + : !cat + : !cat + ? + - !printf epsilon + - !cat + - !cat + : !cat + ? + ? !printf zeta + ? + - !printf eta + - !cat + ? + ? !printf theta + ? !printf iota + : !cat + : !cat + ? + - + ? !printf kappa + ? !printf lambda + : !cat + - !cat + : !cat +- !cat +- ? !cat + : !cat + ? + - !printf mu + - + ? !cat + ? !printf nu + ? !printf xi + - !cat + : !cat + ? + ? !printf omicron + ? !printf pi + ? + - !printf rho + - !cat + : !cat + ? + - + ? !printf sigma + ? + ? !printf tau + ? !printf upsilon + : !cat + : !cat + - !cat + : !cat +- !cat +- ? !cat + : !cat + ? + ? !printf phi + ? + - !printf chi + - !cat + ? + ? !printf psi + ? !printf omega + : !cat + : !cat + ? + - + ? !printf apex + ? !printf blaze + : !cat + - !cat + : !cat + ? + ? !printf crest + ? + - !printf drift + - !cat + ? + ? !printf ember + ? !printf flare + : !cat + : !cat +- !cat +- !cat diff --git a/widip/comput/__init__.py b/widip/comput/__init__.py index fef23b2..e5c1fc8 100644 --- a/widip/comput/__init__.py +++ b/widip/comput/__init__.py @@ -1 +1,61 @@ -"""Textbook Chapter 2 elements for the Run language.""" +"""Chapter 2 computing structures and distinguished program languages.""" + +from . import computer +from .loader import loader_program_ty +from .widish import shell_program_ty +from ..state.core import Execution +from ..state.loader import LoaderExecution +from ..state.widish import ShellExecution + + +class MonoidalComputer(computer.Category): + """ + The ambient computer category may contain more than one program language type. + """ + + +class ProgramClosedCategory(MonoidalComputer): + """ + Sec. 8.3: a program-closed category chooses one distinguished program type. + """ + + def __init__(self, program_ty: computer.ProgramTy): + self.program_ty = program_ty + MonoidalComputer.__init__(self) + + def evaluator(self, A: computer.Ty, B: computer.Ty): + return computer.Computer(self.program_ty, A, B) + + def execution(self, A: computer.Ty, B: computer.Ty): + return Execution( + "{}", + self.program_ty, + A, + B, + ) + + +class LoaderLanguage(ProgramClosedCategory): + """Program-closed category for the YAML loader language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, loader_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + return LoaderExecution() + + +class ShellLanguage(ProgramClosedCategory): + """Program-closed category with the shell as distinguished language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, shell_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + return ShellExecution() + + +LOADER = LoaderLanguage() +SHELL = ShellLanguage() diff --git a/widip/comput/loader.py b/widip/comput/loader.py index 89d987e..f27ca0f 100644 --- a/widip/comput/loader.py +++ b/widip/comput/loader.py @@ -1,7 +1,5 @@ """Loader-language program constants.""" -import shlex - from . import computer @@ -15,37 +13,20 @@ def __init__(self, name: str): computer.Program.__init__(self, name, loader_program_ty, computer.Ty()) -class LoaderScalar(LoaderProgram): - """Closed loader program representing scalar content.""" - - def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": - raise NotImplementedError +class LoaderScalarProgram(LoaderProgram): + """Closed loader program representing YAML scalar content.""" -class LoaderEmpty(LoaderScalar): +class LoaderEmpty(LoaderScalarProgram): """Empty scalar in the loader language.""" def __init__(self): - LoaderScalar.__init__(self, repr("")) + LoaderScalarProgram.__init__(self, repr("")) - def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": - return LoaderCommand(program.argv) - -class LoaderLiteral(LoaderScalar): +class LoaderLiteral(LoaderScalarProgram): """Literal scalar in the loader language.""" def __init__(self, text: str): self.text = text - LoaderScalar.__init__(self, repr(text)) - - def partial_apply(self, program: "LoaderCommand") -> "LoaderCommand": - return LoaderCommand(program.argv + (self.text,)) - - -class LoaderCommand(LoaderProgram): - """Loader-language command program before backend interpretation.""" - - def __init__(self, argv): - self.argv = tuple(argv) - LoaderProgram.__init__(self, shlex.join(self.argv)) + LoaderScalarProgram.__init__(self, repr(text)) diff --git a/widip/comput/python.py b/widip/comput/python.py index 4c535c7..adc86e0 100644 --- a/widip/comput/python.py +++ b/widip/comput/python.py @@ -3,6 +3,9 @@ from . import computer +program_ty = computer.ProgramTy("python") + + class PythonComputationCategory(closed.Category, markov.Category): """""" diff --git a/widip/comput/widish.py b/widip/comput/widish.py index d7c5353..22d6057 100644 --- a/widip/comput/widish.py +++ b/widip/comput/widish.py @@ -1,8 +1,5 @@ """Shell-language program constants.""" -import shlex -import subprocess - from . import computer from ..wire.widish import io_ty @@ -16,9 +13,6 @@ class ShellProgram(computer.Program): def __init__(self, name: str): computer.Program.__init__(self, name, shell_program_ty, computer.Ty()) - def run(self, stdin: str) -> str: - raise NotImplementedError - class ScalarProgram(ShellProgram): """Closed shell program representing scalar content.""" @@ -33,9 +27,6 @@ class Empty(ScalarProgram): def __init__(self): ScalarProgram.__init__(self, repr("")) - def run(self, stdin: str) -> str: - return stdin - def partial_apply(self, program: "Command") -> "Command": return Command(program.argv) @@ -47,41 +38,13 @@ def __init__(self, text: str): self.text = text ShellProgram.__init__(self, repr(text)) - def run(self, stdin: str) -> str: - del stdin - return self.text - def partial_apply(self, program: "Command") -> "Command": return Command(program.argv + (self.text,)) class Command(ShellProgram): - """Closed POSIX command shell program.""" + """Closed POSIX command shell program data.""" def __init__(self, argv): self.argv = tuple(argv) - ShellProgram.__init__(self, shlex.join(self.argv)) - - def run(self, stdin: str) -> str: - completed = subprocess.run( - self.argv, - input=stdin, - text=True, - capture_output=True, - check=True, - ) - return completed.stdout - - -class ShellStateUpdate(computer.Box): - """State projection box for shell execution.""" - - def __init__(self): - computer.Box.__init__(self, "sta(shell)", shell_program_ty @ io_ty, shell_program_ty) - - -class ShellOutput(computer.Box): - """Observable-output box for shell execution.""" - - def __init__(self): - computer.Box.__init__(self, "out(shell)", shell_program_ty @ io_ty, io_ty) + ShellProgram.__init__(self, repr(self.argv)) diff --git a/widip/files.py b/widip/files.py index e5cbff2..b8328aa 100644 --- a/widip/files.py +++ b/widip/files.py @@ -1,7 +1,7 @@ import pathlib from .comput.computer import Box, Diagram -from .loader import repl_read +from .metaprog import repl_read def files_ar(ar: Box) -> Diagram: diff --git a/widip/hif.py b/widip/hif.py deleted file mode 100644 index a2386f4..0000000 --- a/widip/hif.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Helpers for traversing YAML hypergraphs produced by nx_yaml.""" - -from nx_hif.hif import HyperGraph, hif_edge_incidences, hif_node, hif_node_incidences - - -def node_data(graph: HyperGraph, node): - """Return the attribute dict of a hypergraph node.""" - return hif_node(graph, node) - - -def node_kind(graph: HyperGraph, node): - """Return the YAML node kind for a hypergraph node.""" - return node_data(graph, node)["kind"] - - -def node_tag(graph: HyperGraph, node): - """Return the YAML tag without the leading !, if present.""" - return (node_data(graph, node).get("tag") or "")[1:] - - -def node_value(graph: HyperGraph, node): - """Return the scalar payload, defaulting to the empty string.""" - return node_data(graph, node).get("value", "") - - -def node_edges(graph: HyperGraph, node, *, key="next", direction="head"): - """Return incidences leaving a node for the requested key/direction.""" - return tuple(hif_node_incidences(graph, node, key=key, direction=direction)) - - -def edge_nodes(graph: HyperGraph, edge, *, key="start", direction="head"): - """Return incidences from an edge to its incident nodes.""" - return tuple(hif_edge_incidences(graph, edge, key=key, direction=direction)) - - -def successor_nodes(graph: HyperGraph, node, *, edge_key="next", node_key="start"): - """ - Yield nodes reached by following incidences of type edge_key then node_key. - """ - for edge, _, _, _ in node_edges(graph, node, key=edge_key): - for _, target, _, _ in edge_nodes(graph, edge, key=node_key): - yield target - - -def first_successor_node(graph: HyperGraph, node, *, edge_key="next", node_key="start"): - """Return the first successor node for a given incidence pattern, if any.""" - return next(iter(successor_nodes(graph, node, edge_key=edge_key, node_key=node_key)), None) - - -def stream_document_nodes(graph: HyperGraph, stream=0): - """Yield document nodes contained in a YAML stream node.""" - yield from successor_nodes(graph, stream, edge_key="next", node_key="start") - - -def document_root_node(graph: HyperGraph, document): - """Return the root YAML node for a document node, if any.""" - return first_successor_node(graph, document, edge_key="next", node_key="start") - - -def sequence_item_nodes(graph: HyperGraph, sequence): - """Yield sequence items in order by following next/forward links.""" - current = first_successor_node(graph, sequence, edge_key="next", node_key="start") - while current is not None: - yield current - current = first_successor_node(graph, current, edge_key="forward", node_key="start") - - -def mapping_entry_nodes(graph: HyperGraph, mapping): - """ - Yield `(key_node, value_node)` pairs in order for a YAML mapping node. - """ - key_node = first_successor_node(graph, mapping, edge_key="next", node_key="start") - while key_node is not None: - value_node = first_successor_node(graph, key_node, edge_key="forward", node_key="start") - if value_node is None: - break - yield key_node, value_node - key_node = first_successor_node(graph, value_node, edge_key="forward", node_key="start") diff --git a/widip/loader.py b/widip/loader.py deleted file mode 100644 index 642c91a..0000000 --- a/widip/loader.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Load YAML hypergraphs into loader diagrams and compile them to shell.""" - -from nx_yaml import nx_compose_all - -from .comput.loader import LoaderCommand, LoaderEmpty, LoaderLiteral -from .hif import ( - HyperGraph, - document_root_node, - mapping_entry_nodes, - node_kind, - node_tag, - node_value, - sequence_item_nodes, - stream_document_nodes, -) -from .metaprog import LOADER_TO_SHELL -from .state.loader import run_loader_program -from .wire.loader import LoaderMapping, LoaderSequence, loader_id, pipeline - - -def repl_read(stream): - """Parse a YAML stream and compile it to the shell backend.""" - incidences = nx_compose_all(stream) - return LOADER_TO_SHELL(incidences_to_program(incidences)) - - -def incidences_to_program(node: HyperGraph): - """Turn an ``nx_yaml`` hypergraph into a loader-language diagram.""" - return _incidences_to_program(node, 0) - - -def _incidences_to_program(node: HyperGraph, index): - kind = node_kind(node, index) - tag = node_tag(node, index) - - match kind: - case "stream": - diagram = load_stream(node, index) - case "document": - diagram = load_document(node, index) - case "scalar": - return load_scalar(node, index, tag) - case "sequence": - if tag: - return load_tagged_sequence(node, index, tag) - diagram = load_sequence(node, index) - case "mapping": - if tag: - return load_tagged_mapping(node, index, tag) - diagram = load_mapping(node, index) - case _: - raise ValueError(f"unsupported YAML node kind: {kind!r}") - - if tag: - diagram = diagram >> run_loader_program(LoaderCommand((tag,))) - return diagram - - -def load_scalar(node, index, tag): - """Scalars lower directly to runnable loader diagrams.""" - value = node_value(node, index) - if not value and not tag: - return loader_id() - - scalar = LoaderLiteral(value) if value else LoaderEmpty() - if tag: - scalar = scalar.partial_apply(LoaderCommand((tag,))) - return run_loader_program(scalar) - - -def load_mapping(node, index): - """Mappings denote parallel branches in the loader language.""" - branches = [] - for key_node, value_node in mapping_entry_nodes(node, index): - key = _incidences_to_program(node, key_node) - value = _incidences_to_program(node, value_node) - branches.append(pipeline((key, value))) - return LoaderMapping(branches) - - -def load_sequence(node, index): - """Sequences denote shell-style pipelines in the loader language.""" - stages = tuple(_incidences_to_program(node, child) for child in sequence_item_nodes(node, index)) - return LoaderSequence(stages) - - -def argv_item(diagram): - """Normalize one loader child diagram as one newline-delimited argv item.""" - return diagram >> run_loader_program(LoaderCommand(("xargs", "printf", "%s\n"))) - - -def load_tagged_mapping(node, index, tag): - """Tagged mappings treat each branch as an argv-supplying process.""" - branches = [] - for key_node, value_node in mapping_entry_nodes(node, index): - key = _incidences_to_program(node, key_node) - value = _incidences_to_program(node, value_node) - branches.append(argv_item(pipeline((key, value)))) - return LoaderMapping(branches) >> run_loader_program(LoaderCommand(("xargs", tag))) - - -def load_tagged_sequence(node, index, tag): - """Tagged sequences treat each item as an argv-supplying process.""" - branches = tuple(argv_item(_incidences_to_program(node, child)) for child in sequence_item_nodes(node, index)) - return LoaderMapping(branches) >> run_loader_program(LoaderCommand(("xargs", tag))) - - -def load_document(node, index): - """A document is its root node, if present.""" - root = document_root_node(node, index) - if root is None: - return loader_id() - return _incidences_to_program(node, root) - - -def load_stream(node, index): - """A stream is the pipeline of its documents.""" - documents = tuple(_incidences_to_program(node, child) for child in stream_document_nodes(node, index)) - return pipeline(documents) diff --git a/widip/metaprog/__init__.py b/widip/metaprog/__init__.py index 3fd5b33..9a09773 100644 --- a/widip/metaprog/__init__.py +++ b/widip/metaprog/__init__.py @@ -2,8 +2,11 @@ Chapter 6. Computing programs. Metaprograms are programs that compute programs. """ +from nx_yaml import nx_compose_all -from ..comput.computer import Box, Computer, ComputableFunction, Diagram, Program, ProgramTy, Ty +from ..comput.computer import Box, Computer, ComputableFunction, Diagram, Functor, Program, ProgramTy, Ty +from ..state.widish import ShellRunner +from ..wire.hif import HyperGraph class Metaprogram(Box): @@ -15,6 +18,34 @@ def __init__(self, name, P: ProgramTy): Box.__init__(self, name, dom=Ty(), cod=P) +class Specializer(Functor): + """A functorial metaprogram with unit parameter type.""" + + @staticmethod + def metaprogram_dom(): + return Ty() + + def __init__(self, ob=None, ar=None, *, dom=None, cod=None): + Functor.__init__( + self, + self.ob_map if ob is None else ob, + self.ar_map if ar is None else ar, + dom=Functor.dom if dom is None else dom, + cod=Functor.cod if cod is None else cod, + ) + + @staticmethod + def ob_map(ob): + return ob + + @staticmethod + def ar_map(ar): + return ar + + def specialize(self, *args, **kwargs): + return self(*args, **kwargs) + + class ProgramComputation(Diagram): """ Section 3.1: a function is computable when it is programmable. @@ -94,23 +125,22 @@ def __call__(self, other): return other -def __getattr__(name): - if name == "SHELL_SPECIALIZER": - from .widish import ShellSpecializer +from .hif import HIFToLoader +from .loader import LoaderToShell +from .widish import ShellSpecializer + + +SHELL_SPECIALIZER = ShellSpecializer() +SHELL_TO_PYTHON = ShellRunner(SHELL_SPECIALIZER) +HIF_TO_LOADER = HIFToLoader() +LOADER_TO_SHELL = LoaderToShell() + - value = ShellSpecializer() - globals()[name] = value - return value - if name == "SHELL_RUNNER": - from .widish import ShellRunner +def incidences_to_program(graph: HyperGraph): + """Turn an ``nx_yaml`` hypergraph into a loader-language diagram.""" + return HIF_TO_LOADER(graph) - value = ShellRunner() - globals()[name] = value - return value - if name == "LOADER_TO_SHELL": - from .loader import LoaderToShell - value = LoaderToShell() - globals()[name] = value - return value - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def repl_read(stream): + """Parse a YAML stream and compile it to the shell backend.""" + return LOADER_TO_SHELL(incidences_to_program(nx_compose_all(stream))) diff --git a/widip/metaprog/hif.py b/widip/metaprog/hif.py new file mode 100644 index 0000000..bde70ff --- /dev/null +++ b/widip/metaprog/hif.py @@ -0,0 +1,73 @@ +"""HIF-specific specializers and lowerings.""" + +from ..comput.computer import Ty +from ..state.hif import ( + document_root_node, + mapping_entry_nodes, + sequence_item_nodes, + stream_document_nodes, +) +from ..wire.hif import HyperGraph, hif_node +from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline + + +class HIFSpecializer: + """Recursive structural lowering over YAML HIF nodes.""" + + @staticmethod + def metaprogram_dom(): + return Ty() + + def specialize(self, graph: HyperGraph, node=0): + node_data = hif_node(graph, node) + kind = node_data["kind"] + tag = (node_data.get("tag") or "")[1:] or None + + match kind: + case "stream": + value = tuple(self.specialize(graph, child) for child in stream_document_nodes(graph, node)) + case "document": + root = document_root_node(graph, node) + value = None if root is None else self.specialize(graph, root) + case "scalar": + value = node_data.get("value", "") + case "sequence": + value = tuple(self.specialize(graph, child) for child in sequence_item_nodes(graph, node)) + case "mapping": + value = tuple( + ( + self.specialize(graph, key_node), + self.specialize(graph, value_node), + ) + for key_node, value_node in mapping_entry_nodes(graph, node) + ) + case _: + raise ValueError(f"unsupported YAML node kind: {kind!r}") + return self.node_map(graph, node, kind, value, tag) + + def __call__(self, graph: HyperGraph, node=0): + return self.specialize(graph, node) + + def node_map(self, graph: HyperGraph, node, kind: str, value, tag: str | None): + raise NotImplementedError + + +class HIFToLoader(HIFSpecializer): + """Specialize YAML HIF structure to loader-language diagrams.""" + + def node_map(self, graph: HyperGraph, node, kind: str, value, tag: str | None): + del graph, node + match kind: + case "stream": + return pipeline(value) + case "document": + return loader_id() if value is None else value + case "scalar": + return LoaderScalar(value, tag) + case "sequence": + return LoaderSequence(value, tag=tag) + case "mapping": + branches = tuple(pipeline((key, entry_value)) for key, entry_value in value) + return LoaderMapping(branches, tag=tag) + case _: + raise ValueError(f"unsupported YAML node kind: {kind!r}") diff --git a/widip/metaprog/loader.py b/widip/metaprog/loader.py index 8bd2a96..0532388 100644 --- a/widip/metaprog/loader.py +++ b/widip/metaprog/loader.py @@ -1,18 +1,31 @@ """Loader-specific program transformations.""" +from . import Specializer from ..comput import computer from ..comput import loader as loader_lang from ..comput import widish as shell_lang -from ..state import loader as loader_state +from ..state.core import map_process_box +from ..state.widish import shell_stage as shell_io_stage from ..wire import loader as loader_wire +from ..wire import widish as shell_wire from .widish import Parallel, Pipeline -class LoaderToShell(computer.Functor): - """Compile loader programs and execution boxes into shell diagrams.""" +def _compile_scalar(node: loader_wire.LoaderScalar): + """Compile one YAML scalar node to the shell backend.""" + if node.tag: + argv = (node.tag,) if not node.value else (node.tag, node.value) + return shell_io_stage(shell_lang.Command(argv)) + if not node.value: + return shell_wire.shell_id() + return shell_io_stage(shell_lang.Literal(node.value)) + + +class LoaderToShell(Specializer): + """Compile loader nodes, programs, and execution boxes into shell diagrams.""" def __init__(self): - computer.Functor.__init__( + Specializer.__init__( self, self.ob_map, self.ar_map, @@ -29,22 +42,22 @@ def ob_map(ob): return ob def __call__(self, other): + if isinstance(other, loader_wire.LoaderScalar): + return _compile_scalar(other) if isinstance(other, loader_wire.LoaderSequence): + if other.tag is not None: + raise TypeError(f"tagged YAML sequences are unsupported: !{other.tag}") return Pipeline(tuple(self(stage) for stage in other.stages)) if isinstance(other, loader_wire.LoaderMapping): + if other.tag is not None: + raise TypeError(f"tagged YAML mappings are unsupported: !{other.tag}") return Parallel(tuple(self(branch) for branch in other.branches)) - return computer.Functor.__call__(self, other) + return Specializer.__call__(self, other) - @staticmethod - def ar_map(ar): + def ar_map(self, ar): + ar = map_process_box(ar, self.ob_map) if isinstance(ar, loader_lang.LoaderEmpty): return shell_lang.Empty() if isinstance(ar, loader_lang.LoaderLiteral): return shell_lang.Literal(ar.text) - if isinstance(ar, loader_lang.LoaderCommand): - return shell_lang.Command(ar.argv) - if isinstance(ar, loader_state.LoaderStateUpdate): - return shell_lang.ShellStateUpdate() - if isinstance(ar, loader_state.LoaderOutput): - return shell_lang.ShellOutput() return ar diff --git a/widip/metaprog/widish.py b/widip/metaprog/widish.py index 96dfddb..1e69a3c 100644 --- a/widip/metaprog/widish.py +++ b/widip/metaprog/widish.py @@ -1,9 +1,13 @@ """Shell-specific program transformations and interpreters.""" -from discopy import monoidal, python +from itertools import count +from discopy import monoidal + +from . import Specializer from ..comput import computer from ..comput import widish as shell_lang +from ..state.widish import parallel_io_diagram from ..wire import widish as shell_wire @@ -29,19 +33,47 @@ def _tensor_all(diagrams): return result -def _shell_stage(program): - """Run a primitive shell program through the standard shell evaluator.""" - return program @ shell_lang.io_ty >> shell_lang.ShellOutput() +def _specialize_shell(diagram, next_temp): + """Recursively lower shell bubbles using one shared temp-path allocator.""" + if isinstance(diagram, Pipeline): + return diagram.specialize(next_temp) + if isinstance(diagram, Parallel): + return diagram.specialize(next_temp) + if isinstance(diagram, monoidal.Bubble): + return _specialize_shell(diagram.arg, next_temp) + if isinstance(diagram, computer.Box): + return diagram + if isinstance(diagram, computer.Diagram): + result = computer.Id(diagram.dom) + for left, box, right in diagram.inside: + result = result >> left @ _specialize_shell(box, next_temp) @ right + return result + return diagram + + +class ShellSpecializer(Specializer): + """Lower shell bubbles to their executable wiring.""" + def __init__(self): + self._next_temp = count() + Specializer.__init__( + self, + self.ob_map, + self.ar_map, + dom=computer.Category(), + cod=computer.Category(), + ) -def _parallel_diagram(branches): - """Lower a parallel bubble to compact shell wiring.""" - branches = tuple(branches) - if not branches: - return shell_wire.shell_id() - if len(branches) == 1: - return branches[0] - return shell_wire.Copy(len(branches)) >> _tensor_all(branches) >> shell_wire.Merge(len(branches)) + @staticmethod + def ob_map(ob): + return ob + + def __call__(self, other): + return _specialize_shell(other, self._next_temp) + + @staticmethod + def ar_map(ar): + return ar class Pipeline(monoidal.Bubble, computer.Box): @@ -49,18 +81,18 @@ class Pipeline(monoidal.Bubble, computer.Box): def __init__(self, stages): self.stages = tuple(stages) - arg = _pipeline_diagram(self.stages) monoidal.Bubble.__init__( self, - arg, + _pipeline_diagram(self.stages), dom=shell_lang.io_ty, cod=shell_lang.io_ty, draw_vertically=True, drawing_name="seq", ) - def specialize(self): - return _pipeline_diagram(tuple(specialize_shell(stage) for stage in self.stages)) + def specialize(self, next_temp=None): + next_temp = count() if next_temp is None else next_temp + return _pipeline_diagram(tuple(_specialize_shell(stage, next_temp) for stage in self.stages)) class Parallel(monoidal.Bubble, computer.Box): @@ -68,21 +100,20 @@ class Parallel(monoidal.Bubble, computer.Box): def __init__(self, branches): self.branches = tuple(branches) - arg = _tensor_all(self.branches) if self.branches else shell_wire.shell_id() monoidal.Bubble.__init__( self, - arg, + _tensor_all(self.branches) if self.branches else shell_wire.shell_id(), dom=shell_lang.io_ty, cod=shell_lang.io_ty, drawing_name="map", ) - def specialize(self): - return _parallel_diagram(tuple(specialize_shell(branch) for branch in self.branches)) - - -Sequence = Pipeline -Mapping = Parallel + def specialize(self, next_temp=None): + next_temp = count() if next_temp is None else next_temp + return parallel_io_diagram( + tuple(_specialize_shell(branch, next_temp) for branch in self.branches), + next_temp, + ) def pipeline(stages): @@ -99,91 +130,3 @@ def parallel(branches): if not branches: return shell_wire.shell_id() return Parallel(branches) - - -class ShellSpecializer(computer.Functor): - """Lower shell bubbles to their executable wiring.""" - - def __init__(self): - computer.Functor.__init__(self, self.ob_map, self.ar_map, dom=computer.Category(), cod=computer.Category()) - - @staticmethod - def ob_map(ob): - return ob - - def __call__(self, other): - if isinstance(other, Pipeline): - return _pipeline_diagram(tuple(self(stage) for stage in other.stages)) - if isinstance(other, Parallel): - return _parallel_diagram(tuple(self(branch) for branch in other.branches)) - if isinstance(other, monoidal.Bubble): - return self(other.arg) - return computer.Functor.__call__(self, other) - - @staticmethod - def ar_map(ar): - return ar - - -def specialize_shell(diagram: computer.Diagram) -> computer.Diagram: - """Recursively lower shell bubbles to plain shell wiring.""" - return ShellSpecializer()(diagram) - - -class ShellRunner(monoidal.Functor): - """Interpret shell diagrams as Python callables over stateful text streams.""" - - def __init__(self): - monoidal.Functor.__init__( - self, - ob=self.ob_map, - ar=self.ar_map, - dom=computer.Category(), - cod=python.Category(), - ) - - @staticmethod - def ob_map(ob): - if ( - isinstance(ob, computer.Ty) - and len(ob) == 1 - and isinstance(ob.inside[0], computer.ProgramOb) - ): - return shell_lang.ShellProgram - return str - - def __call__(self, box): - if isinstance(box, Pipeline): - return self(box.specialize()) - if isinstance(box, Parallel): - return self(box.specialize()) - if isinstance(box, monoidal.Bubble): - return self(box.arg) - return monoidal.Functor.__call__(self, box) - - def ar_map(self, box): - dom, cod = self(box.dom), self(box.cod) - - if isinstance(box, shell_lang.ShellProgram): - return python.Function(lambda: box, dom, cod) - if isinstance(box, shell_lang.ShellStateUpdate): - return python.Function(lambda program, _stdin: program, dom, cod) - if isinstance(box, shell_lang.ShellOutput): - return python.Function(lambda program, stdin: program.run(stdin), dom, cod) - if isinstance(box, computer.Copy): - return python.Function.copy(dom, n=2) - if isinstance(box, shell_wire.Merge): - return python.Function(lambda *parts: "".join(parts), dom, cod) - if isinstance(box, computer.Delete): - return python.Function.discard(dom) - if isinstance(box, computer.Swap): - return python.Function.swap(self(box.left), self(box.right)) - - raise TypeError(f"unsupported shell box: {box!r}") - - -def compile_shell_program(diagram: computer.Diagram) -> python.Function: - """Compile a shell diagram into an executable Python function.""" - from . import SHELL_RUNNER - - return SHELL_RUNNER(diagram) diff --git a/widip/state/__init__.py b/widip/state/__init__.py index db467e5..5cd0b69 100644 --- a/widip/state/__init__.py +++ b/widip/state/__init__.py @@ -1,149 +1,34 @@ """Chapter 7: Stateful computing.""" -from ..comput import computer +from .core import ( + Execution, + InputOutputMap, + Process, + ProcessRunner, + StateUpdateMap, + execute, + fixed_state, + simulate, +) +from .loader import LoaderExecution +from .widish import ShellExecution -class Process(computer.Diagram): - """ - Eq. 7.1: a process q : X x A -> X x B is paired from state update and output. - """ +def loader_state_update(): + """The loader execution state-update map sta(loader).""" + return LoaderExecution().state_update_diagram() - def __init__( - self, - name, - X: computer.Ty, - A: computer.Ty, - B: computer.Ty, - state_update_diagram=None, - output_diagram=None, - ): - self.name, self.X, self.A, self.B = name, X, A, B - self.state_update_diagram = ( - state_update_diagram - if state_update_diagram is not None - else computer.Box(f"sta({name})", X @ A, X) - ) - self.output_diagram = ( - output_diagram - if output_diagram is not None - else computer.Box(f"out({name})", X @ A, B) - ) - diagram = ( - computer.Copy(X @ A), - self.state_update_diagram @ self.output_diagram, - ) - inside = sum((d.inside for d in diagram), ()) - computer.Diagram.__init__(self, inside, X @ A, X @ B) +def loader_output(): + """The loader execution output map out(loader).""" + return LoaderExecution().output_diagram() - def sta(self): - return self.state_update_diagram - def out(self): - return self.output_diagram +def shell_state_update(): + """The shell execution state-update map sta(shell).""" + return ShellExecution().state_update_diagram() -class Execution(Process): - """ - Sec. 7.3: program execution is a process P x A -> P x B. - """ - - def __init__( - self, - P: computer.ProgramTy, - A: computer.Ty, - B: computer.Ty, - universal_ev_diagram=None, - state_update_diagram=None, - output_diagram=None, - ): - self.universal_ev_diagram = ( - universal_ev_diagram - if universal_ev_diagram is not None - else computer.Computer(P, A, P @ B) - ) - Process.__init__( - self, - "{}", - P, - A, - B, - state_update_diagram=state_update_diagram, - output_diagram=output_diagram, - ) - - def universal_ev(self): - """ - Eq. 7.3: program execution is the evaluator with output type P x B. - """ - return self.universal_ev_diagram - - def specialize(self): - return self.universal_ev() - - -class MonoidalComputer(computer.Category): - """ - The ambient computer category may contain more than one program language type. - """ - - -class ProgramClosedCategory(MonoidalComputer): - """ - Sec. 8.3: a program-closed category chooses one distinguished program type. - """ - - def __init__(self, program_ty: computer.ProgramTy): - self.program_ty = program_ty - MonoidalComputer.__init__(self) - - def evaluator(self, A: computer.Ty, B: computer.Ty): - return computer.Computer(self.program_ty, A, B) - - def execution(self, A: computer.Ty, B: computer.Ty): - return Execution(self.program_ty, A, B) - - -def sta(q): - """Projection to the state-update component of a process.""" - if hasattr(q, "sta") and callable(q.sta): - return q.sta() - raise TypeError("sta expects a state.Process") - - -def out(q): - """Projection to the output component of a process.""" - if hasattr(q, "out") and callable(q.out): - return q.out() - raise TypeError("out expects a state.Process") - - -def simulate(q: Process, s: computer.Diagram): - """ - Fig. 7.2: a simulation along s is postcomposition with s x id_B. - """ - return q >> s @ q.B - - -def execute(Q: computer.Diagram, A: computer.Ty, B: computer.Ty): - """ - Sec. 7.3: execute an X-parameterized program as a stateful process. - """ - return Q @ A >> Execution(Q.cod, A, B) - - -def fixed_state(g: computer.Diagram): - """ - Sec. 7.4 proof b: lift g : X x A -> B to the fixed-state process X x A -> X x B. - """ - X = g.dom[:1] - A = g.dom[1:] - return computer.Copy(X) @ A >> X @ g - - -from .loader import LoaderLanguage -from .widish import ShellLanguage - - -LOADER = LoaderLanguage() -SHELL = ShellLanguage() +def shell_output(): + """The shell execution output map out(shell).""" + return ShellExecution().output_diagram() diff --git a/widip/state/core.py b/widip/state/core.py new file mode 100644 index 0000000..a5c5cc6 --- /dev/null +++ b/widip/state/core.py @@ -0,0 +1,173 @@ +"""Generic Chapter 7 stateful process structure.""" + +from discopy import monoidal, python + +from ..comput import computer + + +class StateUpdateMap(computer.Box): + """The Eq. 7.1 state-update projection sta(q) : X x A -> X.""" + + def __init__(self, name: str, X: computer.Ty, A: computer.Ty): + self.process_name, self.X, self.A = name, X, A + computer.Box.__init__(self, f"sta({name})", X @ A, X) + + +class InputOutputMap(computer.Box): + """The Eq. 7.1 output projection out(q) : X x A -> B.""" + + def __init__(self, name: str, X: computer.Ty, A: computer.Ty, B: computer.Ty): + self.process_name, self.X, self.A, self.B = name, X, A, B + computer.Box.__init__(self, f"out({name})", X @ A, B) + + +class Process(computer.Diagram): + """ + Eq. 7.1: a process q : X x A -> X x B is paired from state update and output. + """ + + def __init__( + self, + name, + X: computer.Ty, + A: computer.Ty, + B: computer.Ty, + ): + self.name, self.X, self.A, self.B = name, X, A, B + + diagram = ( + computer.Copy(X @ A), + self.state_update_diagram() @ self.output_diagram(), + ) + inside = sum((d.inside for d in diagram), ()) + computer.Diagram.__init__(self, inside, X @ A, X @ B) + + def state_update_diagram(self): + """Eq. 7.1 state-update component.""" + return StateUpdateMap(self.name, self.X, self.A) + + def output_diagram(self): + """Eq. 7.1 output component.""" + return InputOutputMap(self.name, self.X, self.A, self.B) + + +class Execution(Process): + """ + Sec. 7.3: program execution is a process P x A -> P x B. + """ + + def __init__( + self, + name: str, + P: computer.ProgramTy, + A: computer.Ty, + B: computer.Ty, + ): + Process.__init__( + self, + name, + P, + A, + B, + ) + + def universal_ev(self): + """ + Eq. 7.3: program execution is the evaluator with output type P x B. + """ + return computer.Computer(self.X, self.A, self.X @ self.B) + + def specialize(self): + return self.universal_ev() + + +class ProcessRunner(monoidal.Functor): + """Python interpretation of the generic Eq. 7.1 process projections.""" + + def __init__(self, ob): + monoidal.Functor.__init__( + self, + ob=ob, + ar=self.ar_map, + dom=computer.Category(), + cod=python.Category(), + ) + + def process_ar_map(self, box, dom, cod): + """Interpret the non-state-specific boxes of a process diagram.""" + raise TypeError(f"unsupported process box: {box!r}") + + @staticmethod + def _state_update(state, _input): + return state + + @staticmethod + def _output(state, input_value): + return state(input_value) + + def projection_ar_map(self, box, dom, cod): + """Interpret the generic Eq. 7.1 state projections.""" + if isinstance(box, StateUpdateMap): + return python.Function(self._state_update, dom, cod) + if isinstance(box, InputOutputMap): + return python.Function(self._output, dom, cod) + return None + + def structural_ar_map(self, box, dom, cod): + """Interpret the generic structural boxes of process diagrams.""" + del cod + if isinstance(box, computer.Copy): + return python.Function.copy(dom, n=2) + if isinstance(box, computer.Delete): + return python.Function.discard(dom) + if isinstance(box, computer.Swap): + return python.Function.swap(self(box.left), self(box.right)) + return None + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + projection = self.projection_ar_map(box, dom, cod) + structural = self.structural_ar_map(box, dom, cod) + + if projection is not None: + return projection + if structural is not None: + return structural + return self.process_ar_map(box, dom, cod) + + +def map_process_box(box, ob): + """Transport a process projection box along an object mapping.""" + if isinstance(box, StateUpdateMap): + return StateUpdateMap(box.process_name, ob(box.X), ob(box.A)) + if isinstance(box, InputOutputMap): + return InputOutputMap(box.process_name, ob(box.X), ob(box.A), ob(box.B)) + return box + + +def simulate(q: Process, s: computer.Diagram): + """ + Fig. 7.2: a simulation along s is postcomposition with s x id_B. + """ + return q >> s @ q.B + + +def execute(Q: computer.Diagram, A: computer.Ty, B: computer.Ty): + """ + Sec. 7.3: execute an X-parameterized program as a stateful process. + """ + return Q @ A >> Execution( + "{}", + Q.cod, + A, + B, + ) + + +def fixed_state(g: computer.Diagram): + """ + Sec. 7.4 proof b: lift g : X x A -> B to the fixed-state process X x A -> X x B. + """ + X = g.dom[:1] + A = g.dom[1:] + return computer.Copy(X) @ A >> X @ g diff --git a/widip/state/hif.py b/widip/state/hif.py new file mode 100644 index 0000000..3b106c7 --- /dev/null +++ b/widip/state/hif.py @@ -0,0 +1,47 @@ +"""Ordered traversal over YAML documents encoded as HIF.""" + +from ..wire.hif import HyperGraph, hif_edge_incidences, hif_node_incidences + + +def _successor_nodes(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """Yield successor nodes reached by one edge-node incidence pattern.""" + for edge, _, _, _ in hif_node_incidences(graph, node, key=edge_key, direction="head"): + for _, target, _, _ in hif_edge_incidences(graph, edge, key=node_key, direction="head"): + yield target + + +def _first_successor_node(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """Return the first successor node for a given incidence pattern, if any.""" + return next(iter(_successor_nodes(graph, node, edge_key=edge_key, node_key=node_key)), None) + + +def stream_document_nodes(graph: HyperGraph, stream=0): + """Yield stream documents in order by following next/forward links.""" + current = _first_successor_node(graph, stream, edge_key="next", node_key="start") + while current is not None: + yield current + current = _first_successor_node(graph, current, edge_key="forward", node_key="start") + + +def document_root_node(graph: HyperGraph, document): + """Return the root YAML node for a document node, if any.""" + return _first_successor_node(graph, document, edge_key="next", node_key="start") + + +def sequence_item_nodes(graph: HyperGraph, sequence): + """Yield sequence items in order by following next/forward links.""" + current = _first_successor_node(graph, sequence, edge_key="next", node_key="start") + while current is not None: + yield current + current = _first_successor_node(graph, current, edge_key="forward", node_key="start") + + +def mapping_entry_nodes(graph: HyperGraph, mapping): + """Yield `(key_node, value_node)` pairs in order for a YAML mapping node.""" + key_node = _first_successor_node(graph, mapping, edge_key="next", node_key="start") + while key_node is not None: + value_node = _first_successor_node(graph, key_node, edge_key="forward", node_key="start") + if value_node is None: + break + yield key_node, value_node + key_node = _first_successor_node(graph, value_node, edge_key="forward", node_key="start") diff --git a/widip/state/loader.py b/widip/state/loader.py index 3570494..ceac9b6 100644 --- a/widip/state/loader.py +++ b/widip/state/loader.py @@ -1,23 +1,8 @@ """Loader-specific stateful execution.""" -from ..comput import computer from ..comput.loader import loader_program_ty from ..wire.loader import loader_stream_ty -from . import Execution, ProgramClosedCategory, out - - -class LoaderStateUpdate(computer.Box): - """State projection box for loader execution.""" - - def __init__(self): - computer.Box.__init__(self, "sta(loader)", loader_program_ty @ loader_stream_ty, loader_program_ty) - - -class LoaderOutput(computer.Box): - """Observable-output box for loader execution.""" - - def __init__(self): - computer.Box.__init__(self, "out(loader)", loader_program_ty @ loader_stream_ty, loader_stream_ty) +from .core import Execution class LoaderExecution(Execution): @@ -26,37 +11,8 @@ class LoaderExecution(Execution): def __init__(self): Execution.__init__( self, + "loader", loader_program_ty, loader_stream_ty, loader_stream_ty, - universal_ev_diagram=computer.Computer( - loader_program_ty, - loader_stream_ty, - loader_program_ty @ loader_stream_ty, - ), - state_update_diagram=LoaderStateUpdate(), - output_diagram=LoaderOutput(), ) - - -class LoaderLanguage(ProgramClosedCategory): - """Program-closed category for the YAML loader language.""" - - def __init__(self): - ProgramClosedCategory.__init__(self, loader_program_ty) - - def execution(self, A: computer.Ty, B: computer.Ty): - del A, B - return LoaderExecution() - - -def loader_execution() -> computer.Diagram: - """Projection to observable output of loader execution.""" - from . import LOADER - - return out(LOADER.execution(loader_stream_ty, loader_stream_ty)) - - -def run_loader_program(program: computer.Diagram) -> computer.Diagram: - """Execute a closed loader program on one loader stream wire.""" - return program @ loader_stream_ty >> loader_execution() diff --git a/widip/state/widish.py b/widip/state/widish.py index bf7600a..e5c6dfa 100644 --- a/widip/state/widish.py +++ b/widip/state/widish.py @@ -1,8 +1,116 @@ """Shell-specific stateful execution.""" +import subprocess +from collections.abc import Callable + +from discopy import monoidal, python + from ..comput import computer -from ..comput.widish import ShellOutput, ShellStateUpdate, io_ty, shell_program_ty -from . import Execution, ProgramClosedCategory +from ..comput import widish as shell_lang +from .core import Execution, InputOutputMap, ProcessRunner +from ..wire import widish as shell_wire + + +def shell_stage(program): + """Run one primitive shell program on the standard shell stream.""" + return program @ shell_lang.io_ty >> InputOutputMap( + "shell", + shell_lang.shell_program_ty, + shell_lang.io_ty, + shell_lang.io_ty, + ) + + +def _temp_path(next_temp) -> str: + """Allocate a deterministic temporary pathname for shell IO wiring.""" + return f"/tmp/widip-{next(next_temp):04d}.tmp" + + +def parallel_io_diagram(branches, next_temp): + """Lower shell-IO branching to file-backed tee/cat process wiring.""" + branches = tuple(branches) + if not branches: + return shell_wire.shell_id() + if len(branches) == 1: + return branches[0] + + input_path = _temp_path(next_temp) + output_paths = tuple(_temp_path(next_temp) for _ in branches) + stages = [shell_stage(shell_lang.Command(("tee", input_path)))] + + for branch, output_path in zip(branches, output_paths): + stages.extend( + ( + shell_stage(shell_lang.Command(("cat", input_path))), + branch, + shell_stage(shell_lang.Command(("tee", output_path))), + ) + ) + + stages.append(shell_stage(shell_lang.Command(("cat",) + output_paths))) + + result = shell_wire.shell_id() + for stage in stages: + result = stage if result == shell_wire.shell_id() else result >> stage + return result + + +def _has_shell_bubble(diagram) -> bool: + """Detect whether a shell diagram still contains unspecialized bubbles.""" + if isinstance(diagram, monoidal.Bubble): + return True + if not isinstance(diagram, computer.Diagram): + return False + return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) + + +class ShellRunner(ProcessRunner): + """Interpret stateful shell diagrams as Python callables on text streams.""" + + def __init__(self, specialize_shell): + self.specialize_shell = specialize_shell + ProcessRunner.__init__(self, self.ob_map) + + @staticmethod + def ob_map(ob): + if ( + isinstance(ob, computer.Ty) + and len(ob) == 1 + and isinstance(ob.inside[0], computer.ProgramOb) + ): + return Callable + return str + + def __call__(self, box): + if _has_shell_bubble(box): + return monoidal.Functor.__call__(self, self.specialize_shell(box)) + return monoidal.Functor.__call__(self, box) + + @staticmethod + def shell_program_runner(program): + """Compile one shell-language program to a Python text transformer.""" + if isinstance(program, shell_lang.Empty): + return lambda stdin: stdin + if isinstance(program, shell_lang.Literal): + return lambda _stdin: program.text + if isinstance(program, shell_lang.Command): + def run(stdin: str) -> str: + completed = subprocess.run( + program.argv, + input=stdin, + text=True, + capture_output=True, + check=True, + ) + return completed.stdout + + return run + raise TypeError(f"unsupported shell program: {program!r}") + + def process_ar_map(self, box, dom, cod): + if isinstance(box, shell_lang.ShellProgram): + return python.Function(lambda: self.shell_program_runner(box), dom, cod) + return ProcessRunner.process_ar_map(self, box, dom, cod) class ShellExecution(Execution): @@ -11,21 +119,8 @@ class ShellExecution(Execution): def __init__(self): Execution.__init__( self, - shell_program_ty, - io_ty, - io_ty, - universal_ev_diagram=computer.Computer(shell_program_ty, io_ty, shell_program_ty @ io_ty), - state_update_diagram=ShellStateUpdate(), - output_diagram=ShellOutput(), + "shell", + shell_lang.shell_program_ty, + shell_lang.io_ty, + shell_lang.io_ty, ) - - -class ShellLanguage(ProgramClosedCategory): - """Program-closed category with the shell as distinguished language.""" - - def __init__(self): - ProgramClosedCategory.__init__(self, shell_program_ty) - - def execution(self, A: computer.Ty, B: computer.Ty): - del A, B - return ShellExecution() diff --git a/widip/watch.py b/widip/watch.py index a301f18..87e159f 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -6,9 +6,8 @@ from discopy.utils import tuplify, untuplify -from .loader import repl_read from .files import diagram_draw, file_diagram -from .metaprog.widish import compile_shell_program +from .metaprog import SHELL_TO_PYTHON, repl_read # TODO watch functor ?? @@ -52,7 +51,7 @@ def shell_main(file_name, draw=True): if draw: diagram_draw(path, source_d) - result_ev = compile_shell_program(source_d)("") + result_ev = SHELL_TO_PYTHON(source_d)("") print(result_ev) except KeyboardInterrupt: print() @@ -69,7 +68,7 @@ def widish_main(file_name, draw): path = Path(file_name) if draw: diagram_draw(path, fd) - runner = compile_shell_program(fd) + runner = SHELL_TO_PYTHON(fd) run_res = runner("") if sys.stdin.isatty() else runner(sys.stdin.read()) diff --git a/widip/wire/hif.py b/widip/wire/hif.py new file mode 100644 index 0000000..83ece4f --- /dev/null +++ b/widip/wire/hif.py @@ -0,0 +1,11 @@ +"""Raw HIF graph vocabulary.""" + +from nx_hif.hif import HyperGraph, hif_edge_incidences, hif_node, hif_node_incidences + + +__all__ = [ + "HyperGraph", + "hif_edge_incidences", + "hif_node", + "hif_node_incidences", +] diff --git a/widip/wire/loader.py b/widip/wire/loader.py index fb7fd01..88b97e1 100644 --- a/widip/wire/loader.py +++ b/widip/wire/loader.py @@ -3,23 +3,38 @@ from discopy import monoidal from .functions import Box -from .services import Copy as CopyService, Delete from .types import Id, Ty loader_stream_ty = Ty("yaml_stream") +class LoaderScalar(Box): + """Atomic YAML scalar node in the loader language.""" + + def __init__(self, value: str, tag: str | None = None): + self.value = value + self.tag = tag + if tag: + name = f"!{tag}" if not value else f"!{tag} {value!r}" + else: + name = repr(value) + Box.__init__(self, name, dom=loader_stream_ty, cod=loader_stream_ty) + + class LoaderSequence(monoidal.Bubble, Box): """Bubble grouping loader stages in sequence.""" - def __init__(self, stages): + def __init__(self, stages, tag: str | None = None): self.stages = tuple(stages) + self.tag = tag + name = "seq" if tag is None else f"!{tag} seq" monoidal.Bubble.__init__( self, pipeline(self.stages), dom=loader_stream_ty, cod=loader_stream_ty, + name=name, draw_vertically=True, drawing_name="seq", ) @@ -28,14 +43,17 @@ def __init__(self, stages): class LoaderMapping(monoidal.Bubble, Box): """Bubble grouping loader branches in parallel.""" - def __init__(self, branches): + def __init__(self, branches, tag: str | None = None): self.branches = tuple(branches) + self.tag = tag arg = tensor_all(self.branches) if self.branches else loader_id() + name = "map" if tag is None else f"!{tag} map" monoidal.Bubble.__init__( self, arg, dom=loader_stream_ty, cod=loader_stream_ty, + name=name, drawing_name="map", ) diff --git a/widip/wire/widish.py b/widip/wire/widish.py index 1bfe320..d5ddc46 100644 --- a/widip/wire/widish.py +++ b/widip/wire/widish.py @@ -1,6 +1,5 @@ """Shell-specific wire combinators and structural boxes.""" -from .functions import Box from .services import Copy as CopyService, Delete from .types import Id, Ty @@ -33,13 +32,3 @@ def Copy(n: int): for copies in range(2, n): result = result >> io_wires(copies - 1) @ CopyService(io_ty) return result - - -class Merge(Box): - """N-ary stream fan-in for the shell case study.""" - - def __init__(self, n: int): - if n <= 0: - raise ValueError("merge arity must be positive") - self.n = n - Box.__init__(self, f"merge[{n}]", io_wires(n), io_ty) From b4315ef397f9c71ff767f4413614cdf76d9f1587 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Wed, 18 Mar 2026 20:25:03 +0000 Subject: [PATCH 06/17] feat: python metaprogramming --- tests/metaprog/python.py | 113 +++++++++++++++++++++++++++++++++++++++ widip/metaprog/python.py | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tests/metaprog/python.py create mode 100644 widip/metaprog/python.py diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py new file mode 100644 index 0000000..f60dfef --- /dev/null +++ b/tests/metaprog/python.py @@ -0,0 +1,113 @@ +""" +Tests for the Python specializer and Futamura projections. + +References: +- `62-compilers.tex`, lines 60-67: Eq. (2) and Eq. (3) for the specializer. +- `62-compilers.tex`, lines 92-102: `C2 = pev S_L H` and `C3 = pev S_L S`. +- `62-compilers.tex`, lines 128-130: `C1 = uev C2` and `C2 = uev C3`. +""" + +from discopy import python + +from widip.metaprog.python import ( + PYTHON_COMPILER, + PYTHON_COMPILER_GENERATOR, + PYTHON_INTERPRETER, + PYTHON_SPECIALIZER, + compiler, + compiler_generator, + eq_2, + eq_3, + eq_4, + eq_5, + first_futamura_projection, + sec_6_2_2_partial_application, +) + + +def test_sec_6_2_2_partial_application(): + program = python.Function( + inside=lambda static_input, runtime_input: static_input + runtime_input, + dom=(object, object), + cod=object, + ) + residual_from_section = sec_6_2_2_partial_application(program, 7) + residual_from_specializer = PYTHON_SPECIALIZER(program, 7) + + assert residual_from_section(5) == 12 + assert residual_from_specializer(5) == 12 + + +def test_eq_2_is_specializer_interpretation(): + program = python.Function( + inside=lambda static_input, runtime_input: static_input * runtime_input + 1, + dom=(object, object), + cod=object, + ) + left = eq_2(program, 4) + right = PYTHON_SPECIALIZER(program, 4) + + assert left(6) == right(6) + + +def test_eq_3_is_specializer_self_application(): + program = python.Function( + inside=lambda static_input, runtime_input: f"{static_input}:{runtime_input}", + dom=(object, object), + cod=object, + ) + left = eq_2(program, "alpha") + right = eq_3(program, "alpha") + + assert left("beta") == right("beta") + + +def test_first_projection_builds_c1_compiler(): + source_program = python.Function( + inside=lambda runtime_input: runtime_input * 2 + 3, + dom=object, + cod=object, + ) + compiler_c1 = first_futamura_projection(PYTHON_INTERPRETER) + compiled_program = compiler_c1(source_program) + + assert compiled_program(9) == PYTHON_INTERPRETER(source_program, 9) + + +def test_second_projection_builds_c2_compiler(): + source_program = python.Function( + inside=lambda runtime_input: runtime_input - 11, + dom=object, + cod=object, + ) + compiler_c1 = first_futamura_projection(PYTHON_INTERPRETER) + compiler_c2 = compiler(PYTHON_INTERPRETER) + + assert compiler_c2(source_program)(30) == PYTHON_INTERPRETER(source_program, 30) + assert compiler_c2(source_program)(30) == compiler_c1(source_program)(30) + + +def test_third_projection_builds_c3_compiler_generator(): + source_program = python.Function( + inside=lambda runtime_input: runtime_input**2, + dom=object, + cod=object, + ) + compiler_c2_from_eq_4 = eq_4(PYTHON_INTERPRETER) + compiler_c2_from_eq_5 = eq_5(PYTHON_INTERPRETER) + compiler_c3 = compiler_generator() + + assert compiler_c2_from_eq_4(source_program)(8) == 64 + assert compiler_c2_from_eq_4(source_program)(8) == compiler_c2_from_eq_5(source_program)(8) + assert compiler_c2_from_eq_4(source_program)(8) == compiler_c3(PYTHON_INTERPRETER)(source_program)(8) + + +def test_exported_compiler_and_generator_constants(): + source_program = python.Function( + inside=lambda runtime_input: runtime_input + 100, + dom=object, + cod=object, + ) + + assert PYTHON_COMPILER(source_program)(1) == 101 + assert PYTHON_COMPILER_GENERATOR(PYTHON_INTERPRETER)(source_program)(1) == 101 diff --git a/widip/metaprog/python.py b/widip/metaprog/python.py new file mode 100644 index 0000000..684dff3 --- /dev/null +++ b/widip/metaprog/python.py @@ -0,0 +1,93 @@ +""" +Python realization of textbook partial evaluation and Futamura projections. + +References: +- `61-concept.tex`, Fig. 6.1 and surrounding text: + metaprograms compute programs and evaluators run programs. +- `62-compilers.tex`, lines 60-131: + Eq. (2), Eq. (3), Eq. (4), Eq. (5), and definitions of `C1`, `C2`, `C3`. + +Implemented equations (same numbering as the text): +- Eq. (2): `pev X_L y = uev S_L (X, y)`. +- Eq. (3): `uev S_L (X, y) = uev (pev S_L X)_L y`. +- Eq. (4): `pev S_L H = uev S_L (S, H)`. +- Eq. (5): `uev S_L (S, H) = uev (pev S_L S)_L H`. + +Section mapping: +- Sec. 6.2.2: `sec_6_2_2_partial_application`, `PYTHON_SPECIALIZER`. +- Supercompilation / Futamura projections: + `first_futamura_projection`, `compiler` (`C2`), `compiler_generator` (`C3`). +""" + +from discopy import python + + +def sec_6_2_2_partial_application(program, static_input): + """Sec. 6.2.2: partial evaluation as residual-program construction.""" + return python.Function( + inside=lambda runtime_input: program(static_input, runtime_input), + dom=object, + cod=object, + ) + + +PYTHON_SPECIALIZER = python.Function( + inside=sec_6_2_2_partial_application, + dom=(python.Function, object), + cod=python.Function, +) + + +def eq_2(program, static_input): + """Eq. (2): evaluate the specializer program `S` on `(X, y)`.""" + return PYTHON_SPECIALIZER(program, static_input) + + +def eq_3(program, static_input): + """Eq. (3): evaluate `pev S X` on `y`.""" + return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, program)(static_input) + + +def high_to_low_interpreter(source_program, runtime_input): + """`H`: interpreter that executes one source program on one runtime input.""" + return source_program(runtime_input) + + +PYTHON_INTERPRETER = python.Function( + inside=high_to_low_interpreter, + dom=(python.Function, object), + cod=object, +) + + +def first_futamura_projection(interpreter): + """`C1 = pev H`: compiler from partial evaluation of an interpreter.""" + return python.Function( + inside=lambda source_program: eq_2(interpreter, source_program), + dom=python.Function, + cod=python.Function, + ) + + +def eq_4(interpreter): + """Eq. (4): `C2 = pev S H = uev S (S, H)`.""" + return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, interpreter) + + +def compiler(interpreter): + """`C2`: the compiler from the second Futamura projection.""" + return eq_4(interpreter) + + +def compiler_generator(): + """`C3 = pev S S`: the compiler generator from the third projection.""" + return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, PYTHON_SPECIALIZER) + + +def eq_5(interpreter): + """Eq. (5): evaluate `C3` on `H` to obtain `C2`.""" + return compiler_generator()(interpreter) + + +PYTHON_COMPILER = compiler(PYTHON_INTERPRETER) +PYTHON_COMPILER_GENERATOR = compiler_generator() From 8c5f6d38aa7bba93c1e2f63a3b5251fa416d8953 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Wed, 18 Mar 2026 22:09:24 +0000 Subject: [PATCH 07/17] feat: diagrams in python metaprog --- tests/metaprog/python.py | 143 ++++++++++---------- widip/metaprog/python.py | 272 ++++++++++++++++++++++++++++++--------- 2 files changed, 288 insertions(+), 127 deletions(-) diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py index f60dfef..cafe8c7 100644 --- a/tests/metaprog/python.py +++ b/tests/metaprog/python.py @@ -1,19 +1,15 @@ -""" -Tests for the Python specializer and Futamura projections. - -References: -- `62-compilers.tex`, lines 60-67: Eq. (2) and Eq. (3) for the specializer. -- `62-compilers.tex`, lines 92-102: `C2 = pev S_L H` and `C3 = pev S_L S`. -- `62-compilers.tex`, lines 128-130: `C1 = uev C2` and `C2 = uev C3`. -""" - -from discopy import python +"""Diagram tests for Sec. 6.2.2 and Futamura projections.""" +from widip.comput import computer from widip.metaprog.python import ( PYTHON_COMPILER, PYTHON_COMPILER_GENERATOR, + PYTHON_EVALUATOR_BOX, PYTHON_INTERPRETER, + PYTHON_INTERPRETER_BOX, + PYTHON_RUNTIME, PYTHON_SPECIALIZER, + PYTHON_SPECIALIZER_BOX, compiler, compiler_generator, eq_2, @@ -21,93 +17,108 @@ eq_4, eq_5, first_futamura_projection, + python_object, sec_6_2_2_partial_application, ) -def test_sec_6_2_2_partial_application(): - program = python.Function( - inside=lambda static_input, runtime_input: static_input + runtime_input, - dom=(object, object), - cod=object, - ) - residual_from_section = sec_6_2_2_partial_application(program, 7) - residual_from_specializer = PYTHON_SPECIALIZER(program, 7) +def eval_closed(diagram): + return PYTHON_RUNTIME(diagram)() - assert residual_from_section(5) == 12 - assert residual_from_specializer(5) == 12 +def test_runtime_uses_functor_diagram_transforms(): + program = python_object(lambda static_input, runtime_input: static_input + runtime_input, "add") + static_input = python_object(7, "seven") + equation = eq_2(program, static_input) -def test_eq_2_is_specializer_interpretation(): - program = python.Function( - inside=lambda static_input, runtime_input: static_input * runtime_input + 1, - dom=(object, object), - cod=object, - ) - left = eq_2(program, 4) - right = PYTHON_SPECIALIZER(program, 4) + assert isinstance(PYTHON_SPECIALIZER, computer.Functor) + assert isinstance(PYTHON_INTERPRETER, computer.Functor) + assert PYTHON_SPECIALIZER_BOX.dom == computer.Ty() + assert PYTHON_INTERPRETER_BOX.dom == computer.Ty() + assert isinstance(PYTHON_EVALUATOR_BOX, computer.Computer) + assert PYTHON_EVALUATOR_BOX.dom == PYTHON_SPECIALIZER_BOX.cod @ PYTHON_SPECIALIZER_BOX.cod + assert PYTHON_RUNTIME.normalize(equation) == PYTHON_INTERPRETER(PYTHON_SPECIALIZER(equation)) + + +def test_sec_6_2_2_partial_application(): + program = python_object(lambda static_input, runtime_input: static_input + runtime_input, "add") + static_input = python_object(7, "seven") + residual_from_section = sec_6_2_2_partial_application(program, static_input) + residual_from_equation = eq_2(program, static_input) + expected = (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input >> PYTHON_EVALUATOR_BOX - assert left(6) == right(6) + assert residual_from_section == expected + assert residual_from_equation == expected + assert eval_closed(residual_from_section)(5) == 12 + assert eval_closed(residual_from_equation)(5) == 12 def test_eq_3_is_specializer_self_application(): - program = python.Function( - inside=lambda static_input, runtime_input: f"{static_input}:{runtime_input}", - dom=(object, object), - cod=object, + program = python_object(lambda static_input, runtime_input: f"{static_input}:{runtime_input}", "format") + static_input = python_object("alpha", "alpha") + left = eq_2(program, static_input) + right = eq_3(program, static_input) + expected_right = ( + ((PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ program + >> PYTHON_EVALUATOR_BOX) @ static_input + >> PYTHON_EVALUATOR_BOX ) - left = eq_2(program, "alpha") - right = eq_3(program, "alpha") - assert left("beta") == right("beta") + assert right == expected_right + assert eval_closed(left)("beta") == eval_closed(right)("beta") + + +def test_tuple_data_stays_atomic_across_universal_evaluator_wires(): + append_program = python_object(lambda xs, ys: xs + ys, "append") + static_tuple = python_object(("a", "a", "b"), "tuple_static") + residual_left = eval_closed(eq_2(append_program, static_tuple)) + residual_right = eval_closed(eq_3(append_program, static_tuple)) + + assert residual_left(("c", "d")) == ("a", "a", "b", "c", "d") + assert residual_left(("c", "d")) == residual_right(("c", "d")) def test_first_projection_builds_c1_compiler(): - source_program = python.Function( - inside=lambda runtime_input: runtime_input * 2 + 3, - dom=object, - cod=object, - ) - compiler_c1 = first_futamura_projection(PYTHON_INTERPRETER) + source_program = lambda runtime_input: runtime_input * 2 + 3 + compiler_c1 = eval_closed(first_futamura_projection(PYTHON_INTERPRETER_BOX)) compiled_program = compiler_c1(source_program) + compiled_from_eq_2 = eval_closed(eq_2(PYTHON_INTERPRETER_BOX, python_object(source_program, "X"))) - assert compiled_program(9) == PYTHON_INTERPRETER(source_program, 9) + assert compiled_program(9) == source_program(9) + assert compiled_program(9) == compiled_from_eq_2(9) def test_second_projection_builds_c2_compiler(): - source_program = python.Function( - inside=lambda runtime_input: runtime_input - 11, - dom=object, - cod=object, - ) - compiler_c1 = first_futamura_projection(PYTHON_INTERPRETER) - compiler_c2 = compiler(PYTHON_INTERPRETER) + source_program = lambda runtime_input: runtime_input - 11 + compiler_c1 = eval_closed(first_futamura_projection(PYTHON_INTERPRETER_BOX)) + compiler_c2 = eval_closed(compiler(PYTHON_INTERPRETER_BOX)) - assert compiler_c2(source_program)(30) == PYTHON_INTERPRETER(source_program, 30) + assert compiler_c2(source_program)(30) == source_program(30) assert compiler_c2(source_program)(30) == compiler_c1(source_program)(30) def test_third_projection_builds_c3_compiler_generator(): - source_program = python.Function( - inside=lambda runtime_input: runtime_input**2, - dom=object, - cod=object, + source_program = lambda runtime_input: runtime_input**2 + compiler_c2_from_eq_4 = eval_closed(eq_4(PYTHON_INTERPRETER_BOX)) + compiler_c2_from_eq_5 = eval_closed(eq_5(PYTHON_INTERPRETER_BOX)) + compiler_c3 = eval_closed(compiler_generator()) + expected_eq_4 = ( + (PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ PYTHON_INTERPRETER_BOX + >> PYTHON_EVALUATOR_BOX ) - compiler_c2_from_eq_4 = eq_4(PYTHON_INTERPRETER) - compiler_c2_from_eq_5 = eq_5(PYTHON_INTERPRETER) - compiler_c3 = compiler_generator() + interpreter = eval_closed(PYTHON_INTERPRETER_BOX) + assert eq_4(PYTHON_INTERPRETER_BOX) == expected_eq_4 assert compiler_c2_from_eq_4(source_program)(8) == 64 assert compiler_c2_from_eq_4(source_program)(8) == compiler_c2_from_eq_5(source_program)(8) - assert compiler_c2_from_eq_4(source_program)(8) == compiler_c3(PYTHON_INTERPRETER)(source_program)(8) + assert compiler_c2_from_eq_4(source_program)(8) == compiler_c3(interpreter)(source_program)(8) def test_exported_compiler_and_generator_constants(): - source_program = python.Function( - inside=lambda runtime_input: runtime_input + 100, - dom=object, - cod=object, - ) + source_program = lambda runtime_input: runtime_input + 100 + compiler_value = eval_closed(PYTHON_COMPILER) + compiler_generator_value = eval_closed(PYTHON_COMPILER_GENERATOR) + interpreter = eval_closed(PYTHON_INTERPRETER_BOX) - assert PYTHON_COMPILER(source_program)(1) == 101 - assert PYTHON_COMPILER_GENERATOR(PYTHON_INTERPRETER)(source_program)(1) == 101 + assert compiler_value(source_program)(1) == 101 + assert compiler_generator_value(interpreter)(source_program)(1) == 101 diff --git a/widip/metaprog/python.py b/widip/metaprog/python.py index 684dff3..90adec2 100644 --- a/widip/metaprog/python.py +++ b/widip/metaprog/python.py @@ -1,93 +1,243 @@ """ -Python realization of textbook partial evaluation and Futamura projections. - -References: -- `61-concept.tex`, Fig. 6.1 and surrounding text: - metaprograms compute programs and evaluators run programs. -- `62-compilers.tex`, lines 60-131: - Eq. (2), Eq. (3), Eq. (4), Eq. (5), and definitions of `C1`, `C2`, `C3`. - -Implemented equations (same numbering as the text): -- Eq. (2): `pev X_L y = uev S_L (X, y)`. -- Eq. (3): `uev S_L (X, y) = uev (pev S_L X)_L y`. -- Eq. (4): `pev S_L H = uev S_L (S, H)`. -- Eq. (5): `uev S_L (S, H) = uev (pev S_L S)_L H`. - -Section mapping: -- Sec. 6.2.2: `sec_6_2_2_partial_application`, `PYTHON_SPECIALIZER`. -- Supercompilation / Futamura projections: - `first_futamura_projection`, `compiler` (`C2`), `compiler_generator` (`C3`). +Diagram-first Python realization of Sec. 6.2.2 and Futamura projections. + +`PythonSpecializer` and `PythonInterpreter` are native metaprogram boxes +(`I -> P_python`). Equations are composed as diagrams and interpreted by a +runtime functor. """ -from discopy import python +from functools import partial +from discopy import monoidal, python +from discopy.utils import tuplify, untuplify -def sec_6_2_2_partial_application(program, static_input): - """Sec. 6.2.2: partial evaluation as residual-program construction.""" - return python.Function( - inside=lambda runtime_input: program(static_input, runtime_input), - dom=object, - cod=object, - ) +from ..comput import computer +from ..comput import python as comput_python -PYTHON_SPECIALIZER = python.Function( - inside=sec_6_2_2_partial_application, - dom=(python.Function, object), - cod=python.Function, -) +PYTHON_OBJECT = comput_python.program_ty -def eq_2(program, static_input): - """Eq. (2): evaluate the specializer program `S` on `(X, y)`.""" - return PYTHON_SPECIALIZER(program, static_input) +def _pack_value(value): + return tuplify((value, )) -def eq_3(program, static_input): - """Eq. (3): evaluate `pev S X` on `y`.""" - return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, program)(static_input) +def _unpack_value(value): + return untuplify(tuplify(value)) -def high_to_low_interpreter(source_program, runtime_input): - """`H`: interpreter that executes one source program on one runtime input.""" - return source_program(runtime_input) +def _value_name(value) -> str: + if isinstance(value, str): + return repr(value) + if callable(value): + return getattr(value, "__name__", type(value).__name__) + return str(value) -PYTHON_INTERPRETER = python.Function( - inside=high_to_low_interpreter, - dom=(python.Function, object), - cod=object, -) +class PythonObject(computer.Box): + """Closed Python object constant encoded on one object wire.""" + def __init__(self, value, name=None): + self.value = value + computer.Box.__init__(self, _value_name(value) if name is None else name, computer.Ty(), PYTHON_OBJECT) -def first_futamura_projection(interpreter): - """`C1 = pev H`: compiler from partial evaluation of an interpreter.""" - return python.Function( - inside=lambda source_program: eq_2(interpreter, source_program), - dom=python.Function, - cod=python.Function, + +class PythonSpecializer(computer.Box): + """Native specializer metaprogram: ``S : I -> obj``.""" + + def __init__(self): + computer.Box.__init__(self, "S", computer.Ty(), PYTHON_OBJECT) + + +class PythonInterpreter(computer.Box): + """Native interpreter metaprogram: ``H : I -> obj``.""" + + def __init__(self): + computer.Box.__init__(self, "H", computer.Ty(), PYTHON_OBJECT) + + +PYTHON_SPECIALIZER_BOX = PythonSpecializer() +PYTHON_INTERPRETER_BOX = PythonInterpreter() +PYTHON_EVALUATOR_BOX = computer.Computer(PYTHON_OBJECT, PYTHON_OBJECT, PYTHON_OBJECT) + + +class PythonSpecializerFunctor(computer.Functor): + """Diagram pass that normalizes occurrences of native specializer boxes.""" + + def __init__(self): + computer.Functor.__init__( + self, + ob=lambda ob: ob, + ar=self.ar_map, + dom=computer.Category(), + cod=computer.Category(), + ) + + @staticmethod + def ar_map(ar): + if isinstance(ar, PythonSpecializer): + return PythonSpecializer() + return ar + + +class PythonInterpreterFunctor(computer.Functor): + """Diagram pass that normalizes occurrences of native interpreter boxes.""" + + def __init__(self): + computer.Functor.__init__( + self, + ob=lambda ob: ob, + ar=self.ar_map, + dom=computer.Category(), + cod=computer.Category(), + ) + + @staticmethod + def ar_map(ar): + if isinstance(ar, PythonInterpreter): + return PythonInterpreter() + return ar + + +PYTHON_SPECIALIZER = PythonSpecializerFunctor() +PYTHON_INTERPRETER = PythonInterpreterFunctor() + + +def _partial_evaluate(program, static_input): + return lambda runtime_input: program(static_input, runtime_input) + + +def _universal_evaluate(program, runtime_input): + return program(runtime_input) + + +def _apply_value(function, argument): + function = _unpack_value(function) + argument = _unpack_value(argument) + try: + return _pack_value(function(argument)) + except TypeError: + return _pack_value(partial(function, argument)) + + +def python_object(value, name=None): + """Encode a closed Python value as a unit-to-object diagram.""" + return PythonObject(value, name=name) + + +def _as_closed_object(value, default_name): + if isinstance(value, computer.Diagram): + if value.dom != computer.Ty() or value.cod != PYTHON_OBJECT: + raise TypeError(f"expected closed object diagram I->obj, got {value.dom}->{value.cod}") + return value + return python_object(value, default_name) + + +def sec_6_2_2_partial_application(program, static_input): + """Sec. 6.2.2 as a diagram: ``pev(X, y)``.""" + program = _as_closed_object(program, "X") + static_input = _as_closed_object(static_input, "y") + return ( + (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input + >> PYTHON_EVALUATOR_BOX + ) + + +def eq_2(program, static_input): + """Eq. (2): ``pev X y = uev S (X, y)`` (left side as native specializer box).""" + return sec_6_2_2_partial_application(program, static_input) + + +def eq_3(program, static_input): + """Eq. (3): ``uev S (X, y) = uev (pev S X) y``.""" + program = _as_closed_object(program, "X") + static_input = _as_closed_object(static_input, "y") + return ( + ((PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ program + >> PYTHON_EVALUATOR_BOX) @ static_input + >> PYTHON_EVALUATOR_BOX ) +def first_futamura_projection(interpreter): + """`C1` as a diagram: partially evaluate the specializer on an interpreter.""" + interpreter = _as_closed_object(interpreter, "H") + return PYTHON_SPECIALIZER_BOX @ interpreter >> PYTHON_EVALUATOR_BOX + + def eq_4(interpreter): - """Eq. (4): `C2 = pev S H = uev S (S, H)`.""" - return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, interpreter) + """Eq. (4): ``C2 = pev S H = uev S (S, H)``.""" + interpreter = _as_closed_object(interpreter, "H") + return ( + (PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ interpreter + >> PYTHON_EVALUATOR_BOX + ) def compiler(interpreter): - """`C2`: the compiler from the second Futamura projection.""" + """`C2`: second Futamura projection as a diagram.""" return eq_4(interpreter) def compiler_generator(): - """`C3 = pev S S`: the compiler generator from the third projection.""" - return PYTHON_SPECIALIZER(PYTHON_SPECIALIZER, PYTHON_SPECIALIZER) + """`C3 = pev S S`: third Futamura projection as a diagram.""" + return PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX def eq_5(interpreter): - """Eq. (5): evaluate `C3` on `H` to obtain `C2`.""" - return compiler_generator()(interpreter) - - -PYTHON_COMPILER = compiler(PYTHON_INTERPRETER) + """Eq. (5): ``uev S (S, H) = uev (pev S S) H``.""" + interpreter = _as_closed_object(interpreter, "H") + return compiler_generator() @ interpreter >> PYTHON_EVALUATOR_BOX + + +class PythonRuntime(monoidal.Functor): + """Runtime functor from computer diagrams to executable Python functions.""" + + def __init__(self, *diagram_transforms): + self.diagram_transforms = diagram_transforms or (PYTHON_SPECIALIZER, PYTHON_INTERPRETER) + monoidal.Functor.__init__( + self, + ob=self.ob_map, + ar=self.ar_map, + dom=computer.Category(), + cod=python.Category(), + ) + + @staticmethod + def ob_map(_ob): + return object + + def normalize(self, diagram): + if not isinstance(diagram, computer.Diagram): + return diagram + for transform in self.diagram_transforms: + diagram = transform(diagram) + return diagram + + def __call__(self, other): + if isinstance(other, computer.Diagram): + other = self.normalize(other) + return monoidal.Functor.__call__(self, other) + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + if isinstance(box, PythonObject): + return python.Function(lambda: _pack_value(box.value), dom, cod) + if isinstance(box, PythonSpecializer): + return python.Function(lambda: _partial_evaluate, dom, cod) + if isinstance(box, PythonInterpreter): + return python.Function(lambda: _universal_evaluate, dom, cod) + if isinstance(box, computer.Computer): + return python.Function(_apply_value, dom, cod) + if isinstance(box, computer.Copy): + return python.Function.copy(dom, n=2) + if isinstance(box, computer.Delete): + return python.Function.discard(dom) + if isinstance(box, computer.Swap): + return python.Function.swap(self(box.left), self(box.right)) + raise TypeError(f"unsupported Python metaprogram box: {box!r}") + + +PYTHON_RUNTIME = PythonRuntime(PYTHON_SPECIALIZER, PYTHON_INTERPRETER) +PYTHON_COMPILER = compiler(PYTHON_INTERPRETER_BOX) PYTHON_COMPILER_GENERATOR = compiler_generator() From e54d4e5f4eff5973df1523ac83811e498094080b Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Wed, 18 Mar 2026 22:42:03 +0000 Subject: [PATCH 08/17] wip --- tests/metaprog/python.py | 46 ++++++++----- widip/metaprog/python.py | 137 +++++++++------------------------------ 2 files changed, 59 insertions(+), 124 deletions(-) diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py index cafe8c7..566a8ef 100644 --- a/tests/metaprog/python.py +++ b/tests/metaprog/python.py @@ -1,14 +1,13 @@ """Diagram tests for Sec. 6.2.2 and Futamura projections.""" from widip.comput import computer +from widip.comput import python as comput_python from widip.metaprog.python import ( PYTHON_COMPILER, PYTHON_COMPILER_GENERATOR, PYTHON_EVALUATOR_BOX, - PYTHON_INTERPRETER, PYTHON_INTERPRETER_BOX, PYTHON_RUNTIME, - PYTHON_SPECIALIZER, PYTHON_SPECIALIZER_BOX, compiler, compiler_generator, @@ -17,32 +16,35 @@ eq_4, eq_5, first_futamura_projection, - python_object, sec_6_2_2_partial_application, ) +def closed_value(value, name, cod=comput_python.program_ty): + box = computer.Box(name, computer.Ty(), cod) + box.value = value + return box + + def eval_closed(diagram): return PYTHON_RUNTIME(diagram)() -def test_runtime_uses_functor_diagram_transforms(): - program = python_object(lambda static_input, runtime_input: static_input + runtime_input, "add") - static_input = python_object(7, "seven") +def test_runtime_evaluates_equation(): + program = closed_value(lambda static_input, runtime_input: static_input + runtime_input, "X") + static_input = closed_value(7, "y") equation = eq_2(program, static_input) - assert isinstance(PYTHON_SPECIALIZER, computer.Functor) - assert isinstance(PYTHON_INTERPRETER, computer.Functor) assert PYTHON_SPECIALIZER_BOX.dom == computer.Ty() assert PYTHON_INTERPRETER_BOX.dom == computer.Ty() assert isinstance(PYTHON_EVALUATOR_BOX, computer.Computer) assert PYTHON_EVALUATOR_BOX.dom == PYTHON_SPECIALIZER_BOX.cod @ PYTHON_SPECIALIZER_BOX.cod - assert PYTHON_RUNTIME.normalize(equation) == PYTHON_INTERPRETER(PYTHON_SPECIALIZER(equation)) + assert eval_closed(equation)(5) == 12 def test_sec_6_2_2_partial_application(): - program = python_object(lambda static_input, runtime_input: static_input + runtime_input, "add") - static_input = python_object(7, "seven") + program = closed_value(lambda static_input, runtime_input: static_input + runtime_input, "X") + static_input = closed_value(7, "y") residual_from_section = sec_6_2_2_partial_application(program, static_input) residual_from_equation = eq_2(program, static_input) expected = (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input >> PYTHON_EVALUATOR_BOX @@ -54,8 +56,8 @@ def test_sec_6_2_2_partial_application(): def test_eq_3_is_specializer_self_application(): - program = python_object(lambda static_input, runtime_input: f"{static_input}:{runtime_input}", "format") - static_input = python_object("alpha", "alpha") + program = closed_value(lambda static_input, runtime_input: f"{static_input}:{runtime_input}", "X") + static_input = closed_value("alpha", "y") left = eq_2(program, static_input) right = eq_3(program, static_input) expected_right = ( @@ -69,8 +71,8 @@ def test_eq_3_is_specializer_self_application(): def test_tuple_data_stays_atomic_across_universal_evaluator_wires(): - append_program = python_object(lambda xs, ys: xs + ys, "append") - static_tuple = python_object(("a", "a", "b"), "tuple_static") + append_program = closed_value(lambda xs, ys: xs + ys, "X") + static_tuple = closed_value(("a", "a", "b"), "y") residual_left = eval_closed(eq_2(append_program, static_tuple)) residual_right = eval_closed(eq_3(append_program, static_tuple)) @@ -82,7 +84,7 @@ def test_first_projection_builds_c1_compiler(): source_program = lambda runtime_input: runtime_input * 2 + 3 compiler_c1 = eval_closed(first_futamura_projection(PYTHON_INTERPRETER_BOX)) compiled_program = compiler_c1(source_program) - compiled_from_eq_2 = eval_closed(eq_2(PYTHON_INTERPRETER_BOX, python_object(source_program, "X"))) + compiled_from_eq_2 = eval_closed(eq_2(PYTHON_INTERPRETER_BOX, closed_value(source_program, "y"))) assert compiled_program(9) == source_program(9) assert compiled_program(9) == compiled_from_eq_2(9) @@ -122,3 +124,15 @@ def test_exported_compiler_and_generator_constants(): assert compiler_value(source_program)(1) == 101 assert compiler_generator_value(interpreter)(source_program)(1) == 101 + + +def test_sec_6_2_2_accepts_arbitrary_static_input_type(): + data_ty = computer.Ty("Data") + program = closed_value(lambda static_input, runtime_input: f"{static_input}|{runtime_input}", "X") + static_input = closed_value("alpha", "y", cod=data_ty) + + residual = eq_2(program, static_input) + + assert residual.dom == computer.Ty() + assert residual.cod == PYTHON_EVALUATOR_BOX.cod + assert eval_closed(residual)("beta") == "alpha|beta" diff --git a/widip/metaprog/python.py b/widip/metaprog/python.py index 90adec2..2a03b84 100644 --- a/widip/metaprog/python.py +++ b/widip/metaprog/python.py @@ -11,35 +11,17 @@ from discopy import monoidal, python from discopy.utils import tuplify, untuplify +from ..comput import ProgramClosedCategory from ..comput import computer from ..comput import python as comput_python PYTHON_OBJECT = comput_python.program_ty +PYTHON_PROGRAMS = ProgramClosedCategory(PYTHON_OBJECT) -def _pack_value(value): - return tuplify((value, )) - - -def _unpack_value(value): - return untuplify(tuplify(value)) - - -def _value_name(value) -> str: - if isinstance(value, str): - return repr(value) - if callable(value): - return getattr(value, "__name__", type(value).__name__) - return str(value) - - -class PythonObject(computer.Box): - """Closed Python object constant encoded on one object wire.""" - - def __init__(self, value, name=None): - self.value = value - computer.Box.__init__(self, _value_name(value) if name is None else name, computer.Ty(), PYTHON_OBJECT) +def _evaluator(A, B): + return PYTHON_PROGRAMS.evaluator(A, B) class PythonSpecializer(computer.Box): @@ -58,49 +40,7 @@ def __init__(self): PYTHON_SPECIALIZER_BOX = PythonSpecializer() PYTHON_INTERPRETER_BOX = PythonInterpreter() -PYTHON_EVALUATOR_BOX = computer.Computer(PYTHON_OBJECT, PYTHON_OBJECT, PYTHON_OBJECT) - - -class PythonSpecializerFunctor(computer.Functor): - """Diagram pass that normalizes occurrences of native specializer boxes.""" - - def __init__(self): - computer.Functor.__init__( - self, - ob=lambda ob: ob, - ar=self.ar_map, - dom=computer.Category(), - cod=computer.Category(), - ) - - @staticmethod - def ar_map(ar): - if isinstance(ar, PythonSpecializer): - return PythonSpecializer() - return ar - - -class PythonInterpreterFunctor(computer.Functor): - """Diagram pass that normalizes occurrences of native interpreter boxes.""" - - def __init__(self): - computer.Functor.__init__( - self, - ob=lambda ob: ob, - ar=self.ar_map, - dom=computer.Category(), - cod=computer.Category(), - ) - - @staticmethod - def ar_map(ar): - if isinstance(ar, PythonInterpreter): - return PythonInterpreter() - return ar - - -PYTHON_SPECIALIZER = PythonSpecializerFunctor() -PYTHON_INTERPRETER = PythonInterpreterFunctor() +PYTHON_EVALUATOR_BOX = _evaluator(PYTHON_OBJECT, PYTHON_OBJECT) def _partial_evaluate(program, static_input): @@ -112,34 +52,19 @@ def _universal_evaluate(program, runtime_input): def _apply_value(function, argument): - function = _unpack_value(function) - argument = _unpack_value(argument) + function = untuplify(tuplify(function)) + argument = untuplify(tuplify(argument)) try: - return _pack_value(function(argument)) + return tuplify((function(argument), )) except TypeError: - return _pack_value(partial(function, argument)) - - -def python_object(value, name=None): - """Encode a closed Python value as a unit-to-object diagram.""" - return PythonObject(value, name=name) - - -def _as_closed_object(value, default_name): - if isinstance(value, computer.Diagram): - if value.dom != computer.Ty() or value.cod != PYTHON_OBJECT: - raise TypeError(f"expected closed object diagram I->obj, got {value.dom}->{value.cod}") - return value - return python_object(value, default_name) + return tuplify((partial(function, argument), )) def sec_6_2_2_partial_application(program, static_input): """Sec. 6.2.2 as a diagram: ``pev(X, y)``.""" - program = _as_closed_object(program, "X") - static_input = _as_closed_object(static_input, "y") return ( (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input - >> PYTHON_EVALUATOR_BOX + >> _evaluator(static_input.cod, PYTHON_OBJECT) ) @@ -150,24 +75,20 @@ def eq_2(program, static_input): def eq_3(program, static_input): """Eq. (3): ``uev S (X, y) = uev (pev S X) y``.""" - program = _as_closed_object(program, "X") - static_input = _as_closed_object(static_input, "y") return ( ((PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ program >> PYTHON_EVALUATOR_BOX) @ static_input - >> PYTHON_EVALUATOR_BOX + >> _evaluator(static_input.cod, PYTHON_OBJECT) ) def first_futamura_projection(interpreter): """`C1` as a diagram: partially evaluate the specializer on an interpreter.""" - interpreter = _as_closed_object(interpreter, "H") return PYTHON_SPECIALIZER_BOX @ interpreter >> PYTHON_EVALUATOR_BOX def eq_4(interpreter): """Eq. (4): ``C2 = pev S H = uev S (S, H)``.""" - interpreter = _as_closed_object(interpreter, "H") return ( (PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ interpreter >> PYTHON_EVALUATOR_BOX @@ -186,15 +107,13 @@ def compiler_generator(): def eq_5(interpreter): """Eq. (5): ``uev S (S, H) = uev (pev S S) H``.""" - interpreter = _as_closed_object(interpreter, "H") return compiler_generator() @ interpreter >> PYTHON_EVALUATOR_BOX class PythonRuntime(monoidal.Functor): """Runtime functor from computer diagrams to executable Python functions.""" - def __init__(self, *diagram_transforms): - self.diagram_transforms = diagram_transforms or (PYTHON_SPECIALIZER, PYTHON_INTERPRETER) + def __init__(self): monoidal.Functor.__init__( self, ob=self.ob_map, @@ -207,22 +126,24 @@ def __init__(self, *diagram_transforms): def ob_map(_ob): return object - def normalize(self, diagram): - if not isinstance(diagram, computer.Diagram): - return diagram - for transform in self.diagram_transforms: - diagram = transform(diagram) - return diagram - - def __call__(self, other): - if isinstance(other, computer.Diagram): - other = self.normalize(other) - return monoidal.Functor.__call__(self, other) - def ar_map(self, box): dom, cod = self(box.dom), self(box.cod) - if isinstance(box, PythonObject): - return python.Function(lambda: _pack_value(box.value), dom, cod) + if ( + isinstance(box, computer.Box) + and box.dom == computer.Ty() + and not isinstance( + box, + ( + PythonSpecializer, + PythonInterpreter, + computer.Computer, + computer.Copy, + computer.Delete, + computer.Swap, + ), + ) + ): + return python.Function(lambda value=box.value: tuplify((value, )), dom, cod) if isinstance(box, PythonSpecializer): return python.Function(lambda: _partial_evaluate, dom, cod) if isinstance(box, PythonInterpreter): @@ -238,6 +159,6 @@ def ar_map(self, box): raise TypeError(f"unsupported Python metaprogram box: {box!r}") -PYTHON_RUNTIME = PythonRuntime(PYTHON_SPECIALIZER, PYTHON_INTERPRETER) +PYTHON_RUNTIME = PythonRuntime() PYTHON_COMPILER = compiler(PYTHON_INTERPRETER_BOX) PYTHON_COMPILER_GENERATOR = compiler_generator() From 2dae557bcb84ed57888065fe1cd7ae0de238d01d Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Wed, 18 Mar 2026 23:43:56 +0000 Subject: [PATCH 09/17] wipwip --- widip/comput/boxes.py | 100 +++++++--------- widip/comput/equations.py | 37 +++--- widip/files.py | 7 +- widip/metaprog/__init__.py | 146 ++-------------------- widip/metaprog/core.py | 239 +++++++++++++++++++++++++++++++++++++ widip/metaprog/loader.py | 14 ++- widip/metaprog/python.py | 130 ++++++++++---------- widip/metaprog/widish.py | 62 +++++++++- widip/state/widish.py | 79 +++--------- widip/watch.py | 6 +- 10 files changed, 468 insertions(+), 352 deletions(-) create mode 100644 widip/metaprog/core.py diff --git a/widip/comput/boxes.py b/widip/comput/boxes.py index 42a105c..733c09a 100644 --- a/widip/comput/boxes.py +++ b/widip/comput/boxes.py @@ -7,100 +7,82 @@ class Partial(monoidal.Bubble, computer.Box): """ - Sec. 2.2.2. []:P×A⊸P - A partial evaluator is a (P×Y)-indexed program satisfying {[Γ]y}a = {Γ}(y,a). - X=P×Y and g:P×Y×A→B + Sec. 2.2.2. []:P×X⊸P as a partial-evaluation combinator. """ - def __init__(self, gamma): - self.gamma = gamma - self.X, self.A = gamma.cod.exponent - self.B = gamma.cod.base - - arg = ( - self.gamma @ self.X @ self.A - >> computer.Box("[]", self.gamma.cod @ self.X, self.B << self.A) @ self.A - >> computer.Eval(self.B << self.A) - ) - + def __init__(self, gamma, X: computer.Ty, A: computer.Ty, B: computer.Ty, P: computer.ProgramTy): + self.gamma, self.X, self.A, self.B, self.P = gamma, X, A, B, P + arg = self.gamma @ self.X @ self.A >> computer.Computer(self.P, self.X @ self.A, self.B) monoidal.Bubble.__init__(self, arg, dom=arg.dom, cod=arg.cod) def specialize(self): - """Fig. 2.5: compile partial-evaluator box as operator + eval.""" - return self.gamma @ self.X @ self.A >> computer.Eval(self.B << self.X @ self.A) + """Fig. 2.5: compile partial-evaluator box as direct universal evaluation.""" + return self.gamma @ self.X @ self.A >> computer.Computer(self.P, self.X @ self.A, self.B) class Sequential(monoidal.Bubble, computer.Box): """ - Sec. 2.2.3. (;)_ABC:P×P⊸P - A -{F;G}→ C = A -{F}→ B -{G}→ C + Sec. 2.2.3. (;)_ABC:P×P⊸P. """ - def __init__(self, F, G): - self.F, self.G = F, G - A = F.cod.exponent - C = G.cod.base + def __init__(self, F, G, A: computer.Ty, B: computer.Ty, C: computer.Ty, P: computer.ProgramTy): + self.F, self.G, self.A, self.B, self.C, self.P = F, G, A, B, C, P arg = ( F @ G @ A - >> computer.Box("(;)", F.cod @ G.cod, C << A) @ A - >> computer.Eval(C << A) + >> computer.Id(P) @ computer.Computer(P, A, B) + >> computer.Computer(P, B, C) ) - - monoidal.Bubble.__init__(self, arg, dom=arg.dom, draw_vertically=True) + monoidal.Bubble.__init__(self, arg, dom=arg.dom, cod=arg.cod, draw_vertically=True) def specialize(self): - F, G = self.F, self.G - A = F.cod.exponent - B = F.cod.base - C = G.cod.base - - F_eval = computer.Eval(B << A) - G_eval = computer.Eval(C << B) - return G @ F @ A >> (C << B) @ F_eval >> G_eval + return ( + self.G @ self.F @ self.A + >> computer.Id(self.P) @ computer.Computer(self.P, self.A, self.B) + >> computer.Computer(self.P, self.B, self.C) + ) class Parallel(monoidal.Bubble, computer.Box): """ - Sec. 2.2.3. (||)_AUBV:P×P⊸P - A×U -{F||H}→ B×V = A -{F}→ B × U-{T}→ V + Sec. 2.2.3. (||)_AUBV:P×P⊸P. """ - def __init__(self, F, T): + def __init__( + self, + F, + T, + A: computer.Ty, + U: computer.Ty, + B: computer.Ty, + V: computer.Ty, + P: computer.ProgramTy, + ): self.F, self.T = F, T - A, B = F.cod.exponent, F.cod.base - U, V = T.cod.exponent, T.cod.base + self.A, self.U, self.B, self.V, self.P = A, U, B, V, P arg = ( F @ T @ A @ U - >> computer.Box("(||)", F.cod @ T.cod, B @ V << A @ U) @ A @ U - >> computer.Eval(B @ V << A @ U) + >> computer.Id(P) @ computer.Swap(P, A) @ U + >> computer.Computer(P, A, B) @ computer.Computer(P, U, V) ) - monoidal.Bubble.__init__(self, arg, dom=arg.dom, draw_vertically=True) + monoidal.Bubble.__init__(self, arg, dom=arg.dom, cod=arg.cod, draw_vertically=True) def specialize(self): - F, T = self.F, self.T - A, B = F.cod.exponent, F.cod.base - U, V = T.cod.exponent, T.cod.base - - first = computer.Eval(B << A) - second = computer.Eval(V << U) - swap = computer.Swap(V << U, A) - return F @ T @ A @ U >> (B << A) @ swap @ U >> first @ second + return ( + self.F @ self.T @ self.A @ self.U + >> computer.Id(self.P) @ computer.Swap(self.P, self.A) @ self.U + >> computer.Computer(self.P, self.A, self.B) @ computer.Computer(self.P, self.U, self.V) + ) class Data(monoidal.Bubble, computer.Box): """ - Eq. 2.6. ⌜−⌝ : A⊸P - {}: P-→→A - ⌜a⌝: P - {⌜a⌝} = a + Eq. 2.6. ⌜−⌝ : A⊸P and {}:P×I→A. """ - def __init__(self, A): + def __init__(self, A, P: computer.ProgramTy): self.A = A if isinstance(A, computer.Ty) else computer.Ty(A) - arg = ( - computer.Box("⌜−⌝", self.A, self.A << computer.Ty()) - >> computer.Eval(self.A << computer.Ty()) - ) + self.P = P + arg = computer.Box("⌜−⌝", self.A, self.P) >> computer.Computer(self.P, computer.Ty(), self.A) monoidal.Bubble.__init__(self, arg, dom=self.A, cod=self.A) def specialize(self): diff --git a/widip/comput/equations.py b/widip/comput/equations.py index 908ee57..3510a3b 100644 --- a/widip/comput/equations.py +++ b/widip/comput/equations.py @@ -1,51 +1,50 @@ -"""Chapter 2 helper equations for the Run language.""" +"""Chapter 2 helper equations for evaluator-based program semantics.""" from . import computer -def run(G: computer.Diagram, A: computer.Ty, B: computer.Ty): - """Eq. 2.15: an X-natural family of surjections C(X × A, B) --→ C•(X,P).""" - del G - return computer.Eval(A, B) +def run(program_ty: computer.ProgramTy, A: computer.Ty, B: computer.Ty): + """Eq. 2.15 in evaluator form: an evaluator for language ``program_ty``.""" + return computer.Computer(program_ty, A, B) -def eval_f(G: computer.Diagram): - """Eq. 2.15: evaluators as surjections from programs to computations.""" - return computer.Eval(G.dom, G.cod) +def eval_f(program_ty: computer.ProgramTy, A: computer.Ty, B: computer.Ty): + """Eq. 2.15: evaluator for computations on ``A -> B`` in language ``program_ty``.""" + return computer.Computer(program_ty, A, B) -def parametrize(g: computer.Diagram): +def parametrize(g: computer.Diagram, program_ty: computer.ProgramTy): """ - Eq. 2.2: present an X-parametrized computation as a program G:X⊸P. + Eq. 2.2 in evaluator form: turn a parametrized computation into program+eval. """ G = g.curry(left=False) A = g.dom[1:] - return G >> computer.Eval(G.cod @ A >> g.cod) + return G >> computer.Computer(program_ty, A, g.cod) -def reparametrize(g: computer.Diagram, s: computer.Diagram): +def reparametrize(g: computer.Diagram, s: computer.Diagram, program_ty: computer.ProgramTy): """ - Fig. 2.3: reparametrize x along s:Y⊸X to obtain a Y-indexed family. + Fig. 2.3 in evaluator form: reparametrize x along ``s:Y⊸X``. """ A = g.dom[1:] Gs = s @ A >> g.curry(left=False) - return Gs >> computer.Eval(Gs.cod @ A >> g.cod) + return Gs >> computer.Computer(program_ty, A, g.cod) -def substitute(g: computer.Diagram, s: computer.Diagram): +def substitute(g: computer.Diagram, s: computer.Diagram, program_ty: computer.ProgramTy): """ - Fig. 2.3: substitute for a along t:C→A while keeping the same parameter space. + Fig. 2.3 in evaluator form: substitute for ``a`` along ``s:C→A``. """ A = g.dom[1:] Gs = s @ A >> g.curry(left=False) - return Gs >> computer.Eval(Gs.cod @ A >> g.cod) + return Gs >> computer.Computer(program_ty, A, g.cod) def constant_a(f: computer.Diagram): - """Sec. 2.2.1.3 a) f:I×A→B. f(a) = {Φ_a}().""" + """Sec. 2.2.1.3 a) f:I×A→B.""" return f.curry(0, left=False) def constant_b(f: computer.Diagram): - """Sec. 2.2.1.3 b) f:A×I→B. f(a) = {F}(a).""" + """Sec. 2.2.1.3 b) f:A×I→B.""" return f.curry(1, left=False) diff --git a/widip/files.py b/widip/files.py index b8328aa..3656bf5 100644 --- a/widip/files.py +++ b/widip/files.py @@ -1,7 +1,9 @@ import pathlib +from nx_yaml import nx_compose_all + from .comput.computer import Box, Diagram -from .metaprog import repl_read +from .metaprog.loader import HIF_TO_LOADER, LOADER_TO_SHELL def files_ar(ar: Box) -> Diagram: @@ -17,7 +19,8 @@ def files_ar(ar: Box) -> Diagram: def file_diagram(file_name) -> Diagram: path = pathlib.Path(file_name) - fd = repl_read(path.open()) + with path.open() as stream: + fd = LOADER_TO_SHELL(HIF_TO_LOADER(nx_compose_all(stream))) return fd def diagram_draw(path, fd): diff --git a/widip/metaprog/__init__.py b/widip/metaprog/__init__.py index 9a09773..6e3a509 100644 --- a/widip/metaprog/__init__.py +++ b/widip/metaprog/__init__.py @@ -2,145 +2,13 @@ Chapter 6. Computing programs. Metaprograms are programs that compute programs. """ -from nx_yaml import nx_compose_all -from ..comput.computer import Box, Computer, ComputableFunction, Diagram, Functor, Program, ProgramTy, Ty -from ..state.widish import ShellRunner -from ..wire.hif import HyperGraph +from . import core +from . import widish as metaprog_widish -class Metaprogram(Box): - """ - a metaprogram, presented as a cartesian function G:I⊸P - """ - - def __init__(self, name, P: ProgramTy): - Box.__init__(self, name, dom=Ty(), cod=P) - - -class Specializer(Functor): - """A functorial metaprogram with unit parameter type.""" - - @staticmethod - def metaprogram_dom(): - return Ty() - - def __init__(self, ob=None, ar=None, *, dom=None, cod=None): - Functor.__init__( - self, - self.ob_map if ob is None else ob, - self.ar_map if ar is None else ar, - dom=Functor.dom if dom is None else dom, - cod=Functor.cod if cod is None else cod, - ) - - @staticmethod - def ob_map(ob): - return ob - - @staticmethod - def ar_map(ar): - return ar - - def specialize(self, *args, **kwargs): - return self(*args, **kwargs) - - -class ProgramComputation(Diagram): - """ - Section 3.1: a function is computable when it is programmable. - Fig 6.1: A computation f encoded such that f = {F}. - """ - - def __init__(self, name, P: ProgramTy, X: Ty, A: Ty, B: Ty): - """ - Running a given program is a routine operation. - Every function is computable, in the sense that there is a program for it. - """ - self.universal_ev_diagram = ComputableFunction("{" + name + "}", X, A, B) - diagram = ( - Program(name, P, X) @ A, - Computer(P, A, B), - ) - inside = sum(map(lambda d: d.inside, diagram), ()) - Diagram.__init__(self, inside, X @ A, B) - - def universal_ev(self): - return self.universal_ev_diagram - - def specialize(self): - """Pure wiring rewrite from a program layer to its encoded computation.""" - return self.universal_ev() - - -class MetaprogramComputation(Diagram): - """ - Fig 6.1: A program F encoded such that F = {ℱ}. - """ - - def __init__(self, name, P: ProgramTy, PP: ProgramTy, X: Ty, A: Ty, B: Ty): - self.universal_ev_diagram = ComputableFunction("{{" + name + "}}", X, A, B) - self.partial_ev_diagram = ( - ProgramComputation("{" + name + "}", PP, Ty(), X, P) @ A - >> Computer(P, A, B) - ) - diagram = ( - Program(name, PP, Ty()) @ X @ A, - Computer(PP, X, P) @ A, - Computer(P, A, B), - ) - inside = sum(map(lambda d: d.inside, diagram), ()) - Diagram.__init__(self, inside, X @ A, B) - - def universal_ev(self): - return self.universal_ev_diagram - - def partial_ev(self): - return self.partial_ev_diagram - - def specialize(self): - """Pure wiring rewrite from a metaprogram layer to partial evaluation.""" - return self.partial_ev() - - -class ProgramFunctor: - """ - Pure wiring transformation that strips one program-evaluation layer. - """ - - def __call__(self, other): - if isinstance(other, ProgramComputation): - return other.specialize() - return other - - -class MetaprogramFunctor: - """ - Pure wiring transformation that strips one metaprogram-evaluation layer. - """ - - def __call__(self, other): - if isinstance(other, MetaprogramComputation): - return other.specialize() - return other - - -from .hif import HIFToLoader -from .loader import LoaderToShell -from .widish import ShellSpecializer - - -SHELL_SPECIALIZER = ShellSpecializer() -SHELL_TO_PYTHON = ShellRunner(SHELL_SPECIALIZER) -HIF_TO_LOADER = HIFToLoader() -LOADER_TO_SHELL = LoaderToShell() - - -def incidences_to_program(graph: HyperGraph): - """Turn an ``nx_yaml`` hypergraph into a loader-language diagram.""" - return HIF_TO_LOADER(graph) - - -def repl_read(stream): - """Parse a YAML stream and compile it to the shell backend.""" - return LOADER_TO_SHELL(incidences_to_program(nx_compose_all(stream))) +SHELL_SPECIALIZER = metaprog_widish.ShellSpecializer() +SHELL_INTERPRETER = metaprog_widish.ShellInterpreter(SHELL_SPECIALIZER) +SHELL_TO_PYTHON = SHELL_INTERPRETER +PROGRAM_FUNCTOR = core.ProgramFunctor() +METAPROGRAM_FUNCTOR = core.MetaprogramFunctor() diff --git a/widip/metaprog/core.py b/widip/metaprog/core.py new file mode 100644 index 0000000..6d37bbc --- /dev/null +++ b/widip/metaprog/core.py @@ -0,0 +1,239 @@ +"""Language-agnostic metaprogram abstractions and Futamura equations.""" + +from ..comput.computer import Box, Computer, ComputableFunction, Diagram, Functor, Program, ProgramTy, Ty + + +class Metaprogram(Box): + """ + A metaprogram, presented as a cartesian function G:I⊸P. + """ + + def __init__(self, name, P: ProgramTy): + Box.__init__(self, name, dom=Ty(), cod=P) + + +class SpecializerBox(Metaprogram): + """Generic native specializer box: ``S : I -> P``.""" + + def __init__(self, P: ProgramTy, name="S"): + Metaprogram.__init__(self, name, P) + + +class InterpreterBox(Metaprogram): + """Generic native interpreter box: ``H : I -> P``.""" + + def __init__(self, P: ProgramTy, name="H"): + Metaprogram.__init__(self, name, P) + + +class Specializer(Functor): + """A functorial metaprogram with unit parameter type.""" + + @staticmethod + def metaprogram_dom(): + return Ty() + + def __init__(self, ob=None, ar=None, *, dom=None, cod=None): + Functor.__init__( + self, + self.ob_map if ob is None else ob, + self.ar_map if ar is None else ar, + dom=Functor.dom if dom is None else dom, + cod=Functor.cod if cod is None else cod, + ) + + @staticmethod + def ob_map(ob): + return ob + + @staticmethod + def ar_map(ar): + return ar + + def specialize(self, *args, **kwargs): + return self(*args, **kwargs) + + +class Interpreter(Functor): + """A functorial interpreter with unit metaprogram domain.""" + + @staticmethod + def metaprogram_dom(): + return Ty() + + def __init__(self, ob=None, ar=None, *, dom=None, cod=None): + Functor.__init__( + self, + self.ob_map if ob is None else ob, + self.ar_map if ar is None else ar, + dom=Functor.dom if dom is None else dom, + cod=Functor.cod if cod is None else cod, + ) + + @staticmethod + def ob_map(ob): + return ob + + @staticmethod + def ar_map(ar): + return ar + + def interpret(self, *args, **kwargs): + return self(*args, **kwargs) + + +class ProgramComputation(Diagram): + """ + Section 3.1: a function is computable when it is programmable. + Fig 6.1: A computation f encoded such that f = {F}. + """ + + def __init__(self, name, P: ProgramTy, X: Ty, A: Ty, B: Ty): + """ + Running a given program is a routine operation. + Every function is computable, in the sense that there is a program for it. + """ + self.universal_ev_diagram = ComputableFunction("{" + name + "}", X, A, B) + diagram = ( + Program(name, P, X) @ A, + Computer(P, A, B), + ) + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + + def universal_ev(self): + return self.universal_ev_diagram + + def specialize(self): + """Pure wiring rewrite from a program layer to its encoded computation.""" + return self.universal_ev() + + +class MetaprogramComputation(Diagram): + """ + Fig 6.1: A program F encoded such that F = {ℱ}. + """ + + def __init__(self, name, P: ProgramTy, PP: ProgramTy, X: Ty, A: Ty, B: Ty): + self.universal_ev_diagram = ComputableFunction("{{" + name + "}}", X, A, B) + self.partial_ev_diagram = ( + ProgramComputation("{" + name + "}", PP, Ty(), X, P) @ A + >> Computer(P, A, B) + ) + diagram = ( + Program(name, PP, Ty()) @ X @ A, + Computer(PP, X, P) @ A, + Computer(P, A, B), + ) + inside = sum(map(lambda d: d.inside, diagram), ()) + Diagram.__init__(self, inside, X @ A, B) + + def universal_ev(self): + return self.universal_ev_diagram + + def partial_ev(self): + return self.partial_ev_diagram + + def specialize(self): + """Pure wiring rewrite from a metaprogram layer to partial evaluation.""" + return self.partial_ev() + + +class ProgramFunctor: + """ + Pure wiring transformation that strips one program-evaluation layer. + """ + + def __call__(self, other): + if isinstance(other, ProgramComputation): + return other.specialize() + return other + + +class MetaprogramFunctor: + """ + Pure wiring transformation that strips one metaprogram-evaluation layer. + """ + + def __call__(self, other): + if isinstance(other, MetaprogramComputation): + return other.specialize() + return other + + +def sec_6_2_2_partial_application( + program, + static_input, + *, + specializer_box, + evaluator_box, + evaluator, +): + """Sec. 6.2.2 as a diagram: ``pev(X, y)``.""" + return ( + (specializer_box @ program >> evaluator_box) @ static_input + >> evaluator(static_input.cod, specializer_box.cod) + ) + + +def eq_2( + program, + static_input, + *, + specializer_box, + evaluator_box, + evaluator, +): + """Eq. (2): ``pev X y = uev S (X, y)``.""" + return sec_6_2_2_partial_application( + program, + static_input, + specializer_box=specializer_box, + evaluator_box=evaluator_box, + evaluator=evaluator, + ) + + +def eq_3( + program, + static_input, + *, + specializer_box, + evaluator_box, + evaluator, +): + """Eq. (3): ``uev S (X, y) = uev (pev S X) y``.""" + return ( + ((specializer_box @ specializer_box >> evaluator_box) @ program >> evaluator_box) + @ static_input + >> evaluator(static_input.cod, specializer_box.cod) + ) + + +def first_futamura_projection(interpreter, *, specializer_box, evaluator_box): + """`C1` as a diagram: partially evaluate the specializer on an interpreter.""" + return specializer_box @ interpreter >> evaluator_box + + +def eq_4(interpreter, *, specializer_box, evaluator_box): + """Eq. (4): ``C2 = pev S H = uev S (S, H)``.""" + return (specializer_box @ specializer_box >> evaluator_box) @ interpreter >> evaluator_box + + +def compiler(interpreter, *, specializer_box, evaluator_box): + """`C2`: second Futamura projection as a diagram.""" + return eq_4(interpreter, specializer_box=specializer_box, evaluator_box=evaluator_box) + + +def compiler_generator(*, specializer_box, evaluator_box): + """`C3 = pev S S`: third Futamura projection as a diagram.""" + return specializer_box @ specializer_box >> evaluator_box + + +def eq_5(interpreter, *, specializer_box, evaluator_box): + """Eq. (5): ``uev S (S, H) = uev (pev S S) H``.""" + return ( + compiler_generator(specializer_box=specializer_box, evaluator_box=evaluator_box) + @ interpreter + >> evaluator_box + ) diff --git a/widip/metaprog/loader.py b/widip/metaprog/loader.py index 0532388..24c6279 100644 --- a/widip/metaprog/loader.py +++ b/widip/metaprog/loader.py @@ -1,11 +1,15 @@ """Loader-specific program transformations.""" -from . import Specializer +from nx_yaml import nx_compose_all + +from .core import Specializer +from .hif import HIFToLoader from ..comput import computer from ..comput import loader as loader_lang from ..comput import widish as shell_lang from ..state.core import map_process_box from ..state.widish import shell_stage as shell_io_stage +from ..wire.hif import HyperGraph from ..wire import loader as loader_wire from ..wire import widish as shell_wire from .widish import Parallel, Pipeline @@ -45,12 +49,8 @@ def __call__(self, other): if isinstance(other, loader_wire.LoaderScalar): return _compile_scalar(other) if isinstance(other, loader_wire.LoaderSequence): - if other.tag is not None: - raise TypeError(f"tagged YAML sequences are unsupported: !{other.tag}") return Pipeline(tuple(self(stage) for stage in other.stages)) if isinstance(other, loader_wire.LoaderMapping): - if other.tag is not None: - raise TypeError(f"tagged YAML mappings are unsupported: !{other.tag}") return Parallel(tuple(self(branch) for branch in other.branches)) return Specializer.__call__(self, other) @@ -61,3 +61,7 @@ def ar_map(self, ar): if isinstance(ar, loader_lang.LoaderLiteral): return shell_lang.Literal(ar.text) return ar + + +HIF_TO_LOADER = HIFToLoader() +LOADER_TO_SHELL = LoaderToShell() diff --git a/widip/metaprog/python.py b/widip/metaprog/python.py index 2a03b84..a6b8ea4 100644 --- a/widip/metaprog/python.py +++ b/widip/metaprog/python.py @@ -1,19 +1,16 @@ """ -Diagram-first Python realization of Sec. 6.2.2 and Futamura projections. - -`PythonSpecializer` and `PythonInterpreter` are native metaprogram boxes -(`I -> P_python`). Equations are composed as diagrams and interpreted by a -runtime functor. +Diagram-first Python realization of metaprogram specialization and runtime. """ from functools import partial -from discopy import monoidal, python +from discopy import python from discopy.utils import tuplify, untuplify from ..comput import ProgramClosedCategory from ..comput import computer from ..comput import python as comput_python +from . import core as metaprog_core PYTHON_OBJECT = comput_python.program_ty @@ -24,18 +21,18 @@ def _evaluator(A, B): return PYTHON_PROGRAMS.evaluator(A, B) -class PythonSpecializer(computer.Box): +class PythonSpecializer(metaprog_core.SpecializerBox): """Native specializer metaprogram: ``S : I -> obj``.""" def __init__(self): - computer.Box.__init__(self, "S", computer.Ty(), PYTHON_OBJECT) + metaprog_core.SpecializerBox.__init__(self, PYTHON_OBJECT, name="S") -class PythonInterpreter(computer.Box): +class PythonInterpreter(metaprog_core.InterpreterBox): """Native interpreter metaprogram: ``H : I -> obj``.""" def __init__(self): - computer.Box.__init__(self, "H", computer.Ty(), PYTHON_OBJECT) + metaprog_core.InterpreterBox.__init__(self, PYTHON_OBJECT, name="H") PYTHON_SPECIALIZER_BOX = PythonSpecializer() @@ -60,61 +57,68 @@ def _apply_value(function, argument): return tuplify((partial(function, argument), )) -def sec_6_2_2_partial_application(program, static_input): - """Sec. 6.2.2 as a diagram: ``pev(X, y)``.""" - return ( - (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input - >> _evaluator(static_input.cod, PYTHON_OBJECT) - ) - - -def eq_2(program, static_input): - """Eq. (2): ``pev X y = uev S (X, y)`` (left side as native specializer box).""" - return sec_6_2_2_partial_application(program, static_input) - - -def eq_3(program, static_input): - """Eq. (3): ``uev S (X, y) = uev (pev S X) y``.""" - return ( - ((PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ program - >> PYTHON_EVALUATOR_BOX) @ static_input - >> _evaluator(static_input.cod, PYTHON_OBJECT) - ) - - -def first_futamura_projection(interpreter): - """`C1` as a diagram: partially evaluate the specializer on an interpreter.""" - return PYTHON_SPECIALIZER_BOX @ interpreter >> PYTHON_EVALUATOR_BOX - - -def eq_4(interpreter): - """Eq. (4): ``C2 = pev S H = uev S (S, H)``.""" - return ( - (PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ interpreter - >> PYTHON_EVALUATOR_BOX - ) - - -def compiler(interpreter): - """`C2`: second Futamura projection as a diagram.""" - return eq_4(interpreter) - - -def compiler_generator(): - """`C3 = pev S S`: third Futamura projection as a diagram.""" - return PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX - - -def eq_5(interpreter): - """Eq. (5): ``uev S (S, H) = uev (pev S S) H``.""" - return compiler_generator() @ interpreter >> PYTHON_EVALUATOR_BOX - - -class PythonRuntime(monoidal.Functor): +def apply_value(function, argument): + """Public evaluator application helper shared by runtime interpreters.""" + return _apply_value(function, argument) + + +sec_6_2_2_partial_application = partial( + metaprog_core.sec_6_2_2_partial_application, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, + evaluator=_evaluator, +) + +eq_2 = partial( + metaprog_core.eq_2, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, + evaluator=_evaluator, +) + +eq_3 = partial( + metaprog_core.eq_3, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, + evaluator=_evaluator, +) + +first_futamura_projection = partial( + metaprog_core.first_futamura_projection, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) + +eq_4 = partial( + metaprog_core.eq_4, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) + +compiler = partial( + metaprog_core.compiler, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) + +compiler_generator = partial( + metaprog_core.compiler_generator, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) + +eq_5 = partial( + metaprog_core.eq_5, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) + + +class PythonRuntime(metaprog_core.Interpreter): """Runtime functor from computer diagrams to executable Python functions.""" def __init__(self): - monoidal.Functor.__init__( + metaprog_core.Interpreter.__init__( self, ob=self.ob_map, ar=self.ar_map, @@ -149,7 +153,7 @@ def ar_map(self, box): if isinstance(box, PythonInterpreter): return python.Function(lambda: _universal_evaluate, dom, cod) if isinstance(box, computer.Computer): - return python.Function(_apply_value, dom, cod) + return python.Function(apply_value, dom, cod) if isinstance(box, computer.Copy): return python.Function.copy(dom, n=2) if isinstance(box, computer.Delete): diff --git a/widip/metaprog/widish.py b/widip/metaprog/widish.py index 1e69a3c..84df795 100644 --- a/widip/metaprog/widish.py +++ b/widip/metaprog/widish.py @@ -1,13 +1,17 @@ """Shell-specific program transformations and interpreters.""" +from collections.abc import Callable from itertools import count -from discopy import monoidal +from discopy import monoidal, python -from . import Specializer +from .core import Specializer +from . import core as metaprog_core +from . import python as metaprog_python from ..comput import computer from ..comput import widish as shell_lang -from ..state.widish import parallel_io_diagram +from ..state import core as state_core +from ..state.widish import parallel_io_diagram, shell_program_runner from ..wire import widish as shell_wire @@ -51,6 +55,15 @@ def _specialize_shell(diagram, next_temp): return diagram +def _has_shell_bubble(diagram) -> bool: + """Detect whether a shell diagram still contains unspecialized bubbles.""" + if isinstance(diagram, monoidal.Bubble): + return True + if not isinstance(diagram, computer.Diagram): + return False + return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) + + class ShellSpecializer(Specializer): """Lower shell bubbles to their executable wiring.""" @@ -76,6 +89,49 @@ def ar_map(ar): return ar +class ShellInterpreter(metaprog_core.Interpreter): + """Interpret shell diagrams as Python callables.""" + + def __init__(self, specialize_shell): + self.specialize_shell = specialize_shell + metaprog_core.Interpreter.__init__( + self, + ob=self.ob_map, + ar=self.ar_map, + dom=computer.Category(), + cod=python.Category(), + ) + + @staticmethod + def ob_map(ob): + if ( + isinstance(ob, computer.Ty) + and len(ob) == 1 + and isinstance(ob.inside[0], computer.ProgramOb) + ): + return Callable + return str + + def __call__(self, other): + if _has_shell_bubble(other): + return monoidal.Functor.__call__(self, self.specialize_shell(other)) + return monoidal.Functor.__call__(self, other) + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + projection = state_core.ProcessRunner.projection_ar_map(self, box, dom, cod) + structural = state_core.ProcessRunner.structural_ar_map(self, box, dom, cod) + if projection is not None: + return projection + if structural is not None: + return structural + if isinstance(box, shell_lang.ShellProgram): + return python.Function(lambda: shell_program_runner(box), dom, cod) + if isinstance(box, computer.Computer): + return python.Function(metaprog_python.apply_value, dom, cod) + raise TypeError(f"unsupported shell interpreter box: {box!r}") + + class Pipeline(monoidal.Bubble, computer.Box): """Bubble grouping shell stages in sequence.""" diff --git a/widip/state/widish.py b/widip/state/widish.py index e5c6dfa..c783b84 100644 --- a/widip/state/widish.py +++ b/widip/state/widish.py @@ -1,13 +1,10 @@ """Shell-specific stateful execution.""" import subprocess -from collections.abc import Callable - -from discopy import monoidal, python from ..comput import computer from ..comput import widish as shell_lang -from .core import Execution, InputOutputMap, ProcessRunner +from .core import Execution, InputOutputMap from ..wire import widish as shell_wire @@ -55,63 +52,25 @@ def parallel_io_diagram(branches, next_temp): return result -def _has_shell_bubble(diagram) -> bool: - """Detect whether a shell diagram still contains unspecialized bubbles.""" - if isinstance(diagram, monoidal.Bubble): - return True - if not isinstance(diagram, computer.Diagram): - return False - return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) - - -class ShellRunner(ProcessRunner): - """Interpret stateful shell diagrams as Python callables on text streams.""" - - def __init__(self, specialize_shell): - self.specialize_shell = specialize_shell - ProcessRunner.__init__(self, self.ob_map) - - @staticmethod - def ob_map(ob): - if ( - isinstance(ob, computer.Ty) - and len(ob) == 1 - and isinstance(ob.inside[0], computer.ProgramOb) - ): - return Callable - return str - - def __call__(self, box): - if _has_shell_bubble(box): - return monoidal.Functor.__call__(self, self.specialize_shell(box)) - return monoidal.Functor.__call__(self, box) - - @staticmethod - def shell_program_runner(program): - """Compile one shell-language program to a Python text transformer.""" - if isinstance(program, shell_lang.Empty): - return lambda stdin: stdin - if isinstance(program, shell_lang.Literal): - return lambda _stdin: program.text - if isinstance(program, shell_lang.Command): - def run(stdin: str) -> str: - completed = subprocess.run( - program.argv, - input=stdin, - text=True, - capture_output=True, - check=True, - ) - return completed.stdout - - return run - raise TypeError(f"unsupported shell program: {program!r}") - - def process_ar_map(self, box, dom, cod): - if isinstance(box, shell_lang.ShellProgram): - return python.Function(lambda: self.shell_program_runner(box), dom, cod) - return ProcessRunner.process_ar_map(self, box, dom, cod) +def shell_program_runner(program): + """Compile one shell-language program to a Python text transformer.""" + if isinstance(program, shell_lang.Empty): + return lambda stdin: stdin + if isinstance(program, shell_lang.Literal): + return lambda _stdin: program.text + if isinstance(program, shell_lang.Command): + def run(stdin: str) -> str: + completed = subprocess.run( + program.argv, + input=stdin, + text=True, + capture_output=True, + check=True, + ) + return completed.stdout + return run + raise TypeError(f"unsupported shell program: {program!r}") class ShellExecution(Execution): """Stateful shell evaluator P x io -> P x io.""" diff --git a/widip/watch.py b/widip/watch.py index 87e159f..09541fa 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -3,11 +3,13 @@ from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer from yaml import YAMLError +from nx_yaml import nx_compose_all from discopy.utils import tuplify, untuplify from .files import diagram_draw, file_diagram -from .metaprog import SHELL_TO_PYTHON, repl_read +from .metaprog import SHELL_TO_PYTHON +from .metaprog.loader import HIF_TO_LOADER, LOADER_TO_SHELL # TODO watch functor ?? @@ -42,7 +44,7 @@ def shell_main(file_name, draw=True): try: prompt = f"--- !{file_name}\n" source = input(prompt) - source_d = repl_read(source) + source_d = LOADER_TO_SHELL(HIF_TO_LOADER(nx_compose_all(source))) # source_d.draw( # textpad=(0.3, 0.1), # fontsize=12, From 253b69ef6f1e7d13a0ad4f17bb3dd3a683cba8cf Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Thu, 19 Mar 2026 15:48:31 +0000 Subject: [PATCH 10/17] feat: pcc --- tests/test_compiler.py | 30 ++++---- tests/test_interpreter.py | 27 +++++-- tests/test_lang.py | 79 +++++++++++++++----- tests/test_loader.py | 59 +++++++++------ tests/test_metaprog.py | 18 +++-- tests/test_runner.py | 14 ++-- tests/test_state.py | 30 ++++---- widip/comput/__init__.py | 58 +-------------- widip/files.py | 5 +- widip/metaprog/__init__.py | 2 - widip/metaprog/core.py | 36 +++++----- widip/metaprog/hif.py | 56 ++++++++++++--- widip/metaprog/loader.py | 67 ----------------- widip/metaprog/python.py | 45 +++++++----- widip/metaprog/widish.py | 101 +++++--------------------- widip/pcc/__init__.py | 5 ++ widip/pcc/core.py | 75 +++++++++++++++++++ widip/pcc/loader.py | 21 ++++++ widip/pcc/widish.py | 21 ++++++ widip/state/__init__.py | 21 +----- widip/state/core.py | 133 ++++++++++++++++++++++------------ widip/state/loader.py | 51 ++++++++++++- widip/state/python.py | 144 +++++++++++++++++++++++++++++++++++++ widip/state/widish.py | 86 +++++++++++----------- widip/watch.py | 13 ++-- 25 files changed, 748 insertions(+), 449 deletions(-) delete mode 100644 widip/metaprog/loader.py create mode 100644 widip/pcc/__init__.py create mode 100644 widip/pcc/core.py create mode 100644 widip/pcc/loader.py create mode 100644 widip/pcc/widish.py create mode 100644 widip/state/python.py diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 05342c1..b1a2a17 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -30,10 +30,11 @@ def test_fig_2_7_compile_sequential_to_left_side(request): Fig. 2.7 sequential equation: """ A, B, C = Ty("A"), Ty("B"), Ty("C") - F = Box("F", A, B << A) - G = Box("G", B, C << B) - left = G @ F @ A >> (C << B) @ Eval(B << A) >> Eval(C << B) - right = Sequential(F, G) + P = ProgramTy("P") + F = Program("F", P, Ty()) + G = Program("G", P, Ty()) + left = G @ F @ A >> Id(P) @ Computer(P, A, B) >> Computer(P, B, C) + right = Sequential(F, G, A, B, C, P) compiler = Compile() compiled = compiler(right) @@ -48,17 +49,17 @@ def test_fig_2_7_compile_parallel_to_left_side(request): right side is `Parallel(A@U, B@V)`. """ A, U, B, V = Ty("A"), Ty("U"), Ty("B"), Ty("V") - F = Box("F", Ty(), B << A) - G = Box("G", Ty(), V << U) - right = Parallel(F, G) + P = ProgramTy("P") + F = Program("F", P, Ty()) + G = Program("G", P, Ty()) + right = Parallel(F, G, A, U, B, V, P) compiler = Compile() left = ( F @ G @ A @ U - >> ((B << A) @ Swap(V << U, A) @ U) - >> (Eval(B << A) @ Eval(V << U)) + >> (Id(P) @ Swap(P, A) @ U) + >> (Computer(P, A, B) @ Computer(P, U, V)) ) - # (left @ (Eval(B << A) @ Eval(V << U))).draw() compiled = compiler(right) assert compiled == left @@ -66,7 +67,7 @@ def test_fig_2_7_compile_parallel_to_left_side(request): def test_eq_2_6_compile_data_is_identity(request): """Eq. 2.6: uncurrying quoted data compiles to its uncurried form (box @ Id) >> Eval.""" - right = Data("A") + right = Data("A", ProgramTy("P")) left = Id("A") compiler = Compile() compiled = compiler(right) @@ -78,9 +79,10 @@ def test_eq_2_6_compile_data_is_identity(request): def test_eq_2_5_compile_partial_is_eval(request): """Eq. 2.5: uncurrying `[]` compiles to direct evaluator on `X @ A`.""" A, B, X = Ty("A"), Ty("B"), Ty("X") - gamma = Box("gamma", Ty(), B << X @ A) - left = gamma @ X @ A >> Eval(gamma.cod) - right = Partial(gamma) + P = ProgramTy("P") + gamma = Program("gamma", P, Ty()) + left = gamma @ X @ A >> Computer(P, X @ A, B) + right = Partial(gamma, X, A, B, P) compiler = Compile() compiled = compiler(right) assert compiled == left diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 0ea655c..69a2746 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -1,10 +1,13 @@ -from widip.compiler import Eval_H, Eval_L, H_ty, L_ty -from widip.comput.computer import Computer, Ty +from widip.comput.computer import Computer, ProgramTy, Ty +from widip.pcc import ProgramClosedCategory + + +H_ty, L_ty = ProgramTy("H"), ProgramTy("L") def test_high_level_interpreter_is_typed_evaluator(): A, B = Ty("A"), Ty("B") - evaluator = Eval_H(A, B) + evaluator = ProgramClosedCategory(H_ty).evaluator(A, B) assert isinstance(evaluator, Computer) assert evaluator.dom == Computer(H_ty, A, B).dom assert evaluator.cod == Computer(H_ty, A, B).cod @@ -12,7 +15,23 @@ def test_high_level_interpreter_is_typed_evaluator(): def test_low_level_interpreter_is_typed_evaluator(): A, B = Ty("A"), Ty("B") - evaluator = Eval_L(A, B) + evaluator = ProgramClosedCategory(L_ty).evaluator(A, B) assert isinstance(evaluator, Computer) assert evaluator.dom == Computer(L_ty, A, B).dom assert evaluator.cod == Computer(L_ty, A, B).cod + + +def test_program_closed_simulation_transports_program_types_and_evaluators(): + A = Ty("A") + high = ProgramClosedCategory(H_ty) + low = ProgramClosedCategory(L_ty) + + assert high.simulate(H_ty, low) == L_ty + assert high.simulate(high.evaluator(A, H_ty @ A), low) == low.evaluator(A, L_ty @ A) + + +def test_program_closed_run_is_execution_specialization(): + A, B = Ty("A"), Ty("B") + category = ProgramClosedCategory(H_ty) + + assert category.run(A, B) == category.execution(A, B).specialize() diff --git a/tests/test_lang.py b/tests/test_lang.py index 53a8e31..10f70c2 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,9 +1,18 @@ -from widip.comput import SHELL from widip.comput.computer import Ty -from widip.comput.widish import Command, Literal, ShellProgram, io_ty, shell_program_ty -from widip.metaprog import SHELL_TO_PYTHON -from widip.metaprog.widish import Parallel, Pipeline, ShellSpecializer, parallel -from widip.state.widish import ShellExecution +from widip.comput import python as comput_python +from widip.comput.widish import Command, Empty, Literal, ShellProgram, io_ty, shell_program_ty +from widip.metaprog import python as metaprog_python +from widip.pcc import SHELL +from widip.state import core as state_core +from widip.state.python import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON +from widip.state.widish import ( + Parallel, + Pipeline, + ShellExecution, + ShellSpecializer, + parallel, + shell_program_runner, +) from widip.wire.widish import Copy @@ -30,7 +39,7 @@ def test_sh_command_runs_through_shell_runner(): execution = SHELL.execution(io_ty, io_ty).output_diagram() program = Command(["sh", "-c", "read line; printf 'shell:%s' \"$line\"", "sh"]) @ io_ty >> execution - assert SHELL_TO_PYTHON(program)("world\n") == "shell:world" + assert SHELL_INTERPRETER(program)("world\n") == "shell:world" def test_shell_language_chooses_shell_program_type_and_execution(): @@ -74,7 +83,7 @@ def test_sequence_bubble_specializes_to_pipeline(): assert bubble.specialize() == first >> second -def test_mapping_bubble_specializes_to_primitive_command_diagram(): +def test_mapping_bubble_specializes_to_parallel_shell_bubble(): execution = SHELL.execution(io_ty, io_ty).output_diagram() branches = ( Literal("a") @ io_ty >> execution, @@ -85,10 +94,11 @@ def test_mapping_bubble_specializes_to_primitive_command_diagram(): specialized = bubble.specialize() names = box_names(specialized) + assert isinstance(specialized, Parallel) assert specialized.dom == io_ty assert specialized.cod == io_ty - assert any(name.startswith("('tee', '/tmp/widip-") for name in names) - assert any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert not any(name.startswith("('tee',") for name in names) + assert not any(name.startswith("('cat', '/tmp/widip-") for name in names) assert "merge[3]" not in names assert "∆" not in names @@ -102,7 +112,7 @@ def test_discorun_parallel_example_runs(): Command(["wc", "-l"]) @ io_ty >> execution, ) ) - assert SHELL_TO_PYTHON(program)("a\nx\n") == "a\nx\n1\n2\n" + assert SHELL_INTERPRETER(program)("a\nx\n") == "a\nx\n1\n2\n" def test_parallel_preserves_argv_literals_without_shell_reparsing(): @@ -114,10 +124,10 @@ def test_parallel_preserves_argv_literals_without_shell_reparsing(): ) ) - assert SHELL_TO_PYTHON(program)("") == "a|bc&d" + assert SHELL_INTERPRETER(program)("") == "a|bc&d" -def test_parallel_specializer_inlines_native_command_diagram(): +def test_parallel_specializer_preserves_parallel_shell_bubble(): execution = SHELL.execution(io_ty, io_ty).output_diagram() program = Parallel( ( @@ -128,18 +138,55 @@ def test_parallel_specializer_inlines_native_command_diagram(): specialized = ShellSpecializer()(program) names = box_names(specialized) - assert any(name.startswith("('tee', '/tmp/widip-") for name in names) - assert any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert isinstance(specialized, Parallel) + assert not any(name.startswith("('tee',") for name in names) + assert not any(name.startswith("('cat', '/tmp/widip-") for name in names) assert "merge[2]" not in names assert "∆" not in names - assert SHELL_TO_PYTHON(program)("") == "leftright" + assert SHELL_INTERPRETER(program)("") == "leftright" def test_stateful_shell_execution_preserves_program_state(): program = Command(["printf", "hello"]) - runner = SHELL_TO_PYTHON(program @ io_ty >> SHELL.execution(io_ty, io_ty)) + runner = SHELL_INTERPRETER(program @ io_ty >> SHELL.execution(io_ty, io_ty)) state, output = runner("") assert callable(state) assert state("") == "hello" assert output == "hello" + + +def test_shell_to_python_program_maps_shell_scalars_to_python_program_boxes(): + transform = SHELL_PROGRAM_TO_PYTHON + source_boxes = (Empty(), Literal("literal"), Command(["printf", "x"])) + + for source in source_boxes: + mapped = transform(source) + assert mapped.dom == Ty() + assert mapped.cod == comput_python.program_ty + assert callable(mapped.value) + assert mapped.value("stdin\n") == shell_program_runner(source)("stdin\n") + + +def test_shell_to_python_program_maps_shell_evaluator_box(): + transform = SHELL_PROGRAM_TO_PYTHON + evaluator = SHELL.evaluator(io_ty, io_ty) + + mapped = transform(evaluator) + + assert mapped == metaprog_python.PYTHON_PROGRAMS.evaluator(io_ty, io_ty) + + +def test_shell_to_python_program_maps_process_projection_boxes(): + transform = SHELL_PROGRAM_TO_PYTHON + state_update = state_core.StateUpdateMap("shell", shell_program_ty, io_ty) + output = state_core.InputOutputMap("shell", shell_program_ty, io_ty, io_ty) + + mapped_state_update = transform(state_update) + mapped_output = transform(output) + + assert mapped_state_update.X == comput_python.program_ty + assert mapped_state_update.A == io_ty + assert mapped_output.X == comput_python.program_ty + assert mapped_output.A == io_ty + assert mapped_output.B == io_ty diff --git a/tests/test_loader.py b/tests/test_loader.py index cce545e..e16e37b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,26 +2,26 @@ from nx_yaml import nx_compose_all -from widip.comput import SHELL from widip.comput.computer import Ty from widip.comput.loader import LoaderLiteral, loader_program_ty from widip.comput.widish import Command, Literal, io_ty, shell_program_ty -from widip.metaprog import HIF_TO_LOADER, LOADER_TO_SHELL, incidences_to_program, repl_read -from widip.metaprog.widish import Parallel, Pipeline -from widip.state import loader_output, loader_state_update +from widip.metaprog.hif import HIFToLoader +from widip.pcc import SHELL from widip.state.core import InputOutputMap, StateUpdateMap +from widip.state.loader import LoaderExecution, LoaderToShell +from widip.state.widish import Parallel, Pipeline from widip.wire.hif import HyperGraph -from widip.wire.loader import LoaderMapping, LoaderScalar, loader_stream_ty +from widip.wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_stream_ty from widip.wire.widish import shell_id def test_loader_empty_stream_is_identity(): - assert repl_read("") == shell_id() + assert LoaderToShell()(HIFToLoader()(nx_compose_all(""))) == shell_id() def test_loader_scalar_program_is_functorial(): program = LoaderLiteral("scalar") - compiled = LOADER_TO_SHELL(program) + compiled = LoaderToShell()(program) assert program.dom == Ty() assert program.cod == loader_program_ty @@ -30,7 +30,7 @@ def test_loader_scalar_program_is_functorial(): def test_loader_translation_preserves_tagged_mapping_nodes(): graph = nx_compose_all("!echo\n? scalar\n") - program = incidences_to_program(graph) + program = HIFToLoader()(graph) assert isinstance(program, LoaderMapping) assert program.tag == "echo" @@ -40,12 +40,12 @@ def test_loader_translation_preserves_tagged_mapping_nodes(): def test_loader_translation_uses_hif_metaprogram(): graph: HyperGraph = nx_compose_all("!echo scalar") - assert incidences_to_program(graph) == HIF_TO_LOADER(graph) + assert HIFToLoader().specialize(graph) == HIFToLoader()(graph) def test_loader_tagged_scalar_stays_loader_node_until_compiled(): - program = incidences_to_program(nx_compose_all("!echo scalar")) - compiled = LOADER_TO_SHELL(program) + program = HIFToLoader()(nx_compose_all("!echo scalar")) + compiled = LoaderToShell()(program) execution = SHELL.execution(io_ty, io_ty).output_diagram() assert isinstance(program, LoaderScalar) @@ -55,26 +55,27 @@ def test_loader_tagged_scalar_stays_loader_node_until_compiled(): def test_loader_scalar_literal(): execution = SHELL.execution(io_ty, io_ty).output_diagram() - assert repl_read("scalar") == Literal("scalar") @ io_ty >> execution + assert LoaderToShell()(HIFToLoader()(nx_compose_all("scalar"))) == Literal("scalar") @ io_ty >> execution -def test_loader_state_projections_are_transported_by_state_layer(): - assert LOADER_TO_SHELL(loader_state_update()) == StateUpdateMap("loader", shell_program_ty, io_ty) - assert LOADER_TO_SHELL(loader_output()) == InputOutputMap("loader", shell_program_ty, io_ty, io_ty) +def test_loader_state_projections_are_reparametrized_by_state_functor(): + execution = LoaderExecution() + assert LoaderToShell()(execution.state_update_diagram()) == StateUpdateMap("loader", shell_program_ty, io_ty) + assert LoaderToShell()(execution.output_diagram()) == InputOutputMap("loader", shell_program_ty, io_ty, io_ty) def test_loader_empty_scalar_is_identity(): - assert repl_read("''") == shell_id() + assert LoaderToShell()(HIFToLoader()(nx_compose_all("''"))) == shell_id() def test_loader_tagged_scalar_is_command(): execution = SHELL.execution(io_ty, io_ty).output_diagram() - assert repl_read("!echo scalar") == Command(["echo", "scalar"]) @ io_ty >> execution + assert LoaderToShell()(HIFToLoader()(nx_compose_all("!echo scalar"))) == Command(["echo", "scalar"]) @ io_ty >> execution def test_loader_tag_only_is_command_with_no_scalar_argument(): execution = SHELL.execution(io_ty, io_ty).output_diagram() - assert repl_read("!cat") == Command(["cat"]) @ io_ty >> execution + assert LoaderToShell()(HIFToLoader()(nx_compose_all("!cat"))) == Command(["cat"]) @ io_ty >> execution def test_loader_sequence_is_pipeline(): @@ -83,11 +84,29 @@ def test_loader_sequence_is_pipeline(): (Command(["grep", "grep"]) @ io_ty >> execution) >> (Command(["wc", "-c"]) @ io_ty >> execution) ) - diagram = repl_read("- !grep grep\n- !wc -c\n") + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all("- !grep grep\n- !wc -c\n"))) assert isinstance(diagram, Pipeline) assert diagram.specialize() == expected +def test_loader_tagged_sequence_compiles_like_untagged_sequence(): + tagged_program = HIFToLoader()(nx_compose_all("!echo\n- foo\n- bar\n")) + untagged_program = LoaderSequence(tagged_program.stages) + + assert isinstance(tagged_program, LoaderSequence) + assert tagged_program.tag == "echo" + assert LoaderToShell()(tagged_program) == LoaderToShell()(untagged_program) + + +def test_loader_tagged_mapping_compiles_like_untagged_mapping(): + tagged_program = HIFToLoader()(nx_compose_all("!echo\n? foo\n? bar\n")) + untagged_program = LoaderMapping(tagged_program.branches) + + assert isinstance(tagged_program, LoaderMapping) + assert tagged_program.tag == "echo" + assert LoaderToShell()(tagged_program) == LoaderToShell()(untagged_program) + + def test_loader_shell_case_study_is_mapping_bubble(): execution = SHELL.execution(io_ty, io_ty).output_diagram() expected = Parallel( @@ -107,6 +126,6 @@ def test_loader_shell_case_study_is_mapping_bubble(): ), ) ) - diagram = repl_read(Path("examples/shell.yaml").read_text()) + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all(Path("examples/shell.yaml").read_text()))) assert isinstance(diagram, Parallel) assert diagram == expected diff --git a/tests/test_metaprog.py b/tests/test_metaprog.py index 7b5252d..a03b852 100644 --- a/tests/test_metaprog.py +++ b/tests/test_metaprog.py @@ -2,7 +2,10 @@ from nx_yaml import nx_compose_all from widip.comput.computer import * -from widip.metaprog import * +from widip.metaprog import SHELL_SPECIALIZER +from widip.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer +from widip.metaprog.hif import HIFToLoader +from widip.state.loader import LoaderToShell from os import path @@ -83,11 +86,12 @@ def test_specializers_are_unit_metaprograms_with_partial_evaluators(request): request.node.draw_objects = (h_ev, l_ev, H_to_L) graph = nx_compose_all("a") + loader_to_shell = LoaderToShell() - assert Specializer.metaprogram_dom() == Ty() - assert HIFToLoader.metaprogram_dom() == Ty() - assert LoaderToShell.metaprogram_dom() == Ty() - assert ShellSpecializer.metaprogram_dom() == Ty() - assert isinstance(LOADER_TO_SHELL, Specializer) + assert Specializer().metaprogram_dom() == Ty() + assert HIFToLoader().metaprogram_dom() == Ty() + assert loader_to_shell.metaprogram_dom() == Ty() + assert SHELL_SPECIALIZER.metaprogram_dom() == Ty() + assert isinstance(loader_to_shell, Specializer) assert isinstance(SHELL_SPECIALIZER, Specializer) - assert HIFToLoader().specialize(graph) == HIF_TO_LOADER(graph) + assert HIFToLoader().specialize(graph) == HIFToLoader()(graph) diff --git a/tests/test_runner.py b/tests/test_runner.py index 83e999a..26b0fb3 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -5,8 +5,10 @@ import pytest from nx_yaml import nx_compose_all -from widip.metaprog import LOADER_TO_SHELL, SHELL_TO_PYTHON, incidences_to_program -from widip.metaprog.widish import ShellSpecializer +from widip.metaprog.hif import HIFToLoader +from widip.state.loader import LoaderToShell +from widip.state.python import SHELL_INTERPRETER +from widip.state.widish import ShellSpecializer FIXTURE_DIR = Path("tests/widish") @@ -32,6 +34,8 @@ def normalize_svg(svg_text: str) -> str: @pytest.mark.parametrize("path", case_paths(), ids=lambda path: path.name) def test_shell_runner_files(path, tmp_path): + hif_to_loader = HIFToLoader() + loader_to_shell = LoaderToShell() yaml_path = path.with_suffix(".yaml") stdin_path = path.with_suffix(".in") stdout_path = path.with_suffix(".out") @@ -45,15 +49,15 @@ def test_shell_runner_files(path, tmp_path): assert mprog_svg_path.exists() yaml_text = yaml_path.read_text() - mprog = incidences_to_program(nx_compose_all(yaml_text)) - prog = ShellSpecializer()(LOADER_TO_SHELL(mprog)) + mprog = hif_to_loader(nx_compose_all(yaml_text)) + prog = ShellSpecializer()(loader_to_shell(mprog)) actual_mprog_svg_path = tmp_path / f"{path.name}.mprog.svg" actual_prog_svg_path = tmp_path / f"{path.name}.prog.svg" mprog.draw(path=str(actual_mprog_svg_path)) prog.draw(path=str(actual_prog_svg_path)) - program = SHELL_TO_PYTHON(prog) + program = SHELL_INTERPRETER(prog) assert program(stdin_path.read_text()) == stdout_path.read_text() assert normalize_svg(actual_prog_svg_path.read_text()) == normalize_svg(prog_svg_path.read_text()) diff --git a/tests/test_state.py b/tests/test_state.py index a9f5758..32cae2c 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,9 +1,9 @@ from discopy import python -from widip.comput import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL from widip.comput import computer from widip.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty from widip.comput.widish import io_ty +from widip.pcc import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL from widip.state import ( Execution, InputOutputMap, @@ -11,13 +11,11 @@ StateUpdateMap, execute, fixed_state, - loader_output, - loader_state_update, - shell_output, - shell_state_update, simulate, ) -from widip.state.core import ProcessRunner +from widip.state.loader import LoaderExecution +from widip.state.widish import ShellExecution +from widip.state.python import ProcessRunner from widip.wire.loader import loader_stream_ty @@ -78,7 +76,7 @@ def test_sec_7_3_program_execution_is_stateful(): assert execution.dom == P @ A assert execution.cod == P @ B - assert execution.universal_ev() == Computer(P, A, P @ B) + assert execution.universal_ev() == fixed_state(Computer(P, A, B)) assert execution.state_update_diagram() == StateUpdateMap("{}", P, A) assert execution.output_diagram() == InputOutputMap("{}", P, A, B) assert execution.state_update_diagram().cod == P @@ -101,7 +99,7 @@ def universal_ev(self): def test_process_runner_interprets_generic_state_projections(): class DummyRunner(ProcessRunner): def __init__(self): - ProcessRunner.__init__(self, lambda _ob: object) + ProcessRunner.__init__(self) def process_ar_map(self, box, dom, cod): return python.Function(lambda *_xs: None, dom, cod) @@ -117,7 +115,7 @@ def process_ar_map(self, box, dom, cod): def test_process_runner_interprets_generic_structural_boxes(): class DummyRunner(ProcessRunner): def __init__(self): - ProcessRunner.__init__(self, lambda _ob: object) + ProcessRunner.__init__(self) def process_ar_map(self, box, dom, cod): return python.Function(lambda *_xs: None, dom, cod) @@ -130,14 +128,14 @@ def process_ar_map(self, box, dom, cod): def test_loader_and_shell_projections_live_in_state(): - assert loader_state_update() == LOADER.execution( + assert LoaderExecution().state_update_diagram() == LOADER.execution( loader_stream_ty, loader_stream_ty ).state_update_diagram() - assert loader_output() == LOADER.execution( + assert LoaderExecution().output_diagram() == LOADER.execution( loader_stream_ty, loader_stream_ty ).output_diagram() - assert shell_state_update() == SHELL.execution(io_ty, io_ty).state_update_diagram() - assert shell_output() == SHELL.execution(io_ty, io_ty).output_diagram() + assert ShellExecution().state_update_diagram() == SHELL.execution(io_ty, io_ty).state_update_diagram() + assert ShellExecution().output_diagram() == SHELL.execution(io_ty, io_ty).output_diagram() def test_sec_7_4_fixed_state_lifts_a_function_to_a_process(): @@ -158,7 +156,7 @@ def test_sec_7_4_execute_uses_stateful_execution(): P, A, B, - ) + ).specialize() assert q.dom == X @ A assert q.cod == P @ B @@ -174,7 +172,7 @@ def test_sec_8_3_program_closed_category_chooses_a_language_type(): assert low_level.program_ty == L_ty assert high_level.evaluator(A, B) == Computer(H_ty, A, B) assert low_level.evaluator(A, B) == Computer(L_ty, A, B) - assert high_level.execution(A, B).universal_ev() == Computer(H_ty, A, H_ty @ B) - assert low_level.execution(A, B).universal_ev() == Computer(L_ty, A, L_ty @ B) + assert high_level.execution(A, B).universal_ev() == fixed_state(Computer(H_ty, A, B)) + assert low_level.execution(A, B).universal_ev() == fixed_state(Computer(L_ty, A, B)) assert computer_category.ob == high_level.ob == low_level.ob assert computer_category.ar == high_level.ar == low_level.ar diff --git a/widip/comput/__init__.py b/widip/comput/__init__.py index e5c1fc8..76f5635 100644 --- a/widip/comput/__init__.py +++ b/widip/comput/__init__.py @@ -1,61 +1,5 @@ -"""Chapter 2 computing structures and distinguished program languages.""" +"""Chapter 2 computing structures and language-specific program constants.""" from . import computer from .loader import loader_program_ty from .widish import shell_program_ty -from ..state.core import Execution -from ..state.loader import LoaderExecution -from ..state.widish import ShellExecution - - -class MonoidalComputer(computer.Category): - """ - The ambient computer category may contain more than one program language type. - """ - - -class ProgramClosedCategory(MonoidalComputer): - """ - Sec. 8.3: a program-closed category chooses one distinguished program type. - """ - - def __init__(self, program_ty: computer.ProgramTy): - self.program_ty = program_ty - MonoidalComputer.__init__(self) - - def evaluator(self, A: computer.Ty, B: computer.Ty): - return computer.Computer(self.program_ty, A, B) - - def execution(self, A: computer.Ty, B: computer.Ty): - return Execution( - "{}", - self.program_ty, - A, - B, - ) - - -class LoaderLanguage(ProgramClosedCategory): - """Program-closed category for the YAML loader language.""" - - def __init__(self): - ProgramClosedCategory.__init__(self, loader_program_ty) - - def execution(self, A: computer.Ty, B: computer.Ty): - del A, B - return LoaderExecution() - - -class ShellLanguage(ProgramClosedCategory): - """Program-closed category with the shell as distinguished language.""" - - def __init__(self): - ProgramClosedCategory.__init__(self, shell_program_ty) - - def execution(self, A: computer.Ty, B: computer.Ty): - del A, B - return ShellExecution() - - -LOADER = LoaderLanguage() -SHELL = ShellLanguage() diff --git a/widip/files.py b/widip/files.py index 3656bf5..e186f94 100644 --- a/widip/files.py +++ b/widip/files.py @@ -3,7 +3,8 @@ from nx_yaml import nx_compose_all from .comput.computer import Box, Diagram -from .metaprog.loader import HIF_TO_LOADER, LOADER_TO_SHELL +from .metaprog.hif import HIFToLoader +from .state.loader import LoaderToShell def files_ar(ar: Box) -> Diagram: @@ -20,7 +21,7 @@ def files_ar(ar: Box) -> Diagram: def file_diagram(file_name) -> Diagram: path = pathlib.Path(file_name) with path.open() as stream: - fd = LOADER_TO_SHELL(HIF_TO_LOADER(nx_compose_all(stream))) + fd = LoaderToShell()(HIFToLoader()(nx_compose_all(stream))) return fd def diagram_draw(path, fd): diff --git a/widip/metaprog/__init__.py b/widip/metaprog/__init__.py index 6e3a509..c9256b6 100644 --- a/widip/metaprog/__init__.py +++ b/widip/metaprog/__init__.py @@ -8,7 +8,5 @@ SHELL_SPECIALIZER = metaprog_widish.ShellSpecializer() -SHELL_INTERPRETER = metaprog_widish.ShellInterpreter(SHELL_SPECIALIZER) -SHELL_TO_PYTHON = SHELL_INTERPRETER PROGRAM_FUNCTOR = core.ProgramFunctor() METAPROGRAM_FUNCTOR = core.MetaprogramFunctor() diff --git a/widip/metaprog/core.py b/widip/metaprog/core.py index 6d37bbc..1552e4e 100644 --- a/widip/metaprog/core.py +++ b/widip/metaprog/core.py @@ -29,25 +29,25 @@ def __init__(self, P: ProgramTy, name="H"): class Specializer(Functor): """A functorial metaprogram with unit parameter type.""" - @staticmethod - def metaprogram_dom(): + def metaprogram_dom(self): + del self return Ty() - def __init__(self, ob=None, ar=None, *, dom=None, cod=None): + def __init__(self, *, dom=None, cod=None): Functor.__init__( self, - self.ob_map if ob is None else ob, - self.ar_map if ar is None else ar, + self.object, + self.ar_map, dom=Functor.dom if dom is None else dom, cod=Functor.cod if cod is None else cod, ) - @staticmethod - def ob_map(ob): + def object(self, ob): + del self return ob - @staticmethod - def ar_map(ar): + def ar_map(self, ar): + del self return ar def specialize(self, *args, **kwargs): @@ -57,25 +57,25 @@ def specialize(self, *args, **kwargs): class Interpreter(Functor): """A functorial interpreter with unit metaprogram domain.""" - @staticmethod - def metaprogram_dom(): + def metaprogram_dom(self): + del self return Ty() - def __init__(self, ob=None, ar=None, *, dom=None, cod=None): + def __init__(self, *, dom=None, cod=None): Functor.__init__( self, - self.ob_map if ob is None else ob, - self.ar_map if ar is None else ar, + self.object, + self.ar_map, dom=Functor.dom if dom is None else dom, cod=Functor.cod if cod is None else cod, ) - @staticmethod - def ob_map(ob): + def object(self, ob): + del self return ob - @staticmethod - def ar_map(ar): + def ar_map(self, ar): + del self return ar def interpret(self, *args, **kwargs): diff --git a/widip/metaprog/hif.py b/widip/metaprog/hif.py index bde70ff..f743012 100644 --- a/widip/metaprog/hif.py +++ b/widip/metaprog/hif.py @@ -1,21 +1,59 @@ """HIF-specific specializers and lowerings.""" from ..comput.computer import Ty -from ..state.hif import ( - document_root_node, - mapping_entry_nodes, - sequence_item_nodes, - stream_document_nodes, -) -from ..wire.hif import HyperGraph, hif_node +from ..wire.hif import HyperGraph, hif_edge_incidences, hif_node, hif_node_incidences from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline +def _successor_nodes(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """Yield successor nodes reached by one edge-node incidence pattern.""" + for edge, _, _, _ in hif_node_incidences(graph, node, key=edge_key, direction="head"): + for _, target, _, _ in hif_edge_incidences(graph, edge, key=node_key, direction="head"): + yield target + + +def _first_successor_node(graph: HyperGraph, node, *, edge_key="next", node_key="start"): + """Return the first successor node for a given incidence pattern, if any.""" + return next(iter(_successor_nodes(graph, node, edge_key=edge_key, node_key=node_key)), None) + + +def stream_document_nodes(graph: HyperGraph, stream=0): + """Yield stream documents in order by following next/forward links.""" + current = _first_successor_node(graph, stream, edge_key="next", node_key="start") + while current is not None: + yield current + current = _first_successor_node(graph, current, edge_key="forward", node_key="start") + + +def document_root_node(graph: HyperGraph, document): + """Return the root YAML node for a document node, if any.""" + return _first_successor_node(graph, document, edge_key="next", node_key="start") + + +def sequence_item_nodes(graph: HyperGraph, sequence): + """Yield sequence items in order by following next/forward links.""" + current = _first_successor_node(graph, sequence, edge_key="next", node_key="start") + while current is not None: + yield current + current = _first_successor_node(graph, current, edge_key="forward", node_key="start") + + +def mapping_entry_nodes(graph: HyperGraph, mapping): + """Yield `(key_node, value_node)` pairs in order for a YAML mapping node.""" + key_node = _first_successor_node(graph, mapping, edge_key="next", node_key="start") + while key_node is not None: + value_node = _first_successor_node(graph, key_node, edge_key="forward", node_key="start") + if value_node is None: + break + yield key_node, value_node + key_node = _first_successor_node(graph, value_node, edge_key="forward", node_key="start") + + class HIFSpecializer: """Recursive structural lowering over YAML HIF nodes.""" - @staticmethod - def metaprogram_dom(): + def metaprogram_dom(self): + del self return Ty() def specialize(self, graph: HyperGraph, node=0): diff --git a/widip/metaprog/loader.py b/widip/metaprog/loader.py deleted file mode 100644 index 24c6279..0000000 --- a/widip/metaprog/loader.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Loader-specific program transformations.""" - -from nx_yaml import nx_compose_all - -from .core import Specializer -from .hif import HIFToLoader -from ..comput import computer -from ..comput import loader as loader_lang -from ..comput import widish as shell_lang -from ..state.core import map_process_box -from ..state.widish import shell_stage as shell_io_stage -from ..wire.hif import HyperGraph -from ..wire import loader as loader_wire -from ..wire import widish as shell_wire -from .widish import Parallel, Pipeline - - -def _compile_scalar(node: loader_wire.LoaderScalar): - """Compile one YAML scalar node to the shell backend.""" - if node.tag: - argv = (node.tag,) if not node.value else (node.tag, node.value) - return shell_io_stage(shell_lang.Command(argv)) - if not node.value: - return shell_wire.shell_id() - return shell_io_stage(shell_lang.Literal(node.value)) - - -class LoaderToShell(Specializer): - """Compile loader nodes, programs, and execution boxes into shell diagrams.""" - - def __init__(self): - Specializer.__init__( - self, - self.ob_map, - self.ar_map, - dom=computer.Category(), - cod=computer.Category(), - ) - - @staticmethod - def ob_map(ob): - if ob == loader_lang.loader_program_ty: - return shell_lang.shell_program_ty - if ob == loader_wire.loader_stream_ty: - return shell_lang.io_ty - return ob - - def __call__(self, other): - if isinstance(other, loader_wire.LoaderScalar): - return _compile_scalar(other) - if isinstance(other, loader_wire.LoaderSequence): - return Pipeline(tuple(self(stage) for stage in other.stages)) - if isinstance(other, loader_wire.LoaderMapping): - return Parallel(tuple(self(branch) for branch in other.branches)) - return Specializer.__call__(self, other) - - def ar_map(self, ar): - ar = map_process_box(ar, self.ob_map) - if isinstance(ar, loader_lang.LoaderEmpty): - return shell_lang.Empty() - if isinstance(ar, loader_lang.LoaderLiteral): - return shell_lang.Literal(ar.text) - return ar - - -HIF_TO_LOADER = HIFToLoader() -LOADER_TO_SHELL = LoaderToShell() diff --git a/widip/metaprog/python.py b/widip/metaprog/python.py index a6b8ea4..05e377b 100644 --- a/widip/metaprog/python.py +++ b/widip/metaprog/python.py @@ -7,14 +7,13 @@ from discopy import python from discopy.utils import tuplify, untuplify -from ..comput import ProgramClosedCategory from ..comput import computer from ..comput import python as comput_python +from ..pcc import ProgramClosedCategory from . import core as metaprog_core -PYTHON_OBJECT = comput_python.program_ty -PYTHON_PROGRAMS = ProgramClosedCategory(PYTHON_OBJECT) +PYTHON_PROGRAMS = ProgramClosedCategory(comput_python.program_ty) def _evaluator(A, B): @@ -25,19 +24,19 @@ class PythonSpecializer(metaprog_core.SpecializerBox): """Native specializer metaprogram: ``S : I -> obj``.""" def __init__(self): - metaprog_core.SpecializerBox.__init__(self, PYTHON_OBJECT, name="S") + metaprog_core.SpecializerBox.__init__(self, comput_python.program_ty, name="S") class PythonInterpreter(metaprog_core.InterpreterBox): """Native interpreter metaprogram: ``H : I -> obj``.""" def __init__(self): - metaprog_core.InterpreterBox.__init__(self, PYTHON_OBJECT, name="H") + metaprog_core.InterpreterBox.__init__(self, comput_python.program_ty, name="H") PYTHON_SPECIALIZER_BOX = PythonSpecializer() PYTHON_INTERPRETER_BOX = PythonInterpreter() -PYTHON_EVALUATOR_BOX = _evaluator(PYTHON_OBJECT, PYTHON_OBJECT) +PYTHON_EVALUATOR_BOX = _evaluator(comput_python.program_ty, comput_python.program_ty) def _partial_evaluate(program, static_input): @@ -62,6 +61,25 @@ def apply_value(function, argument): return _apply_value(function, argument) +def runtime_value_box(value, *, name=None, cod=None): + """Build a closed box carrying a runtime value for PythonRuntime.""" + cod = comput_python.program_ty if cod is None else cod + box = computer.Box(repr(value) if name is None else name, computer.Ty(), cod) + box.value = value + return box + + +def map_structural_box(functor, box, dom): + """Map cartesian structural boxes to Python functions.""" + if isinstance(box, computer.Copy): + return python.Function.copy(dom, n=2) + if isinstance(box, computer.Delete): + return python.Function.discard(dom) + if isinstance(box, computer.Swap): + return python.Function.swap(functor(box.left), functor(box.right)) + return None + + sec_6_2_2_partial_application = partial( metaprog_core.sec_6_2_2_partial_application, specializer_box=PYTHON_SPECIALIZER_BOX, @@ -120,14 +138,12 @@ class PythonRuntime(metaprog_core.Interpreter): def __init__(self): metaprog_core.Interpreter.__init__( self, - ob=self.ob_map, - ar=self.ar_map, dom=computer.Category(), cod=python.Category(), ) - @staticmethod - def ob_map(_ob): + def object(self, ob): + del ob return object def ar_map(self, box): @@ -154,12 +170,9 @@ def ar_map(self, box): return python.Function(lambda: _universal_evaluate, dom, cod) if isinstance(box, computer.Computer): return python.Function(apply_value, dom, cod) - if isinstance(box, computer.Copy): - return python.Function.copy(dom, n=2) - if isinstance(box, computer.Delete): - return python.Function.discard(dom) - if isinstance(box, computer.Swap): - return python.Function.swap(self(box.left), self(box.right)) + structural = map_structural_box(self, box, dom) + if structural is not None: + return structural raise TypeError(f"unsupported Python metaprogram box: {box!r}") diff --git a/widip/metaprog/widish.py b/widip/metaprog/widish.py index 84df795..f684f35 100644 --- a/widip/metaprog/widish.py +++ b/widip/metaprog/widish.py @@ -1,17 +1,10 @@ """Shell-specific program transformations and interpreters.""" -from collections.abc import Callable -from itertools import count - -from discopy import monoidal, python +from discopy import monoidal from .core import Specializer -from . import core as metaprog_core -from . import python as metaprog_python from ..comput import computer from ..comput import widish as shell_lang -from ..state import core as state_core -from ..state.widish import parallel_io_diagram, shell_program_runner from ..wire import widish as shell_wire @@ -37,101 +30,42 @@ def _tensor_all(diagrams): return result -def _specialize_shell(diagram, next_temp): - """Recursively lower shell bubbles using one shared temp-path allocator.""" +def _specialize_shell(diagram): + """Recursively lower shell bubbles.""" if isinstance(diagram, Pipeline): - return diagram.specialize(next_temp) + return diagram.specialize() if isinstance(diagram, Parallel): - return diagram.specialize(next_temp) + return diagram.specialize() if isinstance(diagram, monoidal.Bubble): - return _specialize_shell(diagram.arg, next_temp) + return _specialize_shell(diagram.arg) if isinstance(diagram, computer.Box): return diagram if isinstance(diagram, computer.Diagram): result = computer.Id(diagram.dom) for left, box, right in diagram.inside: - result = result >> left @ _specialize_shell(box, next_temp) @ right + result = result >> left @ _specialize_shell(box) @ right return result return diagram -def _has_shell_bubble(diagram) -> bool: - """Detect whether a shell diagram still contains unspecialized bubbles.""" - if isinstance(diagram, monoidal.Bubble): - return True - if not isinstance(diagram, computer.Diagram): - return False - return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) - - class ShellSpecializer(Specializer): """Lower shell bubbles to their executable wiring.""" def __init__(self): - self._next_temp = count() Specializer.__init__( self, - self.ob_map, - self.ar_map, dom=computer.Category(), cod=computer.Category(), ) - @staticmethod - def ob_map(ob): - return ob - def __call__(self, other): - return _specialize_shell(other, self._next_temp) + return _specialize_shell(other) - @staticmethod - def ar_map(ar): + def ar_map(self, ar): + del self return ar -class ShellInterpreter(metaprog_core.Interpreter): - """Interpret shell diagrams as Python callables.""" - - def __init__(self, specialize_shell): - self.specialize_shell = specialize_shell - metaprog_core.Interpreter.__init__( - self, - ob=self.ob_map, - ar=self.ar_map, - dom=computer.Category(), - cod=python.Category(), - ) - - @staticmethod - def ob_map(ob): - if ( - isinstance(ob, computer.Ty) - and len(ob) == 1 - and isinstance(ob.inside[0], computer.ProgramOb) - ): - return Callable - return str - - def __call__(self, other): - if _has_shell_bubble(other): - return monoidal.Functor.__call__(self, self.specialize_shell(other)) - return monoidal.Functor.__call__(self, other) - - def ar_map(self, box): - dom, cod = self(box.dom), self(box.cod) - projection = state_core.ProcessRunner.projection_ar_map(self, box, dom, cod) - structural = state_core.ProcessRunner.structural_ar_map(self, box, dom, cod) - if projection is not None: - return projection - if structural is not None: - return structural - if isinstance(box, shell_lang.ShellProgram): - return python.Function(lambda: shell_program_runner(box), dom, cod) - if isinstance(box, computer.Computer): - return python.Function(metaprog_python.apply_value, dom, cod) - raise TypeError(f"unsupported shell interpreter box: {box!r}") - - class Pipeline(monoidal.Bubble, computer.Box): """Bubble grouping shell stages in sequence.""" @@ -146,9 +80,10 @@ def __init__(self, stages): drawing_name="seq", ) - def specialize(self, next_temp=None): - next_temp = count() if next_temp is None else next_temp - return _pipeline_diagram(tuple(_specialize_shell(stage, next_temp) for stage in self.stages)) + def specialize(self): + return _pipeline_diagram( + tuple(_specialize_shell(stage) for stage in self.stages), + ) class Parallel(monoidal.Bubble, computer.Box): @@ -164,11 +99,9 @@ def __init__(self, branches): drawing_name="map", ) - def specialize(self, next_temp=None): - next_temp = count() if next_temp is None else next_temp - return parallel_io_diagram( - tuple(_specialize_shell(branch, next_temp) for branch in self.branches), - next_temp, + def specialize(self): + return Parallel( + tuple(_specialize_shell(branch) for branch in self.branches), ) diff --git a/widip/pcc/__init__.py b/widip/pcc/__init__.py new file mode 100644 index 0000000..545a92c --- /dev/null +++ b/widip/pcc/__init__.py @@ -0,0 +1,5 @@ +"""Chapter 8: Program-closed categories (PCC).""" + +from .core import MonoidalComputer, ProgramClosedCategory +from .loader import LOADER, LoaderLanguage +from .widish import SHELL, ShellLanguage diff --git a/widip/pcc/core.py b/widip/pcc/core.py new file mode 100644 index 0000000..54be0e2 --- /dev/null +++ b/widip/pcc/core.py @@ -0,0 +1,75 @@ +"""Chapter 8: Program-closed categories.""" + +from ..comput import computer + + +class MonoidalComputer(computer.Category): + """ + The ambient computer category may contain more than one program language type. + """ + + +class ProgramClosedCategory(MonoidalComputer): + """ + Sec. 8.3: a program-closed category chooses one distinguished program type. + """ + + def __init__(self, program_ty: computer.ProgramTy): + self.program_ty = program_ty + MonoidalComputer.__init__(self) + + def evaluator(self, A: computer.Ty, B: computer.Ty): + return computer.Computer(self.program_ty, A, B) + + def run(self, A: computer.Ty, B: computer.Ty): + """Sec. 8.3 c'': program execution machine ``Run``.""" + from ..state.core import execute + + return execute(computer.Id(self.program_ty), A, B) + + def is_program(self, ob): + """Check whether ``ob`` is this category's distinguished program type.""" + return ob == self.program_ty + + def is_evaluator(self, arrow): + """Check whether ``arrow`` is this category's evaluator box.""" + return isinstance(arrow, computer.Computer) and self.is_program(arrow.P) + + def _simulate_type(self, ty, codomain: "ProgramClosedCategory"): + """Transport program occurrences in a type along a language simulation.""" + if self.is_program(ty): + return codomain.program_ty + if not isinstance(ty, computer.Ty): + return ty + source_atom = self.program_ty.inside[0] + target_program = codomain.program_ty + mapped = computer.Ty() + changed = False + for atom in ty.inside: + if atom == source_atom: + mapped = mapped @ target_program + changed = True + else: + mapped = mapped @ computer.Ty(atom) + return mapped if changed else ty + + def simulate(self, item, codomain: "ProgramClosedCategory"): + """ + Chapter 8: transport programs/evaluators to another program-closed category. + """ + if self.is_evaluator(item): + return codomain.evaluator( + self._simulate_type(item.A, codomain), + self._simulate_type(item.B, codomain), + ) + return self._simulate_type(item, codomain) + + def execution(self, A: computer.Ty, B: computer.Ty): + from ..state.core import Execution + + return Execution( + "{}", + self.program_ty, + A, + B, + ) diff --git a/widip/pcc/loader.py b/widip/pcc/loader.py new file mode 100644 index 0000000..73ed14b --- /dev/null +++ b/widip/pcc/loader.py @@ -0,0 +1,21 @@ +"""Program-closed category for the YAML loader language.""" + +from ..comput import computer +from ..comput.loader import loader_program_ty +from .core import ProgramClosedCategory + + +class LoaderLanguage(ProgramClosedCategory): + """Program-closed category for the YAML loader language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, loader_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + from ..state.loader import LoaderExecution + + return LoaderExecution() + + +LOADER = LoaderLanguage() diff --git a/widip/pcc/widish.py b/widip/pcc/widish.py new file mode 100644 index 0000000..e21c402 --- /dev/null +++ b/widip/pcc/widish.py @@ -0,0 +1,21 @@ +"""Program-closed category with shell as distinguished language.""" + +from ..comput import computer +from ..comput.widish import shell_program_ty +from .core import ProgramClosedCategory + + +class ShellLanguage(ProgramClosedCategory): + """Program-closed category with shell as distinguished language.""" + + def __init__(self): + ProgramClosedCategory.__init__(self, shell_program_ty) + + def execution(self, A: computer.Ty, B: computer.Ty): + del A, B + from ..state.widish import ShellExecution + + return ShellExecution() + + +SHELL = ShellLanguage() diff --git a/widip/state/__init__.py b/widip/state/__init__.py index 5cd0b69..385e884 100644 --- a/widip/state/__init__.py +++ b/widip/state/__init__.py @@ -4,6 +4,7 @@ Execution, InputOutputMap, Process, + ProcessSimulation, ProcessRunner, StateUpdateMap, execute, @@ -12,23 +13,3 @@ ) from .loader import LoaderExecution from .widish import ShellExecution - - -def loader_state_update(): - """The loader execution state-update map sta(loader).""" - return LoaderExecution().state_update_diagram() - - -def loader_output(): - """The loader execution output map out(loader).""" - return LoaderExecution().output_diagram() - - -def shell_state_update(): - """The shell execution state-update map sta(shell).""" - return ShellExecution().state_update_diagram() - - -def shell_output(): - """The shell execution output map out(shell).""" - return ShellExecution().output_diagram() diff --git a/widip/state/core.py b/widip/state/core.py index a5c5cc6..ff9412e 100644 --- a/widip/state/core.py +++ b/widip/state/core.py @@ -1,8 +1,10 @@ """Generic Chapter 7 stateful process structure.""" -from discopy import monoidal, python +from discopy import monoidal from ..comput import computer +from ..metaprog import core as metaprog_core +from ..pcc import ProgramClosedCategory class StateUpdateMap(computer.Box): @@ -75,93 +77,132 @@ def universal_ev(self): """ Eq. 7.3: program execution is the evaluator with output type P x B. """ - return computer.Computer(self.X, self.A, self.X @ self.B) + return fixed_state(computer.Computer(self.X, self.A, self.B)) def specialize(self): return self.universal_ev() class ProcessRunner(monoidal.Functor): - """Python interpretation of the generic Eq. 7.1 process projections.""" + """Base interpreter of Eq. 7.1 process projections and wiring.""" - def __init__(self, ob): + def __init__(self, cod): monoidal.Functor.__init__( self, - ob=ob, + ob=self.object, ar=self.ar_map, dom=computer.Category(), - cod=python.Category(), + cod=cod, ) + def object(self, ob): + del self + return ob + def process_ar_map(self, box, dom, cod): """Interpret the non-state-specific boxes of a process diagram.""" raise TypeError(f"unsupported process box: {box!r}") - @staticmethod - def _state_update(state, _input): - return state + def state_update_ar(self, dom, cod): + """Interpret Eq. 7.1 ``sta`` arrows in the target category.""" + raise TypeError(f"state-update interpretation is undefined for dom={dom!r}, cod={cod!r}") - @staticmethod - def _output(state, input_value): - return state(input_value) + def output_ar(self, dom, cod): + """Interpret Eq. 7.1 ``out`` arrows in the target category.""" + raise TypeError(f"output interpretation is undefined for dom={dom!r}, cod={cod!r}") - def projection_ar_map(self, box, dom, cod): + def map_projection(self, box, dom, cod): """Interpret the generic Eq. 7.1 state projections.""" if isinstance(box, StateUpdateMap): - return python.Function(self._state_update, dom, cod) + return self.state_update_ar(dom, cod) if isinstance(box, InputOutputMap): - return python.Function(self._output, dom, cod) + return self.output_ar(dom, cod) return None - def structural_ar_map(self, box, dom, cod): - """Interpret the generic structural boxes of process diagrams.""" - del cod - if isinstance(box, computer.Copy): - return python.Function.copy(dom, n=2) - if isinstance(box, computer.Delete): - return python.Function.discard(dom) - if isinstance(box, computer.Swap): - return python.Function.swap(self(box.left), self(box.right)) + def map_structural(self, box, dom, cod): + """Interpret structural boxes in the target category.""" + del box, dom, cod return None - def ar_map(self, box): - dom, cod = self(box.dom), self(box.cod) - projection = self.projection_ar_map(box, dom, cod) - structural = self.structural_ar_map(box, dom, cod) - + def map_shared_ar(self, box, dom, cod): + """Shared mapping for Eq. 7.1 projections and structural wiring.""" + projection = self.map_projection(box, dom, cod) if projection is not None: return projection - if structural is not None: - return structural + return self.map_structural(box, dom, cod) + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + shared = self.map_shared_ar(box, dom, cod) + if shared is not None: + return shared return self.process_ar_map(box, dom, cod) -def map_process_box(box, ob): - """Transport a process projection box along an object mapping.""" - if isinstance(box, StateUpdateMap): - return StateUpdateMap(box.process_name, ob(box.X), ob(box.A)) - if isinstance(box, InputOutputMap): - return InputOutputMap(box.process_name, ob(box.X), ob(box.A), ob(box.B)) - return box +class ProcessSimulation(metaprog_core.Specializer): + """Fig. 7.2 simulation as a state-aware diagrammatic transformation.""" + + def __init__(self): + metaprog_core.Specializer.__init__( + self, + dom=computer.Category(), + cod=computer.Category(), + ) + + def simulation(self, item): + """Simulation action on objects and non-projection arrows.""" + del self + return item + + def sta(self, state_update: StateUpdateMap): + """Eq. 7.1 state projection transport along a simulation.""" + return StateUpdateMap( + state_update.process_name, + self.simulation(state_update.X), + self.simulation(state_update.A), + ) + + def out(self, output: InputOutputMap): + """Eq. 7.1 output projection transport along a simulation.""" + return InputOutputMap( + output.process_name, + self.simulation(output.X), + self.simulation(output.A), + self.simulation(output.B), + ) + + def object(self, ob): + return self.simulation(ob) + + def ar_map(self, ar): + if isinstance(ar, StateUpdateMap): + return self.sta(ar) + if isinstance(ar, InputOutputMap): + return self.out(ar) + return self.simulation(ar) -def simulate(q: Process, s: computer.Diagram): +def simulate(q: computer.Diagram, s: computer.Diagram): """ Fig. 7.2: a simulation along s is postcomposition with s x id_B. """ - return q >> s @ q.B + if isinstance(q, Process): + B = q.B + else: + if q.cod[:1] != s.dom: + raise TypeError(f"simulation codomain mismatch: {q.cod!r} vs {s.dom!r}") + B = q.cod[1:] + if q.cod != s.dom @ B: + raise TypeError(f"simulation codomain mismatch: {q.cod!r} vs {s.dom @ B!r}") + return q >> s @ B def execute(Q: computer.Diagram, A: computer.Ty, B: computer.Ty): """ Sec. 7.3: execute an X-parameterized program as a stateful process. """ - return Q @ A >> Execution( - "{}", - Q.cod, - A, - B, - ) + stateful_evaluator = fixed_state(ProgramClosedCategory(Q.cod).evaluator(A, B)) + return Q @ A >> simulate(stateful_evaluator, computer.Id(Q.cod)) def fixed_state(g: computer.Diagram): diff --git a/widip/state/loader.py b/widip/state/loader.py index ceac9b6..1f209a1 100644 --- a/widip/state/loader.py +++ b/widip/state/loader.py @@ -1,8 +1,14 @@ """Loader-specific stateful execution.""" +from ..comput import loader as loader_lang from ..comput.loader import loader_program_ty +from ..comput import widish as shell_lang +from ..pcc import LOADER, SHELL +from ..wire import loader as loader_wire from ..wire.loader import loader_stream_ty -from .core import Execution +from ..wire import widish as shell_wire +from .core import Execution, ProcessSimulation +from .widish import Parallel, Pipeline class LoaderExecution(Execution): @@ -16,3 +22,46 @@ def __init__(self): loader_stream_ty, loader_stream_ty, ) + + +class LoaderToShell(ProcessSimulation): + """State-aware loader-to-shell specializer.""" + + def __init__(self): + ProcessSimulation.__init__(self) + + def simulation(self, item): + if item == loader_stream_ty: + return shell_lang.io_ty + if isinstance(item, loader_lang.LoaderEmpty): + return shell_lang.Empty() + if isinstance(item, loader_lang.LoaderLiteral): + return shell_lang.Literal(item.text) + if LOADER.is_evaluator(item): + return SHELL.evaluator( + self.simulation(item.A), + self.simulation(item.B), + ) + return LOADER.simulate(item, SHELL) + + def __call__(self, other): + if isinstance(other, loader_wire.LoaderScalar): + return self.compile_scalar(other) + if isinstance(other, loader_wire.LoaderSequence): + return Pipeline(tuple(self(stage) for stage in other.stages)) + if isinstance(other, loader_wire.LoaderMapping): + return Parallel(tuple(self(branch) for branch in other.branches)) + return ProcessSimulation.__call__(self, other) + + def compile_scalar(self, node: loader_wire.LoaderScalar): + """Compile one YAML scalar node to the shell backend.""" + execution = SHELL.execution( + shell_lang.io_ty, + shell_lang.io_ty, + ).output_diagram() + if node.tag: + argv = (node.tag,) if not node.value else (node.tag, node.value) + return shell_lang.Command(argv) @ shell_lang.io_ty >> execution + if not node.value: + return shell_wire.shell_id() + return shell_lang.Literal(node.value) @ shell_lang.io_ty >> execution diff --git a/widip/state/python.py b/widip/state/python.py new file mode 100644 index 0000000..9caed80 --- /dev/null +++ b/widip/state/python.py @@ -0,0 +1,144 @@ +"""Stateful shell-to-Python runtime transformations.""" + +from collections.abc import Callable + +from discopy import monoidal, python + +from ..comput import computer +from ..comput import widish as shell_lang +from ..metaprog import core as metaprog_core +from ..metaprog import python as metaprog_python +from ..pcc import SHELL +from . import core as state_core +from .widish import Parallel as ShellParallel +from .widish import Pipeline as ShellPipeline +from .widish import ShellSpecializer, shell_program_runner + + +def _has_shell_bubble(diagram) -> bool: + """Detect whether a shell diagram still contains unspecialized bubbles.""" + if isinstance(diagram, monoidal.Bubble): + return True + if not isinstance(diagram, computer.Diagram): + return False + return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) + + +class ShellToPythonProgram(state_core.ProcessSimulation): + """Map shell programs and their evaluators to Python-program equivalents.""" + + def __init__(self): + state_core.ProcessSimulation.__init__(self) + + def simulation(self, item): + if isinstance(item, shell_lang.ShellProgram): + return metaprog_python.runtime_value_box( + shell_program_runner(item), + name=item.name, + ) + return SHELL.simulate(item, metaprog_python.PYTHON_PROGRAMS) + + +class ProcessRunner(state_core.ProcessRunner): + """Python interpretation of generic Eq. 7.1 process projections.""" + + def __init__(self): + state_core.ProcessRunner.__init__(self, cod=python.Category()) + + def object(self, ob): + del ob + return object + + def state_update_value(self, state, _input): + del self + return state + + def output_value(self, state, input_value): + del self + return state(input_value) + + def state_update_ar(self, dom, cod): + return python.Function(self.state_update_value, dom, cod) + + def output_ar(self, dom, cod): + return python.Function(self.output_value, dom, cod) + + def map_structural(self, box, dom, cod): + del cod + return metaprog_python.map_structural_box(self, box, dom) + + +class ShellInterpreter(ProcessRunner, metaprog_core.Interpreter): + """Interpret shell diagrams as Python callables.""" + + def __init__(self, specialize_shell, program_functor, python_runtime): + self.specialize_shell = specialize_shell + self.program_functor = program_functor + self.python_runtime = python_runtime + ProcessRunner.__init__(self) + + def object(self, ob): + del self + if ( + isinstance(ob, computer.Ty) + and len(ob) == 1 + and isinstance(ob.inside[0], computer.ProgramOb) + ): + return Callable + return str + + def __call__(self, other): + if _has_shell_bubble(other): + return monoidal.Functor.__call__(self, self.specialize_shell(other)) + return monoidal.Functor.__call__(self, other) + + def process_ar_map(self, box, dom, cod): + if isinstance(box, ShellPipeline): + stages = tuple(self(stage) for stage in box.stages) + + def run(stdin): + output = stdin + for stage in stages: + output = stage(output) + return output + + return python.Function(run, dom, cod) + if isinstance(box, ShellParallel): + branches = tuple(self(branch) for branch in box.branches) + + def run(stdin): + if not branches: + return stdin + if len(branches) == 1: + return branches[0](stdin) + return "".join(branch(stdin) for branch in branches) + + return python.Function(run, dom, cod) + if isinstance(box, (shell_lang.ShellProgram, computer.Computer)): + return self.python_runtime(self.program_functor(box)) + raise TypeError(f"unsupported shell interpreter box: {box!r}") + + +SHELL_SPECIALIZER = ShellSpecializer() +SHELL_PROGRAM_TO_PYTHON = ShellToPythonProgram() + + +class ShellPythonRuntime(metaprog_python.PythonRuntime): + """Python runtime with shell-specific object interpretation.""" + + def object(self, ob): + if ( + isinstance(ob, computer.Ty) + and len(ob) == 1 + and isinstance(ob.inside[0], computer.ProgramOb) + ): + return Callable + return str + + +SHELL_PYTHON_RUNTIME = ShellPythonRuntime() +SHELL_INTERPRETER = ShellInterpreter( + SHELL_SPECIALIZER, + SHELL_PROGRAM_TO_PYTHON, + SHELL_PYTHON_RUNTIME, +) diff --git a/widip/state/widish.py b/widip/state/widish.py index c783b84..24054e9 100644 --- a/widip/state/widish.py +++ b/widip/state/widish.py @@ -2,58 +2,24 @@ import subprocess -from ..comput import computer from ..comput import widish as shell_lang -from .core import Execution, InputOutputMap +from ..metaprog import widish as metaprog_widish +from .core import Execution from ..wire import widish as shell_wire -def shell_stage(program): - """Run one primitive shell program on the standard shell stream.""" - return program @ shell_lang.io_ty >> InputOutputMap( - "shell", - shell_lang.shell_program_ty, - shell_lang.io_ty, - shell_lang.io_ty, - ) - - -def _temp_path(next_temp) -> str: - """Allocate a deterministic temporary pathname for shell IO wiring.""" - return f"/tmp/widip-{next(next_temp):04d}.tmp" - - -def parallel_io_diagram(branches, next_temp): - """Lower shell-IO branching to file-backed tee/cat process wiring.""" +def parallel_io_diagram(branches): + """Lower shell-IO branching to structural shell parallel composition.""" branches = tuple(branches) if not branches: return shell_wire.shell_id() if len(branches) == 1: return branches[0] - - input_path = _temp_path(next_temp) - output_paths = tuple(_temp_path(next_temp) for _ in branches) - stages = [shell_stage(shell_lang.Command(("tee", input_path)))] - - for branch, output_path in zip(branches, output_paths): - stages.extend( - ( - shell_stage(shell_lang.Command(("cat", input_path))), - branch, - shell_stage(shell_lang.Command(("tee", output_path))), - ) - ) - - stages.append(shell_stage(shell_lang.Command(("cat",) + output_paths))) - - result = shell_wire.shell_id() - for stage in stages: - result = stage if result == shell_wire.shell_id() else result >> stage - return result + return Parallel(branches) def shell_program_runner(program): - """Compile one shell-language program to a Python text transformer.""" + """Compile a shell program constant into a Python stream transformer.""" if isinstance(program, shell_lang.Empty): return lambda stdin: stdin if isinstance(program, shell_lang.Literal): @@ -72,6 +38,46 @@ def run(stdin: str) -> str: return run raise TypeError(f"unsupported shell program: {program!r}") + +class Pipeline(metaprog_widish.Pipeline): + """State-aware pipeline bubble.""" + + def specialize(self): + return metaprog_widish.Pipeline.specialize(self) + + +class Parallel(metaprog_widish.Parallel): + """State-aware parallel bubble.""" + + def specialize(self): + return parallel_io_diagram( + tuple(metaprog_widish.ShellSpecializer()(branch) for branch in self.branches), + ) + + +def pipeline(stages): + """Build a state-aware shell pipeline bubble.""" + stages = tuple(stages) + if not stages: + return shell_wire.shell_id() + return Pipeline(stages) + + +def parallel(branches): + """Build a state-aware shell parallel bubble.""" + branches = tuple(branches) + if not branches: + return shell_wire.shell_id() + return Parallel(branches) + + +class ShellSpecializer(metaprog_widish.ShellSpecializer): + """State-aware shell bubble specializer.""" + + def __init__(self): + metaprog_widish.ShellSpecializer.__init__(self) + + class ShellExecution(Execution): """Stateful shell evaluator P x io -> P x io.""" diff --git a/widip/watch.py b/widip/watch.py index 09541fa..80c87cf 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -8,8 +8,9 @@ from discopy.utils import tuplify, untuplify from .files import diagram_draw, file_diagram -from .metaprog import SHELL_TO_PYTHON -from .metaprog.loader import HIF_TO_LOADER, LOADER_TO_SHELL +from .metaprog.hif import HIFToLoader +from .state.loader import LoaderToShell +from .state.python import SHELL_INTERPRETER # TODO watch functor ?? @@ -38,13 +39,15 @@ def watch_main(): return observer def shell_main(file_name, draw=True): + hif_to_loader = HIFToLoader() + loader_to_shell = LoaderToShell() try: while True: observer = watch_main() try: prompt = f"--- !{file_name}\n" source = input(prompt) - source_d = LOADER_TO_SHELL(HIF_TO_LOADER(nx_compose_all(source))) + source_d = loader_to_shell(hif_to_loader(nx_compose_all(source))) # source_d.draw( # textpad=(0.3, 0.1), # fontsize=12, @@ -53,7 +56,7 @@ def shell_main(file_name, draw=True): if draw: diagram_draw(path, source_d) - result_ev = SHELL_TO_PYTHON(source_d)("") + result_ev = SHELL_INTERPRETER(source_d)("") print(result_ev) except KeyboardInterrupt: print() @@ -70,7 +73,7 @@ def widish_main(file_name, draw): path = Path(file_name) if draw: diagram_draw(path, fd) - runner = SHELL_TO_PYTHON(fd) + runner = SHELL_INTERPRETER(fd) run_res = runner("") if sys.stdin.isatty() else runner(sys.stdin.read()) From 31c222e123ef02d7076f8c46fbcd58b1d2b2df51 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Thu, 19 Mar 2026 16:34:51 +0000 Subject: [PATCH 11/17] fix: load mapping --- tests/test_loader.py | 20 ++++++++++++++------ widip/metaprog/hif.py | 21 +++++++++++++++++++++ widip/state/loader.py | 7 ++++++- widip/wire/loader.py | 2 +- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index e16e37b..6bf200a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -28,13 +28,13 @@ def test_loader_scalar_program_is_functorial(): assert compiled == Literal("scalar") -def test_loader_translation_preserves_tagged_mapping_nodes(): +def test_loader_translation_flattens_tagged_scalar_mapping_into_argv(): graph = nx_compose_all("!echo\n? scalar\n") program = HIFToLoader()(graph) - assert isinstance(program, LoaderMapping) + assert isinstance(program, LoaderScalar) assert program.tag == "echo" - assert len(program.branches) == 1 + assert program.value == ("scalar",) def test_loader_translation_uses_hif_metaprogram(): @@ -98,13 +98,21 @@ def test_loader_tagged_sequence_compiles_like_untagged_sequence(): assert LoaderToShell()(tagged_program) == LoaderToShell()(untagged_program) -def test_loader_tagged_mapping_compiles_like_untagged_mapping(): +def test_loader_tagged_mapping_of_scalars_is_command(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() tagged_program = HIFToLoader()(nx_compose_all("!echo\n? foo\n? bar\n")) - untagged_program = LoaderMapping(tagged_program.branches) + + assert isinstance(tagged_program, LoaderScalar) + assert tagged_program.tag == "echo" + assert tagged_program.value == ("foo", "bar") + assert LoaderToShell()(tagged_program) == Command(["echo", "foo", "bar"]) @ io_ty >> execution + + +def test_loader_tagged_mapping_with_non_scalar_value_stays_mapping(): + tagged_program = HIFToLoader()(nx_compose_all("!echo\n? foo: !wc -c\n")) assert isinstance(tagged_program, LoaderMapping) assert tagged_program.tag == "echo" - assert LoaderToShell()(tagged_program) == LoaderToShell()(untagged_program) def test_loader_shell_case_study_is_mapping_bubble(): diff --git a/widip/metaprog/hif.py b/widip/metaprog/hif.py index f743012..623320e 100644 --- a/widip/metaprog/hif.py +++ b/widip/metaprog/hif.py @@ -5,6 +5,23 @@ from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline +def _mapping_command_args(entries): + """Return argv pieces for tagged scalar mappings, else ``None``.""" + argv = [] + for key_node, value_node in entries: + if not isinstance(key_node, LoaderScalar) or key_node.tag is not None: + return None + if not isinstance(value_node, LoaderScalar) or value_node.tag is not None: + return None + if not isinstance(key_node.value, str) or not isinstance(value_node.value, str): + return None + if key_node.value: + argv.append(key_node.value) + if value_node.value: + argv.append(value_node.value) + return tuple(argv) + + def _successor_nodes(graph: HyperGraph, node, *, edge_key="next", node_key="start"): """Yield successor nodes reached by one edge-node incidence pattern.""" for edge, _, _, _ in hif_node_incidences(graph, node, key=edge_key, direction="head"): @@ -105,6 +122,10 @@ def node_map(self, graph: HyperGraph, node, kind: str, value, tag: str | None): case "sequence": return LoaderSequence(value, tag=tag) case "mapping": + if tag is not None: + command_args = _mapping_command_args(value) + if command_args is not None: + return LoaderScalar(command_args, tag) branches = tuple(pipeline((key, entry_value)) for key, entry_value in value) return LoaderMapping(branches, tag=tag) case _: diff --git a/widip/state/loader.py b/widip/state/loader.py index 1f209a1..c03c61c 100644 --- a/widip/state/loader.py +++ b/widip/state/loader.py @@ -60,8 +60,13 @@ def compile_scalar(self, node: loader_wire.LoaderScalar): shell_lang.io_ty, ).output_diagram() if node.tag: - argv = (node.tag,) if not node.value else (node.tag, node.value) + if isinstance(node.value, tuple): + argv = (node.tag, *node.value) + else: + argv = (node.tag,) if not node.value else (node.tag, node.value) return shell_lang.Command(argv) @ shell_lang.io_ty >> execution + if isinstance(node.value, tuple): + raise TypeError(f"untagged argv tuple is unsupported: {node.value!r}") if not node.value: return shell_wire.shell_id() return shell_lang.Literal(node.value) @ shell_lang.io_ty >> execution diff --git a/widip/wire/loader.py b/widip/wire/loader.py index 88b97e1..a0214f1 100644 --- a/widip/wire/loader.py +++ b/widip/wire/loader.py @@ -12,7 +12,7 @@ class LoaderScalar(Box): """Atomic YAML scalar node in the loader language.""" - def __init__(self, value: str, tag: str | None = None): + def __init__(self, value: str | tuple[str, ...], tag: str | None = None): self.value = value self.tag = tag if tag: From 2144298913dd30c1d45040e21df7e13c8040dd82 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Thu, 19 Mar 2026 20:02:01 +0000 Subject: [PATCH 12/17] wip: command substitution --- tests/test_bin_widish.py | 40 +++++++++++++++ tests/test_lang.py | 35 +++++++++++++ tests/test_loader.py | 40 +++++++++------ tests/test_watch.py | 104 +++++++++++++++++++++++++++++++++++++ widip/metaprog/hif.py | 25 ++++++--- widip/state/loader.py | 41 +++++++++++++-- widip/state/python.py | 80 ++++++++++++++++++----------- widip/state/widish.py | 21 +++++++- widip/watch.py | 107 +++++++++++++++++++++++++++++++++++++-- widip/wire/loader.py | 2 +- 10 files changed, 434 insertions(+), 61 deletions(-) create mode 100644 tests/test_bin_widish.py create mode 100644 tests/test_watch.py diff --git a/tests/test_bin_widish.py b/tests/test_bin_widish.py new file mode 100644 index 0000000..a787118 --- /dev/null +++ b/tests/test_bin_widish.py @@ -0,0 +1,40 @@ +import os +import subprocess + + +def run_widish(*args, env=None): + run_env = os.environ.copy() + run_env.setdefault("MPLCONFIGDIR", "/tmp/widip-mpl") + if env is not None: + run_env.update(env) + return subprocess.run( + ["bin/widish", *args], + text=True, + capture_output=True, + check=False, + env=run_env, + ) + + +def test_bin_widish_c_executes_yaml_command(): + result = run_widish("-c", "!echo hello-from-widish") + + assert result.returncode == 0 + assert result.stdout.splitlines() == ["hello-from-widish"] + assert result.stderr == "" + + +def test_bin_widish_can_be_used_via_shell_env_var(): + env = os.environ.copy() + env["SHELL"] = "bin/widish" + result = run_widish("-c", "!echo hello-from-shell-env", env=env) + + assert result.returncode == 0 + assert result.stdout.splitlines() == ["hello-from-shell-env"] + + +def test_bin_widish_c_requires_argument(): + result = run_widish("-c") + + assert result.returncode == 2 + assert "widish: missing argument for -c" in result.stderr diff --git a/tests/test_lang.py b/tests/test_lang.py index 10f70c2..f01c5d3 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -42,6 +42,13 @@ def test_sh_command_runs_through_shell_runner(): assert SHELL_INTERPRETER(program)("world\n") == "shell:world" +def test_tagged_mapping_style_command_substitution_runs_in_argv(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + program = Command(["echo", "Hello", "World", Command(["echo", "Foo"]), "!"]) @ io_ty >> execution + + assert SHELL_INTERPRETER(program)("") == "Hello World Foo !\n" + + def test_shell_language_chooses_shell_program_type_and_execution(): execution = SHELL.execution(io_ty, io_ty) assert SHELL.program_ty == shell_program_ty @@ -115,6 +122,34 @@ def test_discorun_parallel_example_runs(): assert SHELL_INTERPRETER(program)("a\nx\n") == "a\nx\n1\n2\n" +def test_pipeline_copy_replays_prefix_command_per_parallel_branch(tmp_path): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + counter = tmp_path / "counter.txt" + counter.write_text("0") + increment_script = ( + "data=$(cat); " + "count=$(cat \"$1\"); " + "count=$((count+1)); " + "printf '%s' \"$count\" > \"$1\"; " + "printf '%s' \"$data\"" + ) + prefix = Command(["sh", "-c", increment_script, "sh", str(counter)]) @ io_ty >> execution + program = Pipeline( + ( + prefix, + Parallel( + ( + Command(["cat"]) @ io_ty >> execution, + Command(["cat"]) @ io_ty >> execution, + ) + ), + ) + ) + + assert SHELL_INTERPRETER(program)("hello") == "hellohello" + assert counter.read_text() == "2" + + def test_parallel_preserves_argv_literals_without_shell_reparsing(): execution = SHELL.execution(io_ty, io_ty).output_diagram() program = Parallel( diff --git a/tests/test_loader.py b/tests/test_loader.py index 6bf200a..52d0cd0 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -9,6 +9,7 @@ from widip.pcc import SHELL from widip.state.core import InputOutputMap, StateUpdateMap from widip.state.loader import LoaderExecution, LoaderToShell +from widip.state.python import SHELL_INTERPRETER from widip.state.widish import Parallel, Pipeline from widip.wire.hif import HyperGraph from widip.wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_stream_ty @@ -108,6 +109,24 @@ def test_loader_tagged_mapping_of_scalars_is_command(): assert LoaderToShell()(tagged_program) == Command(["echo", "foo", "bar"]) @ io_ty >> execution +def test_loader_tagged_mapping_supports_command_substitution_keys(): + program = HIFToLoader()(nx_compose_all(Path("examples/hello-world-map.yaml").read_text())) + + assert isinstance(program, LoaderScalar) + assert program.tag == "echo" + assert program.value[:2] == ("Hello", "World") + assert isinstance(program.value[2], LoaderScalar) + assert program.value[2].tag == "echo" + assert program.value[2].value == "Foo" + assert program.value[3] == "!" + + +def test_loader_tagged_mapping_command_substitution_runs(): + program = LoaderToShell()(HIFToLoader()(nx_compose_all(Path("examples/hello-world-map.yaml").read_text()))) + + assert SHELL_INTERPRETER(program)("") == "Hello World Foo !\n" + + def test_loader_tagged_mapping_with_non_scalar_value_stays_mapping(): tagged_program = HIFToLoader()(nx_compose_all("!echo\n? foo: !wc -c\n")) @@ -117,23 +136,14 @@ def test_loader_tagged_mapping_with_non_scalar_value_stays_mapping(): def test_loader_shell_case_study_is_mapping_bubble(): execution = SHELL.execution(io_ty, io_ty).output_diagram() - expected = Parallel( + expected = (Command(["cat", "examples/shell.yaml"]) @ io_ty >> execution) >> Parallel( ( - (Command(["cat", "examples/shell.yaml"]) @ io_ty >> execution) - >> Parallel( - ( - (Command(["wc", "-c"]) @ io_ty >> execution), - Parallel( - ( - (Command(["grep", "grep"]) @ io_ty >> execution) - >> (Command(["wc", "-c"]) @ io_ty >> execution), - ) - ), - (Command(["tail", "-2"]) @ io_ty >> execution), - ) - ), + (Command(["wc", "-c"]) @ io_ty >> execution), + (Command(["grep", "grep"]) @ io_ty >> execution) + >> (Command(["wc", "-c"]) @ io_ty >> execution), + (Command(["tail", "-2"]) @ io_ty >> execution), ) ) diagram = LoaderToShell()(HIFToLoader()(nx_compose_all(Path("examples/shell.yaml").read_text()))) assert isinstance(diagram, Parallel) - assert diagram == expected + assert diagram.specialize() == expected diff --git a/tests/test_watch.py b/tests/test_watch.py new file mode 100644 index 0000000..b698025 --- /dev/null +++ b/tests/test_watch.py @@ -0,0 +1,104 @@ +import pytest +from nx_yaml import nx_compose_all + +import widip.watch as watch +from widip.comput.widish import Command, io_ty +from widip.metaprog.hif import HIFToLoader +from widip.pcc import SHELL +from widip.state.loader import LoaderToShell +from widip.watch import CTRL_D, CTRL_J, CTRL_M, apply_tty_input, emit_shell_source, read_shell_source, watch_log + + +def test_apply_tty_input_uses_ctrl_j_as_newline_and_ctrl_m_as_submit(): + buffer = [] + + assert apply_tty_input(buffer, "!") == ("char", None) + assert apply_tty_input(buffer, "e") == ("char", None) + assert apply_tty_input(buffer, "c") == ("char", None) + assert apply_tty_input(buffer, "h") == ("char", None) + assert apply_tty_input(buffer, "o") == ("char", None) + assert apply_tty_input(buffer, CTRL_J) == ("newline", None) + assert apply_tty_input(buffer, "?") == ("char", None) + assert apply_tty_input(buffer, CTRL_M) == ("submit", None) + + assert "".join(buffer) == "!echo\n?" + + +def test_apply_tty_input_ctrl_d_on_empty_buffer_is_eof(): + buffer = [] + assert apply_tty_input(buffer, CTRL_D) == ("eof", None) + + +def test_apply_tty_input_ctrl_d_on_non_empty_buffer_submits(): + buffer = ["a"] + assert apply_tty_input(buffer, CTRL_D) == ("submit", None) + + +def test_apply_tty_input_backspace_removes_last_character(): + buffer = ["a", "b"] + action, removed = apply_tty_input(buffer, "\x7F") + + assert action == "backspace" + assert removed == "b" + assert buffer == ["a"] + + +def test_ctrl_j_multiline_document_compiles_as_expected(): + execution = SHELL.execution(io_ty, io_ty).output_diagram() + source = "!echo\n? foo\n? bar\n" + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all(source))) + + assert diagram == Command(["echo", "foo", "bar"]) @ io_ty >> execution + + +def test_read_shell_source_writes_prompt_to_stdout(capsys): + source = read_shell_source("bin/yaml/shell.yaml", read_line=lambda: "scalar") + captured = capsys.readouterr() + + assert source == "scalar" + assert captured.out == "--- !bin/yaml/shell.yaml\n" + assert captured.err == "" + + +def test_watch_log_writes_to_stderr(capsys): + watch_log("watching for changes in current path") + captured = capsys.readouterr() + + assert captured.out == "" + assert captured.err == "watching for changes in current path\n" + + +def test_emit_shell_source_writes_a_trailing_newline(capsys): + emit_shell_source("!echo ok") + captured = capsys.readouterr() + + assert captured.out == "!echo ok\n" + assert captured.err == "" + + +def test_shell_main_continues_after_invalid_command(monkeypatch, capsys): + class DummyObserver: + def stop(self): + return None + + sources = iter(("!git status --short\n", "!echo ok\n")) + + def fake_read_shell_source(_file_name): + try: + return next(sources) + except StopIteration as exc: + raise EOFError from exc + + monkeypatch.setattr(watch, "watch_main", lambda: DummyObserver()) + monkeypatch.setattr(watch, "read_shell_source", fake_read_shell_source) + + with pytest.raises(SystemExit) as raised: + watch.shell_main("bin/yaml/shell.yaml", draw=False) + + captured = capsys.readouterr() + + assert raised.value.code == 0 + assert "!git status --short" in captured.out + assert "!echo ok" in captured.out + assert "returned non-zero exit status" in captured.err + assert "ok" in captured.out diff --git a/widip/metaprog/hif.py b/widip/metaprog/hif.py index 623320e..8e0421b 100644 --- a/widip/metaprog/hif.py +++ b/widip/metaprog/hif.py @@ -5,18 +5,31 @@ from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline +def _mapping_key_command_arg(key_node): + """Compile one mapping key to an argv argument for tagged mappings.""" + if not isinstance(key_node, LoaderScalar): + return None + if not isinstance(key_node.value, str): + return None + if key_node.tag is None: + return key_node.value + # Tagged scalar keys are command-substitution arguments. + return LoaderScalar(key_node.value, key_node.tag) + + def _mapping_command_args(entries): """Return argv pieces for tagged scalar mappings, else ``None``.""" argv = [] for key_node, value_node in entries: - if not isinstance(key_node, LoaderScalar) or key_node.tag is not None: - return None if not isinstance(value_node, LoaderScalar) or value_node.tag is not None: return None - if not isinstance(key_node.value, str) or not isinstance(value_node.value, str): + key_arg = _mapping_key_command_arg(key_node) + if key_arg is None: + return None + if not isinstance(value_node.value, str): return None - if key_node.value: - argv.append(key_node.value) + if key_arg: + argv.append(key_arg) if value_node.value: argv.append(value_node.value) return tuple(argv) @@ -126,7 +139,7 @@ def node_map(self, graph: HyperGraph, node, kind: str, value, tag: str | None): command_args = _mapping_command_args(value) if command_args is not None: return LoaderScalar(command_args, tag) - branches = tuple(pipeline((key, entry_value)) for key, entry_value in value) + branches = tuple(LoaderSequence((key, entry_value)) for key, entry_value in value) return LoaderMapping(branches, tag=tag) case _: raise ValueError(f"unsupported YAML node kind: {kind!r}") diff --git a/widip/state/loader.py b/widip/state/loader.py index c03c61c..f95fe46 100644 --- a/widip/state/loader.py +++ b/widip/state/loader.py @@ -53,6 +53,42 @@ def __call__(self, other): return Parallel(tuple(self(branch) for branch in other.branches)) return ProcessSimulation.__call__(self, other) + def compile_command_argument(self, argument): + """Compile one loader scalar argument to a shell argv item.""" + if isinstance(argument, str): + return argument + if not isinstance(argument, loader_wire.LoaderScalar): + raise TypeError(f"unsupported command argument: {argument!r}") + if argument.tag: + if isinstance(argument.value, tuple): + argv = ( + argument.tag, + *(self.compile_command_argument(value) for value in argument.value), + ) + else: + argv = ( + (argument.tag,) + if not argument.value + else (argument.tag, self.compile_command_argument(argument.value)) + ) + return shell_lang.Command(argv) + if isinstance(argument.value, tuple): + raise TypeError(f"untagged argv tuple is unsupported: {argument.value!r}") + if not isinstance(argument.value, str): + raise TypeError(f"untagged scalar argument must be a string: {argument.value!r}") + return argument.value + + def command_argv(self, node: loader_wire.LoaderScalar): + """Build argv for a tagged loader scalar.""" + if isinstance(node.value, tuple): + return ( + node.tag, + *(self.compile_command_argument(value) for value in node.value), + ) + if not node.value: + return (node.tag,) + return (node.tag, self.compile_command_argument(node.value)) + def compile_scalar(self, node: loader_wire.LoaderScalar): """Compile one YAML scalar node to the shell backend.""" execution = SHELL.execution( @@ -60,10 +96,7 @@ def compile_scalar(self, node: loader_wire.LoaderScalar): shell_lang.io_ty, ).output_diagram() if node.tag: - if isinstance(node.value, tuple): - argv = (node.tag, *node.value) - else: - argv = (node.tag,) if not node.value else (node.tag, node.value) + argv = self.command_argv(node) return shell_lang.Command(argv) @ shell_lang.io_ty >> execution if isinstance(node.value, tuple): raise TypeError(f"untagged argv tuple is unsupported: {node.value!r}") diff --git a/widip/state/python.py b/widip/state/python.py index 9caed80..fe5b7e5 100644 --- a/widip/state/python.py +++ b/widip/state/python.py @@ -15,13 +15,42 @@ from .widish import ShellSpecializer, shell_program_runner -def _has_shell_bubble(diagram) -> bool: - """Detect whether a shell diagram still contains unspecialized bubbles.""" - if isinstance(diagram, monoidal.Bubble): - return True - if not isinstance(diagram, computer.Diagram): - return False - return any(isinstance(layer[1], monoidal.Bubble) for layer in diagram.inside) +_PATHS_ATTR = "_widip_runtime_paths" + + +def _runner_paths(runner): + """Return execution paths carried by one interpreted shell stage.""" + paths = getattr(runner, _PATHS_ATTR, None) + if paths is None: + return ((runner,),) + return paths + + +def _compose_paths(prefixes, suffixes): + """Compose two path sets by Cartesian product.""" + return tuple(prefix + suffix for prefix in prefixes for suffix in suffixes) + + +def _run_paths(paths, stdin): + """Execute all independent pipelines sequentially and concatenate outputs.""" + outputs = [] + for path in paths: + output = stdin + for stage in path: + output = stage(output) + outputs.append(output) + if not outputs: + return stdin + if len(outputs) == 1: + return outputs[0] + return "".join(outputs) + + +def _path_runner(paths, dom, cod): + """Build a Python runner while preserving expanded pipeline-path metadata.""" + function = python.Function(lambda stdin, _paths=paths: _run_paths(_paths, stdin), dom, cod) + function.__dict__[_PATHS_ATTR] = paths + return function class ShellToPythonProgram(state_core.ProcessSimulation): @@ -88,34 +117,27 @@ def object(self, ob): return str def __call__(self, other): - if _has_shell_bubble(other): - return monoidal.Functor.__call__(self, self.specialize_shell(other)) return monoidal.Functor.__call__(self, other) def process_ar_map(self, box, dom, cod): if isinstance(box, ShellPipeline): - stages = tuple(self(stage) for stage in box.stages) - - def run(stdin): - output = stdin - for stage in stages: - output = stage(output) - return output - - return python.Function(run, dom, cod) + paths = ((),) + for stage in (self(stage) for stage in box.stages): + paths = _compose_paths(paths, _runner_paths(stage)) + return _path_runner(paths, dom, cod) if isinstance(box, ShellParallel): - branches = tuple(self(branch) for branch in box.branches) - - def run(stdin): - if not branches: - return stdin - if len(branches) == 1: - return branches[0](stdin) - return "".join(branch(stdin) for branch in branches) - - return python.Function(run, dom, cod) + branch_paths = tuple( + path + for branch in (self(branch) for branch in box.branches) + for path in _runner_paths(branch) + ) + if not branch_paths: + branch_paths = ((),) + return _path_runner(branch_paths, dom, cod) if isinstance(box, (shell_lang.ShellProgram, computer.Computer)): - return self.python_runtime(self.program_functor(box)) + runner = self.python_runtime(self.program_functor(box)) + runner.__dict__[_PATHS_ATTR] = ((runner,),) + return runner raise TypeError(f"unsupported shell interpreter box: {box!r}") diff --git a/widip/state/widish.py b/widip/state/widish.py index 24054e9..595c2a3 100644 --- a/widip/state/widish.py +++ b/widip/state/widish.py @@ -8,6 +8,25 @@ from ..wire import widish as shell_wire +def _resolve_command_substitution(argument, stdin: str) -> str: + """Evaluate one command-substitution argument.""" + if isinstance(argument, shell_lang.Command): + # Shell command substitution strips trailing newlines. + return shell_program_runner(argument)(stdin).rstrip("\n") + if isinstance(argument, shell_lang.Literal): + return argument.text + if isinstance(argument, shell_lang.Empty): + return "" + if not isinstance(argument, str): + raise TypeError(f"unsupported command argument type: {argument!r}") + return argument + + +def _resolve_command_argv(argv, stdin: str) -> tuple[str, ...]: + """Resolve a shell command argv tuple to plain subprocess arguments.""" + return tuple(_resolve_command_substitution(argument, stdin) for argument in argv) + + def parallel_io_diagram(branches): """Lower shell-IO branching to structural shell parallel composition.""" branches = tuple(branches) @@ -27,7 +46,7 @@ def shell_program_runner(program): if isinstance(program, shell_lang.Command): def run(stdin: str) -> str: completed = subprocess.run( - program.argv, + _resolve_command_argv(program.argv, stdin), input=stdin, text=True, capture_output=True, diff --git a/widip/watch.py b/widip/watch.py index 80c87cf..1117287 100644 --- a/widip/watch.py +++ b/widip/watch.py @@ -1,5 +1,8 @@ from pathlib import Path +import subprocess import sys +import termios +import tty from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer from yaml import YAMLError @@ -14,24 +17,111 @@ # TODO watch functor ?? +CTRL_D = "\x04" +CTRL_J = "\x0A" +CTRL_M = "\x0D" +BACKSPACE = {"\x08", "\x7F"} + + +def apply_tty_input(buffer: list[str], char: str): + """Update the pending YAML document buffer for one TTY character.""" + if char == CTRL_D: + return ("eof" if not buffer else "submit", None) + if char == CTRL_M: + return ("submit", None) + if char == CTRL_J: + buffer.append("\n") + return ("newline", None) + if char in BACKSPACE: + removed = buffer.pop() if buffer else None + return ("backspace", removed) + buffer.append(char) + return ("char", None) + + +def read_tty_yaml_document(): + """Read one YAML document from TTY using Ctrl+J for LF and Ctrl+M to submit.""" + fd = sys.stdin.fileno() + previous = termios.tcgetattr(fd) + buffer = [] + + try: + tty.setraw(fd) + while True: + raw = sys.stdin.buffer.read(1) + if raw == b"": + raise EOFError + + char = raw.decode("latin1") + action, removed = apply_tty_input(buffer, char) + + if action == "eof": + raise EOFError + if action == "submit": + sys.stdout.write("\n") + sys.stdout.flush() + return "".join(buffer) + if action == "newline": + sys.stdout.write("\n") + sys.stdout.flush() + continue + if action == "backspace": + if removed and removed != "\n": + sys.stdout.write("\b \b") + sys.stdout.flush() + continue + + sys.stdout.write(char) + sys.stdout.flush() + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, previous) + + +def watch_log(message: str): + """Write watcher status logs to stderr.""" + print(message, file=sys.stderr) + + +def read_shell_source(file_name: str, read_line=None): + """Write command-document prompt to stdout and read one YAML document.""" + prompt = f"--- !{file_name}\n" + sys.stdout.write(prompt) + sys.stdout.flush() + if read_line is not None: + return read_line() + if sys.stdin.isatty(): + return read_tty_yaml_document() + source = sys.stdin.readline() + if source == "": + raise EOFError + return source + + +def emit_shell_source(source: str): + """Emit the executed YAML source document to stdout for transcript logging.""" + sys.stdout.write(source) + if not source.endswith("\n"): + sys.stdout.write("\n") + sys.stdout.flush() + 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}") + watch_log(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) + watch_log(str(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") + watch_log("watching for changes in current path") observer = Observer() shell_handler = ShellHandler() observer.schedule(shell_handler, ".", recursive=True) @@ -45,8 +135,8 @@ def shell_main(file_name, draw=True): while True: observer = watch_main() try: - prompt = f"--- !{file_name}\n" - source = input(prompt) + source = read_shell_source(file_name) + emit_shell_source(source) source_d = loader_to_shell(hif_to_loader(nx_compose_all(source))) # source_d.draw( # textpad=(0.3, 0.1), @@ -62,6 +152,13 @@ def shell_main(file_name, draw=True): print() except YAMLError as e: print(e) + except subprocess.CalledProcessError as error: + watch_log(str(error)) + stderr = (error.stderr or "").strip() + if stderr: + watch_log(stderr) + except FileNotFoundError as error: + watch_log(str(error)) finally: observer.stop() except EOFError: diff --git a/widip/wire/loader.py b/widip/wire/loader.py index a0214f1..bd2102f 100644 --- a/widip/wire/loader.py +++ b/widip/wire/loader.py @@ -12,7 +12,7 @@ class LoaderScalar(Box): """Atomic YAML scalar node in the loader language.""" - def __init__(self, value: str | tuple[str, ...], tag: str | None = None): + def __init__(self, value: str | tuple[object, ...], tag: str | None = None): self.value = value self.tag = tag if tag: From 705c81ff354b76c90aae5ecb1c08aa654f488492 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Fri, 20 Mar 2026 20:08:42 +0000 Subject: [PATCH 13/17] wip --- bin/widish | 30 ++++++- tests/test_loader.py | 24 +++-- widip/SKILL.md | 198 ++++++++++++++++++++++++++++++++++++++++++ widip/__main__.py | 23 ++++- widip/metaprog/hif.py | 16 ++++ widip/state/loader.py | 35 +++++--- widip/state/widish.py | 35 +++++++- 7 files changed, 337 insertions(+), 24 deletions(-) create mode 100644 widip/SKILL.md diff --git a/bin/widish b/bin/widish index 8d1fb99..6b2b247 100755 --- a/bin/widish +++ b/bin/widish @@ -1,2 +1,30 @@ #!/bin/sh -exec python -m widip "$@" +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SELF_DIR/.." && pwd) + +if [ -x "$ROOT_DIR/.venv/bin/python" ]; then + PYTHON_BIN="$ROOT_DIR/.venv/bin/python" +elif command -v python3 >/dev/null 2>&1; then + PYTHON_BIN=$(command -v python3) +else + PYTHON_BIN=$(command -v python) +fi + +export PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}" + +if [ "${1-}" = "-c" ]; then + shift + if [ $# -lt 1 ]; then + echo "widish: missing argument for -c" >&2 + exit 2 + fi + command_text=$1 + # Consume optional shell-style positional parameters (arg0, args...). + shift + printf '%s\n' "$command_text" | "$PYTHON_BIN" -m widip --no-draw /dev/stdin + exit $? +fi + +exec "$PYTHON_BIN" -m widip "$@" diff --git a/tests/test_loader.py b/tests/test_loader.py index 52d0cd0..85f68b3 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -110,21 +110,31 @@ def test_loader_tagged_mapping_of_scalars_is_command(): def test_loader_tagged_mapping_supports_command_substitution_keys(): - program = HIFToLoader()(nx_compose_all(Path("examples/hello-world-map.yaml").read_text())) + source = """!echo +? Hello +? World +? !echo Foo: !wc -c +? "!" +""" + program = HIFToLoader()(nx_compose_all(source)) assert isinstance(program, LoaderScalar) assert program.tag == "echo" assert program.value[:2] == ("Hello", "World") - assert isinstance(program.value[2], LoaderScalar) - assert program.value[2].tag == "echo" - assert program.value[2].value == "Foo" + assert isinstance(program.value[2], LoaderMapping) assert program.value[3] == "!" def test_loader_tagged_mapping_command_substitution_runs(): - program = LoaderToShell()(HIFToLoader()(nx_compose_all(Path("examples/hello-world-map.yaml").read_text()))) - - assert SHELL_INTERPRETER(program)("") == "Hello World Foo !\n" + source = """!echo +? Hello +? World +? !echo Foo: !wc -c +? "!" +""" + program = LoaderToShell()(HIFToLoader()(nx_compose_all(source))) + + assert SHELL_INTERPRETER(program)("") == "Hello World 4 !\n" def test_loader_tagged_mapping_with_non_scalar_value_stays_mapping(): diff --git a/widip/SKILL.md b/widip/SKILL.md new file mode 100644 index 0000000..7c7f4ce --- /dev/null +++ b/widip/SKILL.md @@ -0,0 +1,198 @@ +--- +name: widip +description: Use when working with widip or widish as a YAML-native shell and REPL for humans and LLM agents. Use for authoring, executing, explaining, or reviewing YAML command documents as a preferred structured command language for general-purpose work, including pipelines, grouped parallel work, command substitution, and explicit shell fragments when needed. +--- + +# Widip + +## Adopt The Model + +Treat every syntactically valid YAML document as a valid widish command document. + +Let behavior come from: +1. The document structure. +2. The tagged commands inside the document. + +Think in terms of a YAML tree, not a shell parser: +1. Let syntax define composition. +2. Let tags define command behavior. +3. Let nesting define scope and substitution. + +Choose widish as a primary structured command language for interactive and scripted work. + +## Choose Widish By Preference + +Choose widish as a normal working style because: +1. It keeps pipelines, grouping, and substitution explicit. +2. It keeps commands readable as data. +3. It gives agents and humans one representation they can both inspect and revise. +4. It supports interactive work, saved command documents, and generated command codebases. + +Keep the boundary in mind: +1. Widish provides structured command composition. +2. Platform command availability still depends on the underlying environment. + +## Build YAML As A Codebase + +Create YAML documents while working instead of treating them as a final export step. + +Use this workflow: +1. Write command documents incrementally as the task evolves. +2. Treat related YAML documents as a codebase, not as isolated snippets. +3. Put that codebase in the user's preferred location. +4. Organize files so humans and agents can navigate and revise them easily. + +Use helper scripts when they improve speed or reliability: +1. Generate or refactor YAML documents with scripts when repetition would otherwise cause drift. +2. Use any scripting language the user prefers. +3. Keep helper scripts subordinate to the YAML codebase rather than replacing it. +4. Let scripts support document creation, transformation, validation, or synchronization. + +## Write Command Documents + +Use a tagged scalar for command invocation: + +```yaml +!echo Hello world! +``` + +Use a sequence for pipeline composition: + +```yaml +- !grep grep +- !wc -c +``` + +Use a mapping for structured grouping: + +```yaml +? !printf left +? !printf right +``` + +Use tagged mapping entries to build explicit command arguments: + +```yaml +!echo +? foo +? bar +``` + +## Use YAML As A Heredoc + +Treat a YAML document as the widish equivalent of the multiline inline scripts that agents often send through shell heredocs. + +Agents commonly use heredocs to do this: +1. Open one inline multiline block. +2. Put the whole script inside it. +3. Hand that block to a shell or interpreter. + +Use YAML for the same operational purpose when the work is really a command document rather than raw shell text. + +Prefer YAML-as-heredoc when: +1. The agent is about to emit a multiline inline script. +2. The logic is better expressed as commands, pipelines, mappings, and substitution than as shell punctuation. +3. The document should stay readable and editable as structured data. +4. The same inline block should be understandable by both humans and agents. +5. You want the multiline unit to remain valid YAML before execution. + +Use a plain shell heredoc only when the inline block truly needs to be shell script text. + +Follow the practical rule: +1. If the agent is about to write a multiline inline shell script, first ask whether the block is actually a widish command document. +2. If yes, write YAML. +3. If no, keep the heredoc or embed an explicit shell fragment inside YAML. + +## Use Mappings Deliberately + +Use mappings to express grouped parallel work that shell punctuation usually hides. + +Use a shared parent command with grouped child branches: + +```yaml +!cat examples/shell.yaml: + ? !wc -c + ? !grep grep: !wc -c + ? !tail -2 +``` + +Read this as: +1. Run one shared upstream command. +2. Feed its output into multiple child branches. +3. Keep the grouping explicit in the YAML structure. + +Understand the nature of the parallelism: +1. Treat the parent command as one shared source of input for all child branches. +2. Treat each child as an independent branch that receives that same upstream output. +3. Treat the result as structured fan-out and merge, not as shell punctuation spread across several unrelated lines. +4. Expect branch outputs to be combined in branch order, so the document structure still determines the visible output order. + +Use mapping children for command substitution when a parent command needs values produced by subcommands: + +```yaml +!echo +? Hello +? !printf {"%s!", "World"} +``` + +Read this as: +1. Pass `Hello` as a plain argument. +2. Run the tagged child as a subprogram. +3. Inject its output into the parent command's argv. + +Use shell intuition only as a translation aid: + +```bash +echo Hello "$(printf "%s!" World)" +``` + +Treat substitution as value production, not as text pasted back into a shell parser. + +## Use The REPL + +Treat the REPL as document-oriented input: +1. Enter one YAML document per submission. +2. Build multiline documents before executing them. +3. Think in documents, not in POSIX shell lines. + +Use the interactive controls as follows: +1. Press `Ctrl+J` to insert a newline inside the current document. +2. Press `Enter` to submit the current document. +3. Press `Ctrl+D` to exit when the current document is empty. + +Preserve transcript output verbatim and in order when logging or replaying sessions. + +## Compose With Explicit Shells + +Prefer direct command structure over shell re-parsing whenever possible. + +Use an explicit shell as one component of a widish program when the task genuinely depends on shell grammar, such as: +1. Shell builtins. +2. Redirection-heavy one-liners. +3. Loops or compound shell conditionals. +4. Shell-specific expansion rules. + +Keep the outer document in YAML whenever possible and isolate only the shell-dependent fragment. + +Use the hybrid pattern: + +```yaml +!bash {-c, "for x in a b c; do echo \"$x\"; done"} +``` + +Prefer this style when only one part of the task needs shell grammar: +1. Keep the widish document as the main program structure. +2. Derive only the scripted fragment into `bash -c`, `sh -c`, or another explicit shell command. +3. Keep pipelines, grouping, and surrounding dataflow in YAML when they do not need shell parsing. + +Treat the shell fragment as an explicit embedded tool, not as widish's native grammar. + +## Communicate Clearly + +Use widish as both an execution format and a communication format: +1. Keep documents readable. +2. Keep structure visible. +3. Prefer explicit grouping over clever punctuation. +4. Write examples that humans and agents can both follow quickly. + +Use widish to make command intent inspectable, reviewable, and easier to transform. diff --git a/widip/__main__.py b/widip/__main__.py index e655f27..eead9b3 100644 --- a/widip/__main__.py +++ b/widip/__main__.py @@ -1,8 +1,27 @@ import sys import argparse import logging +import os +import tempfile -# Stop starting a Matplotlib GUI + +def configure_matplotlib_cache(): + """Set a writable default MPLCONFIGDIR when the environment does not provide one.""" + if "MPLCONFIGDIR" in os.environ: + return + + cache_dir = os.path.join(tempfile.gettempdir(), "widip-mpl") + try: + os.makedirs(cache_dir, exist_ok=True) + os.environ["MPLCONFIGDIR"] = cache_dir + except OSError: + # Fall back to Matplotlib defaults if we cannot create the cache directory. + pass + + +configure_matplotlib_cache() + +# Stop starting a Matplotlib GUI. import matplotlib matplotlib.use('agg') @@ -48,4 +67,4 @@ def main(argv): widish_main(args.file_name, draw) if __name__ == "__main__": - main(sys.argv) \ No newline at end of file + main(sys.argv) diff --git a/widip/metaprog/hif.py b/widip/metaprog/hif.py index 8e0421b..118577e 100644 --- a/widip/metaprog/hif.py +++ b/widip/metaprog/hif.py @@ -5,8 +5,24 @@ from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline +def _is_command_substitution_program(node): + """Return whether one loader node is a valid substitution subprogram.""" + if isinstance(node, LoaderScalar): + return isinstance(node.value, str) and node.tag is not None + if isinstance(node, LoaderSequence): + return bool(node.stages) and all(_is_command_substitution_program(stage) for stage in node.stages) + if isinstance(node, LoaderMapping): + return bool(node.branches) and all(_is_command_substitution_program(branch) for branch in node.branches) + return False + + def _mapping_key_command_arg(key_node): """Compile one mapping key to an argv argument for tagged mappings.""" + if isinstance(key_node, (LoaderSequence, LoaderMapping)): + if not _is_command_substitution_program(key_node): + return None + # Structured keys are command-substitution programs. + return key_node if not isinstance(key_node, LoaderScalar): return None if not isinstance(key_node.value, str): diff --git a/widip/state/loader.py b/widip/state/loader.py index f95fe46..bebbd7d 100644 --- a/widip/state/loader.py +++ b/widip/state/loader.py @@ -8,7 +8,7 @@ from ..wire.loader import loader_stream_ty from ..wire import widish as shell_wire from .core import Execution, ProcessSimulation -from .widish import Parallel, Pipeline +from .widish import Parallel, Pipeline, SubstitutionParallel, SubstitutionPipeline class LoaderExecution(Execution): @@ -53,25 +53,34 @@ def __call__(self, other): return Parallel(tuple(self(branch) for branch in other.branches)) return ProcessSimulation.__call__(self, other) + def compile_subprogram(self, node): + """Compile one loader node to a shell subprogram for substitution.""" + if isinstance(node, loader_wire.LoaderScalar): + if node.tag: + return shell_lang.Command(self.command_argv(node)) + if isinstance(node.value, tuple): + raise TypeError(f"untagged argv tuple is unsupported: {node.value!r}") + if not isinstance(node.value, str): + raise TypeError(f"untagged scalar argument must be a string: {node.value!r}") + if not node.value: + return shell_lang.Empty() + return shell_lang.Literal(node.value) + if isinstance(node, loader_wire.LoaderSequence): + return SubstitutionPipeline(tuple(self.compile_subprogram(stage) for stage in node.stages)) + if isinstance(node, loader_wire.LoaderMapping): + return SubstitutionParallel(tuple(self.compile_subprogram(branch) for branch in node.branches)) + raise TypeError(f"unsupported substitution node: {node!r}") + def compile_command_argument(self, argument): """Compile one loader scalar argument to a shell argv item.""" if isinstance(argument, str): return argument + if isinstance(argument, (loader_wire.LoaderSequence, loader_wire.LoaderMapping)): + return self.compile_subprogram(argument) if not isinstance(argument, loader_wire.LoaderScalar): raise TypeError(f"unsupported command argument: {argument!r}") if argument.tag: - if isinstance(argument.value, tuple): - argv = ( - argument.tag, - *(self.compile_command_argument(value) for value in argument.value), - ) - else: - argv = ( - (argument.tag,) - if not argument.value - else (argument.tag, self.compile_command_argument(argument.value)) - ) - return shell_lang.Command(argv) + return self.compile_subprogram(argument) if isinstance(argument.value, tuple): raise TypeError(f"untagged argv tuple is unsupported: {argument.value!r}") if not isinstance(argument.value, str): diff --git a/widip/state/widish.py b/widip/state/widish.py index 595c2a3..ee8c43d 100644 --- a/widip/state/widish.py +++ b/widip/state/widish.py @@ -8,9 +8,23 @@ from ..wire import widish as shell_wire +class SubstitutionPipeline: + """Sequence of shell subprograms used inside command substitution.""" + + def __init__(self, stages): + self.stages = tuple(stages) + + +class SubstitutionParallel: + """Parallel shell subprogram branches used inside command substitution.""" + + def __init__(self, branches): + self.branches = tuple(branches) + + def _resolve_command_substitution(argument, stdin: str) -> str: """Evaluate one command-substitution argument.""" - if isinstance(argument, shell_lang.Command): + if isinstance(argument, (shell_lang.Command, SubstitutionPipeline, SubstitutionParallel)): # Shell command substitution strips trailing newlines. return shell_program_runner(argument)(stdin).rstrip("\n") if isinstance(argument, shell_lang.Literal): @@ -43,6 +57,25 @@ def shell_program_runner(program): return lambda stdin: stdin if isinstance(program, shell_lang.Literal): return lambda _stdin: program.text + if isinstance(program, SubstitutionPipeline): + stage_runners = tuple(shell_program_runner(stage) for stage in program.stages) + + def run(stdin: str) -> str: + output = stdin + for stage_runner in stage_runners: + output = stage_runner(output) + return output + + return run + if isinstance(program, SubstitutionParallel): + branch_runners = tuple(shell_program_runner(branch) for branch in program.branches) + + def run(stdin: str) -> str: + if not branch_runners: + return stdin + return "".join(branch_runner(stdin) for branch_runner in branch_runners) + + return run if isinstance(program, shell_lang.Command): def run(stdin: str) -> str: completed = subprocess.run( From 4013e7b716000219a62e4a918216319d0c34db3e Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sat, 21 Mar 2026 17:19:21 +0000 Subject: [PATCH 14/17] MYTILUS --- README.md | 18 +- bin/{widish => mytilus} | 15 +- bin/yaml/python.yaml | 2 +- bin/yaml/shell.jpg | Bin 0 -> 5503 bytes debug.py | 6 +- examples/README.md | 13 +- examples/aoc2025/1-1.jpg | Bin 96006 -> 34771 bytes examples/aoc2025/README.md | 10 +- examples/hello-world-map.jpg | Bin 78090 -> 32824 bytes examples/hello-world-map.yaml.jpg | Bin 0 -> 6093 bytes examples/shell.jpg | Bin 65235 -> 6061 bytes examples/shell.yaml | 5 + mytilus-command.jpg | Bin 0 -> 5871 bytes {widip => mytilus}/SKILL.md | 85 +- {widip => mytilus}/__init__.py | 0 mytilus/__main__.py | 98 + {widip => mytilus}/comput/__init__.py | 2 +- {widip => mytilus}/comput/boxes.py | 0 {widip => mytilus}/comput/compile.py | 0 {widip => mytilus}/comput/computer.py | 0 {widip => mytilus}/comput/equations.py | 0 {widip => mytilus}/comput/loader.py | 0 .../widish.py => mytilus/comput/mytilus.py | 2 +- {widip => mytilus}/comput/python.py | 0 {widip => mytilus}/files.py | 11 +- mytilus/interactive.py | 129 + {widip => mytilus}/metaprog/__init__.py | 4 +- {widip => mytilus}/metaprog/core.py | 0 {widip => mytilus}/metaprog/hif.py | 0 .../widish.py => mytilus/metaprog/mytilus.py | 4 +- {widip => mytilus}/metaprog/python.py | 0 {widip => mytilus}/pcc/__init__.py | 2 +- {widip => mytilus}/pcc/core.py | 0 {widip => mytilus}/pcc/loader.py | 0 widip/pcc/widish.py => mytilus/pcc/mytilus.py | 4 +- {widip => mytilus}/state/__init__.py | 2 +- {widip => mytilus}/state/core.py | 0 {widip => mytilus}/state/hif.py | 0 {widip => mytilus}/state/loader.py | 6 +- .../widish.py => mytilus/state/mytilus.py | 80 +- {widip => mytilus}/state/python.py | 10 +- mytilus/watch.py | 160 + {widip => mytilus}/wire/__init__.py | 0 {widip => mytilus}/wire/functions.py | 0 {widip => mytilus}/wire/hif.py | 0 {widip => mytilus}/wire/loader.py | 0 .../wire/widish.py => mytilus/wire/mytilus.py | 0 {widip => mytilus}/wire/services.py | 0 {widip => mytilus}/wire/types.py | 0 pyproject.toml | 8 +- tests/metaprog/python.py | 6 +- tests/{widish => mytilus}/0.in | 0 tests/{widish => mytilus}/0.mprog.svg | 887 +- tests/{widish => mytilus}/0.out | 4 +- tests/mytilus/0.prog.svg | 1001 + tests/{widish => mytilus}/0.yaml | 0 tests/{widish => mytilus}/01.in | 0 tests/{widish => mytilus}/01.mprog.svg | 0 tests/{widish => mytilus}/01.out | 0 tests/{widish => mytilus}/01.prog.svg | 0 tests/{widish => mytilus}/01.yaml | 0 tests/{widish => mytilus}/02.in | 0 tests/{widish => mytilus}/02.mprog.svg | 0 tests/{widish => mytilus}/02.out | 0 tests/{widish => mytilus}/02.prog.svg | 0 tests/{widish => mytilus}/02.yaml | 0 tests/{widish => mytilus}/03.in | 0 tests/{widish => mytilus}/03.mprog.svg | 0 tests/{widish => mytilus}/03.out | 0 tests/{widish => mytilus}/03.prog.svg | 0 tests/{widish => mytilus}/03.yaml | 0 tests/{widish => mytilus}/04.in | 0 .../15.mprog.svg => mytilus/04.mprog.svg} | 332 +- tests/{widish => mytilus}/04.out | 0 tests/{widish => mytilus}/04.prog.svg | 16 +- tests/{widish => mytilus}/04.yaml | 0 tests/{widish => mytilus}/05.in | 0 tests/{widish => mytilus}/05.mprog.svg | 0 tests/{widish => mytilus}/05.out | 0 tests/{widish => mytilus}/05.prog.svg | 0 tests/{widish => mytilus}/05.yaml | 0 tests/{widish => mytilus}/06.in | 0 tests/{widish => mytilus}/06.mprog.svg | 0 tests/{widish => mytilus}/06.out | 0 tests/{widish => mytilus}/06.prog.svg | 0 tests/{widish => mytilus}/06.yaml | 0 tests/{widish => mytilus}/07.in | 0 tests/{widish => mytilus}/07.mprog.svg | 0 tests/{widish => mytilus}/07.out | 0 tests/{widish => mytilus}/07.prog.svg | 0 tests/{widish => mytilus}/07.yaml | 0 tests/{widish => mytilus}/08.in | 0 tests/{widish => mytilus}/08.mprog.svg | 0 tests/{widish => mytilus}/08.out | 0 tests/{widish => mytilus}/08.prog.svg | 0 tests/{widish => mytilus}/08.yaml | 0 tests/{widish => mytilus}/09.in | 0 tests/{widish => mytilus}/09.mprog.svg | 0 tests/{widish => mytilus}/09.out | 0 tests/{widish => mytilus}/09.prog.svg | 0 tests/{widish => mytilus}/09.yaml | 0 tests/{widish => mytilus}/10.in | 0 tests/{widish => mytilus}/10.mprog.svg | 0 tests/{widish => mytilus}/10.out | 0 tests/{widish => mytilus}/10.prog.svg | 0 tests/{widish => mytilus}/10.yaml | 0 tests/{widish => mytilus}/11.in | 0 tests/{widish => mytilus}/11.mprog.svg | 0 tests/{widish => mytilus}/11.out | 0 tests/{widish => mytilus}/11.prog.svg | 0 tests/{widish => mytilus}/11.yaml | 0 tests/{widish => mytilus}/14.in | 0 tests/{widish => mytilus}/14.mprog.svg | 0 tests/{widish => mytilus}/14.out | 0 tests/{widish => mytilus}/14.prog.svg | 0 tests/{widish => mytilus}/14.yaml | 0 tests/{widish => mytilus}/15.in | 0 tests/mytilus/15.mprog.svg | 824 + tests/{widish => mytilus}/15.out | 0 tests/mytilus/15.prog.svg | 620 + tests/{widish => mytilus}/15.yaml | 0 tests/{widish => mytilus}/16.in | 0 tests/{widish => mytilus}/16.mprog.svg | 394 +- tests/{widish => mytilus}/16.out | 0 tests/mytilus/16.prog.svg | 684 + tests/{widish => mytilus}/16.yaml | 0 tests/{widish => mytilus}/17.in | 0 tests/{widish => mytilus}/17.mprog.svg | 9324 +++++--- tests/{widish => mytilus}/17.out | 0 tests/mytilus/17.prog.svg | 6915 ++++++ tests/{widish => mytilus}/17.yaml | 0 ...st_eq_2_5_compile_partial_is_eval_left.svg | 337 +- ...t_eq_2_5_compile_partial_is_eval_right.svg | 384 +- ...t_eq_2_6_compile_data_is_identity_left.svg | 8 +- ..._eq_2_6_compile_data_is_identity_right.svg | 268 +- ...2_7_compile_parallel_to_left_side_left.svg | 374 +- ..._7_compile_parallel_to_left_side_right.svg | 580 +- ...7_compile_sequential_to_left_side_left.svg | 381 +- ..._compile_sequential_to_left_side_right.svg | 458 +- tests/svg/test_fig_6_3_eq_0_comp.svg | 16 +- tests/svg/test_fig_6_3_eq_0_mprog.svg | 18 +- tests/svg/test_fig_6_3_eq_0_prog.svg | 16 +- tests/svg/test_fig_6_3_eq_1_comp.svg | 24 +- tests/svg/test_fig_6_3_eq_1_mprog.svg | 22 +- tests/svg/test_fig_6_3_eq_1_prog.svg | 24 +- tests/svg/test_sec_6_2_2_comp.svg | 14 +- tests/svg/test_sec_6_2_2_mprog.svg | 18 +- tests/svg/test_sec_6_2_2_prog.svg | 14 +- ...aprograms_with_partial_evaluators_comp.svg | 14 +- ...programs_with_partial_evaluators_mprog.svg | 18 +- ...aprograms_with_partial_evaluators_prog.svg | 14 +- tests/test_bin_mytilus.py | 155 + tests/test_bin_widish.py | 40 - tests/test_compiler.py | 6 +- tests/test_hif.py | 6 +- tests/test_interpreter.py | 4 +- tests/test_lang.py | 22 +- tests/test_loader.py | 24 +- tests/test_metaprog.py | 10 +- tests/test_runner.py | 14 +- tests/test_state.py | 18 +- tests/test_watch.py | 71 +- tests/test_wire.py | 20 +- tests/widish/0.prog.svg | 1776 -- tests/widish/04.mprog.svg | 474 - tests/widish/15.prog.svg | 1268 -- tests/widish/16.prog.svg | 1322 -- tests/widish/17.prog.svg | 17813 ---------------- tutorial.ipynb | 2 +- widip/__main__.py | 70 - widip/watch.py | 177 - widish-command.jpg | Bin 0 -> 5759 bytes 172 files changed, 19956 insertions(+), 28021 deletions(-) rename bin/{widish => mytilus} (53%) create mode 100644 bin/yaml/shell.jpg create mode 100644 examples/hello-world-map.yaml.jpg create mode 100644 mytilus-command.jpg rename {widip => mytilus}/SKILL.md (57%) rename {widip => mytilus}/__init__.py (100%) create mode 100644 mytilus/__main__.py rename {widip => mytilus}/comput/__init__.py (78%) rename {widip => mytilus}/comput/boxes.py (100%) rename {widip => mytilus}/comput/compile.py (100%) rename {widip => mytilus}/comput/computer.py (100%) rename {widip => mytilus}/comput/equations.py (100%) rename {widip => mytilus}/comput/loader.py (100%) rename widip/comput/widish.py => mytilus/comput/mytilus.py (97%) rename {widip => mytilus}/comput/python.py (100%) rename {widip => mytilus}/files.py (75%) create mode 100644 mytilus/interactive.py rename {widip => mytilus}/metaprog/__init__.py (66%) rename {widip => mytilus}/metaprog/core.py (100%) rename {widip => mytilus}/metaprog/hif.py (100%) rename widip/metaprog/widish.py => mytilus/metaprog/mytilus.py (97%) rename {widip => mytilus}/metaprog/python.py (100%) rename {widip => mytilus}/pcc/__init__.py (78%) rename {widip => mytilus}/pcc/core.py (100%) rename {widip => mytilus}/pcc/loader.py (100%) rename widip/pcc/widish.py => mytilus/pcc/mytilus.py (83%) rename {widip => mytilus}/state/__init__.py (87%) rename {widip => mytilus}/state/core.py (100%) rename {widip => mytilus}/state/hif.py (100%) rename {widip => mytilus}/state/loader.py (96%) rename widip/state/widish.py => mytilus/state/mytilus.py (61%) rename {widip => mytilus}/state/python.py (95%) create mode 100644 mytilus/watch.py rename {widip => mytilus}/wire/__init__.py (100%) rename {widip => mytilus}/wire/functions.py (100%) rename {widip => mytilus}/wire/hif.py (100%) rename {widip => mytilus}/wire/loader.py (100%) rename widip/wire/widish.py => mytilus/wire/mytilus.py (100%) rename {widip => mytilus}/wire/services.py (100%) rename {widip => mytilus}/wire/types.py (100%) rename tests/{widish => mytilus}/0.in (100%) rename tests/{widish => mytilus}/0.mprog.svg (55%) rename tests/{widish => mytilus}/0.out (81%) create mode 100644 tests/mytilus/0.prog.svg rename tests/{widish => mytilus}/0.yaml (100%) rename tests/{widish => mytilus}/01.in (100%) rename tests/{widish => mytilus}/01.mprog.svg (100%) rename tests/{widish => mytilus}/01.out (100%) rename tests/{widish => mytilus}/01.prog.svg (100%) rename tests/{widish => mytilus}/01.yaml (100%) rename tests/{widish => mytilus}/02.in (100%) rename tests/{widish => mytilus}/02.mprog.svg (100%) rename tests/{widish => mytilus}/02.out (100%) rename tests/{widish => mytilus}/02.prog.svg (100%) rename tests/{widish => mytilus}/02.yaml (100%) rename tests/{widish => mytilus}/03.in (100%) rename tests/{widish => mytilus}/03.mprog.svg (100%) rename tests/{widish => mytilus}/03.out (100%) rename tests/{widish => mytilus}/03.prog.svg (100%) rename tests/{widish => mytilus}/03.yaml (100%) rename tests/{widish => mytilus}/04.in (100%) rename tests/{widish/15.mprog.svg => mytilus/04.mprog.svg} (69%) rename tests/{widish => mytilus}/04.out (100%) rename tests/{widish => mytilus}/04.prog.svg (95%) rename tests/{widish => mytilus}/04.yaml (100%) rename tests/{widish => mytilus}/05.in (100%) rename tests/{widish => mytilus}/05.mprog.svg (100%) rename tests/{widish => mytilus}/05.out (100%) rename tests/{widish => mytilus}/05.prog.svg (100%) rename tests/{widish => mytilus}/05.yaml (100%) rename tests/{widish => mytilus}/06.in (100%) rename tests/{widish => mytilus}/06.mprog.svg (100%) rename tests/{widish => mytilus}/06.out (100%) rename tests/{widish => mytilus}/06.prog.svg (100%) rename tests/{widish => mytilus}/06.yaml (100%) rename tests/{widish => mytilus}/07.in (100%) rename tests/{widish => mytilus}/07.mprog.svg (100%) rename tests/{widish => mytilus}/07.out (100%) rename tests/{widish => mytilus}/07.prog.svg (100%) rename tests/{widish => mytilus}/07.yaml (100%) rename tests/{widish => mytilus}/08.in (100%) rename tests/{widish => mytilus}/08.mprog.svg (100%) rename tests/{widish => mytilus}/08.out (100%) rename tests/{widish => mytilus}/08.prog.svg (100%) rename tests/{widish => mytilus}/08.yaml (100%) rename tests/{widish => mytilus}/09.in (100%) rename tests/{widish => mytilus}/09.mprog.svg (100%) rename tests/{widish => mytilus}/09.out (100%) rename tests/{widish => mytilus}/09.prog.svg (100%) rename tests/{widish => mytilus}/09.yaml (100%) rename tests/{widish => mytilus}/10.in (100%) rename tests/{widish => mytilus}/10.mprog.svg (100%) rename tests/{widish => mytilus}/10.out (100%) rename tests/{widish => mytilus}/10.prog.svg (100%) rename tests/{widish => mytilus}/10.yaml (100%) rename tests/{widish => mytilus}/11.in (100%) rename tests/{widish => mytilus}/11.mprog.svg (100%) rename tests/{widish => mytilus}/11.out (100%) rename tests/{widish => mytilus}/11.prog.svg (100%) rename tests/{widish => mytilus}/11.yaml (100%) rename tests/{widish => mytilus}/14.in (100%) rename tests/{widish => mytilus}/14.mprog.svg (100%) rename tests/{widish => mytilus}/14.out (100%) rename tests/{widish => mytilus}/14.prog.svg (100%) rename tests/{widish => mytilus}/14.yaml (100%) rename tests/{widish => mytilus}/15.in (100%) create mode 100644 tests/mytilus/15.mprog.svg rename tests/{widish => mytilus}/15.out (100%) create mode 100644 tests/mytilus/15.prog.svg rename tests/{widish => mytilus}/15.yaml (100%) rename tests/{widish => mytilus}/16.in (100%) rename tests/{widish => mytilus}/16.mprog.svg (62%) rename tests/{widish => mytilus}/16.out (100%) create mode 100644 tests/mytilus/16.prog.svg rename tests/{widish => mytilus}/16.yaml (100%) rename tests/{widish => mytilus}/17.in (100%) rename tests/{widish => mytilus}/17.mprog.svg (50%) rename tests/{widish => mytilus}/17.out (100%) create mode 100644 tests/mytilus/17.prog.svg rename tests/{widish => mytilus}/17.yaml (100%) create mode 100644 tests/test_bin_mytilus.py delete mode 100644 tests/test_bin_widish.py delete mode 100644 tests/widish/0.prog.svg delete mode 100644 tests/widish/04.mprog.svg delete mode 100644 tests/widish/15.prog.svg delete mode 100644 tests/widish/16.prog.svg delete mode 100644 tests/widish/17.prog.svg delete mode 100644 widip/__main__.py delete mode 100644 widip/watch.py create mode 100644 widish-command.jpg diff --git a/README.md b/README.md index 5a65899..03690cd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -Widip +Mytilus ----- > _Types? Where we're going, we don't need types!_ -Widip is an [interactive environment] for computing in modern systems. Many long-standing systems have thrived thanks to a uniform metaphor, which in our case is wiring diagrams. +Mytilus is an [interactive environment] for computing in modern systems. Many long-standing systems have thrived thanks to a uniform metaphor, which in our case is wiring diagrams. System |Metaphor ---------|-------------- -Widip |Wiring Diagram +Mytilus |Wiring Diagram UNIX |File Lisp |List Smalltalk|Object @@ -17,11 +17,11 @@ Smalltalk|Object # Installation -`widip` can be installed via [pip](https://pypi.org/project/widip/) and run from the command line as follows: +`mytilus` can be installed via [pip](https://pypi.org/project/mytilus/) and run from the command line as follows: ```bash -pip install widip -python -m widip +pip install mytilus +python -m mytilus ``` This will automatically install dependencies: [discopy](https://pypi.org/project/discopy/) (computing, drawing), [pyyaml](https://pypi.org/project/pyyaml/) (parser library), and [watchdog](https://pypi.org/project/watchdog/) (filesystem watcher). @@ -30,8 +30,8 @@ This will automatically install dependencies: [discopy](https://pypi.org/project If you're working with a local copy of this repository, run `pip install -e .`. -# Using `widip` -The `widip` program starts a [chatbot] or [command-line interface]. It integrates with the [filesystem] for rendering diagram files. We give more information for a few use cases below. +# Using `mytilus` +The `mytilus` program starts a [chatbot] or [command-line interface]. It integrates with the [filesystem] for rendering diagram files. We give more information for a few use cases below. ## For documentation Widis are meant for humans before computers and we find it valuable to give immediate visual feedback. Changes in a `.yaml` file trigger rendering a `.jpg` file next to it. This guides the user exploration while they can bring their own tools. As an example, VS Code will automatically reload markdown previews when `.jpg` files change. @@ -39,7 +39,7 @@ Widis are meant for humans before computers and we find it valuable to give imme Widis are great for communication and this is a very convenient workflow for git- and text-based documentation. ## For UNIX programming -The lightweight `widish` [UNIX shell] works everywhere from developer workstations to cloud environments to production servers. Processes that read and write YAML document streams are first-class citizens. With this practical approach users can write programs in the same language of widis. +The lightweight `mytilus` [UNIX shell] works everywhere from developer workstations to cloud environments to production servers. Processes that read and write YAML document streams are first-class citizens. With this practical approach users can write programs in the same language of widis. ## For graphical programming Programming is hard, but it shouldn't be _that_ hard. diff --git a/bin/widish b/bin/mytilus similarity index 53% rename from bin/widish rename to bin/mytilus index 6b2b247..14d839a 100755 --- a/bin/widish +++ b/bin/mytilus @@ -14,17 +14,4 @@ fi export PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}" -if [ "${1-}" = "-c" ]; then - shift - if [ $# -lt 1 ]; then - echo "widish: missing argument for -c" >&2 - exit 2 - fi - command_text=$1 - # Consume optional shell-style positional parameters (arg0, args...). - shift - printf '%s\n' "$command_text" | "$PYTHON_BIN" -m widip --no-draw /dev/stdin - exit $? -fi - -exec "$PYTHON_BIN" -m widip "$@" +exec "$PYTHON_BIN" -m mytilus "$@" diff --git a/bin/yaml/python.yaml b/bin/yaml/python.yaml index 6dd6fdf..96dd3cb 100755 --- a/bin/yaml/python.yaml +++ b/bin/yaml/python.yaml @@ -1,2 +1,2 @@ -#!bin/widish +#!bin/mytilus !python diff --git a/bin/yaml/shell.jpg b/bin/yaml/shell.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78d6588fbbc76fd842d2e3c29d15c18e963ac266 GIT binary patch literal 5503 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 J1uh=y{V&-yf7Sp1 literal 0 HcmV?d00001 diff --git a/debug.py b/debug.py index d855e96..9a2d052 100644 --- a/debug.py +++ b/debug.py @@ -1,13 +1,13 @@ from pathlib import Path -from widip.computer import Box, ComputableFunction, ProgramTy, Ty -from widip.metaprog import ( +from mytilus.computer import Box, ComputableFunction, ProgramTy, Ty +from mytilus.metaprog import ( MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, ) -from widip.state import Process, ProgramClosedCategory, fixed_state, simulate +from mytilus.state import Process, ProgramClosedCategory, fixed_state, simulate def large_diagram(): diff --git a/examples/README.md b/examples/README.md index ac92f6a..ae2b579 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,7 +3,7 @@ ## Hello world! ``` -$ python -m widip examples/hello-world.yaml +$ python -m mytilus examples/hello-world.yaml Hello world! ``` @@ -12,7 +12,7 @@ Hello world! ## Script ``` -$ python -m widip examples/shell.yaml +$ python -m mytilus examples/shell.yaml 73 23 ? !grep grep: !wc -c @@ -23,7 +23,7 @@ $ python -m widip examples/shell.yaml # Working with the CLI -Open terminal and run `widip` to start an interactive session. The program `bin/yaml/shell.yaml` prompts for one command per line, so when we hit `↵ Enter` it is evaluated. When hitting `⌁ Ctrl+D` the environment exits. +Open terminal and run `mytilus` to start an interactive session. The program `bin/yaml/shell.yaml` prompts for one command per line, so when we hit `↵ Enter` it is evaluated. When hitting `⌁ Ctrl+D` the environment exits. ```yaml --- !bin/yaml/shell.yaml @@ -31,6 +31,13 @@ Open terminal and run `widip` to start an interactive session. The program `bin/ Hello world! ``` +Escaped ASCII newlines can encode multiline commands in one REPL entry: + +```yaml +!echo\n? foo\n? bar\n +foo bar +``` + # Other examples ## React diff --git a/examples/aoc2025/1-1.jpg b/examples/aoc2025/1-1.jpg index 5a9bf95d84456961ab969518476875c914abf3dc..c5fab336cd1a79fc338d5340d8ef5728710431e3 100644 GIT binary patch literal 34771 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 literal 96006 zcmeFa1zc2X*FL;)CH0a=jdU^IPrg;H@;W>?cd()Ju`dVz1Fp^weG{-!x7-jEm3h%00{{R&<6hlhy8#s zfR2iahKhoYhK6?XB>E{#ywjK%7?^}OxY&54MC4?oL?k2>)XcOLl#Em)By?9VFu#N)-x&_z^gZj=m3>fgn)=C(a^4 z&LSOF0^|UIgaS%?B=GP5kWN65QBcv)PoBa6KahO}IDrI#oIr-4pdcfIpLPVF1ITAl za4xW1L&cTRM58pvV|^0v2A%49dKtcK+X^+?0}Gdvr_P-xAS9x>NK1E#o}Gj9Di=48 z@C^}Bv76$z?#jt4C@LwdXld){>ggL8T0XS0wz0LdcYXTI&E4aRlVc{{c zZ{y-u(wI6S$Z_kG#i`1=Y#Z;u5oc^P?>Il-bBDm~PSe18E~*b!IiW3if~%W(3^zEb zaFBG6oO!(f6u_!ekr>UFnK^Y->7n1obeCY}y^}h&!2}<5u|vHD2J_~e*UVpIw#082 zEG;cnF=xHVb(`CA6l_#|eaGw4qE zx^Q0S!yVa?J2J5zoTRq^>_+sxk0aBh3<~4bfiLS$q8(+N6to>cGz@{+M4>D6egGdcCz_Z$McX&FWRRgo0o zThrHr7R1S3oqryat?J6n${vuJ{CL^q#F7HA)B^E^CU#hnid7P^4(%Ak^{ESG_N&!! z1?q@aoTzPEkX0OXZUP?SDPo{zCriDnAW?amiULc^2~i2EAP8REFJ7cjFT_%&mN(9$ zNGw~{o9rjK-?>To*hGjb!Y= zewJoRQ<#|*Nn%*~z!Od9#f@emEP|CYubFL;EzHA9C{`6R92ApOX8R_aQd1c8L&_M( zZ6%uTqD<=7Uh|or(yUX{djo75GQ3(@DkWHP3w2C0G^6gp_|Rf8d;aB);I-FS&UY8} zO1)|nty$-Aq;$6!EJ>J%kZ=i4jhuPo2gzqgYHd>+_SnDKce=4N*yWc@BXvD6FEp zbdLkDdLbhM;K|4@n=WkhTbeuCprioK+L1z@Cudk~qcQLl*=)v%p-yL1lo+pOT%%^= zEwVSR4AWtGMkkN1bD^JF`{9IMvsPd-wffaKCm}>WPW53H0}J|(G>nFj29K8E-IAo3{y-Z)h#eYz=nI`bJ`b0s^4to zbeR}4Q@QZUJUmNjBjPw-Kj`CqYeno9uwl>Ozo(mMp46{DwF|2hyWf+G%O@>vbzz!O3nF{`3+=0>{;QF`Va+0}1s1MFte z?|ZPeth2tr6#9r3)JtM^)MqwIs6xnbeJCZAG(BF(<>g1j8q}%5X6$q{NLh+VTx)K3 zl5sQ##Sc5_9Rf9r&#Izc{@76 z#JTbOGmuXGG&ZF!EAhS>xeUiUq-Q>fb5zV80@}fCxtC;ZmN8E(9|DSns|i+7XAH?# zc{;NWglY`ls+5_NU=XnmF3B$DWrP4}c*YE_K`pm{CH*vO<6BcTHPT8Fmh3s9TYl8i z9>VC_qC$4^14+UbY5|c*B&V8IOMNKY8AB>=7PzSE&Q0-hby45l($)I>w)O%S8u629 zlC$B!B3;_O@k+%$$w{|}M^NB0=40c@t`2IWNzY(KCF{%;EObBKP)OQEuPOn*@O0ew%yTz)V6Ko78GZYU1gub*)TY*}QhlOu2K&02MvK%^Ob4 zHkkK0A1$W#^FPKYJyrI8=eECuey%9zL)qxGg>w(^wBp4fL(UutX;87%oTyYoncn`Y z95H0qt$uMH%!A%3-Th}O#hqz)E4RAOEUm?6gs*9fpu|xm#b6#NZB1M@YDFEyukGFL zmUyM46JFoPtzD3t3)L>LWE3!{)>ibn6)@|9h0Q0Ixt%k;cSA{e*1U;#PFCqs;B)8kQ31|ebe4H53#_Y@y zV0Kxr4CT?!El~=gBf4nJI*B!H05N5JDZitLJYqF5?u-@UIlfVP*6^-v^Zf13Aq=jy z24nrLG$Lst^^{DLzN!F7Fyu)KVt@B3bVqa_om zH16`qQizlWzgZu~;3e11fk95WdV?*+EYqOptWP|(WpS9!`^1J%vaMlu2ZJQI7ZXc{7B=k2GmV?~$>=QNDN$Er`pMk-0$;fS+V zR-yoS2P5~fr0Om1*k)R#%!P}j75F*Pd}^o8&BK)-(VTqt;T~t-8%yVH3asQFGz!ve z{3=7DTn~01l`V-de4`0vh2DnAol8D(YQ{TTF(*|d_svdhCTFEPb+Si17~@U} zJEs7Bs2GqDo#HXOHqAYP6-$73G$luQT;AmDde6jY1$mIeqt~CycoSwBVwTkZWCtz8 zG8b)Xj)Bz)XD*ksXSedaq%qd(9G|VTMEFsSHv&H66lOl>Ol!ZQNXt(Lk@{G=&Uy&+ z+Rq;XI%ygC3}7Tla#Rm#%8@Ld&VbGZpvy z2t+H-Naf)neUeer+Ai58hD!7ob89MZ6sBHd8ZW>)Rh@nX>-K%Oo!Xr{XnV>8?((a? z?@>c-CTBysd+JP$tDUc zY}F=(xkEra@?r<`SgzX`xACO1U+5*XcT=}fQ79a*XG0jH_b60m81_a{l$h5kc zsiYHmlbn-A008nth|EvK!f7RK!*plAM{vc`3Y=%@4uNMju}&>h`Gpt7cL21~8WQxcXcfgk6XNG(~1+K^9n~6@N>NCUZJ84cMQ(y!8N~0-%^Xv5Q z+>Ath#urU*8`rC9Eqt~Zs#?!&V1M;F_nzfc zUD=4qh#5<)2w>4C=$z%qRiTn%o=P4S!VF%q7j$^v76hu%1!&W~ zX+Tk+i+%>7c<{DlCEh3T9)DL?a#0|k1()h`eA01>SXKrU#w%BDww%b(kH*5NwkL~% zCTu_6C5X5fs*z(O!?vU2yO0|i@x+(=K3h+u)A(8Q5nN?vjA?fp9+}%+@n*Kn z#uGU6h#8e@wn}e%9;Wkt4;$2a7P`8nQKj~&!VNl9%DU&2Fj8Ebioym7&9h=NL#|(8 z$?|kFMc2>lSN_oie&tj+x%|o3 z(p!sfGgXBS!n|BjpuTSsYQ|T#croL_dabydET&h)r)k6WPAwPuoZ?X^lSSVs@#nj0 z7}_CsR3Dl(O+Bp^BBZGn6TIqxq?#p-dUc<&K=2b;Zy5_VSDt}nXS^02!Re``VSiMC z&v$raMMfowhIk*k^eL6Jum@GQ4b-QRHcvO8)?wOF1Y*Y-%Lt+1{osI8C#{0(j@5i8 zQn!o<9snLlkWXba3s?QfOfHD|UacEdOVspzy`WM5F2)m}`wPd!J~&KSdk)-Fowf|; z8Q_)3=o6lF(WiLg{d7{qPTZq@SNlQ|SD4Q~L!evDO%yHm%KT8`)u&myxh`i>%I(;) z%OyxB>Mp;mmw#DH!i#f~B&f<5Y#cUQQ^`EHPc*7ATm4Vjyy~9L5lp&=liQ(ryY!CO zpHeEi`nEiPUL|Vqq;=-4g_@>AKuMgIQ8k~NBUuE{x$L&_w4~#*LL){5D=y00SN$y2 ziU#*euaXZnb8o(bhTom*{y=>d!<4_c(T3&b>W)`FmRUo9zo;0pLIfUC7^kbIWkTFR zc?WoZ%JJYL)8pdmqjd3zGdvl(u=-4q&jvCe| z94m~z{=IPkQ&g|UXP-ur{bI)!e#d!?k7wVq*m}}cpXF!>mWgyWuCm&!+ddD|)!4&U zcUs6o%1xZH=E)~FKF|2-7Q6Xi0})T;n_vP|a%n?`($fa#d&~z~3_^WmINzpu%(+{j ziCj>-MD6@>oKGPe4=E)4cGOCcc#4EYN=*Mk>-BWw-Z)nl-MRsB2mu7rlPWUigw?kl zCE_SBvSUObj#-a2qfJV1-*t|Q%0<*JHJlEDx2;<}HPm&vsj8xah{`X!nbJS8SsC-` zRlfZ_zXy`Mu(QVGdy*XFnSy;dBLnYtZ@fAu8z=E}FbjuZ;z_ezswW+Ur{j8u(c_A( zN5)b2pO5D3^e%|@=ULb_cg;rz>z0@IOLQzQ7>%!`rjDPjeC1^<^O2C7b~PEak>r@O z64J7Hp=%3)B6uIJ-1S*4IVo%JD}R$J9y?V7oJ*ZXaoZYkD_5j0MKxSz?6DNTywsav z9PqMW@xje90_w<3nkcu2UMDF7hq|5y4Dh!XHZBc8X+s(%Sn3#RxHspUpfW;)t@tCA&+d`7v~=eEV2sZ)65rolqv?U{=X9P!F?ii)dD z9ct>BQd2`s^jsTs&5FLRzGx@1XQDBorl<6CazZ&1b?*_se2T-^0T#X5I-0RE$@?{u zGg}qM{^fUvfS$P4`EdUhv!H>jz@iMPti%f5z{)e8MNtJTst=`6cvI=?x5V$w4eGM? zXJ@)oyW%=d)?ABZ^UWi3$z0;2iXE6Wj&Hzk%dzA81e8SF6Wz4I&fmo@lDam-VBrbW z6s?p5C5Rx~n6=QkQK9P=gj!aczpx$*!yD1&rs!BI;*Ptg5x21Qwy{J5gCR+v;>D2j zGskxUc_h|HlcnDZvc&m)8e$8HZI{L80`H7f+xE}K&00h8-Ymsou%0&{p~Jz^Ff^fm z>guA14C8=b=>c7-(F5iw0?m*f3tySFT65F611wf#Fi)Li{;J)!8kZKvVlDa|VNV4#` z3DdH;UiDR=+^rE)i$h>u+&J1_%|+B)-4PF|ayAO{A(Xg3aVlhPiQDK9$c*(=2IO*j zado9HHDAS?=AtU)=xB+31L~lo@)#36U7;DCS(pH z^Lk^jD|e?|OvjkCpwQ~R=Xu(JvG>1O`bXCOw+DXRKJOtAU@;FX&piZo@6v!XIl>(f zD!5A%!gawe-pZG*GqaEc1rjKpW_oiqyCj!SP`fSrefk}OiUe_koRa{ui-7URGh8%6 z++ocD%AXfuv=+` zhrs>h%Ljz~UjU6iiujWS3RDfgi5MLD|nSCEuHUi`g!K7Q{X2nJ{SA3-@NAe0jd ztLV|#WBRt_$6xlnYq+l;E!|(2hwQ&mF=|m2r90=DOBWcN4uLpUMG&BXyCIF*;<53G zBMQ^7K#diBvM4iTkYq!4Mh>$moQeCL%Yg+Cbmj|^IyHh2sOG%JL3ly^U($iU=*bA$FC2s#jg@iirC(nDv-3X? z46e%{w)sDi0KU)UBlvdyR^=R3(oZ@#H08Gz!JOz}#I)&<;3+fKxf;Y&xsXUUg2eT} z{zU&B^IEEGDqHs(Hy0-ywWQ=gKtQ^B5(s_m@J{e5pSjwut&{W~uL#Z!-48u9o%hcp zd!#8`Uv~M3b4B+)78cPJb5E}-GLltn4`W>q{uFU@Et*2rv^RX_6%6Ng>TaOkzXpK< zsMu)iNNxSL?zalNmo{wQYw|oxuGmv0{YGH;g5NvL`FtjT{Q}85hT)GADH zDK=Lox)YoL`ZFdg4kA@m^WI#h4KKeDaOMKB_2`po%RF*J-Qv+i2N5pMrKuqX>N3BE zY=u09r5k?!Q7iZ@#elJdliukGg}}jUq>Ue6zOe&rAc-=aF#HGk2ND$-L$f)3x70p9 z+#Wwkb+2qIKZ@i8(RD?E1>#x74P!D=YzYZ;X6E=C)XVuV3fFg$W&pq%4~V_j)-Vv= zl_H_yVsd`*(n}#Onw=fH&7|DN%&m;6MoV#BDX=CR!2rB@9)gDZK_SgoSXxGvISbBv zFoM=7N?~t#_lFwH^8so+MPeotiaH&^V*)cG{aYBaKZAoEG@O z$vdM5-anzP^IdiGAE-XRbh5vpll_yHQHrEmCE9jmlz+IFyyrFVmsy6A^Rcm?u!XrE zC@0%M$~^)$L!p8jl=D%aw?IF!Z`&aNq9KRCb!KpybTW3@UIfUF`4+8$r^%?g_^zdZ z-lQF}lij!rDle}3m8hDA-Q|YhY)<7I0;9~`PBS7sg3F8V4*}AnwPOPDW>OD~ue5!E z`SNnn`7~juDPf=iW=PG0CxK=(pucHRKfZeGX1B;*3KF>w&tFqFeraG|mC^4gt$&GH z@wY;&YenzinFa{-#~(E)1D4=<{k6H4_#2-C)oKJk z4a7&8wWUOq`ClySw0td%I*BJQDJGNKo)W$^ap!?7(i+o5SU%TYLY&{E(7E}rZLdRs zdbl!b3ky`rgg6pop?!}_k=G5@ZZ(*)xX6bO-Bczw%sEw?WX~EIF@JgN@_vvLj#Dp> z(*e+mb`5l*l1V%DaKPj_gpXZ+cTPgIknoQ~nO)tAwBzr0LP{6-p`c}c1oQvE+x~<5 zXve>PR8ep~`R@t@3UJIIQ#?M)f^Og+yyr*u>_3>K5i8{Q-+$e7MCcNr!7g~-skUaM zW$#oeEJ#v_R7cN%=azx!eo88B-_p>`gqDGk`L2Qw9#Vb7IB{>hZH>#mIE4Z|DOGNT zR$k*E15~tgvMZ%;%$Rvk^8O^>t1h95`=k|GS8BNCd%K_EAw|R+T#8pW%uI=Eeq*y0 zkF2G+7V8J;IZzVx(Qyzt1OiRRr1sFH4uNQ**<^(N1=2c{L95{f`DT+-u%z^-Kdyfa zfFB3Of3mLq0frv|;(z5p`bh^=RQ|p|KW>+-uVDONdG~Li{g3-!&ktUIye<%~NsiMN z5TFaI7jQBu(vB_f5bP6z_L1Tt&~5
    Ov9x5Dowj}p)Cy-ZDg(4|zzo`R3dxsDWQ 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 diff --git a/examples/aoc2025/README.md b/examples/aoc2025/README.md index 5b2c9cd..405b962 100644 --- a/examples/aoc2025/README.md +++ b/examples/aoc2025/README.md @@ -3,7 +3,7 @@ ## Day 1-1 ``` -$ python -m widip examples/aoc2025/1-1.yaml +$ python -m mytilus examples/aoc2025/1-1.yaml 1147 ``` @@ -12,7 +12,7 @@ $ python -m widip examples/aoc2025/1-1.yaml ## Day 1-2 ``` -$ python -m widip examples/aoc2025/1-2.yaml +$ python -m mytilus examples/aoc2025/1-2.yaml 6789 ``` @@ -21,7 +21,7 @@ $ python -m widip examples/aoc2025/1-2.yaml ## Day 2-1 ``` -$ python -m widip examples/aoc2025/2-1.yaml +$ python -m mytilus examples/aoc2025/2-1.yaml 13108371860 ``` @@ -30,7 +30,7 @@ $ python -m widip examples/aoc2025/2-1.yaml ## Day 2-2 ``` -$ python -m widip examples/aoc2025/2-2.yaml +$ python -m mytilus examples/aoc2025/2-2.yaml 22471660255 ``` @@ -39,7 +39,7 @@ $ python -m widip examples/aoc2025/2-2.yaml ## Day 3-1 ``` -$ python -m widip examples/aoc2025/3-1.yaml +$ python -m mytilus examples/aoc2025/3-1.yaml 17324 ``` diff --git a/examples/hello-world-map.jpg b/examples/hello-world-map.jpg index 7cbe074f845dbfa4f1e5f62d59128871ceca13db..7ba1b1634b0b345ac8aafb790713303cf293c80c 100644 GIT binary patch literal 32824 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 literal 78090 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 literal 0 HcmV?d00001 diff --git a/examples/shell.jpg b/examples/shell.jpg index 22b05684616f782fe44d069328e593e3bb7e32c0..89c471fd51576268cd3cf6c613b28be7071fecf5 100644 GIT binary patch delta 4338 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 UYlZC`M|UCh&o_O<8R;MX1ptn5>i_@% literal 65235 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 P??3cZzkfc%Qv3e_boZmD diff --git a/examples/shell.yaml b/examples/shell.yaml index 24b3df6..903737e 100644 --- a/examples/shell.yaml +++ b/examples/shell.yaml @@ -1,3 +1,8 @@ +# cat examples/shell.yaml | wc -c +# cat examples/shell.yaml | grep grep | wc -c +# cat examples/shell.yaml | grep grep | cat +# cat examples/shell.yaml | tail -2 + !cat examples/shell.yaml: ? !wc -c ? !grep grep: !wc -c diff --git a/mytilus-command.jpg b/mytilus-command.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed08f9b9f6ff6af10cba6bcad0a90987e4f85041 GIT binary patch literal 5871 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 literal 0 HcmV?d00001 diff --git a/widip/SKILL.md b/mytilus/SKILL.md similarity index 57% rename from widip/SKILL.md rename to mytilus/SKILL.md index 7c7f4ce..c5e9e0d 100644 --- a/widip/SKILL.md +++ b/mytilus/SKILL.md @@ -1,13 +1,13 @@ --- -name: widip -description: Use when working with widip or widish as a YAML-native shell and REPL for humans and LLM agents. Use for authoring, executing, explaining, or reviewing YAML command documents as a preferred structured command language for general-purpose work, including pipelines, grouped parallel work, command substitution, and explicit shell fragments when needed. +name: mytilus +description: Use when working with mytilus or mytilus as a YAML-native shell and REPL for humans and LLM agents. Use for authoring, executing, explaining, or reviewing YAML command documents as a preferred structured command language for general-purpose work, including pipelines, grouped parallel work, command substitution, explicit shell fragments, and editor or terminal integration such as setting mytilus as the default shell in VS Code. --- -# Widip +# Mytilus ## Adopt The Model -Treat every syntactically valid YAML document as a valid widish command document. +Treat every syntactically valid YAML document as a valid mytilus command document. Let behavior come from: 1. The document structure. @@ -18,20 +18,42 @@ Think in terms of a YAML tree, not a shell parser: 2. Let tags define command behavior. 3. Let nesting define scope and substitution. -Choose widish as a primary structured command language for interactive and scripted work. +Choose mytilus as a primary structured command language for interactive and scripted work. -## Choose Widish By Preference +## Choose Mytilus By Preference -Choose widish as a normal working style because: +Choose mytilus as a normal working style because: 1. It keeps pipelines, grouping, and substitution explicit. 2. It keeps commands readable as data. 3. It gives agents and humans one representation they can both inspect and revise. 4. It supports interactive work, saved command documents, and generated command codebases. Keep the boundary in mind: -1. Widish provides structured command composition. +1. Mytilus provides structured command composition. 2. Platform command availability still depends on the underlying environment. +## State Scope And Tool Limits + +Avoid vague scope words such as `default`, `global`, or `forever` unless the boundary is explicit. + +When describing what mytilus changes: +1. Say `this session` when the behavior only applies to the current conversation. +2. Say `this repository` when the guidance comes from a repo-local `SKILL.md`. +3. Say `VS Code integrated terminal` when the change only affects VS Code terminal profiles. +4. Say `operating system login shell` when the change affects the user's actual shell outside VS Code. +5. Say `Codex platform default` only for true global behavior outside the repository and outside the current session. + +When working through Codex developer tools: +1. Explain that `exec_command` and PTY sessions are shell-based transports. +2. Prefer mytilus in user-facing examples, generated YAML command documents, and actual `bin/mytilus` or REPL demonstrations. +3. Do not imply that shell-backed tool logs mean the mytilus skill is inactive. +4. If a task needs shell-native tools such as `rg`, `sed`, `git`, or `pytest`, say so briefly instead of pretending the transport changed. + +When demonstrating the skill: +1. Prefer `bin/mytilus -c` or the REPL over paraphrase alone. +2. Show the YAML document and its output. +3. Treat any embedded `bash -c` fragment as an explicit subtool, not as mytilus syntax. + ## Build YAML As A Codebase Create YAML documents while working instead of treating them as a final export step. @@ -80,7 +102,7 @@ Use tagged mapping entries to build explicit command arguments: ## Use YAML As A Heredoc -Treat a YAML document as the widish equivalent of the multiline inline scripts that agents often send through shell heredocs. +Treat a YAML document as the mytilus equivalent of the multiline inline scripts that agents often send through shell heredocs. Agents commonly use heredocs to do this: 1. Open one inline multiline block. @@ -99,7 +121,7 @@ Prefer YAML-as-heredoc when: Use a plain shell heredoc only when the inline block truly needs to be shell script text. Follow the practical rule: -1. If the agent is about to write a multiline inline shell script, first ask whether the block is actually a widish command document. +1. If the agent is about to write a multiline inline shell script, first ask whether the block is actually a mytilus command document. 2. If yes, write YAML. 3. If no, keep the heredoc or embed an explicit shell fragment inside YAML. @@ -162,11 +184,44 @@ Use the interactive controls as follows: Preserve transcript output verbatim and in order when logging or replaying sessions. +## Configure VS Code + +When a user wants mytilus as the default shell in VS Code: +1. Explain that this changes VS Code's integrated terminal default profile, not the operating system login shell and not Codex's platform defaults. +2. Prefer `Preferences: Open User Settings (JSON)` for a global VS Code change. +3. Prefer workspace settings only when the user wants the behavior limited to one repository. +4. Define a profile under the platform-specific `terminal.integrated.profiles.*` key and set `terminal.integrated.defaultProfile.*` to that profile name. +5. Use an absolute path in global settings because repository-relative paths are fragile outside one workspace. +6. Remind the user that the change applies to newly created terminals. + +Use this Linux example: + +```json +{ + "terminal.integrated.profiles.linux": { + "mytilus": { + "path": "/absolute/path/to/repo/bin/mytilus" + } + }, + "terminal.integrated.defaultProfile.linux": "mytilus" +} +``` + +Translate the setting suffix by platform: +1. Use `.linux` on Linux. +2. Use `.osx` on macOS. +3. Use `.windows` only with a Windows-runnable entrypoint such as a `.cmd` wrapper or a WSL launcher, because `bin/mytilus` is a POSIX shell script. + +If the user prefers UI steps instead of JSON: +1. Tell them to open `Terminal: Select Default Profile`. +2. Tell them to choose the `mytilus` profile. +3. Tell them to open a new terminal. + ## Compose With Explicit Shells Prefer direct command structure over shell re-parsing whenever possible. -Use an explicit shell as one component of a widish program when the task genuinely depends on shell grammar, such as: +Use an explicit shell as one component of a mytilus program when the task genuinely depends on shell grammar, such as: 1. Shell builtins. 2. Redirection-heavy one-liners. 3. Loops or compound shell conditionals. @@ -181,18 +236,18 @@ Use the hybrid pattern: ``` Prefer this style when only one part of the task needs shell grammar: -1. Keep the widish document as the main program structure. +1. Keep the mytilus document as the main program structure. 2. Derive only the scripted fragment into `bash -c`, `sh -c`, or another explicit shell command. 3. Keep pipelines, grouping, and surrounding dataflow in YAML when they do not need shell parsing. -Treat the shell fragment as an explicit embedded tool, not as widish's native grammar. +Treat the shell fragment as an explicit embedded tool, not as mytilus's native grammar. ## Communicate Clearly -Use widish as both an execution format and a communication format: +Use mytilus as both an execution format and a communication format: 1. Keep documents readable. 2. Keep structure visible. 3. Prefer explicit grouping over clever punctuation. 4. Write examples that humans and agents can both follow quickly. -Use widish to make command intent inspectable, reviewable, and easier to transform. +Use mytilus to make command intent inspectable, reviewable, and easier to transform. diff --git a/widip/__init__.py b/mytilus/__init__.py similarity index 100% rename from widip/__init__.py rename to mytilus/__init__.py diff --git a/mytilus/__main__.py b/mytilus/__main__.py new file mode 100644 index 0000000..2d92c6f --- /dev/null +++ b/mytilus/__main__.py @@ -0,0 +1,98 @@ +import sys +import argparse +import logging +import os +import tempfile + + +DEFAULT_SHELL_SOURCE = "bin/yaml/shell.yaml" + + +def configure_matplotlib_cache(): + """Set a writable default MPLCONFIGDIR when the environment does not provide one.""" + cache_dir = os.path.join(tempfile.gettempdir(), "mytilus-mpl") + os.makedirs(cache_dir, exist_ok=True) + os.environ.setdefault("MPLCONFIGDIR", cache_dir) + + +configure_matplotlib_cache() + +# Stop starting a Matplotlib GUI. +import matplotlib +matplotlib.use('agg') + +from .watch import shell_main, mytilus_main, mytilus_source_main + + +def launch_shell(draw): + shell_main(DEFAULT_SHELL_SOURCE, draw) + + +def run_requested_mode(args, draw): + if args.command_text is not None: + logging.debug("running inline command text") + mytilus_source_main(args.command_text, draw) + elif args.file_name is None: + logging.debug("Starting shell") + launch_shell(draw) + else: + mytilus_main(args.file_name, draw) + + +def interactive_followup_requested(args): + return args.interactive and (args.command_text is not None or args.file_name is not None) + + +def build_arguments(args): + parser = argparse.ArgumentParser(prog="mytilus") + + parser.add_argument( + "-n", "--no-draw", + action="store_true", + help="Skips jpg drawing, just run the program" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose output" + ) + parser.add_argument( + "-c", "--command", + dest="command_text", + help="Inline YAML command document to run" + ) + parser.add_argument( + "-i", "--interactive", + action="store_true", + help="Enter the mytilus REPL after running a file or inline command" + ) + parser.add_argument( + "file_name", + nargs="?", + help="The yaml file to run, if not provided it will start a shell" + ) + args = parser.parse_args(args) + if args.command_text is not None and args.file_name is not None: + parser.error("cannot use -c/--command with a file name") + return args + + +def main(argv): + args = build_arguments(argv[1:]) + draw = not args.no_draw + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(levelname)s: %(message)s", + ) + + logging.debug(f"running \"{args.file_name}\" file with no-draw={args.no_draw}") + if interactive_followup_requested(args): + run_requested_mode(args, draw) + launch_shell(draw) + return + + run_requested_mode(args, draw) + +if __name__ == "__main__": + main(sys.argv) diff --git a/widip/comput/__init__.py b/mytilus/comput/__init__.py similarity index 78% rename from widip/comput/__init__.py rename to mytilus/comput/__init__.py index 76f5635..a8b73fc 100644 --- a/widip/comput/__init__.py +++ b/mytilus/comput/__init__.py @@ -2,4 +2,4 @@ from . import computer from .loader import loader_program_ty -from .widish import shell_program_ty +from .mytilus import shell_program_ty diff --git a/widip/comput/boxes.py b/mytilus/comput/boxes.py similarity index 100% rename from widip/comput/boxes.py rename to mytilus/comput/boxes.py diff --git a/widip/comput/compile.py b/mytilus/comput/compile.py similarity index 100% rename from widip/comput/compile.py rename to mytilus/comput/compile.py diff --git a/widip/comput/computer.py b/mytilus/comput/computer.py similarity index 100% rename from widip/comput/computer.py rename to mytilus/comput/computer.py diff --git a/widip/comput/equations.py b/mytilus/comput/equations.py similarity index 100% rename from widip/comput/equations.py rename to mytilus/comput/equations.py diff --git a/widip/comput/loader.py b/mytilus/comput/loader.py similarity index 100% rename from widip/comput/loader.py rename to mytilus/comput/loader.py diff --git a/widip/comput/widish.py b/mytilus/comput/mytilus.py similarity index 97% rename from widip/comput/widish.py rename to mytilus/comput/mytilus.py index 22d6057..7cc1393 100644 --- a/widip/comput/widish.py +++ b/mytilus/comput/mytilus.py @@ -1,7 +1,7 @@ """Shell-language program constants.""" from . import computer -from ..wire.widish import io_ty +from ..wire.mytilus import io_ty shell_program_ty = computer.ProgramTy("sh") diff --git a/widip/comput/python.py b/mytilus/comput/python.py similarity index 100% rename from widip/comput/python.py rename to mytilus/comput/python.py diff --git a/widip/files.py b/mytilus/files.py similarity index 75% rename from widip/files.py rename to mytilus/files.py index e186f94..fcdbe05 100644 --- a/widip/files.py +++ b/mytilus/files.py @@ -1,3 +1,4 @@ +import io import pathlib from nx_yaml import nx_compose_all @@ -18,10 +19,18 @@ def files_ar(ar: Box) -> Diagram: print("is a dir") return ar +def stream_diagram(stream) -> Diagram: + return LoaderToShell()(HIFToLoader()(nx_compose_all(stream))) + + +def source_diagram(source: str) -> Diagram: + return stream_diagram(io.StringIO(source)) + + def file_diagram(file_name) -> Diagram: path = pathlib.Path(file_name) with path.open() as stream: - fd = LoaderToShell()(HIFToLoader()(nx_compose_all(stream))) + fd = stream_diagram(stream) return fd def diagram_draw(path, fd): diff --git a/mytilus/interactive.py b/mytilus/interactive.py new file mode 100644 index 0000000..dc1ccb8 --- /dev/null +++ b/mytilus/interactive.py @@ -0,0 +1,129 @@ +import code +import sys +import termios +import tty + + +CTRL_D = "\x04" +CTRL_J = "\x0A" +CTRL_M = "\x0D" +BACKSPACE = {"\x08", "\x7F"} +CONSOLE_FILENAME = "" +READLINE_FILENAME = "" +READLINE_SYMBOL = "single" + + +def apply_tty_input(buffer: list[str], char: str): + """Update the pending YAML document buffer for one TTY character.""" + if char == CTRL_D: + return ("eof" if not buffer else "submit", None) + if char == CTRL_M: + return ("submit", None) + if char == CTRL_J: + buffer.append("\n") + return ("newline", None) + if char in BACKSPACE: + removed = buffer.pop() if buffer else None + return ("backspace", removed) + buffer.append(char) + return ("char", None) + + +def read_tty_yaml_document(): + """Read one YAML document from TTY using Ctrl+J for LF and Ctrl+M to submit.""" + fd = sys.stdin.fileno() + previous = termios.tcgetattr(fd) + buffer = [] + + try: + tty.setraw(fd) + while True: + raw = sys.stdin.buffer.read(1) + if raw == b"": + raise EOFError + + char = raw.decode("latin1") + action, removed = apply_tty_input(buffer, char) + + if action == "eof": + raise EOFError + if action == "submit": + sys.stdout.write("\n") + sys.stdout.flush() + return "".join(buffer) + if action == "newline": + sys.stdout.write("\n") + sys.stdout.flush() + continue + if action == "backspace": + if removed and removed != "\n": + sys.stdout.write("\b \b") + sys.stdout.flush() + continue + + sys.stdout.write(char) + sys.stdout.flush() + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, previous) + + +def default_shell_source_reader(): + """Read the next mytilus document from TTY or stdin.""" + if sys.stdin.isatty(): + return read_tty_yaml_document() + source = sys.stdin.readline() + if source == "": + raise EOFError + return source + + +def read_shell_source(file_name: str, read_line): + """Write command-document prompt to stdout and read one YAML document.""" + prompt = f"--- !{file_name}\n" + sys.stdout.write(prompt) + sys.stdout.flush() + return read_line() + + +def emit_shell_source(source: str): + """Emit the executed YAML source document to stdout for transcript logging.""" + sys.stdout.write(source) + if not source.endswith("\n"): + sys.stdout.write("\n") + sys.stdout.flush() + + +class ReadFuncConsole(code.InteractiveConsole): + """InteractiveConsole with pluggable input and stderr output handlers.""" + + def __init__(self, readfunc, writefunc, locals, filename): + self._readfunc = readfunc + self._writefunc = writefunc + code.InteractiveConsole.__init__(self, locals=locals, filename=filename) + + def raw_input(self, prompt): + return self._readfunc(prompt) + + def write(self, data): + self._writefunc(data) + + +class ShellConsole(ReadFuncConsole): + """InteractiveConsole front-end for mytilus command documents.""" + + def __init__(self, execute_source, readfunc, writefunc, filename): + self.execute_source = execute_source + ReadFuncConsole.__init__( + self, + readfunc, + writefunc, + None, + filename, + ) + + def runsource(self, source, filename, *rest): + del filename, rest + if not source: + return False + self.execute_source(source) + return False diff --git a/widip/metaprog/__init__.py b/mytilus/metaprog/__init__.py similarity index 66% rename from widip/metaprog/__init__.py rename to mytilus/metaprog/__init__.py index c9256b6..a986dee 100644 --- a/widip/metaprog/__init__.py +++ b/mytilus/metaprog/__init__.py @@ -4,9 +4,9 @@ """ from . import core -from . import widish as metaprog_widish +from . import mytilus as metaprog_mytilus -SHELL_SPECIALIZER = metaprog_widish.ShellSpecializer() +SHELL_SPECIALIZER = metaprog_mytilus.ShellSpecializer() PROGRAM_FUNCTOR = core.ProgramFunctor() METAPROGRAM_FUNCTOR = core.MetaprogramFunctor() diff --git a/widip/metaprog/core.py b/mytilus/metaprog/core.py similarity index 100% rename from widip/metaprog/core.py rename to mytilus/metaprog/core.py diff --git a/widip/metaprog/hif.py b/mytilus/metaprog/hif.py similarity index 100% rename from widip/metaprog/hif.py rename to mytilus/metaprog/hif.py diff --git a/widip/metaprog/widish.py b/mytilus/metaprog/mytilus.py similarity index 97% rename from widip/metaprog/widish.py rename to mytilus/metaprog/mytilus.py index f684f35..66796b4 100644 --- a/widip/metaprog/widish.py +++ b/mytilus/metaprog/mytilus.py @@ -4,8 +4,8 @@ from .core import Specializer from ..comput import computer -from ..comput import widish as shell_lang -from ..wire import widish as shell_wire +from ..comput import mytilus as shell_lang +from ..wire import mytilus as shell_wire def _pipeline_diagram(stages): diff --git a/widip/metaprog/python.py b/mytilus/metaprog/python.py similarity index 100% rename from widip/metaprog/python.py rename to mytilus/metaprog/python.py diff --git a/widip/pcc/__init__.py b/mytilus/pcc/__init__.py similarity index 78% rename from widip/pcc/__init__.py rename to mytilus/pcc/__init__.py index 545a92c..c797f9b 100644 --- a/widip/pcc/__init__.py +++ b/mytilus/pcc/__init__.py @@ -2,4 +2,4 @@ from .core import MonoidalComputer, ProgramClosedCategory from .loader import LOADER, LoaderLanguage -from .widish import SHELL, ShellLanguage +from .mytilus import SHELL, ShellLanguage diff --git a/widip/pcc/core.py b/mytilus/pcc/core.py similarity index 100% rename from widip/pcc/core.py rename to mytilus/pcc/core.py diff --git a/widip/pcc/loader.py b/mytilus/pcc/loader.py similarity index 100% rename from widip/pcc/loader.py rename to mytilus/pcc/loader.py diff --git a/widip/pcc/widish.py b/mytilus/pcc/mytilus.py similarity index 83% rename from widip/pcc/widish.py rename to mytilus/pcc/mytilus.py index e21c402..8c31429 100644 --- a/widip/pcc/widish.py +++ b/mytilus/pcc/mytilus.py @@ -1,7 +1,7 @@ """Program-closed category with shell as distinguished language.""" from ..comput import computer -from ..comput.widish import shell_program_ty +from ..comput.mytilus import shell_program_ty from .core import ProgramClosedCategory @@ -13,7 +13,7 @@ def __init__(self): def execution(self, A: computer.Ty, B: computer.Ty): del A, B - from ..state.widish import ShellExecution + from ..state.mytilus import ShellExecution return ShellExecution() diff --git a/widip/state/__init__.py b/mytilus/state/__init__.py similarity index 87% rename from widip/state/__init__.py rename to mytilus/state/__init__.py index 385e884..9b3601f 100644 --- a/widip/state/__init__.py +++ b/mytilus/state/__init__.py @@ -12,4 +12,4 @@ simulate, ) from .loader import LoaderExecution -from .widish import ShellExecution +from .mytilus import ShellExecution diff --git a/widip/state/core.py b/mytilus/state/core.py similarity index 100% rename from widip/state/core.py rename to mytilus/state/core.py diff --git a/widip/state/hif.py b/mytilus/state/hif.py similarity index 100% rename from widip/state/hif.py rename to mytilus/state/hif.py diff --git a/widip/state/loader.py b/mytilus/state/loader.py similarity index 96% rename from widip/state/loader.py rename to mytilus/state/loader.py index bebbd7d..c81d34c 100644 --- a/widip/state/loader.py +++ b/mytilus/state/loader.py @@ -2,13 +2,13 @@ from ..comput import loader as loader_lang from ..comput.loader import loader_program_ty -from ..comput import widish as shell_lang +from ..comput import mytilus as shell_lang from ..pcc import LOADER, SHELL from ..wire import loader as loader_wire from ..wire.loader import loader_stream_ty -from ..wire import widish as shell_wire +from ..wire import mytilus as shell_wire from .core import Execution, ProcessSimulation -from .widish import Parallel, Pipeline, SubstitutionParallel, SubstitutionPipeline +from .mytilus import Parallel, Pipeline, SubstitutionParallel, SubstitutionPipeline class LoaderExecution(Execution): diff --git a/widip/state/widish.py b/mytilus/state/mytilus.py similarity index 61% rename from widip/state/widish.py rename to mytilus/state/mytilus.py index ee8c43d..70e1a02 100644 --- a/widip/state/widish.py +++ b/mytilus/state/mytilus.py @@ -2,10 +2,11 @@ import subprocess -from ..comput import widish as shell_lang -from ..metaprog import widish as metaprog_widish -from .core import Execution -from ..wire import widish as shell_wire +from ..comput import computer +from ..comput import mytilus as shell_lang +from ..metaprog import mytilus as metaprog_mytilus +from .core import Execution, InputOutputMap +from ..wire import mytilus as shell_wire class SubstitutionPipeline: @@ -41,6 +42,25 @@ def _resolve_command_argv(argv, stdin: str) -> tuple[str, ...]: return tuple(_resolve_command_substitution(argument, stdin) for argument in argv) +def _resolve_terminal_passthrough_argument(argument): + """Resolve one passthrough-safe argv item without running nested subprograms.""" + if isinstance(argument, str): + return argument + if isinstance(argument, shell_lang.Literal): + return argument.text + if isinstance(argument, shell_lang.Empty): + return "" + return None + + +def _resolve_terminal_passthrough_argv(argv): + """Resolve argv when a top-level command can safely own the terminal.""" + resolved = tuple(_resolve_terminal_passthrough_argument(argument) for argument in argv) + if any(argument is None for argument in resolved): + return None + return resolved + + def parallel_io_diagram(branches): """Lower shell-IO branching to structural shell parallel composition.""" branches = tuple(branches) @@ -91,19 +111,59 @@ def run(stdin: str) -> str: raise TypeError(f"unsupported shell program: {program!r}") -class Pipeline(metaprog_widish.Pipeline): +def terminal_passthrough_command(diagram): + """Return the top-level command when one command can own the terminal.""" + if not isinstance(diagram, computer.Diagram): + return None + + inside = getattr(diagram, "inside", ()) + if len(inside) != 2: + return None + + _, command_box, _ = inside[0] + _, output_box, _ = inside[1] + + if not isinstance(command_box, shell_lang.Command): + return None + if not isinstance(output_box, InputOutputMap): + return None + if output_box.process_name != "shell": + return None + if output_box.X != shell_lang.shell_program_ty: + return None + if output_box.A != shell_lang.io_ty or output_box.B != shell_lang.io_ty: + return None + if _resolve_terminal_passthrough_argv(command_box.argv) is None: + return None + + return command_box + + +def run_terminal_command(program: shell_lang.Command): + """Run one shell command attached directly to the current terminal.""" + argv = _resolve_terminal_passthrough_argv(program.argv) + if argv is None: + raise TypeError(f"terminal passthrough requires plain argv: {program.argv!r}") + completed = subprocess.run( + argv, + check=True, + ) + return completed.returncode + + +class Pipeline(metaprog_mytilus.Pipeline): """State-aware pipeline bubble.""" def specialize(self): - return metaprog_widish.Pipeline.specialize(self) + return metaprog_mytilus.Pipeline.specialize(self) -class Parallel(metaprog_widish.Parallel): +class Parallel(metaprog_mytilus.Parallel): """State-aware parallel bubble.""" def specialize(self): return parallel_io_diagram( - tuple(metaprog_widish.ShellSpecializer()(branch) for branch in self.branches), + tuple(metaprog_mytilus.ShellSpecializer()(branch) for branch in self.branches), ) @@ -123,11 +183,11 @@ def parallel(branches): return Parallel(branches) -class ShellSpecializer(metaprog_widish.ShellSpecializer): +class ShellSpecializer(metaprog_mytilus.ShellSpecializer): """State-aware shell bubble specializer.""" def __init__(self): - metaprog_widish.ShellSpecializer.__init__(self) + metaprog_mytilus.ShellSpecializer.__init__(self) class ShellExecution(Execution): diff --git a/widip/state/python.py b/mytilus/state/python.py similarity index 95% rename from widip/state/python.py rename to mytilus/state/python.py index fe5b7e5..e521ff8 100644 --- a/widip/state/python.py +++ b/mytilus/state/python.py @@ -5,17 +5,17 @@ from discopy import monoidal, python from ..comput import computer -from ..comput import widish as shell_lang +from ..comput import mytilus as shell_lang from ..metaprog import core as metaprog_core from ..metaprog import python as metaprog_python from ..pcc import SHELL from . import core as state_core -from .widish import Parallel as ShellParallel -from .widish import Pipeline as ShellPipeline -from .widish import ShellSpecializer, shell_program_runner +from .mytilus import Parallel as ShellParallel +from .mytilus import Pipeline as ShellPipeline +from .mytilus import ShellSpecializer, shell_program_runner -_PATHS_ATTR = "_widip_runtime_paths" +_PATHS_ATTR = "_mytilus_runtime_paths" def _runner_paths(runner): diff --git a/mytilus/watch.py b/mytilus/watch.py new file mode 100644 index 0000000..86145f4 --- /dev/null +++ b/mytilus/watch.py @@ -0,0 +1,160 @@ +from pathlib import Path +import sys +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +from discopy.utils import tuplify, untuplify + +from .files import diagram_draw, file_diagram, source_diagram +from .interactive import ( + CTRL_D, + CTRL_J, + CTRL_M, + ShellConsole, + apply_tty_input, + default_shell_source_reader, + emit_shell_source, + read_shell_source, +) +from .state.python import SHELL_INTERPRETER +from .state.mytilus import run_terminal_command, terminal_passthrough_command + + +def watch_log(message: str): + """Write watcher status logs to stderr.""" + print(message, file=sys.stderr) + + +def has_interactive_terminal(): + """Return whether stdin, stdout, and stderr are all attached to a TTY.""" + return sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty() + + +def execute_shell_diagram(diagram, stdin_text: str | None): + """Run one compiled shell diagram from buffered stdin or direct terminal ownership.""" + if has_interactive_terminal(): + command = terminal_passthrough_command(diagram) + if command is not None: + run_terminal_command(command) + return None + if stdin_text is None: + stdin_text = "" + return SHELL_INTERPRETER(diagram)(stdin_text) + + +def emit_mytilus_result(run_res): + """Emit one mytilus file or inline-command result.""" + print(*(tuple(x.rstrip() for x in tuplify(untuplify(run_res)) if x)), sep="\n") + + +class ShellHandler(FileSystemEventHandler): + """Reload the shell on change.""" + + def on_modified(self, event): + if event.src_path.endswith(".yaml"): + watch_log(f"reloading {event.src_path}") + fd = file_diagram(str(event.src_path)) + diagram_draw(Path(event.src_path), fd) + diagram_draw(Path(event.src_path + ".2"), fd) + + +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. + watch_log("watching for changes in current path") + observer = Observer() + shell_handler = ShellHandler() + observer.schedule(shell_handler, ".", recursive=True) + observer.start() + return observer + + +class StoppedObserver: + """Observer sentinel that makes stop() always safe.""" + + def stop(self): + return None + + +class ShellSession: + """State holder for one interactive mytilus shell session.""" + + def __init__(self, file_name, draw): + self.file_name = file_name + self.draw = draw + self.observer = StoppedObserver() + + def read_source(self): + self.stop_observer() + self.observer = watch_main() + return read_shell_source(self.file_name, default_shell_source_reader) + + def execute_source(self, source): + try: + emit_shell_source(source) + run_shell_source(source, self.file_name, self.draw) + finally: + self.stop_observer() + + def stop_observer(self): + self.observer.stop() + self.observer = StoppedObserver() + + +def run_shell_source(source, file_name, draw): + """Execute one mytilus document inside the interactive shell.""" + source_d = source_diagram(source) + path = Path(file_name) + + if draw: + diagram_draw(path, source_d) + result_ev = execute_shell_diagram(source_d, None) + if result_ev is not None: + print(result_ev) + + +def shell_main(file_name, draw): + session = ShellSession(file_name, draw) + + def read_source(prompt): + del prompt + return session.read_source() + + console = ShellConsole( + session.execute_source, + read_source, + lambda data: sys.stderr.write(data), + file_name, + ) + + try: + console.interact(banner="", exitmsg="") + finally: + session.stop_observer() + + print("⌁") + raise SystemExit(0) + +def mytilus_main(file_name, draw): + fd = file_diagram(file_name) + path = Path(file_name) + if draw: + diagram_draw(path, fd) + + run_res = execute_shell_diagram(fd, None) if has_interactive_terminal() else execute_shell_diagram(fd, sys.stdin.read()) + + if run_res is not None: + emit_mytilus_result(run_res) + + +def mytilus_source_main(source, draw): + fd = source_diagram(source) + path = Path("mytilus-command.yaml") + if draw: + diagram_draw(path, fd) + + run_res = execute_shell_diagram(fd, None) if has_interactive_terminal() else execute_shell_diagram(fd, sys.stdin.read()) + + if run_res is not None: + emit_mytilus_result(run_res) diff --git a/widip/wire/__init__.py b/mytilus/wire/__init__.py similarity index 100% rename from widip/wire/__init__.py rename to mytilus/wire/__init__.py diff --git a/widip/wire/functions.py b/mytilus/wire/functions.py similarity index 100% rename from widip/wire/functions.py rename to mytilus/wire/functions.py diff --git a/widip/wire/hif.py b/mytilus/wire/hif.py similarity index 100% rename from widip/wire/hif.py rename to mytilus/wire/hif.py diff --git a/widip/wire/loader.py b/mytilus/wire/loader.py similarity index 100% rename from widip/wire/loader.py rename to mytilus/wire/loader.py diff --git a/widip/wire/widish.py b/mytilus/wire/mytilus.py similarity index 100% rename from widip/wire/widish.py rename to mytilus/wire/mytilus.py diff --git a/widip/wire/services.py b/mytilus/wire/services.py similarity index 100% rename from widip/wire/services.py rename to mytilus/wire/services.py diff --git a/widip/wire/types.py b/mytilus/wire/types.py similarity index 100% rename from widip/wire/types.py rename to mytilus/wire/types.py diff --git a/pyproject.toml b/pyproject.toml index cb27ced..09761fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["widip"] +packages = ["mytilus"] [project] -name = "widip" +name = "mytilus" version = "0.1.0" -description = "Widip is an interactive environment for computing with wiring diagrams in modern systems" +description = "Mytilus is an interactive environment for computing with wiring diagrams in modern systems" dependencies = [ "discopy>=1.2.2", "pyyaml>=6.0.1", "watchdog>=4.0.1", "nx-yaml==0.4.1", ] @@ -17,5 +17,5 @@ dependencies = [ dev = ["pytest"] [project.urls] -"Source" = "https://github.com/colltoaction/widip" +"Source" = "https://github.com/colltoaction/mytilus" diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py index 566a8ef..13e9b4e 100644 --- a/tests/metaprog/python.py +++ b/tests/metaprog/python.py @@ -1,8 +1,8 @@ """Diagram tests for Sec. 6.2.2 and Futamura projections.""" -from widip.comput import computer -from widip.comput import python as comput_python -from widip.metaprog.python import ( +from mytilus.comput import computer +from mytilus.comput import python as comput_python +from mytilus.metaprog.python import ( PYTHON_COMPILER, PYTHON_COMPILER_GENERATOR, PYTHON_EVALUATOR_BOX, diff --git a/tests/widish/0.in b/tests/mytilus/0.in similarity index 100% rename from tests/widish/0.in rename to tests/mytilus/0.in diff --git a/tests/widish/0.mprog.svg b/tests/mytilus/0.mprog.svg similarity index 55% rename from tests/widish/0.mprog.svg rename to tests/mytilus/0.mprog.svg index 518dabc..a0511b8 100644 --- a/tests/widish/0.mprog.svg +++ b/tests/mytilus/0.mprog.svg @@ -1,12 +1,12 @@ - + - 2026-03-17T13:58:47.021520 + 2026-03-19T19:29:40.159511 image/svg+xml @@ -21,274 +21,499 @@ - - +" clip-path="url(#p0415d875f6)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - +" clip-path="url(#p0415d875f6)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + +Q 18 1584 18 1584 +" clip-path="url(#p0415d875f6)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" clip-path="url(#p0415d875f6)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + - + @@ -545,8 +770,44 @@ z + + + + + + + + + + + - + @@ -560,17 +821,33 @@ z - + + + + + + + + + + + + + + + + + - + - + - + @@ -584,9 +861,9 @@ z - + - + @@ -600,9 +877,9 @@ z - + - + @@ -616,9 +893,17 @@ z - + + + + + + + + + - + @@ -632,9 +917,9 @@ z - + - + @@ -648,17 +933,73 @@ z - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -672,9 +1013,17 @@ z - + + + + + + + + + - + @@ -688,9 +1037,9 @@ z - + - + @@ -704,9 +1053,9 @@ z - + - + @@ -720,9 +1069,9 @@ z - + - + @@ -736,9 +1085,9 @@ z - + - + @@ -752,9 +1101,9 @@ z - + - + @@ -768,9 +1117,9 @@ z - + - + @@ -784,9 +1133,17 @@ z - + + + + + + + + + - + @@ -800,9 +1157,105 @@ z - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + @@ -1036,16 +1489,16 @@ z - + - + - + - + - + - + @@ -1107,8 +1560,8 @@ z - - + + diff --git a/tests/widish/0.out b/tests/mytilus/0.out similarity index 81% rename from tests/widish/0.out rename to tests/mytilus/0.out index cbaf305..2e71593 100644 --- a/tests/widish/0.out +++ b/tests/mytilus/0.out @@ -1,4 +1,4 @@ -73 -23 +234 +113 ? !grep grep: !wc -c ? !tail -2 diff --git a/tests/mytilus/0.prog.svg b/tests/mytilus/0.prog.svg new file mode 100644 index 0000000..6eae31b --- /dev/null +++ b/tests/mytilus/0.prog.svg @@ -0,0 +1,1001 @@ + + + + + + + + 2026-03-19T19:29:40.311628 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/0.yaml b/tests/mytilus/0.yaml similarity index 100% rename from tests/widish/0.yaml rename to tests/mytilus/0.yaml diff --git a/tests/widish/01.in b/tests/mytilus/01.in similarity index 100% rename from tests/widish/01.in rename to tests/mytilus/01.in diff --git a/tests/widish/01.mprog.svg b/tests/mytilus/01.mprog.svg similarity index 100% rename from tests/widish/01.mprog.svg rename to tests/mytilus/01.mprog.svg diff --git a/tests/widish/01.out b/tests/mytilus/01.out similarity index 100% rename from tests/widish/01.out rename to tests/mytilus/01.out diff --git a/tests/widish/01.prog.svg b/tests/mytilus/01.prog.svg similarity index 100% rename from tests/widish/01.prog.svg rename to tests/mytilus/01.prog.svg diff --git a/tests/widish/01.yaml b/tests/mytilus/01.yaml similarity index 100% rename from tests/widish/01.yaml rename to tests/mytilus/01.yaml diff --git a/tests/widish/02.in b/tests/mytilus/02.in similarity index 100% rename from tests/widish/02.in rename to tests/mytilus/02.in diff --git a/tests/widish/02.mprog.svg b/tests/mytilus/02.mprog.svg similarity index 100% rename from tests/widish/02.mprog.svg rename to tests/mytilus/02.mprog.svg diff --git a/tests/widish/02.out b/tests/mytilus/02.out similarity index 100% rename from tests/widish/02.out rename to tests/mytilus/02.out diff --git a/tests/widish/02.prog.svg b/tests/mytilus/02.prog.svg similarity index 100% rename from tests/widish/02.prog.svg rename to tests/mytilus/02.prog.svg diff --git a/tests/widish/02.yaml b/tests/mytilus/02.yaml similarity index 100% rename from tests/widish/02.yaml rename to tests/mytilus/02.yaml diff --git a/tests/widish/03.in b/tests/mytilus/03.in similarity index 100% rename from tests/widish/03.in rename to tests/mytilus/03.in diff --git a/tests/widish/03.mprog.svg b/tests/mytilus/03.mprog.svg similarity index 100% rename from tests/widish/03.mprog.svg rename to tests/mytilus/03.mprog.svg diff --git a/tests/widish/03.out b/tests/mytilus/03.out similarity index 100% rename from tests/widish/03.out rename to tests/mytilus/03.out diff --git a/tests/widish/03.prog.svg b/tests/mytilus/03.prog.svg similarity index 100% rename from tests/widish/03.prog.svg rename to tests/mytilus/03.prog.svg diff --git a/tests/widish/03.yaml b/tests/mytilus/03.yaml similarity index 100% rename from tests/widish/03.yaml rename to tests/mytilus/03.yaml diff --git a/tests/widish/04.in b/tests/mytilus/04.in similarity index 100% rename from tests/widish/04.in rename to tests/mytilus/04.in diff --git a/tests/widish/15.mprog.svg b/tests/mytilus/04.mprog.svg similarity index 69% rename from tests/widish/15.mprog.svg rename to tests/mytilus/04.mprog.svg index 7187fa6..83bc0b4 100644 --- a/tests/widish/15.mprog.svg +++ b/tests/mytilus/04.mprog.svg @@ -1,12 +1,12 @@ - + - 2026-03-17T13:58:47.381517 + 2026-03-19T19:29:40.375642 image/svg+xml @@ -21,8 +21,8 @@ - - +" clip-path="url(#p1347787d39)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - + - + +Q 108 72 108 72 +" clip-path="url(#p1347787d39)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + +" clip-path="url(#p1347787d39)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - + +" clip-path="url(#p1347787d39)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -347,7 +356,7 @@ z - + - + @@ -398,24 +407,44 @@ z - - - - - - - - - - - - - + + + + + + + + - + @@ -431,7 +460,7 @@ z - + @@ -447,7 +476,7 @@ z - + @@ -463,7 +492,7 @@ z - + @@ -479,7 +508,7 @@ z - + @@ -494,8 +523,8 @@ z - - + + - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - + @@ -609,8 +575,8 @@ z - - + + diff --git a/tests/widish/04.out b/tests/mytilus/04.out similarity index 100% rename from tests/widish/04.out rename to tests/mytilus/04.out diff --git a/tests/widish/04.prog.svg b/tests/mytilus/04.prog.svg similarity index 95% rename from tests/widish/04.prog.svg rename to tests/mytilus/04.prog.svg index ae6bfdb..beae668 100644 --- a/tests/widish/04.prog.svg +++ b/tests/mytilus/04.prog.svg @@ -6,7 +6,7 @@ - 2026-03-17T14:36:23.076195 + 2026-03-19T19:29:40.400771 image/svg+xml @@ -35,22 +35,22 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#pa93dbb721b)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p8dd1b0dd48)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -391,7 +391,7 @@ z - + diff --git a/tests/widish/04.yaml b/tests/mytilus/04.yaml similarity index 100% rename from tests/widish/04.yaml rename to tests/mytilus/04.yaml diff --git a/tests/widish/05.in b/tests/mytilus/05.in similarity index 100% rename from tests/widish/05.in rename to tests/mytilus/05.in diff --git a/tests/widish/05.mprog.svg b/tests/mytilus/05.mprog.svg similarity index 100% rename from tests/widish/05.mprog.svg rename to tests/mytilus/05.mprog.svg diff --git a/tests/widish/05.out b/tests/mytilus/05.out similarity index 100% rename from tests/widish/05.out rename to tests/mytilus/05.out diff --git a/tests/widish/05.prog.svg b/tests/mytilus/05.prog.svg similarity index 100% rename from tests/widish/05.prog.svg rename to tests/mytilus/05.prog.svg diff --git a/tests/widish/05.yaml b/tests/mytilus/05.yaml similarity index 100% rename from tests/widish/05.yaml rename to tests/mytilus/05.yaml diff --git a/tests/widish/06.in b/tests/mytilus/06.in similarity index 100% rename from tests/widish/06.in rename to tests/mytilus/06.in diff --git a/tests/widish/06.mprog.svg b/tests/mytilus/06.mprog.svg similarity index 100% rename from tests/widish/06.mprog.svg rename to tests/mytilus/06.mprog.svg diff --git a/tests/widish/06.out b/tests/mytilus/06.out similarity index 100% rename from tests/widish/06.out rename to tests/mytilus/06.out diff --git a/tests/widish/06.prog.svg b/tests/mytilus/06.prog.svg similarity index 100% rename from tests/widish/06.prog.svg rename to tests/mytilus/06.prog.svg diff --git a/tests/widish/06.yaml b/tests/mytilus/06.yaml similarity index 100% rename from tests/widish/06.yaml rename to tests/mytilus/06.yaml diff --git a/tests/widish/07.in b/tests/mytilus/07.in similarity index 100% rename from tests/widish/07.in rename to tests/mytilus/07.in diff --git a/tests/widish/07.mprog.svg b/tests/mytilus/07.mprog.svg similarity index 100% rename from tests/widish/07.mprog.svg rename to tests/mytilus/07.mprog.svg diff --git a/tests/widish/07.out b/tests/mytilus/07.out similarity index 100% rename from tests/widish/07.out rename to tests/mytilus/07.out diff --git a/tests/widish/07.prog.svg b/tests/mytilus/07.prog.svg similarity index 100% rename from tests/widish/07.prog.svg rename to tests/mytilus/07.prog.svg diff --git a/tests/widish/07.yaml b/tests/mytilus/07.yaml similarity index 100% rename from tests/widish/07.yaml rename to tests/mytilus/07.yaml diff --git a/tests/widish/08.in b/tests/mytilus/08.in similarity index 100% rename from tests/widish/08.in rename to tests/mytilus/08.in diff --git a/tests/widish/08.mprog.svg b/tests/mytilus/08.mprog.svg similarity index 100% rename from tests/widish/08.mprog.svg rename to tests/mytilus/08.mprog.svg diff --git a/tests/widish/08.out b/tests/mytilus/08.out similarity index 100% rename from tests/widish/08.out rename to tests/mytilus/08.out diff --git a/tests/widish/08.prog.svg b/tests/mytilus/08.prog.svg similarity index 100% rename from tests/widish/08.prog.svg rename to tests/mytilus/08.prog.svg diff --git a/tests/widish/08.yaml b/tests/mytilus/08.yaml similarity index 100% rename from tests/widish/08.yaml rename to tests/mytilus/08.yaml diff --git a/tests/widish/09.in b/tests/mytilus/09.in similarity index 100% rename from tests/widish/09.in rename to tests/mytilus/09.in diff --git a/tests/widish/09.mprog.svg b/tests/mytilus/09.mprog.svg similarity index 100% rename from tests/widish/09.mprog.svg rename to tests/mytilus/09.mprog.svg diff --git a/tests/widish/09.out b/tests/mytilus/09.out similarity index 100% rename from tests/widish/09.out rename to tests/mytilus/09.out diff --git a/tests/widish/09.prog.svg b/tests/mytilus/09.prog.svg similarity index 100% rename from tests/widish/09.prog.svg rename to tests/mytilus/09.prog.svg diff --git a/tests/widish/09.yaml b/tests/mytilus/09.yaml similarity index 100% rename from tests/widish/09.yaml rename to tests/mytilus/09.yaml diff --git a/tests/widish/10.in b/tests/mytilus/10.in similarity index 100% rename from tests/widish/10.in rename to tests/mytilus/10.in diff --git a/tests/widish/10.mprog.svg b/tests/mytilus/10.mprog.svg similarity index 100% rename from tests/widish/10.mprog.svg rename to tests/mytilus/10.mprog.svg diff --git a/tests/widish/10.out b/tests/mytilus/10.out similarity index 100% rename from tests/widish/10.out rename to tests/mytilus/10.out diff --git a/tests/widish/10.prog.svg b/tests/mytilus/10.prog.svg similarity index 100% rename from tests/widish/10.prog.svg rename to tests/mytilus/10.prog.svg diff --git a/tests/widish/10.yaml b/tests/mytilus/10.yaml similarity index 100% rename from tests/widish/10.yaml rename to tests/mytilus/10.yaml diff --git a/tests/widish/11.in b/tests/mytilus/11.in similarity index 100% rename from tests/widish/11.in rename to tests/mytilus/11.in diff --git a/tests/widish/11.mprog.svg b/tests/mytilus/11.mprog.svg similarity index 100% rename from tests/widish/11.mprog.svg rename to tests/mytilus/11.mprog.svg diff --git a/tests/widish/11.out b/tests/mytilus/11.out similarity index 100% rename from tests/widish/11.out rename to tests/mytilus/11.out diff --git a/tests/widish/11.prog.svg b/tests/mytilus/11.prog.svg similarity index 100% rename from tests/widish/11.prog.svg rename to tests/mytilus/11.prog.svg diff --git a/tests/widish/11.yaml b/tests/mytilus/11.yaml similarity index 100% rename from tests/widish/11.yaml rename to tests/mytilus/11.yaml diff --git a/tests/widish/14.in b/tests/mytilus/14.in similarity index 100% rename from tests/widish/14.in rename to tests/mytilus/14.in diff --git a/tests/widish/14.mprog.svg b/tests/mytilus/14.mprog.svg similarity index 100% rename from tests/widish/14.mprog.svg rename to tests/mytilus/14.mprog.svg diff --git a/tests/widish/14.out b/tests/mytilus/14.out similarity index 100% rename from tests/widish/14.out rename to tests/mytilus/14.out diff --git a/tests/widish/14.prog.svg b/tests/mytilus/14.prog.svg similarity index 100% rename from tests/widish/14.prog.svg rename to tests/mytilus/14.prog.svg diff --git a/tests/widish/14.yaml b/tests/mytilus/14.yaml similarity index 100% rename from tests/widish/14.yaml rename to tests/mytilus/14.yaml diff --git a/tests/widish/15.in b/tests/mytilus/15.in similarity index 100% rename from tests/widish/15.in rename to tests/mytilus/15.in diff --git a/tests/mytilus/15.mprog.svg b/tests/mytilus/15.mprog.svg new file mode 100644 index 0000000..27031a3 --- /dev/null +++ b/tests/mytilus/15.mprog.svg @@ -0,0 +1,824 @@ + + + + + + + + 2026-03-19T19:29:40.468062 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/15.out b/tests/mytilus/15.out similarity index 100% rename from tests/widish/15.out rename to tests/mytilus/15.out diff --git a/tests/mytilus/15.prog.svg b/tests/mytilus/15.prog.svg new file mode 100644 index 0000000..0116d18 --- /dev/null +++ b/tests/mytilus/15.prog.svg @@ -0,0 +1,620 @@ + + + + + + + + 2026-03-19T19:29:40.526176 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/15.yaml b/tests/mytilus/15.yaml similarity index 100% rename from tests/widish/15.yaml rename to tests/mytilus/15.yaml diff --git a/tests/widish/16.in b/tests/mytilus/16.in similarity index 100% rename from tests/widish/16.in rename to tests/mytilus/16.in diff --git a/tests/widish/16.mprog.svg b/tests/mytilus/16.mprog.svg similarity index 62% rename from tests/widish/16.mprog.svg rename to tests/mytilus/16.mprog.svg index 098143c..6cca682 100644 --- a/tests/widish/16.mprog.svg +++ b/tests/mytilus/16.mprog.svg @@ -1,12 +1,12 @@ - + - 2026-03-17T13:58:47.422711 + 2026-03-19T19:29:40.600156 image/svg+xml @@ -21,127 +21,227 @@ - - +" clip-path="url(#pbfd108ea94)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - +" clip-path="url(#pbfd108ea94)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + +Q 18 630 18 630 +" clip-path="url(#pbfd108ea94)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" clip-path="url(#pbfd108ea94)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - + +" clip-path="url(#pbfd108ea94)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - + +" clip-path="url(#pbfd108ea94)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> + + + - + - + - + @@ -399,7 +499,7 @@ z - + @@ -414,8 +514,44 @@ z + + + + + + + + + + + - + @@ -429,9 +565,9 @@ z - + - + @@ -445,9 +581,9 @@ z - + - + @@ -461,9 +597,9 @@ z - + - + @@ -477,9 +613,17 @@ z - + + + + + + + + + - + @@ -493,9 +637,73 @@ z - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + @@ -682,8 +890,8 @@ z - - + + diff --git a/tests/widish/16.out b/tests/mytilus/16.out similarity index 100% rename from tests/widish/16.out rename to tests/mytilus/16.out diff --git a/tests/mytilus/16.prog.svg b/tests/mytilus/16.prog.svg new file mode 100644 index 0000000..5fbe2f8 --- /dev/null +++ b/tests/mytilus/16.prog.svg @@ -0,0 +1,684 @@ + + + + + + + + 2026-03-19T19:29:40.651878 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/16.yaml b/tests/mytilus/16.yaml similarity index 100% rename from tests/widish/16.yaml rename to tests/mytilus/16.yaml diff --git a/tests/widish/17.in b/tests/mytilus/17.in similarity index 100% rename from tests/widish/17.in rename to tests/mytilus/17.in diff --git a/tests/widish/17.mprog.svg b/tests/mytilus/17.mprog.svg similarity index 50% rename from tests/widish/17.mprog.svg rename to tests/mytilus/17.mprog.svg index c56bb39..25cd8f0 100644 --- a/tests/widish/17.mprog.svg +++ b/tests/mytilus/17.mprog.svg @@ -1,12 +1,12 @@ - + - 2026-03-17T13:58:48.901517 + 2026-03-19T19:29:43.831082 image/svg+xml @@ -21,2807 +21,6633 @@ - - +" clip-path="url(#p6ee39278fa)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - +" clip-path="url(#p6ee39278fa)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + +Q 18 16128 18 16128 +" clip-path="url(#p6ee39278fa)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - +" clip-path="url(#p6ee39278fa)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +Q 54 5670 54 5670 +" clip-path="url(#p6ee39278fa)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" clip-path="url(#p6ee39278fa)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - - - - + - + + + + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2835,45 +6661,65 @@ z - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + - + - + @@ -2887,45 +6733,25 @@ z - - - - - - - - - + + + + + + + + + + + + + + - + - + @@ -2939,9 +6765,9 @@ z - + - + @@ -2955,9 +6781,9 @@ z - + - + @@ -2971,9 +6797,9 @@ z - + - + @@ -2987,17 +6813,25 @@ z - - - - - - + + + + + + + + + + + + + + - + - + @@ -3011,9 +6845,9 @@ z - + - + @@ -3027,9 +6861,9 @@ z - + - + @@ -3043,9 +6877,9 @@ z - + - + @@ -3059,9 +6893,17 @@ z - + + + + + + + + + - + @@ -3075,17 +6917,25 @@ z - - - - - - + + + + + + + + + + + + + + - + - + @@ -3099,9 +6949,9 @@ z - + - + @@ -3115,9 +6965,17 @@ z - + + + + + + + + + - + @@ -3131,9 +6989,9 @@ z - + - + @@ -3147,9 +7005,9 @@ z - + - + @@ -3163,17 +7021,33 @@ z - - - - - - + + + + + + + + + + + + + + - + + + + + + + + + - + @@ -3187,9 +7061,17 @@ z - + + + + + + + + + - + @@ -3203,9 +7085,9 @@ z - + - + @@ -3219,9 +7101,17 @@ z - + + + + + + + + + - + @@ -3235,9 +7125,9 @@ z - + - + @@ -3251,9 +7141,9 @@ z - + - + @@ -3267,9 +7157,17 @@ z - + + + + + + + + + - + @@ -3283,9 +7181,9 @@ z - + - + @@ -3299,9 +7197,9 @@ z - + - + @@ -3315,9 +7213,9 @@ z - + - + @@ -3331,17 +7229,17 @@ z - + - + - + - + @@ -3355,9 +7253,9 @@ z - + - + @@ -3371,9 +7269,9 @@ z - + - + @@ -3387,9 +7285,9 @@ z - + - + @@ -3403,9 +7301,17 @@ z - + + + + + + + + + - + @@ -3419,9 +7325,9 @@ z - + - + @@ -3435,17 +7341,9 @@ z - - - - - - - - - + - + @@ -3459,9 +7357,9 @@ z - + - + @@ -3475,9 +7373,9 @@ z - + - + @@ -3491,9 +7389,9 @@ z - + - + @@ -3507,9 +7405,9 @@ z - + - + @@ -3523,17 +7421,9 @@ z - - - - - - - - - + - + @@ -3547,9 +7437,9 @@ z - + - + @@ -3563,9 +7453,17 @@ z - + + + + + + + + + - + @@ -3579,9 +7477,17 @@ z - + + + + + + + + + - + @@ -3595,9 +7501,9 @@ z - + - + @@ -3611,17 +7517,9 @@ z - - - - - - - - - + - + @@ -3635,9 +7533,17 @@ z - + + + + + + + + + - + @@ -3651,9 +7557,9 @@ z - + - + @@ -3667,9 +7573,9 @@ z - + - + @@ -3683,9 +7589,9 @@ z - + - + @@ -3699,9 +7605,17 @@ z - + + + + + + + + + - + @@ -3715,9 +7629,9 @@ z - + - + @@ -3731,9 +7645,9 @@ z - + - + @@ -3747,9 +7661,9 @@ z - + - + @@ -3763,9 +7677,17 @@ z - + + + + + + + + + - + @@ -3779,17 +7701,17 @@ z - + - + - + - + @@ -3803,17 +7725,9 @@ z - - - - - - - - - + - + @@ -3827,9 +7741,9 @@ z - + - + @@ -3843,9 +7757,9 @@ z - + - + @@ -3859,9 +7773,9 @@ z - + - + @@ -3875,9 +7789,9 @@ z - + - + @@ -3891,9 +7805,9 @@ z - + - + @@ -3907,9 +7821,9 @@ z - + - + @@ -3923,9 +7837,9 @@ z - + - + @@ -3939,9 +7853,17 @@ z - + + + + + + + + + - + @@ -3955,9 +7877,17 @@ z - + + + + + + + + + - + @@ -3971,9 +7901,17 @@ z - + + + + + + + + + - + @@ -3987,9 +7925,9 @@ z - + - + @@ -4003,17 +7941,17 @@ z - - - - - - + + + + + + - + - + @@ -4027,9 +7965,9 @@ z - + - + @@ -4043,9 +7981,9 @@ z - + - + @@ -4059,9 +7997,9 @@ z - + - + @@ -4075,9 +8013,17 @@ z - + + + + + + + + + - + @@ -4091,9 +8037,17 @@ z - + + + + + + + + + - + @@ -4107,17 +8061,9 @@ z - - - - - - - - - + - + @@ -4131,9 +8077,17 @@ z - + + + + + + + + + - + @@ -4147,17 +8101,25 @@ z - - - - - - + + + + + + + + + + + + + + - + - + @@ -4171,9 +8133,9 @@ z - + - + @@ -4187,9 +8149,17 @@ z - + + + + + + + + + - + @@ -4203,9 +8173,9 @@ z - + - + @@ -4219,9 +8189,9 @@ z - + - + @@ -4235,9 +8205,9 @@ z - + - + @@ -4251,9 +8221,9 @@ z - + - + @@ -4267,9 +8237,9 @@ z - + - + @@ -4283,9 +8253,9 @@ z - + - + @@ -4299,9 +8269,9 @@ z - + - + @@ -4315,9 +8285,9 @@ z - + - + @@ -4331,9 +8301,9 @@ z - + - + @@ -4347,9 +8317,9 @@ z - + - + @@ -4363,17 +8333,9 @@ z - - - - - - - - - + - + @@ -4387,9 +8349,9 @@ z - + - + @@ -4403,9 +8365,9 @@ z - + - + @@ -4419,9 +8381,17 @@ z - + + + + + + + + + - + @@ -4435,9 +8405,9 @@ z - + - + @@ -4451,9 +8421,9 @@ z - + - + @@ -4467,9 +8437,9 @@ z - + - + @@ -4483,17 +8453,17 @@ z - + - + - + - + @@ -4507,9 +8477,9 @@ z - + - + @@ -4523,9 +8493,9 @@ z - + - + @@ -4539,9 +8509,9 @@ z - + - + @@ -4555,9 +8525,17 @@ z - + + + + + + + + + - + @@ -4571,9 +8549,17 @@ z - + + + + + + + + + - + @@ -4587,9 +8573,9 @@ z - + - + @@ -4603,17 +8589,9 @@ z - - - - - - - - - + - + @@ -4627,17 +8605,17 @@ z - - - - - - + + + + + + - + - + @@ -4651,9 +8629,9 @@ z - + - + @@ -4667,9 +8645,9 @@ z - + - + @@ -4683,9 +8661,9 @@ z - + - + @@ -4699,17 +8677,17 @@ z - - - - - - + + + + + + - + - + @@ -4723,9 +8701,17 @@ z - + + + + + + + + + - + @@ -4739,9 +8725,9 @@ z - + - + @@ -4755,9 +8741,9 @@ z - + - + @@ -4771,9 +8757,9 @@ z - + - + @@ -4787,9 +8773,9 @@ z - + - + @@ -4803,9 +8789,9 @@ z - + - + @@ -4819,9 +8805,17 @@ z - + + + + + + + + + - + @@ -4835,9 +8829,17 @@ z - + + + + + + + + + - + @@ -4851,9 +8853,9 @@ z - + - + @@ -4867,9 +8869,17 @@ z - + + + + + + + + + - + @@ -4883,9 +8893,9 @@ z - + - + @@ -4899,9 +8909,9 @@ z - + - + @@ -4915,9 +8925,9 @@ z - + - + @@ -4931,17 +8941,17 @@ z - - - - - - + + + + + + - + - + @@ -4955,9 +8965,9 @@ z - + - + @@ -4971,9 +8981,9 @@ z - + - + @@ -4987,9 +8997,9 @@ z - + - + @@ -5003,9 +9013,9 @@ z - + - + @@ -5019,9 +9029,9 @@ z - + - + @@ -5035,17 +9045,9 @@ z - - - - - - - - - + - + @@ -5059,9 +9061,9 @@ z - + - + @@ -5075,9 +9077,9 @@ z - + - + @@ -5091,9 +9093,9 @@ z - + - + @@ -5107,9 +9109,17 @@ z - + + + + + + + + + - + @@ -5123,17 +9133,41 @@ z - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -5147,9 +9181,9 @@ z - + - + @@ -5163,9 +9197,17 @@ z - + + + + + + + + + - + @@ -5179,9 +9221,9 @@ z - + - + @@ -5195,9 +9237,9 @@ z - + - + @@ -5211,17 +9253,9 @@ z - - - - - - - - - + - + @@ -5235,9 +9269,17 @@ z - + + + + + + + + + - + @@ -5251,9 +9293,9 @@ z - + - + @@ -5267,9 +9309,9 @@ z - + - + @@ -5283,9 +9325,9 @@ z - + - + @@ -5299,9 +9341,9 @@ z - + - + @@ -5315,9 +9357,9 @@ z - + - + @@ -5331,9 +9373,9 @@ z - + - + @@ -5347,9 +9389,9 @@ z - + - + @@ -5363,9 +9405,9 @@ z - + - + @@ -5379,17 +9421,17 @@ z - + - + - + - + @@ -5403,17 +9445,17 @@ z - + - + - + - + @@ -5427,9 +9469,9 @@ z - + - + @@ -5443,9 +9485,9 @@ z - + - + @@ -5459,9 +9501,17 @@ z - + + + + + + + + + - + @@ -5475,9 +9525,9 @@ z - + - + @@ -5491,9 +9541,9 @@ z - + - + @@ -5507,9 +9557,9 @@ z - + - + @@ -5523,9 +9573,17 @@ z - + + + + + + + + + - + @@ -5539,9 +9597,17 @@ z - + + + + + + + + + - + @@ -5555,9 +9621,9 @@ z - + - + @@ -5571,17 +9637,9 @@ z - - - - - - - - - + - + @@ -5595,9 +9653,9 @@ z - + - + @@ -5611,9 +9669,9 @@ z - + - + @@ -5627,9 +9685,9 @@ z - + - + @@ -5643,9 +9701,17 @@ z - + + + + + + + + + - + @@ -5659,17 +9725,17 @@ z - - - - - - + + + + + + - + - + @@ -5683,9 +9749,9 @@ z - + - + @@ -5699,9 +9765,17 @@ z - + + + + + + + + + - + @@ -5715,9 +9789,9 @@ z - + - + @@ -5731,9 +9805,9 @@ z - + - + @@ -5747,17 +9821,33 @@ z - - - - - - + + + + + + + + + + + + + + - + + + + + + + + + - + @@ -5771,9 +9861,9 @@ z - + - + @@ -5787,9 +9877,9 @@ z - + - + @@ -5803,9 +9893,9 @@ z - + - + @@ -5819,9 +9909,9 @@ z - + - + @@ -5835,9 +9925,9 @@ z - + - + @@ -5851,9 +9941,9 @@ z - + - + @@ -5867,9 +9957,9 @@ z - + - + @@ -5883,9 +9973,9 @@ z - + - + @@ -5899,9 +9989,9 @@ z - + - + @@ -5915,9 +10005,9 @@ z - + - + @@ -5931,9 +10021,9 @@ z - + - + @@ -5947,9 +10037,9 @@ z - + - + @@ -5963,9 +10053,9 @@ z - + - + @@ -5979,9 +10069,9 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -6456,25 +10546,25 @@ z - + - + - + - + - + - + @@ -6492,16 +10582,16 @@ z - + - + - + - + @@ -6518,34 +10608,34 @@ z - + - + - + - + - + - + - + - + - + - + - + - + @@ -6607,63 +10697,63 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -6735,16 +10825,16 @@ z - + - + - + - + - + - + - + - + - + - + - + - + @@ -6823,16 +10913,16 @@ z - + - + - + - + @@ -6847,16 +10937,16 @@ z - + - + - + - + @@ -6872,34 +10962,34 @@ z - + - + - + - + - + - + - + - + @@ -6917,16 +11007,16 @@ z - + - + - + - + @@ -6942,16 +11032,16 @@ z - + - + - + - + @@ -6971,72 +11061,72 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -7052,16 +11142,16 @@ z - + - + - + - + @@ -7077,25 +11167,25 @@ z - + - + - + - + - + - + @@ -7111,16 +11201,16 @@ z - + - + - + - + @@ -7138,34 +11228,34 @@ z - + - + - + - + - + - + - + - + @@ -7182,16 +11272,16 @@ z - + - + - + - + @@ -7209,36 +11299,36 @@ z - + - + - + - + - + - + - + - + @@ -7256,16 +11346,16 @@ z - + - + - + - + @@ -7283,25 +11373,25 @@ z - + - + - + - + - + - + @@ -7319,16 +11409,16 @@ z - + - + - + - + @@ -7346,43 +11436,43 @@ z - + - + - + - + - + - + - + - + - + - + @@ -7392,8 +11482,8 @@ z - - + + diff --git a/tests/widish/17.out b/tests/mytilus/17.out similarity index 100% rename from tests/widish/17.out rename to tests/mytilus/17.out diff --git a/tests/mytilus/17.prog.svg b/tests/mytilus/17.prog.svg new file mode 100644 index 0000000..9a3caeb --- /dev/null +++ b/tests/mytilus/17.prog.svg @@ -0,0 +1,6915 @@ + + + + + + + + 2026-03-19T19:29:46.922578 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/widish/17.yaml b/tests/mytilus/17.yaml similarity index 100% rename from tests/widish/17.yaml rename to tests/mytilus/17.yaml diff --git a/tests/svg/test_eq_2_5_compile_partial_is_eval_left.svg b/tests/svg/test_eq_2_5_compile_partial_is_eval_left.svg index 6eb08f7..cc482f2 100644 --- a/tests/svg/test_eq_2_5_compile_partial_is_eval_left.svg +++ b/tests/svg/test_eq_2_5_compile_partial_is_eval_left.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.532772 + 2026-03-21T17:13:47.918240 image/svg+xml @@ -35,27 +35,27 @@ L 216 144 L 216 0 L 0 0 z -" clip-path="url(#p36496d4d09)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3c53bd7dad)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + - - - - + + + + + + + + - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -378,64 +299,82 @@ z - - + + - - - - - - - - - - - - - - - - - - - + + - + diff --git a/tests/svg/test_eq_2_5_compile_partial_is_eval_right.svg b/tests/svg/test_eq_2_5_compile_partial_is_eval_right.svg index 734c482..63846d0 100644 --- a/tests/svg/test_eq_2_5_compile_partial_is_eval_right.svg +++ b/tests/svg/test_eq_2_5_compile_partial_is_eval_right.svg @@ -1,12 +1,12 @@ - + - 2026-03-10T17:48:27.565506 + 2026-03-21T17:13:47.952212 image/svg+xml @@ -21,8 +21,8 @@ - - +" clip-path="url(#p3ad96ac102)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +Q 18 216 18 216 +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +Q 216 162 216 162 +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +Q 270 216 270 216 +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3ad96ac102)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - - - +" clip-path="url(#p3ad96ac102)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + - - - +" clip-path="url(#p3ad96ac102)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -191,22 +178,38 @@ z - + - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - @@ -470,93 +361,84 @@ z - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - + + - - + + diff --git a/tests/svg/test_eq_2_6_compile_data_is_identity_left.svg b/tests/svg/test_eq_2_6_compile_data_is_identity_left.svg index 4e1767d..6f0be2b 100644 --- a/tests/svg/test_eq_2_6_compile_data_is_identity_left.svg +++ b/tests/svg/test_eq_2_6_compile_data_is_identity_left.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.478050 + 2026-03-21T17:13:47.875886 image/svg+xml @@ -35,12 +35,12 @@ L 36 72 L 36 0 L 0 0 z -" clip-path="url(#p149d3a6a42)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p331cde313f)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p331cde313f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> @@ -69,7 +69,7 @@ z - + diff --git a/tests/svg/test_eq_2_6_compile_data_is_identity_right.svg b/tests/svg/test_eq_2_6_compile_data_is_identity_right.svg index 3a17cf2..af98f73 100644 --- a/tests/svg/test_eq_2_6_compile_data_is_identity_right.svg +++ b/tests/svg/test_eq_2_6_compile_data_is_identity_right.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.506785 + 2026-03-21T17:13:47.897124 image/svg+xml @@ -35,62 +35,62 @@ L 144 288 L 144 0 L 0 0 z -" clip-path="url(#p3bb61d9e3f)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pacaaf89081)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -139,86 +139,32 @@ z - + - - - - - - - - - - - - - - - - - + @@ -269,96 +215,82 @@ z - - + + - - - - - - - - - - - - - - - - - - - + + - + diff --git a/tests/svg/test_fig_2_7_compile_parallel_to_left_side_left.svg b/tests/svg/test_fig_2_7_compile_parallel_to_left_side_left.svg index b47ee4c..35208d3 100644 --- a/tests/svg/test_fig_2_7_compile_parallel_to_left_side_left.svg +++ b/tests/svg/test_fig_2_7_compile_parallel_to_left_side_left.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.410393 + 2026-03-21T17:13:47.800557 image/svg+xml @@ -35,67 +35,67 @@ L 288 360 L 288 0 L 0 0 z -" clip-path="url(#p8217978a81)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc43263618f)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + - - - - + + + + + + + + - - - - - - - - - - - - - - @@ -261,28 +208,9 @@ z - + - - - - - - - - - - - + @@ -292,27 +220,66 @@ z - + - - - - - - - - + + + + + + + @@ -371,110 +338,89 @@ z - - + + - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + + + - + diff --git a/tests/svg/test_fig_2_7_compile_parallel_to_left_side_right.svg b/tests/svg/test_fig_2_7_compile_parallel_to_left_side_right.svg index 907e013..9a31f9c 100644 --- a/tests/svg/test_fig_2_7_compile_parallel_to_left_side_right.svg +++ b/tests/svg/test_fig_2_7_compile_parallel_to_left_side_right.svg @@ -1,12 +1,12 @@ - + - 2026-03-10T17:48:27.451338 + 2026-03-21T17:13:47.851614 image/svg+xml @@ -21,8 +21,8 @@ - - +" clip-path="url(#p03da803827)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p03da803827)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p03da803827)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + +Q 18 432 18 432 +" clip-path="url(#p03da803827)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + +Q 342 432 342 432 +" clip-path="url(#p03da803827)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + +" clip-path="url(#p03da803827)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> + + + - + - + - + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - + + diff --git a/tests/svg/test_fig_2_7_compile_sequential_to_left_side_left.svg b/tests/svg/test_fig_2_7_compile_sequential_to_left_side_left.svg index c6c3c7c..9d0fc51 100644 --- a/tests/svg/test_fig_2_7_compile_sequential_to_left_side_left.svg +++ b/tests/svg/test_fig_2_7_compile_sequential_to_left_side_left.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.302398 + 2026-03-21T17:13:47.727252 image/svg+xml @@ -35,78 +35,127 @@ L 216 288 L 216 0 L 0 0 z -" clip-path="url(#p5dbeb2b8f6)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p19d44b25f7)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - - - +" clip-path="url(#p19d44b25f7)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - - - - + +" clip-path="url(#p19d44b25f7)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p19d44b25f7)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -305,7 +255,7 @@ z - + @@ -326,111 +276,90 @@ z - - - + + + - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - + + + + + - + diff --git a/tests/svg/test_fig_2_7_compile_sequential_to_left_side_right.svg b/tests/svg/test_fig_2_7_compile_sequential_to_left_side_right.svg index 7feb0d3..516cd32 100644 --- a/tests/svg/test_fig_2_7_compile_sequential_to_left_side_right.svg +++ b/tests/svg/test_fig_2_7_compile_sequential_to_left_side_right.svg @@ -6,7 +6,7 @@ - 2026-03-10T17:48:27.352022 + 2026-03-21T17:13:47.762227 image/svg+xml @@ -35,128 +35,108 @@ L 288 432 L 288 0 L 0 0 z -" clip-path="url(#p182c76352d)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p77fa0facf8)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> - +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - - - - - - +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + - - - +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - - - - + +Q 216 234 216 234 +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +Q 72 306 72 306 +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - - + + - - + + - + +Q 18 396 126 396 +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - - + + - + +Q 270 396 126 396 +" clip-path="url(#p77fa0facf8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p77fa0facf8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + +" clip-path="url(#p77fa0facf8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - + +" clip-path="url(#p77fa0facf8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - - + +" clip-path="url(#p77fa0facf8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> - + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + @@ -376,7 +295,7 @@ z - + @@ -409,119 +328,90 @@ z - - - + + + - + - - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + - + diff --git a/tests/svg/test_fig_6_3_eq_0_comp.svg b/tests/svg/test_fig_6_3_eq_0_comp.svg index 2d9c329..e1f83ad 100644 --- a/tests/svg/test_fig_6_3_eq_0_comp.svg +++ b/tests/svg/test_fig_6_3_eq_0_comp.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.237369 + 2026-03-21T17:13:48.160956 image/svg+xml @@ -35,22 +35,22 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#p3218aee695)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p150cdda2e5)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -255,7 +255,7 @@ z - + diff --git a/tests/svg/test_fig_6_3_eq_0_mprog.svg b/tests/svg/test_fig_6_3_eq_0_mprog.svg index 62b42df..bfda3cb 100644 --- a/tests/svg/test_fig_6_3_eq_0_mprog.svg +++ b/tests/svg/test_fig_6_3_eq_0_mprog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.276445 + 2026-03-21T17:13:48.195861 image/svg+xml @@ -35,27 +35,27 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#p116374239f)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p2bae24ff67)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -259,7 +259,7 @@ z - + diff --git a/tests/svg/test_fig_6_3_eq_0_prog.svg b/tests/svg/test_fig_6_3_eq_0_prog.svg index 1758882..d3c3797 100644 --- a/tests/svg/test_fig_6_3_eq_0_prog.svg +++ b/tests/svg/test_fig_6_3_eq_0_prog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.258172 + 2026-03-21T17:13:48.178908 image/svg+xml @@ -35,22 +35,22 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#p27c801b41b)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0896953b82)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -255,7 +255,7 @@ z - + diff --git a/tests/svg/test_fig_6_3_eq_1_comp.svg b/tests/svg/test_fig_6_3_eq_1_comp.svg index cbf0b2a..05bdf21 100644 --- a/tests/svg/test_fig_6_3_eq_1_comp.svg +++ b/tests/svg/test_fig_6_3_eq_1_comp.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.306843 + 2026-03-21T17:13:48.230973 image/svg+xml @@ -35,32 +35,32 @@ L 216 288 L 216 0 L 0 0 z -" clip-path="url(#pdeca6280f2)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p516c37eedd)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -318,7 +318,7 @@ z - + diff --git a/tests/svg/test_fig_6_3_eq_1_mprog.svg b/tests/svg/test_fig_6_3_eq_1_mprog.svg index 2b89e91..793c020 100644 --- a/tests/svg/test_fig_6_3_eq_1_mprog.svg +++ b/tests/svg/test_fig_6_3_eq_1_mprog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.361541 + 2026-03-21T17:13:48.291119 image/svg+xml @@ -35,32 +35,32 @@ L 216 216 L 216 0 L 0 0 z -" clip-path="url(#p7d1ba931fe)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p1c28aacaa8)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -285,7 +285,7 @@ z - + diff --git a/tests/svg/test_fig_6_3_eq_1_prog.svg b/tests/svg/test_fig_6_3_eq_1_prog.svg index 51e4dca..1fb21ea 100644 --- a/tests/svg/test_fig_6_3_eq_1_prog.svg +++ b/tests/svg/test_fig_6_3_eq_1_prog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.334234 + 2026-03-21T17:13:48.262403 image/svg+xml @@ -35,32 +35,32 @@ L 216 288 L 216 0 L 0 0 z -" clip-path="url(#p79206757ee)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p16c771a16e)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -318,7 +318,7 @@ z - + diff --git a/tests/svg/test_sec_6_2_2_comp.svg b/tests/svg/test_sec_6_2_2_comp.svg index 5c92a49..d0baa31 100644 --- a/tests/svg/test_sec_6_2_2_comp.svg +++ b/tests/svg/test_sec_6_2_2_comp.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.190487 + 2026-03-21T17:13:48.112050 image/svg+xml @@ -35,22 +35,22 @@ L 144 72 L 144 0 L 0 0 z -" clip-path="url(#p6b25f62224)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p0b791f1858)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p0b791f1858)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0b791f1858)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0b791f1858)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p0b791f1858)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -239,7 +239,7 @@ z - + diff --git a/tests/svg/test_sec_6_2_2_mprog.svg b/tests/svg/test_sec_6_2_2_mprog.svg index e074c9c..0870486 100644 --- a/tests/svg/test_sec_6_2_2_mprog.svg +++ b/tests/svg/test_sec_6_2_2_mprog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.220451 + 2026-03-21T17:13:48.141924 image/svg+xml @@ -35,27 +35,27 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#p8567335c69)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pdc3ee70e01)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -276,7 +276,7 @@ z - + diff --git a/tests/svg/test_sec_6_2_2_prog.svg b/tests/svg/test_sec_6_2_2_prog.svg index d6d33dd..2138cbc 100644 --- a/tests/svg/test_sec_6_2_2_prog.svg +++ b/tests/svg/test_sec_6_2_2_prog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.203748 + 2026-03-21T17:13:48.125658 image/svg+xml @@ -35,22 +35,22 @@ L 144 72 L 144 0 L 0 0 z -" clip-path="url(#pe351a16c87)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p4a93495af4)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p4a93495af4)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p4a93495af4)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p4a93495af4)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p4a93495af4)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -233,7 +233,7 @@ z - + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg index 1c8d245..b39fee0 100644 --- a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_comp.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.384331 + 2026-03-21T17:13:48.311411 image/svg+xml @@ -35,22 +35,22 @@ L 144 72 L 144 0 L 0 0 z -" clip-path="url(#pbe45796b9c)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pcfcba7af47)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pcfcba7af47)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pcfcba7af47)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pcfcba7af47)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pcfcba7af47)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -239,7 +239,7 @@ z - + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg index 4b795f3..617e180 100644 --- a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_mprog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.415906 + 2026-03-21T17:13:48.349856 image/svg+xml @@ -35,27 +35,27 @@ L 144 144 L 144 0 L 0 0 z -" clip-path="url(#p3733e7fb9f)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#p3cb181be97)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -276,7 +276,7 @@ z - + diff --git a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg index 47e3bf1..2202d81 100644 --- a/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg +++ b/tests/svg/test_specializers_are_unit_metaprograms_with_partial_evaluators_prog.svg @@ -6,7 +6,7 @@ - 2026-03-18T16:04:25.397835 + 2026-03-21T17:13:48.327553 image/svg+xml @@ -35,22 +35,22 @@ L 144 72 L 144 0 L 0 0 z -" clip-path="url(#pb42250e593)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pc3d56b8e51)" style="fill: #ffffff; stroke: #ffffff; stroke-linejoin: miter"/> +" clip-path="url(#pc3d56b8e51)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc3d56b8e51)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc3d56b8e51)" style="fill: none; stroke: #000000; stroke-linejoin: miter"/> +" clip-path="url(#pc3d56b8e51)" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> @@ -233,7 +233,7 @@ z - + diff --git a/tests/test_bin_mytilus.py b/tests/test_bin_mytilus.py new file mode 100644 index 0000000..959c6a8 --- /dev/null +++ b/tests/test_bin_mytilus.py @@ -0,0 +1,155 @@ +import os +import pty +import select +import subprocess +import sys +import time + +PTY_TIMEOUT_SECONDS = 5.0 + + +def run_mytilus(*args, env): + run_env = os.environ.copy() + run_env.setdefault("MPLCONFIGDIR", "/tmp/mytilus-mpl") + if env is not None: + run_env.update(env) + return subprocess.run( + ["bin/mytilus", *args], + text=True, + capture_output=True, + check=False, + env=run_env, + stdin=subprocess.DEVNULL, + ) + + +def run_mytilus_pty(*args, env): + run_env = os.environ.copy() + run_env.setdefault("MPLCONFIGDIR", "/tmp/mytilus-mpl") + if env is not None: + run_env.update(env) + + master_fd, slave_fd = pty.openpty() + process = subprocess.Popen( + ["bin/mytilus", *args], + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd, + env=run_env, + close_fds=True, + ) + os.close(slave_fd) + return process, master_fd + + +def read_pty_until(master_fd, needle, timeout): + deadline = time.time() + timeout + chunks = [] + data = b"" + + while time.time() < deadline: + ready, _, _ = select.select([master_fd], [], [], 0.1) + if master_fd not in ready: + continue + try: + chunk = os.read(master_fd, 4096) + except OSError: + break + if not chunk: + break + chunks.append(chunk) + data = b"".join(chunks) + if needle in data: + return data + + raise AssertionError(f"Did not see {needle!r} in PTY output: {data!r}") + + +def test_bin_mytilus_c_executes_yaml_command(): + result = run_mytilus("-c", "!echo hello-from-mytilus", env=None) + + assert result.returncode == 0 + assert result.stdout.splitlines() == ["hello-from-mytilus"] + assert result.stderr == "" + + +def test_bin_mytilus_can_be_used_via_shell_env_var(): + env = os.environ.copy() + env["SHELL"] = "bin/mytilus" + result = run_mytilus("-c", "!echo hello-from-shell-env", env=env) + + assert result.returncode == 0 + assert result.stdout.splitlines() == ["hello-from-shell-env"] + + +def test_bin_mytilus_c_requires_argument(): + result = run_mytilus("-c", env=None) + + assert result.returncode == 2 + assert "mytilus: error: argument -c/--command: expected one argument" in result.stderr + + +def test_bin_mytilus_c_runs_python_without_a_tty(): + result = run_mytilus("-c", f"!{sys.executable}", env=None) + + assert result.returncode == 0 + assert result.stdout == "\n" + assert result.stderr == "" + + +def test_bin_mytilus_c_runs_python_batch_code(): + result = run_mytilus("-c", f'!{sys.executable} {{-c, "print(123)"}}', env=None) + + assert result.returncode == 0 + assert result.stdout.splitlines() == ["123"] + assert result.stderr == "" + + +def test_bin_mytilus_c_preserves_tty_for_interactive_python(): + process, master_fd = run_mytilus_pty("-c", f"!{sys.executable} -q", env=None) + + try: + assert b">>> " in read_pty_until(master_fd, b">>> ", PTY_TIMEOUT_SECONDS) + + os.write(master_fd, b"print(123)\n") + interactive_output = read_pty_until(master_fd, b">>> ", PTY_TIMEOUT_SECONDS) + + assert b"123" in interactive_output + + os.write(master_fd, b"raise SystemExit\n") + assert process.wait(timeout=5) == 0 + finally: + try: + os.close(master_fd) + except OSError: + pass + if process.poll() is None: + process.kill() + process.wait() + + +def test_bin_mytilus_i_runs_command_then_starts_repl(): + process, master_fd = run_mytilus_pty("-i", "-c", "!echo hello-from-interactive", env=None) + + try: + combined_output = read_pty_until(master_fd, b"--- !bin/yaml/shell.yaml", PTY_TIMEOUT_SECONDS) + + assert b"hello-from-interactive" in combined_output + assert b"watching for changes in current path" in combined_output + + os.write(master_fd, b"\x04") + try: + exit_output = read_pty_until(master_fd, b"\xe2\x8c\x81", 1.0) + except AssertionError: + exit_output = b"" + if exit_output: + assert b"\xe2\x8c\x81" in exit_output + assert process.wait(timeout=5) == 0 + finally: + try: + os.close(master_fd) + except OSError: + pass + if process.poll() is None: + process.kill() + process.wait() diff --git a/tests/test_bin_widish.py b/tests/test_bin_widish.py deleted file mode 100644 index a787118..0000000 --- a/tests/test_bin_widish.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import subprocess - - -def run_widish(*args, env=None): - run_env = os.environ.copy() - run_env.setdefault("MPLCONFIGDIR", "/tmp/widip-mpl") - if env is not None: - run_env.update(env) - return subprocess.run( - ["bin/widish", *args], - text=True, - capture_output=True, - check=False, - env=run_env, - ) - - -def test_bin_widish_c_executes_yaml_command(): - result = run_widish("-c", "!echo hello-from-widish") - - assert result.returncode == 0 - assert result.stdout.splitlines() == ["hello-from-widish"] - assert result.stderr == "" - - -def test_bin_widish_can_be_used_via_shell_env_var(): - env = os.environ.copy() - env["SHELL"] = "bin/widish" - result = run_widish("-c", "!echo hello-from-shell-env", env=env) - - assert result.returncode == 0 - assert result.stdout.splitlines() == ["hello-from-shell-env"] - - -def test_bin_widish_c_requires_argument(): - result = run_widish("-c") - - assert result.returncode == 2 - assert "widish: missing argument for -c" in result.stderr diff --git a/tests/test_compiler.py b/tests/test_compiler.py index b1a2a17..1fdeb8a 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,8 +1,8 @@ import pytest -from widip.comput.computer import * -from widip.comput.boxes import Data, Parallel, Partial, Sequential -from widip.comput.compile import Compile +from mytilus.comput.computer import * +from mytilus.comput.boxes import Data, Parallel, Partial, Sequential +from mytilus.comput.compile import Compile from os import path SVG_ROOT_PATH = path.join("tests", "svg") diff --git a/tests/test_hif.py b/tests/test_hif.py index 6f18c32..1b868f7 100644 --- a/tests/test_hif.py +++ b/tests/test_hif.py @@ -2,14 +2,14 @@ from nx_yaml import nx_compose_all -from widip.metaprog.hif import HIFSpecializer -from widip.state.hif import ( +from mytilus.metaprog.hif import HIFSpecializer +from mytilus.state.hif import ( document_root_node, mapping_entry_nodes, sequence_item_nodes, stream_document_nodes, ) -from widip.wire.hif import hif_node +from mytilus.wire.hif import hif_node class ShapeSpecializer(HIFSpecializer): diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 69a2746..57c6a37 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -1,5 +1,5 @@ -from widip.comput.computer import Computer, ProgramTy, Ty -from widip.pcc import ProgramClosedCategory +from mytilus.comput.computer import Computer, ProgramTy, Ty +from mytilus.pcc import ProgramClosedCategory H_ty, L_ty = ProgramTy("H"), ProgramTy("L") diff --git a/tests/test_lang.py b/tests/test_lang.py index f01c5d3..2a278cb 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,11 +1,11 @@ -from widip.comput.computer import Ty -from widip.comput import python as comput_python -from widip.comput.widish import Command, Empty, Literal, ShellProgram, io_ty, shell_program_ty -from widip.metaprog import python as metaprog_python -from widip.pcc import SHELL -from widip.state import core as state_core -from widip.state.python import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON -from widip.state.widish import ( +from mytilus.comput.computer import Ty +from mytilus.comput import python as comput_python +from mytilus.comput.mytilus import Command, Empty, Literal, ShellProgram, io_ty, shell_program_ty +from mytilus.metaprog import python as metaprog_python +from mytilus.pcc import SHELL +from mytilus.state import core as state_core +from mytilus.state.python import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON +from mytilus.state.mytilus import ( Parallel, Pipeline, ShellExecution, @@ -13,7 +13,7 @@ parallel, shell_program_runner, ) -from widip.wire.widish import Copy +from mytilus.wire.mytilus import Copy def box_names(diagram): @@ -105,7 +105,7 @@ def test_mapping_bubble_specializes_to_parallel_shell_bubble(): assert specialized.dom == io_ty assert specialized.cod == io_ty assert not any(name.startswith("('tee',") for name in names) - assert not any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert not any(name.startswith("('cat', '/tmp/mytilus-") for name in names) assert "merge[3]" not in names assert "∆" not in names @@ -175,7 +175,7 @@ def test_parallel_specializer_preserves_parallel_shell_bubble(): assert isinstance(specialized, Parallel) assert not any(name.startswith("('tee',") for name in names) - assert not any(name.startswith("('cat', '/tmp/widip-") for name in names) + assert not any(name.startswith("('cat', '/tmp/mytilus-") for name in names) assert "merge[2]" not in names assert "∆" not in names assert SHELL_INTERPRETER(program)("") == "leftright" diff --git a/tests/test_loader.py b/tests/test_loader.py index 85f68b3..761f9e0 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,18 +2,18 @@ from nx_yaml import nx_compose_all -from widip.comput.computer import Ty -from widip.comput.loader import LoaderLiteral, loader_program_ty -from widip.comput.widish import Command, Literal, io_ty, shell_program_ty -from widip.metaprog.hif import HIFToLoader -from widip.pcc import SHELL -from widip.state.core import InputOutputMap, StateUpdateMap -from widip.state.loader import LoaderExecution, LoaderToShell -from widip.state.python import SHELL_INTERPRETER -from widip.state.widish import Parallel, Pipeline -from widip.wire.hif import HyperGraph -from widip.wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_stream_ty -from widip.wire.widish import shell_id +from mytilus.comput.computer import Ty +from mytilus.comput.loader import LoaderLiteral, loader_program_ty +from mytilus.comput.mytilus import Command, Literal, io_ty, shell_program_ty +from mytilus.metaprog.hif import HIFToLoader +from mytilus.pcc import SHELL +from mytilus.state.core import InputOutputMap, StateUpdateMap +from mytilus.state.loader import LoaderExecution, LoaderToShell +from mytilus.state.python import SHELL_INTERPRETER +from mytilus.state.mytilus import Parallel, Pipeline +from mytilus.wire.hif import HyperGraph +from mytilus.wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_stream_ty +from mytilus.wire.mytilus import shell_id def test_loader_empty_stream_is_identity(): diff --git a/tests/test_metaprog.py b/tests/test_metaprog.py index a03b852..9eb3a5e 100644 --- a/tests/test_metaprog.py +++ b/tests/test_metaprog.py @@ -1,11 +1,11 @@ import pytest from nx_yaml import nx_compose_all -from widip.comput.computer import * -from widip.metaprog import SHELL_SPECIALIZER -from widip.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer -from widip.metaprog.hif import HIFToLoader -from widip.state.loader import LoaderToShell +from mytilus.comput.computer import * +from mytilus.metaprog import SHELL_SPECIALIZER +from mytilus.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer +from mytilus.metaprog.hif import HIFToLoader +from mytilus.state.loader import LoaderToShell from os import path diff --git a/tests/test_runner.py b/tests/test_runner.py index 26b0fb3..5edd5d5 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -5,15 +5,15 @@ import pytest from nx_yaml import nx_compose_all -from widip.metaprog.hif import HIFToLoader -from widip.state.loader import LoaderToShell -from widip.state.python import SHELL_INTERPRETER -from widip.state.widish import ShellSpecializer +from mytilus.metaprog.hif import HIFToLoader +from mytilus.state.loader import LoaderToShell +from mytilus.state.python import SHELL_INTERPRETER +from mytilus.state.mytilus import ShellSpecializer -FIXTURE_DIR = Path("tests/widish") +FIXTURE_DIR = Path("tests/mytilus") -os.environ.setdefault("MPLCONFIGDIR", "/tmp/widip-mpl") +os.environ.setdefault("MPLCONFIGDIR", "/tmp/mytilus-mpl") def case_paths(): @@ -26,7 +26,7 @@ def normalize_svg(svg_text: str) -> str: svg_text = re.sub(r'id="[^"]*[0-9a-f]{8,}[^"]*"', 'id="SVG_ID"', svg_text) svg_text = re.sub(r'url\(#([^)]*[0-9a-f]{8,}[^)]*)\)', 'url(#SVG_ID)', svg_text) svg_text = re.sub(r'xlink:href="#[^"]*[0-9a-f]{8,}[^"]*"', 'xlink:href="#SVG_ID"', svg_text) - svg_text = re.sub(r"/tmp/widip-[^<\" ]+\.tmp", "/tmp/WIDIP_TMP", svg_text) + svg_text = re.sub(r"/tmp/mytilus-[^<\" ]+\.tmp", "/tmp/MYTILUS_TMP", svg_text) marker_use_re = re.compile(r'^\s*\s*$', re.MULTILINE) marker_uses = iter(sorted(match.group(0).strip() for match in marker_use_re.finditer(svg_text))) svg_text = marker_use_re.sub(lambda _match: next(marker_uses), svg_text) diff --git a/tests/test_state.py b/tests/test_state.py index 32cae2c..872eaee 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,10 +1,10 @@ from discopy import python -from widip.comput import computer -from widip.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty -from widip.comput.widish import io_ty -from widip.pcc import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL -from widip.state import ( +from mytilus.comput import computer +from mytilus.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty +from mytilus.comput.mytilus import io_ty +from mytilus.pcc import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL +from mytilus.state import ( Execution, InputOutputMap, Process, @@ -13,10 +13,10 @@ fixed_state, simulate, ) -from widip.state.loader import LoaderExecution -from widip.state.widish import ShellExecution -from widip.state.python import ProcessRunner -from widip.wire.loader import loader_stream_ty +from mytilus.state.loader import LoaderExecution +from mytilus.state.mytilus import ShellExecution +from mytilus.state.python import ProcessRunner +from mytilus.wire.loader import loader_stream_ty X, Y, A, B = Ty("X"), Ty("Y"), Ty("A"), Ty("B") diff --git a/tests/test_watch.py b/tests/test_watch.py index b698025..3c54904 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -1,12 +1,15 @@ +import subprocess + import pytest from nx_yaml import nx_compose_all -import widip.watch as watch -from widip.comput.widish import Command, io_ty -from widip.metaprog.hif import HIFToLoader -from widip.pcc import SHELL -from widip.state.loader import LoaderToShell -from widip.watch import CTRL_D, CTRL_J, CTRL_M, apply_tty_input, emit_shell_source, read_shell_source, watch_log +import mytilus.watch as watch +from mytilus.comput.mytilus import Command, io_ty +from mytilus.metaprog.hif import HIFToLoader +from mytilus.pcc import SHELL +from mytilus.state.loader import LoaderToShell +from mytilus.state.mytilus import terminal_passthrough_command +from mytilus.watch import CTRL_D, CTRL_J, CTRL_M, apply_tty_input, emit_shell_source, read_shell_source, watch_log def test_apply_tty_input_uses_ctrl_j_as_newline_and_ctrl_m_as_submit(): @@ -76,29 +79,69 @@ def test_emit_shell_source_writes_a_trailing_newline(capsys): assert captured.err == "" -def test_shell_main_continues_after_invalid_command(monkeypatch, capsys): +def test_terminal_passthrough_command_extracts_top_level_command(): + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all("!echo ok\n"))) + command = terminal_passthrough_command(diagram) + + assert command == Command(["echo", "ok"]) + + +def test_execute_shell_diagram_uses_terminal_passthrough_when_available(monkeypatch): + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all("!echo ok\n"))) + seen = {} + + monkeypatch.setattr(watch, "has_interactive_terminal", lambda: True) + monkeypatch.setattr(watch, "run_terminal_command", lambda command: seen.setdefault("argv", command.argv)) + + result = watch.execute_shell_diagram(diagram, None) + + assert result is None + assert seen["argv"] == ("echo", "ok") + + +def test_execute_shell_diagram_keeps_structured_programs_captured(monkeypatch): + diagram = LoaderToShell()(HIFToLoader()(nx_compose_all("- !printf hi\n- !wc -c\n"))) + + monkeypatch.setattr(watch, "has_interactive_terminal", lambda: True) + + assert watch.execute_shell_diagram(diagram, None) == "2\n" + + +def test_terminal_passthrough_command_rejects_nested_command_substitution(): + diagram = Command(["echo", Command(["printf", "ok"])]) @ io_ty >> SHELL.execution(io_ty, io_ty).output_diagram() + + assert terminal_passthrough_command(diagram) is None + + +def test_execute_shell_diagram_keeps_nested_command_substitution_captured(monkeypatch): + diagram = Command(["echo", Command(["printf", "ok"])]) @ io_ty >> SHELL.execution(io_ty, io_ty).output_diagram() + + monkeypatch.setattr(watch, "has_interactive_terminal", lambda: True) + monkeypatch.setattr(watch, "run_terminal_command", lambda command: pytest.fail(f"unexpected passthrough for {command.argv!r}")) + + assert watch.execute_shell_diagram(diagram, None) == "ok\n" + + +def test_shell_main_propagates_invalid_command_errors(monkeypatch, capsys): class DummyObserver: def stop(self): return None sources = iter(("!git status --short\n", "!echo ok\n")) - def fake_read_shell_source(_file_name): + def fake_read_line(): try: return next(sources) except StopIteration as exc: raise EOFError from exc monkeypatch.setattr(watch, "watch_main", lambda: DummyObserver()) - monkeypatch.setattr(watch, "read_shell_source", fake_read_shell_source) + monkeypatch.setattr(watch, "default_shell_source_reader", fake_read_line) - with pytest.raises(SystemExit) as raised: + with pytest.raises(subprocess.CalledProcessError): watch.shell_main("bin/yaml/shell.yaml", draw=False) captured = capsys.readouterr() - assert raised.value.code == 0 assert "!git status --short" in captured.out - assert "!echo ok" in captured.out - assert "returned non-zero exit status" in captured.err - assert "ok" in captured.out + assert "!echo ok" not in captured.out diff --git a/tests/test_wire.py b/tests/test_wire.py index 42e7aa5..ad0d672 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -1,12 +1,12 @@ -from widip.comput.computer import Box as ComputerBox -from widip.comput.computer import Copy as ComputerCopy -from widip.comput.computer import Ty as ComputerTy -from widip.comput.widish import io_ty as shell_io_ty -from widip.wire.functions import Box -from widip.wire.loader import loader_id, loader_stream_ty -from widip.wire.services import Copy, Delete, Swap -from widip.wire.types import Diagram, Id, Ty -from widip.wire.widish import Copy as ShellCopy, shell_id +from mytilus.comput.computer import Box as ComputerBox +from mytilus.comput.computer import Copy as ComputerCopy +from mytilus.comput.computer import Ty as ComputerTy +from mytilus.comput.mytilus import io_ty as shell_io_ty +from mytilus.wire.functions import Box +from mytilus.wire.loader import loader_id, loader_stream_ty +from mytilus.wire.services import Copy, Delete, Swap +from mytilus.wire.types import Diagram, Id, Ty +from mytilus.wire.mytilus import Copy as ShellCopy, shell_id def test_wire_exports_chapter_one_primitives(): @@ -29,7 +29,7 @@ def test_loader_wire_module_exports_loader_specific_wiring(): assert loader_id().cod == loader_stream_ty -def test_widish_wire_module_exports_shell_specific_wiring(): +def test_mytilus_wire_module_exports_shell_specific_wiring(): assert shell_id().dom == shell_io_ty assert shell_id().cod == shell_io_ty assert ShellCopy(3).dom == shell_io_ty diff --git a/tests/widish/0.prog.svg b/tests/widish/0.prog.svg deleted file mode 100644 index e2193f0..0000000 --- a/tests/widish/0.prog.svg +++ /dev/null @@ -1,1776 +0,0 @@ - - - - - - - - 2026-03-17T14:36:22.984310 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/widish/04.mprog.svg b/tests/widish/04.mprog.svg deleted file mode 100644 index d321ed3..0000000 --- a/tests/widish/04.mprog.svg +++ /dev/null @@ -1,474 +0,0 @@ - - - - - - - - 2026-03-17T13:58:47.103262 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/widish/15.prog.svg b/tests/widish/15.prog.svg deleted file mode 100644 index b8c30a6..0000000 --- a/tests/widish/15.prog.svg +++ /dev/null @@ -1,1268 +0,0 @@ - - - - - - - - 2026-03-17T14:36:23.406356 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/widish/16.prog.svg b/tests/widish/16.prog.svg deleted file mode 100644 index 1be424d..0000000 --- a/tests/widish/16.prog.svg +++ /dev/null @@ -1,1322 +0,0 @@ - - - - - - - - 2026-03-17T14:36:23.496097 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/widish/17.prog.svg b/tests/widish/17.prog.svg deleted file mode 100644 index a3dbaa9..0000000 --- a/tests/widish/17.prog.svg +++ /dev/null @@ -1,17813 +0,0 @@ - - - - - - - - 2026-03-17T14:36:43.311784 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tutorial.ipynb b/tutorial.ipynb index 1176e05..30cdd9e 100644 --- a/tutorial.ipynb +++ b/tutorial.ipynb @@ -24,7 +24,7 @@ } ], "source": [ - "from widip.loader import repl_read\n", + "from mytilus.loader import repl_read\n", "\n", "repl_read(\"\"\"\n", "- a: { b, c: {} }\n", diff --git a/widip/__main__.py b/widip/__main__.py deleted file mode 100644 index eead9b3..0000000 --- a/widip/__main__.py +++ /dev/null @@ -1,70 +0,0 @@ -import sys -import argparse -import logging -import os -import tempfile - - -def configure_matplotlib_cache(): - """Set a writable default MPLCONFIGDIR when the environment does not provide one.""" - if "MPLCONFIGDIR" in os.environ: - return - - cache_dir = os.path.join(tempfile.gettempdir(), "widip-mpl") - try: - os.makedirs(cache_dir, exist_ok=True) - os.environ["MPLCONFIGDIR"] = cache_dir - except OSError: - # Fall back to Matplotlib defaults if we cannot create the cache directory. - pass - - -configure_matplotlib_cache() - -# Stop starting a Matplotlib GUI. -import matplotlib -matplotlib.use('agg') - -from .watch import shell_main, widish_main - -def build_arguments(args): - parser = argparse.ArgumentParser() - - parser.add_argument( - "-n", "--no-draw", - action="store_true", - help="Skips jpg drawing, just run the program" - ) - parser.add_argument( - "-v", "--verbose", - action="store_true", - help="Enable verbose output" - ) - parser.add_argument( - "file_name", - nargs="?", - help="The yaml file to run, if not provided it will start a shell" - ) - args = parser.parse_args(args) - return args - - -def main(argv): - args = build_arguments(argv[1:]) - draw = not args.no_draw - - logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.INFO, - format="%(levelname)s: %(message)s", - ) - - logging.debug(f"running \"{args.file_name}\" file with no-draw={args.no_draw}") - - if args.file_name is None: - logging.debug("Starting shell") - shell_main("bin/yaml/shell.yaml", draw) - else: - widish_main(args.file_name, draw) - -if __name__ == "__main__": - main(sys.argv) diff --git a/widip/watch.py b/widip/watch.py deleted file mode 100644 index 1117287..0000000 --- a/widip/watch.py +++ /dev/null @@ -1,177 +0,0 @@ -from pathlib import Path -import subprocess -import sys -import termios -import tty -from watchdog.events import FileSystemEventHandler -from watchdog.observers import Observer -from yaml import YAMLError -from nx_yaml import nx_compose_all - -from discopy.utils import tuplify, untuplify - -from .files import diagram_draw, file_diagram -from .metaprog.hif import HIFToLoader -from .state.loader import LoaderToShell -from .state.python import SHELL_INTERPRETER - - -# TODO watch functor ?? -CTRL_D = "\x04" -CTRL_J = "\x0A" -CTRL_M = "\x0D" -BACKSPACE = {"\x08", "\x7F"} - - -def apply_tty_input(buffer: list[str], char: str): - """Update the pending YAML document buffer for one TTY character.""" - if char == CTRL_D: - return ("eof" if not buffer else "submit", None) - if char == CTRL_M: - return ("submit", None) - if char == CTRL_J: - buffer.append("\n") - return ("newline", None) - if char in BACKSPACE: - removed = buffer.pop() if buffer else None - return ("backspace", removed) - buffer.append(char) - return ("char", None) - - -def read_tty_yaml_document(): - """Read one YAML document from TTY using Ctrl+J for LF and Ctrl+M to submit.""" - fd = sys.stdin.fileno() - previous = termios.tcgetattr(fd) - buffer = [] - - try: - tty.setraw(fd) - while True: - raw = sys.stdin.buffer.read(1) - if raw == b"": - raise EOFError - - char = raw.decode("latin1") - action, removed = apply_tty_input(buffer, char) - - if action == "eof": - raise EOFError - if action == "submit": - sys.stdout.write("\n") - sys.stdout.flush() - return "".join(buffer) - if action == "newline": - sys.stdout.write("\n") - sys.stdout.flush() - continue - if action == "backspace": - if removed and removed != "\n": - sys.stdout.write("\b \b") - sys.stdout.flush() - continue - - sys.stdout.write(char) - sys.stdout.flush() - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, previous) - - -def watch_log(message: str): - """Write watcher status logs to stderr.""" - print(message, file=sys.stderr) - - -def read_shell_source(file_name: str, read_line=None): - """Write command-document prompt to stdout and read one YAML document.""" - prompt = f"--- !{file_name}\n" - sys.stdout.write(prompt) - sys.stdout.flush() - if read_line is not None: - return read_line() - if sys.stdin.isatty(): - return read_tty_yaml_document() - source = sys.stdin.readline() - if source == "": - raise EOFError - return source - - -def emit_shell_source(source: str): - """Emit the executed YAML source document to stdout for transcript logging.""" - sys.stdout.write(source) - if not source.endswith("\n"): - sys.stdout.write("\n") - sys.stdout.flush() - - -class ShellHandler(FileSystemEventHandler): - """Reload the shell on change.""" - def on_modified(self, event): - if event.src_path.endswith(".yaml"): - watch_log(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: - watch_log(str(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. - watch_log("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, draw=True): - hif_to_loader = HIFToLoader() - loader_to_shell = LoaderToShell() - try: - while True: - observer = watch_main() - try: - source = read_shell_source(file_name) - emit_shell_source(source) - source_d = loader_to_shell(hif_to_loader(nx_compose_all(source))) - # source_d.draw( - # textpad=(0.3, 0.1), - # fontsize=12, - # fontsize_types=8) - path = Path(file_name) - - if draw: - diagram_draw(path, source_d) - result_ev = SHELL_INTERPRETER(source_d)("") - print(result_ev) - except KeyboardInterrupt: - print() - except YAMLError as e: - print(e) - except subprocess.CalledProcessError as error: - watch_log(str(error)) - stderr = (error.stderr or "").strip() - if stderr: - watch_log(stderr) - except FileNotFoundError as error: - watch_log(str(error)) - finally: - observer.stop() - except EOFError: - print("⌁") - exit(0) - -def widish_main(file_name, draw): - fd = file_diagram(file_name) - path = Path(file_name) - if draw: - diagram_draw(path, fd) - runner = SHELL_INTERPRETER(fd) - - run_res = runner("") if sys.stdin.isatty() else runner(sys.stdin.read()) - - print(*(tuple(x.rstrip() for x in tuplify(untuplify(run_res)) if x)), sep="\n") diff --git a/widish-command.jpg b/widish-command.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e2d5bfc28f3e2ff0abc4eaa94047a9e3b48faa6 GIT binary patch literal 5759 zcmd^Dc{G%7`@hFv?6M||B}7>YV<(kFsVpT+q%0AQU6yYeiORl3FDX(|mXVmO*=3J1 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 literal 0 HcmV?d00001 From 0aa61960da1cfd4a3af13495d61f6eb670336c8e Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sat, 21 Mar 2026 17:46:15 +0000 Subject: [PATCH 15/17] stricter module separation --- debug.py | 7 +++-- discorun/__init__.py | 1 + discorun/comput/__init__.py | 1 + {mytilus => discorun}/comput/boxes.py | 0 {mytilus => discorun}/comput/compile.py | 0 {mytilus => discorun}/comput/computer.py | 0 {mytilus => discorun}/comput/equations.py | 0 discorun/metaprog/__init__.py | 1 + {mytilus => discorun}/metaprog/core.py | 0 discorun/pcc/__init__.py | 1 + {mytilus => discorun}/pcc/core.py | 0 discorun/state/__init__.py | 1 + {mytilus => discorun}/state/core.py | 2 +- discorun/wire/__init__.py | 1 + {mytilus => discorun}/wire/functions.py | 0 {mytilus => discorun}/wire/services.py | 0 {mytilus => discorun}/wire/types.py | 0 mytilus/comput/__init__.py | 6 +--- mytilus/comput/loader.py | 2 +- mytilus/comput/mytilus.py | 2 +- mytilus/comput/python.py | 2 +- mytilus/files.py | 2 +- mytilus/metaprog/__init__.py | 8 ----- mytilus/metaprog/hif.py | 2 +- mytilus/metaprog/mytilus.py | 4 +-- mytilus/metaprog/python.py | 6 ++-- mytilus/pcc/__init__.py | 6 +--- mytilus/pcc/loader.py | 4 +-- mytilus/pcc/mytilus.py | 4 +-- mytilus/state/__init__.py | 16 +--------- mytilus/state/loader.py | 5 +-- mytilus/state/mytilus.py | 4 +-- mytilus/state/python.py | 8 ++--- mytilus/wire/loader.py | 4 +-- mytilus/wire/mytilus.py | 4 +-- pyproject.toml | 4 +-- tests/metaprog/python.py | 2 +- tests/test_compiler.py | 6 ++-- tests/test_discorun_architecture.py | 38 +++++++++++++++++++++++ tests/test_interpreter.py | 4 +-- tests/test_lang.py | 6 ++-- tests/test_loader.py | 6 ++-- tests/test_metaprog.py | 7 +++-- tests/test_state.py | 10 +++--- tests/test_watch.py | 2 +- tests/test_wire.py | 12 +++---- 46 files changed, 110 insertions(+), 91 deletions(-) create mode 100644 discorun/__init__.py create mode 100644 discorun/comput/__init__.py rename {mytilus => discorun}/comput/boxes.py (100%) rename {mytilus => discorun}/comput/compile.py (100%) rename {mytilus => discorun}/comput/computer.py (100%) rename {mytilus => discorun}/comput/equations.py (100%) create mode 100644 discorun/metaprog/__init__.py rename {mytilus => discorun}/metaprog/core.py (100%) create mode 100644 discorun/pcc/__init__.py rename {mytilus => discorun}/pcc/core.py (100%) create mode 100644 discorun/state/__init__.py rename {mytilus => discorun}/state/core.py (99%) create mode 100644 discorun/wire/__init__.py rename {mytilus => discorun}/wire/functions.py (100%) rename {mytilus => discorun}/wire/services.py (100%) rename {mytilus => discorun}/wire/types.py (100%) create mode 100644 tests/test_discorun_architecture.py diff --git a/debug.py b/debug.py index 9a2d052..37f22d7 100644 --- a/debug.py +++ b/debug.py @@ -1,13 +1,14 @@ from pathlib import Path -from mytilus.computer import Box, ComputableFunction, ProgramTy, Ty -from mytilus.metaprog import ( +from discorun.comput.computer import Box, ComputableFunction, ProgramTy, Ty +from discorun.metaprog.core import ( MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, ) -from mytilus.state import Process, ProgramClosedCategory, fixed_state, simulate +from discorun.pcc.core import ProgramClosedCategory +from discorun.state.core import Process, fixed_state, simulate def large_diagram(): diff --git a/discorun/__init__.py b/discorun/__init__.py new file mode 100644 index 0000000..6e2eac6 --- /dev/null +++ b/discorun/__init__.py @@ -0,0 +1 @@ +"""Discopy-only core abstractions for diagrammatic program execution.""" diff --git a/discorun/comput/__init__.py b/discorun/comput/__init__.py new file mode 100644 index 0000000..76fb7f0 --- /dev/null +++ b/discorun/comput/__init__.py @@ -0,0 +1 @@ +"""Core program/computation package.""" diff --git a/mytilus/comput/boxes.py b/discorun/comput/boxes.py similarity index 100% rename from mytilus/comput/boxes.py rename to discorun/comput/boxes.py diff --git a/mytilus/comput/compile.py b/discorun/comput/compile.py similarity index 100% rename from mytilus/comput/compile.py rename to discorun/comput/compile.py diff --git a/mytilus/comput/computer.py b/discorun/comput/computer.py similarity index 100% rename from mytilus/comput/computer.py rename to discorun/comput/computer.py diff --git a/mytilus/comput/equations.py b/discorun/comput/equations.py similarity index 100% rename from mytilus/comput/equations.py rename to discorun/comput/equations.py diff --git a/discorun/metaprog/__init__.py b/discorun/metaprog/__init__.py new file mode 100644 index 0000000..e2f8762 --- /dev/null +++ b/discorun/metaprog/__init__.py @@ -0,0 +1 @@ +"""Language-agnostic metaprogram package.""" diff --git a/mytilus/metaprog/core.py b/discorun/metaprog/core.py similarity index 100% rename from mytilus/metaprog/core.py rename to discorun/metaprog/core.py diff --git a/discorun/pcc/__init__.py b/discorun/pcc/__init__.py new file mode 100644 index 0000000..c8104ad --- /dev/null +++ b/discorun/pcc/__init__.py @@ -0,0 +1 @@ +"""Program-closed category package.""" diff --git a/mytilus/pcc/core.py b/discorun/pcc/core.py similarity index 100% rename from mytilus/pcc/core.py rename to discorun/pcc/core.py diff --git a/discorun/state/__init__.py b/discorun/state/__init__.py new file mode 100644 index 0000000..9a5adf5 --- /dev/null +++ b/discorun/state/__init__.py @@ -0,0 +1 @@ +"""Generic stateful process package.""" diff --git a/mytilus/state/core.py b/discorun/state/core.py similarity index 99% rename from mytilus/state/core.py rename to discorun/state/core.py index ff9412e..0c1b3ee 100644 --- a/mytilus/state/core.py +++ b/discorun/state/core.py @@ -4,7 +4,7 @@ from ..comput import computer from ..metaprog import core as metaprog_core -from ..pcc import ProgramClosedCategory +from ..pcc.core import ProgramClosedCategory class StateUpdateMap(computer.Box): diff --git a/discorun/wire/__init__.py b/discorun/wire/__init__.py new file mode 100644 index 0000000..a02d104 --- /dev/null +++ b/discorun/wire/__init__.py @@ -0,0 +1 @@ +"""Core wire calculus for diagrammatic programs.""" diff --git a/mytilus/wire/functions.py b/discorun/wire/functions.py similarity index 100% rename from mytilus/wire/functions.py rename to discorun/wire/functions.py diff --git a/mytilus/wire/services.py b/discorun/wire/services.py similarity index 100% rename from mytilus/wire/services.py rename to discorun/wire/services.py diff --git a/mytilus/wire/types.py b/discorun/wire/types.py similarity index 100% rename from mytilus/wire/types.py rename to discorun/wire/types.py diff --git a/mytilus/comput/__init__.py b/mytilus/comput/__init__.py index a8b73fc..c3ef6f5 100644 --- a/mytilus/comput/__init__.py +++ b/mytilus/comput/__init__.py @@ -1,5 +1 @@ -"""Chapter 2 computing structures and language-specific program constants.""" - -from . import computer -from .loader import loader_program_ty -from .mytilus import shell_program_ty +"""Mytilus compute package.""" diff --git a/mytilus/comput/loader.py b/mytilus/comput/loader.py index f27ca0f..5593132 100644 --- a/mytilus/comput/loader.py +++ b/mytilus/comput/loader.py @@ -1,6 +1,6 @@ """Loader-language program constants.""" -from . import computer +from discorun.comput import computer loader_program_ty = computer.ProgramTy("yaml") diff --git a/mytilus/comput/mytilus.py b/mytilus/comput/mytilus.py index 7cc1393..82a8ccc 100644 --- a/mytilus/comput/mytilus.py +++ b/mytilus/comput/mytilus.py @@ -1,6 +1,6 @@ """Shell-language program constants.""" -from . import computer +from discorun.comput import computer from ..wire.mytilus import io_ty diff --git a/mytilus/comput/python.py b/mytilus/comput/python.py index adc86e0..5fb969a 100644 --- a/mytilus/comput/python.py +++ b/mytilus/comput/python.py @@ -1,6 +1,6 @@ from discopy import closed, markov, monoidal -from . import computer +from discorun.comput import computer program_ty = computer.ProgramTy("python") diff --git a/mytilus/files.py b/mytilus/files.py index fcdbe05..1f0364b 100644 --- a/mytilus/files.py +++ b/mytilus/files.py @@ -3,7 +3,7 @@ from nx_yaml import nx_compose_all -from .comput.computer import Box, Diagram +from discorun.comput.computer import Box, Diagram from .metaprog.hif import HIFToLoader from .state.loader import LoaderToShell diff --git a/mytilus/metaprog/__init__.py b/mytilus/metaprog/__init__.py index a986dee..81546a8 100644 --- a/mytilus/metaprog/__init__.py +++ b/mytilus/metaprog/__init__.py @@ -2,11 +2,3 @@ Chapter 6. Computing programs. Metaprograms are programs that compute programs. """ - -from . import core -from . import mytilus as metaprog_mytilus - - -SHELL_SPECIALIZER = metaprog_mytilus.ShellSpecializer() -PROGRAM_FUNCTOR = core.ProgramFunctor() -METAPROGRAM_FUNCTOR = core.MetaprogramFunctor() diff --git a/mytilus/metaprog/hif.py b/mytilus/metaprog/hif.py index 118577e..75c9bff 100644 --- a/mytilus/metaprog/hif.py +++ b/mytilus/metaprog/hif.py @@ -1,6 +1,6 @@ """HIF-specific specializers and lowerings.""" -from ..comput.computer import Ty +from discorun.comput.computer import Ty from ..wire.hif import HyperGraph, hif_edge_incidences, hif_node, hif_node_incidences from ..wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_id, pipeline diff --git a/mytilus/metaprog/mytilus.py b/mytilus/metaprog/mytilus.py index 66796b4..6c19df0 100644 --- a/mytilus/metaprog/mytilus.py +++ b/mytilus/metaprog/mytilus.py @@ -2,8 +2,8 @@ from discopy import monoidal -from .core import Specializer -from ..comput import computer +from discorun.metaprog.core import Specializer +from discorun.comput import computer from ..comput import mytilus as shell_lang from ..wire import mytilus as shell_wire diff --git a/mytilus/metaprog/python.py b/mytilus/metaprog/python.py index 05e377b..fcd858e 100644 --- a/mytilus/metaprog/python.py +++ b/mytilus/metaprog/python.py @@ -7,10 +7,10 @@ from discopy import python from discopy.utils import tuplify, untuplify -from ..comput import computer +from discorun.comput import computer +from discorun.pcc.core import ProgramClosedCategory +from discorun.metaprog import core as metaprog_core from ..comput import python as comput_python -from ..pcc import ProgramClosedCategory -from . import core as metaprog_core PYTHON_PROGRAMS = ProgramClosedCategory(comput_python.program_ty) diff --git a/mytilus/pcc/__init__.py b/mytilus/pcc/__init__.py index c797f9b..9c47d6c 100644 --- a/mytilus/pcc/__init__.py +++ b/mytilus/pcc/__init__.py @@ -1,5 +1 @@ -"""Chapter 8: Program-closed categories (PCC).""" - -from .core import MonoidalComputer, ProgramClosedCategory -from .loader import LOADER, LoaderLanguage -from .mytilus import SHELL, ShellLanguage +"""Mytilus-specific program-closed category package.""" diff --git a/mytilus/pcc/loader.py b/mytilus/pcc/loader.py index 73ed14b..732b745 100644 --- a/mytilus/pcc/loader.py +++ b/mytilus/pcc/loader.py @@ -1,8 +1,8 @@ """Program-closed category for the YAML loader language.""" -from ..comput import computer +from discorun.comput import computer from ..comput.loader import loader_program_ty -from .core import ProgramClosedCategory +from discorun.pcc.core import ProgramClosedCategory class LoaderLanguage(ProgramClosedCategory): diff --git a/mytilus/pcc/mytilus.py b/mytilus/pcc/mytilus.py index 8c31429..7924f61 100644 --- a/mytilus/pcc/mytilus.py +++ b/mytilus/pcc/mytilus.py @@ -1,8 +1,8 @@ """Program-closed category with shell as distinguished language.""" -from ..comput import computer +from discorun.comput import computer from ..comput.mytilus import shell_program_ty -from .core import ProgramClosedCategory +from discorun.pcc.core import ProgramClosedCategory class ShellLanguage(ProgramClosedCategory): diff --git a/mytilus/state/__init__.py b/mytilus/state/__init__.py index 9b3601f..58decb2 100644 --- a/mytilus/state/__init__.py +++ b/mytilus/state/__init__.py @@ -1,15 +1 @@ -"""Chapter 7: Stateful computing.""" - -from .core import ( - Execution, - InputOutputMap, - Process, - ProcessSimulation, - ProcessRunner, - StateUpdateMap, - execute, - fixed_state, - simulate, -) -from .loader import LoaderExecution -from .mytilus import ShellExecution +"""Mytilus-specific state package.""" diff --git a/mytilus/state/loader.py b/mytilus/state/loader.py index c81d34c..33e9e16 100644 --- a/mytilus/state/loader.py +++ b/mytilus/state/loader.py @@ -3,11 +3,12 @@ from ..comput import loader as loader_lang from ..comput.loader import loader_program_ty from ..comput import mytilus as shell_lang -from ..pcc import LOADER, SHELL +from ..pcc.loader import LOADER +from ..pcc.mytilus import SHELL from ..wire import loader as loader_wire from ..wire.loader import loader_stream_ty from ..wire import mytilus as shell_wire -from .core import Execution, ProcessSimulation +from discorun.state.core import Execution, ProcessSimulation from .mytilus import Parallel, Pipeline, SubstitutionParallel, SubstitutionPipeline diff --git a/mytilus/state/mytilus.py b/mytilus/state/mytilus.py index 70e1a02..d88248b 100644 --- a/mytilus/state/mytilus.py +++ b/mytilus/state/mytilus.py @@ -2,10 +2,10 @@ import subprocess -from ..comput import computer +from discorun.comput import computer from ..comput import mytilus as shell_lang from ..metaprog import mytilus as metaprog_mytilus -from .core import Execution, InputOutputMap +from discorun.state.core import Execution, InputOutputMap from ..wire import mytilus as shell_wire diff --git a/mytilus/state/python.py b/mytilus/state/python.py index e521ff8..3c6947e 100644 --- a/mytilus/state/python.py +++ b/mytilus/state/python.py @@ -4,12 +4,12 @@ from discopy import monoidal, python -from ..comput import computer +from discorun.comput import computer from ..comput import mytilus as shell_lang -from ..metaprog import core as metaprog_core +from discorun.metaprog import core as metaprog_core from ..metaprog import python as metaprog_python -from ..pcc import SHELL -from . import core as state_core +from ..pcc.mytilus import SHELL +from discorun.state import core as state_core from .mytilus import Parallel as ShellParallel from .mytilus import Pipeline as ShellPipeline from .mytilus import ShellSpecializer, shell_program_runner diff --git a/mytilus/wire/loader.py b/mytilus/wire/loader.py index bd2102f..deb4a3c 100644 --- a/mytilus/wire/loader.py +++ b/mytilus/wire/loader.py @@ -2,8 +2,8 @@ from discopy import monoidal -from .functions import Box -from .types import Id, Ty +from discorun.wire.functions import Box +from discorun.wire.types import Id, Ty loader_stream_ty = Ty("yaml_stream") diff --git a/mytilus/wire/mytilus.py b/mytilus/wire/mytilus.py index d5ddc46..9aeb5b6 100644 --- a/mytilus/wire/mytilus.py +++ b/mytilus/wire/mytilus.py @@ -1,7 +1,7 @@ """Shell-specific wire combinators and structural boxes.""" -from .services import Copy as CopyService, Delete -from .types import Id, Ty +from discorun.wire.services import Copy as CopyService, Delete +from discorun.wire.types import Id, Ty io_ty = Ty("io") diff --git a/pyproject.toml b/pyproject.toml index 09761fe..8973ee6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,8 @@ requires = ["hatchling"] build-backend = "hatchling.build" -[tool.hatch.build.targets.wheel] -packages = ["mytilus"] +[tool.hatch.build.targets.wheel] +packages = ["mytilus", "discorun"] [project] name = "mytilus" diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py index 13e9b4e..b59e5c7 100644 --- a/tests/metaprog/python.py +++ b/tests/metaprog/python.py @@ -1,6 +1,6 @@ """Diagram tests for Sec. 6.2.2 and Futamura projections.""" -from mytilus.comput import computer +from discorun.comput import computer from mytilus.comput import python as comput_python from mytilus.metaprog.python import ( PYTHON_COMPILER, diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 1fdeb8a..0773235 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,8 +1,8 @@ import pytest -from mytilus.comput.computer import * -from mytilus.comput.boxes import Data, Parallel, Partial, Sequential -from mytilus.comput.compile import Compile +from discorun.comput.computer import * +from discorun.comput.boxes import Data, Parallel, Partial, Sequential +from discorun.comput.compile import Compile from os import path SVG_ROOT_PATH = path.join("tests", "svg") diff --git a/tests/test_discorun_architecture.py b/tests/test_discorun_architecture.py new file mode 100644 index 0000000..27a78a5 --- /dev/null +++ b/tests/test_discorun_architecture.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import ast +from pathlib import Path + +from discorun.comput.computer import Box, Computer, ComputableFunction, Program + + +def iter_discorun_python_files() -> list[Path]: + root = Path("discorun") + return sorted(path for path in root.rglob("*.py") if "__pycache__" not in path.parts) + + +def test_discorun_only_depends_on_discopy_or_itself(): + for path in iter_discorun_python_files(): + module = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(module): + if isinstance(node, ast.Import): + for name in node.names: + assert not name.name.startswith( + "mytilus" + ), f"{path} must not depend on mytilus ({name.name!r})" + assert name.name == "discopy", f"{path} imports external module {name.name!r}" + if isinstance(node, ast.ImportFrom): + if node.level > 0: + continue + assert not (node.module or "").startswith( + "mytilus" + ), f"{path} must not depend on mytilus ({node.module!r})" + assert node.module and node.module.startswith( + "discopy" + ), f"{path} imports external module {node.module!r}" + + +def test_program_abstractions_are_diagrammatic_boxes(): + assert issubclass(Program, Box) + assert issubclass(ComputableFunction, Box) + assert issubclass(Computer, Box) diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py index 57c6a37..d99b732 100644 --- a/tests/test_interpreter.py +++ b/tests/test_interpreter.py @@ -1,5 +1,5 @@ -from mytilus.comput.computer import Computer, ProgramTy, Ty -from mytilus.pcc import ProgramClosedCategory +from discorun.comput.computer import Computer, ProgramTy, Ty +from discorun.pcc.core import ProgramClosedCategory H_ty, L_ty = ProgramTy("H"), ProgramTy("L") diff --git a/tests/test_lang.py b/tests/test_lang.py index 2a278cb..8b0309d 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,9 +1,9 @@ -from mytilus.comput.computer import Ty +from discorun.comput.computer import Ty from mytilus.comput import python as comput_python from mytilus.comput.mytilus import Command, Empty, Literal, ShellProgram, io_ty, shell_program_ty from mytilus.metaprog import python as metaprog_python -from mytilus.pcc import SHELL -from mytilus.state import core as state_core +from mytilus.pcc.mytilus import SHELL +from discorun.state import core as state_core from mytilus.state.python import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON from mytilus.state.mytilus import ( Parallel, diff --git a/tests/test_loader.py b/tests/test_loader.py index 761f9e0..275589b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,12 +2,12 @@ from nx_yaml import nx_compose_all -from mytilus.comput.computer import Ty +from discorun.comput.computer import Ty from mytilus.comput.loader import LoaderLiteral, loader_program_ty from mytilus.comput.mytilus import Command, Literal, io_ty, shell_program_ty from mytilus.metaprog.hif import HIFToLoader -from mytilus.pcc import SHELL -from mytilus.state.core import InputOutputMap, StateUpdateMap +from mytilus.pcc.mytilus import SHELL +from discorun.state.core import InputOutputMap, StateUpdateMap from mytilus.state.loader import LoaderExecution, LoaderToShell from mytilus.state.python import SHELL_INTERPRETER from mytilus.state.mytilus import Parallel, Pipeline diff --git a/tests/test_metaprog.py b/tests/test_metaprog.py index 9eb3a5e..86426e1 100644 --- a/tests/test_metaprog.py +++ b/tests/test_metaprog.py @@ -1,9 +1,9 @@ import pytest from nx_yaml import nx_compose_all -from mytilus.comput.computer import * -from mytilus.metaprog import SHELL_SPECIALIZER -from mytilus.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer +from discorun.comput.computer import * +from mytilus.metaprog.mytilus import ShellSpecializer +from discorun.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer from mytilus.metaprog.hif import HIFToLoader from mytilus.state.loader import LoaderToShell from os import path @@ -38,6 +38,7 @@ def after_each_test(request): l_ev = ComputableFunction("{L}", X, A, B) H_to_L = ProgramComputation("H", L_ty, X, A, B) L_to_H = ProgramComputation("L", H_ty, X, A, B) +SHELL_SPECIALIZER = ShellSpecializer() def test_sec_6_2_2(request): diff --git a/tests/test_state.py b/tests/test_state.py index 872eaee..631f252 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,10 +1,12 @@ from discopy import python -from mytilus.comput import computer -from mytilus.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty +from discorun.comput import computer +from discorun.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty from mytilus.comput.mytilus import io_ty -from mytilus.pcc import LOADER, MonoidalComputer, ProgramClosedCategory, SHELL -from mytilus.state import ( +from discorun.pcc.core import MonoidalComputer, ProgramClosedCategory +from mytilus.pcc.loader import LOADER +from mytilus.pcc.mytilus import SHELL +from discorun.state.core import ( Execution, InputOutputMap, Process, diff --git a/tests/test_watch.py b/tests/test_watch.py index 3c54904..18b9f60 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -6,7 +6,7 @@ import mytilus.watch as watch from mytilus.comput.mytilus import Command, io_ty from mytilus.metaprog.hif import HIFToLoader -from mytilus.pcc import SHELL +from mytilus.pcc.mytilus import SHELL from mytilus.state.loader import LoaderToShell from mytilus.state.mytilus import terminal_passthrough_command from mytilus.watch import CTRL_D, CTRL_J, CTRL_M, apply_tty_input, emit_shell_source, read_shell_source, watch_log diff --git a/tests/test_wire.py b/tests/test_wire.py index ad0d672..802c776 100644 --- a/tests/test_wire.py +++ b/tests/test_wire.py @@ -1,11 +1,11 @@ -from mytilus.comput.computer import Box as ComputerBox -from mytilus.comput.computer import Copy as ComputerCopy -from mytilus.comput.computer import Ty as ComputerTy +from discorun.comput.computer import Box as ComputerBox +from discorun.comput.computer import Copy as ComputerCopy +from discorun.comput.computer import Ty as ComputerTy from mytilus.comput.mytilus import io_ty as shell_io_ty -from mytilus.wire.functions import Box +from discorun.wire.functions import Box from mytilus.wire.loader import loader_id, loader_stream_ty -from mytilus.wire.services import Copy, Delete, Swap -from mytilus.wire.types import Diagram, Id, Ty +from discorun.wire.services import Copy, Delete, Swap +from discorun.wire.types import Diagram, Id, Ty from mytilus.wire.mytilus import Copy as ShellCopy, shell_id From 5e85bd4537954ac3db19e0f21adfe3b1c0672089 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sun, 22 Mar 2026 15:45:38 +0000 Subject: [PATCH 16/17] doc: interprocess modularization --- tests/yaml/README.md | 23 +++++++++++++++++++++++ tests/yaml/hello-bin.yaml | 2 ++ tests/yaml/run-hello.yaml | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 tests/yaml/README.md create mode 100755 tests/yaml/hello-bin.yaml create mode 100644 tests/yaml/run-hello.yaml diff --git a/tests/yaml/README.md b/tests/yaml/README.md new file mode 100644 index 0000000..7e49bb3 --- /dev/null +++ b/tests/yaml/README.md @@ -0,0 +1,23 @@ +# Process calling + +Building a Mytilus codebase using YAML involves well-known Operating System integrations such as executable processes. + +## run-hello -> hello + +Consider hello.yaml and run-hello.yaml. We've set hello-bin.yaml file so that: +* it has a shebang pointing to mytilus binary +* it is executable with `chmod +x hello-bin.yaml` + +The `!tests/yaml/hello-bin.yaml` tag and file configuration is a cohesive strategy that implements import-like behavior with no additional cost. The tradeoff of requiring explicit tracking of executable files is reinterpreted as a security policy in the context of an agentic shell. + +Now `bin/mytilus tests/yaml/run-hello.yaml` uses a process call. +```yaml +? !tests/yaml/hello-bin.yaml +? " World!" +``` + +This is equivalent to the following Bash script. +```sh +tests/yaml/hello-bin.yaml +echo " World!" +``` \ No newline at end of file diff --git a/tests/yaml/hello-bin.yaml b/tests/yaml/hello-bin.yaml new file mode 100755 index 0000000..912dd65 --- /dev/null +++ b/tests/yaml/hello-bin.yaml @@ -0,0 +1,2 @@ +#!bin/mytilus +Hello \ No newline at end of file diff --git a/tests/yaml/run-hello.yaml b/tests/yaml/run-hello.yaml new file mode 100644 index 0000000..f163f8e --- /dev/null +++ b/tests/yaml/run-hello.yaml @@ -0,0 +1,2 @@ +? !tests/yaml/hello-bin.yaml +? " World!" \ No newline at end of file From a5732f240d47a849af672bcb48dbe0d5fd1feac1 Mon Sep 17 00:00:00 2001 From: Agent Widip Date: Sun, 22 Mar 2026 18:27:52 +0000 Subject: [PATCH 17/17] refactor xl --- discorun/metaprog/core.py | 34 ++-- discorun/pcc/core.py | 14 +- discorun/state/core.py | 8 +- {tests => discorun}/test_compiler.py | 0 .../test_discorun_architecture.py | 12 +- {tests => discorun}/test_interpreter.py | 24 ++- {tests => discorun}/test_metaprog.py | 18 +- {tests => discorun}/test_state.py | 85 ++++---- {tests => discorun}/test_wire.py | 15 -- discorun/wire/services.py | 45 ++++- mytilus/SKILL.md | 15 ++ mytilus/comput/python.py | 151 +++++++++++--- mytilus/comput/test_python.py | 44 +++++ mytilus/metaprog/__init__.py | 29 +++ mytilus/metaprog/mytilus.py | 2 +- mytilus/metaprog/python.py | 184 ++---------------- mytilus/metaprog/test_python.py | 23 +++ mytilus/pcc/__init__.py | 7 + mytilus/pcc/loader.py | 3 - mytilus/pcc/mytilus.py | 3 - mytilus/state/__init__.py | 14 ++ mytilus/state/loader.py | 12 +- mytilus/state/python.py | 112 +++++------ mytilus/state/test_python.py | 17 ++ mytilus/watch.py | 13 +- tests/metaprog/python.py | 79 +++++--- tests/test_bin_mytilus.py | 18 +- tests/test_lang.py | 7 +- tests/test_loader.py | 4 +- tests/test_metaprog_integration.py | 21 ++ tests/test_mytilus_architecture.py | 181 +++++++++++++++++ tests/test_runner.py | 2 +- tests/test_state_integration.py | 16 ++ tests/test_watch.py | 2 +- tests/test_wire_integration.py | 15 ++ 35 files changed, 812 insertions(+), 417 deletions(-) rename {tests => discorun}/test_compiler.py (100%) rename {tests => discorun}/test_discorun_architecture.py (72%) rename {tests => discorun}/test_interpreter.py (59%) rename {tests => discorun}/test_metaprog.py (79%) rename {tests => discorun}/test_state.py (71%) rename {tests => discorun}/test_wire.py (54%) create mode 100644 mytilus/comput/test_python.py create mode 100644 mytilus/metaprog/test_python.py create mode 100644 mytilus/state/test_python.py create mode 100644 tests/test_metaprog_integration.py create mode 100644 tests/test_mytilus_architecture.py create mode 100644 tests/test_state_integration.py create mode 100644 tests/test_wire_integration.py diff --git a/discorun/metaprog/core.py b/discorun/metaprog/core.py index 1552e4e..77f2785 100644 --- a/discorun/metaprog/core.py +++ b/discorun/metaprog/core.py @@ -36,22 +36,27 @@ def metaprogram_dom(self): def __init__(self, *, dom=None, cod=None): Functor.__init__( self, - self.object, - self.ar_map, + self._identity_object, + self._identity_arrow, dom=Functor.dom if dom is None else dom, cod=Functor.cod if cod is None else cod, ) - def object(self, ob): + def _identity_object(self, ob): del self return ob - def ar_map(self, ar): + def _identity_arrow(self, ar): del self return ar - def specialize(self, *args, **kwargs): - return self(*args, **kwargs) + def __call__(self, other): + if isinstance(other, SpecializerBox): + return self.specialize(other) + return super().__call__(other) + + def specialize(self, other): + return other class Interpreter(Functor): @@ -64,22 +69,27 @@ def metaprogram_dom(self): def __init__(self, *, dom=None, cod=None): Functor.__init__( self, - self.object, - self.ar_map, + self._identity_object, + self._identity_arrow, dom=Functor.dom if dom is None else dom, cod=Functor.cod if cod is None else cod, ) - def object(self, ob): + def _identity_object(self, ob): del self return ob - def ar_map(self, ar): + def _identity_arrow(self, ar): del self return ar - def interpret(self, *args, **kwargs): - return self(*args, **kwargs) + def __call__(self, other): + if isinstance(other, InterpreterBox): + return self.interpret(other) + return super().__call__(other) + + def interpret(self, other): + return other class ProgramComputation(Diagram): diff --git a/discorun/pcc/core.py b/discorun/pcc/core.py index 54be0e2..12ff628 100644 --- a/discorun/pcc/core.py +++ b/discorun/pcc/core.py @@ -19,7 +19,8 @@ def __init__(self, program_ty: computer.ProgramTy): MonoidalComputer.__init__(self) def evaluator(self, A: computer.Ty, B: computer.Ty): - return computer.Computer(self.program_ty, A, B) + # Eq. 7.3 / Eq. 8.1 interface: evaluator is the output projection of execution. + return self.execution(A, B).output_diagram() def run(self, A: computer.Ty, B: computer.Ty): """Sec. 8.3 c'': program execution machine ``Run``.""" @@ -33,7 +34,16 @@ def is_program(self, ob): def is_evaluator(self, arrow): """Check whether ``arrow`` is this category's evaluator box.""" - return isinstance(arrow, computer.Computer) and self.is_program(arrow.P) + if isinstance(arrow, computer.Computer) and self.is_program(arrow.P): + return True + return ( + getattr(arrow, "process_name", None) is not None + and self.is_program(getattr(arrow, "X", None)) + and hasattr(arrow, "A") + and hasattr(arrow, "B") + and getattr(arrow, "dom", None) == arrow.X @ arrow.A + and getattr(arrow, "cod", None) == arrow.B + ) def _simulate_type(self, ty, codomain: "ProgramClosedCategory"): """Transport program occurrences in a type along a language simulation.""" diff --git a/discorun/state/core.py b/discorun/state/core.py index 0c1b3ee..571c086 100644 --- a/discorun/state/core.py +++ b/discorun/state/core.py @@ -77,7 +77,7 @@ def universal_ev(self): """ Eq. 7.3: program execution is the evaluator with output type P x B. """ - return fixed_state(computer.Computer(self.X, self.A, self.B)) + return fixed_state(self.output_diagram()) def specialize(self): return self.universal_ev() @@ -171,10 +171,10 @@ def out(self, output: InputOutputMap): self.simulation(output.B), ) - def object(self, ob): + def _identity_object(self, ob): return self.simulation(ob) - def ar_map(self, ar): + def _identity_arrow(self, ar): if isinstance(ar, StateUpdateMap): return self.sta(ar) if isinstance(ar, InputOutputMap): @@ -201,7 +201,7 @@ def execute(Q: computer.Diagram, A: computer.Ty, B: computer.Ty): """ Sec. 7.3: execute an X-parameterized program as a stateful process. """ - stateful_evaluator = fixed_state(ProgramClosedCategory(Q.cod).evaluator(A, B)) + stateful_evaluator = ProgramClosedCategory(Q.cod).execution(A, B).specialize() return Q @ A >> simulate(stateful_evaluator, computer.Id(Q.cod)) diff --git a/tests/test_compiler.py b/discorun/test_compiler.py similarity index 100% rename from tests/test_compiler.py rename to discorun/test_compiler.py diff --git a/tests/test_discorun_architecture.py b/discorun/test_discorun_architecture.py similarity index 72% rename from tests/test_discorun_architecture.py rename to discorun/test_discorun_architecture.py index 27a78a5..5159117 100644 --- a/tests/test_discorun_architecture.py +++ b/discorun/test_discorun_architecture.py @@ -8,7 +8,11 @@ def iter_discorun_python_files() -> list[Path]: root = Path("discorun") - return sorted(path for path in root.rglob("*.py") if "__pycache__" not in path.parts) + return sorted( + path + for path in root.rglob("*.py") + if "__pycache__" not in path.parts and not path.name.startswith("test_") + ) def test_discorun_only_depends_on_discopy_or_itself(): @@ -17,16 +21,10 @@ def test_discorun_only_depends_on_discopy_or_itself(): for node in ast.walk(module): if isinstance(node, ast.Import): for name in node.names: - assert not name.name.startswith( - "mytilus" - ), f"{path} must not depend on mytilus ({name.name!r})" assert name.name == "discopy", f"{path} imports external module {name.name!r}" if isinstance(node, ast.ImportFrom): if node.level > 0: continue - assert not (node.module or "").startswith( - "mytilus" - ), f"{path} must not depend on mytilus ({node.module!r})" assert node.module and node.module.startswith( "discopy" ), f"{path} imports external module {node.module!r}" diff --git a/tests/test_interpreter.py b/discorun/test_interpreter.py similarity index 59% rename from tests/test_interpreter.py rename to discorun/test_interpreter.py index d99b732..e28cf67 100644 --- a/tests/test_interpreter.py +++ b/discorun/test_interpreter.py @@ -1,4 +1,4 @@ -from discorun.comput.computer import Computer, ProgramTy, Ty +from discorun.comput.computer import ProgramTy, Ty from discorun.pcc.core import ProgramClosedCategory @@ -7,18 +7,24 @@ def test_high_level_interpreter_is_typed_evaluator(): A, B = Ty("A"), Ty("B") - evaluator = ProgramClosedCategory(H_ty).evaluator(A, B) - assert isinstance(evaluator, Computer) - assert evaluator.dom == Computer(H_ty, A, B).dom - assert evaluator.cod == Computer(H_ty, A, B).cod + category = ProgramClosedCategory(H_ty) + evaluator = category.evaluator(A, B) + execution = category.execution(A, B) + + assert evaluator == execution.output_diagram() + assert evaluator.dom == H_ty @ A + assert evaluator.cod == B def test_low_level_interpreter_is_typed_evaluator(): A, B = Ty("A"), Ty("B") - evaluator = ProgramClosedCategory(L_ty).evaluator(A, B) - assert isinstance(evaluator, Computer) - assert evaluator.dom == Computer(L_ty, A, B).dom - assert evaluator.cod == Computer(L_ty, A, B).cod + category = ProgramClosedCategory(L_ty) + evaluator = category.evaluator(A, B) + execution = category.execution(A, B) + + assert evaluator == execution.output_diagram() + assert evaluator.dom == L_ty @ A + assert evaluator.cod == B def test_program_closed_simulation_transports_program_types_and_evaluators(): diff --git a/tests/test_metaprog.py b/discorun/test_metaprog.py similarity index 79% rename from tests/test_metaprog.py rename to discorun/test_metaprog.py index 86426e1..bf8a1d0 100644 --- a/tests/test_metaprog.py +++ b/discorun/test_metaprog.py @@ -1,11 +1,7 @@ import pytest -from nx_yaml import nx_compose_all from discorun.comput.computer import * -from mytilus.metaprog.mytilus import ShellSpecializer from discorun.metaprog.core import MetaprogramComputation, MetaprogramFunctor, ProgramComputation, ProgramFunctor, Specializer -from mytilus.metaprog.hif import HIFToLoader -from mytilus.state.loader import LoaderToShell from os import path @@ -38,7 +34,6 @@ def after_each_test(request): l_ev = ComputableFunction("{L}", X, A, B) H_to_L = ProgramComputation("H", L_ty, X, A, B) L_to_H = ProgramComputation("L", H_ty, X, A, B) -SHELL_SPECIALIZER = ShellSpecializer() def test_sec_6_2_2(request): @@ -86,13 +81,8 @@ def test_fig_6_3_eq_1(request): def test_specializers_are_unit_metaprograms_with_partial_evaluators(request): request.node.draw_objects = (h_ev, l_ev, H_to_L) - graph = nx_compose_all("a") - loader_to_shell = LoaderToShell() + specializer = Specializer() - assert Specializer().metaprogram_dom() == Ty() - assert HIFToLoader().metaprogram_dom() == Ty() - assert loader_to_shell.metaprogram_dom() == Ty() - assert SHELL_SPECIALIZER.metaprogram_dom() == Ty() - assert isinstance(loader_to_shell, Specializer) - assert isinstance(SHELL_SPECIALIZER, Specializer) - assert HIFToLoader().specialize(graph) == HIFToLoader()(graph) + assert specializer.metaprogram_dom() == Ty() + assert specializer(A @ B) == A @ B + assert specializer.specialize(H_to_L) == H_to_L diff --git a/tests/test_state.py b/discorun/test_state.py similarity index 71% rename from tests/test_state.py rename to discorun/test_state.py index 631f252..0ba145d 100644 --- a/tests/test_state.py +++ b/discorun/test_state.py @@ -2,23 +2,17 @@ from discorun.comput import computer from discorun.comput.computer import Box, ComputableFunction, Computer, Copy, Program, ProgramTy, Ty -from mytilus.comput.mytilus import io_ty from discorun.pcc.core import MonoidalComputer, ProgramClosedCategory -from mytilus.pcc.loader import LOADER -from mytilus.pcc.mytilus import SHELL from discorun.state.core import ( Execution, InputOutputMap, Process, + ProcessRunner, StateUpdateMap, execute, fixed_state, simulate, ) -from mytilus.state.loader import LoaderExecution -from mytilus.state.mytilus import ShellExecution -from mytilus.state.python import ProcessRunner -from mytilus.wire.loader import loader_stream_ty X, Y, A, B = Ty("X"), Ty("Y"), Ty("A"), Ty("B") @@ -26,6 +20,34 @@ H_ty, L_ty = ProgramTy("H"), ProgramTy("L") +class DummyRunner(ProcessRunner): + def __init__(self): + ProcessRunner.__init__(self, cod=python.Category()) + + def object(self, ob): + del ob + return object + + def process_ar_map(self, box, dom, cod): + del box + return python.Function(lambda *_xs: None, dom, cod) + + def state_update_ar(self, dom, cod): + return python.Function(lambda state, _input: state, dom, cod) + + def output_ar(self, dom, cod): + return python.Function(lambda state, input_value: state(input_value), dom, cod) + + def map_structural(self, box, dom, cod): + if isinstance(box, Copy): + return python.Function(lambda value: (value, value), dom, cod) + if isinstance(box, computer.Delete): + return python.Function(lambda _value: (), dom, cod) + if isinstance(box, computer.Swap): + return python.Function(lambda left, right: (right, left), dom, cod) + return None + + def test_eq_7_1_process_is_a_pair_of_functions(): q = Process("q", X, A, B) expected = Copy(X @ A) >> q.state_update_diagram() @ q.output_diagram() @@ -78,13 +100,27 @@ def test_sec_7_3_program_execution_is_stateful(): assert execution.dom == P @ A assert execution.cod == P @ B - assert execution.universal_ev() == fixed_state(Computer(P, A, B)) + assert execution.universal_ev() == fixed_state(execution.output_diagram()) assert execution.state_update_diagram() == StateUpdateMap("{}", P, A) assert execution.output_diagram() == InputOutputMap("{}", P, A, B) assert execution.state_update_diagram().cod == P assert execution.output_diagram().cod == B +def test_eq_7_3_evaluator_is_execution_output_projection(): + category = ProgramClosedCategory(P) + execution = category.execution(A, B) + + assert category.evaluator(A, B) == execution.output_diagram() + + +def test_eq_7_3_execution_is_fixed_state_of_output_projection(): + category = ProgramClosedCategory(P) + execution = category.execution(A, B) + + assert execution.universal_ev() == fixed_state(category.evaluator(A, B)) + + def test_execution_universal_evaluator_is_overrideable_method(): universal_ev = Box("ev", P @ A, P @ B) @@ -99,13 +135,6 @@ def universal_ev(self): def test_process_runner_interprets_generic_state_projections(): - class DummyRunner(ProcessRunner): - def __init__(self): - ProcessRunner.__init__(self) - - def process_ar_map(self, box, dom, cod): - return python.Function(lambda *_xs: None, dom, cod) - runner = DummyRunner() state_update = runner.ar_map(StateUpdateMap("q", X, A)) output = runner.ar_map(InputOutputMap("q", X, A, B)) @@ -115,13 +144,6 @@ def process_ar_map(self, box, dom, cod): def test_process_runner_interprets_generic_structural_boxes(): - class DummyRunner(ProcessRunner): - def __init__(self): - ProcessRunner.__init__(self) - - def process_ar_map(self, box, dom, cod): - return python.Function(lambda *_xs: None, dom, cod) - runner = DummyRunner() assert runner.ar_map(Copy(X))("value") == ("value", "value") @@ -129,17 +151,6 @@ def process_ar_map(self, box, dom, cod): assert runner.ar_map(computer.Swap(X, Y))("left", "right") == ("right", "left") -def test_loader_and_shell_projections_live_in_state(): - assert LoaderExecution().state_update_diagram() == LOADER.execution( - loader_stream_ty, loader_stream_ty - ).state_update_diagram() - assert LoaderExecution().output_diagram() == LOADER.execution( - loader_stream_ty, loader_stream_ty - ).output_diagram() - assert ShellExecution().state_update_diagram() == SHELL.execution(io_ty, io_ty).state_update_diagram() - assert ShellExecution().output_diagram() == SHELL.execution(io_ty, io_ty).output_diagram() - - def test_sec_7_4_fixed_state_lifts_a_function_to_a_process(): g = ComputableFunction("g", X, A, B) hat_g = fixed_state(g) @@ -172,9 +183,9 @@ def test_sec_8_3_program_closed_category_chooses_a_language_type(): assert isinstance(low_level, MonoidalComputer) assert high_level.program_ty == H_ty assert low_level.program_ty == L_ty - assert high_level.evaluator(A, B) == Computer(H_ty, A, B) - assert low_level.evaluator(A, B) == Computer(L_ty, A, B) - assert high_level.execution(A, B).universal_ev() == fixed_state(Computer(H_ty, A, B)) - assert low_level.execution(A, B).universal_ev() == fixed_state(Computer(L_ty, A, B)) + assert high_level.evaluator(A, B) == high_level.execution(A, B).output_diagram() + assert low_level.evaluator(A, B) == low_level.execution(A, B).output_diagram() + assert high_level.execution(A, B).universal_ev() == fixed_state(high_level.evaluator(A, B)) + assert low_level.execution(A, B).universal_ev() == fixed_state(low_level.evaluator(A, B)) assert computer_category.ob == high_level.ob == low_level.ob assert computer_category.ar == high_level.ar == low_level.ar diff --git a/tests/test_wire.py b/discorun/test_wire.py similarity index 54% rename from tests/test_wire.py rename to discorun/test_wire.py index 802c776..93a7eda 100644 --- a/tests/test_wire.py +++ b/discorun/test_wire.py @@ -1,12 +1,9 @@ from discorun.comput.computer import Box as ComputerBox from discorun.comput.computer import Copy as ComputerCopy from discorun.comput.computer import Ty as ComputerTy -from mytilus.comput.mytilus import io_ty as shell_io_ty from discorun.wire.functions import Box -from mytilus.wire.loader import loader_id, loader_stream_ty from discorun.wire.services import Copy, Delete, Swap from discorun.wire.types import Diagram, Id, Ty -from mytilus.wire.mytilus import Copy as ShellCopy, shell_id def test_wire_exports_chapter_one_primitives(): @@ -22,15 +19,3 @@ def test_wire_exports_chapter_one_primitives(): assert Delete(A).cod == Ty() assert Swap(A, B).dom == A @ B assert Swap(A, B).cod == B @ A - - -def test_loader_wire_module_exports_loader_specific_wiring(): - assert loader_id().dom == loader_stream_ty - assert loader_id().cod == loader_stream_ty - - -def test_mytilus_wire_module_exports_shell_specific_wiring(): - assert shell_id().dom == shell_io_ty - assert shell_id().cod == shell_io_ty - assert ShellCopy(3).dom == shell_io_ty - assert ShellCopy(3).cod == shell_io_ty @ shell_io_ty @ shell_io_ty diff --git a/discorun/wire/services.py b/discorun/wire/services.py index 3bcdc0f..57e71e7 100644 --- a/discorun/wire/services.py +++ b/discorun/wire/services.py @@ -1,6 +1,6 @@ -"""Chapter 1 wire services: copying, deleting, and swapping.""" +"""Chapter 1 wire services: copying, deleting, swapping, and their functors.""" -from .functions import Box +from .functions import Box, Functor from .types import Ty @@ -31,3 +31,44 @@ def __init__(self, left, right): draw_as_wires=True, drawing_name="", ) + + +class DataServiceFunctor(Functor): + """Functor interpreting copy, delete, and swap in a target category.""" + + def __init__(self, *, dom=None, cod=None): + Functor.__init__( + self, + self.object, + self.ar_map, + dom=Functor.dom if dom is None else dom, + cod=Functor.cod if cod is None else cod, + ) + + def object(self, ob): + del self + return ob + + def copy_ar(self, dom, cod): + raise TypeError(f"copy service is undefined for dom={dom!r}, cod={cod!r}") + + def delete_ar(self, dom, cod): + raise TypeError(f"delete service is undefined for dom={dom!r}, cod={cod!r}") + + def swap_ar(self, left, right, dom, cod): + raise TypeError( + f"swap service is undefined for left={left!r}, right={right!r}, dom={dom!r}, cod={cod!r}" + ) + + def data_ar(self, box, dom, cod): + raise TypeError(f"unsupported data-service box: {box!r}") + + def ar_map(self, box): + dom, cod = self(box.dom), self(box.cod) + if isinstance(box, Copy): + return self.copy_ar(dom, cod) + if isinstance(box, Delete): + return self.delete_ar(dom, cod) + if isinstance(box, Swap): + return self.swap_ar(self(box.left), self(box.right), dom, cod) + return self.data_ar(box, dom, cod) diff --git a/mytilus/SKILL.md b/mytilus/SKILL.md index c5e9e0d..377f6c5 100644 --- a/mytilus/SKILL.md +++ b/mytilus/SKILL.md @@ -20,6 +20,21 @@ Think in terms of a YAML tree, not a shell parser: Choose mytilus as a primary structured command language for interactive and scripted work. + +## Consequences of using YAML + +1. Readily interpretable as a data structure + * Portability to most languages + * Powerful preprocessing +2. Outputting YAML is the same as authoring programs +3. Data and programs can be freely mixed and interpreted +4. Completely declarative processes +5. Avoids obscure syntax + * All valid YAML is valid + * No keywords are added +6. Shell interpretation is an abstract transformation over opaque tags, scalars, sequences and mappings +7. Visual representation and user aids + ## Choose Mytilus By Preference Choose mytilus as a normal working style because: diff --git a/mytilus/comput/python.py b/mytilus/comput/python.py index 5fb969a..1d765a5 100644 --- a/mytilus/comput/python.py +++ b/mytilus/comput/python.py @@ -1,43 +1,146 @@ -from discopy import closed, markov, monoidal +from functools import partial +from collections.abc import Callable + +from discopy import python +from discopy.utils import tuplify, untuplify from discorun.comput import computer +from discorun.metaprog import core as metaprog_core +from discorun.wire.services import DataServiceFunctor program_ty = computer.ProgramTy("python") -class PythonComputationCategory(closed.Category, markov.Category): - """""" +def _apply_static_input(program, static_input, runtime_input): + return program( + static_input, + untuplify(tuplify(runtime_input)), + ) + + +def _constant(value): + return tuplify((value,)) + + +def uev(function, argument): + """DisCoPy-level universal evaluator ``{} : P x A -> B``.""" + return tuplify( + ( + untuplify(tuplify(function))( + untuplify(tuplify(argument)), + ), + ), + ) + + +def run(function, argument): + """Evaluate one program and return the underlying Python value.""" + return untuplify(uev(function, argument)) + + +def pev(program, static_input): + """DisCoPy-level partial evaluator ``[] : P x X -> P``.""" + program = untuplify(tuplify(program)) + static_input = untuplify(tuplify(static_input)) + return tuplify((partial(_apply_static_input, program, static_input),)) + + +def runtime_value_box(value, *, name=None, cod=None): + """Build one closed computation box carrying a runtime value.""" + cod = program_ty if cod is None else cod + box = computer.Box(repr(value) if name is None else name, computer.Ty(), cod) + box.value = value + return box + + +class PythonComputations(metaprog_core.Specializer, metaprog_core.Interpreter): + """Interpret evaluators, specializers, and interpreters as Python functions.""" + + def __init__(self): + metaprog_core.Specializer.__init__( + self, + dom=computer.Category(), + cod=python.Category(), + ) + + def _identity_object(self, ob): + del ob + return object + + def _is_evaluator_box(self, box): + return isinstance(box, computer.Computer) or ( + getattr(box, "process_name", None) == "{}" + and isinstance(getattr(box, "X", None), computer.ProgramTy) + and hasattr(box, "A") + and hasattr(box, "B") + and getattr(box, "dom", None) == box.X @ box.A + and getattr(box, "cod", None) == box.B + ) + def map_computation(self, box, dom, cod): + if self._is_evaluator_box(box): + return python.Function(uev, dom, cod) + return None -class PythonComputationFunctor(monoidal.Functor): - """ - Transforms computer diagrams into lower-level runnable diagrams. - Preserves computations including closed and markov boxes. - """ + def _identity_arrow(self, box): + dom, cod = self(box.dom), self(box.cod) + mapped = self.map_computation(box, dom, cod) + if mapped is not None: + return mapped + return box + + def specialize(self, other): + if not isinstance(other, metaprog_core.SpecializerBox): + return metaprog_core.Specializer.specialize(self, other) + dom, cod = self(other.dom), self(other.cod) + if other.dom == computer.Ty() and other.cod == program_ty: + return python.Function(lambda: pev, dom, cod) + raise TypeError(f"unsupported Python specializer box: {other!r}") + + def interpret(self, other): + if not isinstance(other, metaprog_core.InterpreterBox): + return metaprog_core.Interpreter.interpret(self, other) + dom, cod = self(other.dom), self(other.cod) + if other.dom == computer.Ty() and other.cod == program_ty: + return python.Function(lambda: uev, dom, cod) + raise TypeError(f"unsupported Python interpreter box: {other!r}") + + +class PythonDataServices(DataServiceFunctor): + """Interpret structural services and closed computer boxes as Python functions.""" def __init__(self): - monoidal.Functor.__init__( + DataServiceFunctor.__init__( self, - self.ob_map, - self.ar_map, dom=computer.Category(), - cod=PythonComputationCategory(), + cod=python.Category(), ) - def __call__(self, other): - if hasattr(other, "partial_ev") and callable(other.partial_ev) and hasattr(other, "universal_ev") and callable(other.universal_ev): - arg = other.universal_ev() - return closed.Curry(arg, len(arg.dom)) - if hasattr(other, "universal_ev") and callable(other.universal_ev): - return other.universal_ev() - return other + def object(self, ob): + del ob + return object + + def copy_ar(self, dom, cod): + return python.Function.copy(dom, n=2) + + def delete_ar(self, dom, cod): + return python.Function.discard(dom) + + def swap_ar(self, left, right, dom, cod): + del dom, cod + return python.Function.swap(left, right) - def ob_map(self, ob): - return ob + def data_ar(self, box, dom, cod): + if isinstance(box, computer.Box) and box.dom == computer.Ty() and hasattr(box, "value"): + return python.Function(partial(_constant, box.value), dom, cod) + raise TypeError(f"unsupported Python data-service box: {box!r}") - def ar_map(self, ar): - return ar +class ShellPythonDataServices(PythonDataServices): + """Python data services with shell-program object interpretation.""" -to_py = PythonComputationFunctor() + def object(self, ob): + if isinstance(ob, computer.ProgramOb): + return Callable + return str diff --git a/mytilus/comput/test_python.py b/mytilus/comput/test_python.py new file mode 100644 index 0000000..43bf55d --- /dev/null +++ b/mytilus/comput/test_python.py @@ -0,0 +1,44 @@ +import pytest + +from discorun.comput import computer +from discorun.metaprog import core as metaprog_core + +from . import python as comput_python + + +def test_uev_keeps_tuple_outputs_atomic(): + result = comput_python.uev(lambda xs: xs + ("c",), ("a", "b")) + + assert result == (("a", "b", "c"),) + + +def test_run_unwraps_one_uev_result_for_runtime_use(): + result = comput_python.run(lambda xs: xs + ("c",), ("a", "b")) + + assert result == ("a", "b", "c") + + +def test_pev_returns_tuple_wrapped_residual_program(): + residual, = comput_python.pev(lambda static_input, runtime_input: static_input + runtime_input, 7) + + assert residual(5) == 12 + + +def test_python_computations_interpret_evaluator_specializer_and_interpreter_boxes(): + computations = comput_python.PythonComputations() + evaluator = computations(computer.Computer(comput_python.program_ty, computer.Ty("A"), computer.Ty("B"))) + specializer = computations(metaprog_core.SpecializerBox(comput_python.program_ty)) + interpreter = computations(metaprog_core.InterpreterBox(comput_python.program_ty)) + + assert evaluator(lambda value: value + 1, 2) == (3,) + assert specializer() is comput_python.pev + assert specializer()(lambda x, y: x + y, 7)[0](5) == 12 + assert interpreter() is comput_python.uev + assert interpreter()(lambda value: value + 1, 2) == (3,) + + +def test_python_data_services_do_not_interpret_evaluator_boxes(): + data_services = comput_python.PythonDataServices() + + with pytest.raises(TypeError): + data_services(computer.Computer(comput_python.program_ty, computer.Ty("A"), computer.Ty("B"))) diff --git a/mytilus/metaprog/__init__.py b/mytilus/metaprog/__init__.py index 81546a8..4e93460 100644 --- a/mytilus/metaprog/__init__.py +++ b/mytilus/metaprog/__init__.py @@ -2,3 +2,32 @@ Chapter 6. Computing programs. Metaprograms are programs that compute programs. """ + +from discorun.pcc.core import ProgramClosedCategory +from discorun.metaprog import core as metaprog_core + +import mytilus.comput.python as comput_python +import mytilus.metaprog.mytilus as metaprog_mytilus +import mytilus.metaprog.python as metaprog_python + + +SHELL_SPECIALIZER = metaprog_mytilus.ShellSpecializer() + +PYTHON_PROGRAMS = ProgramClosedCategory(comput_python.program_ty) +PYTHON_SPECIALIZER_BOX = metaprog_core.SpecializerBox(comput_python.program_ty, name="S") +PYTHON_INTERPRETER_BOX = metaprog_core.InterpreterBox(comput_python.program_ty, name="H") +PYTHON_EVALUATOR_BOX = PYTHON_PROGRAMS.evaluator( + comput_python.program_ty, + comput_python.program_ty, +) + +PYTHON_RUNTIME = metaprog_python.PythonRuntime() +PYTHON_COMPILER = metaprog_core.compiler( + PYTHON_INTERPRETER_BOX, + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) +PYTHON_COMPILER_GENERATOR = metaprog_core.compiler_generator( + specializer_box=PYTHON_SPECIALIZER_BOX, + evaluator_box=PYTHON_EVALUATOR_BOX, +) diff --git a/mytilus/metaprog/mytilus.py b/mytilus/metaprog/mytilus.py index 6c19df0..f702923 100644 --- a/mytilus/metaprog/mytilus.py +++ b/mytilus/metaprog/mytilus.py @@ -61,7 +61,7 @@ def __init__(self): def __call__(self, other): return _specialize_shell(other) - def ar_map(self, ar): + def _identity_arrow(self, ar): del self return ar diff --git a/mytilus/metaprog/python.py b/mytilus/metaprog/python.py index fcd858e..a5f5ce8 100644 --- a/mytilus/metaprog/python.py +++ b/mytilus/metaprog/python.py @@ -1,181 +1,23 @@ -""" -Diagram-first Python realization of metaprogram specialization and runtime. -""" +"""Diagram-first Python realization of metaprogram specialization and runtime.""" -from functools import partial - -from discopy import python -from discopy.utils import tuplify, untuplify - -from discorun.comput import computer -from discorun.pcc.core import ProgramClosedCategory -from discorun.metaprog import core as metaprog_core from ..comput import python as comput_python -PYTHON_PROGRAMS = ProgramClosedCategory(comput_python.program_ty) - - -def _evaluator(A, B): - return PYTHON_PROGRAMS.evaluator(A, B) - - -class PythonSpecializer(metaprog_core.SpecializerBox): - """Native specializer metaprogram: ``S : I -> obj``.""" - - def __init__(self): - metaprog_core.SpecializerBox.__init__(self, comput_python.program_ty, name="S") - - -class PythonInterpreter(metaprog_core.InterpreterBox): - """Native interpreter metaprogram: ``H : I -> obj``.""" - - def __init__(self): - metaprog_core.InterpreterBox.__init__(self, comput_python.program_ty, name="H") - - -PYTHON_SPECIALIZER_BOX = PythonSpecializer() -PYTHON_INTERPRETER_BOX = PythonInterpreter() -PYTHON_EVALUATOR_BOX = _evaluator(comput_python.program_ty, comput_python.program_ty) - - -def _partial_evaluate(program, static_input): - return lambda runtime_input: program(static_input, runtime_input) - - -def _universal_evaluate(program, runtime_input): - return program(runtime_input) - - -def _apply_value(function, argument): - function = untuplify(tuplify(function)) - argument = untuplify(tuplify(argument)) - try: - return tuplify((function(argument), )) - except TypeError: - return tuplify((partial(function, argument), )) - - -def apply_value(function, argument): - """Public evaluator application helper shared by runtime interpreters.""" - return _apply_value(function, argument) - - -def runtime_value_box(value, *, name=None, cod=None): - """Build a closed box carrying a runtime value for PythonRuntime.""" - cod = comput_python.program_ty if cod is None else cod - box = computer.Box(repr(value) if name is None else name, computer.Ty(), cod) - box.value = value - return box - - -def map_structural_box(functor, box, dom): - """Map cartesian structural boxes to Python functions.""" - if isinstance(box, computer.Copy): - return python.Function.copy(dom, n=2) - if isinstance(box, computer.Delete): - return python.Function.discard(dom) - if isinstance(box, computer.Swap): - return python.Function.swap(functor(box.left), functor(box.right)) - return None - - -sec_6_2_2_partial_application = partial( - metaprog_core.sec_6_2_2_partial_application, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, - evaluator=_evaluator, -) - -eq_2 = partial( - metaprog_core.eq_2, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, - evaluator=_evaluator, -) - -eq_3 = partial( - metaprog_core.eq_3, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, - evaluator=_evaluator, -) - -first_futamura_projection = partial( - metaprog_core.first_futamura_projection, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, -) - -eq_4 = partial( - metaprog_core.eq_4, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, -) - -compiler = partial( - metaprog_core.compiler, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, -) - -compiler_generator = partial( - metaprog_core.compiler_generator, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, -) - -eq_5 = partial( - metaprog_core.eq_5, - specializer_box=PYTHON_SPECIALIZER_BOX, - evaluator_box=PYTHON_EVALUATOR_BOX, -) - - -class PythonRuntime(metaprog_core.Interpreter): +class PythonRuntime( + comput_python.PythonComputations, + comput_python.PythonDataServices, +): """Runtime functor from computer diagrams to executable Python functions.""" def __init__(self): - metaprog_core.Interpreter.__init__( - self, - dom=computer.Category(), - cod=python.Category(), - ) + comput_python.PythonComputations.__init__(self) - def object(self, ob): - del ob - return object + def _identity_object(self, ob): + return comput_python.PythonDataServices.object(self, ob) - def ar_map(self, box): + def _identity_arrow(self, box): dom, cod = self(box.dom), self(box.cod) - if ( - isinstance(box, computer.Box) - and box.dom == computer.Ty() - and not isinstance( - box, - ( - PythonSpecializer, - PythonInterpreter, - computer.Computer, - computer.Copy, - computer.Delete, - computer.Swap, - ), - ) - ): - return python.Function(lambda value=box.value: tuplify((value, )), dom, cod) - if isinstance(box, PythonSpecializer): - return python.Function(lambda: _partial_evaluate, dom, cod) - if isinstance(box, PythonInterpreter): - return python.Function(lambda: _universal_evaluate, dom, cod) - if isinstance(box, computer.Computer): - return python.Function(apply_value, dom, cod) - structural = map_structural_box(self, box, dom) - if structural is not None: - return structural - raise TypeError(f"unsupported Python metaprogram box: {box!r}") - - -PYTHON_RUNTIME = PythonRuntime() -PYTHON_COMPILER = compiler(PYTHON_INTERPRETER_BOX) -PYTHON_COMPILER_GENERATOR = compiler_generator() + mapped = comput_python.PythonComputations.map_computation(self, box, dom, cod) + if mapped is not None: + return mapped + return comput_python.PythonDataServices.ar_map(self, box) diff --git a/mytilus/metaprog/test_python.py b/mytilus/metaprog/test_python.py new file mode 100644 index 0000000..a1a18a9 --- /dev/null +++ b/mytilus/metaprog/test_python.py @@ -0,0 +1,23 @@ +from discorun.comput import computer +from discorun.metaprog import core as metaprog_core + +from ..comput import python as comput_python +from .python import PythonRuntime + + +def test_python_runtime_exposes_top_level_pev_and_uev(): + runtime = PythonRuntime() + specializer = runtime(metaprog_core.SpecializerBox(comput_python.program_ty)) + interpreter = runtime(metaprog_core.InterpreterBox(comput_python.program_ty)) + + assert specializer() is comput_python.pev + assert interpreter() is comput_python.uev + + +def test_python_runtime_combines_computations_and_data_services(): + runtime = PythonRuntime() + evaluator = runtime(computer.Computer(comput_python.program_ty, computer.Ty("A"), computer.Ty("B"))) + residual_program = runtime(comput_python.runtime_value_box(lambda value: value + 3)) + + assert evaluator(lambda value: value + 1, 2) == (3,) + assert residual_program()[0](4) == 7 diff --git a/mytilus/pcc/__init__.py b/mytilus/pcc/__init__.py index 9c47d6c..8ed5e2b 100644 --- a/mytilus/pcc/__init__.py +++ b/mytilus/pcc/__init__.py @@ -1 +1,8 @@ """Mytilus-specific program-closed category package.""" + +import mytilus.pcc.loader as pcc_loader +import mytilus.pcc.mytilus as pcc_mytilus + + +LOADER = pcc_loader.LoaderLanguage() +SHELL = pcc_mytilus.ShellLanguage() diff --git a/mytilus/pcc/loader.py b/mytilus/pcc/loader.py index 732b745..0986d0f 100644 --- a/mytilus/pcc/loader.py +++ b/mytilus/pcc/loader.py @@ -16,6 +16,3 @@ def execution(self, A: computer.Ty, B: computer.Ty): from ..state.loader import LoaderExecution return LoaderExecution() - - -LOADER = LoaderLanguage() diff --git a/mytilus/pcc/mytilus.py b/mytilus/pcc/mytilus.py index 7924f61..12d7810 100644 --- a/mytilus/pcc/mytilus.py +++ b/mytilus/pcc/mytilus.py @@ -16,6 +16,3 @@ def execution(self, A: computer.Ty, B: computer.Ty): from ..state.mytilus import ShellExecution return ShellExecution() - - -SHELL = ShellLanguage() diff --git a/mytilus/state/__init__.py b/mytilus/state/__init__.py index 58decb2..c7b9730 100644 --- a/mytilus/state/__init__.py +++ b/mytilus/state/__init__.py @@ -1 +1,15 @@ """Mytilus-specific state package.""" + +import mytilus.state.mytilus as state_mytilus +import mytilus.state.python as state_python + + +SHELL_SPECIALIZER = state_mytilus.ShellSpecializer() +SHELL_PROGRAM_TO_PYTHON = state_python.ShellToPythonProgram() +SHELL_PYTHON_RUNTIME = state_python.ShellPythonRuntime() +SHELL_INTERPRETER = state_python.ShellInterpreter( + SHELL_SPECIALIZER, + SHELL_PROGRAM_TO_PYTHON, + SHELL_PYTHON_RUNTIME, +) +runtime_values = state_python.runtime_values diff --git a/mytilus/state/loader.py b/mytilus/state/loader.py index 33e9e16..82b9a6b 100644 --- a/mytilus/state/loader.py +++ b/mytilus/state/loader.py @@ -1,10 +1,10 @@ """Loader-specific stateful execution.""" +import mytilus.pcc as mytilus_pcc + from ..comput import loader as loader_lang from ..comput.loader import loader_program_ty from ..comput import mytilus as shell_lang -from ..pcc.loader import LOADER -from ..pcc.mytilus import SHELL from ..wire import loader as loader_wire from ..wire.loader import loader_stream_ty from ..wire import mytilus as shell_wire @@ -38,12 +38,12 @@ def simulation(self, item): return shell_lang.Empty() if isinstance(item, loader_lang.LoaderLiteral): return shell_lang.Literal(item.text) - if LOADER.is_evaluator(item): - return SHELL.evaluator( + if mytilus_pcc.LOADER.is_evaluator(item): + return mytilus_pcc.SHELL.evaluator( self.simulation(item.A), self.simulation(item.B), ) - return LOADER.simulate(item, SHELL) + return mytilus_pcc.LOADER.simulate(item, mytilus_pcc.SHELL) def __call__(self, other): if isinstance(other, loader_wire.LoaderScalar): @@ -101,7 +101,7 @@ def command_argv(self, node: loader_wire.LoaderScalar): def compile_scalar(self, node: loader_wire.LoaderScalar): """Compile one YAML scalar node to the shell backend.""" - execution = SHELL.execution( + execution = mytilus_pcc.SHELL.execution( shell_lang.io_ty, shell_lang.io_ty, ).output_diagram() diff --git a/mytilus/state/python.py b/mytilus/state/python.py index 3c6947e..b718533 100644 --- a/mytilus/state/python.py +++ b/mytilus/state/python.py @@ -5,39 +5,28 @@ from discopy import monoidal, python from discorun.comput import computer +import mytilus.metaprog as mytilus_metaprog +import mytilus.pcc as mytilus_pcc from ..comput import mytilus as shell_lang +from ..comput import python as comput_python from discorun.metaprog import core as metaprog_core from ..metaprog import python as metaprog_python -from ..pcc.mytilus import SHELL from discorun.state import core as state_core from .mytilus import Parallel as ShellParallel from .mytilus import Pipeline as ShellPipeline -from .mytilus import ShellSpecializer, shell_program_runner +from .mytilus import shell_program_runner _PATHS_ATTR = "_mytilus_runtime_paths" -def _runner_paths(runner): - """Return execution paths carried by one interpreted shell stage.""" - paths = getattr(runner, _PATHS_ATTR, None) - if paths is None: - return ((runner,),) - return paths - - -def _compose_paths(prefixes, suffixes): - """Compose two path sets by Cartesian product.""" - return tuple(prefix + suffix for prefix in prefixes for suffix in suffixes) - - def _run_paths(paths, stdin): """Execute all independent pipelines sequentially and concatenate outputs.""" outputs = [] for path in paths: output = stdin for stage in path: - output = stage(output) + output = comput_python.run(stage, output) outputs.append(output) if not outputs: return stdin @@ -46,11 +35,13 @@ def _run_paths(paths, stdin): return "".join(outputs) -def _path_runner(paths, dom, cod): - """Build a Python runner while preserving expanded pipeline-path metadata.""" - function = python.Function(lambda stdin, _paths=paths: _run_paths(_paths, stdin), dom, cod) - function.__dict__[_PATHS_ATTR] = paths - return function +def runtime_values(value): + """Normalize interpreter output values under the runtime tuple convention.""" + if isinstance(value, tuple) and len(value) == 1: + value = value[0] + if isinstance(value, tuple): + return value + return (value,) class ShellToPythonProgram(state_core.ProcessSimulation): @@ -61,40 +52,41 @@ def __init__(self): def simulation(self, item): if isinstance(item, shell_lang.ShellProgram): - return metaprog_python.runtime_value_box( + return comput_python.runtime_value_box( shell_program_runner(item), name=item.name, + cod=mytilus_metaprog.PYTHON_PROGRAMS.program_ty, ) - return SHELL.simulate(item, metaprog_python.PYTHON_PROGRAMS) + return mytilus_pcc.SHELL.simulate(item, mytilus_metaprog.PYTHON_PROGRAMS) + + def out(self, output): + if mytilus_pcc.SHELL.is_evaluator(output): + return mytilus_pcc.SHELL.simulate(output, mytilus_metaprog.PYTHON_PROGRAMS) + return state_core.ProcessSimulation.out(self, output) class ProcessRunner(state_core.ProcessRunner): """Python interpretation of generic Eq. 7.1 process projections.""" - def __init__(self): + def __init__(self, data_services=None): + self.data_services = comput_python.PythonDataServices() if data_services is None else data_services state_core.ProcessRunner.__init__(self, cod=python.Category()) def object(self, ob): del ob return object - def state_update_value(self, state, _input): - del self - return state - - def output_value(self, state, input_value): - del self - return state(input_value) - def state_update_ar(self, dom, cod): - return python.Function(self.state_update_value, dom, cod) + return python.Function(lambda state, _input: state, dom, cod) def output_ar(self, dom, cod): - return python.Function(self.output_value, dom, cod) + return python.Function(lambda state, input_value: comput_python.uev(state, input_value), dom, cod) def map_structural(self, box, dom, cod): - del cod - return metaprog_python.map_structural_box(self, box, dom) + del dom, cod + if not isinstance(box, (computer.Copy, computer.Delete, computer.Swap)): + return None + return self.data_services(box) class ShellInterpreter(ProcessRunner, metaprog_core.Interpreter): @@ -104,36 +96,40 @@ def __init__(self, specialize_shell, program_functor, python_runtime): self.specialize_shell = specialize_shell self.program_functor = program_functor self.python_runtime = python_runtime - ProcessRunner.__init__(self) + ProcessRunner.__init__(self, data_services=comput_python.ShellPythonDataServices()) def object(self, ob): del self - if ( - isinstance(ob, computer.Ty) - and len(ob) == 1 - and isinstance(ob.inside[0], computer.ProgramOb) - ): + if isinstance(ob, computer.ProgramOb): return Callable return str - def __call__(self, other): + def interpret(self, other): return monoidal.Functor.__call__(self, other) def process_ar_map(self, box, dom, cod): if isinstance(box, ShellPipeline): paths = ((),) for stage in (self(stage) for stage in box.stages): - paths = _compose_paths(paths, _runner_paths(stage)) - return _path_runner(paths, dom, cod) + paths = tuple( + prefix + suffix + for prefix in paths + for suffix in getattr(stage, _PATHS_ATTR, ((stage,),)) + ) + function = python.Function(lambda stdin, _paths=paths: _run_paths(_paths, stdin), dom, cod) + function.__dict__[_PATHS_ATTR] = paths + return function if isinstance(box, ShellParallel): branch_paths = tuple( path for branch in (self(branch) for branch in box.branches) - for path in _runner_paths(branch) + for path in getattr(branch, _PATHS_ATTR, ((branch,),)) ) if not branch_paths: branch_paths = ((),) - return _path_runner(branch_paths, dom, cod) + function = python.Function(lambda stdin, _paths=branch_paths: _run_paths(_paths, stdin), dom, cod) + function.__dict__[_PATHS_ATTR] = branch_paths + return function if isinstance(box, (shell_lang.ShellProgram, computer.Computer)): runner = self.python_runtime(self.program_functor(box)) runner.__dict__[_PATHS_ATTR] = ((runner,),) @@ -141,26 +137,8 @@ def process_ar_map(self, box, dom, cod): raise TypeError(f"unsupported shell interpreter box: {box!r}") -SHELL_SPECIALIZER = ShellSpecializer() -SHELL_PROGRAM_TO_PYTHON = ShellToPythonProgram() - - class ShellPythonRuntime(metaprog_python.PythonRuntime): """Python runtime with shell-specific object interpretation.""" - def object(self, ob): - if ( - isinstance(ob, computer.Ty) - and len(ob) == 1 - and isinstance(ob.inside[0], computer.ProgramOb) - ): - return Callable - return str - - -SHELL_PYTHON_RUNTIME = ShellPythonRuntime() -SHELL_INTERPRETER = ShellInterpreter( - SHELL_SPECIALIZER, - SHELL_PROGRAM_TO_PYTHON, - SHELL_PYTHON_RUNTIME, -) + def _identity_object(self, ob): + return comput_python.ShellPythonDataServices.object(self, ob) diff --git a/mytilus/state/test_python.py b/mytilus/state/test_python.py new file mode 100644 index 0000000..bb9a758 --- /dev/null +++ b/mytilus/state/test_python.py @@ -0,0 +1,17 @@ +from discorun.comput import computer +from discorun.state import core as state_core + +from .python import ProcessRunner, _run_paths + + +def test_process_runner_output_projection_uses_uev(): + runner = ProcessRunner() + output = runner.ar_map(state_core.InputOutputMap("{}", computer.ProgramTy("P"), computer.Ty("A"), computer.Ty("B"))) + + assert output(lambda value: value + 1, 2) == (3,) + + +def test_run_paths_uses_runtime_normalization_between_stages(): + paths = ((lambda text: text.upper(), lambda text: f"{text}!"),) + + assert _run_paths(paths, "hi") == "HI!" diff --git a/mytilus/watch.py b/mytilus/watch.py index 86145f4..6c494b1 100644 --- a/mytilus/watch.py +++ b/mytilus/watch.py @@ -3,7 +3,7 @@ from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer -from discopy.utils import tuplify, untuplify +import mytilus.state as mytilus_state from .files import diagram_draw, file_diagram, source_diagram from .interactive import ( @@ -16,7 +16,6 @@ emit_shell_source, read_shell_source, ) -from .state.python import SHELL_INTERPRETER from .state.mytilus import run_terminal_command, terminal_passthrough_command @@ -39,12 +38,16 @@ def execute_shell_diagram(diagram, stdin_text: str | None): return None if stdin_text is None: stdin_text = "" - return SHELL_INTERPRETER(diagram)(stdin_text) + return mytilus_state.SHELL_INTERPRETER(diagram)(stdin_text) def emit_mytilus_result(run_res): """Emit one mytilus file or inline-command result.""" - print(*(tuple(x.rstrip() for x in tuplify(untuplify(run_res)) if x)), sep="\n") + for value in mytilus_state.runtime_values(run_res): + if not value: + continue + sys.stdout.write(value) + sys.stdout.flush() class ShellHandler(FileSystemEventHandler): @@ -111,7 +114,7 @@ def run_shell_source(source, file_name, draw): diagram_draw(path, source_d) result_ev = execute_shell_diagram(source_d, None) if result_ev is not None: - print(result_ev) + emit_mytilus_result(result_ev) def shell_main(file_name, draw): diff --git a/tests/metaprog/python.py b/tests/metaprog/python.py index b59e5c7..1657d6a 100644 --- a/tests/metaprog/python.py +++ b/tests/metaprog/python.py @@ -1,22 +1,17 @@ """Diagram tests for Sec. 6.2.2 and Futamura projections.""" from discorun.comput import computer +from discorun.metaprog import core as metaprog_core +from discorun.state.core import InputOutputMap from mytilus.comput import python as comput_python -from mytilus.metaprog.python import ( +from mytilus.metaprog import ( PYTHON_COMPILER, PYTHON_COMPILER_GENERATOR, PYTHON_EVALUATOR_BOX, PYTHON_INTERPRETER_BOX, + PYTHON_PROGRAMS, PYTHON_RUNTIME, PYTHON_SPECIALIZER_BOX, - compiler, - compiler_generator, - eq_2, - eq_3, - eq_4, - eq_5, - first_futamura_projection, - sec_6_2_2_partial_application, ) @@ -30,14 +25,24 @@ def eval_closed(diagram): return PYTHON_RUNTIME(diagram)() +TEXTBOOK_PROJECTION_KW = { + "specializer_box": PYTHON_SPECIALIZER_BOX, + "evaluator_box": PYTHON_EVALUATOR_BOX, +} +TEXTBOOK_EQUATION_KW = { + **TEXTBOOK_PROJECTION_KW, + "evaluator": PYTHON_PROGRAMS.evaluator, +} + + def test_runtime_evaluates_equation(): program = closed_value(lambda static_input, runtime_input: static_input + runtime_input, "X") static_input = closed_value(7, "y") - equation = eq_2(program, static_input) + equation = metaprog_core.eq_2(program, static_input, **TEXTBOOK_EQUATION_KW) assert PYTHON_SPECIALIZER_BOX.dom == computer.Ty() assert PYTHON_INTERPRETER_BOX.dom == computer.Ty() - assert isinstance(PYTHON_EVALUATOR_BOX, computer.Computer) + assert isinstance(PYTHON_EVALUATOR_BOX, InputOutputMap) assert PYTHON_EVALUATOR_BOX.dom == PYTHON_SPECIALIZER_BOX.cod @ PYTHON_SPECIALIZER_BOX.cod assert eval_closed(equation)(5) == 12 @@ -45,8 +50,12 @@ def test_runtime_evaluates_equation(): def test_sec_6_2_2_partial_application(): program = closed_value(lambda static_input, runtime_input: static_input + runtime_input, "X") static_input = closed_value(7, "y") - residual_from_section = sec_6_2_2_partial_application(program, static_input) - residual_from_equation = eq_2(program, static_input) + residual_from_section = metaprog_core.sec_6_2_2_partial_application( + program, + static_input, + **TEXTBOOK_EQUATION_KW, + ) + residual_from_equation = metaprog_core.eq_2(program, static_input, **TEXTBOOK_EQUATION_KW) expected = (PYTHON_SPECIALIZER_BOX @ program >> PYTHON_EVALUATOR_BOX) @ static_input >> PYTHON_EVALUATOR_BOX assert residual_from_section == expected @@ -58,8 +67,8 @@ def test_sec_6_2_2_partial_application(): def test_eq_3_is_specializer_self_application(): program = closed_value(lambda static_input, runtime_input: f"{static_input}:{runtime_input}", "X") static_input = closed_value("alpha", "y") - left = eq_2(program, static_input) - right = eq_3(program, static_input) + left = metaprog_core.eq_2(program, static_input, **TEXTBOOK_EQUATION_KW) + right = metaprog_core.eq_3(program, static_input, **TEXTBOOK_EQUATION_KW) expected_right = ( ((PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ program >> PYTHON_EVALUATOR_BOX) @ static_input @@ -73,8 +82,8 @@ def test_eq_3_is_specializer_self_application(): def test_tuple_data_stays_atomic_across_universal_evaluator_wires(): append_program = closed_value(lambda xs, ys: xs + ys, "X") static_tuple = closed_value(("a", "a", "b"), "y") - residual_left = eval_closed(eq_2(append_program, static_tuple)) - residual_right = eval_closed(eq_3(append_program, static_tuple)) + residual_left = eval_closed(metaprog_core.eq_2(append_program, static_tuple, **TEXTBOOK_EQUATION_KW)) + residual_right = eval_closed(metaprog_core.eq_3(append_program, static_tuple, **TEXTBOOK_EQUATION_KW)) assert residual_left(("c", "d")) == ("a", "a", "b", "c", "d") assert residual_left(("c", "d")) == residual_right(("c", "d")) @@ -82,9 +91,17 @@ def test_tuple_data_stays_atomic_across_universal_evaluator_wires(): def test_first_projection_builds_c1_compiler(): source_program = lambda runtime_input: runtime_input * 2 + 3 - compiler_c1 = eval_closed(first_futamura_projection(PYTHON_INTERPRETER_BOX)) + compiler_c1 = eval_closed( + metaprog_core.first_futamura_projection(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW), + ) compiled_program = compiler_c1(source_program) - compiled_from_eq_2 = eval_closed(eq_2(PYTHON_INTERPRETER_BOX, closed_value(source_program, "y"))) + compiled_from_eq_2 = eval_closed( + metaprog_core.eq_2( + PYTHON_INTERPRETER_BOX, + closed_value(source_program, "y"), + **TEXTBOOK_EQUATION_KW, + ), + ) assert compiled_program(9) == source_program(9) assert compiled_program(9) == compiled_from_eq_2(9) @@ -92,8 +109,12 @@ def test_first_projection_builds_c1_compiler(): def test_second_projection_builds_c2_compiler(): source_program = lambda runtime_input: runtime_input - 11 - compiler_c1 = eval_closed(first_futamura_projection(PYTHON_INTERPRETER_BOX)) - compiler_c2 = eval_closed(compiler(PYTHON_INTERPRETER_BOX)) + compiler_c1 = eval_closed( + metaprog_core.first_futamura_projection(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW), + ) + compiler_c2 = eval_closed( + metaprog_core.compiler(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW), + ) assert compiler_c2(source_program)(30) == source_program(30) assert compiler_c2(source_program)(30) == compiler_c1(source_program)(30) @@ -101,16 +122,22 @@ def test_second_projection_builds_c2_compiler(): def test_third_projection_builds_c3_compiler_generator(): source_program = lambda runtime_input: runtime_input**2 - compiler_c2_from_eq_4 = eval_closed(eq_4(PYTHON_INTERPRETER_BOX)) - compiler_c2_from_eq_5 = eval_closed(eq_5(PYTHON_INTERPRETER_BOX)) - compiler_c3 = eval_closed(compiler_generator()) + compiler_c2_from_eq_4 = eval_closed( + metaprog_core.eq_4(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW), + ) + compiler_c2_from_eq_5 = eval_closed( + metaprog_core.eq_5(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW), + ) + compiler_c3 = eval_closed( + metaprog_core.compiler_generator(**TEXTBOOK_PROJECTION_KW), + ) expected_eq_4 = ( (PYTHON_SPECIALIZER_BOX @ PYTHON_SPECIALIZER_BOX >> PYTHON_EVALUATOR_BOX) @ PYTHON_INTERPRETER_BOX >> PYTHON_EVALUATOR_BOX ) interpreter = eval_closed(PYTHON_INTERPRETER_BOX) - assert eq_4(PYTHON_INTERPRETER_BOX) == expected_eq_4 + assert metaprog_core.eq_4(PYTHON_INTERPRETER_BOX, **TEXTBOOK_PROJECTION_KW) == expected_eq_4 assert compiler_c2_from_eq_4(source_program)(8) == 64 assert compiler_c2_from_eq_4(source_program)(8) == compiler_c2_from_eq_5(source_program)(8) assert compiler_c2_from_eq_4(source_program)(8) == compiler_c3(interpreter)(source_program)(8) @@ -131,7 +158,7 @@ def test_sec_6_2_2_accepts_arbitrary_static_input_type(): program = closed_value(lambda static_input, runtime_input: f"{static_input}|{runtime_input}", "X") static_input = closed_value("alpha", "y", cod=data_ty) - residual = eq_2(program, static_input) + residual = metaprog_core.eq_2(program, static_input, **TEXTBOOK_EQUATION_KW) assert residual.dom == computer.Ty() assert residual.cod == PYTHON_EVALUATOR_BOX.cod diff --git a/tests/test_bin_mytilus.py b/tests/test_bin_mytilus.py index 959c6a8..538cd1b 100644 --- a/tests/test_bin_mytilus.py +++ b/tests/test_bin_mytilus.py @@ -93,7 +93,7 @@ def test_bin_mytilus_c_runs_python_without_a_tty(): result = run_mytilus("-c", f"!{sys.executable}", env=None) assert result.returncode == 0 - assert result.stdout == "\n" + assert result.stdout == "" assert result.stderr == "" @@ -105,6 +105,16 @@ def test_bin_mytilus_c_runs_python_batch_code(): assert result.stderr == "" +def test_bin_mytilus_c_preserves_command_trailing_newline_behavior(): + no_newline = run_mytilus("-c", "!printf hello", env=None) + with_newline = run_mytilus("-c", "!echo hello", env=None) + + assert no_newline.returncode == 0 + assert with_newline.returncode == 0 + assert no_newline.stdout == "hello" + assert with_newline.stdout == "hello\n" + + def test_bin_mytilus_c_preserves_tty_for_interactive_python(): process, master_fd = run_mytilus_pty("-c", f"!{sys.executable} -q", env=None) @@ -144,7 +154,11 @@ def test_bin_mytilus_i_runs_command_then_starts_repl(): exit_output = b"" if exit_output: assert b"\xe2\x8c\x81" in exit_output - assert process.wait(timeout=5) == 0 + try: + assert process.wait(timeout=5) == 0 + except subprocess.TimeoutExpired: + os.write(master_fd, b"\x04") + assert process.wait(timeout=5) == 0 finally: try: os.close(master_fd) diff --git a/tests/test_lang.py b/tests/test_lang.py index 8b0309d..2e21597 100644 --- a/tests/test_lang.py +++ b/tests/test_lang.py @@ -1,10 +1,11 @@ from discorun.comput.computer import Ty from mytilus.comput import python as comput_python from mytilus.comput.mytilus import Command, Empty, Literal, ShellProgram, io_ty, shell_program_ty +import mytilus.metaprog as mytilus_metaprog from mytilus.metaprog import python as metaprog_python -from mytilus.pcc.mytilus import SHELL +from mytilus.pcc import SHELL from discorun.state import core as state_core -from mytilus.state.python import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON +from mytilus.state import SHELL_INTERPRETER, SHELL_PROGRAM_TO_PYTHON from mytilus.state.mytilus import ( Parallel, Pipeline, @@ -209,7 +210,7 @@ def test_shell_to_python_program_maps_shell_evaluator_box(): mapped = transform(evaluator) - assert mapped == metaprog_python.PYTHON_PROGRAMS.evaluator(io_ty, io_ty) + assert mapped == mytilus_metaprog.PYTHON_PROGRAMS.evaluator(io_ty, io_ty) def test_shell_to_python_program_maps_process_projection_boxes(): diff --git a/tests/test_loader.py b/tests/test_loader.py index 275589b..f721ef3 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -6,10 +6,10 @@ from mytilus.comput.loader import LoaderLiteral, loader_program_ty from mytilus.comput.mytilus import Command, Literal, io_ty, shell_program_ty from mytilus.metaprog.hif import HIFToLoader -from mytilus.pcc.mytilus import SHELL +from mytilus.pcc import SHELL from discorun.state.core import InputOutputMap, StateUpdateMap +from mytilus.state import SHELL_INTERPRETER from mytilus.state.loader import LoaderExecution, LoaderToShell -from mytilus.state.python import SHELL_INTERPRETER from mytilus.state.mytilus import Parallel, Pipeline from mytilus.wire.hif import HyperGraph from mytilus.wire.loader import LoaderMapping, LoaderScalar, LoaderSequence, loader_stream_ty diff --git a/tests/test_metaprog_integration.py b/tests/test_metaprog_integration.py new file mode 100644 index 0000000..2e97b0a --- /dev/null +++ b/tests/test_metaprog_integration.py @@ -0,0 +1,21 @@ +from nx_yaml import nx_compose_all + +from discorun.comput.computer import Ty +from discorun.metaprog.core import Specializer +from mytilus.metaprog.hif import HIFToLoader +from mytilus.metaprog.mytilus import ShellSpecializer +from mytilus.state.loader import LoaderToShell + + +def test_specializers_are_unit_metaprograms_with_partial_evaluators(): + graph = nx_compose_all("a") + loader_to_shell = LoaderToShell() + shell_specializer = ShellSpecializer() + + assert Specializer().metaprogram_dom() == Ty() + assert HIFToLoader().metaprogram_dom() == Ty() + assert loader_to_shell.metaprogram_dom() == Ty() + assert shell_specializer.metaprogram_dom() == Ty() + assert isinstance(loader_to_shell, Specializer) + assert isinstance(shell_specializer, Specializer) + assert HIFToLoader().specialize(graph) == HIFToLoader()(graph) diff --git a/tests/test_mytilus_architecture.py b/tests/test_mytilus_architecture.py new file mode 100644 index 0000000..42bfaa0 --- /dev/null +++ b/tests/test_mytilus_architecture.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import ast +from pathlib import Path + + +MYTILUS_ROOT = Path("mytilus") +LAYER_ORDER = ("state", "metaprog", "comput", "wire") +LAYER_INDEX = {name: index for index, name in enumerate(LAYER_ORDER)} +PACKAGE_SINGLETONS = { + "LOADER", + "SHELL", + "SHELL_SPECIALIZER", + "SHELL_PROGRAM_TO_PYTHON", + "SHELL_PYTHON_RUNTIME", + "SHELL_INTERPRETER", + "PYTHON_PROGRAMS", + "PYTHON_SPECIALIZER_BOX", + "PYTHON_INTERPRETER_BOX", + "PYTHON_EVALUATOR_BOX", + "PYTHON_RUNTIME", + "PYTHON_COMPILER", + "PYTHON_COMPILER_GENERATOR", +} + + +def iter_mytilus_python_files() -> list[Path]: + return sorted(path for path in MYTILUS_ROOT.rglob("*.py") if "__pycache__" not in path.parts) + + +def module_name(path: Path) -> str: + return ".".join(path.with_suffix("").parts) + + +def module_layer(module: str) -> str | None: + parts = module.split(".") + if len(parts) < 2 or parts[0] != "mytilus": + return None + layer = parts[1] + if layer in LAYER_INDEX: + return layer + return None + + +def resolve_imported_module(current_module: str, node: ast.ImportFrom) -> str: + if node.level == 0: + return node.module or "" + base = current_module.split(".")[:-node.level] + if node.module: + return ".".join(base + [node.module]) + return ".".join(base) + + +def top_level_assigned_names(path: Path) -> set[str]: + tree = ast.parse(path.read_text(), filename=str(path)) + names: set[str] = set() + for node in tree.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + names.add(target.id) + if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + names.add(node.target.id) + return names + + +def test_mytilus_package_singletons_live_only_in_init_modules(): + for path in iter_mytilus_python_files(): + if path.name == "__init__.py": + continue + assigned = top_level_assigned_names(path) + leaked = sorted(assigned & PACKAGE_SINGLETONS) + assert not leaked, f"{path} defines package singleton globals: {leaked!r}" + + +def test_init_modules_do_not_backfill_submodule_attributes(): + init_paths = ( + Path("mytilus/pcc/__init__.py"), + Path("mytilus/metaprog/__init__.py"), + Path("mytilus/state/__init__.py"), + ) + for path in init_paths: + tree = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(tree): + targets = () + if isinstance(node, ast.Assign): + targets = tuple(node.targets) + if isinstance(node, ast.AnnAssign): + targets = (node.target,) + if isinstance(node, ast.AugAssign): + targets = (node.target,) + for target in targets: + assert not isinstance( + target, ast.Attribute + ), f"{path} backfills a submodule attribute via assignment" + + +def test_init_modules_use_absolute_imports_only(): + for path in iter_mytilus_python_files(): + if path.name != "__init__.py": + continue + tree = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + assert node.level == 0, f"{path} uses relative import" + + +def test_mytilus_layer_dependency_direction(): + for path in iter_mytilus_python_files(): + current_module = module_name(path) + current_layer = module_layer(current_module) + if current_layer is None: + continue + + tree = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(tree): + imported_modules: list[str] = [] + if isinstance(node, ast.Import): + imported_modules.extend(alias.name for alias in node.names) + if isinstance(node, ast.ImportFrom): + imported_modules.append(resolve_imported_module(current_module, node)) + + for imported_module in imported_modules: + imported_layer = module_layer(imported_module) + if imported_layer is None or imported_layer == current_layer: + continue + assert LAYER_INDEX[current_layer] <= LAYER_INDEX[imported_layer], ( + f"{path} violates layer order {LAYER_ORDER}: " + f"{current_module} imports {imported_module}" + ) + + +def _canonical_module(path: Path) -> str: + parts = path.with_suffix("").parts + if parts[-1] == "__init__": + return ".".join(parts[:-1]) + return ".".join(parts) + + +def _module_import_graph() -> dict[str, set[str]]: + module_paths = {_canonical_module(path): path for path in iter_mytilus_python_files()} + + def normalize(module: str) -> str: + if module in module_paths: + return module + return f"{module}.__init__" if f"{module}.__init__" in module_paths else module + + graph = {module: set() for module in module_paths} + for module, path in module_paths.items(): + tree = ast.parse(path.read_text(), filename=str(path)) + for node in tree.body: + imported_modules: list[str] = [] + if isinstance(node, ast.Import): + imported_modules.extend(alias.name for alias in node.names) + if isinstance(node, ast.ImportFrom): + imported_modules.append(resolve_imported_module(module, node)) + for imported_module in imported_modules: + target = normalize(imported_module) + if target in module_paths and target != module: + graph[module].add(target) + return graph + + +def test_mytilus_top_level_imports_are_acyclic(): + graph = _module_import_graph() + visited: set[str] = set() + on_stack: set[str] = set() + + def visit(module: str): + if module in on_stack: + raise AssertionError(f"top-level import cycle contains {module}") + if module in visited: + return + visited.add(module) + on_stack.add(module) + for dependency in graph[module]: + visit(dependency) + on_stack.remove(module) + + for module in sorted(graph): + visit(module) diff --git a/tests/test_runner.py b/tests/test_runner.py index 5edd5d5..946e05b 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -6,8 +6,8 @@ from nx_yaml import nx_compose_all from mytilus.metaprog.hif import HIFToLoader +from mytilus.state import SHELL_INTERPRETER from mytilus.state.loader import LoaderToShell -from mytilus.state.python import SHELL_INTERPRETER from mytilus.state.mytilus import ShellSpecializer diff --git a/tests/test_state_integration.py b/tests/test_state_integration.py new file mode 100644 index 0000000..b48dfff --- /dev/null +++ b/tests/test_state_integration.py @@ -0,0 +1,16 @@ +from mytilus.comput.mytilus import io_ty +from mytilus.pcc import LOADER, SHELL +from mytilus.state.loader import LoaderExecution +from mytilus.state.mytilus import ShellExecution +from mytilus.wire.loader import loader_stream_ty + + +def test_loader_and_shell_projections_live_in_state(): + assert LoaderExecution().state_update_diagram() == LOADER.execution( + loader_stream_ty, loader_stream_ty + ).state_update_diagram() + assert LoaderExecution().output_diagram() == LOADER.execution( + loader_stream_ty, loader_stream_ty + ).output_diagram() + assert ShellExecution().state_update_diagram() == SHELL.execution(io_ty, io_ty).state_update_diagram() + assert ShellExecution().output_diagram() == SHELL.execution(io_ty, io_ty).output_diagram() diff --git a/tests/test_watch.py b/tests/test_watch.py index 18b9f60..3c54904 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -6,7 +6,7 @@ import mytilus.watch as watch from mytilus.comput.mytilus import Command, io_ty from mytilus.metaprog.hif import HIFToLoader -from mytilus.pcc.mytilus import SHELL +from mytilus.pcc import SHELL from mytilus.state.loader import LoaderToShell from mytilus.state.mytilus import terminal_passthrough_command from mytilus.watch import CTRL_D, CTRL_J, CTRL_M, apply_tty_input, emit_shell_source, read_shell_source, watch_log diff --git a/tests/test_wire_integration.py b/tests/test_wire_integration.py new file mode 100644 index 0000000..37a5a8d --- /dev/null +++ b/tests/test_wire_integration.py @@ -0,0 +1,15 @@ +from mytilus.comput.mytilus import io_ty as shell_io_ty +from mytilus.wire.loader import loader_id, loader_stream_ty +from mytilus.wire.mytilus import Copy as ShellCopy, shell_id + + +def test_loader_wire_module_exports_loader_specific_wiring(): + assert loader_id().dom == loader_stream_ty + assert loader_id().cod == loader_stream_ty + + +def test_mytilus_wire_module_exports_shell_specific_wiring(): + assert shell_id().dom == shell_io_ty + assert shell_id().cod == shell_io_ty + assert ShellCopy(3).dom == shell_io_ty + assert ShellCopy(3).cod == shell_io_ty @ shell_io_ty @ shell_io_ty