❄️ Tinsel AoC DSL

Canonical Advent of Code Walkthroughs

This page walks through 6 representative Advent of Code challenges solved end-to-end in Tinsel, illustrating how Tinsel's primitives replace dozens of lines of boilerplate in general-purpose languages. At the bottom of the page is a complete index of all runnable scripts in examples/.


Walkthrough 1: Text & Pattern Ingestion

(AoC 2023 Day 1: Trebuchet?! & AoC 2024 Day 3: Mull It Over)

Problem Summary

  1. Trebuchet: Extract the first and last digit from each line of text, combine them into a two-digit number first * 10 + last, and sum across all lines.
  2. Mull It Over: Scan corrupted memory text for valid mul(X,Y) instructions while honoring do() (enable) and don't() (disable) gates.

Tinsel Solution

tinsel
// Part A: AoC 2023 Day 1 (Character arithmetic + negative indexing + fused sum)
let trebuchet_input = "1abc2\npqr3stu8vwx\na1b2c3d4e5f\ntreb7uchet"

let calibration_sum = (for line in trebuchet_input.lines() {
    let digits = for ch in line.chars() {
        if ch in '0'..='9' {
            yield ch - '0'
        }
    }
    if digits.len() > 0 {
        yield digits[0] * 10 + digits[-1]
    }
}).sum()

assert(calibration_sum == 142)

// Part B: AoC 2024 Day 3 (Split on do()/don't() + unanchored .scan())
let memory = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"

let part1 = memory.scan("mul({x},{y})").map(m -> m.x * m.y).sum()
let part2 = (for chunk in memory.split("do()") {
    let enabled_prefix = chunk.split("don't()")[0]
    yield enabled_prefix.scan("mul({x},{y})").map(m -> m.x * m.y).sum()
}).sum()

assert(part1 == 161 && part2 == 48)
Note

Why Tinsel Shines: Instead of compiling regular expressions (regexp.MustCompile) and maintaining stateful scanner loops, digits[-1] indexes the tail directly and .scan("mul({x},{y})") extracts and parses integer fields in one call.


Walkthrough 2: 2D Grid Neighborhoods & Wavefronts

(AoC 2025 Day 4: Printing Department & Day 7: Laboratories)

Problem Summary

  1. Printing Department: On a 2D grid of @ paper rolls, count how many @ cells have fewer than 4 adjacent @ neighbors in their 8-way Moore neighborhood.
  2. Laboratories: A tachyon beam enters at 'S' and moves downward row by row. When a beam hits a splitter '^', it splits into left (x - 1) and right (x + 1) beams and increments the split counter.

Tinsel Solution

tinsel
// 1. AoC 2025 Day 4: 8-way boundary-aware grid neighborhood in 6 lines
let warehouse = "..@@.@@@@.\n@@@.@.@.@@\n@@@@@.@.@@\n@.@@@@..@.\n@@.@@@@.@@\n.@@@@@@@.@\n.@.@.@.@@@\n@.@@@.@@@@\n.@@@@@@@@.\n@.@.@@@.@.".grid()

let accessible_count = warehouse.find_all('@').filter(pos -> {
    warehouse.adj8(pos).filter(nb -> warehouse[nb] == '@').len() < 4
}).len()

assert(accessible_count == 13)

// 2. AoC 2025 Day 7: Beam-splitting wavefront with .unique()
let lab = ".......S.......\n...............\n.......^.......\n...............\n......^.^......\n...............\n.....^.^.^.....".grid()
let start = lab.find('S')
let mut beams = [start.x]
let mut total_splits = 0

for y in (start.y + 1)..lab.height {
    let next_beams = []
    for x in beams {
        if x in 0..lab.width {
            if lab[vec2(x, y)] == '^' {
                total_splits += 1
                if x - 1 >= 0 { next_beams.push(x - 1) }
                if x + 1 < lab.width { next_beams.push(x + 1) }
            } else {
                next_beams.push(x)
            }
        }
    }
    beams = next_beams.unique()
}

assert(total_splits == 6)

Walkthrough 3: Interval Set Algebra at Scale

(AoC 2023 Day 5: Seed Fertilizer & AoC 2025 Day 5: Cafeteria)

