❄️ Tinsel AoC DSL

Lesson 05: Input Ingestion, Template Patterns & File I/O

In competitive programming, up to half of your coding time can easily be lost to input parsing. In this lesson, you will learn how Tinsel eliminates regular expressions and manual token splitting using string template patterns, unanchored .scan(), high-speed integer scanning (.ints()), section splitting (.sections()), and file I/O.


1. Learning Objectives

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

  • Extract typed variables directly from formatted strings with let "template" = line and match.
  • Scan noisy or corrupted text for embedded patterns using .scan("template").
  • Extract all signed integers from arbitrary strings in a single call with .ints().
  • Split multi-part puzzle inputs by blank lines with .sections() and read external files via read_file, read_lines, read_sections, and args.

2. Full-Line String Template Destructuring

Consider a typical Advent of Code input line:

text
Sensor at x=238914, y=-390123: closest beacon is at x=201923, y=401293

In Tinsel, you write the literal string format with {var} placeholders directly on the left-hand side of a let binding or inside a match arm:

tinsel
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

// Numeric fields are automatically parsed into 64-bit integers!
assert(sx == 50 && sy == -100)
assert(vec2(sx, sy) <-> vec2(bx, by) == 160)

Multi-Format Dispatch with match

When input lines can take several different formats (such as CPU instructions or directional commands), combine string template patterns with match:

tinsel
fn exec_cmd(pos, line) {
    match line {
        "forward {n}" | "fd {n}" -> pos + vec2(n, 0),
        "down {n}"               -> pos + vec2(0, n),
        "up {n}"                 -> pos + vec2(0, -n),
        "teleport ({x}, {y})"    -> vec2(x, y),
        _                        -> pos
    }
}

let final_pos = ["forward 5", "down 3", "fd 2", "up 1"].fold(vec2(0, 0), exec_cmd)
assert(final_pos == vec2(7, 2))

3. High-Speed Signed Integer Extraction (.ints())

When you only care about the numbers in a line regardless of surrounding words or punctuation, use .ints() (or global parse_ints(s)):

tinsel
let text = "Blueprint 3: Each ore robot costs 4 ore. Clay costs -2 offset and 14 ore."
let nums = text.ints()
assert(nums == [3, 4, -2, 14])

// Destructure directly into variables:
let [id, ore_cost, offset, clay_cost] = text.ints()
assert(id == 3 && clay_cost == 14)

.ints() uses a zero-allocation Go byte scanner under the hood, extracting millions of signed integers per second without regular expression compilation.


4. Unanchored Substring Scanning (.scan())

While let "template" = line matches the entire string from start to end, some puzzles hide valid tokens inside corrupted text (such as AoC 2024 Day 3: xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)).

Use text.scan(template) (or scan_pattern(text, template)) to find all non-overlapping substrings matching template. Each match is returned as a map containing the captured {var} values (auto-parsed as int when numeric) plus _match (the raw matched substring):

tinsel
let corrupted = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
let matches = corrupted.scan("mul({x},{y})")

let total = matches.map(m -> m.x * m.y).sum()
assert(matches.len() == 4)
assert(matches[0]._match == "mul(2,4)")
assert(total == 161)

5. Multi-Section Inputs, Lines & Words

Many puzzles divide their input into two or more sections separated by a blank line (\n\n), such as a 2D map followed by movement instructions, or ordering rules followed by update lists:

  • s.sections() (or .paragraphs(), .blocks()): Splits by one or more blank lines (\n\n or \r\n\r\n).
  • s.lines(): Splits by line breaks, stripping \r.
  • s.words(): Splits by any whitespace run (spaces, tabs, newlines).
tinsel
let raw_puzzle = "10-20\n30-40\n\n15\n25\n35"
let [ranges_block, ids_block] = raw_puzzle.sections()

let ranges = for line in ranges_block.lines() {
    let "{lo}-{hi}" = line
    yield lo..=hi
}
let ids = ids_block.ints()

