IlluLang Logo

IlluLang

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 / false
  • string -- 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*3
  • tuple -- immutable ordered sequence: (1, "hello", true)
  • function -- defined with fn keyword
  • none -- the absence of a value: none
  • any -- 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 ^ 101024
// Euclidean (floor) division 17 // 35, -7 // 3-3
% Euclidean modulo (always non-negative when divisor is positive) 17 % 32, -7 % 32
/ Division (int when evenly divisible, float otherwise) 10 / 25, 10 / 33.333...
<< Left bit shift (integers only) 1 << 416
>> Right bit shift (arithmetic, integers only) 16 >> 24

Logical vs Bitwise Behavior

The operators and, or, xor, not serve dual purpose: - On booleans: logical operations (true and falsefalse) - On integers: bitwise operations (0b1100 and 0b10108, 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 << 416 (multiply by 2⁴) - 16 >> 24 (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): notandxoror. 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 when x is array or string.
  • Helpers: push(arr, value) returns a new array with value appended; 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 elements
  • type(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

  • let requires at least 2 elements and enforces the declared type annotation.
  • var is 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] or mat_get(m, 0, 1) returns a single element
  • Element assignment: m[0][1] = 42 or mat_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 ([]).
  • return support: use the return keyword inside function bodies to return a value.
  • Implicit return: the last expression evaluated in a function body is returned automatically if no return statement is reached.
fn double(n) { n * 2 }         ## returns n*2 implicitly
var square = lm(x) { x * x }   ## lambda implicit return

Control Flow

  • if / else blocks (parentheses around condition are optional)
  • while loops (parentheses around condition are optional)
  • C-style for (init; cond; step) is not supported; use for ... in ... {} forms below
  • for i in start->end {} -- range loop (exclusive end): i iterates values from start up to (but not including) end
  • for i in start->end:step {} -- range with step
  • for 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 = element
  • for k in dict {} -- iterate dictionary keys
  • for k, v in dict {} -- iterate dictionary key-value pairs
  • match expr { case val { } ... default { } } -- pattern matching
  • break and continue inside loops
  • break 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 newline
  • input([prompt], [type]) -- reads a line from stdin, returns a string. Optional prompt is printed first. Optional type parameter converts the result: input(">> ", int), input(">> ", float), input(">> ", bool). Type can be a bare identifier (int) or a string ("int").
  • assert(condition, [message]) -- verifies condition is truthy; raises error[assert] with optional message hint on failure
  • read_file(path) -- reads file into a string (empty string on failure)
  • write_file(path, contents) -- writes string to file, returns bool
  • append_file(path, contents) -- appends string to file, returns bool
  • exists(path) -- returns bool if file exists
  • list_dir([path]) -- returns array of file names in a directory (default ".")
  • file_delete(path) -- deletes a file, returns bool
  • file_copy(src, dst) -- copies a file, returns bool
  • file_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 checks
  • is_dict(v) -- returns true if value is a dict
  • is_function(v) -- returns true if value is a function
  • is_matrix(v) -- returns true if value is a matrix
  • is_empty(value) -- returns true if array, string, dict, or matrix is empty
  • is_strict(v) -- returns whether a variable is strict (let)
  • to_int(value) -- converts to int
  • to_float(value) -- converts to float
  • to_string(value) -- converts to string
  • char(code) -- converts a Unicode code point (0..1114111) to a one-character UTF-8 string
  • ord(str) -- returns the Unicode code point of the first character in str (returns 0 for an empty string)
  • get_args([index]) -- returns CLI script args as array of strings; with index returns one arg or none

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 string
  • lower(str) -- lowercase version of string
  • substr(str, start, [len]) -- extract substring starting at start with optional len; if len omitted, extracts to end
  • trim(str) -- remove leading and trailing whitespace
  • split(str, [delim]) -- split string by delimiter (default " "), returns array
  • join(arr, [sep]) -- join array elements with separator (default ""), returns string
  • starts_with(str, prefix) -- returns bool
  • ends_with(str, suffix) -- returns bool
  • index_of(str, needle) -- returns index or -1
  • find(str, needle) -- alias for index_of()
  • repeat(str, count) -- repeats string count times
  • replace(str, from, to) -- replaces all occurrences
  • char_at(str, index) -- single-character string

Array Operations

  • len(arr) -- length of array
  • slice(arr, start, [end]) -- extract subarray from start to end (exclusive); if end omitted, extracts to end
  • push(arr, value) -- returns a new array with value appended
  • pop(arr) -- returns a new array without the last element
  • max(arr), min(arr) -- numeric extrema
  • contains(arr, value) -- bool membership check (arrays only)
  • includes(arr, value) -- alias for contains() (arrays only)
  • reverse(arr), sort(arr), unique(arr), flatten(arr) -- array helpers (sort uses O(n log n) merge sort)

Dict Operations

  • dict() -- create an empty dict
  • dict_get(d, key) -- get value by key, returns 0 if not found
  • dict_set(d, key, value) -- set key-value pair
  • dict_has(d, key) -- returns bool
  • dict_remove(d, key) -- remove key
  • dict_keys(d) -- returns array of keys
  • dict_values(d) -- returns array of values
  • keys(d) -- alias for dict_keys
  • values(d) -- alias for dict_values

Math

  • abs(x) -- absolute value
  • sqrt(x) -- square root
  • pow(base, exp) -- exponentiation
  • floor(x) -- floor
  • ceil(x) -- ceiling
  • round(x, [decimals]) -- round to nearest integer, or to decimals decimal places
  • random() -- random float in [0, 1); automatically seeded on first call
  • log(x) -- natural logarithm
  • log10(x) -- base-10 logarithm
  • exp(x) -- e raised to the power x

Trigonometry

  • sin(x), cos(x), tan(x) -- trig functions (radians)
  • asin(x), acos(x), atan(x) -- inverse trig
  • atan2(y, x) -- two-argument arctangent

Functional

  • map(arr, fn) -- apply fn to each element, returns new array
  • filter(arr, fn) -- keep elements where fn returns truthy
  • reduce(arr, fn, [initial]) -- accumulate via fn(acc, elem); if initial omitted, uses first element
  • foreach(arr, fn) -- call fn(elem) for each element
  • enumerate(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 ID
  • sb_append(sb, value) -- append value to builder
  • sb_to_string(sb) -- get the built string
  • sb_len(sb) -- current length in characters
  • sb_count(sb) -- number of append operations
  • sb_clear(sb) -- reset the builder

Reference Cells

  • ref(value) -- create a reference cell, returns cell ID
  • deref(cell_id) -- read the cell's value
  • ref_set(cell_id, value) -- update the cell's value

Typed Array Constructors

  • int_array(size, default) -- create array of ints
  • float_array(size, default) -- create array of floats
  • bool_array(size, default) -- create array of bools
  • string_array(size, default) -- create array of strings

Async / Await

  • async(fn) -- schedule a function for async execution, returns task ID
  • await(task_id) -- execute task and return result
  • await_all(t1, t2, ...) -- execute all tasks, return array of results

System

  • clock() -- current time in seconds
  • exit([code]) -- exit the process
  • env_get(name) -- get environment variable
  • env_set(name, value) -- set environment variable
  • env_list() -- returns all environment variables as a dict
  • getcwd() -- current working directory
  • chdir(path) -- change working directory
  • system_exec(cmd) -- execute shell command, returns output

Path

  • path_join(parts...) -- joins path components
  • path_dir(path) -- directory portion of path
  • path_base(path) -- filename portion of path
  • path_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 \uXXXX Unicode 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.
  • clear command: Type clear to 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_LOG to 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 when conditions 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.