AILANG v0.5.11 - 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.
Required Program Structure
module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} {
print(show(42)) -- print expects string, use show() for numbers
}
Rules:
- First line:
module benchmark/solution - Use
print(show(value))for numbers,print(str)for strings - Inside
{ }blocks: uselet x = e;(semicolons). NEVER uselet x = e in! - No loops - use recursion
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
Quick Reference Examples
Print calculation:
module benchmark/solution
export func main() -> () ! {IO} = print(show(5 % 3))
HTTP GET:
module benchmark/solution
import std/net (httpGet)
export func main() -> () ! {IO, Net} = print(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))]));
print(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)
}
AI effect (call LLM):
module benchmark/solution
import std/ai (call)
export func main() -> () ! {IO, AI} = print(call("What is 2+2?"))
What AILANG Does NOT Have
| Invalid | Use Instead |
|---|---|
for/while loops | Recursion |
[x*2 for x in xs] | map(\x. x*2, xs) or recursion |
var/let mut | Immutable 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 curried | f(a)(b) - curried calls chain |
mixing let x = e in with ; | Use ONE style consistently |
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;
print(show(x + y))
}
-- Expression style: equals + in
export func main() -> () ! {IO} =
let x = 1 in
let y = 2 in
print(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
print(show(x + y))
}
Simple rule: See { -> use ;. See = -> use in.
Curried Functions (CRITICAL)
AILANG functions are curried. Multi-arg lambdas must be called with chained applications:
-- Define curried function
let add = \x. \y. x + y
-- CORRECT: Chain calls
let result = add(3)(4) -- Returns 7
-- WRONG: Don't use tuple-style
let result = add(3, 4) -- ERROR: arity mismatch!
Higher-order example (compose, foldl):
module benchmark/solution
export func main() -> () ! {IO} {
-- WRONG: let compose = ... in (don't use "in" in { } blocks!)
-- RIGHT: let compose = ...;
let compose = \f. \g. \x. f(g(x));
let double = \x. x * 2;
let addOne = \x. x + 1;
-- WRONG: compose(addOne, double) (arity mismatch!)
-- RIGHT: compose(addOne)(double) (chain curried calls)
let doubleThenAdd = compose(addOne)(double);
print(show(doubleThenAdd(5)))
}
Curried foldl (block style):
module benchmark/solution
-- Define curried foldl
func foldl(f: int -> int -> int, acc: int, xs: [int]) -> int =
match xs {
[] => acc,
x :: rest => foldl(f, f(acc)(x), rest) -- f(acc)(x) chains the curried call
}
export func main() -> () ! {IO} {
let add = \a. \b. a + b; -- Semicolons in block!
let sum = foldl(add, 0, [1,2,3,4,5]);
print(show(sum))
}
Syntax Reference
| Construct | Syntax |
|---|---|
| Module | module path/name |
| Import | import std/io (println) |
| Import alias | import std/list as L |
| Function | export func name(x: int) -> int ! {IO} { body } |
| Lambda | \x. x * 2 |
| Pattern match | match x { 0 => a, n => b } (use =>, commas between arms) |
| ADT | type Tree = Leaf(int) | Node(Tree, int, Tree) |
| Record | {name: "A", age: 30} |
| Record update | {base | field: val} |
| List cons | x :: 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:
-- Pure (no effects)
func add(x: int, y: int) -> int = x + y
-- IO effect
func greet(name: string) -> () ! {IO} = print("Hello " ++ name)
-- Multiple effects
func process(path: string) -> () ! {IO, FS} {
let content = readFile(path);
print(content)
}
| 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:
import std/io (println, readLine)
import std/fs (readFile, writeFile)
import std/net (httpGet, httpPost, httpRequest)
import std/json (encode, decode)
import std/list (map, filter, foldl, length, concat)
import std/string (split, trim, stringToInt) -- stringToInt returns Option[int]!
import std/ai (call)
import std/sem (make_frame_at, store_frame, load_frame, update_frame)
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) => print("Parsed: " ++ show(n)),
None => print("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);
print(msg)
}
Operators
| Type | Operators |
|---|---|
| Arithmetic | +, -, *, /, %, ** |
| Comparison | <, >, <=, >=, ==, != |
| Logical | &&, ||, ! |
| String | ++ (concatenation) |
| List | :: (cons/prepend) |
Pattern Matching
-- 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
}
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
Build and encode JSON:
import std/json (encode, jo, ja, kv, js, jnum)
-- Build JSON object
let json = encode(jo([kv("name", js("Alice")), kv("age", jnum(30.0))]))
-- Result: "{\"name\":\"Alice\",\"age\":30}"
-- 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)
}
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 accessor functions:
get(obj, key)->Option[Json]- Get value by keyhas(obj, key)->bool- Check if key existsgetOr(obj, key, default)->Json- Get with fallbackasString(j)->Option[string]- Extract stringasNumber(j)->Option[float]- Extract numberasBool(j)->Option[bool]- Extract booleanasArray(j)->Option[List[Json]]- Extract arrayasObject(j)->Option[List[{key: string, value: Json}]]- Extract object
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]
Semantic Caching Doctrine (SharedMem + SharedIndex)
AILANG programs should treat SharedMem + SharedIndex as the primary persistent working memory for agents.
When to Store
Cache any expensive or repeatable computation (LLM calls, parsing, summarizing, planning, search) as a sem_frame:
import std/sem (make_frame_at, store_frame, load_frame)
-- After expensive computation, cache it
let frame = make_frame_at("plan:" ++ goal_id, plan_text, plan_bytes, timestamp);
let _ = store_frame("plan:" ++ goal_id, frame)
When to Retrieve
Use SharedIndex for meaning-based retrieval. Do NOT guess keys or enumerate all keys unless implementing maintenance tooling:
-- Find similar content by SimHash (fast, deterministic)
let results = _sharedindex_find_simhash("beliefs", query_hash, 5, 100, true)
-- Find by neural embedding (semantic similarity via Ollama)
let query_emb = _ollama_embed("embeddinggemma", query_text);
let results = _sharedindex_find_by_embedding("beliefs", query_emb, 5, 100, true)
How to Evolve State
Use update_frame with CAS for conflict-free multi-agent updates:
import std/sem (update_frame, UpdateResult)
match update_frame("plan:" ++ goal_id, \frame. refine_plan(frame)) {
Updated(new_frame) => continue_with(new_frame),
Conflict(current) => retry_with_backoff(current),
Missing => create_initial_plan()
}
How to Debug
Enable tracing to see all SharedMem/SharedIndex operations:
ailang run --caps IO,SharedMem,SharedIndex --trace-sharedmem --entry main file.ail
Namespace Conventions
Organize frames by domain using namespace prefixes:
| Prefix | Purpose |
|---|---|
plan: | Planning state, goals, strategies |
belief: | Agent beliefs, world model |
doc: | Document summaries, parsed content |
cache: | Expensive computation results |
session: | Per-session temporary state |
Two-Tier Search
| Tier | Method | Use Case | Speed |
|---|---|---|---|
| Tier 1 | SimHash | Near-duplicates, typo tolerance | ~1ms |
| Tier 2 | Neural embedding | Semantic similarity, paraphrases | ~160ms |
Canonical pattern:
-- Check for exact/near match first (fast)
let simhash = _simhash(query_text);
let near_matches = _sharedindex_find_simhash("cache", simhash, 1, 50, true);
match near_matches {
[] => {
-- No near match: compute fresh, store result
let result = expensive_computation(query_text);
let frame = make_frame_at("cache:" ++ query_id, query_text, result, now());
let _ = store_frame("cache:" ++ query_id, frame);
result
},
[hit, ..._] => {
-- Found cached result
match load_frame(hit.key) {
Some(frame) => frame.opaque,
None => expensive_computation(query_text) -- Race: deleted between search and load
}
}
}
Embeddings Doctrine
If AI/Embedding capability is available (Ollama + EmbeddingGemma), use it to make semantic caching robust to paraphrase.
When to compute embeddings:
- Compute embeddings for frames that represent durable knowledge: document summaries, plans, policies, entity facts, extracted structured state.
- Avoid embedding ephemeral "chatty" content. Prefer stable canonical text.
What to embed:
- Embed the frame's
content, not the fullopaquepayload. contentshould be short, canonical, and discriminative (1-3 sentences plus key identifiers/constraints).
How to retrieve (hybrid pattern):
-- 1. SimHash candidates (fast, bounded)
let hash = _simhash(query);
let candidates = _sharedindex_find_simhash("beliefs", hash, 100, 1000, true);
-- 2. Embed query once
let qemb = _ollama_embed("embeddinggemma", query);
-- 3. Rerank with embedding search
let results = _sharedindex_find_by_embedding("beliefs", qemb, 5, 100, true);
How to store with embeddings:
import std/sem (make_frame_at, store_frame, with_embedding)
-- Compute embedding at store time (expensive, do once)
let emb_floats = _ollama_embed("embeddinggemma", content);
let emb_bytes = _embedding_encode(emb_floats);
let frame = with_embedding(make_frame_at(key, content, payload, ts), emb_bytes, 768);
let _ = store_frame(key, frame);
let _ = _sharedindex_upsert_emb("ns", key, frame.simhash, emb_floats, frame.ver, frame.ts)
Debugging embeddings:
- If a decision depends on embedding rerank, record: model name, candidate set size, similarity scores, and chosen key.
Running with SharedMem/SharedIndex
# Basic SharedMem (key-value cache)
ailang run --caps IO,SharedMem --entry main file.ail
# Full semantic retrieval (SimHash + embeddings)
ailang run --caps IO,SharedMem,SharedIndex --entry main file.ail
# With Ollama embeddings (requires: ollama serve && ollama pull embeddinggemma)
ailang run --caps IO,SharedMem,SharedIndex --entry main file.ail
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)
}
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
Common Mistakes
| Mistake | Fix |
|---|---|
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 xs | Use recursion or map/filter |
Missing ! {IO} | Add effect to signature |
let x = 1; let y = 2 at top level | Wrap in { } block |
match x { ... } inside block | Extract to helper function (parser bug) |
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!