Skip to main content

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:

  1. First line: module benchmark/solution
  2. Use print(show(value)) for numbers, print(str) for strings
  3. Inside { } blocks: use let x = e; (semicolons). NEVER use let x = e in!
  4. 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

InvalidUse Instead
for/while loopsRecursion
[x*2 for x in xs]map(\x. x*2, xs) or recursion
var/let mutImmutable let bindings
list.map()map(f, list)
import "std/io"import std/io (println)
{"key": "val"}jo([kv("key", js("val"))])
f(a, b) for curriedf(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

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

Effects (Side Effects Must Be Declared)

Every function performing I/O must declare effects:

-- 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)
}
EffectFunctionsImport
IOprint, println, readLinestd/io (print is builtin)
FSreadFile, writeFilestd/fs
NethttpGet, httpPost, httpRequeststd/net
EnvgetEnv, getEnvOrstd/env
AIcallstd/ai
Debuglog, checkstd/debug
SharedMem_sharedmem_get, _sharedmem_put, _sharedmem_casbuiltins
SharedIndex_sharedindex_upsert, _sharedindex_find_simhashbuiltins

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

TypeOperators
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 key
  • has(obj, key) -> bool - Check if key exists
  • getOr(obj, key, default) -> Json - Get with fallback
  • asString(j) -> Option[string] - Extract string
  • asNumber(j) -> Option[float] - Extract number
  • asBool(j) -> Option[bool] - Extract boolean
  • asArray(j) -> Option[List[Json]] - Extract array
  • asObject(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:

PrefixPurpose
plan:Planning state, goals, strategies
belief:Agent beliefs, world model
doc:Document summaries, parsed content
cache:Expensive computation results
session:Per-session temporary state
TierMethodUse CaseSpeed
Tier 1SimHashNear-duplicates, typo tolerance~1ms
Tier 2Neural embeddingSemantic 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 full opaque payload.
  • content should 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

MistakeFix
print(42)print(show(42)) - print needs string
import "std/io"import std/io (println) - no quotes
list.map(f)map(f, list) - standalone function
for x in xsUse recursion or map/filter
Missing ! {IO}Add effect to signature
let x = 1; let y = 2 at top levelWrap in { } block
match x { ... } inside blockExtract 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!