Syntax

Full syntax reference for the Tish language.

Keywords

  • fn — function declaration (replaces function; function also supported)
  • let — mutable variable declaration (block-scoped)
  • const — immutable variable declaration (block-scoped, error on reassignment)
  • if, else, while, for, return, break, continue, switch, case, default, do, throw, try, catch, typeof
  • true, false, null

Literals

  • Numbers: 1, 1.5, 0.5, scientific notation 1.5e-3, 1e10, 2E+3, and radix literals 0xff, 0o17, 0b1010 (optional _ separators)
  • Strings: "hello", 'world' (escapes: \n, \r, \t, \\, \", \')
  • Booleans: true, false
  • Null: null
  • Arrays: [1, 2, 3]
  • Objects: { x: 1, y: 2 } (fixed keys at parse time)

Variable declarations

let x = 1                 // mutable, block-scoped
const y = 2               // immutable
let a = 1, b = 2, c       // comma-separated declarators (c is uninitialized)
let [p, q] = pair, r = 3  // a destructuring declarator may appear in the list

Comma-separated declarators also work in a for initializer:

for (let i = 0, n = arr.length; i < n; i++) {
  // ...
}

Each declarator binds in the same scope (no nested block) — let a = 1, b = 2 is equivalent to writing the two lets on separate lines.

Functions

fn name(a, b) { return a + b }
fn double(x) = x * 2   // single-expression, implicit return

Assignment

  • x = expr — assigns to existing let variable
  • Compound: x += expr, x -= expr, x *= expr, x /= expr, x %= expr
  • const variables cannot be reassigned (runtime error)

Modules

  • import { a, b } from "./mod.tish" — named imports
  • import * as M from "./mod.tish" — namespace import
  • import X from "./mod.tish" — default import
  • export const x, export fn f, export default expr — exports

See Modules for full details.

Indentation

  • Braces optional: use indentation for blocks.
  • Tab and space normalized: 1 tab = 1 level; 2 spaces = 1 level.
  • No mixing errors: both styles work; only consistent level matters.

Ignoring indentation (debugging)

When you're debugging how nested blocks transpile, you can tell the lexer to ignore indentation so blocks are delimited only by braces { … }. Fully brace-delimited code parses identically either way, so this isolates whether a block-nesting problem comes from the indentation layer.

  • Set the environment variable TISH_IGNORE_INDENT=1 — honored by every command that parses Tish (run, build, dump-ast, fmt, lint).
  • Or pass the flag to the AST dumper: tish dump-ast --ignore-indent file.tish (the flag is OR'd with the env var, mirroring --no-optimize / TISH_NO_OPTIMIZE).
# Inspect the parsed block structure with indentation ignored
tish dump-ast --ignore-indent app.tish
 
# Apply it to any command — e.g. transpile to JavaScript and compare the output
TISH_IGNORE_INDENT=1 tish build app.tish --target js -o app.js

With indentation ignored, an indented line no longer opens a block, so every block must use explicit braces.

Grammar (informal)

Program     := Statement*
Statement   := Block | VarDecl | ExprStmt | If | While | For | Return | Break | Continue | FunDecl | Import | Export
Import      := 'import' ( '{' ImportSpec+ '}' | '*' 'as' Ident | Ident ) 'from' String
Export      := 'export' ( 'default' Expr | 'const' VarDecl | 'let' VarDecl | FunDecl )
Block       := Indent Statement* Dedent  |  '{' Statement* '}'
VarDecl     := ('let' | 'const') Ident TypeAnn? ('=' Expr)? ';'?
ExprStmt    := Expr ';'?
If          := 'if' '(' Expr ')' Statement ('else' Statement)?
While       := 'while' '(' Expr ')' Statement
For         := 'for' '(' Init? ';' Cond? ';' Update? ')' Statement
             | 'for' '(' ('let'|'const') Ident 'of' Expr ')' Statement
Return      := 'return' Expr? ';'?
FunDecl     := ('fn' | 'function') Ident '(' TypedParams? ')' TypeAnn? '=' Expr
             | ('fn' | 'function') Ident '(' TypedParams? ')' TypeAnn? Block
Expr        := Assign | NullishCoalesce | Or | ...
Assign      := Ident '=' Expr
NullishCoalesce := Or ('??' Or)*

TypeAnn     := ':' Type
Type        := Ident | Type '[]' | '{' (Ident TypeAnn ',')* '}' | Type '|' Type
TypedParams := TypedParam (',' TypedParam)*
TypedParam  := Ident TypeAnn?

Improve this documentation