❄️ Tinsel AoC DSL

Lesson 02: Collections, Slicing & Yield Comprehensions

In this lesson, we explore Tinsel's core collection types—arrays, tuples, and hash maps—along with range slicing, universal membership testing (in / !in), for ... in ... { yield } comprehensions, and lazy pull-based iterators (.iter()).


1. Learning Objectives

By the end of this lesson, you will be able to:

  • Index, slice (arr[a..b]), and mutate dynamic arrays, including negative tail indexing (arr[-1]).
  • Group composite states in tuples and store key-value data in hash maps.
  • Test membership across collections, strings, and ranges with in and !in.
  • Construct and filter arrays declaratively with for ... in ... { yield } comprehensions.
  • Chain eager array methods (.map(), .filter(), .sort_by()) and lazy short-circuiting .iter() streams.

2. Dynamic Arrays & Range Slicing

Arrays are ordered, dynamically sized sequences:

tinsel
let nums = [10, 20, 30, 40, 50]

// 0-based indexing and negative indexing from the end:
assert(nums[0] == 10)
assert(nums[-1] == 50)
assert(nums[-2] == 40)

Range Slicing (arr[start..end])

Both arrays and strings support direct slicing with half-open (..), inclusive (..=), open-start (..b), open-end (a..), and negative-bound (a..-1) ranges:

tinsel
let items = ["a", "b", "c", "d", "e"]
assert(items[1..4] == ["b", "c", "d"])
assert(items[..2] == ["a", "b"])
assert(items[3..] == ["d", "e"])
assert(items[1..-1] == ["b", "c", "d"])

let word = "advent"[1..=4]
assert(word == "dven")

In-Place Mutation & Concatenation

  • .push(val) appends an element in-place; .pop() removes and returns the last element.
  • arr1 + arr2 returns a new concatenated array.
tinsel
let stack = [1, 2]
stack.push(3)
assert(stack == [1, 2, 3])
let top = stack.pop()
assert(top == 3 && stack == [1, 2])
assert(stack + [4, 5] == [1, 2, 4, 5])

3. Fixed Tuples

Tuples are created with comma-separated expressions inside parentheses (a, b, c) (a 1-element tuple uses a trailing comma (x,)). They are ideal for coordinate pairs, graph search states, and returning multiple values from a function:

tinsel
let state = (vec2(3, 4), "NORTH", 100)
let (pos, heading, energy) = state

assert(pos == vec2(3, 4))
assert(heading == "NORTH" && energy == 100)

4. Hash Maps & Universal Structural Hashing

Maps associate keys with values. Unlike Go (where slices cannot be map keys) or Java/Python (where mutable lists cannot be hashed), Tinsel hashes arrays, tuples, vectors, and structs by value.

Map Literals & Key Syntax

  • Bare identifiers { x: 10, y: 20 } create string keys "x" and "y".
  • String, integer, float, and boolean literals can be used directly as keys ({ -1: "left", 1: "right" }).
  • Non-literal expression keys (variables, vectors, tuples, structs) are wrapped in brackets { [expr]: val }:
tinsel
let start = vec2(0, 0)
let visited = {
    label: "origin",
    [start]: 0,
    [(1, 2)]: 15
}

assert(visited.label == "origin")
assert(visited["label"] == "origin")
assert(visited[vec2(0, 0)] == 0)
assert(visited[(1, 2)] == 15)

Universal Membership (in and !in)

The in and !in operators work uniformly across maps, arrays, strings, ranges (0..10, 'a'..='z'), sets, grids, and range sets:

tinsel
let scores = { alice: 100, bob: 85 }
if "alice" in scores {
    scores.alice += 20
}
assert(scores.alice == 120 && "charlie" !in scores)
assert('v' in "advent" && 7 in 1..=10)

5. for ... in ... { yield } Comprehensions

In Tinsel, for loops are expressions. When a for loop body contains yield, it evaluates to a new array containing all yielded elements:

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

Nested Comprehensions

Each for ... yield expression produces an array for its own loop level; yielding an inner comprehension produces a 2D array (or call .flatten() to flatten into a 1D array):