assert(ranges.len() == 2 && ids == [15, 25, 35])

6. File I/O, CLI args & Cryptographic Digests

Reading & Writing Files

Tinsel provides both global functions and string path methods for file access:

  • read_file(path) or path.read_file() -> string
  • read_lines(path) or path.read_lines() -> [string]
  • read_sections(path) or path.read_sections() -> [string]
  • write_file(path, content) -> bool, append_file(path, content) -> bool, file_exists(path) -> bool

Parameterizing Scripts with args

Command-line arguments passed after the .tin script name are available in the global args array. Idiomatic Tinsel scripts support an optional args[0] file path with an inline fallback sample:

tinsel
let input = if args.len() > 0 && file_exists(args[0]) {
    read_file(args[0])
} else {
    "10 20 30\n40 50 60"
}
assert(input.ints().sum() == 210)

Cryptographic Hashing (md5 & sha256)

For hash-mining puzzles (such as AoC 2015 Day 4 or 2016 Day 5), md5 and sha256 are built in as both global functions and string methods:

tinsel
let digest = "abcdef609043".md5()
assert(digest.starts_with("000001dbb"))
assert("tinsel".sha256().len() == 64)

7. Common Pitfalls & Gotchas

Warning

Gotcha 1: .ints() parses - as a negative sign Calling "10-20".ints() returns [10, -20] because -20 is scanned as a signed integer! When - is a range separator between positive numbers, use a string template (let "{lo}-{hi}" = s) or s.split("-").

Warning

Gotcha 2: Exact delimiter matching in let "template" = s Full-line template destructuring requires literal delimiters (including spaces and punctuation) to match accurately. When whitespace between columns is variable (e.g., "Card 1: 41 48"), use .split(":") combined with .ints() or .words().


8. Hands-On Exercises

Exercise 5.1: Inclusive Range Parser (Warmup)

Given "12-45, 50-80, 100-120", split by ", ", destructure each " {lo}-{hi} " range with a string template, and sum the inclusive span lengths (hi - lo + 1).

Solution & Explanation
tinsel
let input = "12-45, 50-80, 100-120"

let total_span = (for part in input.split(", ") {
    let "{lo}-{hi}" = part
    yield (lo..=hi).len()
}).sum()

assert(total_span == 34 + 31 + 21)
println("Total span:", total_span)

Exercise 5.2: Corrupted Conditional Multiplier (Practice)

Scan "mul(2,4)don't()mul(5,5)do()mul(11,8)" by splitting on "do()" and ignoring everything after "don't()" in each segment, then summing x * y for all valid mul({x},{y}) calls.

Solution & Explanation
tinsel
let input = "mul(2,4)don't()mul(5,5)do()mul(11,8)"

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

assert(enabled_sum == 2 * 4 + 11 * 8)
println("Enabled sum:", enabled_sum)

Exercise 5.3: Two-Section Crate Rearrangement (Challenge)

Given an input with initial named stacks and move instructions separated by a blank line, parse both sections and simulate moving count items from src to dst.

Solution & Explanation
tinsel
let raw = "A: Z N\nB: M C D\nC: P\n\nmove 1 from B to A\nmove 3 from A to C"
let [stacks_sec, moves_sec] = raw.sections()

let stacks = {}
for line in stacks_sec.lines() {
    let "{name}: {items}" = line
    stacks[name] = items.words()
}

for line in moves_sec.lines() {
    let "move {n} from {src} to {dst}" = line
    for _ in 0..n {
        let val = stacks[src].pop()
        stacks[dst].push(val)
    }
}

assert(stacks["C"] == ["P", "D", "N", "Z"])
println("Stack C:", stacks["C"])

Next Steps

In Lesson 06: Spatial Vectors & 2D Grids, we will explore Tinsel's 2D/3D vector types, the <-> Manhattan distance operator, and the contiguous 2D character grid engine.