Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f83b59b
wip
colltoaction Dec 10, 2025
3f5208c
discopy <-> hif representing
colltoaction Dec 11, 2025
91a5748
remove failing tests
colltoaction Dec 11, 2025
62970a8
fix representing. it has many TODOs after Jules generated a POC solut…
colltoaction Dec 11, 2025
2daebc8
remove autogenerated representing.py
colltoaction Dec 12, 2025
75fed61
update nx-yaml
colltoaction Dec 12, 2025
104e9a6
remove unused composing code
colltoaction Dec 12, 2025
d16ddc2
discopy_to_hif
colltoaction Dec 17, 2025
02c98f7
create bin/widish and bin/py/python.yaml executables to stop calling …
colltoaction Dec 18, 2025
3e83d47
change box G to tag names
colltoaction Dec 18, 2025
d10bdad
loader improvements
colltoaction Dec 18, 2025
6f774c6
Update discopy to 1.2.2 with generics fix
colltoaction Dec 19, 2025
9fbc8b0
improve SHELL_RUNNER definition
colltoaction Dec 19, 2025
a3ad916
Enable stdin piping
santuchoagus Dec 19, 2025
3e07749
introduce compiler.py
colltoaction Dec 21, 2025
6c093c1
Implement __debug__ guard for JPGs
colltoaction Dec 22, 2025
63dc748
Interactive shell tests using pytest
colltoaction Dec 22, 2025
7688784
Fix empty map and list in loader.py
colltoaction Dec 22, 2025
bc6d1f4
async main
colltoaction Dec 22, 2025
8757d2e
migrate from watchdog to watchfiles
colltoaction Dec 22, 2025
03e6d00
Fix tty, stdout and stderr split
colltoaction Dec 22, 2025
b412f0c
thunk + force
colltoaction Dec 22, 2025
3656a20
asyncio subprocess
colltoaction Dec 22, 2025
03df57c
shell function
colltoaction Dec 22, 2025
b7037f6
quickfix
colltoaction Dec 22, 2025
cb2c0e8
POSIX Shell Utility Requirements
colltoaction Dec 23, 2025
e407539
thunk module
colltoaction Dec 23, 2025
3215688
improve debug mode module loading
colltoaction Dec 23, 2025
46b0015
to_hif, from_hif
colltoaction Dec 23, 2025
6e85bcb
thunk improvements
colltoaction Dec 23, 2025
b4d0ef2
split watch.py into files
colltoaction Dec 23, 2025
7d4223c
hif quickfix
colltoaction Dec 23, 2025
abfc01c
Fix thunk dag and cycle
colltoaction Dec 23, 2025
b913fc9
introduce foliation
colltoaction Dec 23, 2025
96f8f35
Introduce higher-order abstractions
google-labs-jules[bot] Dec 24, 2025
f1bf036
Refactor interactive logic to watch
google-labs-jules[bot] Dec 24, 2025
1bba3e9
YAML category
google-labs-jules[bot] Dec 24, 2025
3ee11f5
thunk refactor using inspect
colltoaction Dec 24, 2025
717f343
decouple thunk unwrap from recurse
colltoaction Dec 24, 2025
bae3e66
JPG makefile
colltoaction Dec 25, 2025
935e65a
Introduce diagram box classes
colltoaction Dec 26, 2025
f5a0a90
Use appropriate names for Programs as Diagrams
google-labs-jules[bot] Dec 26, 2025
4253ec6
process class fix
colltoaction Dec 26, 2025
2b2e3f6
loader exponential fixes
colltoaction Dec 26, 2025
1dd7ff8
symmetric.Swap, markov.Copy, markov.Discard
colltoaction Dec 26, 2025
7a347bc
rename yaml boxes
colltoaction Dec 27, 2025
04e8047
add widish cast
colltoaction Dec 27, 2025
51c373e
compiler template
colltoaction Dec 27, 2025
c105355
add traced template
colltoaction Dec 27, 2025
8de43ea
New computer types
colltoaction Dec 27, 2025
1da00cb
Improve scalar compilation to data
colltoaction Dec 28, 2025
47bd5fe
add Exec template
colltoaction Dec 28, 2025
c57f750
add thunk helpers
colltoaction Dec 29, 2025
266b204
thunk map and reduce
colltoaction Dec 29, 2025
b21fa9c
introduce contextlib and contexvars
colltoaction Dec 30, 2025
2ca48b3
Simplify load_scalar, unify compiler (#39)
google-labs-jules[bot] Dec 30, 2025
a794a89
alias-anchor loading template
colltoaction Dec 30, 2025
03e201f
move watch functions to interactive
colltoaction Dec 30, 2025
6332516
dependencies fix
colltoaction Dec 30, 2025
a6d1438
compiler addition
colltoaction Dec 30, 2025
fc06307
fix weakref in thunk
colltoaction Dec 30, 2025
30ff456
update nx-yaml>=1.0.0
colltoaction Dec 30, 2025
851b827
use contextlib in loader.py
colltoaction Dec 31, 2025
0b2744b
bin README
colltoaction Dec 31, 2025
e7cb7cd
Refactor YAML Sequence and Mapping to be closed Bubbles (#41)
colltoaction Dec 31, 2025
2d354fb
!exec tag compilation and execution via Gamma (#42)
google-labs-jules[bot] Dec 31, 2025
702c819
Refactor Process and Widish
colltoaction Jan 1, 2026
4cbe73b
YAML-based test harness (#44)
colltoaction Jan 1, 2026
8090682
Refactor thunk to use native async/await and support generic brace ex…
google-labs-jules[bot] Jan 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Widip
*.jpg

# VS code
.vscode

Expand Down Expand Up @@ -64,6 +67,7 @@ cover/

# Django stuff:
*.log
!tests/*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
Expand Down
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
JPG_FILES := $(shell git ls-files '*.jpg')
YAML_FILES := $(JPG_FILES:.jpg=.yaml)

.PHONY: all clean

all: $(JPG_FILES)

%.jpg: %.yaml
@echo "Generating $@..."
@echo $< | bin/yaml/shell.yaml

clean:
rm -f $(JPG_FILES)
24 changes: 24 additions & 0 deletions bin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Execution Model

In `widip`, YAML tags such as `!eval`, `!read`, or `!print` are interpreted as **commands to execute**.

## Using Executables

You can use **any executable** that exists in your system's `$PATH` or by providing a relative/absolute path.

**Examples:**

* **System tools:** `!python`, `!grep`, `!awk`.
* **Custom scripts:** `!./myscript.sh`.

## Using Other YAML Files

You can also use other `widip` YAML files as commands, provided they are executable (e.g., they have a valid shebang like `#!bin/widish`). This allows you to compose complex pipelines from smaller, reusable diagrams.

**Example:**

```yaml
- !executable.yaml
```

This will execute the `executable.yaml` diagram as a step in your current pipeline.
2 changes: 2 additions & 0 deletions bin/widish
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
exec python -O -m widip "$@"
2 changes: 2 additions & 0 deletions bin/yaml/python.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!bin/widish
!python
2 changes: 2 additions & 0 deletions bin/yaml/range.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/widish
!seq { 1, 100 }
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Hello world!

![](hello-world.jpg)

![](hello-world.shell.jpg)

## Script

```
Expand All @@ -21,6 +23,8 @@ $ python -m widip examples/shell.yaml

![IMG](shell.jpg)

![IMG](shell.shell.jpg)


# 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.
Expand Down
Binary file removed examples/aoc2025/1-1.jpg
Binary file not shown.
Binary file modified examples/hello-world.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/hello-world.shell.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/shell.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/shell.shell.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/shell.yaml.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ name = "widip"
version = "0.1.0"
description = "Widip is an interactive environment for computing with wiring diagrams in modern systems"
dependencies = [
"discopy>=1.2.1", "pyyaml>=6.0.1", "watchdog>=4.0.1", "nx-yaml==0.3.0",
"discopy>=1.2.2", "watchfiles>=1.1.1", "nx-yaml>=1.0.0",
]

[project.optional-dependencies]
test = [
"pytest",
"pytest-asyncio",
]

[project.urls]
"Source" = "https://github.com/colltoaction/widip"
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
27 changes: 27 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Widish Tests

This directory contains test cases for the `widish` shell environment. `widish` combines the familiarity of shell commands with the structure and composability of YAML.

## What is Widish?

`widish` allows you to write shell scripts as YAML documents. Data flows through the structure, enabling:

- **Structured Pipelines**: Use YAML sequences (lists) to pipe data between commands.
- **Structured Data**: Pass structured data (like YAML mappings) between processes, not just text streams.
- **Composition**: Reuse YAML files as scripts/commands within other YAML scripts.
- **Implicit Parallelism**: Use mappings to branch data flow to multiple commands simultaneously.

## Running Tests

Tests are run using `pytest` and the `tests/test_harness.py` script. The harness executes each `.test.yaml` file using `bin/widish` and compares the standard output to the corresponding `.log` file.

```bash
pytest tests/test_harness.py
```

## Test Case Format

Each test case consists of two files:

1. `tests/CASE.test.yaml`: The input YAML script.
2. `tests/CASE.log`: The expected standard output.
2 changes: 2 additions & 0 deletions tests/fan-out.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
2
WIDIP STUFF
4 changes: 4 additions & 0 deletions tests/fan-out.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- !echo { "widip", "project" }
- !tr { "[:lower:]", "[:upper:]" }
- ? !wc -w
? !sed "s/PROJECT/STUFF/"
2 changes: 2 additions & 0 deletions tests/git-first-commit.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
? !id 8f20e0d66c3a6587ccd484642cea4d5db9eb9756
? !date "Tue Feb 6 12:12:01 2024 -0300"
3 changes: 3 additions & 0 deletions tests/git-first-commit.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!git { log, "--max-parents=0" }:
!grep commit: !sed "s/commit /? !id /"
!grep Date: !sed "s/Date: /? !date \"/;s/$/\"/"
5 changes: 5 additions & 0 deletions tests/infinite-counter.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
1
2
3
4
5
2 changes: 2 additions & 0 deletions tests/infinite-counter.test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- !bin/yaml/range.yaml
- !head { -n, 5 }
37 changes: 37 additions & 0 deletions tests/test_harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest
import subprocess
import glob
import os

# Find all test cases
TEST_DIR = os.path.dirname(__file__)
TEST_CASES = sorted(glob.glob(os.path.join(TEST_DIR, "*.test.yaml")))

@pytest.mark.parametrize("test_file", TEST_CASES)
def test_case(test_file):
# Determine the log file path
log_file = test_file.replace(".test.yaml", ".log")

# Check if log file exists
assert os.path.exists(log_file), f"Log file missing for {test_file}"

# Read input and expected output
with open(test_file, "r") as f:
input_content = f.read()

with open(log_file, "r") as f:
expected_output = f.read()

# Run the shell
# Assuming running from repo root
cmd = ["bin/widish", test_file]

result = subprocess.run(
cmd,
text=True,
capture_output=True,
check=False
)

# Assert output
assert result.stdout == expected_output
51 changes: 42 additions & 9 deletions widip/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import sys
if __debug__:
# Non-interactive backend for file output
import matplotlib
matplotlib.use('agg')

# Stop starting a Matplotlib GUI
import matplotlib
matplotlib.use('agg')
import argparse
import asyncio

from .watch import shell_main, widish_main
from .interactive import async_shell_main, async_widish_main, async_command_main
from .watch import run_with_watcher


match sys.argv:
case [_]:
shell_main("bin/yaml/shell.yaml")
case [_, file_name, *args]: widish_main(file_name, *args)
def main():
parser = argparse.ArgumentParser(
description="Widip: an interactive environment for computing with wiring diagrams"
)
parser.add_argument(
"-c",
dest="command_string",
help="read commands from the first non-option argument"
)
parser.add_argument(
"operands",
nargs=argparse.REMAINDER,
help="[command_string | file] [arguments...]"
)

args = parser.parse_args()

try:
if args.command_string is not None:
asyncio.run(async_command_main(args.command_string, *args.operands))
elif args.operands:
file_name = args.operands[0]
file_args = args.operands[1:]
asyncio.run(async_widish_main(file_name, *file_args))
else:
async_shell_runner = async_shell_main("bin/yaml/shell.yaml")
interactive_shell = run_with_watcher(async_shell_runner)
asyncio.run(interactive_shell)
except KeyboardInterrupt:
pass


if __name__ == "__main__":
main()
56 changes: 56 additions & 0 deletions widip/compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from discopy import closed

from .computer import *
from .yaml import *


def compile_ar(ar):
if isinstance(ar, Scalar):
if ar.tag == "exec":
return Exec(ar.dom, ar.cod)
if ar.tag:
return Program(ar.tag, dom=ar.dom, cod=ar.cod).uncurry()
return Data(ar.dom, ar.cod)
if isinstance(ar, Sequence):
inner_diagram = ar.args[0]
inside_compiled = SHELL_COMPILER(inner_diagram)

if ar.dom[:1] == Language:
return inside_compiled

if ar.n == 2:
return inside_compiled >> Pair(inside_compiled.cod, ar.cod)

return inside_compiled >> Sequential(inside_compiled.cod, ar.cod)

if isinstance(ar, Mapping):
inner_diagram = ar.args[0]
inside_compiled = SHELL_COMPILER(inner_diagram)
return inside_compiled >> Concurrent(inside_compiled.cod, ar.cod)

if isinstance(ar, Alias):
return Data(ar.dom, ar.cod) >> Copy(ar.cod, 2) >> closed.Id(ar.cod) @ Discard(ar.cod)
if isinstance(ar, Anchor):
return Copy(ar.dom, 2) >> closed.Id(ar.dom) @ Discard(ar.dom)
return ar


class ShellFunctor(closed.Functor):
def __init__(self):
super().__init__(
lambda ob: ob,
compile_ar,
dom=Yaml,
cod=Computation
)

SHELL_COMPILER = ShellFunctor()


def compile_shell_program(diagram):
"""
close input parameters (constants)
drop outputs matching input parameters
all boxes are io->[io]"""
diagram = SHELL_COMPILER(diagram)
return diagram
Loading