Skip to main content

AILANG v0.4.4 - AI Teaching Prompt (Pattern Sugar + Syntactic Sugar + JSON Decode + Environment Variables + Net Effects)

CRITICAL: Write code in AILANG syntax. NOT Python, NOT Rust, NOT JavaScript.

AILANG is a pure functional language with Hindley-Milner type inference and algebraic effects.

Quick Reference:

  • Module: module path/name (required first line)
  • Import: import std/io (println) (NO quotes on module path)
  • Function: export func name(x: int) -> int ! {IO} { body }
  • Lambda: \x. x * 2 or func(x: int) -> int { x * 2 }
  • Pattern: match x { 0 => a, n => b } (use => not : or ->, use commas between arms)
  • Effect: ! {IO, FS, Net, Env} after return type
  • Zero-arg call: f () (SPACE required!) NOT f() at top-level (v0.4.1: f() works in expressions)
  • ADT: type Tree = Leaf(int) | Node(Tree, int, Tree)
  • Record: {name: "A", age: 30} | Update: {base | field: val}
  • List: [] | x :: xs (sugar v0.4.1+) | match xs { [] => ..., x :: xs => ... } (sugar v0.4.4+) | ::(h, t) (canonical)
  • Type: int -> bool (sugar v0.4.1+) or funcType int bool (canonical)
  • Testing (v0.3.20+): test "name" = expr | property "name" (x: int) = expr
  • Sugar (v0.4.1+): :: cons (expressions + patterns v0.4.4+), -> arrow types, f() expr calls | Disable: --strict-syntax

Entry module prelude (v0.3.16+): print builtin available in entry modules (no import needed!)

Critical: What AILANG is NOT

DO NOT generate code like this - these will FAIL:

WRONG - Single-line statements without module:

PRINT 5 % 3

Every program MUST start with module benchmark/solution

WRONG - Imperative style with loops:

loop {
x = read();
if (x == "") { break; }
print(x);
}

AILANG has NO loop, break, while, for. Use recursion!

WRONG - Python/JavaScript syntax:

for i in range(10):
print(i)

This is not Python. Use functional recursion.

WRONG - Assignment statements:

x = 10;
x = x + 1;

No mutable variables. Use let bindings.

WRONG - Implicit global state:

total = 0  # Global variable

def add(x):
global total
total += x

AILANG has NO global mutable state. Thread state explicitly!

WRONG - C-style zero-argument calls:

getMessage()    -- ❌ Parse error!
render() -- ❌ Parse error!

AILANG uses ML-style syntax: f () (space required) NOT f()

✅ CORRECT - Functional AILANG:

module benchmark/solution

import std/io (println)

export func loop(n: int) -> () ! {IO} {
if n > 10
then ()
else {
println(show(n));
loop(n + 1)
}
}

export func main() -> () ! {IO} {
loop(1)
}

MANDATORY Structure

EVERY AILANG program MUST have this structure:

  1. Module declaration: module benchmark/solution (first line)
  2. Imports: import std/io (println) (if using IO)
  3. Functions: export func main() -> () ! {IO} { ... }

IMPORTANT SYNTAX RULES:

  • Use func NOT fn, function, or def
  • Use type Name[a] = Constructor(a) | Constructor2 NOT type Name { } or enum
  • NO namespace syntax (::), just use constructor names directly
  • Semicolons REQUIRED between statements in blocks
  • Pattern matching uses => NOT : or ->
  • NO for, while, var, const, let mut, or any imperative constructs
  • Zero-arg calls: Use f () (space!) NOT f() - empty parens without space are parse errors

What Works (v0.4.1)

Modules, functions, lambdas, pattern matching, ADTs, records, effects (! {IO, FS, Net, Env}), recursion, numeric conversions, HTTP with headers, JSON encoding/decoding, property-based testing, environment variables, syntactic sugar (::, ->, f()).

Entry-module prelude: print builtin (no import) | Auto-import: std/prelude (comparisons work without imports)

Syntactic Sugar (v0.4.1 - NEW!)

AILANG v0.4.1 introduces optional syntactic sugar for common patterns. These desugar to canonical forms during parsing.

S-CONS: Infix Cons Operator (::)

Sugar: x :: xs (infix cons for lists, works in both expressions AND patterns since v0.4.4) Canonical: ::(x, xs) (function call, still works everywhere) Right-associative: 1 :: 2 :: []::(1, ::(2, []))

✅ Both forms are equivalent: x :: xs::(x, xs) (bijective desugaring)

-- ✅ Sugar syntax works in expressions
let list = 1 :: 2 :: 3 :: []

-- ✅ Sugar syntax NOW WORKS in patterns (v0.4.4+)
match list {
[] => 0,
x :: xs => x + sumList(xs) -- Sugar form (NEW!)
}

-- ✅ Canonical form still works everywhere
match list {
[] => 0,
::(h, t) => h + sumList(t) -- Canonical form
}

-- ✅ Right-associative chaining works
match list {
a :: b :: c => a + b, -- Means: a :: (b :: c)
_ => 0
}

-- ✅ Mixed forms in same match (both are equivalent)
match list {
x :: [] => "singleton",
::(x, ::(y, [])) => "pair", -- Canonical
x :: y :: rest => "many", -- Sugar
_ => "other"
}

Note: In --strict-syntax mode, only the canonical form ::(x, xs) is allowed.

S-ARROWTYPE: Function Type Arrows (->)

Sugar: int -> bool (arrow type syntax) Canonical: funcType int bool (explicit type constructor) Right-associative: int -> bool -> stringfuncType int (funcType bool string)

-- Sugar syntax (v0.4.1+)
let f: int -> bool = \x. x > 0

-- Desugars to canonical form
let f: funcType int bool = \x. x > 0

-- Multi-argument function types
let add: int -> int -> int = \x. \y. x + y

S-CALL0: Zero-Argument Calls (f())

Sugar: f() (zero-arg call, works at both statement and expression level) Canonical: f (()) (function applied to unit)

