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 * 2orfunc(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!) NOTf()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+) orfuncType 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:
- Module declaration:
module benchmark/solution(first line) - Imports:
import std/io (println)(if using IO) - Functions:
export func main() -> () ! {IO} { ... }
IMPORTANT SYNTAX RULES:
- Use
funcNOTfn,function, ordef - Use
type Name[a] = Constructor(a) | Constructor2NOTtype Name { }orenum - 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!) NOTf()- 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 -> string → funcType 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, returnsResult[HttpResponse, NetError]- Headers are list of records:
[{name: string, value: string}] - JSON encoding: use
std/jsonfunctions (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.bodyif you actually need data from it - printingresp.statusis 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)- ReturnsResult[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)orErr(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 NonestringToFloat(s: string) -> Option[float]- Parse string to float, returns Some(f) or None- Both return
Option[T]fromstd/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/whileloops → use recursion - ❌ NO list comprehensions
[x*2 for x in list]→ usemapor recursion - ❌ NO tuple destructuring
let (x, y) = pair→ use record fields or pattern match in function - ❌ NO function type syntax
func(T) -> Uin 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"→ useimport std/io(no quotes!) - ❌ NO JSON literals
{"key": "value"}→ usejo([kv("key", js("value"))]) - ❌ NO
http.post(),http.get(),http()functions → usehttpRequest("POST", url, headers, body)fromstd/net - ❌ NO
json.encode(),JSON.parse(),parse_json()→ useencode(jo([...]))anddecode(jsonStr)fromstd/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→ uselet 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:
print→ NO IMPORT NEEDED in entry modules (modules withexport func main)!println→import std/io (println)(still requires import)readFile,writeFile→import std/fs (readFile, writeFile)now,sleep→import std/clock (now, sleep)httpGet,httpPost→import std/net (httpGet, httpPost)(simple wrappers - use for GET with no headers)httpRequest→import std/net (httpRequest)ONLY way to make HTTP POST/custom headers (no http.post()!)getEnv,hasEnv,getEnvOr→import 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:
| Module | Functions | Import Example |
|---|---|---|
std/io | print, println, readLine | import std/io (println) |
std/fs | readFile, writeFile, fileExists | import std/fs (readFile) |
std/net | httpRequest, httpGet, httpPost, httpFormPost | import std/net (httpRequest) |
std/json | encode, decode, jo, ja, kv, js, jnum | import std/json (encode, decode) |
std/env | getEnv, hasEnv, getEnvOr | import std/env (getEnv) |
std/clock | now, sleep | import std/clock (now) |
std/string | stringToInt, stringToFloat, length, toUpper, toLower, etc. | import std/string (stringToInt) |
std/prelude | show, comparison ops, type class instances | AUTO-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:
- Generates 100 random test cases per property
- Runs property on each case
- 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. bodyfor lambda syntax (NOTfunc(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
[]andx :: 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) andx :: 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 mainwith 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? Useprint(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
showis a builtin (don't import it)
This is v0.4.1 - Focus on: JSON Decode, Effect System, State Threading, Environment Variables