Syntax
Full syntax reference for the Tish language.
Keywords
fn— function declaration (replacesfunction;functionalso 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,typeoftrue,false,null
Literals
- Numbers:
1,1.5,0.5, scientific notation1.5e-3,1e10,2E+3, and radix literals0xff,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 listComma-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 returnAssignment
x = expr— assigns to existingletvariable- 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 importsimport * as M from "./mod.tish"— namespace importimport X from "./mod.tish"— default importexport 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.jsWith 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?