❄️ Tinsel AoC DSL

15-Minute Language Tour

This page gives a complete, high-level tour of Tinsel—from basic syntax and expression-oriented control flow to custom structs, pattern matching, spatial grids, interval algebra, and declarative state-space search.

Tip

Every snippet on this page is runnable! You can also run examples/language_tour.tin via ./bin/tinsel run examples/language_tour.tin.


1. Variables, Mutability & Expression Blocks

Variables declared with let (as well as function parameters and for loop variables) are immutable by default. Use let mut whenever a variable needs to be reassigned with = or compound assignment (+=, -=, *=, etc.):

tinsel
let base = 100
let mut score = base
score += 25
assert(score == 125)

// Simultaneous tuple reassignment (ideal for swaps and Fibonacci state updates)
let mut a = 1
let mut b = 2
(a, b) = (b, a + b)
assert(a == 2 && b == 3)

In Tinsel, almost every construct—blocks { ... }, if/else chains, match arms, and for ... yield loops—is an expression that evaluates to a value:

tinsel
let delta = {
    let step = 6
    if step % 2 == 0 { step * 10 } else { step }
}
assert(delta == 60)

2. Primitives, Automatic bigint & String Interpolation

Tinsel provides 64-bit signed integers (int), arbitrary-precision integers (bigint), 64-bit floats (float), Unicode runes (char), booleans (bool), nil, and UTF-8 strings (string).

  • Automatic bigint Promotion: Integer literals and 64-bit int arithmetic (+, -, *) that overflow signed 64-bit bounds automatically promote to bigint without silent wrapping.
  • Character Arithmetic: '7' - '0' yields 7, and 'a' + 2 yields 'c'.
  • Interpolated Strings (f"..."): Embed arbitrary expressions inside {...}:
tinsel
let mask = 0b1010_1100
let digit = '9' - '0'
let max_i64 = 9223372036854775807
let huge = max_i64 + 1 // Automatically promotes to bigint!

let msg = f"digit={digit}, hex=0x{mask.hex()}, huge={huge}"
assert(msg == "digit=9, hex=0xac, huge=9223372036854775808")

3. Collections, Slicing & Universal Membership (in / !in)

Tinsel includes dynamic arrays, fixed tuples, and hash maps with structural equality and hashing (arrays, tuples, vectors, and structs can all be used directly as map keys!).

tinsel
let nums = [10, 20, 30, 40, 50]
assert(nums[-1] == 50)            // Negative indexing from the end
assert(nums[1..4] == [20, 30, 40]) // Half-open range slice
assert(nums[1..-1] == [20, 30, 40])
assert(30 in nums && 99 !in nums) // Universal membership operators

// Map literals support bare identifier keys (strings), literals, or [expr] keys:
let visited = {
    origin: true,
    [vec2(3, 4)]: true
}
assert( visited.origin && vec2(3, 4) in visited )

4. for ... yield Comprehensions & Lazy .iter() Streams

A for loop containing yield evaluates directly to a new array of yielded items. When immediately followed by .sum(), .product(), .min(), .max(), or .len(), the compiler fuses the comprehension into a zero-allocation scalar accumulator loop:

tinsel
let squares_of_evens = for x in 1..=6 {
    if x % 2 == 0 {
        yield x * x
    }
}
assert(squares_of_evens == [4, 16, 36])

// Fused zero-allocation sum comprehension:
let mult_sum = (for x in 1..100 {
    if x % 3 == 0 || x % 5 == 0 { yield x }
}).sum()
assert(mult_sum == 2318)

For massive or infinite sequences that need early termination, .iter() creates a lazy pull-based Iter stream (std/iter):

tinsel
let first_three = (1..1_000_000_000)
    .iter()
    .filter(x -> x % 17 == 0)
    .map(x -> x * x)
    .take(3)
    .collect()
assert(first_three == [289, 1156, 2601])

5. Functions, Closures & Forward Pipelines (|>)

Functions are declared with fn and support parameter pattern destructuring and default values. Anonymous lambdas use ->. The forward pipeline operator |> passes the left-hand value into the right-hand call (as the first argument, or at the _ placeholder):

tinsel
from "std/array" import filter, map as arr_map

fn manhattan((x1, y1), (x2, y2) = (0, 0)) {
    abs(x1 - x2) + abs(y1 - y2)
}
assert(manhattan((3, -4)) == 7)

let result = "10 -5 20 8 -2"
    |> parse_ints
    |> filter(x -> x > 0)
    |> arr_map(x -> x * 2)
    |> sum
assert(result == 76)

6. User-Defined struct, Option & Pattern Matching (match)

Define custom data types with struct. Struct instances automatically gain value-based equality (==), zero-sort hash keys (usable in maps and sets), to_string formatting, and pattern matching:

tinsel
struct Beacon(id, pos, active = true) {
    fn dist(self, other) {
        self.pos <-> other.pos
    }
}

let b1 = Beacon(1, vec2(0, 0))
let b2 = Beacon(2, vec2(3, 4))
assert(b1.dist(b2) == 7)
assert(b1 == Beacon(1, vec2(0, 0), true))

Tinsel provides first-class Option values (Some(v) and None) with postfix ? (returns true if Some) and ! (unwraps Some(v)), alongside rich match expressions:

