diff --git a/interactive/examples/aoc2023/.gitignore b/interactive/examples/aoc2023/.gitignore
new file mode 100644
index 000000000..e8e450bed
--- /dev/null
+++ b/interactive/examples/aoc2023/.gitignore
@@ -0,0 +1 @@
+gen/
diff --git a/interactive/examples/aoc2023/GAPS.md b/interactive/examples/aoc2023/GAPS.md
new file mode 100644
index 000000000..f0521c2f6
--- /dev/null
+++ b/interactive/examples/aoc2023/GAPS.md
@@ -0,0 +1,74 @@
+# Language gaps, from the AoC 2023 survey
+
+Thirteen recurring gaps hit while expressing the 33 parts in this suite,
+ranked by how many days each would have unlocked or simplified. They are
+recorded together here rather than as thirteen issues: most are one-line
+asks whose real content is the workaround they forced, and they rank
+against each other better than they read alone. Each entry names what is
+missing, how the programs coped, and the smallest change that would have
+sufficed.
+
+1. **Integer division family: `/`, `%`, shifts.** Worked around on four
+ days — enumeration fixpoints (day 6), parity folds (10p1), relational
+ modulo via a k-table cross join (15p1), a 40-step binary-halving gadget
+ (18) — and a named blocker for days 8, 23, 24. Minimal fix: `/` and `%`
+ BinOps. The highest value per implementation line in the survey.
+2. **No `sum`/`max` reducers.** Every solved day paid this tax: sum is
+ `collect` + `fold` (materializing a list to add numbers), max is `min`
+ over `BIG - x` or a negate — the latter interacting badly with corgi's
+ encoding-order contract for negatives. Minimal fix: `sum` and `max`
+ beside `min` and `count`.
+3. **`distinct` erases the value to unit**, so every iterative loop's state
+ must be packed into keys and re-projected around every join (days 3, 5,
+ 10, 14, 16, 18, 19, 22). Minimal fix: a value-preserving idempotent
+ reducer — `distinct` on rows, or blessing `min` as the var-reducer idiom
+ (days 17 and 22 show it works).
+4. **No scalar let-bindings or user scalar functions**; repeated
+ subexpressions are copy-pasted or staged through extra `map`s (days 1p2,
+ 7, 18, 19 — day 19's projections had to be machine-generated). Minimal
+ fix: `let x = t in t` in the scalar grammar.
+5. **No collection-level abstraction** (parameterized subgraphs): day
+ 14p2's four roll directions and day 16's two beam programs are
+ copy-pasted pipelines. Minimal fix: named subgraph definitions with
+ collection parameters.
+6. **No string/char literals**; all text logic is numeric charcode
+ comparison (days 1, 10, 13, 15, 16, 18, 19). Minimal fix: `'a'` char
+ literals, `"abc"` as list-of-codes sugar.
+7. **`a - b` is multiset subtraction**; the natural "set minus" reading
+ silently corrupts counts when `b` has rows outside `a` (bit day 10;
+ latent everywhere). Idiom: `a - (a |> join b)`. Minimal fix: an `except`
+ operator.
+8. **One `EDGES_FILE` round-robined across inputs** makes multi-relation
+ programs awkward; the tag+filter workaround (days 5, 19, 22) then runs
+ into corgi's arity-uniformity contract. Minimal fix: `EDGES_FILE_0`,
+ `EDGES_FILE_1`, ...
+9. **No extrinsic iteration bound** (SQL's `RETURN AT RECURSION LIMIT`);
+ bounds must be encoded in the data. This is ergonomics, not expressive
+ power: a one-round stall (`var d = x;` then `x + (d | negate)` fires at
+ round 0 only) seeds a marker that marches `k -> k+1` under
+ `filter($0[0] < K)` with O(1) live rows per round — the full
+ delayed-pulse stepwise driver, in the language today. The residual ask
+ is sugar: an iteration cap on scopes, or a blessed pulse idiom in the
+ examples.
+10. **No substring/window primitive**: k-char windows need k−1 self-joins
+ (day 1p2); line equality via collected lists (day 13). Minimal fix: a
+ windowing flatmap or list-slicing scalars.
+11. **A reduce's List value cannot be re-projected as a single field**:
+ bare `$1` splices, `$1[0]` takes an element; idiom `if(1, $1, 0)`
+ (day 13). Minimal fix: a whole-value field form, e.g. `val($n)`.
+12. **`&&` exists but `||` doesn't** (workaround `or(a,b)`); trivial
+ grammar fix (day 10).
+13. **No non-integer arithmetic** (float/decimal, sqrt): the day 25
+ blocker; contributes to 6 and 24. Worth fixing only if DDIR wants
+ numeric-analysis workloads — everything else in AoC fits i64.
+
+## Near-misses that are not gaps
+
+Two things initially read as gaps and turned out not to be. The
+delayed-pulse driver above (entry 9) is fully expressible via the `var`
+stall — the survey's first pass got this wrong, and the construction is
+worth knowing. And several days where SQL's stepwise idioms would not
+transliterate ended up with *better* programs for it: day 14's closed-form
+roll and day 22's interval-overlap fall are shorter, clearer, and more
+incremental than their SQL counterparts. Absence of a feature is
+occasionally load-bearing.
diff --git a/interactive/examples/aoc2023/README.md b/interactive/examples/aoc2023/README.md
new file mode 100644
index 000000000..652f26c91
--- /dev/null
+++ b/interactive/examples/aoc2023/README.md
@@ -0,0 +1,63 @@
+# AoC 2023 in DDIR
+
+A language-coverage and regression suite: 33 parts across 17 days of Advent
+of Code 2023, expressed in pipe-form DDIR (`.ddp`) and checked against the
+answers of Materialize's sqllogictest oracles
+(`test/sqllogictest/advent-of-code/2023/`) — same inputs, same expected
+answers, oracle quirks reproduced where needed to match.
+
+Inputs are committed in their dense original text form (`dayNN/input.txt`,
+the compact puzzle text as it appears in the slt oracles); `transcribe.py`
+mechanically regenerates the i64 fact files the programs read (text →
+`(line, pos, charcode)`, grids → `(row, col, cell)`, names → ids) into the
+gitignored `gen/` directory at runtime. All puzzle logic lives in the
+`.ddp` programs. `transcribe.py --pad` additionally writes day05's
+arity-padded fact files for the corgi backend, which crashes on
+mixed-arity inputs.
+
+## Run
+
+ cargo build --release --example ddir
+ ./run.sh # every part on the vec backend vs expected.txt; nonzero exit on any mismatch
+
+`run.sh` first runs `transcribe.py` (python3) to regenerate `gen/`, then
+runs each part as `EDGES_FILE=gen/ ddir --backend=vec
+dayNN/partN.ddp 10 0 1 0`; the answer is the `Int` on the
+`[partN]` inspect line.
+
+`run.sh` asserts the **vec** backend only; corgi currently disagrees or
+crashes on 4 of these parts (known open issues, minimized separately) and
+is not asserted yet.
+
+## Verdicts
+
+CLEAN = direct expression; AWKWARD = right answer via a workaround idiom.
+
+| day | p1 | p2 | note |
+|-----|----|----|------|
+| 01 | clean | awkward | no string literals: digit words matched as charcode filters over k-wide windows (k−1 self-joins) |
+| 02 | clean | clean | anti-join as `games - bad`; max via `min` of (BIG − x) |
+| 03 | awkward | awkward | run assembly needs 2 fixpoints; `distinct` erases values, so loop state is packed into keys |
+| 04 | clean | clean | p2: card copies ARE collection multiplicities; the cascade is a bare `var` fixpoint |
+| 05 | clean | awkward | stage-chain fixpoint, identity fallback = anti-join; p2 range split/holes all key-packed |
+| 06 | awkward | — | no `/` or sqrt: hold times enumerated by fixpoint; slt has no separate part 2 |
+| 07 | clean | awkward | hand type = pairwise-equality count; p2 stages maps to stand in for scalar lets |
+| 10 | clean | awkward | loop = fixpoint, n/2 via parity fold; p2: `-` is multiset subtraction, set-minus needs an explicit anti-join |
+| 11 | clean | clean | gap rows/cols by anti-join; L1 over all-pairs (slt input has no gaps, so p1 = p2) |
+| 13 | awkward | clean | whole-line equality via collected Lists; p2 smudge = exactly one mismatched mirrored cell |
+| 14 | clean | awkward | p1: closed-form roll, zero recursion; p2: round-tagged fixpoint with 4 hand-unrolled direction pipelines |
+| 15 | awkward | clean | no `%`: modulo done relationally via a k-table cross join; p2 boxes need no sequencing |
+| 16 | clean | clean | beam automaton with a literal transition table via flatmap |
+| 17 | clean | clean | Bellman-Ford: `var mc = seeds + moves \| min`; p2 = 3 filter tweaks on p1 |
+| 18 | awkward | awkward | shoelace prefix fixpoint; /2 via a 40-step binary-decomposition gadget; p2 hex decoded from charcodes |
+| 19 | clean | clean | first-match = min (priority, next) inside the fixpoint; p2 reproduces the slt's negative-product quirk |
+| 22 | clean | clean | interval overlap, no cell expansion; p2's count-inside-fixpoint converges fine |
+
+Days 08, 09, 12, 20, 21, 23, 24 are absent because their slt files carry no
+usable oracle (EXPLAIN-only, stubbed, or self-inconsistent); day 25's oracle
+is an artifact of floating-point spectral iteration, which DDIR (integers
+only) cannot chase.
+
+The survey behind this suite also produced a ranked catalogue of 13
+recurring language gaps (integer division/modulo, `max`/`sum` reducers,
+value-preserving `distinct`, scalar lets, ...): see [GAPS.md](GAPS.md).
diff --git a/interactive/examples/aoc2023/day01/input.txt b/interactive/examples/aoc2023/day01/input.txt
new file mode 100644
index 000000000..1f82e6e40
--- /dev/null
+++ b/interactive/examples/aoc2023/day01/input.txt
@@ -0,0 +1,9 @@
+noveneiner9
+seventwoseven114
+1two4two
+hell0l1
+79430242
+159a951
+0
+seven2seven
+h4mb5rg
diff --git a/interactive/examples/aoc2023/day01/part1.ddp b/interactive/examples/aoc2023/day01/part1.ddp
new file mode 100644
index 000000000..a10cc25e5
--- /dev/null
+++ b/interactive/examples/aoc2023/day01/part1.ddp
@@ -0,0 +1,18 @@
+-- AoC 2023 day 1 part 1: per line, first digit * 10 + last digit; sum.
+-- input 0: (line, pos, charcode)
+
+let digits = input 0
+ | filter($0[2] >= 48 && $0[2] <= 57)
+ | key($0[0] ; $0[1], $0[2] - 48);
+-- (line ; pos, digit)
+
+let first = digits | min | map($0 ; $1[1]);
+let last = digits | map($0 ; 0 - $1[0], $1[1]) | min | map($0 ; $1[1]);
+
+let calib = first | join(last, ($0 ; $1[0] * 10 + $2[0]));
+-- (line ; value)
+
+export "result" = calib
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day01/part2.ddp b/interactive/examples/aoc2023/day01/part2.ddp
new file mode 100644
index 000000000..6cb6783b0
--- /dev/null
+++ b/interactive/examples/aoc2023/day01/part2.ddp
@@ -0,0 +1,43 @@
+-- AoC 2023 day 1 part 2: digits and spelled-out digit words count; sum of
+-- first*10+last per line. Words matched by charcode against windows built
+-- from k successive self-joins (no substring primitive, no string literals).
+-- input 0: (line, pos, charcode)
+
+let chars = input 0 | key($0[0], $0[1] ; $0[2]);
+-- (line, pos ; code)
+
+let s2 = chars | map($0[0], $0[1] + 1 ; $0[1], $1[0])
+ | join(chars, ($0[0], $1[0] ; $1[1], $2[0]));
+let s3 = s2 | map($0[0], $0[1] + 2 ; $0[1], $1[0], $1[1])
+ | join(chars, ($0[0], $1[0] ; $1[1], $1[2], $2[0]));
+let s4 = s3 | map($0[0], $0[1] + 3 ; $0[1], $1[0], $1[1], $1[2])
+ | join(chars, ($0[0], $1[0] ; $1[1], $1[2], $1[3], $2[0]));
+let s5 = s4 | map($0[0], $0[1] + 4 ; $0[1], $1[0], $1[1], $1[2], $1[3])
+ | join(chars, ($0[0], $1[0] ; $1[1], $1[2], $1[3], $1[4], $2[0]));
+-- sk: (line, pos ; c0..c{k-1})
+
+let digits = chars | filter($1[0] >= 48 && $1[0] <= 57)
+ | map($0[0] ; $0[1], $1[0] - 48);
+
+let w_zero = s4 | filter($1[0]==122 && $1[1]==101 && $1[2]==114 && $1[3]==111) | map($0[0] ; $0[1], 0);
+let w_one = s3 | filter($1[0]==111 && $1[1]==110 && $1[2]==101) | map($0[0] ; $0[1], 1);
+let w_two = s3 | filter($1[0]==116 && $1[1]==119 && $1[2]==111) | map($0[0] ; $0[1], 2);
+let w_three = s5 | filter($1[0]==116 && $1[1]==104 && $1[2]==114 && $1[3]==101 && $1[4]==101) | map($0[0] ; $0[1], 3);
+let w_four = s4 | filter($1[0]==102 && $1[1]==111 && $1[2]==117 && $1[3]==114) | map($0[0] ; $0[1], 4);
+let w_five = s4 | filter($1[0]==102 && $1[1]==105 && $1[2]==118 && $1[3]==101) | map($0[0] ; $0[1], 5);
+let w_six = s3 | filter($1[0]==115 && $1[1]==105 && $1[2]==120) | map($0[0] ; $0[1], 6);
+let w_seven = s5 | filter($1[0]==115 && $1[1]==101 && $1[2]==118 && $1[3]==101 && $1[4]==110) | map($0[0] ; $0[1], 7);
+let w_eight = s5 | filter($1[0]==101 && $1[1]==105 && $1[2]==103 && $1[3]==104 && $1[4]==116) | map($0[0] ; $0[1], 8);
+let w_nine = s4 | filter($1[0]==110 && $1[1]==105 && $1[2]==110 && $1[3]==101) | map($0[0] ; $0[1], 9);
+
+let findings = digits + w_zero + w_one + w_two + w_three + w_four + w_five + w_six + w_seven + w_eight + w_nine;
+-- (line ; pos, digit)
+
+let first = findings | min | map($0 ; $1[1]);
+let last = findings | map($0 ; 0 - $1[0], $1[1]) | min | map($0 ; $1[1]);
+let calib = first | join(last, ($0 ; $1[0] * 10 + $2[0]));
+
+export "result" = calib
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day02/input.txt b/interactive/examples/aoc2023/day02/input.txt
new file mode 100644
index 000000000..3b9787f1c
--- /dev/null
+++ b/interactive/examples/aoc2023/day02/input.txt
@@ -0,0 +1,5 @@
+Game 1: 1 red, 4 green, 4 blue; 3 red, 4 blue; 2 red, 1 green; 1 green, 1 red
+Game 2: 20 green, 1 red; 1 blue, 4 red; 1 green, 1 red, 1 blue; 1 green, 1 red, 1 blue
+Game 3: 8 red, 1 blue, 5 green; 9 red, 3 blue, 3 green; 3 green, 4 red
+Game 4: 0 blue, 8 red, 4 green; 7 green, 8 blue, 9 red; 2 green, 5 red, 1 blue
+Game 5: 20 red, 10 blue, 9 green; 2 blue, 3 red, 7 green; 1 green, 3 blue
diff --git a/interactive/examples/aoc2023/day02/part1.ddp b/interactive/examples/aoc2023/day02/part1.ddp
new file mode 100644
index 000000000..9bae18c25
--- /dev/null
+++ b/interactive/examples/aoc2023/day02/part1.ddp
@@ -0,0 +1,13 @@
+-- AoC 2023 day 2 part 1: sum of game ids where every set fits 13g/12r/14b.
+-- input 0: (game, set, green, red, blue)
+
+let games = input 0 | key($0[0] ;) | distinct;
+let bad = input 0
+ | filter(not($0[2] <= 13 && $0[3] <= 12 && $0[4] <= 14))
+ | key($0[0] ;) | distinct;
+let good = games - bad;
+
+export "result" = good
+ | key(; $0[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day02/part2.ddp b/interactive/examples/aoc2023/day02/part2.ddp
new file mode 100644
index 000000000..aacb59298
--- /dev/null
+++ b/interactive/examples/aoc2023/day02/part2.ddp
@@ -0,0 +1,16 @@
+-- AoC 2023 day 2 part 2: per game, max needed of each color; sum of products.
+-- max via min of (BIG - x), avoiding negatives.
+-- input 0: (game, set, green, red, blue)
+
+let maxg = input 0 | key($0[0] ; 1000000 - $0[2]) | min | map($0 ; 1000000 - $1[0]);
+let maxr = input 0 | key($0[0] ; 1000000 - $0[3]) | min | map($0 ; 1000000 - $1[0]);
+let maxb = input 0 | key($0[0] ; 1000000 - $0[4]) | min | map($0 ; 1000000 - $1[0]);
+
+let power = maxg
+ | join(maxr, ($0 ; $1[0] * $2[0]))
+ | join(maxb, ($0 ; $1[0] * $2[0]));
+
+export "result" = power
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day03/input.txt b/interactive/examples/aoc2023/day03/input.txt
new file mode 100644
index 000000000..9b317e101
--- /dev/null
+++ b/interactive/examples/aoc2023/day03/input.txt
@@ -0,0 +1,7 @@
+...14...954......104...98..........11...222.........38.104....708..........................217..................330.................19..
+.......@...................*...............................*.664........677................@....459.........187..........73.............
+....41............178.....398....*...548..495..........983.........99.........282......409........*...........$.248...............165...
+......261......300...............704.&.......*.......*........9.65..904.....6....*773....=.....680../511...2*.....=..99*....*..../......
+..........200..............398.......22...100...........&...........10.......*.......73.....833...*...........*......300.............22.
+..................@.100....*...........*...............*....19...300.....*.................@....954.......................200...........
+.....-....&..@............828...........@268..844....534...................563.........409........$..........244.........722.286........
diff --git a/interactive/examples/aoc2023/day03/part1.ddp b/interactive/examples/aoc2023/day03/part1.ddp
new file mode 100644
index 000000000..a05a5a324
--- /dev/null
+++ b/interactive/examples/aoc2023/day03/part1.ddp
@@ -0,0 +1,47 @@
+-- AoC 2023 day 3 part 1: sum of numbers adjacent (8-way) to a symbol.
+-- input 0: (row, col, charcode); '.' = 46, digits 48..57.
+-- NB: `distinct` erases values, so loop state is packed into keys.
+
+let cells = input 0 | filter($0[2] != 46) | key($0[0], $0[1] ; $0[2]);
+let numerals = cells | filter($1[0] >= 48 && $1[0] <= 57) | map($0 ; $1[0] - 48);
+let symbols = cells | filter(not($1[0] >= 48 && $1[0] <= 57));
+
+-- run starts: digit cells with no digit to the left
+let has_left = numerals | map($0[0], $0[1] + 1 ; $1[0]) | join(numerals, ($0 ; $2[0]));
+let starts = numerals - has_left;
+
+-- grow runs rightward: key (row, start, end_exclusive, value)
+runs: {
+ let grow = run | map($0[0], $0[2] ; $0[1], $0[3])
+ | join(numerals, ($0[0], $1[0], $0[1] + 1, $1[1] * 10 + $2[0] ;));
+ var run = (starts | map($0[0], $0[1], $0[1] + 1, $1[0] ;)) + grow | distinct;
+}
+let extendable = runs::run | map($0[0], $0[2] ; $0[1], $0[3])
+ | join(numerals, ($0[0], $1[0], $0[1], $1[1] ;));
+let maximal = runs::run - extendable;
+-- key (row, start, end, value)
+
+-- label each digit cell with its run start: key (row, col, start)
+labels: {
+ let ext = label | map($0[0], $0[1] + 1 ; $0[2]) | join(numerals, ($0[0], $0[1], $1[0] ;));
+ var label = (starts | map($0[0], $0[1], $0[1] ;)) + ext | distinct;
+}
+
+-- digit cells adjacent to a symbol
+let symnbrs = symbols
+ | flatmap(list(tuple(-1,-1), tuple(-1,0), tuple(-1,1),
+ tuple(0,-1), tuple(0,0), tuple(0,1),
+ tuple(1,-1), tuple(1,0), tuple(1,1)))
+ | map($0[0] + $1[1][0], $0[1] + $1[1][1] ;) | distinct;
+
+let active_runs = labels::label
+ | map($0[0], $0[1] ; $0[2]) | join(symnbrs, ($0[0], $1[0] ;)) | distinct;
+-- key (row, start) of runs adjacent to a symbol
+
+let part_numbers = maximal | map($0[0], $0[1] ; $0[3]) | join(active_runs, ($0 ; $1[0]));
+-- (row, start ; value)
+
+export "result" = part_numbers
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day03/part2.ddp b/interactive/examples/aoc2023/day03/part2.ddp
new file mode 100644
index 000000000..8948c71ff
--- /dev/null
+++ b/interactive/examples/aoc2023/day03/part2.ddp
@@ -0,0 +1,52 @@
+-- AoC 2023 day 3 part 2: gears = '*' adjacent to exactly two part numbers;
+-- sum of the two numbers' product per gear.
+-- input 0: (row, col, charcode)
+
+let cells = input 0 | filter($0[2] != 46) | key($0[0], $0[1] ; $0[2]);
+let numerals = cells | filter($1[0] >= 48 && $1[0] <= 57) | map($0 ; $1[0] - 48);
+let gears = cells | filter($1[0] == 42);
+
+let has_left = numerals | map($0[0], $0[1] + 1 ; $1[0]) | join(numerals, ($0 ; $2[0]));
+let starts = numerals - has_left;
+
+runs: {
+ let grow = run | map($0[0], $0[2] ; $0[1], $0[3])
+ | join(numerals, ($0[0], $1[0], $0[1] + 1, $1[1] * 10 + $2[0] ;));
+ var run = (starts | map($0[0], $0[1], $0[1] + 1, $1[0] ;)) + grow | distinct;
+}
+let extendable = runs::run | map($0[0], $0[2] ; $0[1], $0[3])
+ | join(numerals, ($0[0], $1[0], $0[1], $1[1] ;));
+let maximal = runs::run - extendable;
+let partvals = maximal | map($0[0], $0[1] ; $0[3]);
+-- (row, start ; value)
+
+labels: {
+ let ext = label | map($0[0], $0[1] + 1 ; $0[2]) | join(numerals, ($0[0], $0[1], $1[0] ;));
+ var label = (starts | map($0[0], $0[1], $0[1] ;)) + ext | distinct;
+}
+let labelcells = labels::label | map($0[0], $0[1] ; $0[2]);
+
+let gearnbrs = gears
+ | flatmap(list(tuple(-1,-1), tuple(-1,0), tuple(-1,1),
+ tuple(0,-1), tuple(0,0), tuple(0,1),
+ tuple(1,-1), tuple(1,0), tuple(1,1)))
+ | map($0[0] + $1[1][0], $0[1] + $1[1][1] ; $0[0], $0[1]);
+-- (nbr_cell ; gear)
+
+let pairs = gearnbrs
+ | join(labelcells, ($1[0], $1[1], $0[0], $2[0] ;))
+ | distinct;
+-- key (gear_r, gear_c, part_row, part_start)
+
+let gearvals = pairs
+ | map($0[2], $0[3] ; $0[0], $0[1])
+ | join(partvals, ($1[0], $1[1] ; $2[0]));
+-- (gear ; part value)
+
+export "result" = gearvals
+ | collect
+ | filter(len($1) == 2)
+ | map(; $1[0][0] * $1[1][0])
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day04/input.txt b/interactive/examples/aoc2023/day04/input.txt
new file mode 100644
index 000000000..7f0a6d002
--- /dev/null
+++ b/interactive/examples/aoc2023/day04/input.txt
@@ -0,0 +1,10 @@
+Card 1: 91 58 89 8 19 64 92 28 22 1 | 6 94 21 70 81 59 5 35 24 31 43 69 91 12 51 53 98 50 70 98 47 6 9 49 50
+Card 2: 49 56 57 80 28 9 3 19 55 6 | 35 25 76 45 35 73 12 93 29 23 50 33 75 36 4 33 90 84 1 9 44 62 99 80 85
+Card 3: 97 29 93 95 66 40 97 9 58 11 | 22 56 90 13 40 84 83 70 65 80 73 84 58 93 98 79 46 51 47 70 8 50 43 70
+Card 4: 62 79 90 45 63 70 75 26 14 92 | 70 5 69 58 80 64 72 4 36 24 40 76 79 16 79 11 80 88 49 92 15 24 5 49 22
+Card 5: 54 26 80 65 14 46 77 59 12 20 | 96 89 95 25 19 22 34 9 24 86 87 63 16 31 5 22 91 71 8 80 33 2 65 67 78
+Card 6: 22 10 58 44 5 97 97 57 88 8 | 54 50 79 45 2 40 90 30 82 37 29 99 50 90 51 84 97 62 8 4 89 82 86 59 65
+Card 7: 65 94 76 4 41 40 1 6 50 96 | 82 90 42 92 22 18 29 96 47 91 71 2 5 3 42 73 45 26 15 13 29 37 7 63 81
+Card 8: 73 19 52 43 47 54 6 86 12 34 | 25 70 26 15 35 10 65 81 48 72 98 48 18 94 8 34 6 44 79 25 77 27 78 61 28
+Card 9: 32 51 38 86 17 56 42 4 67 38 | 55 5 26 91 98 11 52 1 48 13 55 95 60 15 16 51 54 22 91 8 26 70 26 35 92
+Card 10: 6 40 74 5 31 63 1 5 12 64 | 88 7 91 54 4 62 37 66 5 69 59 78 17 47 61 2 6 56 36 59 2 71 63 87 72
diff --git a/interactive/examples/aoc2023/day04/part1.ddp b/interactive/examples/aoc2023/day04/part1.ddp
new file mode 100644
index 000000000..2a5ee34ea
--- /dev/null
+++ b/interactive/examples/aoc2023/day04/part1.ddp
@@ -0,0 +1,13 @@
+-- AoC 2023 day 4 part 1 (slt semantics): per card, numbers appearing >1
+-- time across all 35 slots; points = 2^(k-1); sum.
+-- input 0: (card, side, idx, value)
+
+let dups = input 0 | key($0[0], $0[3] ;) | count | filter($1[0] > 1);
+let points = dups
+ | map($0[0] ; $0[1]) | collect
+ | map($0 ; fold($1, 0, if(^1 == 0, 1, ^1 * 2)));
+
+export "result" = points
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day04/part2.ddp b/interactive/examples/aoc2023/day04/part2.ddp
new file mode 100644
index 000000000..2f1992909
--- /dev/null
+++ b/interactive/examples/aoc2023/day04/part2.ddp
@@ -0,0 +1,26 @@
+-- AoC 2023 day 4 part 2: cascading card copies; total card count.
+-- Copies are collection multiplicities; the cascade is a var fixpoint.
+-- input 0: (card, side, idx, value)
+
+let winners = input 0 | filter($0[1] == 0) | key($0[0], $0[3] ;);
+let ours = input 0 | filter($0[1] == 1) | key($0[0], $0[3] ;);
+let matches = ours | join(winners, ($0 ;)) | map($0[0] ;) | count;
+-- (card ; m): number of matching (ours, winners) pairs
+
+let cards = input 0 | key($0[0] ;) | distinct;
+
+-- prize targets: key (card, target) for target in card+1 .. card+m
+wt: {
+ let step = t | map($0[0] ; $0[1]) | join(matches, ($0[0], $1[0] + 1, $2[0] ;))
+ | filter($0[1] <= $0[0] + $0[2]) | map($0[0], $0[1] ;);
+ var t = (matches | filter($1[0] >= 1) | map($0[0], $0[0] + 1 ;)) + step | distinct;
+}
+let wtflat = wt::t | map($0[0] ; $0[1]);
+
+-- instances: multiplicity of (card ;) = copies of that card
+insts: {
+ let spawned = inst | join(wtflat, ($2[0] ;));
+ var inst = cards + spawned | arrange;
+}
+
+export "result" = insts::inst | key(;) | count | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day05/input.txt b/interactive/examples/aoc2023/day05/input.txt
new file mode 100644
index 000000000..00b666e47
--- /dev/null
+++ b/interactive/examples/aoc2023/day05/input.txt
@@ -0,0 +1,165 @@
+seeds: 141812878 853583433 69532151 734372491 182396959 4723992392 8947211973 5 4238233746 414976297 3674819199 51868842
+
+seed-to-soil map:
+47738968 98357 182795944
+7626588292 7848955494 927588242
+34324971 6812781938 837374212
+2166766538 726174459 6843311291
+239754864 684391 555585341
+418284536 8839949654 642749254
+9423585483 8225843682 329442345
+8712565737 9399753119 5
+74289536 9359574415 717691786
+
+soil-to-fertilizer map:
+8986393527 4888517941 585279262
+7613871113 9877446651 84269233
+2423271674 7586366221 7659569
+3622861142 916381775 74546981
+3996339781 3244535459 98765225
+586791635 8419253759 59179897
+7576358959 6127297299 519542837
+7876479671 6556651697 2721518
+8412917365 865866259 75627132
+8919467753 2818783878 5
+4729257824 8988777624 275397538
+2626168725 5 375375337
+7963626731 8731755354 562155822
+4484955989 2 31166388
+6394952326 9434564563 344217764
+5818263571 8292867479 46133627
+6635767943 663896613 39649384
+1179179943 58595967 326966131
+461712455 2756694296 566766331
+267854195 942359332 92191415
+1676162127 2415173728 7527572
+8235458679 517558444 8481131
+2193675972 5252589743 48214883
+4312181391 5588755717 5725572
+451918428 7726618552 39256967
+4734828421 378251365 56849366
+7864169188 795712941 852726511
+6498165846 819955597 825591722
+6737759128 1943367748 153632389
+81836871 3619315963 5999745237
+5533126476 1625724578 11196339
+814358688 9878499637 982477257
+4386394269 3737453299 68828912
+1 535278815 55
+9455234351 63518366 96697897
+298142454 9711262844 545237968
+6548387663 5498764299 556695254
+4421182848 3813437218 18532535
+
+fertilizer-to-water map:
+5188185212 713592736 572122634
+3996516993 8368372653 29257663
+7629744559 8723951436 36722366
+517117434 6799613578 62912912
+7444887598 1378359775 142239913
+8946622323 461231638 98648353
+895954325 5555555 419161656
+1269235415 8199792497 62639226
+277191562 35368196 783296989
+2994552458 6347418557 828619684
+7 245618496 85983123
+599564274 6975655213 441448691
+7312274117 4916534261 32238555
+444977452 8543586232 53382156
+682172835 2567878724 62974419
+8912874438 5539912916 51886594
+4257175799 1517431879 97672792
+9481282796 996563263 8713257
+8269679713 2281488697 77222754
+2566468873 4747453236 37516457
+9491212658 1459322382 56814988
+142239541 25962654 99568155
+3475998687 9 537527222
+7223664422 4413723656 2795999379
+5781744284 5851593946 52411132
+2615616119 2198338791 93894182
+725796923 6293593866 672373647
+5291123587 1 45165371
+
+water-to-light map:
+9483826148 971239373 57981779
+866572267 5355262121 658481698
+6149496513 2245492751 89485475
+7743881746 5949772182 31781539
+915838939 73797715 9628255536
+49687174 729495672 53487321
+1774464825 4513912578 89436881
+734978995 62382679 39621782
+8579142462 9 79959373
+3999434125 1722538882 315735941
+9758171681 5256942648 79846743
+676724382 222319917 43462458
+9842566327 2265547154 99978196
+3885113248 6341877917 53731483
+7451763582 1 22747236
+2222665944 8558249644 858131997
+2222613482 1369627666 23224913
+2122417343 892313426 54167146
+9888331148 1128482359 94371883
+55684927 195154981 59748169
+8819525361 7614218117 73293139
+241798649 664138941 854619722
+9716684718 3664657195 253795579
+3431191398 9414159725 8345323
+4742633341 757873782 42893665
+5976897431 6681469458 1532556
+5 947566478 61872123
+8793878654 8231529123 34617151
+136924369 6845534171 1385592
+538695647 894877235 85583498
+3327525594 1748821684 434778753
+
+light-to-temperature map:
+6778296876 1588184826 9373785
+5958282911 253795579 867819861
+422679395 2761173364 193474551
+253795579 4218627316 4
+5652112248 987641937 721917256
+2349984397 7942568282 71771239
+8969933137 5781654773 394652
+1898117348 8342374669 537694616
+4219243162 8214944547 736996395
+5912961492 8868498531 27622883
+346958533 4173157962 51979735
+1916631453 9117978288 18581458
+2945679922 4497199716 5152578
+9179535252 5162391577 549347823
+
+temperature-to-humidity map:
+7239471736 657945134 92565479
+4691325348 8569173453 532754925
+5919891224 6397216467 3372871
+3822197491 2782624568 698583215
+6125121482 639986598 98215788
+6574396598 665121945 619454968
+2412529992 2776226287 1626339
+2272785524 417214458 621918434
+5629812155 68588342 911161561
+9324482921 2524842229 67458368
+5932612196 2357165667 93374591
+1274118925 4785728782 685125685
+4352611699 7119321598 69449264
+6387566129 5474547438 5352628
+3928922836 7972535629 916644549
+1662726326 6558668448 362791743
+836597248 451582864 36438897
+738226654 681268313 785154987
+9563614959 8661586162 448483715
+9984139884 5113486123 19999477
+2755987212 2214749752 872249174
+887416884 6486692479 38549881
+
+humidity-to-location map:
+858893435 311438446 562265762
+2454586869 3679665416 225353643
+9519322712 63232264 128625398
+441771557 9 2371977356
+5 8193625898 775413179
+1952973915 6235154499 759143921
+6572 58451 75391174
+6941443142 589862612 261347316
diff --git a/interactive/examples/aoc2023/day05/part1.ddp b/interactive/examples/aoc2023/day05/part1.ddp
new file mode 100644
index 000000000..995a6a6c1
--- /dev/null
+++ b/interactive/examples/aoc2023/day05/part1.ddp
@@ -0,0 +1,21 @@
+-- AoC 2023 day 5 part 1: chain each seed through 7 remapping stages; min.
+-- input 0: tagged rows -- (0, seed) | (1, stage, dst, src, len)
+
+let seeds = input 0 | filter($0[0] == 0) | key(0, $0[1] ;);
+let entries = input 0 | filter($0[0] == 1) | key($0[1] ; $0[2], $0[3], $0[4]);
+-- (stage ; dst, src, len)
+
+chain: {
+ let cur = active | filter($0[0] <= 6);
+ let cands = cur | map($0[0] ; $0[1])
+ | join(entries, ($0[0], $1[0], $2[0], $2[1], $2[2] ;))
+ | filter($0[3] <= $0[1] && $0[1] < $0[3] + $0[4]);
+ -- key (stage, idx, dst, src, len)
+ let mapped = cands | map($0[0] + 1, $0[1] + $0[2] - $0[3] ;);
+ let covered = cands | map($0[0], $0[1] ;) | distinct;
+ let unmapped = (cur - covered) | map($0[0] + 1, $0[1] ;);
+ var active = seeds + mapped + unmapped | distinct;
+}
+
+export "result" = chain::active | filter($0[0] == 7)
+ | key(; $0[1]) | min | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day05/part2.ddp b/interactive/examples/aoc2023/day05/part2.ddp
new file mode 100644
index 000000000..26d2e966d
--- /dev/null
+++ b/interactive/examples/aoc2023/day05/part2.ddp
@@ -0,0 +1,47 @@
+-- AoC 2023 day 5 part 2: seed RANGES through the 7 stages; min location.
+-- Ranges split into mapped intersections and identity holes each stage.
+-- input 0: tagged rows -- (0, start, len) | (1, stage, dst, src, len)
+
+let seeds = input 0 | filter($0[0] == 0) | key(0, $0[1], $0[1] + $0[2] ;);
+let entries = input 0 | filter($0[0] == 1) | key($0[1] ; $0[2], $0[3], $0[4]);
+
+ranges: {
+ let cur = active | filter($0[0] <= 6);
+ -- key (stage, s, e)
+
+ let isect = cur | map($0[0] ; $0[1], $0[2])
+ | join(entries, ($0[0], $1[0], $1[1], $2[0], $2[1], $2[2] ;))
+ | map($0[0], $0[1], $0[2],
+ if($0[1] > $0[4], $0[1], $0[4]),
+ if($0[2] < $0[4] + $0[5], $0[2], $0[4] + $0[5]),
+ $0[3], $0[4] ;)
+ | filter($0[3] < $0[4]);
+ -- key (stage, s, e, cs, ce, dst, src)
+
+ let mapped = isect | map($0[0] + 1, $0[3] + $0[5] - $0[6], $0[4] + $0[5] - $0[6] ;);
+
+ -- candidate hole starts per interval I=(stage,s,e): s and every ce
+ let cand = (cur | map($0[0], $0[1], $0[2], $0[1] ;))
+ + (isect | map($0[0], $0[1], $0[2], $0[4] ;)) | distinct;
+ -- key (stage, s, e, h)
+
+ let csets = isect | map($0[0], $0[1], $0[2] ; $0[3]);
+ -- (I ; cs)
+
+ let pairs = cand | map($0[0], $0[1], $0[2] ; $0[3])
+ | join(csets, ($0[0], $0[1], $0[2], $1[0] ; $2[0]))
+ | filter($1[0] >= $0[3]);
+ -- (I, h ; cs) for cs >= h
+
+ let hend = pairs | min;
+ let holes_bounded = hend | filter($0[3] < $1[0]) | map($0[0] + 1, $0[3], $1[0] ;);
+
+ let withcs = pairs | map($0 ;) | distinct;
+ let nocs = cand - withcs;
+ let holes_tail = nocs | filter($0[3] < $0[2]) | map($0[0] + 1, $0[3], $0[2] ;);
+
+ var active = seeds + mapped + holes_bounded + holes_tail | distinct;
+}
+
+export "result" = ranges::active | filter($0[0] == 7)
+ | key(; $0[1]) | min | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day06/input.txt b/interactive/examples/aoc2023/day06/input.txt
new file mode 100644
index 000000000..073fc4d3a
--- /dev/null
+++ b/interactive/examples/aoc2023/day06/input.txt
@@ -0,0 +1,6 @@
+16 18
+20 20
+27 30
+50 47
+60 49
+78 62
diff --git a/interactive/examples/aoc2023/day06/part1.ddp b/interactive/examples/aoc2023/day06/part1.ddp
new file mode 100644
index 000000000..f81ebedc7
--- /dev/null
+++ b/interactive/examples/aoc2023/day06/part1.ddp
@@ -0,0 +1,24 @@
+-- AoC 2023 day 6: product over races of #{hold t : t*(time-t) > dist}.
+-- No sqrt/div in DDIR: enumerate hold times by fixpoint instead of the
+-- SQL's closed-form quadratic (which the slt itself computes two ways,
+-- disagreeing by float drift; enumeration is exact).
+-- input 0: (time, distance)
+
+let races = input 0 | key($0[0] ; $0[1]);
+
+cands: {
+ let step = c | filter($0[1] + 1 < $0[0]) | map($0[0], $0[1] + 1 ;);
+ var c = (races | map($0[0], 1 ;)) + step | distinct;
+}
+
+let wins = cands::c
+ | map($0[0] ; $0[1])
+ | join(races, ($0[0], $1[0], $2[0] ;))
+ | filter($0[1] * ($0[0] - $0[1]) > $0[2])
+ | map($0[0] ;) | count;
+-- (time ; n_wins)
+
+export "result" = wins
+ | key(; $1[0]) | collect
+ | map(; fold($1, 1, ^1 * ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day07/input.txt b/interactive/examples/aoc2023/day07/input.txt
new file mode 100644
index 000000000..00fd5e6cf
--- /dev/null
+++ b/interactive/examples/aoc2023/day07/input.txt
@@ -0,0 +1,34 @@
+67AJ5 568
+79Q36 923
+99325 894
+8JK7Q 177
+5J663 919
+82574 158
+229T2 817
+355A6 917
+K43K3 767
+92765 525
+8QKT8 129
+35J28 541
+4967J 132
+784T9 621
+6A6A9 327
+9KAJ4 643
+9T479 463
+6QT5K 932
+Q7T66 738
+4333T 611
+8TJ29 215
+7TT7Q 472
+9T8J6 275
+66Q85 835
+KTA66 697
+TA876 326
+27858 512
+79Q99 749
+AA5QA 457
+792K6 762
+KK6TA 635
+5KT55 349
+TQ495 158
+5J2TK 432
diff --git a/interactive/examples/aoc2023/day07/part1.ddp b/interactive/examples/aoc2023/day07/part1.ddp
new file mode 100644
index 000000000..caa9ab5ac
--- /dev/null
+++ b/interactive/examples/aoc2023/day07/part1.ddp
@@ -0,0 +1,40 @@
+-- AoC 2023 day 7 part 1: rank hands (type, then card order); sum rank*bid.
+-- Type from the pairwise-equality count: 5oak=10 4oak=6 FH=4 3oak=3 2p=2 p=1 hi=0.
+-- input 0: (id, c1..c5, bid) charcodes
+
+let hands = input 0 | key(;
+ $0[0],
+ ($0[1]==$0[2]) + ($0[1]==$0[3]) + ($0[1]==$0[4]) + ($0[1]==$0[5])
+ + ($0[2]==$0[3]) + ($0[2]==$0[4]) + ($0[2]==$0[5])
+ + ($0[3]==$0[4]) + ($0[3]==$0[5])
+ + ($0[4]==$0[5]),
+ if($0[1] < 58, $0[1] - 48, if($0[1] == 84, 10, if($0[1] == 74, 11, if($0[1] == 81, 12, if($0[1] == 75, 13, 14))))),
+ if($0[2] < 58, $0[2] - 48, if($0[2] == 84, 10, if($0[2] == 74, 11, if($0[2] == 81, 12, if($0[2] == 75, 13, 14))))),
+ if($0[3] < 58, $0[3] - 48, if($0[3] == 84, 10, if($0[3] == 74, 11, if($0[3] == 81, 12, if($0[3] == 75, 13, 14))))),
+ if($0[4] < 58, $0[4] - 48, if($0[4] == 84, 10, if($0[4] == 74, 11, if($0[4] == 81, 12, if($0[4] == 75, 13, 14))))),
+ if($0[5] < 58, $0[5] - 48, if($0[5] == 84, 10, if($0[5] == 74, 11, if($0[5] == 81, 12, if($0[5] == 75, 13, 14))))),
+ $0[6]);
+-- (; id, t, v1..v5, bid)
+
+let less = hands | join(hands, (
+ if($1[1] < $2[1], 1, if($1[1] > $2[1], 0,
+ if($1[2] < $2[2], 1, if($1[2] > $2[2], 0,
+ if($1[3] < $2[3], 1, if($1[3] > $2[3], 0,
+ if($1[4] < $2[4], 1, if($1[4] > $2[4], 0,
+ if($1[5] < $2[5], 1, if($1[5] > $2[5], 0,
+ if($1[6] < $2[6], 1, 0))))))))))),
+ $2[0] ;));
+-- key (left_is_less, right_id)
+
+let ranks = less | filter($0[0] == 1) | map($0[1] ;) | count;
+-- (id ; #smaller)
+
+let bids = hands | map($1[0] ; $1[7]);
+
+-- ranks misses the weakest hand (zero smaller); add every bid once instead.
+let above = ranks | join(bids, ($0 ; $1[0] * $2[0]));
+
+export "result" = above + bids
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day07/part2.ddp b/interactive/examples/aoc2023/day07/part2.ddp
new file mode 100644
index 000000000..14a8a64e6
--- /dev/null
+++ b/interactive/examples/aoc2023/day07/part2.ddp
@@ -0,0 +1,53 @@
+-- AoC 2023 day 7 part 2: J is a joker (worth 1; joins the largest group).
+-- Doubled pair-count type: hi=0 p=2 2p=4 3=6 FH=8 4=12 5=20; jokers add
+-- (g1+j)(g1+j-1) - g1(g1-1). No scalar let: stage via successive maps.
+-- input 0: (id, c1..c5, bid) charcodes
+
+let stage = input 0 | key(;
+ $0[0],
+ -- doubled non-joker pair count
+ 2 * ( (($0[1]==$0[2]) && ($0[1]!=74)) + (($0[1]==$0[3]) && ($0[1]!=74)) + (($0[1]==$0[4]) && ($0[1]!=74)) + (($0[1]==$0[5]) && ($0[1]!=74))
+ + (($0[2]==$0[3]) && ($0[2]!=74)) + (($0[2]==$0[4]) && ($0[2]!=74)) + (($0[2]==$0[5]) && ($0[2]!=74))
+ + (($0[3]==$0[4]) && ($0[3]!=74)) + (($0[3]==$0[5]) && ($0[3]!=74))
+ + (($0[4]==$0[5]) && ($0[4]!=74)) ),
+ -- joker count
+ ($0[1]==74) + ($0[2]==74) + ($0[3]==74) + ($0[4]==74) + ($0[5]==74),
+ -- largest non-joker group
+ fold(list(
+ if($0[1]==74, 0, 1 + ($0[2]==$0[1]) + ($0[3]==$0[1]) + ($0[4]==$0[1]) + ($0[5]==$0[1])),
+ if($0[2]==74, 0, 1 + ($0[1]==$0[2]) + ($0[3]==$0[2]) + ($0[4]==$0[2]) + ($0[5]==$0[2])),
+ if($0[3]==74, 0, 1 + ($0[1]==$0[3]) + ($0[2]==$0[3]) + ($0[4]==$0[3]) + ($0[5]==$0[3])),
+ if($0[4]==74, 0, 1 + ($0[1]==$0[4]) + ($0[2]==$0[4]) + ($0[3]==$0[4]) + ($0[5]==$0[4])),
+ if($0[5]==74, 0, 1 + ($0[1]==$0[5]) + ($0[2]==$0[5]) + ($0[3]==$0[5]) + ($0[4]==$0[5]))),
+ 0, if(^0 > ^1, ^0, ^1)),
+ if($0[1]==74, 1, if($0[1] < 58, $0[1] - 48, if($0[1] == 84, 10, if($0[1] == 81, 12, if($0[1] == 75, 13, 14))))),
+ if($0[2]==74, 1, if($0[2] < 58, $0[2] - 48, if($0[2] == 84, 10, if($0[2] == 81, 12, if($0[2] == 75, 13, 14))))),
+ if($0[3]==74, 1, if($0[3] < 58, $0[3] - 48, if($0[3] == 84, 10, if($0[3] == 81, 12, if($0[3] == 75, 13, 14))))),
+ if($0[4]==74, 1, if($0[4] < 58, $0[4] - 48, if($0[4] == 84, 10, if($0[4] == 81, 12, if($0[4] == 75, 13, 14))))),
+ if($0[5]==74, 1, if($0[5] < 58, $0[5] - 48, if($0[5] == 84, 10, if($0[5] == 81, 12, if($0[5] == 75, 13, 14))))),
+ $0[6]);
+-- (; id, pc2, j, g1, v1..v5, bid)
+
+let hands = stage | map(;
+ $1[0],
+ $1[1] + ($1[3] + $1[2]) * ($1[3] + $1[2] - 1) - $1[3] * ($1[3] - 1),
+ $1[4], $1[5], $1[6], $1[7], $1[8], $1[9]);
+-- (; id, type2, v1..v5, bid)
+
+let less = hands | join(hands, (
+ if($1[1] < $2[1], 1, if($1[1] > $2[1], 0,
+ if($1[2] < $2[2], 1, if($1[2] > $2[2], 0,
+ if($1[3] < $2[3], 1, if($1[3] > $2[3], 0,
+ if($1[4] < $2[4], 1, if($1[4] > $2[4], 0,
+ if($1[5] < $2[5], 1, if($1[5] > $2[5], 0,
+ if($1[6] < $2[6], 1, 0))))))))))),
+ $2[0] ;));
+
+let ranks = less | filter($0[0] == 1) | map($0[1] ;) | count;
+let bids = hands | map($1[0] ; $1[7]);
+let above = ranks | join(bids, ($0 ; $1[0] * $2[0]));
+
+export "result" = above + bids
+ | key(; $1[0]) | collect
+ | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day10/input.txt b/interactive/examples/aoc2023/day10/input.txt
new file mode 100644
index 000000000..5a3144ebd
--- /dev/null
+++ b/interactive/examples/aoc2023/day10/input.txt
@@ -0,0 +1,56 @@
+SJJL|-.LFSS.S-|7-L-L-L|F7FS|J.LL-.|J.L7SJ|.J-7F..-S7|7SFSSSJLLJ|-..L..J-L7JJS.J-7-LJ-|.J7||.LF.7.7SF7LL-.|7-7F77|LFJFSS77JF.S.|.-F
+J..-.--S|SL.|S.7L7-|SFL.FJF7L-LJ-SFL|L|--F.-FS|7.LJLJ.-.|LF|J.L-JJJ7F|S..FSSSLL-F.-FFL7-7LLLFJ.LFSSJFF7F.7F|F7LJ|F||L7.7.7|7-|J77S
+JL7|JSJ|FLLSLFF|-7JSJSJFL|L.J|||J-S.S..|-SF-|----JS.-J|JJ.|-F||.FL..JLL7.-LJ7FSJFL..|F7JJJ|LLJFFJJFLL7.LJJJF--FFLS-|LLLS-..-|S|J7F
+|JFF-L|F7|.F.L7J-7F-.LSF7J-|S7|FLS--JF.SF.S.S.LSLL..F7.SS7L|7J|.---|7JL|7SLL|SS.JLLL|L.7..JL||JL|JL.S777.|-JSSL|S.7JJFLJ.SJ-.|L|FJ
+7|L-S7|JFL.SJ7FL|.LLJ.F7SF-.-SL77-FJ7S-J|SS-|L-J|J.7F|.7.S.LS-|.JJJ|.S|.|777-LJ-.J.7-LSSL|SJ.J.-F.-LSS-SJ.LL|S7|FJL77.FFFJJ.-LS-|-
+SJ.-|..-LF7-FS.S|7F|LJ--SS-SFFF||S7|FF.7..F7|JFJJJ--7JLS||S7J--J-JF-|7SFJ|7.||||7LS|-..FJ.|S77JF.F77L.|.-F.L.-J-FL-JFL-|.S.LJ|J.L7
+FF7F-.7.-LS-FF.--LL.|.7L-J7FS-.F--S7.LJLL.-7FF|7J7L..S7LJL7-JLF-S7JJ|-LS7S-|JLF7LFSJSF7L-F.7S-|7S-.F||.J.-F|-SJL|SFF7|-|F--||7LJ7F
+S.J7L.F|S-.F|FSJ|--|L.JS77JL7.SS7|FJFFF-F|.SF7LL.JFL7SJJLL|LLSS.L|F.|F-|S|JJ--FJLFJJ7..77.S|LJFJJS.-7S-LJ|L|S-J|FLLLJFSSFS.J-FS|LJ
+SF.77J-L-7|FF-J-77-L-F|J.|F.J7SJL..SF||S77.7J|J.L-|-7L-JSJ.S.-FL-F7FL|SSFS7-|S-.L.FF-L-F..J-FS77.JJ-SF7SS--.JLLSJ.F7LS.JS|..J|J|7S
+J|L-7|77|-F-SF7-..7|.77L7.LJLLL..LSJFLJ|-|L7|-S||||F|LLF77-LF||-|S.JL.|.LF7S7.FFF--SFF.-J-F.F7JJ.7JLJ-7S-LLSFLJ7.-JF.SSLLSSSJSSF7.
+.|.L.|.|FJ.L||.77L-SJ.|F7-.L7LJ7LJ||.SJF-.||SF..L|JSL.-S.SFLSJLJ7S-FFFJ7F.J-L...|LFLF-SSLS7F|7J.-.F||.|L|JLJ|LJ...FSJ||---7|.--LF.
+LFJ|7L|S||..J|7S..J--.77F7-7F.S-J777.||FFL|.|-FJSSFFLSJ.|J-.JS-FSS.|F7.S.SJ-JJS.-S|LJJ-JLJS-7SSF7JS7.|J77-JS7J.F|JFFF|S-F||SS.|LFF
+FJ-|F7.F-LLF-.J7LS|LJSSF|.7S||-F|7S.J.|JSJLLJF-LS|SFJ7LL7SSS7S.S-|.S7||SLFSLFL|.LJ-S77|L|F|S-.7F7F.JJ-FL|J7S.7J.FJ7S|JJ|L7F7F|S|.|
+..JF.-L|JFL-FS|FJ77..S|-SLFLL7||SL|-L-.|.7JLF7J--7J-7S7L77JLJFS.J.7..7S7F7|S7|.LFLJS-.L|LS7FJFL-L-L-7S77-LJ-.JFFJ.J7L.FJSJLFSJ|L7F
+FFLS|-S.|F|SLFL|SF|JLS|FFFSJLJ|F.J|LSJJ7FS|LL7-.J-LFJ|7JJ.7|J.-JF.L...LS7LFJJF7JJ.L...|S..-7.-S.JFLLSLFS|JFJ-.L|L.L7FSJ.|LFL-F7F.7
+SJ-7LS7.-FL|F.FJ|7|SJL7-J77L-FF.F-|7-|7JL.SF7|FSL-J|.LL.|.7LSJS||7SL7.7.JSFFJFS-7.LLLS..7L.SFFJ--|-SFF7||SJS-7|-|J-.SSJ|.77.|-L.F-
+.LJJ-L7.---.S-7.7|FS..SJ|SLJ|FS|SLLLFJ7.F-F7LL7S-S-JF.7F7LL-J-.|L|-|-.L.7SSS|SL||-|JFFL.J7SJSF.F.F7S|FL--LLJ-L.SJ.LLJLLFS7J-.|FJ.J
+JSFLJ|.JJFFLF|F|LJ||L7-J7JS.|LL7JSFSJJSSLFJ7|L.FJJLL-FFLSS.FJ-.SSSLLJL777FL.-..7SF-.-.SFF-.J7F|JJS---L|.L|-.FSJ-||S-LJFS.S7|J-7J-S
+L77SLFJ7FJ.|-|.|7LJS7LL.-7.LFJLSFJ|S-FJ..LJ.7-SJ-|LJ77|JFLSF|.SJ.|L--L|S7-SS.-.F-JS--|SLS-SS-F77LF..JS-FJF.SJ7.FJ|-SF-LSLL||J.|-.7
+.-S|S--FSJ--7|F7-L|-SS-JL|FLFFJL7-FJJSJS7SJL7.SS|S|.LLSS7FS-.J-JSF.|777--7.|J|7S|.L|L|SL|SSF|7L.F|JF.FJ--.J-|S.S.SSS..SL-|S.S.L|-J
+7JSJ7|J7--LSS|7SLFSJ-.SF-|77||....7.L7J|7|7|.-.LJ..LSJ7|SFLSFJF|JFL7F|7|LJFJ7|LJSSJ.7-|7JSJL.J|F.|.J7-F7-F|-S.-LS-.-JS.SSS|JLS||F-
+SJL.LL7JJS-S|-SF-LS.7FL-F7L|F7.SFFF7J77-SLF|.LFFLLSS7S|----SLL7FL7||LFFSJLF-.|7F--JF7..JL---S.SSSJF|SLSJ.-L|LFJJL-7S-7FF--SL||FLSL
+F7S.|7SL-|.F|.L.-.7L|LLL-L7SJL-JJJ|J-FJLSLF7F7JFJ7.FL|7J.7F.|.JSL.|L7S---.F-.-.FSL|LLSFLF|7.7-|J7-|-7..-SJF-|S|J7LL7FSJ|F.7JJF.7|J
+S.L|SL|JF..SLJL|S.F7-||77LLFJ.S|S-LL.J-7|FF-SJJJL|J.FLJLL7JFFJ..FFJLSSSS-|-77-S-.F777--|S.---.F-SLL|S7S7SJLSF-S.-|LJ7-L--7..-|.7L.
+F.|7.-JL|7L.JFF|LLLJF-S7F|LF-7LL|7|.J-S..S|FFJ7.FSL.|-LJ-|JLLL.LLL-7-F-|L-L-|.77SJSF-SF7.SFJL|.F|.SS..FL7L.7FSL7SL7SJSSFSS|SF.-F77
+.SLL.JL|L.77S.77L|SJF..FL|-F.S-SS.7SS-LLFS|L.S7J7L|F|L-7|JJ-77SF-77|LFLSF7LJFS||LJJ|J7.-LFF-L.|LF.LS|J-.LJ|J.L|--J7.S.-L.-LFF7F77-
+--J-.SL--L7JJSFJLJS.FL.7-F7-SJS.L.S.LF|7J-|77FS|-J-|S.FSLS.7.J|J.LLF-LL.S--7S.-LSS|JLJF.S|.F7SLSS.L-L.|F-JL|LFS-S.J77-L7.|7|S|.S.|
+7SJS-|F-.F...LFSJFLFLFFFJ||JL--J|S.LJ-LL|7S-FF-J.7F.JJ|L-.JF7J|L|S-.LJ|S.SFSLLS.L7SJFSJ.SL--L|F.S.SLS.|S.-..7F7|LSF|SJFS7FJ7S|J.-J
+7...J.L--7S-SF.LSJ7JLJFL7LFFLJ..|FFL-7FSJ|S|LLL.FL7-LL|-JFF-||L-JF7FLJJLL-|S...S.|L.-FJF.FS-.7.JJ|.L7J.LSSS7.S..J-7|-SFSSF7S..SL7F
+LJFFJJFLS..|.L7J.-S7-.-|--|7JJFL-.|JF|.S.-.J|F7|SFSF|-L-SJLL.-.---L7FS-LL7|FS7L.J|||S-F|SJFL7F7|-L7L.SS.FL.LSF-7|F.F.|7FJLJF7L--SL
+-J-S.F--.SFF..L7JJ.FSLJLLSS7F7JJJ.JLLLF|.7SFSL7L|-FSS.|7LS777LL7--.7F-.L|L.-F-7..|LS-J.LJL..F.S-J7-7LL7-L7LF|LS.J7LL|77S-F.S.|.7FS
+J|LJ7LS|-.|7L777|-|LL77JF--7LSF|L||S...-.JLS.|..7-|LL7S.JF.-.JFJF7JFFL.|S-7.J.7.JJ-LJL-7LF--LL|J.L|-LL7L-FF7-.-SL.S7.LF.|JFS--7.FS
+7-L7SSSL7S|S|-L|.7.S|-7.SJSF7|S7J7|JLLSFJS.FL.L-LSFF.SF-7-7-S||S.JLFSLS-F-S|LSFS.JL-77|7.7-7.|FJL|SL-.JLLS-J|.SJF|-|LFSLJF-JFSJ|F.
+SLL.-JJJLFFSFF|JJLF-|-SSFL7.|-FF|-J7.SS|FS7J7.S7.7|-FS7L-.SJ-|FF.7-|SF..7S7F-S-S||L-7|SFF-.S|--|7S-L7JJLFJ..|.SJFS.7J---L--.S|F-7J
+.F.FLJJ|J|JF|LJ-SL.|77FJ-LL|S-JJ7F-LLSL-SFLLFJ7SS|JJFFF7JFF-J.-..|7F-7|-JFF.FJJ.L.|J|SJJ|..J||J.J|.|7-S---F..-SLS.7.7SFF-L.7.S||-7
+JF-SFL.S|LL7FF-.F...JSS||.7-JFJLFS.J|7|S7SJ|-J.|-|L|JS.7F-7F|F|.|JSJSS7FLLS.S7--L-7.--SSLJL||77-F|.FJ7|7JS|F7JJ-L-J-|JSSSF..LF-LJS
+L-F|F77SFS7JS-JFS77FF|SJFFLJS|LJ7J-.L.L--FSS77S.J.7FLLSLS|77LFLJ|7|L-7..-F77S.-S--F..|JLF|-7L.S7J|F|-LJ.FJ7|.7SSL.7JS.-.L|-FLL-JF7
+7F|FF-SLLJL|||FL-F.77L.L-J.J|F-||.7.-S7F-7F-7.|LSS7LL.SJS--L|L-F.JSS-|7FSSSLJ.-|L|7F|F-LS.J-LLL-SLFF.F7SFSJLJ-S.LLFJF|7JLF.S7-F--F
+-L7J-FL-L|...7.SL.FSJ||7--FF7L7.J-FSL-SFL.F|7|||7F.-F..--L-S--JLFJ7J.|7-LJ.L|L..L|.7|7.|S7FLLJS-J|S7|JJSFJJ.7JLJ7FS-L7S7FSSF-L-S7|
+.J|.JJL7--SS-S|L-LLLF|S7JJ7L-F-S.|-|7FL-.7SJ7S.FSF.|S7|-|L.L..-L.-LJL-|7-7L.7S7F|77JL77SL7|JLLF|LJS|LJFL-FSL.F.-JSS7-FF|S.||..S7SF
+JF-77L-L.FS-SLLJFSL.-7|S.L7.S.JLS7SJ.7J7|LJJSLSJF.-F|SFFS|||F77-.-.J||7FLJF--7LL|L.7.77SF|FJF-LJFJS|-FJ|L|L.7JJ|F..|L.LL..J-|J-|.L
+7.|L-.-SFFS||.-|JLLLJ7-7JF|7F||SL-FS-J7SSJFJ--|J-SJ-|F||JL7F|F|J.LJS-JJ-S|JS.J.|.|7-F.7|LS--F7-.F|JJ.LS--.-|.J|J|SF.FS-JS.LLLF7S-F
+SFL.FJF7S.JF7JS7--|S.JJ7F--SS7L|SJ.J.SJ|SJ77-SLSFJJ7|-77-S7L-7S..-.L.JS.S7FFF|-77.SS-LF|LSFLFLJJL|FL.-LL|J-JFJ7-.L|.LSJ.77LJFF7L7.
+.L-F-F.FS7.F7JL7.7J7J|J|SFSJ7LLSSFJ-FSJ-.7.SLFJJF.FJJ|7.L777J7.S77...7|SFS-.L7.FS.J.-S.FJS7SJLJS-J|LJ-.J|L--F|S||SLJFJJJJSSF-L.FLJ
+|7.SS-FSS-FL||SL.7L|7.7FL.77S|F.S-.LFSS.SJJ-77JFL7F-SS.SLFLFSJ|.LF7SJJFJ||F7F|SLF.||JSF-J-..-77S.|.S||F7--7FL7LLS7||-.7|JJ-.7FLS-F
+7JL-..7.L.-SS.J7FS-L-L|F.JLF-.|.7.F-SL.-7JF.S7-FSJ-|F.LJ--FJ.SLF--|L7.F-L7-S-77.S.7.JFSLJ-FS77JJ|||S7J-JSJ.|-|F7L--L7S|LF-J|LS..--
+F.-JSF-.SF7LFF-F7-.|.77SF|.SFL.-S|L7-7.--F77SSSL-FSLF7FL7|SLFLS|FF7L-JJJL7.J-|-|F-.-L7-.JJ|F7.7-F-L.LJ7JJSLJFL.7|L7LS7|L-FJ7L-LJJS
+|-J7.L---.7-77-7FLF7.JJ-LS.|-S|-S7JS7SJ|.J|L..SJ.J|7SL.7|.LJ.S-7F--.7S.J-.SS7.7SS7-SFLJ|SJ-|L7|S.FF7S-7S.SF7S.|JL-S-F|.777FJSFL77J
+7JFF7-|S.J.FSJ-.L|S|L.-JJJSLS7F|7J-7FLS7SF|.|J7LJ-|S7..7SJL...SS7J.|L|SFJ.7F-.--F.777J77L.F|-L-|.7SS..7JL-S-L.J7FFFSSS77-S..L|..L|
+J7JFL-||F.S--LFLS7L|.L.SJL.-LFFSJJ.7FJL|SJ-JJJS7|LSL7.F-J7.-|L-J.|.JL77J7J.|..FLSJ.LLJ||LLL|.SF77S--J-.-|S7F||77F|JJLF|-L..|-|7--7
+.|7SJS-....7LJ.7L-SF--|L.LLFJSFF|.JF.-LFF7LJJ7.JJ..7L|-S.FLJ7-F7S-||J77SFJ.LSLLF|.F-F7-|FF7FJ.7LF.L-J-.F7.F7JFFS|--L7-.-S|7-LF|..S
+7F7.JLSJ7|7L.FJ|J7JJLF7JJFJ7.L-J-FJFFLL|.LFL-L|-|J|-||J-F-.JF-|7J..7F.S|J7.SFJS7.JLLL-S7SJSF-77JL-LF77L-S|FS.JLJ7SLF|FJ.F|-S|L|-JF
+.J-.|J--F--J-77-|7-JJ.7SJSJJ7|F|.FLJ.7JFJ.J-L.S|-7|-J7.FLSS|J-|--.JS7-L.7L7S7JSJJ||-S-L-7J|SLJF7FJSS|.L.F-F7LL.F7.7-L.7SL|.777|J77
+J7-7.|S-LS7|FJLJ-SF||S7777-|.77FSL|SFLFF.SS7|L-.JFSJ7.|L|LJF|J|7-S7S7-FL.FJJ77|SL|J.SJ-|FL-|7.JJS-S|SS|L-7LJ-S7-J-F-JLL-|LSJ|JLJ7.
+S77SS7J.JFFL7|S-L.SSFL.S|SJ||JJ7.SSJS|LJ--.J|-.-S7JLJJSFJ-L-FS|SL77JLJ-LJS|FJL---L|.S7LF-JJ7-|S|....SJLJSJ-.LJ.FL7.FLS.7L.-LJ-S7|S
+FL|JJF|FFFJ.JJSL--F.FJFJ-FLLLJ|F|SLLSL77JS7L.777--JFJ|F|JS|.|.-|7L..L-SL7-7JSF-|.J.J--S|JSS|S.F|.L7LSSFSL-.F|F|7S--JJ|S-F7F-7LLFL|
diff --git a/interactive/examples/aoc2023/day10/part1.ddp b/interactive/examples/aoc2023/day10/part1.ddp
new file mode 100644
index 000000000..8da57f94d
--- /dev/null
+++ b/interactive/examples/aoc2023/day10/part1.ddp
@@ -0,0 +1,34 @@
+-- AoC 2023 day 10 part 1: loop length / 2 from S through mutual pipe links.
+-- codes: '-'=45 '|'=124 'F'=70 'L'=76 'J'=74 '7'=55 '.'=46 'S'=83
+-- input 0: (row, col, code), 1-indexed
+
+let symbols = input 0 | key($0[0], $0[1] ; $0[2]);
+let pipes = symbols | filter($1[0] != 46 && $1[0] != 83);
+
+let e1 = pipes | map($0[0], $0[1],
+ if($1[0] == 124, $0[0] - 1, if($1[0] == 70, $0[0] + 1, if($1[0] == 76, $0[0] - 1, $0[0]))),
+ if($1[0] == 45, $0[1] - 1, if($1[0] == 74, $0[1] - 1, if($1[0] == 55, $0[1] - 1, $0[1]))) ;);
+let e2 = pipes | map($0[0], $0[1],
+ if($1[0] == 124, $0[0] + 1, if($1[0] == 74, $0[0] - 1, if($1[0] == 55, $0[0] + 1, $0[0]))),
+ if($1[0] == 45, $0[1] + 1, if($1[0] == 70, $0[1] + 1, if($1[0] == 76, $0[1] + 1, $0[1]))) ;);
+let r1 = e1 | map($0[2], $0[3], $0[0], $0[1] ;);
+let r2 = e2 | map($0[2], $0[3], $0[0], $0[1] ;);
+let scell = symbols | filter($1[0] == 83);
+let s4 = (scell | map($0[0], $0[1], $0[0] + 1, $0[1] ;))
+ + (scell | map($0[0], $0[1], $0[0], $0[1] + 1 ;))
+ + (scell | map($0[0], $0[1], $0[0] - 1, $0[1] ;))
+ + (scell | map($0[0], $0[1], $0[0], $0[1] - 1 ;));
+
+let symm = e1 + e2 + r1 + r2 + s4 | count | filter($1[0] == 2)
+ | map($0[0], $0[1] ; $0[2], $0[3]);
+-- (r1, c1 ; r2, c2), mutual links only
+
+reach: {
+ let next = r | join(symm, ($2[0], $2[1] ;));
+ var r = (scell | map($0[0], $0[1] ;)) + next | distinct;
+}
+
+export "result" = reach::r
+ | key(; 1) | collect
+ | map(; fold($1, tuple(0, 0), tuple(1 - ^1[0], ^1[1] + ^1[0]))[1])
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day10/part2.ddp b/interactive/examples/aoc2023/day10/part2.ddp
new file mode 100644
index 000000000..ddd3e7fa3
--- /dev/null
+++ b/interactive/examples/aoc2023/day10/part2.ddp
@@ -0,0 +1,58 @@
+-- AoC 2023 day 10 part 2: cells enclosed by the loop. Diagonal parity walk:
+-- Enclosed(r+1,c+1) = Enclosed(r,c) toggled by loop pipes J - | F at (r,c).
+-- (The slt resolves S to '???', which never toggles; we mirror that.)
+-- input 0: (row, col, code), 1-indexed
+
+let symbols = input 0 | key($0[0], $0[1] ; $0[2]);
+let pipes = symbols | filter($1[0] != 46 && $1[0] != 83);
+
+let e1 = pipes | map($0[0], $0[1],
+ if($1[0] == 124, $0[0] - 1, if($1[0] == 70, $0[0] + 1, if($1[0] == 76, $0[0] - 1, $0[0]))),
+ if($1[0] == 45, $0[1] - 1, if($1[0] == 74, $0[1] - 1, if($1[0] == 55, $0[1] - 1, $0[1]))) ;);
+let e2 = pipes | map($0[0], $0[1],
+ if($1[0] == 124, $0[0] + 1, if($1[0] == 74, $0[0] - 1, if($1[0] == 55, $0[0] + 1, $0[0]))),
+ if($1[0] == 45, $0[1] + 1, if($1[0] == 70, $0[1] + 1, if($1[0] == 76, $0[1] + 1, $0[1]))) ;);
+let r1 = e1 | map($0[2], $0[3], $0[0], $0[1] ;);
+let r2 = e2 | map($0[2], $0[3], $0[0], $0[1] ;);
+let scell = symbols | filter($1[0] == 83);
+let s4 = (scell | map($0[0], $0[1], $0[0] + 1, $0[1] ;))
+ + (scell | map($0[0], $0[1], $0[0], $0[1] + 1 ;))
+ + (scell | map($0[0], $0[1], $0[0] - 1, $0[1] ;))
+ + (scell | map($0[0], $0[1], $0[0], $0[1] - 1 ;));
+
+let symm = e1 + e2 + r1 + r2 + s4 | count | filter($1[0] == 2)
+ | map($0[0], $0[1] ; $0[2], $0[3]);
+
+reach: {
+ let next = r | join(symm, ($2[0], $2[1] ;));
+ var r = (scell | map($0[0], $0[1] ;)) + next | distinct;
+}
+let loop = reach::r;
+-- key (r, c) of loop cells (incl. S)
+
+-- loop cells whose ORIGINAL symbol toggles parity (S resolves to '???': never)
+let toggles = loop | join(symbols, ($0 ; $2[0]))
+ | filter(or(or($1[0] == 74, $1[0] == 45), or($1[0] == 124, $1[0] == 70)))
+ | map($0 ;);
+
+status: {
+ let cur = st | map($0[0], $0[1] ; $0[2]) | join(symbols, ($0 ; $1[0]));
+ -- in-grid status cells (r, c ; e)
+ let tog = cur | join(toggles, ($0[0] + 1, $0[1] + 1, 1 - $1[0] ;));
+ let matched = cur | join(toggles, ($0 ; $1[0]));
+ let nontog = (cur - matched) | map($0[0] + 1, $0[1] + 1, $1[0] ;);
+ var st = (symbols | filter(or($0[0] == 1, $0[1] == 1)) | map($0[0], $0[1], 0 ;)) + tog + nontog | distinct;
+}
+
+let enclosed = status::st | filter($0[2] == 1) | map($0[0], $0[1] ;);
+
+-- pipe = reach minus S cells with no symm out-edge (slt keeps those countable)
+let ssrc = symm | map($0 ;) | distinct;
+let scellk = scell | map($0[0], $0[1] ;);
+let isoS = scellk - (scellk | join(ssrc, ($0 ;)));
+let pipe = loop - isoS;
+
+-- set-minus needs an anti-join: bare `-` is multiset subtraction and lets
+-- non-enclosed pipe cells cancel unrelated enclosed cells in the count.
+let enc_on_pipe = enclosed | join(pipe, ($0 ;));
+export "result" = (enclosed - enc_on_pipe) | key(;) | count | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day11/input.txt b/interactive/examples/aoc2023/day11/input.txt
new file mode 100644
index 000000000..44cfb3997
--- /dev/null
+++ b/interactive/examples/aoc2023/day11/input.txt
@@ -0,0 +1,57 @@
+#?##..#?.???..##??...??....#....?....#?...#.?..?.#...#....?#..#..##...???.##?...?#####.....##..?.??.?........?.#..#?##..#.###??......??.?#..
+.#...#.#?.#...?.#??..##.?##?.#......##.?...?...??#.?.?..?.###??.#.......#...?#.##.##.?#.#?..##?#.###.?..#.#?........?.??##..#?....##...?..??
+?.#..###????#.?.#....?#.?.?.?.#.?..?.#.#.#.?.#.??..?.??#????.?.#.#.?...#.#..?#?#?.#...?..#.#...?.?#?.?...#?..?.?.?##.??.?.#......#.??....##.
+..#.....?.??..#?...?##?....#??.###??.#.#.#..?.#?..?##..?.?##..##.##?.#?..??#?..#.?.#....#??.?.?.?.??#?#.#...?.#?.??.#??.?.?#.#...?...??#..?#
+#.??..#?.?....#.?.?.##.#...???...??.#.#..?..##...?.??.###.???#..#??....?.??.?#?#?...#??....?#.?#?...?..#..#.?..?.?..#.#.#.#...#.??..#.#.?#..
+..##....?....#.??..?.#..?#.?#?..?#?..?....#..??....##?#..##?.#.?.?..??.#......#...#.?.#.#..?...............###?.##.?##..#???##??.?.?###...?.
+#.#..#####??#??##.###...#.#.#.#.####..?#.?.##...#.##..#.#..#.?.#.#?.#.#...?....??#.#..?#.##..##?......#?.#.#..?.......#?.?.....#..?.#.#??##.
+.##.?.##..?#.##..?#.#....#...##.?.#?#...##??.#?##.?..?#...???.#..?.#?......?.#?#..##???.#...##.#..#?..?.#...?.?..##..?.??##?..?#??..#..?#.??
+#.#.#........?.???..#.###?##.?....?###...#?##?..##..#?.?...#...#??.??.?##...?...????....?.?..#...#...#...#.?.#.....??..???..?#??..?#.#.?.#.?
+?.?...#..?.#.#..?..###?.#?#?.##.??...??#...#?#.???.?#?#.#.#?...#....#.?.##...#?##..#..?#.#...??#.#.?..?#.#?#.....#?.......??#.###...#?#.....
+..#?...#.#?.#.??..?.?..#.#...?#..#.#......?..#??#...?..#.#...#?..#?.#.#.?.###..????#?..?.??.#..?#?..#........#....?###...#?..#.?#.#?.#??....
+.##..#?##..#?...#?.#.?.....#..?..?#.??..#..#.?...?.##.?.#..###.#?.?#.....##?.??#..?..#..??##?..#?#...##.#.?.?#.?..#...##..??.???..??..#.#...
+?.#..#.#?....?.?.?.#.?.#.#?#.#......?.#?....#.#.#.?#.##....?....#....#.#?..#.#.?#...?.?..?..###?.?.?.###..#..??#????#.....#...##.?#..#???.#.
+?#..?..?#.#.?.?...??.#.......#?#.?..#..?.#.??.#?.#.#?...##..?..?#??.#???......?.?.....#.?#.#?#??.?.##..??.??.#??##?.#....?#.#.#??.?.?.?.?#.?
+....#.#.??.??.?..###?#.?..##.???.?.#?#....##?...##..#.#?#.##?.#......#.?#.?.#..??#.#..#?#..?.........##.#.??.??#.##.##?.?#??..#.?#.##.?.?.?#
+..??.###....?#?.???..#.....??.??#...##.?..#...##..#.#..?#.....#.???.....#.#??...#......?.?.#.#??##?.#.??#..#...????.#..#.?#..##?..?.?.?..#..
+#?.....#.???#..##.#?.?#.?....?#??...?.....##...#.#.#.##?..?..#.#....??.?#?.?#..#?....#.?#???.#?#..?.#.?#.???..?.?..?.#.?#?..###??.?##..#.###
+????#.##....?.#.??.?....#?..?#...#..#???....#.##..???.#..##?.#.....#.??.#.....??..?....#....???##??....#....?#.?#...?..?..??....?..#.##.????
+..?#?#??..##??#.....?.?#...??.?#...?#####?.?#.?#?##.#.?#?#??.??..?.?...#.??..#...#.#..##?.#..##?#.?.#??#?.#?.??#...#??.#.?#..??.###.?.#..?.?
+..?#??.#?.#.?...##.#..#?.??#?..#...?##.#?.#?...##.#..###....?..#.....#?##.??#.##???.?.###...##.???....?.??.?#?.#?.#?.?#.??..###.?.#.?.....##
+#..#.....#.#.???.#..??......?.?...?#?.#?.#??.#?#?##..?..??##.#..?..##.#.....##?.??.#..#?.??##?#??.??#??#.#?....?#?....??#..?.??..?#?..##?##.
+..?..#??.?.?..?.??....#..#?.???..???.......#?#.?..?....##...#...#..?..?#...?..??.....#.#??#?.###..#?.#.#?.......??..#..?.??..#...##?..#.?..#
+?.?....??.#.???.##?.#.?.?.??#.?.###.......?..#...??.#...#.#.?#.?..?...???.##..?.?..####?#....?.#..?...###?...??........#???##..##..??#.#.??.
+?#.#?#...?.?....?....?...?.##.....##.##?.....?#####?..#?#.??#??.??#.....?..##..#?.#.#.#?#..#??......#.#?.###.#?#?.?.#.....#...?.#..#?....??.
+#?...??...?..?#?#......?.....#.????#?.#?##???.#.?..##.#.##.....?..?.##.##.......?.##?......?#.#?.#.##...?..?#??..#..#..#.#?..?#...??#.?#.?.#
+#..?....?#?#......#...#.........??#............#.?..?#.#....#.?#..#.....#??...#.?.?#??..?..??.#.?.#.....#?....?##.#?##..#?#...?#.??.?.??..?.
+..###?...???.?#.##???#?.#?.?.?#.#.#.##.?#.####.#?.???.....##.?...#.##..?....#?.?#.?...#.##.?..#..??#..?#..#?#...?..?..??.....?...?.?...?.#?#
+#??#.?##.#.?..##?..#.#??#????.?#.?....?...#....??...##?.#.?#?.?.##.#???.?#.?#?..?#.#.#?###?.....##..?..?.#?..?.?.#.#..?.?..?#.?##..#.#..?.#?
+.?.#.....##..#?#.??...?...?....????.#?##?....#.#.#.?.#??...?.?.##.#.##.?...##.#???#.?.?......???.?.?..?..??..#.?##..###?.##.##?##.?.###??..?
+??.#..###..??#.#.#.#.??.....?#??...?...?#.?...?#.#..#?#..?..#?.#.?#..?.#?.#.?#..#?..##.......??.?##?#??#??....#.......#.#...?#.?.?....?#..#.
+?.?#..##?..?....??.#..#....#?....?..?..#.#.#.#?...#....##.??#.##.?.???#.#...?.....?.#...?#..?...??..#.#.?..?........###.#..##.?..#.#..??....
+.....?.??#.?..#?#.#?.??........#????.......###.......?#??.##..#?####...?#.?.?.##??#?...?#.#...?...?...##..???.#.#.??????....#..#.?.?.##.?..?
+??.?.#?..#.??.#.#.#.??.#.#??#.#...?...##......?...?...##..?.#?.?#...?..#....##.#?.#..?.....?...#?.?#.?#...?#??#?...??......?#?.??#.#??...?.?
+#?.###..#.....#?....#.##...#.##.....#.#.#.#..##.....?..#.....?##...#?.??.......#??##?..###??....?.#?......#.??..#??.#?.#.#.###??.#?..?#?#..#
+?.?....?.#??.#....?#?#?.?#.?.....#??#.??.?#?#.#......?.#?...?#..#.....??#.?....??.##...?..#?.#??.#.?.??....#?.?.#...##.?#...##....#.#??..?#.
+?#?.?....?.##...??...#?##.#.????##.#..?..?..#..##.??#?.?.??#...?.?...#..?..#..??.?.?#.#.#??..?.#......#...#?.?...?#?.......#.???.#?.##..#?#.
+..??.#..??#####.#?.........#.#.?####...?#.?.#??.#??#..?##?#.??#....?.??...?..?..??.#...?..????..#..##?###...#.??.?##..#.#..?.#..##?#.??.?#.?
+?.##.?.#.##?.?...#...#?##.#.?.#..?#.?.??##?#??.?.?.#.???...??#.##..?#.??#.??....?..#?..##?#?##???.#..#??.?#???...#?##..#.#..#......??#..###.
+###...#...??.#.?..#?..?...#.#?.?.?.?#....#.?#.#..####.?..??##?....?#.?.....#...?#??....??....#.????.?...#.??.#..#..??.?..##..#.?..?..##.#?..
+#....#.?.##.?.......#...##?..#...#.?##..###?..#...?....?#????###.?.....?##.##?.#?.###?.#???#..#....?...#?.....#...?..#.?.#???#.?#.??.?#.#.??
+#...#??......?..#.......??.?..#....#.?#.#?..#.##..??##..#?#.#?...#...?....?.?.?...#??#?#..####...#..?.#?#..#..#.?.####..#?#.##?#.?#?#.....?.
+...#?...#...?#?.##.??..#..#?.?.....#.#?#...#.......#..#....?.??...#???.?...?.#??##.?##?##.#??.#?..##??...#..?.....#?#..##??..###...??.??#?#.
+.........?.#.?.#?...?..?.??#?#.?.#??......#..#.#.?#?.#??#...#?#?.#?.#..?#...?...##?#.##..?.##......??.?##.?###?.#?##..#.?#?...#..##....?##?#
+#??#.##.##..#..#???.?.?#.#...##?##?#.#??.?.#.#??.#.#.#.....?#???#??#..?.#.?..??..?.?....?#.....#.?#.??.?.????#..?.#.??###.??#?..#?..??##..?#
+#.#??..#.?....###.....?#.....##?.#?.?.#.??##...?#?#.?...#..?#.?...?#..?#?#.##.?....##....#..#.##?.??..?.?.?#.?###.?.##?...#...??....#.#..#?.
+?..?..?.?.????#???.??.#.???.??..##..#..??##?.#?..?#??##...?????.#?..#...#..?#...?..?#..#.#....?....#.#..?..?#??#??##....#?..?.???...#.#.#.?#
+..##?#.#....??...........?.##.....?#?..#..?..#?.?.?...#?#.?###...#.#.?#?..?.#.??#?##...?..?........?.#.....##..?#..?..#.??.?...?.?###.#?#...
+..?...#.#?#..#?..#..#..#...#..?.....#.#...#..#?.?.....?..??...#.?.?......?.#?##..?#..#.?..#..?#??...#.#.....?#...?.#...#.#..#.#?.#?.?##?#?##
+#?.#...##.?.#.####.??.#?.?#?.#...#.?#.??#??.##.#.#..#...#.?.#.#.#.###....?.??.?......?....#????..#.......#..#.###.#.?....?#...#..?.#...#.#..
+#...??...??..?.#?...##.?..?#.??.?.#.#..?..###?.????#....#..?.?#.#.#####?##...#.?...??.??...#.#?#.#?..?#..?#??.##.?.?..#....?.#..##.?..?...#.
+?..#?##???..?.##??.?....##.??..?.#?....?..?..?....??#.#.??.?...?#?....#.?.#?????#.?#..#?..##...?????##.....#.#.####.?#.?...#?..?.??##?..#.??
+?....?.#.#?..?.?...#..#.#?.#..#...?.#.?#.?.#.##??..#..#?#?#.#??..#.??.?...?.?..?##?...?.?..##.?..?.???...?#.##?#..??..#?#...?..?....#..#.##?
+#?#.?#......?.#??##...?#.#.##.?...?...?...?#..#?###.##?##..?.#?#.?#..#...#....#?#.#??...?#.#?..#?##?..??..?..#?....?#.#?..#.#.##.?#.?.#..##?
+.#.....##.##?.?...??..##...#?.#...#...#.??.?...#..?.?.#.##?.?#.#?..#???#?#?#.##..#.#.?...........#.?#..##...##??##..?.?###??#.#........#.?.#
+?..#...#.##..?#.?.##..?..?.#....??#...#?..#.#.?.??.?..?#.#.#..??##?.##..?##?#..#?..??#.#.?.?#.?....#.##?#..#....?..##.??###?#...?..#.##.?#?#
+.??......?..??##?..??...?...?##?.?###?..##?..#?..???#.##.##.?..#..???..???...??#..?##...##.?..?.??#..#####?.#.#..?......##.?.##.#.?#??#..#..
+.#?..#.?.#...?.???.?..#...?...?.#....?..#........#.??..?...#.#?..?#....#...#.#...?.?.##???..???..#..??.?..#..#.##?..?#.#..##.??##...?#.....?
diff --git a/interactive/examples/aoc2023/day11/part1.ddp b/interactive/examples/aoc2023/day11/part1.ddp
new file mode 100644
index 000000000..0693bc0f8
--- /dev/null
+++ b/interactive/examples/aoc2023/day11/part1.ddp
@@ -0,0 +1,35 @@
+-- AoC 2023 day 11: sum of L1 distances between galaxies after gap
+-- expansion (factor 1). '#' = 35.
+-- input 0: (row, col, code), 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let g = cells | filter($1[0] == 35) | map($0 ;);
+
+let all_rows = cells | map($0[0] ;) | distinct;
+let rows_with = g | map($0[0] ;) | distinct;
+let row_gaps = all_rows - rows_with;
+let all_cols = cells | map($0[1] ;) | distinct;
+let cols_with = g | map($0[1] ;) | distinct;
+let col_gaps = all_cols - cols_with;
+
+let gk = g | key(; $0[0], $0[1]);
+let rgk = row_gaps | key(; $0[0]);
+let cgk = col_gaps | key(; $0[0]);
+
+let rcnt = gk | join(rgk, ($1[0], $1[1] ; $2[0])) | filter($1[0] < $0[0]) | map($0 ;) | count;
+let radj = (rcnt | map($0 ; 1 * $1[0])) + ((g - (g | join(rcnt, ($0 ;)))) | map($0 ; 0));
+let ccnt = gk | join(cgk, ($1[0], $1[1] ; $2[0])) | filter($1[0] < $0[1]) | map($0 ;) | count;
+let cadj = (ccnt | map($0 ; 1 * $1[0])) + ((g - (g | join(ccnt, ($0 ;)))) | map($0 ; 0));
+
+let gal = radj | join(cadj, ($0[0] + $1[0], $0[1] + $2[0] ;));
+let gs = gal | key(; $0[0], $0[1]);
+
+let contrib = gs | join(gs, (;
+ if(or($1[0] < $2[0], ($1[0] == $2[0]) && ($1[1] < $2[1])),
+ if($1[0] < $2[0], $2[0] - $1[0], $1[0] - $2[0])
+ + if($1[1] < $2[1], $2[1] - $1[1], $1[1] - $2[1]),
+ 0)));
+
+export "result" = contrib
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day11/part2.ddp b/interactive/examples/aoc2023/day11/part2.ddp
new file mode 100644
index 000000000..2912821f1
--- /dev/null
+++ b/interactive/examples/aoc2023/day11/part2.ddp
@@ -0,0 +1,35 @@
+-- AoC 2023 day 11: sum of L1 distances between galaxies after gap
+-- expansion (factor 999999). '#' = 35.
+-- input 0: (row, col, code), 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let g = cells | filter($1[0] == 35) | map($0 ;);
+
+let all_rows = cells | map($0[0] ;) | distinct;
+let rows_with = g | map($0[0] ;) | distinct;
+let row_gaps = all_rows - rows_with;
+let all_cols = cells | map($0[1] ;) | distinct;
+let cols_with = g | map($0[1] ;) | distinct;
+let col_gaps = all_cols - cols_with;
+
+let gk = g | key(; $0[0], $0[1]);
+let rgk = row_gaps | key(; $0[0]);
+let cgk = col_gaps | key(; $0[0]);
+
+let rcnt = gk | join(rgk, ($1[0], $1[1] ; $2[0])) | filter($1[0] < $0[0]) | map($0 ;) | count;
+let radj = (rcnt | map($0 ; 999999 * $1[0])) + ((g - (g | join(rcnt, ($0 ;)))) | map($0 ; 0));
+let ccnt = gk | join(cgk, ($1[0], $1[1] ; $2[0])) | filter($1[0] < $0[1]) | map($0 ;) | count;
+let cadj = (ccnt | map($0 ; 999999 * $1[0])) + ((g - (g | join(ccnt, ($0 ;)))) | map($0 ; 0));
+
+let gal = radj | join(cadj, ($0[0] + $1[0], $0[1] + $2[0] ;));
+let gs = gal | key(; $0[0], $0[1]);
+
+let contrib = gs | join(gs, (;
+ if(or($1[0] < $2[0], ($1[0] == $2[0]) && ($1[1] < $2[1])),
+ if($1[0] < $2[0], $2[0] - $1[0], $1[0] - $2[0])
+ + if($1[1] < $2[1], $2[1] - $1[1], $1[1] - $2[1]),
+ 0)));
+
+export "result" = contrib
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day13/input.txt b/interactive/examples/aoc2023/day13/input.txt
new file mode 100644
index 000000000..044fb8a90
--- /dev/null
+++ b/interactive/examples/aoc2023/day13/input.txt
@@ -0,0 +1,114 @@
+#.###..##
+..###..#.
+.##.....#
+..###....
+..##..#..
+.##.#..#.
+##...##..
+..#.#.#.#
+#####.###
+....#.##.
+.#.######
+....##...
+#..##...#
+
+###.#..###.#....#
+...#....#..#..#.#
+...#...##.#.#.#..
+##.#...##.#.....#
+.#..#....##...##.
+.##....#...#.#...
+..#.##.##.#..##.#
+........#.##.#..#
+
+#...#..#.......
+.#.###.##.#.###
+.#.##..........
+##.##.##.###.##
+..###.#.....#..
+..###.#.....#..
+##.##.##.###.##
+#....#.##...###
+#.#.....##..#..
+...###.....#...
+..#.....#..#...
+##.###.##......
+...#.##.#.#.#.#
+..........####.
+
+...#.....
+...#.....
+...#.....
+#...##...
+..#.....#
+...#.####
+.##....##
+......#..
+#.....#..
+....#.#.#
+##...####
+
+...##.##.#.##
+#....#..#..##
+##.##....###.
+#..###....#.#
+###...##..#..
+#.#...#.#.##.
+###..#.......
+..##.#.#...##
+##..#..#..#.#
+......#...###
+..#..#.##....
+##.#...#.#...
+#..#.....####
+....##.##..#.
+####......###
+
+#.##..#..##
+.#.#####..#
+.##.##...##
+.#.#..#...#
+...####.#..
+#..######..
+..#....#...
+.#####.##.#
+..........#
+##.#..#.#.#
+.......#...
+
+.#.#..#....###...
+######..#.###.#..
+##..##.#..#..#...
+...##...##...#.#.
+#...#..####..#.#.
+..####.#..#...#.#
+####.#......#.#..
+##..##...#.#...#.
+
+#..#.##..........
+.#....##....#.#.#
+.##.....##....###
+#####...##...##..
+###.....#...###.#
+#....#.#......#.#
+#..#...###...#..#
+#.#......#.###.#.
+#..#.##..........
+
+.##.#.#.##..#.#..
+...####...##..#..
+....##...#...##.#
+..###..#..#..####
+.#...##...#.###..
+...###.....#...##
+
+.#...#.####.#
+##..#.#....#.
+.#..#...#..#.
+..#....#.#.#.
+.#...##....#.
+#.###.##..###
+#..#.#....###
+.##..#.#.#...
+.##.#.##..#..
+#...###..##..
diff --git a/interactive/examples/aoc2023/day13/part1.ddp b/interactive/examples/aoc2023/day13/part1.ddp
new file mode 100644
index 000000000..04dc9cd6a
--- /dev/null
+++ b/interactive/examples/aoc2023/day13/part1.ddp
@@ -0,0 +1,39 @@
+-- AoC 2023 day 13 part 1: mirror axes. Axis before row a is valid when no
+-- row pair (x, 2a-1-x) has differing lines. Lines/columns compared as
+-- collected list values (structural ==). Sum 100*(a-1) + (c-1).
+-- input 0: (block, row, col, code), 1-indexed
+
+let cells = input 0 | key($0[0], $0[1], $0[2] ; $0[3]);
+
+let rlines = cells | map($0[0], $0[1] ; $0[2], $1[0]) | collect;
+-- (b, r ; [(c, code)...])
+let clines = cells | map($0[0], $0[2] ; $0[1], $1[0]) | collect;
+-- (b, c ; [(r, code)...])
+
+-- row axes ------------------------------------------------------------
+let rcand = cells | map($0[0], $0[1] ;) | distinct; -- (b, a)
+-- if(1, $1, 0) carries the collected List as ONE field (bare $1 would splice)
+let rlk = rlines | map($0[0] ; $0[1], if(1, $1, 0)); -- (b ; x, L)
+let t1 = rcand | map($0[0] ; $0[1]) | join(rlk, ($0[0], $1[0], $2[0] ; $2[1]));
+-- (b, a, x ; Lx)
+let t2 = t1 | map($0[0], 2 * $0[1] - 1 - $0[2] ; $0[1], $0[2], $1[0]);
+-- (b, y ; a, x, Lx)
+let rviol = t2 | join(rlines, ($0[0], $1[0] ; ($1[1] < $0[1]) && not($1[2] == $2)))
+ | filter($1[0] == 1) | map($0 ;) | distinct;
+-- (b, a) with a mismatched pair
+let rvalid = rcand - rviol;
+let rscore = rvalid | key(; 100 * ($0[1] - 1));
+
+-- column axes ---------------------------------------------------------
+let ccand = cells | map($0[0], $0[2] ;) | distinct;
+let clk = clines | map($0[0] ; $0[1], if(1, $1, 0));
+let s1 = ccand | map($0[0] ; $0[1]) | join(clk, ($0[0], $1[0], $2[0] ; $2[1]));
+let s2 = s1 | map($0[0], 2 * $0[1] - 1 - $0[2] ; $0[1], $0[2], $1[0]);
+let cviol = s2 | join(clines, ($0[0], $1[0] ; ($1[1] < $0[1]) && not($1[2] == $2)))
+ | filter($1[0] == 1) | map($0 ;) | distinct;
+let cvalid = ccand - cviol;
+let cscore = cvalid | key(; $0[1] - 1);
+
+export "result" = rscore + cscore
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day13/part2.ddp b/interactive/examples/aoc2023/day13/part2.ddp
new file mode 100644
index 000000000..3e3466330
--- /dev/null
+++ b/interactive/examples/aoc2023/day13/part2.ddp
@@ -0,0 +1,29 @@
+-- AoC 2023 day 13 part 2: axes with EXACTLY ONE mismatched mirrored cell
+-- pair. Sum 100*(a-1) for rows + (c-1) for cols.
+-- input 0: (block, row, col, code), 1-indexed
+
+let cells = input 0 | key($0[0], $0[1], $0[2] ; $0[3]);
+let cb = cells | map($0[0] ; $0[1], $0[2], $1[0]); -- (b ; r, c, s)
+
+-- row axes
+let rcand = cells | map($0[0], $0[1] ;) | distinct;
+let u1 = rcand | map($0[0] ; $0[1])
+ | join(cb, ($0[0], 2 * $1[0] - 1 - $2[0], $2[1] ; $1[0], $2[0], $2[2]));
+-- (b, y, c ; a, x, s)
+let rmis = u1 | join(cells, ($0[0], $1[0] ; ($1[1] < $0[1]) && not($1[2] == $2[0])))
+ | filter($1[0] == 1) | map($0 ;) | count | filter($1[0] == 1);
+-- (b, a ; 1): exactly one smudge
+let rscore = rmis | key(; 100 * ($0[1] - 1));
+
+-- column axes
+let ccand = cells | map($0[0], $0[2] ;) | distinct;
+let v1 = ccand | map($0[0] ; $0[1])
+ | join(cb, ($0[0], $2[0], 2 * $1[0] - 1 - $2[1] ; $1[0], $2[1], $2[2]));
+-- (b, r, y ; a, x, s)
+let cmis = v1 | join(cells, ($0[0], $1[0] ; ($1[1] < $0[2]) && not($1[2] == $2[0])))
+ | filter($1[0] == 1) | map($0 ;) | count | filter($1[0] == 1);
+let cscore = cmis | key(; $0[1] - 1);
+
+export "result" = rscore + cscore
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day14/input.txt b/interactive/examples/aoc2023/day14/input.txt
new file mode 100644
index 000000000..761c7ce75
--- /dev/null
+++ b/interactive/examples/aoc2023/day14/input.txt
@@ -0,0 +1,7 @@
+.O.#....#.#.#.....O..O..O...
+.O.#....O....#..#....#####..
+.O..##..O..#O...##..#...O...
+O.##.OO#O.#...#.#.#.O..##.O.
+O..#..OO#...#..#.#O.#.#.OO.#
+#.#.#.O.O##...O...#0##..O...
+.O#...#..#.O..O.#.##..#..O..
diff --git a/interactive/examples/aoc2023/day14/part1.ddp b/interactive/examples/aoc2023/day14/part1.ddp
new file mode 100644
index 000000000..0fd256894
--- /dev/null
+++ b/interactive/examples/aoc2023/day14/part1.ddp
@@ -0,0 +1,31 @@
+-- AoC 2023 day 14 part 1: roll rocks north; load = sum(1 + R - r).
+-- Closed form, no recursion: each 'O' lands at (nearest blocker above) +
+-- (its rank among rocks in the same blocker segment). Sentinel cubes at
+-- row 0 remove the no-blocker case; rank counts include self (no defaults).
+-- input 0: (row, col, code) 1-indexed; 'O'=79 '#'=35 '.'=46
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let os = cells | filter($1[0] == 79) | map($0 ;);
+let cubes = cells | filter($1[0] == 35) | map($0 ;);
+
+let allcols = cells | map($0[1] ;) | distinct;
+let cubesN = cubes + (allcols | map(0, $0[0] ;));
+let cubcol = cubesN | map($0[1] ; $0[0]); -- (c ; rc)
+
+-- nearest cube above each rock
+let b = os | map($0[1] ; $0[0])
+ | join(cubcol, ($0[0], $1[0] ; $2[0]))
+ | filter($1[0] < $0[1])
+ | map($0[1], $0[0] ; 1000000 - $1[0]) | min | map($0 ; 1000000 - $1[0]);
+-- (r, c ; b)
+
+let seg = b | map($0[1], $1[0] ; $0[0]); -- (c, b ; r)
+let cnt = seg | join(seg, ($0[0], $0[1], $1[0] ; $2[0]))
+ | filter($1[0] <= $0[2]) | map($0 ;) | count;
+-- (c, b, r ; n): rank incl. self
+let rolled = cnt | map($0[1] + $1[0], $0[0] ;); -- (newr, c)
+
+let maxr = cells | key(; 1000000 - $0[0]) | min | map(; 1000000 - $1[0]);
+export "result" = rolled | map(; $0[0]) | join(maxr, (; 1 + $2[0] - $1[0]))
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day14/part2.ddp b/interactive/examples/aoc2023/day14/part2.ddp
new file mode 100644
index 000000000..b6a7c70d7
--- /dev/null
+++ b/interactive/examples/aoc2023/day14/part2.ddp
@@ -0,0 +1,82 @@
+-- AoC 2023 day 14 part 2: spin cycles (N,W,S,E) to the slt's recursion
+-- limit (the state converges well before). Rock sets tagged by round k;
+-- monotone accumulation, k capped at 141. Rolls are closed-form.
+-- input 0: (row, col, code) 1-indexed; 'O'=79 '#'=35 '.'=46
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let rocks0 = cells | filter($1[0] == 79) | map(0, $0[0], $0[1] ;);
+let cubes = cells | filter($1[0] == 35) | map($0 ;);
+
+let allcols = cells | map($0[1] ;) | distinct;
+let allrows = cells | map($0[0] ;) | distinct;
+let maxr = cells | key(; 1000000 - $0[0]) | min | map(; 1000000 - $1[0]);
+let maxc = cells | key(; 1000000 - $0[1]) | min | map(; 1000000 - $1[0]);
+
+-- sentinel cubes on all four borders
+let cubesX = cubes
+ + (allcols | map(0, $0[0] ;))
+ + (allcols | key(; $0[0]) | join(maxr, ($2[0] + 1, $1[0] ;)))
+ + (allrows | map($0[0], 0 ;))
+ + (allrows | key(; $0[0]) | join(maxc, ($1[0], $2[0] + 1 ;)));
+
+let cubcol = cubesX | map($0[1] ; $0[0]); -- (c ; rc)
+let cubrow = cubesX | map($0[0] ; $0[1]); -- (r ; cc)
+
+spin: {
+ let cur = os | filter($0[0] <= 140);
+ -- (k, r, c)
+
+ -- roll north: blocker = nearest cube above; rank among segment peers
+ let bn = cur | map($0[2] ; $0[0], $0[1])
+ | join(cubcol, ($0[0], $1[0], $1[1] ; $2[0]))
+ | filter($1[0] < $0[2])
+ | map($0[1], $0[2], $0[0] ; 1000000 - $1[0]) | min | map($0 ; 1000000 - $1[0]);
+ -- (k, r, c ; b)
+ let segn = bn | map($0[0], $0[2], $1[0] ; $0[1]); -- (k, c, b ; r)
+ let osn = segn | join(segn, ($0[0], $0[1], $0[2], $1[0] ; $2[0]))
+ | filter($1[0] <= $0[3]) | map($0 ;) | count
+ | map($0[0], $0[2] + $1[0], $0[1] ;);
+ -- (k, r', c)
+
+ -- roll west
+ let bw = osn | map($0[1] ; $0[0], $0[2])
+ | join(cubrow, ($0[0], $1[0], $1[1] ; $2[0]))
+ | filter($1[0] < $0[2])
+ | map($0[1], $0[0], $0[2] ; 1000000 - $1[0]) | min | map($0 ; 1000000 - $1[0]);
+ -- (k, r, c ; b)
+ let segw = bw | map($0[0], $0[1], $1[0] ; $0[2]); -- (k, r, b ; c)
+ let osw = segw | join(segw, ($0[0], $0[1], $0[2], $1[0] ; $2[0]))
+ | filter($1[0] <= $0[3]) | map($0 ;) | count
+ | map($0[0], $0[1], $0[2] + $1[0] ;);
+ -- (k, r, c')
+
+ -- roll south: blocker = nearest cube below; rank counts peers >= r
+ let bs = osw | map($0[2] ; $0[0], $0[1])
+ | join(cubcol, ($0[0], $1[0], $1[1] ; $2[0]))
+ | filter($1[0] > $0[2])
+ | map($0[1], $0[2], $0[0] ; $1[0]) | min;
+ -- (k, r, c ; b)
+ let segs = bs | map($0[0], $0[2], $1[0] ; $0[1]); -- (k, c, b ; r)
+ let oss = segs | join(segs, ($0[0], $0[1], $0[2], $1[0] ; $2[0]))
+ | filter($1[0] >= $0[3]) | map($0 ;) | count
+ | map($0[0], $0[2] - $1[0], $0[1] ;);
+
+ -- roll east
+ let be = oss | map($0[1] ; $0[0], $0[2])
+ | join(cubrow, ($0[0], $1[0], $1[1] ; $2[0]))
+ | filter($1[0] > $0[2])
+ | map($0[1], $0[0], $0[2] ; $1[0]) | min;
+ -- (k, r, c ; b)
+ let sege = be | map($0[0], $0[1], $1[0] ; $0[2]); -- (k, r, b ; c)
+ let ose = sege | join(sege, ($0[0], $0[1], $0[2], $1[0] ; $2[0]))
+ | filter($1[0] >= $0[3]) | map($0 ;) | count
+ | map($0[0] + 1, $0[1], $0[2] - $1[0] ;);
+ -- (k+1, r, c'): next round
+
+ var os = rocks0 + ose | distinct;
+}
+
+let final = spin::os | filter($0[0] == 141);
+export "result" = final | map(; $0[1]) | join(maxr, (; 1 + $2[0] - $1[0]))
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day15/input.txt b/interactive/examples/aoc2023/day15/input.txt
new file mode 100644
index 000000000..a915ec5e9
--- /dev/null
+++ b/interactive/examples/aoc2023/day15/input.txt
@@ -0,0 +1 @@
+rn=1,xw-,nn=8,zg=2,lw=4,oo=2,tt-,wv=9,hy=7,rs=8,sm=4,lf-,td=9,zz=1,ca=2,nd-
diff --git a/interactive/examples/aoc2023/day15/part1.ddp b/interactive/examples/aoc2023/day15/part1.ddp
new file mode 100644
index 000000000..f1b3bb988
--- /dev/null
+++ b/interactive/examples/aoc2023/day15/part1.ddp
@@ -0,0 +1,27 @@
+-- AoC 2023 day 15 part 1: HASH each command; sum. h = ((h+c)*17) % 256.
+-- No % operator: modulo done relationally (cross join a k-table, keep the
+-- residue in [0,256)).
+-- input 0: (r, pos, code), 1-indexed
+
+let chars = input 0 | key($0[0], $0[1] ; $0[2]);
+let lens = chars | map($0[0] ;) | count; -- (r ; len)
+
+let ktab = chars | key(;) | distinct
+ | flatmap(list(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,
+ 18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33))
+ | key(; $1[1]);
+-- (; k)
+
+hash: {
+ let t = st | map($0[0], $0[1] + 1 ; $0[2])
+ | join(chars, (; $0[0], $0[1], ($1[0] + $2[0]) * 17));
+ -- (; r, i, t)
+ let next = t | join(ktab, ($1[0], $1[1], $1[2] - 256 * $2[0] ;))
+ | filter($0[2] >= 0 && $0[2] < 256);
+ var st = (lens | map($0[0], 0, 0 ;)) + next | distinct;
+}
+
+export "result" = hash::st | map($0[0] ; $0[1], $0[2])
+ | join(lens, ($0[0] ; $1[0], $1[1], $2[0])) | filter($1[0] == $1[2])
+ | key(; $1[1]) | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day15/part2.ddp b/interactive/examples/aoc2023/day15/part2.ddp
new file mode 100644
index 000000000..91644692b
--- /dev/null
+++ b/interactive/examples/aoc2023/day15/part2.ddp
@@ -0,0 +1,37 @@
+-- AoC 2023 day 15 part 2: lens boxes. A label survives iff its last op is
+-- '='; slot = rank of first-op-after-last-delete among box mates.
+-- input 0: (r, l1, l2, opchar, digitcode|0); '='=61
+let ops = input 0 | key($0[0] ; $0[1], $0[2], if($0[3] == 61, $0[4] - 48, 0));
+
+let ktab = ops | key(;) | distinct
+ | flatmap(list(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,
+ 18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33))
+ | key(; $1[1]);
+
+let opsk = ops | map($1[0], $1[1] ; $0[0], $1[2]); -- (l1,l2 ; r, op)
+let dels = opsk | filter($1[1] == 0) | map($0 ; 1000000 - $1[0]) | min | map($0 ; 1000000 - $1[0]);
+-- (l1,l2 ; last delete r)
+
+let after = opsk | join(dels, ($0 ; $1[0], $1[1], $2[0])) | filter($1[0] > $1[2]) | map($0 ; $1[0], $1[1]);
+let nodel = opsk - (opsk | join(dels, ($0 ; $1[0], $1[1])));
+let finals = after + nodel; -- (l1,l2 ; r, op)
+
+let last = finals | map($0 ; 1000000 - $1[0], $1[1]) | min | map($0 ; $1[1]);
+let first = finals | map($0 ; $1[0]) | min; -- (l1,l2 ; minr)
+let present = last | filter($1[0] > 0); -- (l1,l2 ; focal)
+
+-- box = HASH(label): two rounds of *17 mod 256 (relational modulo)
+let t1 = present | key(; $0[0], $0[1], $1[0]); -- (; l1,l2,focal)
+let h1 = t1 | join(ktab, ($1[0], $1[1] ; $1[2], $1[0] * 17 - 256 * $2[0]))
+ | filter($1[1] >= 0 && $1[1] < 256); -- (l1,l2 ; focal, h1)
+let t2 = h1 | key(; $0[0], $0[1], $1[0], ($1[1] + $0[1]) * 17);
+let hh = t2 | join(ktab, ($1[0], $1[1] ; $1[2], $1[3] - 256 * $2[0]))
+ | filter($1[1] >= 0 && $1[1] < 256); -- (l1,l2 ; focal, box)
+
+let boxed = hh | join(first, ($1[1] ; $2[0], $1[0])); -- (box ; minr, focal)
+let rank = boxed | join(boxed, ($0[0], $1[0], $1[1] ; $2[0]))
+ | filter($1[0] <= $0[1]) | map($0 ;) | count; -- (box, minr, focal ; slot)
+
+export "result" = rank | key(; (1 + $0[0]) * $1[0] * $0[2])
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day16/input.txt b/interactive/examples/aoc2023/day16/input.txt
new file mode 100644
index 000000000..c6b9f66e0
--- /dev/null
+++ b/interactive/examples/aoc2023/day16/input.txt
@@ -0,0 +1,25 @@
+-............./...\................\......-.......\....|...--....................\.............\...\.-........
+/\...............\.........|....|..../...-....\...\../...........\..\..................................|......
+.\..........\..../....../............-.-.........|........../.............|.............-......./..........\..
+.\.......\..........\.......\.\........................\.....|...\.../........|..............\\...........|..-
+.\...........\.....\../...-\..|....-.................-.|.....|/\...\................././.........\.\.../......
+./....\...\..............|...\.......\...-.....|..|...../...../..-.....................\.......\...../...|..|.
+...........|.-.........|.....-.../......\./.........\\....\...\..|./...-.............\........................
+-.......................\.............\........\.\...........-....................../...........\....|.-......
+-............\..........-./.....\\......\.......|.....-..................-.-\\.....|............-....\........
+..............-....|..................-......|....-.../....................-....\...........\.................
+................\.......\../.............../......|.....\............-.....\...\....................|..-.../.-
+|\.............-......./...........-........../......\-.........-.....................................|\......
+.....\.....||.........-............../............|....-...........\.\.....................................|\.
+......................./...\......\......./|...\..........|...\.../...........\....\./..-..........\..........
+.....\................\.\..............\./.\..-......|........../.....\..\.........|....\....\.....\..........
+...\...\...\.......|.....\\.\..\........\.-.....\.|..................................|......-.................
+\|...../............................|.../.\......\......-.............|....|...|...-.......|.....\............
+.\/..........-..........|........./...................\.........../\.......-.............../............./\.-.
+.......\.......\...\............\.-.../.......\....................|.../..............\./.........-......\.-..
+../...\...-...|.|../......\......\...\...-................................\.........-........-............./..
+...\.../..|........\...|...../../...|............-..........|/...............|..........\|................./..
+......\/.\......|..................-...\.......\..|........./.-......-...........-...\......|..........-|/-...
+.\.........-........./........................|...........\/....\-......\...\../\.............................
+|.....\.\.....|-.............|.......|...........\..|\..........\..|........................|....-.......\//..
+..........-.............\.........|......\.......\.../..../.\.-..........\....../........................\....
diff --git a/interactive/examples/aoc2023/day16/part1.ddp b/interactive/examples/aoc2023/day16/part1.ddp
new file mode 100644
index 000000000..1eeed94db
--- /dev/null
+++ b/interactive/examples/aoc2023/day16/part1.ddp
@@ -0,0 +1,28 @@
+-- AoC 2023 day 16 part 1: beam bouncing; count energized cells.
+-- dirs: r=0 l=1 u=2 d=3; '.'=46 '-'=45 '|'=124 '/'=47 '\'=92
+-- input 0: (row, col, code) 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+
+let shift = cells | key(;) | distinct | flatmap(list(
+ tuple(0,46,0,1,0), tuple(0,45,0,1,0), tuple(0,124,1,0,3), tuple(0,124,-1,0,2),
+ tuple(0,47,-1,0,2), tuple(0,92,1,0,3),
+ tuple(1,46,0,-1,1), tuple(1,45,0,-1,1), tuple(1,124,1,0,3), tuple(1,124,-1,0,2),
+ tuple(1,47,1,0,3), tuple(1,92,-1,0,2),
+ tuple(2,46,-1,0,2), tuple(2,45,0,1,0), tuple(2,45,0,-1,1), tuple(2,124,-1,0,2),
+ tuple(2,47,0,1,0), tuple(2,92,0,-1,1),
+ tuple(3,46,1,0,3), tuple(3,45,0,1,0), tuple(3,45,0,-1,1), tuple(3,124,1,0,3),
+ tuple(3,47,0,-1,1), tuple(3,92,0,1,0)))
+ | key($1[1][0], $1[1][1] ; $1[1][2], $1[1][3], $1[1][4]);
+-- (dir, sym ; dr, dc, newdir)
+
+beam: {
+ let onsym = l | map($0[0], $0[1] ; $0[2]) | join(cells, ($1[0], $2[0] ; $0[0], $0[1]));
+ -- (dir, sym ; r, c)
+ let next = onsym | join(shift, ($1[0] + $2[0], $1[1] + $2[1], $2[2] ;));
+ var l = (cells | filter($0[0] == 1 && $0[1] == 1) | map(1, 1, 0 ;)) + next | distinct;
+}
+
+export "result" = beam::l | map($0[0], $0[1] ;)
+ | join(cells, ($0 ;)) | distinct
+ | key(;) | count | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day16/part2.ddp b/interactive/examples/aoc2023/day16/part2.ddp
new file mode 100644
index 000000000..384037206
--- /dev/null
+++ b/interactive/examples/aoc2023/day16/part2.ddp
@@ -0,0 +1,44 @@
+-- AoC 2023 day 16 part 2: best edge seed. Beams tagged by source (sd, si).
+-- Mirrors the slt exactly, including its 'u' seeds sitting at row=MAX(c).
+-- input 0: (row, col, code) 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+
+let shift = cells | key(;) | distinct | flatmap(list(
+ tuple(0,46,0,1,0), tuple(0,45,0,1,0), tuple(0,124,1,0,3), tuple(0,124,-1,0,2),
+ tuple(0,47,-1,0,2), tuple(0,92,1,0,3),
+ tuple(1,46,0,-1,1), tuple(1,45,0,-1,1), tuple(1,124,1,0,3), tuple(1,124,-1,0,2),
+ tuple(1,47,1,0,3), tuple(1,92,-1,0,2),
+ tuple(2,46,-1,0,2), tuple(2,45,0,1,0), tuple(2,45,0,-1,1), tuple(2,124,-1,0,2),
+ tuple(2,47,0,1,0), tuple(2,92,0,-1,1),
+ tuple(3,46,1,0,3), tuple(3,45,0,1,0), tuple(3,45,0,-1,1), tuple(3,124,1,0,3),
+ tuple(3,47,0,-1,1), tuple(3,92,0,1,0)))
+ | key($1[1][0], $1[1][1] ; $1[1][2], $1[1][3], $1[1][4]);
+
+let rows = cells | map($0[0] ;) | distinct;
+let cols = cells | map($0[1] ;) | distinct;
+let minc = cells | key(; $0[1]) | min;
+let maxc = cells | key(; 1000000 - $0[1]) | min | map(; 1000000 - $1[0]);
+let minr = cells | key(; $0[0]) | min;
+
+let seeds = (rows | key(; $0[0]) | join(minc, ($1[0], $2[0], 0, 0, $1[0] ;)))
+ + (rows | key(; $0[0]) | join(maxc, ($1[0], $2[0], 1, 1, $1[0] ;)))
+ + (cols | key(; $0[0]) | join(minr, ($2[0], $1[0], 3, 3, $1[0] ;)))
+ + (cols | key(; $0[0]) | join(maxc, ($2[0], $1[0], 2, 2, $1[0] ;)));
+-- key (r, c, dir, sd, si)
+
+beam: {
+ let onsym = l | map($0[0], $0[1] ; $0[2], $0[3], $0[4])
+ | join(cells, ($1[0], $2[0] ; $0[0], $0[1], $1[1], $1[2]));
+ -- (dir, sym ; r, c, sd, si)
+ let next = onsym | join(shift, ($1[0] + $2[0], $1[1] + $2[1], $2[2], $1[2], $1[3] ;));
+ var l = seeds + next | distinct;
+}
+
+let energized = beam::l | map($0[0], $0[1] ; $0[3], $0[4])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1] ;)) | distinct
+ | map($0[2], $0[3] ;) | count;
+-- (sd, si ; n)
+
+export "result" = energized | key(; 1000000 - $1[0]) | min | map(; 1000000 - $1[0])
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day17/input.txt b/interactive/examples/aoc2023/day17/input.txt
new file mode 100644
index 000000000..5b12ee132
--- /dev/null
+++ b/interactive/examples/aoc2023/day17/input.txt
@@ -0,0 +1,11 @@
+62838899848717171482491386857
+97364142198727715957423491912
+86369399573486615223592185179
+65896629415741215317915596532
+87429913559342885454881133182
+87599176619626884624793447611
+69826949796636945138977813282
+97787786569751297721492648197
+56111693893781611276884581493
+11326495731998819531787964758
+45631262918273787936318151868
diff --git a/interactive/examples/aoc2023/day17/part1.ddp b/interactive/examples/aoc2023/day17/part1.ddp
new file mode 100644
index 000000000..666d6dd8a
--- /dev/null
+++ b/interactive/examples/aoc2023/day17/part1.ddp
@@ -0,0 +1,29 @@
+-- AoC 2023 day 17 part 1: min-cost crucible path; state (r,c,dr,dc,steps),
+-- straight while steps<3, turns reset steps. min-reducer fixpoint.
+-- input 0: (row, col, cost) 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let seeds = (cells | filter($0[0] == 1 && $0[1] == 1) | map(1, 1, 1, 0, 0 ; 0))
+ + (cells | filter($0[0] == 1 && $0[1] == 1) | map(1, 1, 0, 1, 0 ; 0));
+
+dijk: {
+ let straight = mc | filter($0[4] < 3)
+ | map($0[0] + $0[2], $0[1] + $0[3] ; $0[2], $0[3], $0[4], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], $1[2] + 1 ; $1[3] + $2[0]));
+ let turna = mc
+ | map($0[0] + $0[3], $0[1] + $0[2] ; $0[3], $0[2], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], 1 ; $1[2] + $2[0]));
+ let turnb = mc
+ | map($0[0] - $0[3], $0[1] - $0[2] ; 0 - $0[3], 0 - $0[2], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], 1 ; $1[2] + $2[0]));
+ var mc = seeds + straight + turna + turnb | min;
+}
+
+let maxr = cells | key(; 1000000 - $0[0]) | min | map(; 1000000 - $1[0]);
+let maxc = cells | key(; 1000000 - $0[1]) | min | map(; 1000000 - $1[0]);
+let corner = maxr | join(maxc, (; $1[0], $2[0]));
+
+export "result" = dijk::mc | key(; $0[0], $0[1], $1[0])
+ | join(corner, (; $1[0] == $2[0], $1[1] == $2[1], $1[2]))
+ | filter($1[0] && $1[1]) | key(; $1[2]) | min
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day17/part2.ddp b/interactive/examples/aoc2023/day17/part2.ddp
new file mode 100644
index 000000000..a1ac420f8
--- /dev/null
+++ b/interactive/examples/aoc2023/day17/part2.ddp
@@ -0,0 +1,29 @@
+-- AoC 2023 day 17 part 2: ultra crucible; straight while steps<10, turns
+-- (and the goal) require steps>=4.
+-- input 0: (row, col, cost) 1-indexed
+
+let cells = input 0 | key($0[0], $0[1] ; $0[2]);
+let seeds = (cells | filter($0[0] == 1 && $0[1] == 1) | map(1, 1, 1, 0, 0 ; 0))
+ + (cells | filter($0[0] == 1 && $0[1] == 1) | map(1, 1, 0, 1, 0 ; 0));
+
+dijk: {
+ let straight = mc | filter($0[4] < 10)
+ | map($0[0] + $0[2], $0[1] + $0[3] ; $0[2], $0[3], $0[4], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], $1[2] + 1 ; $1[3] + $2[0]));
+ let turna = mc | filter($0[4] >= 4)
+ | map($0[0] + $0[3], $0[1] + $0[2] ; $0[3], $0[2], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], 1 ; $1[2] + $2[0]));
+ let turnb = mc | filter($0[4] >= 4)
+ | map($0[0] - $0[3], $0[1] - $0[2] ; 0 - $0[3], 0 - $0[2], $1[0])
+ | join(cells, ($0[0], $0[1], $1[0], $1[1], 1 ; $1[2] + $2[0]));
+ var mc = seeds + straight + turna + turnb | min;
+}
+
+let maxr = cells | key(; 1000000 - $0[0]) | min | map(; 1000000 - $1[0]);
+let maxc = cells | key(; 1000000 - $0[1]) | min | map(; 1000000 - $1[0]);
+let corner = maxr | join(maxc, (; $1[0], $2[0]));
+
+export "result" = dijk::mc | filter($0[4] >= 4) | key(; $0[0], $0[1], $1[0], $0[4])
+ | join(corner, (; $1[0] == $2[0], $1[1] == $2[1], $1[2], $1[3]))
+ | filter($1[0] && $1[1]) | key(; $1[2]) | min
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day18/input.txt b/interactive/examples/aoc2023/day18/input.txt
new file mode 100644
index 000000000..f2ced3b66
--- /dev/null
+++ b/interactive/examples/aoc2023/day18/input.txt
@@ -0,0 +1,15 @@
+R 1 (#53d732)
+L 5 (#292431)
+U 6 (#4c7272)
+L 9 (#49ace3)
+U 3 (#7b94e6)
+R 1 (#5579d4)
+L 9 (#1d7886)
+U 3 (#171219)
+R 9 (#45fa39)
+R 11 (#222422)
+U 6 (#91c869)
+L 8 (#7581c5)
+U 9 (#46aab5)
+R 9 (#6f72a5)
+L 7 (#42abb1)
diff --git a/interactive/examples/aoc2023/day18/part1.ddp b/interactive/examples/aoc2023/day18/part1.ddp
new file mode 100644
index 000000000..c2a2dbcdf
--- /dev/null
+++ b/interactive/examples/aoc2023/day18/part1.ddp
@@ -0,0 +1,38 @@
+-- AoC 2023 day 18 part 1: lavaduct area via shoelace + perimeter/2 + 1.
+-- input 0: (line, dircode, steps, h1..h6); U=85 D=68 L=76 R=82
+
+let instr = input 0 | key($0[0] ;
+ if($0[1] == 85, -1, if($0[1] == 68, 1, 0)),
+ if($0[1] == 76, -1, if($0[1] == 82, 1, 0)),
+ $0[2]);
+-- (line ; dr, dc, steps)
+
+path: {
+ let step = p | map($0[0] ; $0[1], $0[2])
+ | join(instr, ($0[0] + 1, $1[0] + $2[0] * $2[2], $1[1] + $2[1] * $2[2] ;));
+ var p = (instr | filter($0[0] == 1) | map(1, 0, 0 ;)) + step | distinct;
+}
+
+let terms = path::p | map($0[0] + 1 ; $0[1], $0[2])
+ | join(path::p | map($0[0] ; $0[1], $0[2]),
+ (; ($1[0] + $2[0]) * ($1[1] - $2[1])));
+let s = terms | collect | map(; fold($1, 0, ^1 + ^0[0])) | map(; if($1[0] < 0, 0 - $1[0], $1[0]));
+let perim = instr | key(; $1[2]) | collect | map(; fold($1, 0, ^1 + ^0[0]));
+let n = s | join(perim, (; $1[0] + $2[0]));
+-- (; |S| + P), even; halve by binary decomposition (no division)
+
+let powtab = instr | key(;) | distinct
+ | flatmap(list(549755813888, 274877906944, 137438953472, 68719476736, 34359738368, 17179869184, 8589934592, 4294967296, 2147483648, 1073741824, 536870912, 268435456, 134217728, 67108864, 33554432, 16777216, 8388608, 4194304, 2097152, 1048576, 524288, 262144, 131072, 65536, 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1))
+ | key($1[0] ; $1[1]);
+-- (i ; 2^(39-i))
+
+halve: {
+ let next = st | map($0[0] ; $0[1], $0[2])
+ | join(powtab, ($0[0] + 1,
+ if($1[0] >= 2 * $2[0], $1[0] - 2 * $2[0], $1[0]),
+ if($1[0] >= 2 * $2[0], $1[1] + $2[0], $1[1]) ;));
+ var st = (n | map(0, $1[0], 0 ;)) + next | distinct;
+}
+
+export "result" = halve::st | filter($0[0] == 40) | key(; $0[2] + 1)
+ | arrange | inspect(part1);
\ No newline at end of file
diff --git a/interactive/examples/aoc2023/day18/part2.ddp b/interactive/examples/aoc2023/day18/part2.ddp
new file mode 100644
index 000000000..d18699108
--- /dev/null
+++ b/interactive/examples/aoc2023/day18/part2.ddp
@@ -0,0 +1,38 @@
+-- AoC 2023 day 18 part 2: directions/steps decoded from the hex color.
+-- last hex digit: 0=R 1=D 2=L 3=U; first five = steps.
+-- input 0: (line, dircode, steps, h1..h6)
+
+let instr = input 0 | key($0[0] ;
+ if($0[8] == 51, -1, if($0[8] == 49, 1, 0)),
+ if($0[8] == 50, -1, if($0[8] == 48, 1, 0)),
+ ((((if($0[3] <= 57, $0[3] - 48, $0[3] - 87)) * 16 + if($0[4] <= 57, $0[4] - 48, $0[4] - 87)) * 16 + if($0[5] <= 57, $0[5] - 48, $0[5] - 87)) * 16 + if($0[6] <= 57, $0[6] - 48, $0[6] - 87)) * 16 + if($0[7] <= 57, $0[7] - 48, $0[7] - 87));
+
+path: {
+ let step = p | map($0[0] ; $0[1], $0[2])
+ | join(instr, ($0[0] + 1, $1[0] + $2[0] * $2[2], $1[1] + $2[1] * $2[2] ;));
+ var p = (instr | filter($0[0] == 1) | map(1, 0, 0 ;)) + step | distinct;
+}
+
+let terms = path::p | map($0[0] + 1 ; $0[1], $0[2])
+ | join(path::p | map($0[0] ; $0[1], $0[2]),
+ (; ($1[0] + $2[0]) * ($1[1] - $2[1])));
+let s = terms | collect | map(; fold($1, 0, ^1 + ^0[0])) | map(; if($1[0] < 0, 0 - $1[0], $1[0]));
+let perim = instr | key(; $1[2]) | collect | map(; fold($1, 0, ^1 + ^0[0]));
+let n = s | join(perim, (; $1[0] + $2[0]));
+-- (; |S| + P), even; halve by binary decomposition (no division)
+
+let powtab = instr | key(;) | distinct
+ | flatmap(list(549755813888, 274877906944, 137438953472, 68719476736, 34359738368, 17179869184, 8589934592, 4294967296, 2147483648, 1073741824, 536870912, 268435456, 134217728, 67108864, 33554432, 16777216, 8388608, 4194304, 2097152, 1048576, 524288, 262144, 131072, 65536, 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1))
+ | key($1[0] ; $1[1]);
+-- (i ; 2^(39-i))
+
+halve: {
+ let next = st | map($0[0] ; $0[1], $0[2])
+ | join(powtab, ($0[0] + 1,
+ if($1[0] >= 2 * $2[0], $1[0] - 2 * $2[0], $1[0]),
+ if($1[0] >= 2 * $2[0], $1[1] + $2[0], $1[1]) ;));
+ var st = (n | map(0, $1[0], 0 ;)) + next | distinct;
+}
+
+export "result" = halve::st | filter($0[0] == 40) | key(; $0[2] + 1)
+ | arrange | inspect(part2);
\ No newline at end of file
diff --git a/interactive/examples/aoc2023/day19/input.txt b/interactive/examples/aoc2023/day19/input.txt
new file mode 100644
index 000000000..0f65a7f7a
--- /dev/null
+++ b/interactive/examples/aoc2023/day19/input.txt
@@ -0,0 +1,16 @@
+in{x<1164:zoz,s>1473:A,a<8576:ask,A}
+ask{m<2275:rsx,zoz}
+rsx{a>8922:A,s>4213:A,R}
+zoz{m>5813:A,s>4522:A,x<245:R,krw}
+krw{a>3747:dqu,a>299:R,a<927:A,A}
+ton{a<8226:ktx,m>1965:ktx,s>3591:uhu,dqu}
+dqu{m<6866:uhu,s>4649:A,R}
+uhu{a<6293:A,lel}
+lel{a<6145:ktx,A}
+ktx{s>8889:R,a>3215:R,R}
+
+{x=61,m=818,a=525,s=29}
+{x=225,m=7722,a=964,s=466}
+{x=528,m=3628,a=914,s=8823}
+{x=13,m=675,a=5933,s=9}
+{x=9693,m=8583,a=125,s=787}
diff --git a/interactive/examples/aoc2023/day19/part1.ddp b/interactive/examples/aoc2023/day19/part1.ddp
new file mode 100644
index 000000000..3042da137
--- /dev/null
+++ b/interactive/examples/aoc2023/day19/part1.ddp
@@ -0,0 +1,26 @@
+-- AoC 2023 day 19 part 1: run each part through the workflows; first
+-- matching rule (min priority) decides; sum x+m+a+s of accepted.
+-- input 0: rules (1, state, prio, field, cmp, val, next) / parts (0, x, m, a, s, 0, 0)
+
+let rulesbys = input 0 | filter($0[0] == 1) | key($0[1] ; $0[2], $0[3], $0[4], $0[5], $0[6]);
+-- (state ; prio, field, cmp, val, next)
+let parts = input 0 | filter($0[0] == 0) | key(0, $0[1], $0[2], $0[3], $0[4] ;);
+
+move: {
+ let mtch = mv | map($0[0] ; $0[1], $0[2], $0[3], $0[4])
+ | join(rulesbys, ($0[0], $1[0], $1[1], $1[2], $1[3] ;
+ if($2[2] == 0,
+ if($2[1] == 0, $1[0], if($2[1] == 1, $1[1], if($2[1] == 2, $1[2], $1[3]))) < $2[3],
+ if($2[1] == 0, $1[0], if($2[1] == 1, $1[1], if($2[1] == 2, $1[2], $1[3]))) > $2[3]),
+ $2[0], $2[4]))
+ | filter($1[0] == 1) | map($0 ; $1[1], $1[2])
+ | min;
+ -- (state,x,m,a,s ; first prio, next)
+ let moved = mtch | map($1[1], $0[1], $0[2], $0[3], $0[4] ;);
+ var mv = parts + moved | distinct;
+}
+
+export "result" = move::mv | filter($0[0] == -1)
+ | key(; $0[1] + $0[2] + $0[3] + $0[4])
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day19/part2.ddp b/interactive/examples/aoc2023/day19/part2.ddp
new file mode 100644
index 000000000..e605d4e98
--- /dev/null
+++ b/interactive/examples/aoc2023/day19/part2.ddp
@@ -0,0 +1,36 @@
+-- AoC 2023 day 19 part 2: hypercube regions through the workflow tree.
+-- Faithful to the slt: no emptiness clamping, so impossible regions
+-- contribute negative products (their oracle answer is negative too).
+-- input 0: rules (1, state, prio, field, cmp, val, next) / parts (0, ...)
+
+let rules = input 0 | filter($0[0] == 1) | key($0[1], $0[2] ; $0[3], $0[4], $0[5], $0[6]);
+-- (state, prio ; field, cmp, val, next)
+
+flow: {
+ let cur = reg | map($0[0], $0[1] ; $0[2], $0[3], $0[4], $0[5], $0[6], $0[7], $0[8], $0[9]);
+ let b1 = cur | join(rules, ($2[3], 1,
+ if(($2[0] == 0) && ($2[1] == 1), if($2[2] + 1 > $1[0], $2[2] + 1, $1[0]), $1[0]),
+ if(($2[0] == 0) && ($2[1] == 0), if($2[2] - 1 < $1[1], $2[2] - 1, $1[1]), $1[1]),
+ if(($2[0] == 1) && ($2[1] == 1), if($2[2] + 1 > $1[2], $2[2] + 1, $1[2]), $1[2]),
+ if(($2[0] == 1) && ($2[1] == 0), if($2[2] - 1 < $1[3], $2[2] - 1, $1[3]), $1[3]),
+ if(($2[0] == 2) && ($2[1] == 1), if($2[2] + 1 > $1[4], $2[2] + 1, $1[4]), $1[4]),
+ if(($2[0] == 2) && ($2[1] == 0), if($2[2] - 1 < $1[5], $2[2] - 1, $1[5]), $1[5]),
+ if(($2[0] == 3) && ($2[1] == 1), if($2[2] + 1 > $1[6], $2[2] + 1, $1[6]), $1[6]),
+ if(($2[0] == 3) && ($2[1] == 0), if($2[2] - 1 < $1[7], $2[2] - 1, $1[7]), $1[7]) ;));
+ let b2 = cur | join(rules, ($0[0], $0[1] + 1,
+ if(($2[0] == 0) && ($2[1] == 0), if($2[2] > $1[0], $2[2], $1[0]), $1[0]),
+ if(($2[0] == 0) && ($2[1] == 1), if($2[2] < $1[1], $2[2], $1[1]), $1[1]),
+ if(($2[0] == 1) && ($2[1] == 0), if($2[2] > $1[2], $2[2], $1[2]), $1[2]),
+ if(($2[0] == 1) && ($2[1] == 1), if($2[2] < $1[3], $2[2], $1[3]), $1[3]),
+ if(($2[0] == 2) && ($2[1] == 0), if($2[2] > $1[4], $2[2], $1[4]), $1[4]),
+ if(($2[0] == 2) && ($2[1] == 1), if($2[2] < $1[5], $2[2], $1[5]), $1[5]),
+ if(($2[0] == 3) && ($2[1] == 0), if($2[2] > $1[6], $2[2], $1[6]), $1[6]),
+ if(($2[0] == 3) && ($2[1] == 1), if($2[2] < $1[7], $2[2], $1[7]), $1[7]) ;));
+ var reg = (rules | key(;) | distinct | map(0, 1, 1, 4000, 1, 4000, 1, 4000, 1, 4000 ;))
+ + b1 + b2 | arrange;
+}
+
+export "result" = flow::reg | filter($0[0] == -1)
+ | key(; (1 + $0[3] - $0[2]) * (1 + $0[5] - $0[4]) * (1 + $0[7] - $0[6]) * (1 + $0[9] - $0[8]))
+ | collect | map(; fold($1, 0, ^1 + ^0[0]))
+ | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/day22/input.txt b/interactive/examples/aoc2023/day22/input.txt
new file mode 100644
index 000000000..5bf460533
--- /dev/null
+++ b/interactive/examples/aoc2023/day22/input.txt
@@ -0,0 +1,25 @@
+3,5,62~3,9,62
+5,5,623~5,1,623
+8,3,176~8,2,176
+7,6,17~7,8,17
+9,3,821~9,3,163
+2,9,71~2,6,71
+3,3,514~3,3,390
+7,4,494~7,9,494
+5,9,842~5,9,840
+9,1,41~9,1,296
+5,4,276~5,4,94
+3,3,838~3,6,838
+9,8,425~6,8,425
+1,2,55~1,8,55
+1,4,249~3,4,249
+8,8,541~5,8,541
+5,4,634~5,4,365
+4,9,745~4,9,293
+3,6,621~3,6,287
+4,9,645~4,9,389
+7,1,712~0,1,712
+8,2,69~7,2,69
+2,3,374~8,3,374
+7,9,495~0,9,495
+4,9,200~8,9,200
diff --git a/interactive/examples/aoc2023/day22/part1.ddp b/interactive/examples/aoc2023/day22/part1.ddp
new file mode 100644
index 000000000..f1ebf5bcd
--- /dev/null
+++ b/interactive/examples/aoc2023/day22/part1.ddp
@@ -0,0 +1,39 @@
+-- AoC 2023 day 22 part 1: bricks fall (interval overlap + max-rest fixpoint);
+-- count bricks that uniquely support nothing.
+-- input 0: (r, x1,y1,z1, x2,y2,z2)
+
+-- slt's generate_series drops bricks with ANY reversed axis; mirror that.
+let bricks = input 0 | filter($0[1] <= $0[4] && $0[2] <= $0[5] && $0[3] <= $0[6])
+ | key($0[0] ; $0[1], $0[2], $0[3], $0[4], $0[5], $0[6]);
+-- (r ; xlo, ylo, zlo, xhi, yhi, zhi)
+
+let bu = bricks | key(; $0[0], $1[0], $1[1], $1[2], $1[3], $1[4], $1[5]);
+let ovl = bu | join(bu, ($1[0], $2[0] ;
+ (if($1[1] > $2[1], $1[1], $2[1]) <= if($1[4] < $2[4], $1[4], $2[4]))
+ && (if($1[2] > $2[2], $1[2], $2[2]) <= if($1[5] < $2[5], $1[5], $2[5]))
+ && ($1[3] < $2[3]),
+ $1[6] - $1[3] + 1))
+ | filter($1[0] == 1) | map($0[0] ; $0[1], $1[1]);
+-- (lower ; upper, height_lower) for xy-overlapping pairs
+
+-- resting z per brick, stored negated (mz = BIG - z) so `min` computes max
+rests: {
+ let lifted = rest | join(ovl, ($2[0] ; $1[0] - $2[1]));
+ var rest = (bricks | map($0[0] ; 999999)) + lifted | min;
+}
+let rest = rests::rest;
+-- (r ; BIG - resting z), BIG = 1000000
+
+-- direct supports: rest_top(lower) + 1 == rest_z(upper)
+let s1 = ovl | join(rest, ($1[0] ; $0[0], $2[0] - $1[1]));
+-- (upper ; lower, required mz_upper)
+let sup = s1 | join(rest, ($1[0], $0[0], $1[1] == $2[0] ;))
+ | filter($0[2] == 1) | map($0[0], $0[1] ;);
+-- (lower, upper)
+
+let uniq2 = sup | map($0[1] ;) | count | filter($1[0] == 1) | map($0 ;);
+
+let critical = sup | map($0[1] ; $0[0]) | join(uniq2, ($1[0] ;)) | distinct;
+let total = input 0 | key(;) | count; -- ALL lines, per the slt
+export "result" = total | join(critical | key(;) | count, (; $1[0] - $2[0]))
+ | arrange | inspect(part1);
diff --git a/interactive/examples/aoc2023/day22/part2.ddp b/interactive/examples/aoc2023/day22/part2.ddp
new file mode 100644
index 000000000..e38184cdf
--- /dev/null
+++ b/interactive/examples/aoc2023/day22/part2.ddp
@@ -0,0 +1,51 @@
+-- AoC 2023 day 22 part 2: transitive falls -- (a, b) when every supporter of b
+-- falls with a; count pairs.
+-- input 0: (r, x1,y1,z1, x2,y2,z2)
+
+-- slt's generate_series drops bricks with ANY reversed axis; mirror that.
+let bricks = input 0 | filter($0[1] <= $0[4] && $0[2] <= $0[5] && $0[3] <= $0[6])
+ | key($0[0] ; $0[1], $0[2], $0[3], $0[4], $0[5], $0[6]);
+-- (r ; xlo, ylo, zlo, xhi, yhi, zhi)
+
+let bu = bricks | key(; $0[0], $1[0], $1[1], $1[2], $1[3], $1[4], $1[5]);
+let ovl = bu | join(bu, ($1[0], $2[0] ;
+ (if($1[1] > $2[1], $1[1], $2[1]) <= if($1[4] < $2[4], $1[4], $2[4]))
+ && (if($1[2] > $2[2], $1[2], $2[2]) <= if($1[5] < $2[5], $1[5], $2[5]))
+ && ($1[3] < $2[3]),
+ $1[6] - $1[3] + 1))
+ | filter($1[0] == 1) | map($0[0] ; $0[1], $1[1]);
+-- (lower ; upper, height_lower) for xy-overlapping pairs
+
+-- resting z per brick, stored negated (mz = BIG - z) so `min` computes max
+rests: {
+ let lifted = rest | join(ovl, ($2[0] ; $1[0] - $2[1]));
+ var rest = (bricks | map($0[0] ; 999999)) + lifted | min;
+}
+let rest = rests::rest;
+-- (r ; BIG - resting z), BIG = 1000000
+
+-- direct supports: rest_top(lower) + 1 == rest_z(upper)
+let s1 = ovl | join(rest, ($1[0] ; $0[0], $2[0] - $1[1]));
+-- (upper ; lower, required mz_upper)
+let sup = s1 | join(rest, ($1[0], $0[0], $1[1] == $2[0] ;))
+ | filter($0[2] == 1) | map($0[0], $0[1] ;);
+-- (lower, upper)
+
+let uniq2 = sup | map($0[1] ;) | count | filter($1[0] == 1) | map($0 ;);
+
+let supL = sup | map($0[0] ; $0[1]); -- (lower ; upper)
+let supcnt = sup | map($0[1] ;) | count; -- (b ; #supporters)
+let seed = sup | map($0[1] ; $0[0]) | join(uniq2, ($1[0], $0[0] ;));
+-- (a, b): b uniquely supported by a
+
+trans: {
+ let fell = st | map($0[1] ; $0[0]) | join(supL, ($1[0], $2[0] ;))
+ | map($0 ;) | count;
+ -- (a, b ; #supporters of b falling with a)
+ let full = fell | map($0[1] ; $0[0], $1[0])
+ | join(supcnt, ($1[0], $0[0], $1[1] == $2[0] ;))
+ | filter($0[2] == 1) | map($0[0], $0[1] ;);
+ var st = seed + full | distinct;
+}
+
+export "result" = trans::st | key(;) | count | arrange | inspect(part2);
diff --git a/interactive/examples/aoc2023/expected.txt b/interactive/examples/aoc2023/expected.txt
new file mode 100644
index 000000000..d753192b0
--- /dev/null
+++ b/interactive/examples/aoc2023/expected.txt
@@ -0,0 +1,34 @@
+# day part answer (vec backend; oracle: Materialize sqllogictest advent-of-code/2023)
+01 1 278
+01 2 391
+02 1 8
+02 2 2567
+03 1 11374
+03 2 1587570
+04 1 184
+04 2 978
+05 1 60602459
+05 2 5
+06 1 1180707528
+07 1 340665
+07 2 332531
+10 1 1439
+10 2 2073
+11 1 129655908
+11 2 129655908
+13 1 100
+13 2 16
+14 1 146
+14 2 105
+15 1 2021
+15 2 6155
+16 1 15
+16 2 613
+17 1 156
+17 2 190
+18 1 73
+18 2 34133752459
+19 1 42458
+19 2 -257636238955235
+22 1 23
+22 2 3
diff --git a/interactive/examples/aoc2023/run.sh b/interactive/examples/aoc2023/run.sh
new file mode 100755
index 000000000..ade1193b1
--- /dev/null
+++ b/interactive/examples/aoc2023/run.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+# Run every AoC 2023 program on the vec backend and check the answers.
+# Usage: ./run.sh [path/to/ddir] (build with: cargo build --release --example ddir)
+cd "$(dirname "$0")" || exit 1
+DDIR=${1:-../../../target/release/examples/ddir}
+python3 transcribe.py || exit 1 # dense dayNN/input.txt -> gen/dayNN/ fact files
+fail=0
+while read -r day part expected; do
+ case "$day" in ''|'#'*) continue;; esac
+ dir=day$day
+ inp=gen/$dir/input.txt
+ [ -f "gen/$dir/input$part.txt" ] && inp=gen/$dir/input$part.txt # day05/day15: per-part inputs
+ arity=$(head -1 "$inp" | awk '{print NF}')
+ got=$(EDGES_FILE=$inp "$DDIR" --backend=vec "$dir/part$part.ddp" "$arity" 10 0 1 0 2>&1 \
+ | sed -n "s/.*\\[part$part\\].*Int(\\(-\\{0,1\\}[0-9]*\\)).*/\\1/p")
+ if [ "$got" = "$expected" ]; then
+ echo "day$day part$part: ok ($got)"
+ else
+ echo "day$day part$part: FAIL (expected $expected, got '$got')"
+ fail=1
+ fi
+done < expected.txt
+exit $fail
diff --git a/interactive/examples/aoc2023/transcribe.py b/interactive/examples/aoc2023/transcribe.py
new file mode 100644
index 000000000..a0b3623bb
--- /dev/null
+++ b/interactive/examples/aoc2023/transcribe.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+"""Regenerate the i64 fact files the .ddp programs consume from the dense
+puzzle inputs committed as dayNN/input.txt (the compact text form of the
+Materialize sqllogictest oracles). Output goes to gen/dayNN/ (gitignored);
+run.sh invokes this before running the suite.
+
+Per-day modes: grids become "row col charcode" (or digit) cell facts, 0- or
+1-indexed to mirror each day's SQL; text lines become charcode or parsed
+numeric facts; numeric rows pass through; day05 and day15 split one dense
+input into the two per-part fact files. Passing --pad additionally writes
+day05's zero-padded uniform-arity copies (input1p.txt / input2p.txt) for
+the corgi backend, which crashes on mixed-arity inputs.
+"""
+import os, re, sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+
+
+def dense(day):
+ with open(os.path.join(HERE, day, 'input.txt')) as f:
+ return f.read().rstrip('\n')
+
+
+def emit(day, rows, name='input.txt'):
+ path = os.path.join(HERE, 'gen', day)
+ os.makedirs(path, exist_ok=True)
+ with open(os.path.join(path, name), 'w') as f:
+ for row in rows:
+ print(*row, file=f)
+
+
+def cells(text, base, cell=ord):
+ """Grid/text -> one fact per character: (row, col, cell(ch))."""
+ return [(r, c, cell(ch)) for r, line in enumerate(text.split('\n'), base)
+ for c, ch in enumerate(line, base)]
+
+
+def day02(text):
+ """'Game N: 1 red, 4 green; ...' -> (game, set, green, red, blue)."""
+ rows = []
+ for line in text.split('\n'):
+ m = re.match(r"Game (\d+): (.*)", line)
+ for k, s in enumerate(m.group(2).split(';')):
+ cnt = dict.fromkeys(('green', 'red', 'blue'), 0)
+ for n, color in re.findall(r"(\d+) (\w+)", s):
+ cnt[color] = int(n)
+ rows.append((m.group(1), k, cnt['green'], cnt['red'], cnt['blue']))
+ return rows
+
+
+def day04(text):
+ """'Card N: w.. | h..' -> (card, side, idx, value); side 0 = winning."""
+ rows = []
+ for line in text.split('\n'):
+ m = re.match(r"Card\s+(\d+): (.*)", line)
+ for side, nums in enumerate(m.group(2).split('|')):
+ rows += [(int(m.group(1)), side, i, v)
+ for i, v in enumerate(nums.split())]
+ return rows
+
+
+def day05(pad):
+ """Seeds + 7 maps -> input1: '0 seed' / '1 stage dst src len';
+ input2: seeds read as (start, len) pairs, same map entries."""
+ blocks = dense('day05').split('\n\n')
+ seeds = [int(x) for x in blocks[0].split(':')[1].split()]
+ entries = [(1, stage, *line.split())
+ for stage, b in enumerate(blocks[1:])
+ for line in b.strip().split('\n')
+ if line.strip() and not line.endswith('map:')]
+ parts = {
+ 'input1.txt': [(0, s) for s in seeds] + entries,
+ 'input2.txt': [(0, seeds[i], seeds[i + 1])
+ for i in range(0, len(seeds), 2)] + entries,
+ }
+ for name, rows in parts.items():
+ emit('day05', rows, name)
+ if pad:
+ arity = max(len(r) for r in rows)
+ emit('day05', [r + (0,) * (arity - len(r)) for r in rows],
+ name.replace('.txt', 'p.txt'))
+
+
+def day07(text):
+ """'XXXXX bid' -> (id, c1..c5, bid) with hand charcodes."""
+ return [(i, *(ord(c) for c in hand), bid)
+ for i, (hand, bid) in enumerate(ln.split() for ln in text.split('\n'))]
+
+
+def day13(text):
+ """Blank-line-separated grids -> (block, row, col, charcode), 1-indexed."""
+ return [(b, r, c, ord(ch)) for b, blk in enumerate(text.split('\n\n'), 1)
+ for r, line in enumerate(blk.split('\n'), 1)
+ for c, ch in enumerate(line, 1)]
+
+
+def day15():
+ """Comma-separated commands -> input1: (cmd, pos, charcode);
+ input2: (cmd, l1, l2, opchar, digitcode_or_0)."""
+ cmds = dense('day15').split(',')
+ emit('day15', [(r, i, ord(ch)) for r, cmd in enumerate(cmds, 1)
+ for i, ch in enumerate(cmd, 1)], 'input1.txt')
+ rows = []
+ for r, cmd in enumerate(cmds, 1):
+ if cmd.endswith('-'):
+ rows.append((r, ord(cmd[0]), ord(cmd[1]), ord('-'), 0))
+ else:
+ lab, foc = cmd.split('=')
+ rows.append((r, ord(lab[0]), ord(lab[1]), ord('='), ord(foc)))
+ emit('day15', rows, 'input2.txt')
+
+
+def day18(text):
+ """'D N (#xxxxxx)' -> (line, dircode, N, h1..h6) with hex charcodes."""
+ rows = []
+ for i, line in enumerate(text.split('\n'), 1):
+ m = re.match(r"([UDLR]) (\d+) \(#([0-9a-f]{6})\)", line)
+ rows.append((i, ord(m.group(1)), m.group(2), *(ord(c) for c in m.group(3))))
+ return rows
+
+
+def day19(text):
+ """Workflows + parts. State names -> ids (in=0, A=-1, R=-2, rest 3+);
+ rules: (1, state, prio, field, cmp, val, next); parts: (0, x, m, a, s, 0, 0)."""
+ wf_txt, parts_txt = text.split('\n\n')
+ ids = {'in': 0, 'A': -1, 'R': -2}
+ sid = lambda name: ids.setdefault(name, len(ids))
+ rows = []
+ for line in wf_txt.strip().split('\n'):
+ m = re.match(r"(\w+)\{(.*)\}", line)
+ st = sid(m.group(1))
+ for prio, rule in enumerate(m.group(2).split(','), 1):
+ mm = re.match(r"([xmas])([<>])(\d+):(\w+)", rule)
+ if mm:
+ rows.append((1, st, prio, 'xmas'.index(mm.group(1)),
+ 0 if mm.group(2) == '<' else 1,
+ int(mm.group(3)), sid(mm.group(4))))
+ else:
+ rows.append((1, st, prio, 0, 1, 0, sid(rule))) # always: x > 0
+ for line in parts_txt.strip().split('\n'):
+ x, m_, a, s = map(int, re.findall(r"=(\d+)", line))
+ rows.append((0, x, m_, a, s, 0, 0))
+ return rows
+
+
+def day22(text):
+ """'x,y,z~x,y,z' -> (line, x1, y1, z1, x2, y2, z2), 1-indexed."""
+ return [(i, *a.split(','), *b.split(','))
+ for i, (a, b) in enumerate((ln.split('~') for ln in text.split('\n')), 1)]
+
+
+MODES = {
+ 'day01': lambda t: cells(t, 0), # text lines, 0-indexed
+ 'day02': day02,
+ 'day03': lambda t: cells(t, 0), # grid, 0-indexed
+ 'day04': day04,
+ 'day06': lambda t: [ln.split() for ln in t.split('\n')], # numeric passthrough
+ 'day07': day07,
+ 'day10': lambda t: cells(t, 1), # grid, 1-indexed
+ 'day11': lambda t: cells(t, 1),
+ 'day13': day13,
+ 'day14': lambda t: cells(t, 1),
+ 'day16': lambda t: cells(t, 1),
+ 'day17': lambda t: cells(t, 1, cell=int), # digit grid
+ 'day18': day18,
+ 'day19': day19,
+ 'day22': day22,
+}
+
+if __name__ == '__main__':
+ days = [a for a in sys.argv[1:] if not a.startswith('--')]
+ for day in days or sorted(MODES) + ['day05', 'day15']:
+ if day == 'day05':
+ day05('--pad' in sys.argv[1:])
+ elif day == 'day15':
+ day15()
+ else:
+ emit(day, MODES[day](dense(day)))