Skip to main content

AILANG v0.7.0 - AI Teaching Prompt

AILANG is a pure functional language with Hindley-Milner type inference and algebraic effects. Write code using recursion (no loops), pattern matching, and explicit effect declarations.

⚠️ AILANG is NOT Python! Do not use Python syntax like def, for, while, class, or if x:. See "What AILANG Does NOT Have" below.

Required Program Structure

module myproject/mymodule

export func main() -> () ! {IO} {
println(show(42)) -- println is in prelude, no import needed!
}

Rules:

  1. First line MUST be module path/name matching the file path
  2. Use println(show(value)) for numbers, println(str) for strings
  3. Inside { } blocks: use let x = e; (semicolons). NEVER use let x = e in!
  4. No loops - use recursion

println vs print

FunctionImport Required?Behavior
printlnNO (prelude)WITH newline
printYES (import std/io (print))NO newline
-- println works without import (prelude provides it)
println("A"); println("B") -- Output: A\nB\n

-- print requires import for no-newline output
import std/io (print)
print("A"); print("B") -- Output: AB

Use println for most output. Use print ONLY when building output on one line.

CLI Exploration

ailang --help                   # all ailang abilities
ailang docs --list # List stdlib modules
ailang docs std/io # Show module exports
ailang builtins list # All 72 builtins
ailang builtins show _ai_call # Detailed documentation
ailang check file.ail # Type-check without running
ailang examples search "recursion" # Find working code examples
ailang examples show fold_reduce # View example with expected output

Quick Reference Examples

Print calculation:

module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} = println(show(5 % 3))

If-then-else (NO braces!):

module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} {
let x = 10;
let msg = if x > 0 then "positive" else "not positive";
println(msg)
}

HTTP GET:

module benchmark/solution
import std/net (httpGet)
export func main() -> () ! {IO, Net} = println(httpGet("https://example.com"))

HTTP POST with JSON:

module benchmark/solution
import std/net (httpPost)
import std/json (encode, jo, kv, js, jnum)
export func main() -> () ! {IO, Net} {
let data = encode(jo([kv("message", js("hello")), kv("count", jnum(42.0))]));
println(httpPost("https://httpbin.org/post", data))
}

Filter list recursively:

module benchmark/solution
export func filter(people: [{name: string, age: int}], minAge: int) -> [{name: string, age: int}] =
match people {
[] => [],
p :: rest => if p.age >= minAge then p :: filter(rest, minAge) else filter(rest, minAge)
}

Record update (immutable):

module benchmark/solution
export func main() -> () ! {IO} {
let person = {name: "Alice", age: 30, city: "NYC"};
let older = {person | age: person.age + 1}; -- Update one field
let moved = {older | city: "SF"}; -- Chain updates
println(person.name ++ ", " ++ show(person.age) ++ ", " ++ person.city);
println(older.name ++ ", " ++ show(older.age) ++ ", " ++ older.city);
println(moved.name ++ ", " ++ show(moved.age) ++ ", " ++ moved.city)
}

Open records (width subtyping):

module benchmark/solution

-- EXACT record: only accepts {name: string}, rejects extra fields
pure func getNameExact(p: {name: string}) -> string = p.name

-- OPEN record with | r: accepts extra fields
pure func getName(p: {name: string | r}) -> string = p.name

-- OPEN record with ... sugar (equivalent to | r with fresh variable)
pure func getEmail(u: {email: string, ...}) -> string = u.email

export func main() -> () ! {IO} {
-- Exact: only works with exact shape
println(getNameExact({name: "Alice"}));

-- Open: accepts any record with at least name field
println(getName({name: "Bob", age: 30}));
println(getName({name: "Charlie", age: 25, city: "NYC"}));

-- Ellipsis sugar: same behavior
println(getEmail({email: "test@example.com", name: "Test", id: 123}))
}

AI effect (call LLM):

module benchmark/solution
import std/ai (call)
export func main() -> () ! {IO, AI} = println(call("What is 2+2?"))

What AILANG Does NOT Have

InvalidUse Instead
for/while loopsRecursion
[x*2 for x in xs]map(\x. x*2, xs) or recursion
var/let mutImmutable let bindings
list.map()map(f, list)
import "std/io"import std/io (println)
{"key": "val"}jo([kv("key", js("val"))])
f(a)(b) for multi-arg funcf(a, b) - multi-arg funcs use commas
if x { ... }if x then ... else ... - NO braces!
mixing let x = e in with ;Use ONE style consistently
let (x, y) = tupleUse match tuple { (x, y) => ... }
\(a, b). body pair syntaxUse func(a: T, b: U) -> R { body }
nested func f(...) =Use let f = \x. body for nested functions
!conditionnot condition - AILANG uses not for boolean negation
concat(a, b)a ++ b - use ++ for string/list concatenation

