❄️ Tinsel AoC DSL

Syntax, Grammar & Type System Reference

This page is the authoritative specification for Tinsel's lexical grammar, operator precedence hierarchy, runtime type system, pattern matching, user-defined struct declarations, prototype metamethod hooks (__proto__), module system, and formal EBNF grammar.


1. Lexical Specification

Whitespace, Newlines & Comments

  • Whitespace: Spaces (\x20) and tabs (\t) separate tokens and are otherwise ignored.
  • Line Terminators: Newlines (\n, \r\n) and semicolons (;) terminate statements unless an expression is syntactically incomplete (such as after an infix operator or inside unclosed parentheses (...), brackets [...], or braces {...}).
  • Single-Line Comments: Begin with // and continue to the end of the line.
  • Multi-Line Comments: Enclosed in /* ... */ and may span multiple lines.

Identifiers

Identifiers start with an ASCII letter or underscore, followed by any number of ASCII letters, digits, or underscores ([a-zA-Z_][a-zA-Z0-9_]*).

Note

Module Privacy Convention (_): Top-level identifiers beginning with an underscore (e.g. _helper, _cache) are private to the declaring module and are excluded from its exported symbol table.

Literals

  • Integer Literals (int / bigint): Decimal, hexadecimal (0x / 0X), octal (0o / 0O), and binary (0b / 0B) literals supporting optional _ digit separators. Literals that exceed signed 64-bit bounds (-9223372036854775808 to 9223372036854775807) automatically promote to arbitrary-precision bigint:
    tinsel
    let dec = 1_000_000
    let hex_mask = 0xDEAD_BEEF
    let oct_perm = 0o755
    let bin_flags = 0b1011_0001
    let huge = 999999999999999999999999999999
    assert(type_of(dec) == "int" && type_of(huge) == "bigint")
    
  • Floating-Point Literals (float): IEEE-754 double-precision numbers in standard decimal or scientific notation:
    tinsel
    let pi = 3.141592653589793
    let eps = 2.5e-4
    let billion = 1e9
    
  • Character Literals (char): Single Unicode code points (runes) enclosed in single quotes '...', supporting \n, \r, \t, \0, \\, \', \xNN, \uNNNN, and \U000NNNNN escapes:
    tinsel
    let ch = 'S'
    let alpha = '\u03b1'
    assert(('7' - '0') == 7 && ('a' + 2) == 'c')
    
  • String Literals (string): Immutable UTF-8 strings enclosed in double quotes "..." supporting standard and Unicode escape sequences.
  • Interpolated Strings (f"..."): Strings prefixed with f evaluate arbitrary Tinsel expressions inside {expr} and splice their formatted string representations (escape literal braces with \{ and \}):
    tinsel
    let x = 10
    let y = 25
    let msg = f"Point({x}, {y}) has sum {x + y}"
    assert(msg == "Point(10, 25) has sum 35")
    
  • Template Pattern Strings: String literals containing {ident} placeholders in pattern positions (let "..." = s or match s { "..." -> ... }) destructure formatted strings and automatically parse integer substrings into int:
    tinsel
    let "Sensor at x={sx}, y={sy}" = "Sensor at x=15, y=-40"
    assert(sx == 15 && sy == -40)
    

Keywords

Category Keywords
Declarations let, mut, fn, struct
Control Flow if, else, match, for, in, yield, while, break, continue, return
Modules import, from, as, export
Literals true, false, nil
Note

Contextual keywords (in, from, as) are permitted as struct field names, map property names, and method names.


2. Bindings & Immutability by Default (let vs. let mut)

Variables declared with let, for loop iteration variables, and function parameters are immutable by default. Attempting to reassign an immutable binding with = or a compound assignment operator (+=, -=, etc.) produces a compile-time error.

To allow reassigning a variable (x = new_val, x += 1, or simultaneous tuple assignment (a, b) = (b, a)), declare it with let mut:

tinsel
let base = 10
let mut acc = base
acc += 5

let mut a = 0
let mut b = 1
(a, b) = (b, a + b)
assert(acc == 15 && a == 1 && b == 1)
Tip

In-Place Container Mutation vs. Variable Rebinding: Mutating the contents of a collection or struct field in-place (arr.push(x), arr[0] = 99, m["k"] = 1, g[pos] = '#') modifies the underlying heap object and does not require let mut on the variable holding the container reference.

Optional Type Annotations

let declarations accept optional type annotations (let count: int = 0, let grid_pts: [(int, int)] = []) for documentation clarity. Type annotations are checked syntactically by the parser and erased at compile time.


3. Pratt Operator Precedence Hierarchy

Tinsel uses a top-down Pratt parser (internal/parser/parser.go). Operators are listed below from lowest binding power (0) to highest (140):

