❄️ Tinsel AoC DSL

Lesson 01: Variables, Expressions & Control Flow

Welcome to the first lesson of the Tinsel Step-by-Step Tutorial. In this lesson, you will learn how Tinsel handles variables, mutability, scalar types, expression-oriented blocks, conditionals, and iterative loops.


1. Learning Objectives

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

  • Declare immutable (let) and mutable (let mut) variables and perform simultaneous tuple swaps.
  • Work with integers, automatic arbitrary-precision bigint promotion, floats, characters, and interpolated f"..." strings.
  • Use { ... } blocks and if/else chains as value-producing expressions.
  • Write iterative loops using while, break, and continue.

2. Variables & Immutability by Default

In algorithmic puzzles, accidental state mutation inside helper functions or loops is a common source of subtle bugs. Tinsel bindings are immutable by default.

Immutable Bindings (let)

A variable bound with let cannot be reassigned:

tinsel
let target_year = 2025
let base_offset = 100
assert(target_year + base_offset == 2125)
// target_year = 2026 // Compile Error: cannot assign to immutable variable "target_year"

Mutable Bindings (let mut), Compound Assignment & Swaps

When you intend to update a variable over time, declare it with let mut. Mutable variables support standard reassignment (=), compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=), and simultaneous tuple reassignment:

tinsel
let mut counter = 0
counter += 5
counter *= 4
assert(counter == 20)

// Simultaneous tuple reassignment (evaluates RHS before assigning LHS):
let mut a = 10
let mut b = 99
(a, b) = (b, a)
assert(a == 99 && b == 10)

Variable Shadowing

You can re-declare a variable with let in the same or an inner scope to transform values step by step without making the binding mutable:

tinsel
let raw = "  42  "
let raw = raw.trim()
let raw = raw.to_int()
assert(raw * 2 == 84)

3. Scalar Types & String Interpolation

Integers (int), Bitwise Operations & Automatic bigint

Standard integers are signed 64-bit values. Integer division truncates toward zero, and literals can be written in decimal, hexadecimal (0x), octal (0o), or binary (0b) with optional _ separators:

tinsel
let quotient = 17 / 5   // 3
let remainder = 17 % 5  // 2
let mask = 0xFF
let flags = 0b1011_0001
let shifted = (flags & 0x0F) << 4

assert(quotient == 3 && remainder == 2)
assert(mask == 255 && shifted == 16)
assert(255.hex(4) == "00ff" && 5.bin(4) == "0101")
Note

Automatic bigint Promotion: Whenever an integer literal or an arithmetic operation (+, -, *) on 64-bit int values overflows the signed 64-bit range (> 2^{63}-1 or < -2^{63}), Tinsel automatically promotes the result to an arbitrary-precision bigint without wrapping:

tinsel
let max_i64 = 9223372036854775807
let promoted = max_i64 + 1
assert(type_of(promoted) == "bigint")
assert(promoted - 1 == max_i64)

Floats (float), Characters (char) & Booleans (bool)

  • Floats: 64-bit IEEE-754 floating-point numbers (3.14, 1e9, 2.5e-4).
  • Characters: Single-quoted Unicode runes ('A', '\n', '\u03b1'). Characters support direct offset arithmetic ('7' - '0' == 7, 'a' + 3 == 'd').
  • Booleans & Truthiness: true and false with short-circuiting && (AND) and || (OR). In conditions, nil, false, 0, 0.0, "", [], and None are falsy; all other values are truthy.
tinsel
let digit_char = '8'
let digit_val = digit_char - '0'
let next_letter = 'a' + 2
assert(digit_val == 8 && next_letter == 'c')

String Interpolation (f"...")

Prefix a string literal with f to evaluate arbitrary Tinsel expressions inside {...} (escape literal braces with \{ and \}):

tinsel
let part1 = 142
let part2 = 281
let summary = f"Part 1: {part1}, Part 2: {part2}, Sum: {part1 + part2}"
assert(summary == "Part 1: 142, Part 2: 281, Sum: 423")

4. Expression-Oriented Blocks & Conditionals

In Tinsel, { ... } blocks and if/else constructs are expressions. The value of a block is its final trailing expression:

tinsel
let bonus = {
    let base = 20
    let multiplier = 3
    base * multiplier + 5
}
assert(bonus == 65)

Because if ... else ... evaluates to a value, you can bind the result of a conditional directly to an immutable let variable:

tinsel
let score = 85
let rank = if score >= 90 {
    "Gold"
} else if score >= 75 {
    "Silver"
} else {
    "Bronze"
}
assert(rank == "Silver")

5. Iterative Control Flow: while, break & continue

Use while when iterating until a dynamic condition is met:

tinsel
let mut i = 1
let mut total = 0

while i <= 10 {
    if i == 5 {
        i += 1
        continue // Skip 5
    }
    if i > 8 {
        break // Stop after 8
    }
    total += i
    i += 1
}

// 1 + 2 + 3 + 4 + 6 + 7 + 8 = 31
assert(total == 31)

6. Common Pitfalls & Gotchas

Warning

Gotcha 1: Reassigning an immutable let binding Function parameters, for loop variables, and plain let variables are immutable. If you need to update a counter or accumulator in a loop, always declare it with let mut.

Warning

Gotcha 2: Negative modulo remainders Standard % in Tinsel (as in Go and C) preserves the sign of the dividend (-3 % 10 == -3). When wrapping circular dials or grid coordinates that may go negative, use ((a % m) + m) % m or math.euclid_mod(a, m) from std/math.


7. Hands-On Exercises

Exercise 1.1: Expression-Based FizzBuzz (Warmup)

Compute the FizzBuzz labels for 1 through 15 using an if/else expression and count how many numbers are divisible by 3 or 5.

Solution & Explanation
tinsel
let mut n = 1
let mut hit_count = 0

while n <= 15 {
    let label = if n % 15 == 0 {
        "FizzBuzz"
    } else if n % 3 == 0 {
        "Fizz"
    } else if n % 5 == 0 {
        "Buzz"
    } else {
        to_string(n)
    }
    if label != to_string(n) {
        hit_count += 1
    }
    n += 1
}

assert(hit_count == 7)
println("Fizz/Buzz hits in 1..15:", hit_count)

Exercise 1.2: Collatz Stopping Time (Practice)

Starting at N = 27, apply N \to N / 2 if even and N \to 3N + 1 if odd until N == 1. Count the total number of steps required.

Solution & Explanation
tinsel
let mut n = 27
let mut steps = 0

while n != 1 {
    n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 }
    steps += 1
}

assert(steps == 111)
println("Steps for 27 to reach 1:", steps)

Exercise 1.3: Digital Root Calculator (Challenge)

The digital root of a non-negative integer is found by repeatedly summing its base-10 digits until a single digit (< 10) remains (e.g., 9875 \to 29 \to 11 \to 2). Compute the digital root of 9875.

Solution & Explanation
tinsel
let mut n = 9875

while n >= 10 {
    let mut digit_sum = 0
    let mut temp = n
    while temp > 0 {
        digit_sum += temp % 10
        temp /= 10
    }
    n = digit_sum
}

assert(n == 2)
println("Digital root of 9875:", n)

Next Steps

In Lesson 02: Collections, Slicing & Yield Comprehensions, you will learn how to manipulate arrays, tuples, and hash maps, and how to transform sequences with for ... in ... { yield } comprehensions and lazy .iter() streams.