-- ✅ WORKS: Expression context
let result = if ready() then compute() else 0

-- ✅ WORKS: Statement/top-level context (v0.4.1+)
main() -- Now works at top level!

-- ✅ ALSO WORKS: Canonical syntax (always available)
main (()) -- Explicit unit application

The lexer creates a UNIT token for () without spaces, enabling the sugar to work seamlessly at all levels.

Strict Syntax Mode

Disable all syntactic sugar with --strict-syntax flag or :strict REPL command:

# CLI usage
ailang run --strict-syntax module.ail
ailang check --strict-syntax module.ail
ailang repl --strict-syntax

# REPL usage
λ> :strict # Toggle strict mode
Strict syntax mode enabled
Syntactic sugar (::, ->, f()) will be rejected

λ> let list = 1 :: 2 :: []
Error: CONS sugar not allowed in strict mode
Suggestion: Use `::(x, xs)` (canonical syntax) instead of `x :: xs`

HTTP POST with JSON Example (Complete - Common Use Case!)

⚠️ Keep it simple: If you only need the HTTP status code, just print resp.status - don't parse resp.body unnecessarily.

HTTP POST with JSON payload and custom headers:

module benchmark/solution

import std/net (httpRequest)
import std/json (encode, jo, kv, js, jnum)

export func main() -> () ! {Net, IO} {
-- Build JSON body
let jsonBody = encode(jo([
kv("message", js("Hello from AILANG")),
kv("count", jnum(42.0))
]));

-- Custom headers
let headers = [
{name: "Content-Type", value: "application/json"},
{name: "X-Custom-Header", value: "myvalue"}
];

-- Make POST request
match httpRequest("POST", "https://httpbin.org/post", headers, jsonBody) {
Ok(resp) => print(show(resp.status)), -- ✅ Just print status!
Err(_) => print("error")
}
}

Key points:

  • httpRequest(method, url, headers, body) - 4 arguments, returns Result[HttpResponse, NetError]
  • Headers are list of records: [{name: string, value: string}]
  • JSON encoding: use std/json functions (jo, kv, js, jnum)
  • Pattern match on Result to handle success/error
  • Access response with resp.status, resp.body, resp.headers
  • Keep it simple: Only parse resp.body if you actually need data from it - printing resp.status is often enough

JSON Decoding with Pattern Matching (Separate Use Case)

⚠️ IMPORTANT: This is a SEPARATE pattern from HTTP requests. Don't combine them unless you actually need to parse response bodies!

Parsing JSON data and extracting values:

module benchmark/solution

import std/json (Json, JNull, JString, JNumber, JObject, decode)
import std/result (Result, Ok, Err)

-- Helper: Find a key in a JSON object
func findKey(kvs: List[{key: string, value: Json}], target: string) -> Json {
match kvs {
[] => JNull,
[kv, ...rest] => if _str_eq(kv.key, target) then kv.value else findKey(rest, target)
}
}

-- Example: Parse {"name":"Alice","age":30} and extract name
export func main() -> int {
let jsonStr = "{\"name\":\"Alice\",\"age\":30}";
let result = decode(jsonStr);
match result {
Ok(JObject(kvs)) => match findKey(kvs, "name") {
JString(s) => if _str_eq(s, "Alice") then 0 else 1,
_ => 2
},
Ok(_) => 3,
Err(_) => 4
}
}

Key points:

  • decode(jsonStr) - Returns Result[Json, string]
  • JSON ADT constructors: JNull, JBool(bool), JNumber(float), JString(string), JArray(List[Json]), JObject(List[{key: string, value: Json}])
  • Pattern match on Result first: Ok(json) or Err(msg)
  • Pattern match on Json constructors to extract values
  • JObject contains list of {key: string, value: Json} records
  • Use helper function to find keys in object

JSON ADT Constructors:

type Json =
| JNull
| JBool(bool)
| JNumber(float)
| JString(string)
| JArray(List[Json])
| JObject(List[{key: string, value: Json}])

Complete example with HTTP + JSON decode:

module benchmark/solution

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

-- Helper: Find key in JSON object
func findKey(kvs: List[{key: string, value: Json}], target: string) -> Json {
match kvs {
[] => JNull,
[kv, ...rest] => if _str_eq(kv.key, target) then kv.value else findKey(rest, target)
}
}

export func main() -> () ! {Net, IO} {
-- Make request
match httpRequest("GET", "https://api.example.com/user/123", [], "") {
Ok(resp) => {
-- Parse response body
match decode(resp.body) {
Ok(JObject(kvs)) => match findKey(kvs, "name") {
JString(name) => print("User: " ++ name),
_ => print("No name found")
},
Ok(_) => print("Invalid JSON format"),
Err(msg) => print("Parse error: " ++ msg)
}
},
Err(_) => print("HTTP error")
}
}

String Parsing with Option Types (v0.4.3+)

Safe parsing of user input to numbers:

module benchmark/solution

import std/string (stringToInt, stringToFloat)
import std/option (Option, Some, None, getOrElse)
import std/io (println)

-- Parse and validate age (reject negative)
pure func parseAge(input: string) -> Option[int] {
match stringToInt(input) {
Some(age) => if age >= 0 then Some(age) else None,
None => None
}
}

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

-- Invalid input
match stringToInt("abc") {
Some(n) => println("Should not happen"),
None => println("Correctly rejected 'abc'")
};

-- Float parsing
match stringToFloat("3.14") {
Some(f) => println("Pi: " ++ show(f)),
None => println("Invalid float")
};

-- With default value
let age = getOrElse(stringToInt("invalid"), 18);
println("Age with default: " ++ show(age));

()
}

Key points:

  • stringToInt(s: string) -> Option[int] - Parse string to integer, returns Some(n) or None
  • stringToFloat(s: string) -> Option[float] - Parse string to float, returns Some(f) or None
  • Both return Option[T] from std/option - NO exceptions!
  • Pattern match on Some/None to handle success/failure
  • Use getOrElse(option, default) for fallback values
  • Supports: signed numbers (+/-), scientific notation (1e-10), decimals
  • Rejects: empty strings, mixed alphanumeric, invalid formats