tinsel
let pairs = (for r in 0..2 {
    yield for c in 0..3 {
        if r != c {
            yield (r, c)
        }
    }
}).flatten()
assert(pairs == [(0, 1), (0, 2), (1, 0), (1, 2)])

Zero-Allocation Comprehension Fusion

When a for ... { yield expr } comprehension is immediately reduced with .sum(), .product(), .min(), .max(), or .len(), the Tinsel bytecode optimizer fuses the loop and reduction into a single stack accumulator without allocating an intermediate array:

tinsel
let sum_of_cubes = (for x in 1..=10 {
    yield x * x * x
}).sum()
assert(sum_of_cubes == 3025)

6. Higher-Order Array Methods & Lazy .iter() Streams

Eager Array Methods

Arrays provide rich functional transformations out of the box:

tinsel
let nums = [5, 2, 8, 2, 9, 1]

assert(nums.unique() == [5, 2, 8, 9, 1])
assert(nums.sort() == [1, 2, 2, 5, 8, 9])
assert(nums.sort_desc() == [9, 8, 5, 2, 2, 1])
assert(nums.filter(x -> x >= 5).map(x -> x * 10) == [50, 80, 90])
assert([1, 2, 3, 4].fold(0, (acc, x) -> acc + x) == 10)

let words = ["pear", "fig", "banana", "apple"]
assert(words.sort_by(w -> w.len()) == ["fig", "pear", "apple", "banana"])

let matrix = [[1, 2, 3], [4, 5, 6]]
assert(matrix.transpose() == [[1, 4], [2, 5], [3, 6]])

Lazy Pull-Based Iterators (.iter())

When searching across huge ranges (0..1_000_000_000) where you only need the first few matches, call .iter() (std/iter) to stream values on demand and short-circuit immediately with .take(n), .find(pred), .any(pred), or .all(pred):

tinsel
let first_match = (100..1_000_000_000)
    .iter()
    .find(x -> x % 37 == 0 && x % 41 == 0)

assert(first_match == 1517)

7. Common Pitfalls & Gotchas

Warning

Gotcha 1: Bare Identifier vs. Variable Key in Map Literals In { k: 10 }, k is a literal string key "k". If k is a variable holding a key value, wrap it in brackets: { [k]: 10 }.

Warning

Gotcha 2: .sort() returns a new sorted array Calling arr.sort() or arr.reverse() returns a new array and does not mutate arr in-place. Assign the result (let sorted = arr.sort()).


8. Hands-On Exercises

Exercise 2.1: Calibration Sum (Warmup)

Given an array of digit rows [[1, 2], [3, 8], [1, 2, 3, 4, 5], [7]], use a for ... yield comprehension and negative indexing (r[-1]) to form first * 10 + last for each row and compute their sum.

Solution & Explanation
tinsel
let rows = [[1, 2], [3, 8], [1, 2, 3, 4, 5], [7]]

let total = (for r in rows {
    yield r[0] * 10 + r[-1]
}).sum()

assert(total == 142)
println("Calibration sum:", total)

Exercise 2.2: Character Frequency Map (Practice)

Count the frequency of each character in "abracadabra" using a map and verify how many times 'a' and 'r' appear.

Solution & Explanation
tinsel
let counts = {}
for ch in "abracadabra".chars() {
    counts[ch] = counts.get(ch, 0) + 1
}

assert(counts['a'] == 5 && counts['r'] == 2)
println("Count of 'a':", counts['a'])

Exercise 2.3: Lazy Pythagorean Triples (Challenge)

Use a comprehension over a in 1..=20 and b in a..=20 to yield all triples (a, b, c) where a^2 + b^2 = c^2 for an integer c \le 25.

Solution & Explanation
tinsel
let triples = (for a in 1..=20 {
    yield for b in a..=20 {
        let c2 = a * a + b * b
        let c = round(sqrt(c2))
        if c <= 25 && c * c == c2 {
            yield (a, b, c)
        }
    }
}).flatten()

assert(triples.len() == 7)
assert((3, 4, 5) in triples && (5, 12, 13) in triples)
println("Triples:", triples)

Next Steps

In Lesson 03: Functions, Closures & Pipelines, we will explore named functions, parameter destructuring, lexical closures, and the |> pipeline operator.