tinsel
struct Beacon(id, pos, active = true)

fn classify(item) {
    match item {
        Some(Beacon(id, pos, true)) if pos <-> vec2(0, 0) <= 10 -> f"active #{id}",
        Some(Beacon(id, _, false)) -> f"offline #{id}",
        None -> "empty",
        _ -> "far"
    }
}

let reading = Some(Beacon(2, vec2(4, 3)))
assert(reading? && reading!.id == 2)
assert(classify(reading) == "active #2")

7. Regex-Free Input Ingestion

Puzzle inputs rarely require regular expressions in Tinsel:

  1. String Template Patterns: Bind variables directly from structured lines (int values are parsed automatically).
  2. Unanchored .scan(template): Extract all matching substrings from noisy text into an array of maps.
  3. Fast Byte-Scanned .ints(): Extract all signed integers from any string.
tinsel
// 1. Full-line template destructuring
let line = "Sensor at x=50, y=-100: closest beacon is at x=10, y=20"
let "Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}" = line
assert(vec2(sx, sy) <-> vec2(bx, by) == 160)

// 2. Unanchored pattern scanning across corrupted text
let noisy = "xmul(2,4)%&mul[3,7]!@^mul(5,5)+mul(11,8)"
let total = noisy.scan("mul({a},{b})").map(m -> m.a * m.b).sum()
assert(total == 121)

8. Spatial Geometry (vec2, vec3, <->, grid)

2D and 3D integer vectors (vec2, vec3), the infix Manhattan distance operator (<->), and flat row-major character grids (std/grid) make spatial puzzles effortless:

tinsel
from "std/grid" import parse_grid

let g = parse_grid("#####\n#S..#\n#.###\n#..E#\n#####")
let start = g.find('S')
let goal = g.find('E')

// g.adj4(pos) automatically filters out-of-bounds coordinates
let open_moves = g.adj4(start).filter(p -> g[p] != '#')
assert(open_moves == [vec2(2, 1), vec2(1, 2)])
assert((start <-> goal) == 4)
assert(g.width == 5 && g.height == 5)

9. Interval Algebra (RangeSet) & Orbit Cycle Fast-Forwarding

When Part 2 scales coordinates to 10^{10} or iterations to 10^{12}, Tinsel's standard library solves them without brute-force loops:

tinsel
from "std/intervals" import range_set
from "std/orbit" import orbit

// 1. Disjoint sweep-line RangeSet & piecewise interval trisection remap
let seeds = range_set([79..93, 55..68])
let remapped = seeds.remap([[50, 98, 2], [52, 50, 48]])
assert(remapped.len() == 27 && remapped.min() == 57)

// 2. Periodicity detection & O(1) modulo fast-forwarding over 1 trillion steps
let state_1t = orbit({
    initial: 5,
    steps: 1_000_000_000_000,
    step: x -> (x * 3 + 1) % 100
})
assert(state_1t == 5)

10. Declarative State-Space Search & Memoization

std/search provides bfs, dfs, dijkstra, astar, toposort, and union_find, while std/memo provides automatic multi-argument memoize:

tinsel
from "std/search" import dijkstra, union_find
from "std/memo" import memoize

// Weighted shortest path from 0 to 10
let path_res = dijkstra({
    start: 0,
    goal: 10,
    step: x -> [[x + 1, 2], [x + 3, 5]]
})
assert(path_res.found && path_res.cost == 17)

// Disjoint-Set Union (Union-Find)
let uf = union_find(["A", "B", "C"])
uf.union("A", "B")
assert(uf.connected("A", "B") && uf.count() == 2)

// Top-down DP memoization
let mut ways = nil
ways = memoize(n -> if n <= 1 { 1 } else { ways(n - 1) + ways(n - 2) })
assert(ways(20) == 10946)

11. Why Tinsel? Eliminating the Puzzle Boilerplate Tax

In general-purpose languages like Go, Java, or Rust, solving algorithmic puzzles often requires writing 50–100 lines of plumbing (priority queue interfaces, coordinate bounds checks, composite state hashing, regex submatch parsing, and manual interval splitting) before expressing the actual algorithm.

Capability Typical General-Purpose Boilerplate Tinsel Primitive
Input Parsing regexp.MustCompile, strings.Split, strconv.Atoi loops let "x={x}, y={y}" = s, .ints(), .scan()
2D Grids & Bounds Nested [][]byte slices, manual dx/dy arrays, nx >= 0 && nx < w parse_grid, g[pos], pos in g, g.adj4(pos)
Manhattan Distance abs(x1 - x2) + abs(y1 - y2) helper functions Infix operator: p1 <-> p2
Composite Map Keys String serialization hacks ("%d,%d") or custom equals/hashCode Universal structural hashing on tuples, arrays, vectors & structs
Shortest Path Search 40+ lines of PriorityQueue / heap.Interface + visited maps dijkstra({ start, goal, step }) / astar / bfs
Interval Translation 70+ lines of sweep-line sorting and 3-way segment splitting range_set([...]).remap(rules)
10^{12}-Step Cycles Manual history map, index tracking, and offset modulo math orbit({ initial, steps, step })
Integer Overflow Silent 64-bit wrap-around or verbose BigInteger method chains Automatic bigint promotion on 64-bit overflow

Next Steps