Let Bindings: Block Style vs Expression Style

Rule: Inside { } blocks, use SEMICOLONS. With = bodies, use in.

-- Block style: curly braces + semicolons
export func main() -> () ! {IO} {
let x = 1;
let y = 2;
println(show(x + y))
}

-- Expression style: equals + in
export func main() -> () ! {IO} =
let x = 1 in
let y = 2 in
println(show(x + y))

-- WRONG: Using `in` inside `{ }` block causes scope errors
export func main() -> () ! {IO} {
let x = 1 in -- DON'T use `in` inside blocks!
let y = 2; -- ERROR: x out of scope
println(show(x + y))
}

Simple rule: See { -> use ;. See = -> use in.

Function Calls: Multi-arg vs Curried (CRITICAL)

Two styles with DIFFERENT call syntax:

-- MULTI-ARG FUNC: Call with commas f(a, b)
func add(a: int, b: int) -> int = a + b
let result = add(3, 4) -- CORRECT: 7
-- let bad = add(3)(4) -- WRONG: arity mismatch!

-- CURRIED LAMBDA: Call with chained f(a)(b)
let addC = \a. \b. a + b
let result = addC(3)(4) -- CORRECT: 7
let add10 = addC(10) -- Partial application!
-- let bad = addC(3, 4) -- WRONG: arity mismatch!

Rule: Match call style to definition style:

DefinitionCall StylePartial App?
func f(a, b) = ...f(a, b)No
let f = \a. \b. ...f(a)(b)Yes

Higher-order with curried lambdas:

module benchmark/solution
import std/io (println)

export func main() -> () ! {IO} {
let compose = \f. \g. \x. f(g(x));
let double = \x. x * 2;
let addOne = \x. x + 1;
let doubleThenAdd = compose(addOne)(double); -- Curried: chain calls
println(show(doubleThenAdd(5)))
}

Using std/list foldl (IMPORTANT - use inline func syntax!):

module benchmark/solution
import std/io (println)
import std/list (foldl)

-- std/list foldl signature: foldl(f: (acc, elem) -> acc, initial, list)
-- RECOMMENDED: Use inline func syntax for reliable type inference

export func main() -> () ! {IO} {
-- MOST RELIABLE: inline func with explicit types
let sum = foldl(func(acc: int, x: int) -> int { acc + x }, 0, [1,2,3,4,5]);
println(show(sum))
}

foldl syntax options:

-- RECOMMENDED: space-separated multi-param lambda (concise)
foldl(\acc x. acc + x, 0, xs)

-- VERBOSE: inline func with explicit types (also works)
foldl(func(acc: int, x: int) -> int { acc + x }, 0, xs)

-- WRONG: curried lambda (arity mismatch error!)
foldl(\acc. \x. acc + x, 0, xs)

Sorting with sortBy:

import std/list (sortBy)

-- Comparator returns: negative if a < b, 0 if equal, positive if a > b
func cmpInt(a: int, b: int) -> int = a - b

let sorted = sortBy(cmpInt, [3, 1, 4, 1, 5]) -- [1, 1, 3, 4, 5]

-- Reverse sort: swap arguments
func cmpDesc(a: int, b: int) -> int = b - a
let descending = sortBy(cmpDesc, [3, 1, 4]) -- [4, 3, 1]

Recursive Lambdas (letrec)

Use letrec when a lambda needs to call itself:

module benchmark/solution

export func main() -> () ! {IO} {
-- WRONG: `let` for self-referential lambda
-- let factorial = \n. if n <= 1 then 1 else n * factorial(n - 1); -- ERROR: undefined 'factorial'

-- RIGHT: use `letrec` for recursive lambdas
letrec factorial = \n. if n <= 1 then 1 else n * factorial(n - 1);
println(show(factorial(5))) -- 120
}

When to use letrec vs func:

CaseUse
Top-level recursive functionfunc f(...) = ... (preferred)
Recursive lambda inside blockletrec f = \x. ... f(...)
Non-recursive lambdalet f = \x. ...

Mutual recursion also works:

letrec isEven = \n. if n == 0 then true else isOdd(n - 1);
letrec isOdd = \n. if n == 0 then false else isEven(n - 1);
println(show(isEven(4))) -- true

Type Annotations for Higher-Order Functions

Annotate function types with parentheses around arrow types:

-- Single-argument function type
let double: int -> int = \x. x * 2

-- Higher-order: function taking a function
let apply: (int -> int) -> int -> int = \f. \x. f(x)

-- Multiple function parameters
let compose: (int -> int) -> (int -> int) -> int -> int = \f. \g. \x. f(g(x))

Type annotation on multi-arg named function:

func twice(f: int -> int, x: int) -> int = f(f(x))

Syntax Reference

ConstructSyntax
Modulemodule path/name
Importimport std/io (println)
Import aliasimport std/list as L
Functionexport func name(x: int) -> int ! {IO} { body }
Lambda\x. x * 2
Recursive lambdaletrec f = \x. ... f(x) ...
Pattern matchmatch x { 0 => a, n => b } (use =>, commas between arms)
ADTtype Tree = Leaf(int) | Node(Tree, int, Tree)
ADT with Eqtype Color = Red | Green | Blue deriving (Eq)
Record{name: "A", age: 30}
Record update{base | field: val}
Open record type{name: string | r} or {name: string, ...}
List consx :: xs or ::(x, xs)
Array literal#[1, 2, 3]
Effect! {IO, FS, Net} after return type

Effects (Side Effects Must Be Declared)

Every function performing I/O must declare effects in the signature!

-- Pure (no effects) - use `pure func` or omit effect annotation
pure func add(x: int, y: int) -> int = x + y

-- IO effect - for print/println/readLine
func greet(name: string) -> () ! {IO} = println("Hello " ++ name)

-- Single effect with return value
func ask(prompt: string) -> string ! {IO} {
println(prompt);
readLine()
}

-- Multiple effects
func process(path: string) -> () ! {IO, FS} {
let content = readFile(path);
println(content)
}

-- Main function typically needs effects
export func main() -> () ! {IO} {
println("Hello, World!")
}

-- Main with file system access
export func main() -> () ! {IO, FS} {
let content = readFile("data.txt");
println(content)
}

-- Main with network
export func main() -> () ! {IO, Net} {
let body = httpGet("https://example.com");
println(body)
}

Common effect combinations:

TaskRequired Effects
Print output! {IO}
Read files! {FS} (add IO if also printing)
HTTP requests! {Net} (add IO if also printing)
Environment vars! {Env}
AI calls! {AI}
Full program! {IO, FS, Net} as needed

Effect errors and how to fix:

Error: Effect checking failed for function 'main'
Function uses effects not declared in signature
Missing effects: IO

Fix: Add the missing effect to your function signature:

-- WRONG: missing IO effect
export func main() -> () { println("hi") }

-- CORRECT: declare IO effect
export func main() -> () ! {IO} { println("hi") }

| Effect | Functions | Import |
|--------|-----------|--------|
| `IO` | `print`, `println`, `readLine` | `std/io` (print is builtin) |
| `FS` | `readFile`, `writeFile` | `std/fs` |
| `Net` | `httpGet`, `httpPost`, `httpRequest` | `std/net` |
| `Env` | `getEnv`, `getEnvOr` | `std/env` |
| `AI` | `call` | `std/ai` |
| `Debug` | `log`, `check` | `std/debug` |
| `SharedMem` | `_sharedmem_get`, `_sharedmem_put`, `_sharedmem_cas` | builtins |
| `SharedIndex` | `_sharedindex_upsert`, `_sharedindex_find_simhash` | builtins |

## Standard Library

**Auto-imported (no import needed):**
- `print` (entry modules only)
- `show(x)` - convert to string
- Comparison operators: `<`, `>`, `<=`, `>=`, `==`, `!=`

**Common imports:**
```ailang
import std/io (println, readLine)
import std/fs (readFile, writeFile)
import std/net (httpGet, httpPost, httpRequest)
import std/json (encode, decode, get, getString, getNumber, getInt, getBool, getArray, getObject, asString, asNumber, asArray)
import std/json (filterStrings, filterNumbers, allStrings, allNumbers, getStringArrayOrEmpty)
import std/list (map, filter, foldl, length, concat, sortBy, take, drop, nth, last, any, findIndex)
import std/string (split, chars, trim, stringToInt, stringToFloat, contains, find, substring, intToStr, floatToStr, join)
import std/math (floatToInt, intToFloat, floor, ceil, round, sqrt, pow, abs_Float, abs_Int)
import std/ai (call)
import std/sem (make_frame_at, store_frame, load_frame, update_frame)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)

List functions (std/list):

  • map(f, xs) - Apply function to each element
  • filter(p, xs) - Keep elements matching predicate
  • foldl(f, acc, xs) - Left fold (use func(acc: T, x: U) -> T { ... } for f)
  • length(xs) - List length
  • concat(xs, ys) - Concatenate two lists
  • sortBy(cmp, xs) - Sort with comparator
  • take(n, xs) - Take first n elements
  • drop(n, xs) - Drop first n elements
  • nth(xs, idx) -> Option[a] - Get element at index (0-based), None if out of bounds
  • last(xs) -> Option[a] - Get last element, None if empty
  • any(p, xs) -> bool - Check if any element satisfies predicate
  • findIndex(p, xs) -> Option[int] - Find index of first matching element

String functions (std/string):

  • contains(hay, needle) -> bool - Check if string contains substring
  • find(hay, needle) -> int - Index of substring, or -1 if not found
  • substring(s, start, end) -> string - Extract substring
  • split(s, delim) -> [string] - Split string by delimiter
  • chars(s) -> [string] - Convert string to list of single-character strings (Unicode-aware)
  • stringToInt(s) -> Option[int] - Parse integer
  • stringToFloat(s) -> Option[float] - Parse float
  • intToStr(n) -> string - Convert int to string
  • floatToStr(f) -> string - Convert float to string
  • join(delim, xs) -> string - Join list of strings with delimiter

Math functions (std/math):

  • floatToInt(x) -> int - Convert float to int (truncates toward zero)
  • intToFloat(x) -> float - Convert int to float
  • floor(x) -> float, ceil(x) -> float, round(x) -> float - Rounding
  • sqrt(x), pow(x, y), abs_Float(x), abs_Int(x) - Math operations

String Parsing (Returns Option)

stringToInt returns Option[int], NOT an int. You MUST pattern match:

module benchmark/solution
import std/string (stringToInt)

export func main() -> () ! {IO} {
match stringToInt("42") {
Some(n) => println("Parsed: " ++ show(n)),
None => println("Invalid number")
}
}

With file reading (effect composition):

module benchmark/solution
import std/fs (readFile)
import std/string (stringToInt)

-- Pure function (no effects)
pure func formatMessage(name: string, count: int) -> string =
"User " ++ name ++ " has " ++ show(count) ++ " items"

-- FS effect only
func readCount(filename: string) -> int ! {FS} {
let content = readFile(filename);
match stringToInt(content) {
Some(n) => n,
None => 0
}
}

-- Combined effects: IO + FS
export func main() -> () ! {IO, FS} {
let count = readCount("data.txt");
let msg = formatMessage("Alice", count);
println(msg)
}

Constructing Option Values

To RETURN an Option from a function, use Some(value) or None:

module benchmark/solution
import std/option (Option, Some, None) -- REQUIRED for constructing Option

-- Return Some(value) for success, None for failure
pure func safeDivide(a: int, b: int) -> Option[int] =
if b == 0 then None else Some(a / b)

-- Return Option from validation
pure func validateAge(age: int) -> Option[int] =
if age >= 0 && age <= 150 then Some(age) else None

Error Handling with Result Type

Use the polymorphic Result[a] type for error handling:

module benchmark/solution
import std/string (stringToInt)

-- Polymorphic Result type - works with any success type!
-- Ok(a) takes the polymorphic type, Err(string) is always a string
type Result[a] = Ok(a) | Err(string)

-- Parse integer, returning Result
pure func parseIntResult(s: string) -> Result[int] =
match stringToInt(s) {
Some(n) => Ok(n),
None => Err("Invalid integer")
}

-- Safe division
pure func divSafe(a: int, b: int) -> Result[int] =
if b == 0 then Err("Division by zero") else Ok(a / b)

-- Chain results: parse then divide
pure func parseAndDivide(s: string, divisor: int) -> Result[int] =
match parseIntResult(s) {
Ok(n) => divSafe(n, divisor),
Err(msg) => Err(msg)
}

-- Format Result for output
pure func showResult(r: Result[int]) -> string =
match r {
Ok(v) => "Result: " ++ show(v),
Err(msg) => "Error: " ++ msg
}

export func main() -> () ! {IO} {
println(showResult(parseAndDivide("10", 2))); -- Result: 5
println(showResult(parseAndDivide("10", 0))); -- Error: Division by zero
println(showResult(parseAndDivide("abc", 2))) -- Error: Invalid integer
}

Polymorphic ADTs with mixed field types:

-- Ok(a) uses the type parameter, Err(string) uses a concrete type
type Result[a] = Ok(a) | Err(string)

-- This works correctly:
-- Ok : forall a. a -> Result[a] (polymorphic field)
-- Err : forall a. string -> Result[a] (concrete field)

-- Both constructors can be used with any Result[a] type:
let r1: Result[int] = Ok(42)
let r2: Result[int] = Err("failed")
let r3: Result[string] = Ok("hello")
let r4: Result[string] = Err("also failed")

Number Conversions (std/math)

module benchmark/solution
import std/math (floatToInt, intToFloat, round)
import std/io (println)

export func main() -> () ! {IO} {
let f = 3.7;
let i = floatToInt(f); -- 3 (truncates toward zero)
let r = floatToInt(round(f)); -- 4 (round first, then convert)
let back = intToFloat(i); -- 3.0
println(show(i));
println(show(r))
}

Character Processing

Use chars(s) to convert a string to a list of single-character strings:

module benchmark/solution
import std/string (chars)
import std/list (filter, length)

-- Count vowels in a string
pure func countVowels(s: string) -> int {
let cs = chars(s);
let vowels = filter(func(c: string) -> bool {
c == "a" || c == "e" || c == "i" || c == "o" || c == "u"
}, cs);
length(vowels)
}

export func main() -> () ! {IO} {
println(show(countVowels("hello"))) -- 2
}

Note: chars is Unicode-aware - emoji and accented characters are handled correctly.

Operators

TypeOperators
Arithmetic+, -, *, /, %, **
Comparison<, >, <=, >=, ==, !=
Logical&&, ||, not
String/List++ (concatenation)
List:: (cons/prepend)

Boolean Operations (IMPORTANT)

⚠️ AILANG uses not, NOT ! for boolean negation!

-- WRONG: Python/C-style negation
if !isEmpty(list) then ... -- ERROR: unknown unary operator: !
if !done then ... -- ERROR

-- CORRECT: AILANG-style negation
if not isEmpty(list) then ... -- Works!
if not done then ... -- Works!

Boolean operators:

-- AND: &&
if x > 0 && y > 0 then "both positive" else "no"

-- OR: ||
if x == 0 || y == 0 then "has zero" else "no"

-- NOT: not (NOT `!`)
if not isEmpty(list) then process(list) else []

String and List Concatenation

Use ++ for concatenation (NOT concat()):

-- String concatenation
let greeting = "Hello" ++ " " ++ "World" -- "Hello World"

-- List concatenation
let combined = [1, 2] ++ [3, 4] -- [1, 2, 3, 4]

-- Mixed in expressions
let msg = "Count: " ++ show(length(items))

Pattern Matching

CRITICAL: When a match arm needs multiple let bindings, wrap in { }:

-- WRONG: multiple lets without braces causes parse error
match queue {
[] => result,
node :: rest =>
let x = process(node);
let y = update(x); -- ERROR: expected => got let
recurse(rest, y)
}

-- CORRECT: wrap multiple lets in { }
match queue {
[] => result,
node :: rest => {
let x = process(node);
let y = update(x);
recurse(rest, y)
}
}

On lists:

match xs {
[] => 0,
x :: rest => x + sum(rest)
}

On ADTs:

match tree {
Leaf(n) => n,
Node(l, v, r) => countNodes(l) + 1 + countNodes(r)
}

On Option:

match result {
Some(x) => x,
None => defaultValue
}

On Records (destructuring):

match person {
{name, age} => name ++ " is " ++ show(age)
}

-- Record with renaming
match config {
{host, port: p} => host ++ ":" ++ show(p)
}

-- Nested record patterns
match data {
{user: {email: e}} => e
}

Record pattern syntax:

  • {name} - shorthand, binds field "name" to variable "name"
  • {name: n} - renaming, binds field "name" to variable "n"
  • {name, age} - multiple fields
  • {user: {name}} - nested records
  • {name, ...} - rest pattern (matches some fields, ignores others)

Deriving Eq for ADT Types

Use deriving (Eq) to auto-generate == and != for ADT types:

module benchmark/solution

-- Enum-style ADT with derived equality
type Color = Red | Green | Blue deriving (Eq)

-- ADT with fields also works
type Shape = Circle(int) | Rectangle(int, int) deriving (Eq)

export func main() -> () ! {IO} {
let sameColor = Red == Red; -- true
let diffColor = Red != Blue; -- true
let sameShape = Circle(5) == Circle(5); -- true
let diffShape = Circle(5) != Rectangle(5, 10); -- true
print("Color test: " ++ show(sameColor));
print("Shape test: " ++ show(sameShape))
}

Note: Derived equality compares by constructor and field values (structural equality).

Polymorphic ADTs with Mixed Field Types

ADT constructors can mix polymorphic and concrete field types:

module benchmark/solution
import std/string (stringToInt)

-- Result type: Ok uses type parameter, Err always takes string
type Result[a] = Ok(a) | Err(string)

-- Either type: both sides polymorphic
type Either[a, b] = Left(a) | Right(b)

-- Validation: success is polymorphic, errors are string list
type Validation[a] = Valid(a) | Invalid([string])

-- Usage examples
pure func safeDivide(a: int, b: int) -> Result[int] =
if b == 0 then Err("division by zero") else Ok(a / b)

pure func parsePositive(s: string) -> Result[int] =
match stringToInt(s) {
Some(n) => if n > 0 then Ok(n) else Err("must be positive"),
None => Err("not a number")
}

export func main() -> () ! {IO} {
let r1 = safeDivide(10, 2); -- Ok(5)
let r2 = safeDivide(10, 0); -- Err("division by zero")

match r1 {
Ok(v) => println("Got: " ++ show(v)),
Err(msg) => println("Error: " ++ msg)
};

match r2 {
Ok(v) => println("Got: " ++ show(v)),
Err(msg) => println("Error: " ++ msg)
}
}

Key pattern: When a constructor has a concrete type like Err(string), that type is preserved regardless of the ADT's type parameter. This enables idiomatic error handling where errors are always strings but success values can be any type.

Option type with findFirst and mapOption:

module benchmark/solution

-- Define your own Option if needed (or import std/option)
type Option[a] = Some(a) | None

-- Find first element matching predicate
func findFirst(pred: int -> bool, xs: [int]) -> Option[int] =
match xs {
[] => None,
x :: rest => if pred(x) then Some(x) else findFirst(pred, rest)
}

-- Map over Option
func mapOption(f: int -> int, opt: Option[int]) -> Option[int] =
match opt {
Some(v) => Some(f(v)),
None => None
}

export func main() -> () ! {IO} {
-- Inside { } block: use semicolons, NOT "in"
-- WRONG: let isEven = \n. n % 2 == 0 in
-- RIGHT: let isEven = \n. n % 2 == 0;
let isEven = \n. n % 2 == 0;
let double = \x. x * 2;
let nums = [1, 3, 4, 7, 8];
let found = findFirst(isEven, nums);
let doubled = mapOption(double, found);
print(match doubled { Some(v) => "Found: " ++ show(v), None => "Not found" })
}

ADT state machine (traffic light):

module benchmark/solution

type State = Green(int) | Yellow(int) | Red(int)
type Event = Tick | Reset

func transition(state: State, event: Event) -> State =
match event {
Reset => Green(20),
Tick => match state {
Green(t) => if t > 1 then Green(t - 1) else Yellow(3),
Yellow(t) => if t > 1 then Yellow(t - 1) else Red(10),
Red(t) => if t > 1 then Red(t - 1) else Green(20)
}
}

func showState(s: State) -> string =
match s {
Green(t) => "GREEN(" ++ show(t) ++ ")",
Yellow(t) => "YELLOW(" ++ show(t) ++ ")",
Red(t) => "RED(" ++ show(t) ++ ")"
}

export func main() -> () ! {IO} {
-- WRONG: let s0 = Green(2) in (don't use "in" in blocks!)
-- RIGHT: let s0 = Green(2);
let s0 = Green(2);
let s1 = transition(s0, Tick);
let s2 = transition(s1, Tick);
print(showState(s2))
}

JSON

Common JSON imports (use all you need):

-- For building JSON
import std/json (encode, jo, ja, kv, js, jnum, jb, jn)

-- For parsing JSON
import std/json (decode, get, getString, getInt, getArray, asString, asNumber, asArray)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)

Build and encode JSON:

import std/json (encode, jo, ja, kv, js, jnum, jb, jn)

-- JSON constructors: js(string), jnum(float), jb(bool), jn() for null
-- jo([kv(k,v)...]) for objects, ja([...]) for arrays

-- Build JSON object with all types
let json = encode(jo([
kv("name", js("Alice")),
kv("age", jnum(30.0)),
kv("active", jb(true))
]))
-- Result: "{\"name\":\"Alice\",\"age\":30,\"active\":true}"

-- Build JSON array
let arr = encode(ja([jnum(1.0), jnum(2.0), jnum(3.0)]))
-- Result: "[1,2,3]"

Decode JSON (returns Result):

import std/json (decode, Json, JObject, JString)

let result = decode("{\"name\":\"Alice\"}");
match result {
Ok(json) => print(show(json)),
Err(msg) => print("Parse error: " ++ msg)
}

Parse JSON array directly (IMPORTANT - use asArray!):

module benchmark/solution
import std/json (decode, asArray, getString)
import std/option (Some, None)
import std/result (Ok, Err)

-- When decode returns an array, use asArray to convert it
export func main() -> () ! {IO} {
let json = "[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]";
match decode(json) {
Ok(parsed) => match asArray(parsed) {
Some(items) => {
-- items is now [Json], iterate with recursion
printNames(items)
},
None => println("Not an array")
},
Err(e) => println("Parse error: " ++ e)
}
}

func printNames(items: [Json]) -> () ! {IO} =
match items {
[] => (),
item :: rest => {
match getString(item, "name") {
Some(name) => println(name),
None => ()
};
printNames(rest)
}
}

Access JSON values (IMPORTANT for parsing tasks):

import std/json (decode, get, has, getOr, asString, asNumber, asBool, asArray)
import std/option (Option, Some, None)

-- Decode and extract values
match decode("{\"name\":\"Alice\",\"age\":30}") {
Ok(obj) => {
-- get(obj, key) -> Option[Json]
match get(obj, "name") {
Some(j) => match asString(j) {
Some(name) => print("Name: " ++ name),
None => print("name is not a string")
},
None => print("no name field")
};
-- asNumber returns Option[float]
match get(obj, "age") {
Some(j) => match asNumber(j) {
Some(age) => print("Age: " ++ show(age)),
None => print("age is not a number")
},
None => print("no age field")
}
},
Err(msg) => print("Parse error: " ++ msg)
}

JSON convenience functions (RECOMMENDED for parsing):

Use these to reduce nested Option matching from 4 levels to 2:

import std/json (decode, getString, getNumber, getInt, getBool, getArray, getObject)
import std/string (intToStr)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)

-- Extract string field with single match
func getName(obj: Json) -> string =
match getString(obj, "name") {
Some(name) => name,
None => "unknown"
}

-- Extract int field (JSON numbers are floats, getInt truncates)
func getAge(obj: Json) -> int =
match getInt(obj, "age") {
Some(age) => age,
None => 0
}

-- Full example
export func main() -> () ! {IO} =
match decode("{\"name\":\"Alice\",\"age\":30}") {
Ok(obj) => print(getName(obj) ++ " is " ++ intToStr(getAge(obj))),
Err(e) => print("Error: " ++ e)
}

JSON accessor functions:

  • getString(obj, key) -> Option[string] - Get string value directly
  • getNumber(obj, key) -> Option[float] - Get float value directly
  • getInt(obj, key) -> Option[int] - Get int value (truncates float)
  • getBool(obj, key) -> Option[bool] - Get boolean value directly
  • getArray(obj, key) -> Option[List[Json]] - Get array value by key
  • getObject(obj, key) -> Option[Json] - Get nested object directly
  • asString(j) -> Option[string] - Convert Json to string
  • asNumber(j) -> Option[float] - Convert Json to number
  • asBool(j) -> Option[bool] - Convert Json to bool
  • asArray(j) -> Option[List[Json]] - Convert Json to array (USE THIS for top-level arrays!)

JSON array type extraction:

Extract typed arrays from JSON with two variants: permissive (skip non-matching) and strict (fail on mismatch).

import std/json (decode, getArray, filterStrings, allStrings, getStringArrayOrEmpty)
import std/option (Some, None)
import std/result (Ok, Err)

-- Permissive: filterStrings skips non-strings
let mixed = [JString("a"), JNumber(1.0), JString("b")];
filterStrings(mixed) -- ["a", "b"]

-- Strict: allStrings fails if ANY element is not a string
allStrings(mixed) -- None (has JNumber)
allStrings([JString("a"), JString("b")]) -- Some(["a", "b"])

-- Convenience wrappers for common patterns:
match decode("{\"tags\": [\"a\", \"b\"]}") {
Ok(obj) => {
-- getStringArrayOrEmpty: returns [] if missing or invalid
let tags = getStringArrayOrEmpty(obj, "tags"); -- ["a", "b"]
let missing = getStringArrayOrEmpty(obj, "nope"); -- []
print("Got " ++ show(length(tags)) ++ " tags")
},
Err(e) => print("Error: " ++ e)
}

HTTP Requests

Simple GET/POST:

import std/net (httpGet, httpPost)
let body = httpGet("https://example.com")
let resp = httpPost("https://api.example.com", jsonData)

Advanced with headers:

import std/net (httpRequest)
let headers = [{name: "Authorization", value: "Bearer token"}];
match httpRequest("POST", url, headers, body) {
Ok(resp) => print(resp.body),
Err(Transport(msg)) => print("Error: " ++ msg),
Err(_) => print("Other error")
}

AI Effect

Call external LLMs with a string->string interface:

import std/ai (call)

func ask_ai(question: string) -> string ! {AI} = call(question)

Run with providers:

ailang run --caps IO,AI --ai claude-haiku-4-5 --entry main file.ail  # Anthropic
ailang run --caps IO,AI --ai gpt5-mini --entry main file.ail # OpenAI
ailang run --caps IO,AI --ai gemini-2-5-flash --entry main file.ail # Google (uses ADC)
ailang run --caps IO,AI --ai-stub --entry main file.ail # Testing stub

Arrays (O(1) indexed access)

import std/array as A

let arr = #[10, 20, 30] -- Array literal
let val = A.get(arr, 0) -- O(1) access: 10
let safe = A.getOpt(arr, 99) -- None (out of bounds)
let arr2 = A.set(arr, 0, 99) -- O(n) copy: #[99, 20, 30]

List Operations

-- Recursive sum (no loops!)
func sum(xs: [int]) -> int =
match xs {
[] => 0,
x :: rest => x + sum(rest)
}

-- Recursive map
func map(f: int -> int, xs: [int]) -> [int] =
match xs {
[] => [],
x :: rest => f(x) :: map(f, rest)
}

List index access:

import std/list (nth, last, any, findIndex)
import std/option (Some, None)

let xs = [10, 20, 30];

-- nth: get element by index (0-based)
match nth(xs, 1) {
Some(x) => print("Element at 1: " ++ show(x)), -- 20
None => print("Index out of bounds")
};

-- last: get last element
match last(xs) {
Some(x) => print("Last: " ++ show(x)), -- 30
None => print("Empty list")
};

-- any: check if any element matches predicate
let hasEven = any(\x. x % 2 == 0, xs); -- true (20 is even)

-- findIndex: find index of first matching element
match findIndex(\x. x > 15, xs) {
Some(i) => print("First > 15 at index: " ++ show(i)), -- 1
None => print("Not found")
}

Testing

Inline tests on functions (recommended):

-- Tests are pairs of (input, expected_output)
pure func square(x: int) -> int tests [(0, 0), (5, 25)] { x * x }

pure func double(x: int) -> int tests [(0, 0), (3, 6), (5, 10)] { x * 2 }

Run: ailang test file.ail

Multi-Module Projects

myapp/
├── data.ail -- module myapp/data
├── storage.ail -- module myapp/storage
└── main.ail -- module myapp/main
-- myapp/data.ail
module myapp/data
export type User = { name: string, age: int }

-- myapp/main.ail
module myapp/main
import myapp/data (User)
export func main() -> () ! {IO} = print("Hello")

Run: ailang run --entry main --caps IO myapp/main.ail

Reserved Keywords

AILANG reserves 43 keywords. You cannot use these as variable or function names.

CategoryKeywords
Control Flowif, then, else, match, with, select, timeout
Definitionsfunc, pure, let, letrec, in
Type Systemtype, class, instance, forall, exists, deriving
Modulesmodule, import, export, extern
Testingtest, tests, property, properties, assert
Verificationrequires, ensures, invariant
Concurrencyspawn, parallel, channel, send, recv
Booleantrue, false, and, or, not

Common mistake:

-- WRONG: 'exists' is reserved (for existential types)
let exists = fileExists(path)

-- CORRECT: use alternative name
let found = fileExists(path)

Module Import Rules

Imports are NOT transitive. When module A imports module B, A does not get B's imports.

-- Module B
module myapp/db
import std/fs (readFile)
export func loadConfig() -> string ! {FS} = readFile("config.json")

-- Module A - WRONG: std/fs not available just because B uses it
module myapp/main
import myapp/db (loadConfig)
let data = readFile("other.txt") -- ERROR: module std/fs not imported

-- Module A - CORRECT: explicitly import what you use
module myapp/main
import std/fs (readFile)
import myapp/db (loadConfig)
let data = readFile("other.txt") -- Works!

Rule: Import everything you directly use in your module.

Common Mistakes

MistakeFix
print(42)print(show(42)) - print needs string
import "std/io"import std/io (println) - no quotes
list.map(f)map(f, list) - standalone function
for x in xsUse recursion or map/filter
Missing ! {IO}Add effect to signature
let x = 1; let y = 2 at top levelWrap in { } block
Multiple lets in match arm without { }Wrap in { } block (see Pattern Matching)
module benchmark/mynameUse exactly module benchmark/solution
Using reserved keyword as nameSee Reserved Keywords section above
Expecting transitive importsEach module must import what it uses directly

Running Programs

ailang run --entry main --caps IO file.ail           # IO only
ailang run --entry main --caps IO,FS file.ail # IO + File System
ailang run --entry main --caps IO,Net file.ail # IO + Network
ailang run --entry main --caps IO,AI --ai MODEL file.ail # IO + AI
ailang run --entry main --caps IO,SharedMem file.ail # IO + Semantic cache
ailang repl # Interactive REPL

Flags must come BEFORE the filename!