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_]*).
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 (-9223372036854775808to9223372036854775807) automatically promote to arbitrary-precisionbigint:tinsellet 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:tinsellet 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\U000NNNNNescapes:tinsellet 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 withfevaluate arbitrary Tinsel expressions inside{expr}and splice their formatted string representations (escape literal braces with\{and\}):tinsellet 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 "..." = sormatch s { "..." -> ... }) destructure formatted strings and automatically parse integer substrings intoint:tinsellet "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 |
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:
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)
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),""(emptystring),[](emptyarray), andNone(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]:tinsellet 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 (ornilif empty). - Conditional Expressions (
if ... else ...): Yield the value of the executed branch:tinsellet 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),Iterstreams, or any object implementingfn iter(self). - When the loop body contains
yield expr, theforexpression evaluates to an array of all yielded elements. - Non-yielding
forloops execute for side effects and have their array allocation elided by the compiler.
tinsellet squares = for x in 1..=5 { if x % 2 == 1 { yield x * x } } assert(squares == [1, 9, 25]) - Iterate over arrays, strings (yielding
- While Loops (
while cond { ... }): Repeat whilecondis truthy. Supportbreakandcontinue.
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 |
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:
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 writeexport fn ...orexport 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:
- Embedded standard library modules (
"std/<name>"or shorthand"<name>"). - Absolute file paths.
- Relative paths (
./or../) resolved relative to the importing file's directory. - Current working directory.
- Directories listed in the
TINSEL_PATHenvironment variable. (The.tinfile extension is optional in import strings.)
- Embedded standard library modules (
9. Formal EBNF Grammar
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 ;