Precedence Operator / Token Associativity Description
0 (Lowest) ;, \n, }, ], ) None Statement / block delimiter
10 -> Right (11, 10) Lambda expression: params -> body
20 |> Left (20, 21) Forward pipeline: x |> f \equiv f(x), x |> f(a) \equiv f(x, a)
30 || Left (30, 31) Short-circuit logical OR
40 && Left (40, 41) Short-circuit logical AND
50 ==, !=, <, <=, >, >=, in, !in Left (50, 51) Structural equality, relational ordering, membership (in / !in)
52 | Left (52, 53) Bitwise OR
54 ^ Left (54, 55) Bitwise XOR
56 & Left (56, 57) Bitwise AND
60 .., ..= Non-assoc (61, 61) Half-open [a, b), inclusive [a, b], and open slicing ranges (a.., ..b, ..)
70 <-> Left (70, 71) Manhattan / taxicab distance (vec2, vec3, int, float)
75 <<, >> Left (75, 76) Bitwise left shift and arithmetic right shift
80 +, - Left (80, 81) Addition, concatenation, subtraction
90 *, /, % Left (90, 91) Multiplication, vector scalar scaling, division, remainder
100 -, !, ~ (Prefix) Prefix (100) Unary negation, logical NOT, bitwise NOT
110 !, ? (Postfix) Postfix (110) Option force-unwrap (opt!) and presence check (opt? \to bool)
120 ., :: Left (120, 121) Field access, method invocation, module member access
130 (, [, { Left (130, 0) Function call, indexing / slicing, block / map literal
140 (Highest) Literals, IDENT, ArrayLit, MapLit Primary Atomic literals and identifiers

Compound Assignment Operators

Assignable targets (let mut identifiers, array/map indices a[i], and struct/map fields obj.field) support: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=.


4. Runtime Types & Truthiness

Summary of Runtime Value Types

Type Tag (type_of(v)) Category Description & Literal / Constructor Syntax
"nil" Primitive Absence of value (nil)
"bool" Primitive Boolean truth value (true, false)
"int" Primitive Signed 64-bit integer (42, 0xFF, 0b1010)
"bigint" Numeric Arbitrary-precision integer (automatic on 64-bit overflow, or bigint("..."))
"float" Primitive 64-bit IEEE-754 floating-point number (3.14, 1e9)
"char" Primitive Unicode code point rune ('a', '\n')
"string" Sequence Immutable UTF-8 string ("hello", f"val={x}")
"array" Collection Dynamic contiguous array ([1, 2, 3]) and fixed-length tuples ((a, b))
"map" Collection Structural hash table ({ a: 1, [expr]: 2 }, map())
"option" Algebraic Optional value (Some(v), None)
"vec2" Spatial Immutable 2D integer coordinate (vec2(x, y))
"vec3" Spatial Immutable 3D integer coordinate (vec3(x, y, z))
"range" Interval Half-open or inclusive integer interval (0..10, 1..=5)
"closure" Callable Function or lambda with lexical upvalues (fn f(x) {}, x -> x + 1)
"module" Namespace Imported Tinsel module (import "std/math")
"grid", "rangeset", "set", "deque", "Iter", "<StructName>" Prototype / struct Pure-Tinsel domain objects and user-defined struct instances backed by __proto__

Truthiness Rules

In conditional contexts (if, while, &&, ||, !):

  • Falsy: nil, false, 0 (int), 0.0 (float), "" (empty string), [] (empty array), and None (option).
  • Truthy: true, non-zero numbers, non-empty strings, non-empty arrays, Some(v), and all maps, structs, vectors, ranges, and closures.

Map Literal Key Rules

In a map literal { ... }:

  • Bare identifiers create string keys: { x: 10, y: 20 } is equivalent to { "x": 10, "y": 20 }.
  • String, integer, float, and boolean literals evaluate directly as keys: { 1: "one", "two": 2 }.
  • Computed or composite expression keys (variables, tuples, vectors, struct instances) must be enclosed in square brackets [expr]:
    tinsel
    let start = vec2(2, 3)
    let visited = { [start]: 0, [(0, 1)]: 1 }
    assert(visited[start] == 0 && visited[(0, 1)] == 1)
    

5. Expressions, Control Flow & Comprehensions

Tinsel is expression-oriented:

  • Block Expressions ({ ... }): Evaluate statements sequentially and yield the value of the final trailing expression (or nil if empty).
  • Conditional Expressions (if ... else ...): Yield the value of the executed branch:
    tinsel
    let score = 85
    let label = if score >= 100 { "gold" } else if score >= 50 { "silver" } else { "bronze" }
    assert(label == "silver")
    
  • For Loops & Comprehensions (for pat in iterable { ... }):
    • Iterate over arrays, strings (yielding char), ranges (a..b, a..=b), maps (yielding keys), Iter streams, or any object implementing fn iter(self).
    • When the loop body contains yield expr, the for expression evaluates to an array of all yielded elements.
    • Non-yielding for loops execute for side effects and have their array allocation elided by the compiler.
    tinsel
    let squares = for x in 1..=5 {
        if x % 2 == 1 { yield x * x }
    }
    assert(squares == [1, 9, 25])
    
  • While Loops (while cond { ... }): Repeat while cond is truthy. Support break and continue.

6. Pattern Matching (match & Destructuring)

Patterns appear in let declarations, for loop bindings, function and lambda parameters, and match arms (match target { pat if guard -> expr, ... }):

Pattern Kind Syntax Example Semantics
Wildcard _ Matches any value and discards it
Variable Binding x Matches any value and binds it to x (if x is already bound in the same pattern, checks structural equality ==!)
Literal 0, '#', "ok", true, nil Matches exact literal value
Range 1..10, '0'..='9', 'a'..='z' Matches int or char within the half-open or closed interval
Alternative (|) '+' | '-', 0 | 1 Matches if any sub-pattern matches
Tuple / Fixed Array (x, y), [a, b, c] Matches sequence of exact length and destructures elements
Slice / Rest (..) [head, ..tail], [first, .., last] Matches prefix and/or suffix of an array and binds middle slice
String Template "move {n} from {a} to {b}" Matches string structure, binds {vars}, and auto-converts integers
Option Some(v), None Matches and unwraps option values
Positional Struct Circle(r), Rect(w, w) Matches struct type __type__ and destructures fields positionally
Named Struct / Map Point { x: 0, y }, { id, score: 100 } Matches map keys or struct fields by name
tinsel
struct Rect(w, h)

fn classify(item) {
    match item {
        Rect(s, s) -> f"square {s}x{s}",
        Rect(w, h) if w > h -> f"wide {w}x{h}",
        [first, ..middle, last] -> f"seq({first}..{last}, mid={middle.len()})",
        "cmd {op} {arg}" -> f"{op}:{arg + 1}",
        _ -> "other"
    }
}

assert(classify(Rect(4, 4)) == "square 4x4")
assert(classify([10, 20, 30, 40]) == "seq(10..40, mid=2)")
assert(classify("cmd add 9") == "add:10")

7. User-Defined struct Declarations & Prototype Protocol (__proto__)

struct Declarations

User-defined struct types declare named fields (with optional default values) and an optional method block where the first parameter of each method is self:

tinsel
struct Interval(lo, hi = 100) {
    fn span(self) {
        self.hi - self.lo
    }

    fn contains(self, x) {
        x >= self.lo && x < self.hi
    }

    fn to_string(self) {
        f"[{self.lo}..{self.hi})"
    }
}

let iv = Interval(10, 25)
assert(iv.span() == 15)
assert(18 in iv && 30 !in iv)
assert(to_string(iv) == "[10..25)")
assert(iv == Interval(10, 25))

Zero-Sort Structural Hashing & Equality

Every struct declaration desugars into a shared prototype descriptor (__proto__) storing the ordered field list (__fields__) and method closures. Instances with 0 to 4 fields compute comparable Go hash keys (StructHashKey0..4) directly in field-declaration order without sorting map keys, making user-defined structs as fast as tuples when used as map keys, set elements, or dijkstra states.

Prototype Metamethod Hooks

Any struct (or map with a __proto__ table) can hook into built-in operators by defining these methods:

Operator / Built-in Metamethod Signature Description
obj[k] fn index_get(self, k) Custom bracket indexing read
obj[k] = v fn index_set(self, k, v) Custom bracket indexing write
elem in obj / elem !in obj fn contains(self, elem) Custom membership test (in / !in)
obj.len() / len(obj) fn len(self) Custom length / cardinality
to_string(obj) / f"{obj}" fn to_string(self) Custom string formatting
for x in obj / obj.iter() fn iter(self) Returns an Iter or array for iteration

8. The Module System (import & export)

Every .tin file is an isolated module with its own global scope:

  • Public vs. Private Symbols: Top-level bindings without a leading underscore (fn solve(), let dirs = ..., struct Node(...)) are automatically exported. Top-level bindings starting with _ (let _cache = {}, fn _helper() {}) are module-private. You may also write export fn ... or export let ... explicitly.
  • Import Forms:
    tinsel
    import "std/math"                        // Binds module to `math`
    import "std/grid" as g                   // Binds module to alias `g`
    from "std/search" import dijkstra, bfs   // Binds specific exports
    import { gcd, lcm as least_mul } from "std/math"
    from "std/itertools" import *            // Imports all public exports
    let dyn_mod = import("std/orbit")        // Dynamic runtime import
    
  • Singleton Caching & Circular Import Detection: Modules are evaluated once on first import and cached by canonical path. Circular imports are detected at runtime with a clear error trace.
  • Resolution Order:
    1. Embedded standard library modules ("std/<name>" or shorthand "<name>").
    2. Absolute file paths.
    3. Relative paths (./ or ../) resolved relative to the importing file's directory.
    4. Current working directory.
    5. Directories listed in the TINSEL_PATH environment variable. (The .tin file extension is optional in import strings.)

9. Formal EBNF Grammar

ebnf
Program         = { Statement , [ TERMINATOR ] } ;
TERMINATOR      = ";" | "\n" ;

Statement       = LetDecl | AssignStmt | FnDecl | StructDecl
                | ForStmt | WhileStmt | YieldStmt | ReturnStmt
                | BreakStmt | ContinueStmt | ImportStmt | ExportStmt | ExprStmt ;

LetDecl         = "let" , [ "mut" ] , Pattern , [ ":" , Type ] , "=" , Expr ;
AssignStmt      = Expr , AssignOp , Expr ;
AssignOp        = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>=" ;
Param           = Pattern , [ "=" , Expr ] ;
FnDecl          = "fn" , IDENT , "(" , [ Param , { "," , Param } ] , ")" , Block ;
StructField     = IDENT , [ "=" , Expr ] ;
StructDecl      = "struct" , IDENT , [ "(" , [ StructField , { "," , StructField } ] , ")" ] , [ "{" , { FnDecl } , "}" ] ;
WhileStmt       = "while" , Expr , Block ;
ForStmt         = "for" , Pattern , "in" , Expr , Block ;
YieldStmt       = "yield" , Expr ;
ReturnStmt      = "return" , [ Expr ] ;
BreakStmt       = "break" ;
ContinueStmt    = "continue" ;
ExprStmt        = Expr ;

ImportStmt      = "import" , STRING_LIT , [ "as" , IDENT ]
                | "from" , STRING_LIT , "import" , ( "*" | ImportSpecList )
                | "import" , "{" , ImportSpecList , "}" , "from" , STRING_LIT
                | "import" , "*" , [ "as" , IDENT ] , "from" , STRING_LIT ;
ImportSpecList  = ImportSpec , { "," , ImportSpec } , [ "," ] ;
ImportSpec      = IDENT , [ "as" , IDENT ] ;
ExportStmt      = "export" , ( LetDecl | FnDecl | StructDecl ) ;

Block           = "{" , { Statement , [ TERMINATOR ] } , [ Expr ] , "}" ;

Pattern         = SinglePattern , { "|" , SinglePattern } ;
SinglePattern   = "_" | RangePat | LiteralPat | StructPat | MapPat
                | BindingPat | TuplePat | SlicePat | StringPat ;
LiteralPat      = INT_LIT | CHAR_LIT | STRING_LIT | "true" | "false" | "nil" ;
RangePat        = ( INT_LIT | CHAR_LIT ) , ( ".." | "..=" ) , ( INT_LIT | CHAR_LIT ) ;
StructPat       = IDENT , "(" , [ Pattern , { "," , Pattern } ] , ")"
                | IDENT , "{" , [ MapPatEntry , { "," , MapPatEntry } ] , "}" ;
MapPat          = "{" , [ MapPatEntry , { "," , MapPatEntry } ] , "}" ;
MapPatEntry     = ( IDENT | STRING_LIT ) , [ ":" , Pattern ] ;
BindingPat      = IDENT ;
TuplePat        = "(" , [ Pattern , { "," , Pattern } , [ "," ] ] , ")" ;
SlicePat        = "[" , [ Pattern , { "," , Pattern } ] , [ ".." , [ Pattern ] , { "," , Pattern } ] , "]" ;
StringPat       = PAT_STRING ;

MapLit          = "{" , [ MapEntry , { ( "," | TERMINATOR ) , MapEntry } , [ "," | TERMINATOR ] ] , "}" ;
MapEntry        = ( IDENT | STRING_LIT | INT_LIT | FLOAT_LIT | BOOL_LIT | "[" , Expr , "]" ) , ":" , Expr ;

Expr            = LambdaExpr | PipelineExpr | BinaryExpr | UnaryExpr | PostfixExpr
                | CallExpr | IndexExpr | MemberExpr | IfExpr | MatchExpr
                | MapLit | Block | PrimaryExpr ;
IfExpr          = "if" , Expr , Block , [ "else" , ( Block | IfExpr ) ] ;
MatchExpr       = "match" , Expr , "{" , { MatchArm , [ "," | TERMINATOR ] } , "}" ;
MatchArm        = Pattern , [ "if" , Expr ] , "->" , Expr ;
LambdaExpr      = ( Pattern | "(" , [ Param , { "," , Param } ] , ")" ) , "->" , Expr
                | "fn" , "(" , [ Param , { "," , Param } ] , ")" , Block ;