Common pattern - validation:

-- Validate positive numbers
pure func parsePositive(s: string) -> Option[int] {
match stringToInt(s) {
Some(n) => if n > 0 then Some(n) else None,
None => None
}
}

Critical Limitations

Syntax that does NOT exist (use alternatives):

  • ❌ NO for/while loops → use recursion
  • ❌ NO list comprehensions [x*2 for x in list] → use map or recursion
  • ❌ NO tuple destructuring let (x, y) = pair → use record fields or pattern match in function
  • ❌ NO function type syntax func(T) -> U in type annotations → use lambda values with inferred types
  • ✅ Haskell-style list cons works in patterns since v0.4.4: x :: xs (desugars to ::(x, xs))
  • ❌ NO quoted imports import "std/io" → use import std/io (no quotes!)
  • ❌ NO JSON literals {"key": "value"} → use jo([kv("key", js("value"))])
  • ❌ NO http.post(), http.get(), http() functions → use httpRequest("POST", url, headers, body) from std/net
  • ❌ NO json.encode(), JSON.parse(), parse_json() → use encode(jo([...])) and decode(jsonStr) from std/json
  • ❌ NO named parameters http.post(url=..., body=...) → AILANG uses positional parameters only
  • ❌ NO mutable variables var/let mut → everything immutable
  • ❌ NO assignment x = y → use let x = y in expr
  • ❌ NO error propagation ? → pattern match on Result
  • ❌ NO list spread [x, ...rest] in literals → use ::(h, t) in patterns
  • ❌ NO Python/JS method calls list.map(), list.append() → use standalone functions
  • DON'T overcomplicate → Keep solutions simple - only add complexity when actually needed
  • 🐛 Known parser bug: Match inside statement blocks has delimiter tracking issues → Workaround: Extract to helper function or use as final block expression (fix in progress)