Problem Summary

  1. Seed Fertilizer: Thread initial seed intervals through 7 sequential piecewise translation layers ([dest_start, src_start, length]) and find the minimum final location coordinate.
  2. Cafeteria: Normalize overlapping inclusive fresh-ingredient ranges (lo-hi) into a disjoint RangeSet and count how many candidate IDs fall inside any fresh interval.

Tinsel Solution

tinsel
from "std/intervals" import range_set

// 1. AoC 2023 Day 5: Multi-layer piecewise interval trisection & remap
let seeds = range_set([79..93, 55..68])
let layers = [
    [[50, 98, 2], [52, 50, 48]],
    [[0, 15, 37], [37, 52, 2], [39, 0, 15]],
    [[49, 53, 8], [0, 11, 42], [42, 0, 7], [57, 7, 4]],
    [[88, 18, 7], [18, 25, 70]],
    [[45, 77, 23], [81, 45, 19], [68, 64, 13]],
    [[0, 69, 1], [1, 0, 69]],
    [[60, 56, 37], [56, 93, 4]]
]

let final_locations = layers.fold(seeds, (rs, layer) -> rs.remap(layer))
assert(final_locations.min() == 46)

// 2. AoC 2025 Day 5: Sweep-line normalized interval membership
let cafeteria_input = "3-5\n10-14\n16-20\n12-18\n\n1\n5\n8\n11\n17\n32"
let [ranges_sec, ids_sec] = cafeteria_input.sections()

let fresh = range_set(for line in ranges_sec.lines() {
    let "{lo}-{hi}" = line
    yield lo..=hi
})
let fresh_count = ids_sec.ints().filter(id -> id in fresh).len()
assert(fresh_count == 3)

(AoC 2024 Day 16: Reindeer Maze)

Problem Summary

Navigate a reindeer from 'S' (facing East) to 'E' in a 2D maze where stepping forward costs 1 point and rotating 90^\circ left or right costs 1000 points.

Tinsel Solution

tinsel
import "std/grid" as grid
from "std/search" import dijkstra

let maze = "###############\n#.......#....E#\n#.#.###.#.###.#\n#.....#.#...#.#\n#.###.#####.#.#\n#.#.#.......#.#\n#.#.#####.###.#\n#...........#.#\n###.#.#####.#.#\n#...#.....#.#.#\n#.#.#.###.#.#.#\n#.....#...#.#.#\n#.###.#.#.#.#.#\n#S..#.....#...#\n###############".grid()

let start_pos = maze.find('S')
let end_pos = maze.find('E')

let result = dijkstra({
    start: (start_pos, 0), // (position, heading 0 = East)
    goal: ((pos, _)) -> pos == end_pos,
    step: ((pos, dir)) -> {
        let moves = [
            [(pos, grid.turn_right(dir)), 1000],
            [(pos, grid.turn_left(dir)), 1000]
        ]
        let next = grid.forward(pos, dir)
        if maze[next] != '#' {
            moves.push([(next, dir), 1])
        }
        moves
    }
})

assert(result.found && result.cost == 7036)

Walkthrough 5: 3D Union-Find & Bitmask BFS

(AoC 2025 Day 8: Playground & Day 10: Factory)

