IlluLang Syntax Reference
This document summarizes IlluLang’s current syntax and features. It aligns with the VERSION file in the repository (reference 1.3.0). See also STDLIB_REFERENCE for builtins and CHANGELOG for release notes.
File extension
.ilu-- IlluLang source files
Comments
## ...-- line comment#* ... *#-- block comment (can span multiple lines)
Notes: - Block comments nest correctly within code. - Comment markers inside string/backtick literals are preserved as text.
Execution Fast Paths
Loop bodies may use an optimized execution path for simple integer assignments (performance only). Unsupported statement shapes automatically fall back to canonical parser/evaluator behavior.
For optimization verification in debug/CI runs:
- ILLULANG_VERIFY_FASTPATH=1
- ILLULANG_VERIFY_FASTPATH_RATE=N (sample 1/N statements, 1 = every statement)
Types
int-- integer numbers (e.g.,123,0xFF,0b1010,0o17,0d42)float-- floating numbers (e.g.,3.14)bool--true/falsestring-- double-quoted text:"hello", or triple-quoted multi-line:"""multi\nline"""array-- list:[1, 2, 3]dict-- map:{key: value}matrix-- strict 2D grid:let m: matrix[int] = 2*3tuple-- immutable ordered sequence:(1, "hello", true)function-- defined withfnkeywordnone-- the absence of a value:noneany-- wildcard type that accepts any value
Number Bases
IlluLang supports numeric literals in four bases:
| Prefix | Base | Example | Decimal Value |
|---|---|---|---|
0b |
Binary | 0b1010 |
10 |
0o |
Octal | 0o17 |
15 |
0x |
Hexadecimal | 0xFF |
255 |
0d |
Explicit decimal | 0d42 |
42 |
Arithmetic between values of the same base preserves the base in the result:
var a = 0xFF + 0x01 ## → 0x100 (hex preserved)
var b = 0b1010 + 0b1 ## → 0b1011 (binary preserved)
Use base(value, target) to convert between bases, and base(value) to query the current base:
display(base(255, 16)) ## → 0xff
display(base(255, 2)) ## → 0b11111111
display(base(0xFF)) ## → 16
Scientific Notation
Float literals support scientific notation with e or E:
var big = 1e9 ## 1000000000.0
var small = 2.5e-3 ## 0.0025
var avogadro = 6.022e23
Integers
Integers are 64-bit signed (long long), supporting values up to ±9,223,372,036,854,775,807.
Numbers are displayed in Python-style full-digit format (no scientific notation truncation for reasonable values).
Declarations
- Typed (strict):
let name : type = expr - Typed (strict, default value):
let name : type - Dynamic:
var name = expr - Dynamic (optional annotation):
var name : type = expr,var name : type - Tuple unpacking:
let [a, b] = expr,var [a, b] = expr
Examples:
let x : int = 5
let count : int
var y = 3.2
let [a, b] = [1, 2]
Rules:
- let must include an explicit type annotation.
- let variables are non-dynamic: reassignment must keep the declared type.
- var variables are dynamic: reassignment can change type.
- Reserved words and type names cannot be used as variable names.
- is_strict(x) returns whether x is a strict (let) variable.
- is_strict(varName) checks variable declaration mode.
- is_strict(1) (or any direct value/expression) returns true.
Typed collection annotations are accepted in declaration syntax:
- array[int]
- array[int|float]
- dict[int]
- matrix[int]
For strict let declarations:
- array[...] requires an element type annotation.
- dict[...] requires a value type annotation; dictionary keys are always strings.
- matrix[...] requires an element type annotation; matrices are strict-only (let).
- Initializer literals must match those declared collection element types.
Expressions
- Arithmetic:
+,-,*,/,^(power),//(euclidean division),%(euclidean modulo) - Comparisons:
==,!=,<,<=,>,>= - Bitwise/Shift:
not,and,xor,or,<<(left shift),>>(right shift) - Pipe:
|(used for type unions in annotations, e.g.int|float; not a pipeline operator) - Ternary:
cond ? expr_if_true : expr_if_false - Parentheses:
(expr) - String concatenation:
"a" + "b"
Operator Details
| Operator | Description | Example |
|---|---|---|
^ |
Power / exponentiation | 2 ^ 10 → 1024 |
// |
Euclidean (floor) division | 17 // 3 → 5, -7 // 3 → -3 |
% |
Euclidean modulo (always non-negative when divisor is positive) | 17 % 3 → 2, -7 % 3 → 2 |
/ |
Division (int when evenly divisible, float otherwise) | 10 / 2 → 5, 10 / 3 → 3.333... |
<< |
Left bit shift (integers only) | 1 << 4 → 16 |
>> |
Right bit shift (arithmetic, integers only) | 16 >> 2 → 4 |
Logical vs Bitwise Behavior
The operators and, or, xor, not serve dual purpose:
- On booleans: logical operations (true and false → false)
- On integers: bitwise operations (0b1100 and 0b1010 → 8, i.e. 0b1000)
- On floats: bitwise operations (cast to int, operate, cast back)
not on an integer performs bitwise complement: not 0xFF → -256 (i.e. ~0xFF).
Shift Operators
<< and >> are integer-only shift operators:
- 1 << 4 → 16 (multiply by 2⁴)
- 16 >> 2 → 4 (divide by 2²)
- >> uses arithmetic right shift (preserves sign): -1 >> 1 → -1
- Shifting by a negative amount or ≥ bit width yields 0
- Precedence: shift binds tighter than comparisons, looser than addition
Logical precedence (highest to lowest): not → and → xor → or.
Ternary ?: binds lower than logical operators.
Strict numeric rule:
- Mixed int/float arithmetic is normally allowed (e.g., 7 + 8.5).
- If a strict (let) typed numeric variable participates, mixed numeric arithmetic is rejected.
- Example: let a: int = 7; a + 8.5 -> error.
Assignment and Updates
- Reassignment:
x = expr - Compound assignment:
+=,-=,*=,/=,^=,//=,%=,<<=,>>= - Increment/decrement:
x++,++x,x--,--x - Custom step increment/decrement:
++3x(prefix, step 3),x++2(postfix, step 2),--5x,x--3
Examples:
var x = 1
x = x + 1
x += 2
x ^= 3 ## power-assign: x = x ^ 3
x //= 2 ## euclidean-assign: x = x // 2
x %= 5 ## modulo-assign: x = x % 5
var y = x++
var z = ++x
## Custom step increments
var a = 10
++3a ## a becomes 13 (increment by 3)
a++5 ## returns 13, then a becomes 18 (increment by 5)
--2a ## a becomes 16 (decrement by 2)
a--4 ## returns 16, then a becomes 12 (decrement by 4)
String Interpolation
Use backticks with ${expr} placeholders:
var name = "Ada"
display(`Hello, ${name}`)
display(`1 + 2 = ${1 + 2}`)
String Escape Sequences
Inside both "..." and backtick `...` strings:
| Sequence | Result |
|----------|--------|
| \n | newline |
| \t | tab |
| \r | carriage return |
| \\ | literal backslash \ |
| \" | literal double quote " |
| \` | literal backtick ` |
Example:
display("line1\nline2") ## prints on two lines
display("col1\tcol2") ## tab-separated
Multi-line Strings
Use triple double-quotes to span multiple lines:
var text = """
This is a
multi-line string.
"""
Triple-quoted strings preserve embedded newlines. Escape sequences still work inside them.
String Indexing
Access individual characters by index (0-based, negative indices count from end):
var ch = "hello"[0] ## "h"
var last = "hello"[-1] ## "o"
var s = "world"
display(s[2]) ## "r"
Arrays
- Literal:
[1, 2, 3] - Indexing:
a[0](0-based, valid in expressions) - Builtins:
len(x)returns length whenxis array or string. - Helpers:
push(arr, value)returns a new array withvalueappended;pop(arr)returns a new array without the last element. - Since arrays are functional for these helpers, assign results back when mutating:
arr = push(arr, value)arr = pop(arr)
Array Arithmetic
Arrays support arithmetic with integers for structural operations:
| Expression | Result | Description |
|---|---|---|
[5] + 2 |
[5, 0, 0] |
Extend: append N zeros |
[5, 8] - 1 |
[5] |
Shrink: remove last N elements |
[7] * 2 |
[7, 7] |
Repeat: duplicate array N times |
[5, 6] ^ 2 |
[[5,6]:[5,6]] |
Square: convert to matrix (dynamic var only) |
Compound assignment versions also work: arr += 2, arr -= 1, arr *= 3, arr ^= 2.
Array-to-array operations:
| Expression | Result | Description |
|---|---|---|
[1, 2] + [3, 4] |
[1, 2, 3, 4] |
Concatenate two arrays |
[1, 2, 3] - [2] |
[1, 3] |
Remove last occurrence of each right-hand element |
Restrictions: - Array / int is not allowed. - Arithmetic on dicts is not allowed.
Range Operator
Create integer ranges with start -> end (exclusive end, like Python's range()):
var nums = 1 -> 6 ## [1, 2, 3, 4, 5]
var odds = 1 -> 10:2 ## [1, 3, 5, 7, 9] (step)
var down = 10 -> 0:3 ## [10, 7, 4, 1] (descending)
var rev = 1 -> 10:-2 ## [9, 7, 5, 3, 1] (reverse of 1->10:2)
Optional :STEP controls spacing. A positive step follows the natural direction (start < end ascending, start > end descending). A negative step returns the reversed sequence of the corresponding positive-step range.
Dictionaries
- Literal:
{name: "Bob", age: 25} - Access by key:
d["name"](returns the value for key"name") - Builtins:
dict(),dict_get(),dict_set(),dict_keys() - Keys are always strings.
- Indexed assignment is supported:
let d: dict[int] = {count: 0}
d["count"] = 1
display(d["count"]) ## prints 1
Restrictions:
- Arithmetic operations (+, -, *, /, ^, //, %) on dict variables themselves are not allowed (e.g., d + 1 is an error).
- Arithmetic on dict values accessed by key works normally: d["count"] + 1 is valid because d["count"] evaluates to the stored value type (int, float, etc.).
- Compound indexed assignment is also supported: d["key"] += 1, d["key"] *= 2, etc.
Tuples
Tuples are immutable, heterogeneous ordered sequences created with parenthesised comma-separated values.
Creation
var t = (1, "hello", true)
let t2: tuple[int|string] = (42, "world")
A single parenthesised expression (x) is not a tuple - at least two elements (separated by commas) are required:
var a = (5) ## just the int 5, not a tuple
var b = (5, 10) ## tuple with 2 elements
var special rules
When using var (dynamic) without a type annotation, tuple creation has special unwrapping:
var a = () ## none (empty tuple becomes none)
var b = (42,) ## 42 (single-element tuple unwraps to the value)
var c = (1, 2, 3) ## (1, 2, 3) tuple (2+ elements stay as tuple)
Use a trailing comma to create a single-element tuple with var. With let and a type annotation, this unwrapping does not apply.
Indexing (read-only)
var t = (10, 20, 30)
display(t[0]) ## 10
display(t[-1]) ## 30 (negative wraps)
Tuples are immutable - index assignment is a compile-time error:
t[0] = 99 ## Error: Cannot assign to tuple elements (tuples are immutable)
Arithmetic (element-wise)
Tuples of equal length support element-wise +, -, *, /, //, %, ^:
var a = (1, 2, 3)
var b = (4, 5, 6)
display(a + b) ## (5, 7, 9)
display(a * b) ## (4, 10, 18)
Builtins
len(t)- returns the number of elementstype(t)- returns"tuple(int|string)"with unique element types
Type annotations
let t: tuple = (1, 2)
let t2: tuple[int|float] = (1, 3.14)
let vs var
letrequires at least 2 elements and enforces the declared type annotation.varis dynamic: 0 elements →none, 1 element → unwrapped, 2+ → tuple.
Matrices
Matrices are strict 2D grids with a fixed element type. They can only be declared with let (strict typing required).
Declaration
Matrices require a type annotation with an element type:
let m: matrix[int] = 2*3 ## 2 rows × 3 columns, zero-initialized
let n: matrix[float] = 3*3 ## 3×3 float matrix
Literal Syntax
Use [[row]:[row]] syntax to initialize with values:
let m: matrix[int] = [[1, 2, 3]:[4, 5, 6]] ## 2×3 matrix
let identity: matrix[int] = [[1, 0]:[0, 1]] ## 2×2 identity
Rows are separated by : and enclosed in [[ ]]. All rows must have the same number of columns, and all values must match the declared element type.
Access
- Row access:
m[0]returns the first row as an array - Element access:
m[0][1]ormat_get(m, 0, 1)returns a single element - Element assignment:
m[0][1] = 42ormat_set(m, 0, 1, 42)sets a single element (mutates in place) - Compound assignment:
m[0][1] += 10,m[1][0] *= 2, etc. -- all compound operators work
Matrix Builtins
| Function | Description |
|---|---|
is_matrix(v) |
Returns true if v is a matrix |
mat_rows(m) |
Number of rows |
mat_cols(m) |
Number of columns |
mat_get(m, r, c) |
Get element at row r, column c |
mat_set(m, r, c, val) |
Set element at row r, column c to val |
mat_transpose(m) |
Returns a new transposed matrix |
mat_flatten(m) |
Returns all elements as a flat array |
Example
let m: matrix[int] = [[1, 2]:[3, 4]]
display(mat_get(m, 0, 0)) ## 1
mat_set(m, 1, 1, 42)
display(mat_get(m, 1, 1)) ## 42
display(mat_rows(m)) ## 2
display(mat_cols(m)) ## 2
var t = mat_transpose(m)
display(mat_flatten(t)) ## [1, 3, 2, 42]
Array-to-Matrix Conversion
Dynamic (var) arrays can be squared into a matrix using the ^ operator:
var a = [5, 6]
var m = a ^ 2 ## creates matrix [[5,6]:[5,6]]
This is only allowed for dynamic variables, not for strict let arrays.
Indexed Assignment
- Arrays:
arr[index] = value - Dictionaries:
dict["key"] = value - Matrices:
m[row][col] = value
Indexed Compound Assignment
Compound operators work on indexed elements the same way they work on variables:
| Target | Syntax | Description |
|---|---|---|
| Array element | arr[i] += 5 |
Add 5 to element at index i |
| Dict value | d["key"] -= 1 |
Subtract 1 from value at key "key" |
| Matrix element | m[r][c] *= 2 |
Multiply element at (r, c) by 2 |
All compound operators are supported: +=, -=, *=, /=, ^=, //=, %=, <<=, >>=.
Examples:
var arr = [10, 20, 30]
arr[0] += 5 ## arr becomes [15, 20, 30]
arr[1] -= 10 ## arr becomes [15, 10, 30]
let d: dict[int] = {count: 0, total: 100}
d["count"] += 1 ## d["count"] becomes 1
d["total"] //= 3 ## d["total"] becomes 33
let m: matrix[int] = [[1, 2]:[3, 4]]
m[0][0] += 10 ## m[0][0] becomes 11
m[1][1] ^= 2 ## m[1][1] becomes 16
Functions
- Definition:
fn add(a, b) {
return a + b
}
- Type annotations:
fn add(a: int, b: int) -> int {
return a + b
}
- Parameter annotations are optional; unannotated parameters are dynamic.
- Annotated parameters are enforced at call time.
- Annotated return types (
-> type) are enforced when returning. - Anonymous functions:
var square = lm(x) {
return x * x
}
- Lambda parameters can also be typed:
var add = lm(a: int, b: int) { a + b }
- Function references: assign a named function (including builtins) to a variable and call it:
var printer = display
printer("hello") ## calls display("hello")
fn greet(name) { return "Hi, " + name }
var g = greet
display(g("Ada")) ## prints "Hi, Ada"
- Function type annotations (for variables holding functions):
let f: function[int, int] -> int = add ## function taking (int, int) returning int
let g: function[any] -> any = display ## any parameter / return types
- Default parameter values:
fn greet(name, greeting = "Hello") {
return greeting + ", " + name
}
display(greet("Ada")) ## "Hello, Ada"
display(greet("Ada", "Hi")) ## "Hi, Ada"
- Parameters with defaults must come after required parameters.
- Supported default value types: integers, floats, strings, booleans,
none, and empty arrays ([]). returnsupport: use thereturnkeyword inside function bodies to return a value.- Implicit return: the last expression evaluated in a function body is returned automatically if no
returnstatement is reached.
fn double(n) { n * 2 } ## returns n*2 implicitly
var square = lm(x) { x * x } ## lambda implicit return
Control Flow
if/elseblocks (parentheses around condition are optional)whileloops (parentheses around condition are optional)- C-style
for (init; cond; step)is not supported; usefor ... in ... {}forms below for i in start->end {}-- range loop (exclusive end):iiterates values fromstartup to (but not including)endfor i in start->end:step {}-- range with stepfor i in expr {}-- foreach over array/string:i= value (element or character)for i, v in expr {}-- foreach with index and value:i= index,v= elementfor k in dict {}-- iterate dictionary keysfor k, v in dict {}-- iterate dictionary key-value pairsmatch expr { case val { } ... default { } }-- pattern matchingbreakandcontinueinside loopsbreak return [expr]to exit the loop and the current function
Examples:
var x = 0
if x == 0 { x = 1 } else { x = 2 } ## parens optional
if (x == 1) { display("also works") } ## parens allowed
var i = 0
while i < 5 { ## parens optional
i = i + 1
}
## for-in range (1 through 5, exclusive end)
var total = 0
for n in 1->6 {
total = total + n
}
## total == 15
## for-in range with step (odd numbers 1..9)
var odds_sum = 0
for n in 1->10:2 {
odds_sum = odds_sum + n
}
## odds_sum == 25
## for-in foreach over array: single var = VALUE
var items = [10, 20, 30]
for v in items {
display(v) ## v = 10, 20, 30
}
## for-in foreach over array: two vars = index + value
for i, v in items {
display(i, v) ## i=0 v=10, i=1 v=20, i=2 v=30
}
## for-in foreach over string: single var = char, two vars = index + char
for ch in "hello" {
display(ch)
}
for i, ch in "hello" {
display(i, ch)
}
## for-in foreach over dict
var d = {name: "Ada", age: 30}
for key in d {
display(key) ## "name", "age"
}
for key, val in d {
display(key, val) ## "name" "Ada", "age" 30
}
## match statement
match x {
case 1 { display("one") }
case 2 { display("two") }
default { display("other") }
}
fn find_three() {
var i = 0
while (i < 5) {
i = i + 1
if (i == 3) { break return i }
}
return -1
}
Switch/Case
switch is a convenience alias for match. They are fully interchangeable:
var code = 200
switch code {
case 200 { display("OK") }
case 404 { display("Not Found") }
case 500 { display("Server Error") }
default { display("Unknown") }
}
The semantics are identical to match -- the first matching case executes and the rest are skipped. Use default for the fallback branch.
Pattern Matching Destructuring
match (and switch) support array destructuring in case arms. You can bind individual elements, or use the rest pattern ...name to capture remaining elements:
var data = [1, 2, 3, 4, 5]
match data {
case [a, b] {
display("exactly two:", a, b)
}
case [head, ...tail] {
display("head:", head, "tail:", tail)
}
default {
display("no match")
}
}
## prints: head: 1 tail: [2, 3, 4, 5]
Exact match
case [a, b] matches only when the array has exactly 2 elements. The values are bound to a and b.
Rest pattern
case [first, second, ...rest] matches when the array has at least 2 elements. first and second bind to the first two elements, and rest captures the remaining elements as a new array.
var nums = [10, 20, 30]
match nums {
case [x, ...xs] {
display("first:", x) ## 10
display("rest:", xs) ## [20, 30]
}
}
Generators and Yield
A function that uses yield becomes a generator. When called, it runs to completion and returns an array of all yielded values:
fn fibonacci(n) {
var a = 0
var b = 1
for i in 0->n {
yield a
var temp = a
a = b
b = temp + b
}
}
var fibs = fibonacci(8)
display(fibs) ## [0, 1, 1, 2, 3, 5, 8, 13]
Key points:
- yield expr adds expr to the result array and continues execution.
- The function automatically returns the array of all yielded values when it finishes.
- If a return statement is reached before the function ends, the values yielded so far are returned.
- Generators are detected automatically -- any function whose body contains yield is treated as a generator.
fn evens_up_to(limit) {
var i = 0
while i < limit {
if (i % 2 == 0) { yield i }
i = i + 1
}
}
display(evens_up_to(10)) ## [0, 2, 4, 6, 8]
Closures and Reference Cells
Lambdas (lm) automatically capture variables from their enclosing scope. This enables closures:
fn make_adder(n) {
return lm(x) { x + n }
}
var add5 = make_adder(5)
display(add5(3)) ## 8
display(add5(10)) ## 15
Reference Cells
For shared mutable state across closures, use reference cells (ref, deref, ref_set):
fn make_counter() {
var cell = ref(0)
return lm() {
ref_set(cell, deref(cell) + 1)
return deref(cell)
}
}
var counter = make_counter()
display(counter()) ## 1
display(counter()) ## 2
display(counter()) ## 3
| Function | Description |
|---|---|
ref(value) |
Create a reference cell, returns a cell ID (int) |
deref(cell_id) |
Read the current value of the cell |
ref_set(cell_id, value) |
Update the cell's value |
Reference cells are useful when multiple closures need to share and mutate the same piece of state.
String Builder
For efficient string construction, use the string builder API instead of repeated concatenation:
var sb = sb_new()
sb_append(sb, "Hello")
sb_append(sb, ", ")
sb_append(sb, "world!")
var result = sb_to_string(sb)
display(result) ## "Hello, world!"
display(sb_len(sb)) ## 13
| Function | Description |
|---|---|
sb_new() |
Create a new string builder, returns a builder ID (int) |
sb_append(sb, value) |
Append a value (converted to string) to the builder |
sb_to_string(sb) |
Get the built string |
sb_len(sb) |
Get the current length in characters |
sb_count(sb) |
Get the number of append operations performed |
sb_clear(sb) |
Reset the builder (clear contents and count) |
Typed Array Constructors
Create pre-allocated homogeneous arrays with a default value:
var ints = int_array(5, 0) ## [0, 0, 0, 0, 0]
var floats = float_array(3, 1.5) ## [1.5, 1.5, 1.5]
var flags = bool_array(4, false) ## [false, false, false, false]
var names = string_array(2, "") ## ["", ""]
| Function | Description |
|---|---|
int_array(size, default) |
Create an array of size ints, each set to default |
float_array(size, default) |
Create an array of size floats |
bool_array(size, default) |
Create an array of size bools |
string_array(size, default) |
Create an array of size strings |
These are useful for pre-allocating buffers and matrices without manual loops.
Async / Await
Run functions asynchronously with async, then collect results with await or await_all:
fn compute(n) {
var total = 0
for i in 0->n {
total = total + i * i + 1
}
return total
}
var t1 = async(lm() { compute(5) })
var t2 = async(lm() { compute(10) })
var r1 = await(t1)
var r2 = await(t2)
display(r1, r2) ## 26 101
## Or collect all at once:
var t3 = async(lm() { compute(5) })
var t4 = async(lm() { compute(10) })
var results = await_all(t3, t4)
display(results) ## [26, 101]
| Function | Description |
|---|---|
async(fn) |
Schedule a zero-argument function for execution, returns a task ID (int) |
await(task_id) |
Execute the task (if pending) and return its result |
await_all(t1, t2, ...) |
Execute all tasks and return an array of results |
achieve_async("name", args...) |
Schedule an intent dispatch for deferred execution, returns a task ID |
is_pending(task_id) |
Check whether a task is still pending (true/false) |
task_count() |
Return the number of async tasks created in the current session |
Note: Tasks are executed eagerly when await or await_all is called (cooperative, single-threaded). This is by design for deterministic scripting behavior.
Async Intents
You can dispatch intents asynchronously using achieve_async:
intent compute(x) priority 10 { return x * x + 1 }
var t1 = achieve_async("compute", 5)
var t2 = achieve_async("compute", 10)
var r1 = await(t1) ## 26
var r2 = await(t2) ## 101
## Or batch:
var t3 = achieve_async("compute", 3)
var t4 = achieve_async("compute", 7)
var all = await_all(t3, t4) ## [10, 50]
Error Handling
Use try / instead blocks to guard code:
try {
var data = read_file("input.txt")
display(data)
} instead {
display("Failed: " + error)
}
throw
Use throw to raise user-defined errors that can be caught by try/instead.
Thrown errors are categorised as error[throw] and reference Enum.Errors.ThrowError:
## Statement form
throw "something went wrong"
## Expression form (as a function call)
throw("invalid argument")
## Caught by try/instead
try {
if x < 0 {
throw("x must be non-negative")
}
} instead {
display("Error: " + error) ## error variable contains the message
}
assert
assert(condition) or assert(condition, message) verifies that condition is truthy.
If the assertion fails, an error[assert] is raised with the optional message as a hint:
assert(len(items) > 0, "items must not be empty")
assert(x >= 0)
Enum.Errors
Enum.Errors provides named constants for every error category.
Each error message automatically includes the matching Enum.Errors.* reference:
Enum.Errors.SyntaxError
Enum.Errors.TypeError
Enum.Errors.DivisionByZero
Enum.Errors.IndexOutOfRange
Enum.Errors.RuntimeError
Enum.Errors.ThrowError
Enum.Errors.AssertionError
Enum.Errors.KeyboardInterrupt
Use them for comparison or for descriptive error handling:
let err = Enum.Errors.DivisionByZero
display(err) ## "Enum.Errors.DivisionByZero"
Deletion (del)
Use del to remove user-defined names or indexed entries:
var nums = [10, 20, 30]
del nums[1]
display(nums) ## [10, 30]
var cfg = {"mode": "dev", "port": 8080}
del cfg["mode"]
display(cfg) ## {"port": 8080}
del name removes a user-defined variable, custom type, or custom enum category by name.
del name.member removes a dictionary member (dot path form).
del Enum.Category also removes a user-defined enum category.
del Enum.Category.Member removes a user-defined enum member.
del name[index] removes an element from arrays or dicts.
Named imports remain protected: deleting internals like del lib["x"] is blocked.
Deleting the alias variable itself (del lib) is allowed.
Built-in enums are protected: del Enum.Errors is not allowed.
Built-in enum members are also protected: del Enum.Errors.SyntaxError is not allowed.
Custom Enums (makenum)
makenum is append-only for existing categories: only missing members are added.
Existing members are left unchanged.
Built-in enums can be extended only with a singleton declaration per call:
makenum Errors { CustomError: 999 }
Modules
Use import to load a module by path, and export to expose values from that file.
## math.ilu
export var pi = 3.1415
export fn add(a, b) { return a + b }
import "math.ilu"
display(pi)
display(add(1, 2))
Named imports
Use import name "path" to import all exports as a dictionary bound to name:
import math "math.ilu"
display(math.pi)
display(math.add(2, 3))
display(math["pi"])
display(math["add"](2, 3))
Both dot notation (name.member) and bracket notation (name["member"]) are supported for named imports.
Named imports are read-only for mutation: assignments like name.member = ..., name["member"] = ..., and compound variants are blocked.
For regular dictionaries, dot assignment and compound assignment are supported:
let cfg: dict[int] = {port: 8000}
cfg.port += 1
cfg.port = 9000
Multiple named imports can be comma-separated:
import math "math.ilu", utils "utils.ilu"
Internal libraries
IlluLang also supports internal module IDs inside angle brackets, imported as strings:
import "<windows>"
import win "<windows>"
"<...>" imports are handled by the runtime (not the filesystem). Unknown internal IDs raise an import error.
Custom Types (maketype)
maketype registers a named type alias/constraint:
maketype point {tuple[int|int]}
maketype names {array[string]}
maketype mode {0|1|2}
maketype tokens {[string, 5]}
maketype cfg {{key: 5, key2: [string]}}
Custom types can then be used in let declarations:
let p: point = (3, 4)
let ns: names = ["Alice", "Bob"]
let m: mode = 1
let t: tokens = ["a", 5, "b", 5]
When a let/typed var declaration omits = ..., defaults are inferred from the custom constraint:
- maketype test {5} then let v: test initializes to 5
- maketype test {[]} then let v: test initializes to []
- Dict/tuple/matrix constraints infer corresponding structured defaults
Constraint examples:
- maketype mode {0|1|2}: int literal union.
- maketype letters {["a", "b"]}: array where each element must match one listed option.
- maketype tokens {[string, 5]}: array where each element is any string or literal 5.
- maketype cfg {{key: 5, key2: [string]}}: dict with exact keys and per-key constraints.
- maketype pair {(5, 5)}: tuple with exact shape/value constraints.
The type() function returns the custom type name for variables declared with a custom type:
maketype score {int}
let s: score = 42
display(type(s)) ## "score"
Intent-oriented paradigm (IlluLang exclusive)
IlluLang introduces a novel "intent-oriented" paradigm where programs declare named intents (goals) and provide handlers that achieve them. This is useful for orchestration, UI event handling, and flexible composition.
- Define an intent handler with
intent name(params) [priority N] [when condition] { #* body *# }. - Intent parameters can be optionally annotated with base types (
int,float,bool,string,array,dict,matrix,function). - Invoke with statement form:
achieve name(arg1, arg2)(displays result). - Or expression form:
var r = achieve("name", arg1, arg2)(returns handler result).
Intent Handler Options
Priority: Handlers with higher priority execute first. Handlers are sorted descending by priority at runtime.
intent greet(who) priority 10 {
display("High-priority greeting: " + who)
return true
}
intent greet_typed(who: string) priority 12 {
return true
}
intent greet(who) priority 5 {
display("Low-priority greeting: " + who)
return false
}
achieve greet("world") // High-priority runs first; execution stops if it returns truthy
When: Conditional execution. If when evaluates to falsy, the handler is skipped.
intent process(data) when (starts_with(type(data), "array")) {
display("Processing array: " + type(data))
}
Score: Adds a dynamic score to the handler. Effective ordering is priority + score.
intent route(msg) priority 5 score (len(msg) * 0.1) {
return true
}
Chaining control: halt stops evaluation, continue forces the next handler.
intent route(msg) priority 10 {
if (len(msg) > 100) { halt }
continue
}
Fallback: otherwise runs if no handler matches or all are falsy.
otherwise route(msg) {
display("No handler matched")
}
Handler Execution Policy
When achieve is called:
1. All handlers for that intent are collected and sorted by priority (highest first).
2. For each handler in order:
- The when condition (if present) is evaluated in the handler's frame.
- If when is falsy, the handler is skipped.
- Otherwise, the handler executes.
- If the handler returns a truthy value (non-false, non-zero, non-empty-string), execution stops.
3. Return value (for expression form) is the last handler's return value if it was truthy; otherwise 0.
Behavior notes:
- Multiple handlers per intent are fully supported.
- Handlers have their own lexical frame and inherit parameters.
- If a parameter is typed, handlers only run when the provided argument matches that type.
- return in a handler sets __return in that frame and is retrieved after execution.
- halt and continue can control chaining without returning a value.
Introspection and Timeouts
var names = intents()
var hs = handlers("route")
achieve route(msg) within 200ms
Generator Intent Handlers
Intent handlers can use yield to produce multiple values. When a handler body contains yield, the yielded values are collected and returned as an array:
intent fibonacci(n) priority 10 {
var a = 0
var b = 1
var i = 0
while i < n {
yield a
var temp = a + b
a = b
b = temp
i = i + 1
}
}
var fibs = achieve("fibonacci", 8)
display(fibs) ## [0, 1, 1, 2, 3, 5, 8, 13]
Generator support works in both regular intent handlers and otherwise fallback handlers.
Builtins
I/O
display(expr, ...)-- prints one or more values to stdout, space-separated, followed by a newlineinput([prompt], [type])-- reads a line from stdin, returns a string. Optional prompt is printed first. Optionaltypeparameter converts the result:input(">> ", int),input(">> ", float),input(">> ", bool). Type can be a bare identifier (int) or a string ("int").assert(condition, [message])-- verifiesconditionis truthy; raiseserror[assert]with optional message hint on failureread_file(path)-- reads file into a string (empty string on failure)write_file(path, contents)-- writes string to file, returns boolappend_file(path, contents)-- appends string to file, returns boolexists(path)-- returns bool if file existslist_dir([path])-- returns array of file names in a directory (default".")file_delete(path)-- deletes a file, returns boolfile_copy(src, dst)-- copies a file, returns boolfile_move(src, dst)-- moves/renames a file, returns bool
Type & Introspection
type(value)-- returns type as a detailed string:- Scalars:
"int","float","bool","string","function","unknown" - Arrays:
"array[int]","array[int, float]"(union of element types) - Dicts:
"dict[string, int]"(union of value types) - Matrices:
"matrix[int]" is_int(v),is_float(v),is_bool(v),is_string(v),is_array(v)-- type checksis_dict(v)-- returnstrueif value is a dictis_function(v)-- returnstrueif value is a functionis_matrix(v)-- returnstrueif value is a matrixis_empty(value)-- returnstrueif array, string, dict, or matrix is emptyis_strict(v)-- returns whether a variable is strict (let)to_int(value)-- converts to intto_float(value)-- converts to floatto_string(value)-- converts to stringchar(code)-- converts a Unicode code point (0..1114111) to a one-character UTF-8 stringord(str)-- returns the Unicode code point of the first character instr(returns0for an empty string)get_args([index])-- returns CLI script args as array of strings; with index returns one arg ornone
Notes:
- char(233) returns "é", char(9731) returns "☃".
- Values 128..159 are valid Unicode C1 control code points and are often non-printable in terminals.
String Operations
len(str)-- length of string, array, dict (number of keys), or matrix (rows x cols)upper(str)-- uppercase version of stringlower(str)-- lowercase version of stringsubstr(str, start, [len])-- extract substring starting atstartwith optionallen; iflenomitted, extracts to endtrim(str)-- remove leading and trailing whitespacesplit(str, [delim])-- split string by delimiter (default" "), returns arrayjoin(arr, [sep])-- join array elements with separator (default""), returns stringstarts_with(str, prefix)-- returns boolends_with(str, suffix)-- returns boolindex_of(str, needle)-- returns index or -1find(str, needle)-- alias forindex_of()repeat(str, count)-- repeats stringcounttimesreplace(str, from, to)-- replaces all occurrenceschar_at(str, index)-- single-character string
Array Operations
len(arr)-- length of arrayslice(arr, start, [end])-- extract subarray fromstarttoend(exclusive); ifendomitted, extracts to endpush(arr, value)-- returns a new array withvalueappendedpop(arr)-- returns a new array without the last elementmax(arr),min(arr)-- numeric extremacontains(arr, value)-- bool membership check (arrays only)includes(arr, value)-- alias forcontains()(arrays only)reverse(arr),sort(arr),unique(arr),flatten(arr)-- array helpers (sortuses O(n log n) merge sort)
Dict Operations
dict()-- create an empty dictdict_get(d, key)-- get value by key, returns 0 if not founddict_set(d, key, value)-- set key-value pairdict_has(d, key)-- returns booldict_remove(d, key)-- remove keydict_keys(d)-- returns array of keysdict_values(d)-- returns array of valueskeys(d)-- alias fordict_keysvalues(d)-- alias fordict_values
Math
abs(x)-- absolute valuesqrt(x)-- square rootpow(base, exp)-- exponentiationfloor(x)-- floorceil(x)-- ceilinground(x, [decimals])-- round to nearest integer, or todecimalsdecimal placesrandom()-- random float in [0, 1); automatically seeded on first calllog(x)-- natural logarithmlog10(x)-- base-10 logarithmexp(x)-- e raised to the power x
Trigonometry
sin(x),cos(x),tan(x)-- trig functions (radians)asin(x),acos(x),atan(x)-- inverse trigatan2(y, x)-- two-argument arctangent
Functional
map(arr, fn)-- apply fn to each element, returns new arrayfilter(arr, fn)-- keep elements where fn returns truthyreduce(arr, fn, [initial])-- accumulate via fn(acc, elem); ifinitialomitted, uses first elementforeach(arr, fn)-- call fn(elem) for each elementenumerate(arr)-- returns [[index, elem], ...]zip(arr1, arr2)-- pairs elements: [[a, b], ...]sum(arr)-- sum numeric elements
String Builder
sb_new()-- create a new string builder, returns builder IDsb_append(sb, value)-- append value to buildersb_to_string(sb)-- get the built stringsb_len(sb)-- current length in characterssb_count(sb)-- number of append operationssb_clear(sb)-- reset the builder
Reference Cells
ref(value)-- create a reference cell, returns cell IDderef(cell_id)-- read the cell's valueref_set(cell_id, value)-- update the cell's value
Typed Array Constructors
int_array(size, default)-- create array of intsfloat_array(size, default)-- create array of floatsbool_array(size, default)-- create array of boolsstring_array(size, default)-- create array of strings
Async / Await
async(fn)-- schedule a function for async execution, returns task IDawait(task_id)-- execute task and return resultawait_all(t1, t2, ...)-- execute all tasks, return array of results
System
clock()-- current time in secondsexit([code])-- exit the processenv_get(name)-- get environment variableenv_set(name, value)-- set environment variableenv_list()-- returns all environment variables as a dictgetcwd()-- current working directorychdir(path)-- change working directorysystem_exec(cmd)-- execute shell command, returns output
Path
path_join(parts...)-- joins path componentspath_dir(path)-- directory portion of pathpath_base(path)-- filename portion of pathpath_ext(path)-- file extension (including dot)
JSON
json_parse(str)-- parse a JSON string into an IlluLang value (objects → dicts, arrays → arrays). Supports all standard escapes including\b,\f, and\uXXXXUnicode escapes.json_stringify(value)-- convert an IlluLang value to a JSON string
Examples:
var s = " hello world "
display(trim(s)) ## "hello world"
display(substr(s, 3, 5)) ## "hello"
display(upper("test")) ## "TEST"
var items = [1, 2, 3]
items = push(items, 4) ## items becomes [1, 2, 3, 4]
display(slice(items, 1, 3)) ## prints [2, 3]
REPL Features
- Colorized: Keywords, types, strings, numbers (including base prefixes), operators, and builtins are syntax-highlighted with ANSI colors.
- Multiline: Accepts multiline inputs (balanced parentheses/braces) and shows a colorized preview.
- History: Line editing and history supported via linenoise.
clearcommand: Typeclearto clear the terminal screen.- Ctrl+C: Cancels the current line input (does not exit the REPL).
- Ctrl+D: Exits the REPL.
Running
- Start the REPL:
illulang
- Execute a file:
illulang program.ilu
- Control logging (set
ILLULANG_LOGto error|warn|info|debug):
ILLULANG_LOG=debug illulang program.ilu
- Uninstall the VS Code extension:
illulang --stop-extension
- Dump bytecode listing:
illulang --bytecode program.ilu
Advanced Topics
Lexical Scoping
Variables and parameters follow lexical scoping. Inner scopes can access outer scope variables but cannot modify them in outer scope (modifications are local).
Function Returns
Use the return keyword to return early from a function:
fn check(x) {
if (x < 0) return "negative"
if (x == 0) return "zero"
return "positive"
}
Intent Best Practices
- Use descriptive intent names (pronouns: "verify_input", "process_data").
- Handlers should be idempotent when possible.
- Use
whenconditions to guard expensive or side-effectful operations. - Return values to signal success/failure; implement proper error handling.
Architecture Notes
- Parser: Recursive-descent, direct execution.
- Evaluator: Direct tree-walking interpreter.
- Runtime: Lexical frames for scoping, intent registry for handler management.
- Bytecode VM: Experimental (ast.c + vm.c).
This file is updated as the language evolves. See CHANGELOG.html for recent changes.