Important: show is builtin (don't import). Higher-order functions use lambda values, not type annotations. Keep solutions focused - don't combine unrelated patterns unnecessarily.

IMPORT CHECKLIST - Read Before Writing Code!

std/prelude is AUTO-IMPORTED!

You NO LONGER need to import Ord or Eq for comparison operators:

module benchmark/solution

import std/io (println)

export func main() -> () ! {IO} {
if 5 > 3 then println("Works!") else () // Comparison operators auto-imported!
}

Common imports:

  • printNO IMPORT NEEDED in entry modules (modules with export func main)!
  • printlnimport std/io (println) (still requires import)
  • readFile, writeFileimport std/fs (readFile, writeFile)
  • now, sleepimport std/clock (now, sleep)
  • httpGet, httpPostimport std/net (httpGet, httpPost) (simple wrappers - use for GET with no headers)
  • httpRequestimport std/net (httpRequest) ONLY way to make HTTP POST/custom headers (no http.post()!)
  • getEnv, hasEnv, getEnvOrimport std/env (getEnv, hasEnv, getEnvOr) for environment variables (v0.4.0+)
  • JSON encoding → import std/json (encode, jo, ja, kv, js, jnum)
  • JSON decoding → import std/json (Json, JNull, JBool, JNumber, JString, JArray, JObject, decode) for parsing JSON (v0.4.1+)
  • String parsing → import std/string (stringToInt, stringToFloat) returns Option[T] (v0.4.3+)
  • show → builtin, DO NOT IMPORT
  • <, >, <=, >=, ==, !=AUTO-IMPORTED, NO IMPORT NEEDED!

Standard Library Module Reference:

ModuleFunctionsImport Example
std/ioprint, println, readLineimport std/io (println)
std/fsreadFile, writeFile, fileExistsimport std/fs (readFile)
std/nethttpRequest, httpGet, httpPost, httpFormPostimport std/net (httpRequest)
std/jsonencode, decode, jo, ja, kv, js, jnumimport std/json (encode, decode)
std/envgetEnv, hasEnv, getEnvOrimport std/env (getEnv)
std/clocknow, sleepimport std/clock (now)
std/stringstringToInt, stringToFloat, length, toUpper, toLower, etc.import std/string (stringToInt)
std/preludeshow, comparison ops, type class instancesAUTO-IMPORTED (no import needed)

⚠️ Common mistake: readFile is in std/fs, NOT std/io!

IMPORTANT: If you see error "No instance for Ord" or "No instance for Eq", you may have set AILANG_NO_PRELUDE=1. This is rare - std/prelude is auto-imported by default.

Module Structure

Every AILANG program must be a module with exported functions:

module benchmark/solution

import std/io (println)

export func main() -> () ! {IO} {
println("Hello, World!")
}

IMPORTANT: Always use module benchmark/solution as the module name for benchmark programs.

Testing (v0.3.20+)

Property-based testing with automatic counterexample shrinking

Unit Tests (Simple Assertions)

test "name" = boolean_expression

Examples:

test "addition works" = 1 + 1 == 2
test "list operations" = head([1, 2, 3]) == 1
test "string concat" = "hello" ++ " world" == "hello world"

Property Tests (QuickCheck-Style)

property "name" (param: type, ...) = boolean_expression

Examples:

// Commutativity
property "addition is commutative" (x: int, y: int) =
x + y == y + x

// Associativity
property "concat is associative" (a: string, b: string, c: string) =
(a ++ b) ++ c == a ++ (b ++ c)

// Identity
property "zero is identity" (x: int) =
x + 0 == x && 0 + x == x

Supported types: int, float, bool, string, list(T), Option(T), Result(T, E), custom ADTs

Running tests:

ailang test file.ail                    # Run all tests
ailang test file.ail --format json # JSON output for CI

How it works:

  1. Generates 100 random test cases per property
  2. Runs property on each case
  3. If failure found, shrinks to minimal counterexample

Higher-Order Functions (Lambda Values, NOT Type Annotations!)

CRITICAL: AILANG does NOT have func(T) -> U type syntax. Use lambda values instead!

Common use case: Compose, map, filter

module benchmark/solution

-- ❌ WRONG - func(b) -> c is NOT valid type syntax
export func compose[a, b, c](f: func(b) -> c, g: func(a) -> b) -> func(a) -> c {
func(x: a) -> c { f(g(x)) }
}

-- ✅ CORRECT - Use lambda values
export func main() -> () ! {IO} =
-- Compose: apply g, then f
let compose = \f. \g. \x. f(g(x)) in
let double = \x. x * 2 in
let addOne = \x. x + 1 in
let addOneThenDouble = compose(double)(addOne) in
let result = addOneThenDouble(5) in -- (5 + 1) * 2 = 12
print(show(result))

List operations with higher-order functions:

module benchmark/solution

export func main() -> () ! {IO} =
-- Map: apply function to each element
let map = \f. \xs.
match xs {
[] => [],
x :: rest => f(x) :: map(f)(rest) -- v0.4.4+ sugar syntax
} in

-- Filter: keep elements that satisfy predicate
let filter = \pred. \xs.
match xs {
[] => [],
x :: rest => -- v0.4.4+ sugar syntax
if pred(x)
then x :: filter(pred)(rest)
else filter(pred)(rest)
} in

let square = \x. x * x in
let isEven = \x. x % 2 == 0 in
let nums = [1, 2, 3, 4, 5] in
let squared = map(square)(nums) in
let evens = filter(isEven)(nums) in
print("Squared: " ++ show(squared) ++ ", Evens: " ++ show(evens))

Key points:

  • Use \param. body for lambda syntax (NOT func(param) -> Type)
  • Currying: \f. \g. \x. means function takes 3 arguments separately
  • No first-class function types - pass lambdas as values
  • Pattern match on lists with [] and x :: xs (sugar v0.4.4+) or ::(x, xs) (canonical)

List Operations (Recursion, NO Comprehensions!)

CRITICAL: AILANG has NO list comprehensions [x*2 for x in list]. Use recursion!

Common patterns: sum, length, filter

module benchmark/solution

-- Recursive sum
export func sum(xs: List[int]) -> int =
match xs {
[] => 0,
x :: rest => x + sum(rest) -- v0.4.4+ sugar syntax
}

-- Recursive length (polymorphic)
export func length[a](xs: List[a]) -> int =
match xs {
[] => 0,
_ :: rest => 1 + length(rest) -- v0.4.4+ sugar syntax
}

-- Recursive filter
export func filter[a](pred: a -> bool, xs: List[a]) -> List[a] =
match xs {
[] => [],
x :: rest => -- v0.4.4+ sugar syntax
if pred(x)
then x :: filter(pred, rest)
else filter(pred, rest)
}

export func main() -> () ! {IO} =
let nums = [1, 2, 3, 4, 5] in
let total = sum(nums) in
let count = length(nums) in
print("Sum: " ++ show(total) ++ ", Count: " ++ show(count))

Key points:

  • Use List[T] for type annotations (NOT [T] in function signatures)
  • Pattern match with [] (empty) and x :: xs (sugar v0.4.4+) or ::(x, xs) (canonical)
  • List literals [1, 2, 3] work in expressions, not type annotations
  • No list comprehensions - write recursive functions instead

NEW (v0.3.16): Entry-Module Prelude - print Builtin

Entry modules (modules with export func main) automatically get the print builtin - no import needed!

The print Builtin

Signature: print : string -> () ! {IO}

What it does: Prints a string to stdout with automatic newline (don't concatenate "\n"!)

Where it's available:

  • ✅ Entry modules (modules with export func main with 0 parameters)
  • ✅ REPL (interactive mode)
  • ❌ Library modules (modules without export func main)

Entry Module Examples

✅ CORRECT - Using print in entry module:

module benchmark/solution

-- NO import needed!
export func main() -> () ! {IO} {
print("Hello, World!")
}

✅ CORRECT - With show() for numbers:

module benchmark/solution

export func main() -> () ! {IO} {
let x = 42 in
print("Answer: " ++ show(x)) -- prints: Answer: 42
}

✅ CORRECT - Multiple prints:

module benchmark/solution

export func main() -> () ! {IO} = {
print("Line 1");
print("Line 2");
print("Line 3")
}

❌ WRONG - Don't concatenate newlines (print already adds one!):

module benchmark/solution

export func main() -> () ! {IO} = {
print("Fizz" ++ "\n") -- ❌ Creates double newlines!
}
-- Output: "Fizz\n\n" (blank line after each print)

✅ CORRECT - No manual newlines needed:

module benchmark/solution

export func main() -> () ! {IO} = {
print("Fizz") -- ✅ Automatic newline added
}
-- Output: "Fizz\n" (one newline only)

Library Module Pattern

❌ WRONG - print not available in libraries:

module mylib/utils

-- This will FAIL with "undefined variable: print"
export func greet(name: string) -> () ! {IO} {
print("Hello, " ++ name)
}

✅ CORRECT - Use println from std/io:

module mylib/utils

import std/io (println)

export func greet(name: string) -> () ! {IO} {
println("Hello, " ++ name)
}

Why This Matters

AI-First Philosophy: Entry modules are for simple scripts and benchmarks where print is the most common operation. Libraries should be explicit about their dependencies.

Rule of thumb:

  • Writing a benchmark/script with export func main? Use print (no import)
  • Writing a library module? Use import std/io (println)

Effect System - Explicit Side Effects

CRITICAL: AILANG tracks ALL side effects in function signatures!

Effect Syntax

-- Pure function (no effects)
export func add(x: int, y: int) -> int {
x + y
}

-- Function with IO effect
export func printSum(a: int, b: int) -> () ! {IO} {
println("Sum: " ++ show(a + b))
}

-- Function with FS effect
export func readCount(filename: string) -> string ! {FS} {
readFile(filename)
}

-- Function with MULTIPLE effects
export func logToFile(filename: string, message: string) -> () ! {IO, FS} {
println("Logging: " ++ message);
writeFile(filename, message)
}

-- Function with Net effect (simple)
export func fetchData(url: string) -> string ! {Net} {
httpGet(url)
}

-- Function with Net effect + custom headers (v0.3.9+)
export func postWithHeaders(url: string, body: string) -> string ! {Net, IO} {
let headers = [
{name: "Content-Type", value: "application/json"},
{name: "X-Custom-Header", value: "value"}
];
match httpRequest("POST", url, headers, body) {
Ok(resp) => resp.body,
Err(Transport(msg)) => {
println("Network error: " ++ msg);
""
},
Err(InvalidHeader(hdr)) => {
println("Invalid header: " ++ hdr);
""
},
Err(_) => ""
}
}

-- Function with Env effect (v0.4.0+)
export func getApiKey() -> string ! {Env} {
match getEnv("API_KEY") {
Ok(key) => key,
Err(NotFound(msg)) => "",
Err(NotAllowed(msg)) => ""
}
}

-- Function with Env effect + fallback (v0.4.0+)
export func getPort() -> int ! {Env} {
let portStr = getEnvOr("PORT", "8080");
-- Parse portStr to int (simplified)
8080
}

Effect Declaration Pattern: -> ReturnType ! {Effect1, Effect2}

Effect Composition Rules

Rule 1: Pure functions CANNOT call effectful functions

-- ❌ WRONG - pure function calling IO
export func compute() -> int {
println("computing..."); -- ERROR: IO effect not declared!
42
}

-- ✅ CORRECT - declare the effect
export func compute() -> int ! {IO} {
println("computing...");
42
}

Rule 2: Effects propagate upward

export func helper() -> () ! {IO} {
println("helper")
}

-- ✅ Must declare IO since helper uses IO
export func caller() -> () ! {IO} {
helper()
}

-- ❌ WRONG - missing IO effect
export func caller() -> () {
helper() -- ERROR: IO effect not declared!
}

Rule 3: Multiple effects must ALL be declared

-- Uses both FS (readFile) and IO (println)
export func processFile(path: string) -> () ! {IO, FS} {
let content = readFile(path);
println("Read: " ++ content)
}

Effect System Examples

Example 1: Pure computation with effect-ful output

module benchmark/solution

import std/io (println)

-- Pure function
export func double(x: int) -> int {
x * 2
}

-- Pure function
export func add(a: int, b: int) -> int {
a + b
}

-- Effectful function (uses IO)
export func printResult(label: string, value: int) -> () ! {IO} {
println(label ++ ": " ++ show(value))
}

-- Effectful function calling pure functions
export func compute_and_print() -> () ! {IO} {
let x = double(5);
let y = add(x, 3);
printResult("Result", y)
}

export func main() -> () ! {IO} {
compute_and_print()
}

Example 2: File operations with multiple effects

module benchmark/solution

import std/io (println)
import std/fs (readFile, writeFile)

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

-- FS effect only
export func readCount(filename: string) -> string ! {FS} {
readFile(filename)
}

-- IO effect only
export func printMessage(message: string) -> () ! {IO} {
println(message)
}

-- BOTH IO and FS effects
export func processUser(name: string, filename: string) -> () ! {IO, FS} {
let countStr = readCount(filename);
let message = formatMessage(name, 42);
printMessage(message)
}

export func main() -> () ! {IO, FS} {
processUser("Alice", "count.txt")
}

Example 3: Effect separation

module benchmark/solution

import std/io (println)

-- ✅ Pure function - can be tested without IO
export func calculateTotal(items: [int]) -> int {
match items {
[] => 0,
x :: xs => x + calculateTotal(xs) -- v0.4.4+ sugar syntax
}
}

-- ✅ Effectful function - handles IO separately
export func displayTotal(items: [int]) -> () ! {IO} {
let total = calculateTotal(items);
println("Total: " ++ show(total))
}

export func main() -> () ! {IO} {
displayTotal([1, 2, 3, 4, 5])
}

Example 4: Environment variables (v0.4.0+)

module benchmark/solution

import std/io (println)
import std/env (getEnv, hasEnv, getEnvOr)

-- Check if env variable exists
export func checkApiKey() -> () ! {Env, IO} {
if hasEnv("API_KEY")
then println("API_KEY is set")
else println("API_KEY not found")
}

-- Get env variable with error handling
export func getConfig() -> string ! {Env} {
match getEnv("CONFIG_PATH") {
Ok(path) => path,
Err(NotFound(msg)) => "/default/config.json",
Err(NotAllowed(msg)) => "/default/config.json"
}
}

-- Get env variable with fallback
export func getPort() -> string ! {Env} {
getEnvOr("PORT", "8080")
}

export func main() -> () ! {Env, IO} {
checkApiKey();
let config = getConfig();
let port = getPort();
println("Config: " ++ config ++ ", Port: " ++ port)
}

Common Effect Errors and Fixes

Error: "Effect mismatch"

-- ❌ Function declares IO but doesn't use it
export func compute() -> int ! {IO} {
42 -- No IO used!
}

-- ✅ Fix: Remove unnecessary effect
export func compute() -> int {
42
}

Error: "Missing IO effect"

-- ❌ Uses println but doesn't declare IO
export func greet(name: string) -> () {
println("Hello, " ++ name)
}

-- ✅ Fix: Add IO effect
export func greet(name: string) -> () ! {IO} {
println("Hello, " ++ name)
}

Error: "Missing FS effect"

-- ❌ Reads file but only declares IO
export func logFile(path: string) -> () ! {IO} {
let content = readFile(path);
println(content)
}

-- ✅ Fix: Add FS effect
export func logFile(path: string) -> () ! {IO, FS} {
let content = readFile(path);
println(content)
}

NEW: Explicit State Threading

AILANG has NO mutable global variables. State must be threaded explicitly through function parameters.

State Threading Pattern

❌ WRONG - Implicit global state (Python style):

total = 0  # Global mutable variable

def add(x):
global total
total += x
return total

✅ CORRECT - Explicit state threading (AILANG):

module benchmark/solution

import std/io (println)

-- Function takes state as parameter, returns (newState, result)
export func add(value: int, state: int) -> (int, int) {
let newState = state + value;
(newState, newState)
}

-- Thread state through multiple operations
export func demo() -> () ! {IO} {
let state0 = 0;
println("Initial: " ++ show(state0));

let r1 = add(5, state0);
match r1 {
(state1, result1) => {
println("After add(5): " ++ show(state1));

let r2 = add(10, state1);
match r2 {
(state2, result2) => println("After add(10): " ++ show(state2))
}
}
}
}

export func main() -> () ! {IO} {
demo()
}

State Threading Examples

Example 1: Counter with operations

module benchmark/solution

import std/io (println)

-- State threading: (value, state) -> (newState, newState)
export func add(value: int, state: int) -> (int, int) {
let newState = state + value;
(newState, newState)
}

export func multiply(value: int, state: int) -> (int, int) {
let newState = state * value;
(newState, newState)
}

export func main() -> () ! {IO} {
let state0 = 0;
println("Initial: " ++ show(state0));

-- add(5, 0) = (5, 5)
let r1 = add(5, state0);
match r1 {
(state1, _) => {
println("After add: " ++ show(state1));

-- multiply(3, 5) = (15, 15)
let r2 = multiply(3, state1);
match r2 {
(state2, _) => {
println("After multiply: " ++ show(state2));

-- add(10, 15) = (25, 25)
let r3 = add(10, state2);
match r3 {
(state3, _) => println("After add: " ++ show(state3))
}
}
}
}
}
}

Example 2: Accumulator pattern

module benchmark/solution

import std/io (println)

-- Process list element, updating accumulator
export func processItem(item: int, acc: int) -> int {
acc + item * 2
}

-- Thread accumulator through list
export func processAll(items: [int], acc: int) -> int {
match items {
[] => acc,
x :: xs => { -- v0.4.4+ sugar syntax
let newAcc = processItem(x, acc);
processAll(xs, newAcc)
}
}
}

export func main() -> () ! {IO} {
let items = [1, 2, 3, 4, 5];
let result = processAll(items, 0);
println("Result: " ++ show(result))
}

Functions

-- Simple pure function
export func add(x: int, y: int) -> int {
x + y
}

-- Function with effects (IO, FS)
export func greet(name: string) -> () ! {IO} {
println("Hello, " ++ name)
}

-- Generic function
export func identity[a](x: a) -> a {
x
}

-- Multi-statement body (SEMICOLONS REQUIRED!)
export func compute() -> int {
let x = 10;
let y = 20;
x + y
}

Anonymous Functions

Inline lambda expressions with familiar syntax:

-- Anonymous function literal
let double = func(x: int) -> int { x * 2 }

-- With type inference
let add = func(x, y) { x + y }

-- With effects
let greet = func(name: string) -> () ! {IO} {
println("Hello, " ++ name)
}

-- Higher-order functions
let apply = func(f: func(int) -> int, x: int) -> int { f(x) }
apply(func(n: int) -> int { n * 2 }, 5) -- 10

IMPORTANT: Use => for lambdas (\x. body), use -> { for func expressions.

Recursive Lambdas

The letrec keyword enables recursive function definitions in expressions:

-- Fibonacci using letrec
letrec fib = \n. if n < 2 then n else fib(n - 1) + fib(n - 2) in
fib(10) -- 55

-- Factorial
letrec factorial = \n. if n == 0 then 1 else n * factorial(n - 1) in
factorial(5) -- 120

Use case: Recursive functions in REPL or inside expressions without full function declarations.

Numeric Conversions

Explicit type conversion builtins for int ↔ float:

-- Int to Float
intToFloat(42) -- 42.0

-- Float to Int (truncates towards zero)
floatToInt(3.14) -- 3
floatToInt(-3.14) -- -3

-- Mixed arithmetic (requires conversion)
let result = intToFloat(1) + 2.5 -- 3.5

IMPORTANT: AILANG does NOT do automatic numeric coercion. You MUST use these builtins.

Block Expressions

Blocks allow sequencing statements with semicolons:

export func demo() -> () ! {IO} {
{
println("First");
println("Second");
println("Third")
}
}

-- Blocks are expressions - the last value is returned
export func compute() -> int {
{
let x = 10;
let y = 20;
x + y
}
}

CRITICAL: Semicolons REQUIRED between statements! Missing semicolons will cause parse errors.

Records (with Update Syntax!)

Records are structural types with named fields:

-- Record literal
let person = {name: "Alice", age: 30, city: "NYC"}

-- Field access
person.name -- "Alice"
person.age -- 30

-- Use in functions
export func describe(p: {name: string, age: int}) -> string {
p.name ++ " is " ++ show(p.age)
}

Record Update Syntax:

Functional record updates create a new record with specified fields changed:

let person = {name: "Alice", age: 30, city: "NYC"};

-- Update one field
let older = {person | age: 31};
-- Result: {name: "Alice", age: 31, city: "NYC"}

-- Update multiple fields
let moved = {older | city: "SF", age: 32};
-- Result: {name: "Alice", age: 32, city: "SF"}

-- Complex base expressions work
let updated = {getRecord() | field: value};
let nested = {config.server | port: 8080};

IMPORTANT: Updates are immutable - they create NEW records, leaving originals unchanged!

Immutable Data Structures Example

module benchmark/solution

import std/io (println)

export func formatPerson(p: {name: string, age: int, city: string}) -> string {
p.name ++ ", " ++ show(p.age) ++ ", " ++ p.city
}

export func main() -> () ! {IO} {
let original = {name: "Alice", age: 25, city: "NYC"};

-- Update age (creates NEW record)
let older = {original | age: 26};

-- Update city from ORIGINAL (not from older!)
let moved = {original | city: "SF"};

-- Print all three - original is unchanged!
println(formatPerson(original)); -- Alice, 25, NYC
println(formatPerson(older)); -- Alice, 26, NYC
println(formatPerson(moved)) -- Alice, 25, SF
}

Algebraic Data Types (ADTs)

✅ CORRECT AILANG SYNTAX:

-- Option type (Maybe)
type Option[a] = Some(a) | None

-- Result type (Either)
type Result[a, e] = Ok(a) | Err(e)

-- Tree type
type Tree = Leaf(int) | Node(Tree, int, Tree)

-- Status enum
type Status = Pending | InProgress | Completed

Multi-line ADTs (with optional leading pipe):

type Tree =
| Leaf(int)
| Node(Tree, int, Tree)

type Option[a] =
| Some(a)
| None

Exhaustive Pattern Matching

CRITICAL: All pattern matches must be exhaustive (handle all constructors)!

❌ WRONG - Non-exhaustive (missing cases):

type Status = Pending | InProgress | Completed

export func describe(status: Status) -> string {
match status {
Pending => "Waiting"
-- ❌ Missing InProgress and Completed!
}
}

✅ CORRECT - Exhaustive matching:

type Status = Pending | InProgress | Completed

export func describe(status: Status) -> string {
match status {
Pending => "Waiting to start",
InProgress => "Currently working",
Completed => "All done"
}
}

Pattern Matching Examples

Example 1: Option type (no crashes)

module benchmark/solution

import std/io (println)

type Option[a] = Some(a) | None

export func safeDivide(a: float, b: float) -> Option[float] {
if b == 0.0
then None
else Some(a / b)
}

export func describeResult(result: Option[float]) -> string {
match result {
Some(v) => "Result: " ++ show(v),
None => "Error: Division by zero"
}
}

export func main() -> () ! {IO} {
let r1 = safeDivide(10.0, 2.0);
println(describeResult(r1));

let r2 = safeDivide(10.0, 0.0);
println(describeResult(r2))
}

Example 2: Tree operations

module benchmark/solution

import std/io (println)

type Tree = Leaf(int) | Node(Tree, int, Tree)

export func treeSum(t: Tree) -> int {
match t {
Leaf(v) => v,
Node(l, v, r) => treeSum(l) + v + treeSum(r)
}
}

export func treeMax(t: Tree) -> int {
match t {
Leaf(v) => v,
Node(l, v, r) => {
let lmax = treeMax(l);
let rmax = treeMax(r);
if v >= lmax && v >= rmax
then v
else if lmax >= rmax
then lmax
else rmax
}
}
}

export func main() -> () ! {IO} {
let tree = Node(Leaf(1), 5, Node(Leaf(3), 7, Leaf(2)));
println("Sum: " ++ show(treeSum(tree)));
println("Max: " ++ show(treeMax(tree)))
}

Pattern Matching

Pattern matching is exhaustive and supports multiple patterns:

-- Option type matching
match result {
Some(v) => println("Got: " ++ show(v)),
None => println("No value")
}

-- List matching (v0.4.4+: sugar syntax supported!)
match xs {
[] => println("Empty"),
x :: xs => println("Head: " ++ show(x)) -- Sugar (v0.4.4+)
}

-- Alternative: Canonical form (still works)
match xs {
[] => println("Empty"),
::(x, xs) => println("Head: " ++ show(x)) -- Canonical
}

-- Tuple matching
match pair {
(x, y) => x + y
}

-- Nested patterns (v0.4.4+: sugar works!)
match xs {
a :: b :: c => a + b, -- Right-associative: a :: (b :: c)
_ => 0
}

-- Tree patterns
match tree {
Leaf(v) => v,
Node(Leaf(x), v, Leaf(y)) => x + v + y,
Node(l, v, r) => treeSum(l) + v + treeSum(r)
}

-- Guards
match x {
n if n < 0 => "negative",
n if n == 0 => "zero",
n => "positive"
}

Recursion (instead of loops)

Recursion works perfectly in v0.3.15:

export func factorial(n: int) -> int {
if n <= 1
then 1
else n * factorial(n - 1)
}

-- Recursion with IO effects and blocks
export func countdown(n: int) -> () ! {IO} {
if n <= 0 then {
println("Done!")
} else {
println(show(n));
countdown(n - 1)
}
}

NEW: Canonical List Operations

AILANG provides canonical ways to transform lists - use these patterns!

Map Pattern

Transform each element:

module benchmark/solution

import std/io (println)

-- Canonical map implementation
export func map[a, b](f: func(a) -> b, xs: [a]) -> [b] {
match xs {
[] => [],
x :: rest => f(x) :: map(f, rest) -- v0.4.4+ sugar syntax
}
}

export func main() -> () ! {IO} {
let numbers = [1, 2, 3, 4, 5];
let double = func(x: int) -> int { x * 2 };
let doubled = map(double, numbers);

-- Print result
println(show(doubled)) -- [2, 4, 6, 8, 10]
}

Filter Pattern

Keep elements matching predicate:

module benchmark/solution

import std/io (println)

-- Canonical filter implementation
export func filter[a](pred: func(a) -> bool, xs: [a]) -> [a] {
match xs {
[] => [],
x :: rest => -- v0.4.4+ sugar syntax
if pred(x)
then x :: filter(pred, rest)
else filter(pred, rest)
}
}

export func main() -> () ! {IO} {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let isEven = func(n: int) -> bool { n % 2 == 0 };
let evens = filter(isEven, numbers);

println(show(evens)) -- [2, 4, 6, 8, 10]
}

Fold Pattern

Reduce list to single value:

module benchmark/solution

import std/io (println)

-- Canonical fold (left fold)
export func foldl[a, b](f: func(b, a) -> b, acc: b, xs: [a]) -> b {
match xs {
[] => acc,
x :: rest => foldl(f, f(acc, x), rest) -- v0.4.4+ sugar syntax
}
}

export func main() -> () ! {IO} {
let numbers = [1, 2, 3, 4, 5];
let add = func(a: int, b: int) -> int { a + b };
let sum = foldl(add, 0, numbers);

println("Sum: " ++ show(sum)) -- Sum: 15
}

Complete Example: Filter + Sum

module benchmark/solution

import std/io (println)

export func filter[a](pred: func(a) -> bool, xs: [a]) -> [a] {
match xs {
[] => [],
x :: rest => -- v0.4.4+ sugar syntax
if pred(x)
then x :: filter(pred, rest)
else filter(pred, rest)
}
}

export func foldl[a, b](f: func(b, a) -> b, acc: b, xs: [a]) -> b {
match xs {
[] => acc,
x :: rest => foldl(f, f(acc, x), rest) -- v0.4.4+ sugar syntax
}
}

export func main() -> () ! {IO} {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let isEven = func(n: int) -> bool { n % 2 == 0 };
let add = func(a: int, b: int) -> int { a + b };

let evens = filter(isEven, numbers);
let sum = foldl(add, 0, evens);

println("Sum of evens: " ++ show(sum)) -- Sum of evens: 30
}

Common Mistakes to Avoid

Don't use for/while loops:

for i in [1, 2, 3] { println(i) }  -- ❌ NOT supported

Use recursion:

export func printAll(xs: [int]) -> () ! {IO} {
match xs {
[] => (),
x :: rest => { -- v0.4.4+ sugar syntax
println(show(x));
printAll(rest)
}
}
}

Don't forget semicolons in blocks:

{
println("First")
println("Second") -- ❌ Parse error! Missing semicolon
}

Add semicolons between statements:

{
println("First");
println("Second") -- ✅ Last statement doesn't need semicolon
}

Don't import show:

import std/io (println, show)  -- ❌ show not in std/io

show is a builtin:

import std/io (println)
-- show is available automatically
println(show(42))

Don't use invented HTTP APIs:

-- ❌ These DON'T exist in AILANG!
http.post(url, json=body, headers=h)
http("POST", url, body: json)
response = http.post(url, headers, body)

Use httpRequest from std/net:

import std/net (httpRequest)
import std/json (encode, jo, kv, js, jnum)

let jsonBody = encode(jo([kv("message", js("Hello")), kv("count", jnum(42.0))]));
let headers = [{name: "Content-Type", value: "application/json"}];
match httpRequest("POST", "https://httpbin.org/post", headers, jsonBody) {
Ok(resp) => print(show(resp.status)),
Err(_) => print("error")
}

Don't forget module and export:

func main() {  -- ❌ Missing 'module' declaration and 'export'
println("hello")
}

Always use module and export:

module benchmark/solution

import std/io (println)

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

Don't forget effect declarations:

-- ❌ Uses println but doesn't declare IO
export func greet() -> () {
println("Hello")
}

Declare all effects:

-- ✅ Declares IO effect
export func greet() -> () ! {IO} {
println("Hello")
}

Don't use mutable globals:

counter = 0  # ❌ No global state in AILANG

def increment():
global counter
counter += 1

Thread state explicitly:

export func increment(counter: int) -> int {
counter + 1
}

-- Call: let newCounter = increment(counter)

Don't use Haskell-style list cons:

-- ❌ Parse error - x::rest looks natural but doesn't work!
match xs {
[] => 0,
x::rest => x + sum(rest)
}

Use constructor syntax:

-- ✅ CORRECT - ::(x, rest) is canonical form
match xs {
[] => 0,
::(x, rest) => x + sum(rest)
}

-- ✅ ALSO CORRECT (v0.4.4+) - x :: xs is sugar form
match xs {
[] => 0,
x :: rest => x + sum(rest) -- Sugar syntax
}

Don't use quoted imports (from Python/JS):

-- ❌ Parse error - quotes not allowed!
import "std/io" (println)
import "std/net" (httpRequest)

Import without quotes:

-- ✅ CORRECT - no quotes on module path
import std/io (println)
import std/net (httpRequest)

Don't use func(T) -> U in type annotations:

-- ❌ Parse error - func(T) -> U not valid type syntax!
export func map[a, b](f: func(a) -> b, xs: [a]) -> [b] {
match xs {
[] => [],
::(x, rest) => ::(f(x), map(f, rest))
}
}

Use lambda values OR inferred types:

-- ✅ Option 1: Lambda values (let inference figure it out)
export func map[a, b](f, xs) {
match xs {
[] => [],
::(x, rest) => ::(f(x), map(f, rest))
}
}

-- ✅ Option 2: Use arrow syntax in type annotation
export func map[a, b](f: a -> b, xs: [a]) -> [b] {
match xs {
[] => [],
::(x, rest) => ::(f(x), map(f, rest))
}
}

Quick Reference

Essential patterns:

-- Module structure
module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} { ... }

-- Effects
export func pure() -> int { 42 }
export func withIO() -> () ! {IO} { println("hi") }
export func withFS() -> string ! {FS} { readFile("file.txt") }
export func withBoth() -> () ! {IO, FS} { ... }

-- State threading
export func stateful(value: int, state: int) -> (int, int) {
let newState = state + value;
(newState, newState)
}

-- ADTs
type Option[a] = Some(a) | None
match opt {
Some(v) => ...,
None => ...
}

-- Records
let r = {name: "Alice", age: 30};
let updated = {r | age: 31};

-- Lists
let xs = [1, 2, 3];
match xs {
[] => ...,
x :: rest => ... -- v0.4.4+ sugar syntax
}

-- Recursion
export func loop(n: int) -> () ! {IO} {
if n > 10 then () else {
println(show(n));
loop(n + 1)
}
}

Effect system quick reference:

  • Pure function: -> Type
  • IO only: -> Type ! {IO}
  • FS only: -> Type ! {FS}
  • Multiple effects: -> Type ! {IO, FS}
  • Net effect: -> Type ! {Net}

Remember:

  • Every program needs module benchmark/solution
  • Effects must be declared in signatures (! {IO, FS})
  • State must be threaded explicitly (no globals)
  • Pattern matches must be exhaustive
  • Use recursion instead of loops
  • Semicolons between statements in blocks
  • show is a builtin (don't import it)

This is v0.4.1 - Focus on: JSON Decode, Effect System, State Threading, Environment Variables