Problem Summary

  1. Playground: Given 3D junction box coordinates, sort all pairwise edges by squared Euclidean distance, union the 10 closest pairs using Disjoint-Set Union (union_find), and multiply the sizes of the 3 largest connected circuits.
  2. Factory: Given target indicator lights [.##.] and button wiring masks (3) (1,3) (2), find the minimum button presses to reach the target bitmask from 0 via bfs.

Tinsel Solution

tinsel
from "std/search" import union_find, bfs

// 1. AoC 2025 Day 8: 3D distance sorting + Union-Find
let boxes_raw = "162,817,812\n57,618,57\n906,360,560\n592,479,940\n352,342,300\n466,668,158\n542,29,236\n431,825,988\n739,650,466\n52,470,668\n216,146,977\n819,987,18\n117,168,530\n805,96,715\n346,949,466\n970,615,88\n941,993,340\n862,61,35\n984,92,344\n425,690,689"

let pts = boxes_raw.lines().map(l -> {
    let [x, y, z] = l.ints()
    vec3(x, y, z)
})

let pairs = []
for i in 0..pts.len() {
    for j in (i + 1)..pts.len() {
        let d = pts[i] - pts[j]
        pairs.push([d.x * d.x + d.y * d.y + d.z * d.z, i, j])
    }
}
let sorted_pairs = pairs.sort_by(p -> p[0])

let uf = union_find(0..pts.len())
for [_, u, v] in sorted_pairs[..10] {
    uf.union(u, v)
}

let groups = uf.groups()
let top3_prod = groups.keys().map(r -> groups[r].len()).sort_desc()[..3].product()
assert(top3_prod == 40)

// 2. AoC 2025 Day 10: Bitmask shortest path with bfs
let machine = "[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}"
let "[{diagram}] {rest}" = machine
let mut target_mask = 0
for [idx, ch] in diagram.chars().enumerate() {
    if ch == '#' { target_mask |= (1 << idx) }
}

let button_masks = for m in rest.scan("({wiring})") {
    let mut bmask = 0
    for bit in to_string(m.wiring).ints() {
        bmask |= (1 << bit)
    }
    yield bmask
}

let min_presses = bfs({
    start: 0,
    goal: target_mask,
    step: state -> button_masks.map(b -> state ^ b)
}).cost
assert(min_presses == 2)

Walkthrough 6: Memoized DAG Paths

(AoC 2025 Day 11: Reactor)

Problem Summary

Given a directed acyclic graph of reactor conduits (node: target1 target2 ...), count the total number of distinct paths from "you" to "out".

Tinsel Solution

tinsel
from "std/memo" import memoize

let reactor_input = "aaa: you hhh\nyou: bbb ccc\nbbb: ddd eee\nccc: ddd eee fff\nddd: ggg\neee: out\nfff: out\nggg: out\nhhh: ccc fff iii\niii: out"

let graph = {}
for line in reactor_input.lines() {
    let "{node}: {targets}" = line
    graph[node] = targets.words()
}

let mut count_paths = nil
count_paths = memoize(node -> {
    if node == "out" { return 1 }
    if node !in graph { return 0 }
    graph[node].map(next -> count_paths(next)).sum()
})

assert(count_paths("you") == 5)

Index of Runnable Repository Examples (examples/)

Every script in examples/ can be executed directly via ./bin/tinsel run <path> (or in batch mode via ./bin/tinsel examples/aoc2025/):

Script Path Algorithmic Focus
examples/language_tour.tin Interactive tour of structs, options, pattern matching, lazy iterators, and intervals
examples/dungeon_explorer.tin Procedural dungeon generator (Kruskal's MST + std/grid) and multi-objective bfs auto-explorer
examples/ascii_raymarcher.tin 3D Signed Distance Field (SDF) ASCII raymarcher with smooth CSG (smin), Phong lighting, and soft shadows
examples/symbolic_math.tin Symbolic calculus differentiator, nested match algebraic simplifier, and exact Newton-Raphson solver
examples/cellular_automata.tin Sparse Game of Life spaceship/oscillator classifier and std/orbit Wireworld logic circuit simulator
examples/stdlib_example.tin Overview of std/math, std/grid, std/search, and std/orbit
examples/multi_file_example.tin Multi-file module imports (examples/modules/grid_utils.tin & aoc_utils.tin)
examples/solve_with_file.tin Reading external input files via args[0] and read_file
examples/aoc2015_day4.tin MD5 hash mining (md5 intrinsic)
examples/aoc2015_day7.tin 16-bit bitwise logic gate circuit emulation with memoization
examples/aoc2020_day13.tin Chinese Remainder Theorem (math.crt) for bus schedules
examples/aoc2020_day17.tin 3D & 4D Conway Cubes cellular automata (std/vec adj_all)
examples/aoc2023_day1.tin Digit extraction and comprehension fusion
examples/aoc2023_day5.tin Multi-layer interval trisection and RangeSet.remap()
examples/aoc2023_day18.tin Polygon area via math.shoelace and math.picks_theorem
examples/aoc2024_day3.tin Unanchored corrupted text scanning (.scan("mul({x},{y})"))
examples/aoc2024_day16.tin Weighted Dijkstra maze search over (pos, dir) states
examples/aoc2025/day01.tin .. day12.tin Complete 12-day suite for Advent of Code 2025 (Part One)