ailang
llms.txt38 starsChanged 6 days agoApache-2.0View on GitHub
AILANG is a deterministic, effect-typed programming language designed as a safe target for AI-generated programs. Side effects are declared in function signatures (! {IO, FS, Net}), execution is reproducible, and runs emit structured traces so agents can localize errors. This file bundles the AILANG documentation, README, language reference, and active teaching prompt for LLM consumption. Last updated: 2026-11-06T08:13:03Z Example status <!-- EXAMPLES_STATUS_END --> [](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=reliability_rating) [](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=security_rating) [](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=sqale_rating) [](https://github.com/sunholo-data/ailang/actions/workflows/codeql.yml) [](https://securityscorecards.dev/viewer/?uri=github.com/sunholo-data/ailang) [](https://www.bestpractices.dev/projects/12676) Third-party verification. AILANG is written autonomously by AI agents via its…
- Pipes a download into a shell
- Reads credentials
- Installs packages
- Sends data out
In this repository85 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- docs/llms.txtllms.txt
- agent-inboxSkill
- ailang-core-triageSkill
- ailang-packagesSkill
- benchmark-managerSkill
- benchmark-runnerSkill
- builtin-developerSkill
- cli-doc-maintainerSkill
- cloud-cascade-debugSkill
- cloud-setupSkill
- codebase-organizerSkill
- collaboration-hubSkill
- coordinator-helperSkill
- design-doc-creatorSkill
- design-spec-auditorSkill
- docs-syncSkill
- eval-analyzerSkill
- eval-gap-finderSkill
- gemini-cli-helperSkill
- github-issue-triageSkill
- headless-runnerSkill
- local-model-onboardingSkill
- local-ollama-evalSkill
- mission-briefSkill
- mission-controlSkill
- mission-loop-changeSkill
- model-managerSkill
- motoko-analyzerSkill
- parser-developerSkill
- post-releaseSkill
- pr-monitorSkill
- prompt-managerSkill
- release-managerSkill
- sonarcloud-triageSkill
- sprint-evaluatorSkill
- sprint-executorSkill
- sprint-plannerSkill
- test-coverage-guardianSkill
- trace-debuggerSkill
- use-ailangSkill
- wasm-debuggerSkill
- agent-inboxSkill
- ailang-core-triageSkill
- ailang-packagesSkill
- benchmark-managerSkill
- benchmark-runnerSkill
- builtin-developerSkill
- cli-doc-maintainerSkill
- cloud-cascade-debugSkill
- cloud-setupSkill
- codebase-organizerSkill
- collaboration-hubSkill
- coordinator-helperSkill
- design-doc-creatorSkill
- design-spec-auditorSkill
- docs-syncSkill
- eval-analyzerSkill
- eval-gap-finderSkill
- The first 60 shown.
The file
Show its rules and text
We split this llms.txt into the rules it gives an agent
33 rules. About 0 are common, 16 are distinctive, and 17 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈16
Distinctive · estimate
This author's own choices. Worth a look.
17
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 80
Start a new Codex session after installation.
- Very specific: line 253
All AILANG browser demos run on WebAssembly — try them live:
- Very specific: line 264
Standard Library Included: All 20 stdlib modules (std/list, std/json, etc.) embedded and auto-loaded
- Distinctive: line 265
Client-Side Execution: No server needed after initial load
- Distinctive: line 495
Check if a line needs continuation (for multi-line input UIs).
- Distinctive: line 588
Import a module's exports into the REPL environment.
- Distinctive: line 598
Returns: Import result message
- Distinctive: line 602
Call a function from a loaded module.
- Very specific: line 872
Call a module export asynchronously.
- Distinctive: line 1005
Every effect call writes a typed event to the cognitive event log:
- Distinctive: line 1154
Lazy Loading: Only load WASM when user navigates to playground
- Distinctive: line 1199
Solution: Use a modern browser:
- Distinctive: line 1208
Check browser console for network errors
- Very specific: line 1209
Verify ailang.wasm path is correct
- Very specific: line 1210
Ensure wasm_exec.js loaded first
- Distinctive: line 1211
Check CORS headers if serving from different domain
- Very specific: line 1215
Solution: Wait for init() promise or use onReady():
- Very specific: line 1239
Use async methods if handlers return Promises: await repl.evalAsync(...)
- Very specific: line 1240
Check that ailangSetAIHandler was called before AI.call() — it auto-grants the AI capability
- Very specific: line 1246
Solution: Check browser console for warnings during stdlib loading.
- Distinctive: line 1254
Solution: Add type annotations to function definitions:
- Distinctive: line 1279
Solution: Check the error message and fix the module code:
- Very specific: line 1382
No syntax-freeze decision is made here.
- Very specific: line 1396
Use ailang check examples/runnable/reserved_keyword_names.ail to check the corrected three-parameter call.
- Very specific: line 1411
⚠️ AILANG is NOT Python!: Do not use Python syntax like def, for, while, class, or if x:.
- Very specific: line 1425
Use println(show(value)) for numbers, println(str) for strings.
- Distinctive: line 1430
No loops — use recursion
- Very specific: line 1449
Use
printlnfor most output: Use print ONLY when building output on one line. - Very specific: line 1464
Use import std/io (readLineOpt) for loops that must distinguish a blank line from EOF: readLineOpt() -> Option[string] !
- Very specific: line 1497
Print only what the task asks for, nothing more: If the task wants 40, print 40 — never Result: 40, Answer: 40, or The result is 40.
- Distinctive: line 1506
When the task says *"print the result"* or *"print if they unify"*, it means print the bare value(s) it just defined — not a sentence …
- Distinctive: line 1509
Treat the expected output as a contract: emit exactly those characters and stop.
- Distinctive: line 1966
Fix: Add the missing effect to your function signature:
≈ “Common” is an estimate: at least 1 in 5 of the 3,592 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
# AILANG Documentation for LLMs
AILANG is a deterministic, effect-typed programming language designed as a safe target for AI-generated programs. Side effects are declared in function signatures (`! {IO, FS, Net}`), execution is reproducible, and runs emit structured traces so agents can localize errors.
This file bundles the AILANG documentation, README, language reference, and active teaching prompt for LLM consumption.
Last updated: 2026-11-06T08:13:03Z
---
# README
<p align="center">
<img src="docs/static/img/ailang-logo.svg" alt="AILANG Logo" width="128" height="128">
</p>
# AILANG: The Deterministic Language for AI Coders
<!-- EXAMPLES_STATUS_START -->


[Example status](https://ailang.sunholo.com/docs/examples)
<!-- EXAMPLES_STATUS_END -->
[](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=reliability_rating)
[](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=security_rating)
[](https://sonarcloud.io/component_measures?id=sunholo-data_ailang&metric=sqale_rating)
[](https://github.com/sunholo-data/ailang/actions/workflows/codeql.yml)
[](https://securityscorecards.dev/viewer/?uri=github.com/sunholo-data/ailang)
[](https://www.bestpractices.dev/projects/12676)
> **Third-party verification.** AILANG is written autonomously by AI agents via its own [coordinator](https://ailang.sunholo.com/docs/guides/coordinator). The badges above are independent static-analysis and supply-chain scores — not self-reported. The [benchmark dashboard](https://ailang.sunholo.com/docs/benchmarks/performance) publishes the live correctness signal across current benchmark and model cohorts.
AILANG is a purely functional, effect-typed language designed as a **deterministic execution substrate** for AI-generated code. Every construct has deterministic semantics that can be reflected, verified, and serialized.
**[Documentation](https://ailang.sunholo.com/)** | **[Examples](https://ailang.sunholo.com/docs/examples)** | **[Live Demos](https://www.sunholo.com/ailang-demos/)** | **[Vision](https://ailang.sunholo.com/docs/vision)** | **[Benchmarks](https://ailang.sunholo.com/docs/benchmarks/performance)**
---
## Quick Start
AILANG is designed to be used by AI coding agents. The
[AILANG Bootstrap](https://github.com/sunholo-data/ailang_bootstrap) packages the
current language guidance, reusable skills, and local MCP tools for Claude Code
and OpenAI Codex.
Install the AILANG CLI first:
```bash
curl -fsSL https://ailang.sunholo.com/install.sh | bash
ailang --version
```
The agent integrations call the local `ailang` executable, so it must be
available on `PATH`.
### With Claude Code
```text
/plugin marketplace add sunholo-data/ailang_bootstrap
/plugin install ailang@sunholo-data/ailang_bootstrap
```
### With OpenAI Codex
AILANG Bootstrap also ships a Codex plugin manifest. Add its stable marketplace:
```bash
# Required by the plugin's local MCP server on first launch
node --version
npm --version
codex plugin marketplace add sunholo-data/ailang_bootstrap --ref stable
codex plugin add ailang@ailang-marketplace
codex plugin list
```
Start a new Codex session after installation. Alternatively, after adding the
marketplace, launch `codex`, enter `/plugins`, and install `ailang` from the
AILANG marketplace. The plugin can also be installed from the Plugins directory
in the Codex desktop app. Codex plugins are not currently available in the IDE
extension.
Codex reads this repository's [`AGENTS.md`](AGENTS.md) automatically, whether or
not the plugin is installed. The plugin adds the reusable AILANG skills and MCP
tools; `AGENTS.md` supplies repository-specific workflows and guardrails.
### What the Agent Package Provides
- **CLI integration** - Skills and tools use the separately installed `ailang` executable
- **MCP tools** - `ailang_prompt`, `ailang_check`, `ailang_run`, `ailang_builtins`, `ailang_eval`
- **Agent skills** - AILANG authoring, debugging, messaging, design-doc, and sprint workflows
- **Teaching guidance** - Current syntax rules via repository guidance, skills, and `ailang prompt`
The packaging is intentionally native to each host:
| Capability | Claude Code | Codex |
|---|---|---|
| Persistent guidance | `CLAUDE.md` | `AGENTS.md` |
| Reusable workflows | Plugin skills | Plugin skills |
| Local AILANG MCP tools | Yes | Yes |
| Host-native commands | Claude slash commands | Skills and MCP tools |
| AILANG Bootstrap hooks | Claude hooks | Not currently packaged |
Once installed, ask the agent to write AILANG code. All agents should run
`ailang prompt` before authoring `.ail` files; it is the installed CLI's source
for current syntax and idioms.
See [ailang_bootstrap](https://github.com/sunholo-data/ailang_bootstrap) for details.
The installer detects your OS and architecture automatically. To pin a release:
```bash
curl -fsSL https://ailang.sunholo.com/install.sh | VERSION=v0.33.1 bash
```
<details>
<summary>Click to expand manual installation instructions</summary>
```bash
# macOS (Apple Silicon)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.arm64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/
# macOS (Intel)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/
# Linux
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/linux.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/
# From source
git clone https://github.com/sunholo-data/ailang.git
cd ailang && make install
# Verify
ailang --version
```
</details>
For complete setup instructions, see the [Getting Started Guide](https://ailang.sunholo.com/docs/guides/getting-started).
### Hello World
```ailang
module examples/hello_world
import std/io (println)
export func main() -> () ! {IO} {
println("Hello, World!")
}
```
```bash
ailang run --caps IO examples/hello_world.ail
# Output: Hello, World!
```
### Interactive REPL
```bash
ailang repl
λ> 1 + 2
3 :: Int
λ> let double = \x. x * 2 in double(21)
42 :: Int
λ> :type \x. x + x
\x. x + x :: ∀α. Num α ⇒ α → α
λ> :quit
```
---
## Key Features
- **Pure functional** - Lambda calculus, closures, pattern matching, ADTs
- **Type inference** - Hindley-Milner with row polymorphism
- **Effect system** - Capability-based security (IO, FS, Net, Clock, AI)
- **Deterministic** - Replayable execution, structured traces
- **AI-first** - Designed for machine reasoning, not human convenience
- **LSP** - `ailang lsp --stdio` ships in the binary: diagnostics, hover types, go-to-def, references, document symbols. One-command VS Code install: `ailang editor install vscode` ([guide](https://ailang.sunholo.com/docs/guides/lsp))
Learn more: [Why AILANG?](https://ailang.sunholo.com/docs/vision) | [No Loops Design](https://ailang.sunholo.com/docs/reference/no-loops) | [Go Interop](https://ailang.sunholo.com/docs/guides/go-interop) | [Stability Promise (1.x)](https://ailang.sunholo.com/docs/reference/stability)
---
## Development
```bash
make install # Build and install
make test # Run all tests
make repl # Start REPL
make lint # Run linter
```
**Guides:**
- [Development Guide](https://ailang.sunholo.com/docs/guides/development)
- [CONTRIBUTING.md](CONTRIBUTING.md)
- [AGENTS.md](AGENTS.md) - Cross-agent repository instructions (including Codex)
- [CLAUDE.md](CLAUDE.md) - Detailed AILANG operational workflows
---
## Project Structure
```
ailang/
├── cmd/ailang/ # CLI
├── internal/ # Compiler (lexer, parser, types, eval, effects)
├── std/ # Standard library (std/io, std/fs, std/json, std/zip, std/xml, etc.)
├── examples/ # Example and reference programs
├── docs/ # Documentation website source
└── design_docs/ # Design documents
```
---
## License
Apache 2.0 - See [LICENSE](LICENSE)
AILANG draws inspiration from Haskell, OCaml, Rust, and Idris/Agda.
---
*For AI agents: Deterministic functional language with Hindley-Milner type inference, algebraic effects, and explicit effect tracking. Read [AGENTS.md](AGENTS.md), then use `ailang prompt` for current language guidance.*
---
# Guide: wasm-integration.md
---
id: wasm-integration
title: WebAssembly Integration Guide
sidebar_label: WASM Integration
reviewBy: 2027-02-08
---
# WebAssembly Integration Guide
AILANG can run entirely in the browser using WebAssembly, enabling interactive demonstrations and online playgrounds without requiring server-side execution.
:::tip See WASM in Action
All AILANG browser demos run on WebAssembly — try them live:
- **[DocParse](https://www.sunholo.com/ailang-demos/docparse.html)** — Parse DOCX, PPTX, XLSX, PDF entirely in-browser
- **[Document Extractor](https://www.sunholo.com/ailang-demos/extractor.html)** — AI-powered extraction using WASM modules
- **[All 9 Demos](https://www.sunholo.com/ailang-demos/)** — Full demo gallery
:::
## Overview
The AILANG WebAssembly build provides:
- **Full Language Support**: Complete AILANG interpreter compiled to WASM
- **Standard Library Included**: All 20 stdlib modules (`std/list`, `std/json`, etc.) embedded and auto-loaded
- **Client-Side Execution**: No server needed after initial load
- **Small Bundle Size**: ~33MB uncompressed (~7MB with gzip)
- **React Integration**: Ready-made component for easy integration
- **Offline Capable**: Works offline after first load
## Quick Start
### Option 1: From Release (Recommended)
Download the pre-built WASM bundle from the latest release. All three files are version-matched and built from the same commit:
```bash
# Download and extract — includes ailang.wasm, wasm_exec.js, ailang-repl.js
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/ailang-wasm.tar.gz | tar -xz
```
The archive contains:
| File | Size | Description |
|------|------|-------------|
| `ailang.wasm` | ~33MB | Compiled WASM binary with embedded stdlib |
| `wasm_exec.js` | ~17KB | Go WASM runtime (version-matched to build toolchain) |
| `ailang-repl.js` | ~5KB | JavaScript wrapper class (`AilangREPL`) |
### Option 2: Build from Source
```bash
cd ailang
make build-wasm
# Copy wasm_exec.js from your Go installation
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
```
This produces `bin/ailang.wasm`. You'll also need [ailang-repl.js](https://github.com/sunholo-data/ailang/blob/main/web/ailang-repl.js) from the `web/` directory.
### Integration Options
#### Option A: Docusaurus (Recommended)
1. Copy assets:
```bash
cp ailang.wasm docs/static/wasm/
cp wasm_exec.js docs/static/wasm/
cp ailang-repl.js docs/src/components/
cp web/AilangRepl.jsx docs/src/components/
```
2. Add to `docusaurus.config.js`:
```javascript
module.exports = {
scripts: [
{
src: '/wasm/wasm_exec.js',
async: false,
},
],
// ... rest of config
};
```
3. Use in MDX:
```mdx
---
title: Try AILANG
---
import AilangRepl from '@site/src/components/AilangRepl';
<AilangRepl />
```
#### Option B: Vanilla HTML
```html
<!DOCTYPE html>
<html>
<head>
<title>AILANG REPL</title>
<script src="wasm_exec.js"></script>
<script src="ailang-repl.js"></script>
</head>
<body>
<div id="repl-container"></div>
<script>
const repl = new AilangREPL();
repl.init('/path/to/ailang.wasm').then(() => {
console.log('AILANG ready!');
// Evaluate expressions
const result = repl.eval('1 + 2');
console.log(result); // "3 :: Int"
});
</script>
</body>
</html>
```
#### Option C: React (Custom)
```jsx
import { useEffect, useState } from 'react';
import AilangREPL from './ailang-repl';
export default function MyReplComponent() {
const [repl, setRepl] = useState(null);
const [result, setResult] = useState('');
useEffect(() => {
const replInstance = new AilangREPL();
replInstance.init('/wasm/ailang.wasm').then(() => {
setRepl(replInstance);
});
}, []);
const handleEval = (input) => {
if (repl) {
const output = repl.eval(input);
setResult(output);
}
};
return (
<div>
<input onKeyDown={(e) => {
if (e.key === 'Enter') handleEval(e.target.value);
}} />
<pre>{result}</pre>
</div>
);
}
```
## JavaScript API
### `AilangREPL` Class
```javascript
const repl = new AilangREPL();
```
#### Methods
##### `init(wasmPath)`
Initialize the WASM module.
```javascript
await repl.init('/wasm/ailang.wasm');
```
**Parameters:**
- `wasmPath` (string): Path to `ailang.wasm` file
**Returns:** Promise that resolves when REPL is ready
##### `eval(input)`
Evaluate an AILANG expression.
```javascript
const result = repl.eval('1 + 2');
// Returns: "3 :: Int"
```
**Parameters:**
- `input` (string): AILANG code to evaluate
**Returns:** Result string (includes value and type)
##### `command(cmd)`
Execute a REPL command.
```javascript
const type = repl.command(':type \x. x');
// Returns: "\x. x :: a -> a"
```
**Parameters:**
- `cmd` (string): REPL command (e.g., `:type`, `:help`)
**Returns:** Command output string
##### `reset()`
Reset the REPL environment.
```javascript
repl.reset();
```
**Returns:** Status message
##### `onReady(callback)`
Register callback for when REPL is ready.
```javascript
repl.onReady(() => {
console.log('REPL initialized!');
});
```
##### `getVersion()`
Get version information.
```javascript
const version = repl.getVersion();
// Returns: "v0.7.2" (or null if not ready)
```
**Returns:** Version string or `null`
##### `getVersionInfo()`
Get detailed version information.
```javascript
const info = repl.getVersionInfo();
// Returns: { version: "v0.7.2", buildTime: "2026-02-06T...", platform: "js/wasm" }
```
**Returns:** Object with `version`, `buildTime`, `platform` fields, or `null`
##### `needsContinuation(line)`
Check if a line needs continuation (for multi-line input UIs).
```javascript
repl.needsContinuation('let x = 5 in'); // true
repl.needsContinuation('1 + 2'); // false
```
**Returns:** `boolean`
### Module Loading API (v0.7.1+)
The WASM REPL supports loading complete AILANG modules, enabling complex browser-based demos with multiple function definitions.
:::tip Why Module Loading?
The REPL evaluates expressions line-by-line, so function definitions like `let add = \x. \y. x + y` don't persist in scope for subsequent calls. The Module Loading API solves this by compiling entire modules and storing their exports for later use.
:::
##### `loadModule(name, code)`
Load an AILANG module into the registry.
```javascript
const result = repl.loadModule('math', `
let add: Int -> Int -> Int = \\x. \\y. x + y
let mul: Int -> Int -> Int = \\x. \\y. x * y
`);
if (result.success) {
console.log('Exports:', result.exports);
// Exports: ["add", "mul"]
} else {
console.error('Error:', result.error);
}
```
**Parameters:**
- `name` (string): Module name (e.g., `"math"`, `"invoice_processor"`)
- `code` (string): AILANG source code
**Returns:** Object with:
- `success` (boolean): Whether loading succeeded
- `exports` (string[]): List of exported function names (on success)
- `error` (string): Error message (on failure)
:::warning Type Annotations Required
For functions with numeric operations, you must include explicit type annotations:
```javascript
// ✅ Works
let add: Int -> Int -> Int = \\x. \\y. x + y
// ❌ Fails with "ambiguous type variable"
let add = \\x. \\y. x + y
```
:::
:::info Export Behavior (v0.7.1.2+)
Modules can use explicit exports or export all bindings:
**Explicit exports** (recommended for libraries):
```javascript
// Only public_func is exported, private_func is hidden
repl.loadModule('mylib', `
module mylib
export pure func public_func(x: int) -> int = x * 2
pure func private_func(x: int) -> int = x * 3
`);
// result.exports: ["public_func"]
```
**Implicit exports** (for REPL-style code):
```javascript
// All top-level bindings are exported
repl.loadModule('utils', `
let double: Int -> Int = \\x. x * 2
let triple: Int -> Int = \\x. x * 3
`);
// result.exports: ["double", "triple"]
```
:::
##### `listModules()` (v0.7.1+)
List all loaded modules.
```javascript
const modules = repl.listModules();
// Returns: ["math", "utils"]
```
**Returns:** Array of module names
##### `importModule(moduleName)` (v0.7.1+)
Import a module's exports into the REPL environment.
```javascript
repl.importModule('math');
// Now you can use: repl.eval('add(2)(3)')
```
**Parameters:**
- `moduleName` (string): Name of a loaded module
**Returns:** Import result message
##### `call(moduleName, funcName, ...args)`
Call a function from a loaded module. This is a convenience method that:
1. Looks up the function from the module registry
2. Converts JavaScript arguments to AILANG values
3. Invokes the function directly (no eval)
```javascript
// Load a module
repl.loadModule('math', `
let add: Int -> Int -> Int = \\x. \\y. x + y
let greet = \\name. "Hello, " <> name
`);
// Call functions
const sum = repl.call('math', 'add', 2, 3);
if (sum.success) {
console.log(sum.result); // "5 :: Int"
}
const greeting = repl.call('math', 'greet', 'World');
if (greeting.success) {
console.log(greeting.result); // "\"Hello, World\" :: String"
}
```
**Parameters:**
- `moduleName` (string): Module containing the function
- `funcName` (string): Function to call
- `...args` (any): Arguments (converted to AILANG values)
**Returns:** Object with:
- `success` (boolean): Whether the call succeeded
- `result` (string): Result with value and type (on success)
- `error` (string): Error message (on failure)
**Supported argument types:**
| JavaScript Type | AILANG Syntax |
|-----------------|---------------|
| `number` | `42` or `3.14` |
| `string` | `"text"` |
| `boolean` | `true` / `false` |
| `array` | `[1, 2, 3]` |
### Module Loading Example
Here's a complete example building an invoice processor demo:
```html
<!DOCTYPE html>
<html>
<head>
<title>Invoice Processor Demo</title>
<script src="wasm_exec.js"></script>
<script src="ailang-repl.js"></script>
</head>
<body>
<script>
const repl = new AilangREPL();
repl.init('/wasm/ailang.wasm').then(() => {
// Load the invoice processor module
const result = repl.loadModule('invoice', `
-- Invoice processing functions
let calculateTotal: Int -> Float -> Float = \\quantity. \\unitPrice.
int_to_float(quantity) * unitPrice
let applyDiscount: Float -> Float -> Float = \\total. \\discountPercent.
total * (1.0 - discountPercent / 100.0)
let formatCurrency: Float -> String = \\amount.
"$" <> float_to_string(amount)
`);
if (!result.success) {
console.error('Failed to load module:', result.error);
return;
}
console.log('Loaded exports:', result.exports);
// ["calculateTotal", "applyDiscount", "formatCurrency"]
// Process an invoice
const subtotal = repl.call('invoice', 'calculateTotal', 5, 19.99);
if (subtotal.success) {
console.log('Subtotal:', subtotal.result);
// "99.95 :: Float"
}
const total = repl.call('invoice', 'applyDiscount', 99.95, 10);
if (total.success) {
console.log('After 10% discount:', total.result);
// "89.955 :: Float"
}
// Or use eval directly after importing
repl.importModule('invoice');
const formatted = repl.eval('formatCurrency(89.95)');
console.log('Formatted:', formatted);
// "\"$89.95\" :: String"
});
</script>
</body>
</html>
```
### Effect Handlers API (v0.7.1+)
The WASM REPL supports configuring AILANG's algebraic effect system from JavaScript. This enables browser-based programs to use effects like AI, IO, Net, FS, and more by providing JavaScript callback implementations.
:::tip Why Effect Handlers?
AILANG tracks side effects in the type system. A function declared as `func ask(q: string) -> string ! {AI}` requires the `AI` capability to run. In the CLI, capabilities are granted via `--caps`. In the browser, you provide JavaScript implementations for each effect operation.
:::
#### Low-Level Globals
These functions are registered on the global `window` object when the WASM module loads:
| Global Function | Purpose |
|----------------|---------|
| `ailangSetEffectHandler(name, ops)` | Override any effect with JS callbacks |
| `ailangSetAIHandler(fn)` | Convenience wrapper for AI effect |
| `ailangGrantCapability(name)` | Grant a capability without a handler |
| `ailangEvalAsync(expr)` | Evaluate with async effect support |
| `ailangCallAsync(mod, fn, ...args)` | Call module function with async effects |
#### `AilangREPL` Wrapper Methods
The `AilangREPL` class wraps all low-level globals with error handling:
```javascript
const repl = new AilangREPL();
await repl.init('/wasm/ailang.wasm');
// Register effect handlers
repl.setEffectHandler('IO', 'print', (msg) => { output.textContent += msg; });
repl.setAIHandler(async (input) => { /* call LLM API */ });
repl.grantCapability('IO');
// Async evaluation (returns Promise)
const result = await repl.evalAsync('perform IO.print("hello")');
const output = await repl.callAsync('demo', 'processData', 'input');
```
:::info Wrapper vs Low-Level API
The wrapper methods (`repl.setEffectHandler()`) provide error handling and readiness checks. The low-level globals (`ailangSetEffectHandler()`) are called directly on `window` and skip these checks. For most use cases, prefer the wrapper methods.
:::
#### `ailangSetEffectHandler(effectName, handlers)`
Override any effect's operations with JavaScript callbacks. Auto-grants the named capability.
```javascript
// IO effect — route print/println to DOM
ailangSetEffectHandler("IO", {
print: (msg) => { output.textContent += msg; },
println: (msg) => { output.textContent += msg + '\n'; },
readLine: () => prompt("Enter input:")
});
// Net effect — use browser fetch
ailangSetEffectHandler("Net", {
httpRequest: async (method, url, headers, body) => {
const resp = await fetch(url, {
method,
headers: JSON.parse(headers),
body
});
return JSON.stringify({
status: resp.status,
body: await resp.text(),
ok: resp.ok
});
}
});
// Clock effect — use JS Date
ailangSetEffectHandler("Clock", {
now: () => Date.now(),
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms))
});
```
**Parameters:**
- `effectName` (string): Effect name matching the AILANG capability (e.g., `"IO"`, `"AI"`, `"Net"`)
- `handlers` (object): Map of operation names to JS functions
**Returns:** `{ success: true }` or `{ success: false, error: "..." }`
**Supported handler return types:**
- Synchronous values (string, number, boolean, null)
- Promises (resolved value is converted to AILANG)
#### `ailangSetAIHandler(callback)`
Convenience wrapper for the AI effect. Sets both the effect handler and the dedicated `AIHandler` on the effect context. This is the recommended way to enable `std/ai.call()` in the browser.
```javascript
// Connect to Gemini Flash API
ailangSetAIHandler(async function(input) {
const resp = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: input }] }]
})
}
);
const data = await resp.json();
return data.candidates[0].content.parts[0].text;
});
```
**Parameters:**
- `callback` (function): JS function that takes a string and returns a string (or Promise\<string\>)
**Returns:** `{ success: true }` or `{ success: false, error: "..." }`
:::info Auto-Grant
`ailangSetAIHandler` automatically grants the `AI` capability and registers the handler in the effect registry. You don't need to call `ailangGrantCapability("AI")` separately.
:::
#### `ailangGrantCapability(name)`
Grant a named capability without providing a handler. Useful when the effect operations are already registered (e.g., built-in effects that work in WASM).
```javascript
ailangGrantCapability("IO");
ailangGrantCapability("Debug");
```
**Parameters:**
- `name` (string): Capability name
**Returns:** `{ success: true }` or `{ success: false, error: "..." }`
#### Valid Capabilities
| Capability | Description | Common Operations |
|------------|-------------|-------------------|
| `IO` | Console/display I/O | `print`, `println`, `readLine` |
| `FS` | File system access | `readFile`, `writeFile`, `exists` |
| `Net` | Network requests | `httpGet`, `httpPost`, `httpRequest` |
| `AI` | AI model completion | `complete` |
| `Clock` | Time operations | `now`, `sleep` |
#### `ailangEvalAsync(expression)`
Evaluate an AILANG expression asynchronously. Returns a JavaScript Promise. **Required when the expression may trigger effect handlers that return Promises** (e.g., AI calls, network requests).
```javascript
// Sync handler — ailangEval() works fine
ailangSetAIHandler((input) => "mock: " + input);
const result = repl.eval('import std/ai as AI in AI.call("hello")');
// Async handler — MUST use ailangEvalAsync
ailangSetAIHandler(async (input) => {
const resp = await fetch('/api/ai', { method: 'POST', body: input });
return await resp.text();
});
const result = await ailangEvalAsync('import std/ai as AI in AI.call("hello")');
```
**Parameters:**
- `expression` (string): AILANG expression or REPL command
**Returns:** Promise\<string\> — resolves with result string, rejects on error
#### `ailangCallAsync(moduleName, funcName, ...args)`
Call a module export asynchronously. Returns a JavaScript Promise. Same as `repl.call()` but supports async effect handlers.
```javascript
// Load a module that uses AI
repl.loadModule('demo', `
module demo
import std/ai as AI
export func askAI(question: string) -> string ! {AI} =
AI.call(question)
`);
// Call with async AI handler
ailangSetAIHandler(async (input) => {
// ... fetch from LLM API
return "AI response";
});
const result = await ailangCallAsync('demo', 'askAI', 'What is 2+2?');
// result: { success: true, result: '"AI response" :: String' }
```
**Parameters:**
- `moduleName` (string): Module containing the function
- `funcName` (string): Function to call
- `...args` (any): Arguments (number, string, boolean)
**Returns:** `Promise<{ success, result?, error? }>`
### Effect Handler Example
Complete example using AI and IO effects in the browser:
```html
<!DOCTYPE html>
<html>
<head>
<title>AILANG Effects Demo</title>
<script src="wasm_exec.js"></script>
<script src="ailang-repl.js"></script>
</head>
<body>
<div id="output"></div>
<script>
const repl = new AilangREPL();
const output = document.getElementById('output');
repl.init('/wasm/ailang.wasm').then(async () => {
// Set up IO effect — route output to DOM
ailangSetEffectHandler("IO", {
print: (msg) => { output.textContent += msg; },
println: (msg) => { output.textContent += msg + '\n'; }
});
// Set up AI effect — mock handler for demo
ailangSetAIHandler((input) => {
return "The answer is 42";
});
// Load a module that uses both effects
repl.loadModule('demo', `
module demo
import std/ai as AI
export func greet(name: string) -> () ! {IO} =
println("Hello, ${name}!")
export func ask(question: string) -> string ! {AI} =
AI.call(question)
`);
// Call IO function (sync is fine for sync handlers)
repl.call('demo', 'greet', 'World');
// DOM shows: "Hello, World!"
// Call AI function
const answer = await ailangCallAsync('demo', 'ask', 'What is the meaning of life?');
console.log(answer.result);
// '"The answer is 42" :: String'
});
</script>
</body>
</html>
```
### Complete Method Reference
| Method | Returns | Since | Description |
|--------|---------|-------|-------------|
| `init(wasmPath)` | `Promise<this>` | v0.7.0 | Initialize WASM module |
| `eval(input)` | `string` | v0.7.0 | Evaluate expression |
| `command(cmd)` | `string` | v0.7.0 | Execute REPL command |
| `reset()` | `string` | v0.7.0 | Reset environment + reload stdlib |
| `getVersion()` | `string` or `null` | v0.7.0 | Get version string |
| `getVersionInfo()` | `Object` or `null` | v0.7.0 | Get version, buildTime, platform |
| `needsContinuation(line)` | `boolean` | v0.7.0 | Check if line needs more input |
| `onReady(callback)` | `void` | v0.7.0 | Register ready callback |
| `loadModule(name, code)` | `{success, exports?, error?}` | v0.7.1 | Compile and register module |
| `listModules()` | `string[]` | v0.7.1 | List loaded module names |
| `call(mod, func, ...args)` | `{success, result?, error?}` | v0.7.1 | Call module export |
| `importModule(name)` | `string` | v0.7.1 | Import into REPL env |
| `setEffectHandler(cap, op, fn)` | `{success, error?}` | v0.7.2 | Register effect handler |
| `setAIHandler(fn)` | `{success, error?}` | v0.7.2 | Register AI handler |
| `grantCapability(cap)` | `{success, error?}` | v0.7.2 | Grant effect capability |
| `evalAsync(input)` | `Promise<string>` | v0.7.2 | Async eval (for effects) |
| `callAsync(mod, func, ...args)` | `Promise<{success, result?, error?}>` | v0.7.2 | Async call (for effects) |
## Cognitive OS Substrate (v0.21.x)
The WASM build ships the **Cognitive OS substrate** — a deterministic agent runtime layered on top of the standard WASM REPL. Agents declare effects, mutate scoped DOM regions, exchange messages, and produce replayable event logs.
The Go-side substrate is complete and shippable (M-COG-RUNTIME, see [design doc](https://github.com/sunholo-data/ailang/blob/dev/design_docs/implemented/v0_21_0/m-cog-runtime.md)). The browser-side host JS that lights up the bridges is a follow-up sprint ([M-COG-RUNTIME-BROWSER](https://github.com/sunholo-data/ailang/blob/dev/design_docs/planned/v0_21_0/m-cog-runtime-browser.md)) — once it ships, the JS APIs below become directly usable end-to-end.
### Effects shipped
| Effect | Purpose | Stdlib module |
|--------|---------|----------------|
| `DOM` | Scoped, structured browser-DOM mutation (no raw HTML/JS injection) | `std/dom` |
| `Msg` | Inter-agent messaging fabric — same code over LocalWorker / BroadcastChannel | `std/cognition` |
| `Trace` | Cognitive event emission (already shipped v0.11.1; extension lands in M-COG-RUNTIME-BROWSER) | `std/trace` |
### Step-pattern interface
DOM and Msg handlers follow the [step-pattern interface](https://github.com/sunholo-data/ailang/blob/dev/internal/effects/ai_step.go) established by AI:
- **DOMHandler**: `ApplyPatch` / `ApplyBatch` / `Subscribe`
- **MsgHandler**: `Send` / `Recv` / `Subscribe`
Each handler ships with a deterministic `Stub*Handler` for testing and Result-returning op variants (`applyPatchResult` etc.) that produce typed `Result[T, {code, message}]` for graceful AILANG-side error handling.
### Cognitive event log
Every effect call writes a typed event to the cognitive event log:
| Event kind | Source |
|------------|--------|
| `MessageSent` | `Msg.send` |
| `MessageReceived` | `Msg.recv` |
| `PatchApplied` | `DOM.applyPatch` / `DOM.applyBatch` |
| `CapabilityExceeded` | Budget overrun (M-COG-RUNTIME-BROWSER wires this) |
| `TraceCaptured` | `Trace` span completion (M-COG-RUNTIME-BROWSER ext) |
The log exports to JSONL for deterministic replay across machines. The `Replayer` re-feeds events through a fresh scheduler in canonical `(LamportClock, Sender)` order and produces byte-identical dispatch slices — the "deterministic distributed replay" property the [umbrella design doc](https://github.com/sunholo-data/ailang/blob/dev/design_docs/planned/v0_21_0/m-wasm-reflective-runtime.md) names as the strategic claim of the Cognitive OS.
### JavaScript API (Cognitive OS bridges)
The browser-side host JS registers callbacks for each effect; the WASM bridge invokes them from Go:
```javascript
// DOM
ailangSetDOMApplyPatchHandler((region, patch) => {
// patch = {ctor: "AddPanel", fields: ["title", "content"]} | UpdateNode | RemoveNode | AddTimeline
// Apply to <div data-cog-region="${region}"> and return {node_id, budget_remaining}
});
ailangSetDOMApplyBatchHandler((region, patches) => { /* ... */ });
// Msg (transport-independent)
ailangSetMsgSendHandler((to, payload) => {
// postMessage on BroadcastChannel(to) or send over WebSocket
return {msg_id, clock, budget_remaining};
});
ailangSetMsgRecvHandler((mailbox) => {
// Pull the next envelope; can return a Promise for async transports
return {msg_id, from, to, payload, clock};
});
```
### Example: replayable DOM mutation
```ailang
module myagent
import std/dom (DOMPatch, AddPanel, applyPatchResult)
import std/cognition (sendMsgResult)
import std/result (Result, Ok, Err)
export func renderHeatmap(region: string) -> string ! {DOM} = {
match applyPatchResult(region, AddPanel("Status", "Running...")) {
Ok(r) => "node:${r.node_id}",
Err(e) => "failed:${e.code}"
}
}
export func relay(to: string, payload: string) -> string ! {Msg} = {
match sendMsgResult(to, payload) {
Ok(r) => "sent:${r.msg_id}@${show(r.clock)}",
Err(e) => "send_failed:${e.code}"
}
}
```
A full runnable demo lives at [`examples/cognitive_os/single_agent_replay.ail`](https://github.com/sunholo-data/ailang/blob/dev/examples/cognitive_os/single_agent_replay.ail).
### Status
| Component | Status |
|-----------|--------|
| DOM / Msg effect labels in type system | ✅ Shipped (v0.21.x) |
| Step-pattern handler interfaces + stubs | ✅ Shipped |
| Result-returning op variants | ✅ Shipped |
| AILANG stdlib bindings (`std/dom`, `std/cognition`) | ✅ Shipped |
| WASM bridges (8 JS globals: `ailangSetDOM*Handler`, `ailangSetMsg*Handler`) | ✅ Shipped |
| Lamport clock + cognitive event log + JSONL replay | ✅ Shipped |
| Transport trait + LocalWorker + BroadcastChannel | ✅ Shipped |
| Deterministic scheduler + replay engine | ✅ Shipped |
| `NativeMsgHandler` wrapping Transport (non-WASM) | ✅ Shipped |
| Browser host JS (`host.js`, `replay.js`, `canonical_dom.js`) | ⏳ M-COG-RUNTIME-BROWSER |
| IndexedDB persistence sink | ⏳ M-COG-RUNTIME-BROWSER |
| Subscribe ops wired through `FnCaller` to AILANG closures | ⏳ M-COG-RUNTIME-BROWSER |
| `!: SharedMem` + `!: SemanticSearch` (semantic memory) | ⏳ M-COG-MEMORY (v0.22.0) |
| WebSocket / FirestoreRelay / WebRTC transports | ⏳ M-COG-MESH (v0.22.x → v0.23) |
| Collaborative 4-agent demo | ⏳ M-COG-MESH |
## REPL Commands
The WebAssembly REPL supports the same commands as the CLI:
| Command | Description |
|---------|-------------|
| `:help` | Show available commands |
| `:type <expr>` | Display expression type |
| `:instances` | Show type class instances |
| `:reset` | Clear environment |
## Limitations
The browser version has these limitations compared to the CLI:
| Feature | CLI | WASM |
|---------|-----|------|
| Expression evaluation | Yes | Yes |
| Type inference | Yes | Yes |
| Pattern matching | Yes | Yes |
| Type classes | Yes | Yes |
| Module loading | Yes | Yes (v0.7.1+) |
| Standard library | Yes | Yes (v0.7.1+)* |
| Explicit exports | Yes | Yes (v0.7.1.2+) |
| Effect handlers | Yes (`--caps`) | Yes (v0.7.1+, via JS callbacks) |
| AI effect (`std/ai`) | Yes | Yes (v0.7.1+, via `ailangSetAIHandler`) |
| IO effect (print/read) | Yes (terminal) | Yes (v0.7.1+, via JS callbacks) |
| Net effect (HTTP) | Yes | Via JS callbacks (CORS applies) |
| FS effect (files) | Yes (filesystem) | Via JS callbacks (localStorage/IndexedDB) |
| Async evaluation | N/A | Yes (v0.7.1+, `ailangEvalAsync`) |
| Custom file imports | Yes | No (use `loadModule()`) |
| History persistence | Yes | No |
\* The standard library (20 modules including `std/list`, `std/json`, `std/string`) is embedded in the WASM binary and auto-loaded on init. Use `importModule('std/list')` to bring stdlib exports into scope. Custom user file imports require `loadModule()` instead.
## Deployment
### Static Hosting
WASM files work on any static host:
```bash
# Build and deploy
make build-wasm
cp bin/ailang.wasm your-site/static/wasm/
# Deploy your-site/ to Netlify/Vercel/GitHub Pages
```
### CDN Optimization
1. **Enable Compression:**
```nginx
# nginx.conf
gzip_types application/wasm;
```
2. **Set Cache Headers:**
```nginx
location ~* \.wasm$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
```
3. **Use HTTP/2:**
WASM benefits from HTTP/2 multiplexing for faster loading.
### Performance Tips
- **Lazy Loading**: Only load WASM when user navigates to playground
- **Service Worker**: Cache WASM for offline use
- **CDN**: Serve from edge locations
- **Preload**: Add `<link rel="preload" href="ailang.wasm" as="fetch">`
## CI/CD Integration
### GitHub Actions
The release workflow automatically builds and bundles the WASM archive with all dependencies:
```yaml
# .github/workflows/release.yml (excerpt)
- name: Build WASM binary
run: make build-wasm
- name: Bundle WASM archive with JS dependencies
run: |
# Get version-matched wasm_exec.js from Go toolchain
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" bin/
cp web/ailang-repl.js bin/
cd bin && tar -czf ailang-wasm.tar.gz ailang.wasm wasm_exec.js ailang-repl.js
```
The archive is verified (size checks for all 3 files) and attached to the GitHub Release as `ailang-wasm.tar.gz`.
### Docusaurus Deployment
WASM is included in documentation builds:
```yaml
# .github/workflows/docusaurus-deploy.yml (excerpt)
- name: Build WASM binary
run: make build-wasm
- name: Copy static assets
run: |
cp bin/ailang.wasm docs/static/wasm/
cp web/ailang-repl.js docs/src/components/
```
## Troubleshooting
### "WebAssembly not supported"
**Solution**: Use a modern browser:
- Chrome 57+
- Firefox 52+
- Safari 11+
- Edge 16+
### "Failed to load AILANG WASM"
**Solutions**:
1. Check browser console for network errors
2. Verify `ailang.wasm` path is correct
3. Ensure `wasm_exec.js` loaded first
4. Check CORS headers if serving from different domain
### "REPL not initialized"
**Solution**: Wait for `init()` promise or use `onReady()`:
```javascript
repl.init('/wasm/ailang.wasm').then(() => {
// Safe to use repl here
repl.eval('1 + 2');
});
```
### Slow Loading
**Solutions**:
1. Enable gzip compression (reduces to ~1-2MB)
2. Use CDN
3. Add preload hints:
```html
<link rel="preload" href="/wasm/ailang.wasm" as="fetch" crossorigin>
```
### Effects Don't Work
**Solutions**:
1. Grant the capability first: `repl.grantCapability('IO')`
2. Register handlers: `repl.setEffectHandler('IO', 'print', fn)`
3. Use async methods if handlers return Promises: `await repl.evalAsync(...)`
4. Check that `ailangSetAIHandler` was called before `AI.call()` — it auto-grants the AI capability
### "undefined global variable: X from std/Y"
**Cause**: Module's stdlib import failed during initialization.
**Solution**: Check browser console for warnings during stdlib loading. Try `repl.reset()` to reload the standard library.
### Module Loading Errors
#### "ambiguous type variable α with classes [Num]"
**Cause**: Functions with numeric operations need explicit type annotations.
**Solution**: Add type annotations to function definitions:
```javascript
// ❌ Fails
let add = \\x. \\y. x + y
// ✅ Works
let add: Int -> Int -> Int = \\x. \\y. x + y
```
#### "module X not loaded (use loadModule first)"
**Cause**: Trying to call or import a module that hasn't been loaded.
**Solution**: Load the module before using it:
```javascript
// First load
repl.loadModule('mymodule', code);
// Then import or call
repl.importModule('mymodule');
```
#### "parse error" or "type error" from loadModule
**Cause**: Invalid AILANG syntax or type errors in module code.
**Solution**: Check the error message and fix the module code:
```javascript
const result = repl.loadModule('test', badCode);
if (!result.success) {
console.error('Module error:', result.error);
// e.g., "parse error: expected expression at line 3"
}
```
## Examples
See:
- [Live Playground](/docs/playground) - Try it now
- [Integration Example](https://github.com/sunholo-data/ailang/blob/main/web/example.mdx)
- [Component Source](https://github.com/sunholo-data/ailang/blob/main/web/AilangRepl.jsx)
## Next Steps
- [Try the Playground](/docs/playground)
- [Download Latest Release](https://github.com/sunholo-data/ailang/releases/latest)
- [Report Issues](https://github.com/sunholo-data/ailang/issues)
---
# Reference: reserved-keywords.md
---
title: Reserved Keywords
---
# Reserved Keywords
The lexer keyword map contains **41 entries**. Most cannot be used as names.
The four contextual testing words remain legal identifiers where a testing marker
is not expected; their reservation is contextual, not global. `case` is an ordinary
identifier, not a keyword. No words are released by this diagnostic change.
A reserved word in a parameter name produces `PAR_RESERVED_KEYWORD` at that word.
The invalid parameter is dropped for parser recovery; the source still fails checking.
Record-type fields retain `PAR_FIELD_NAME_EXPECTED` with a reservation explanation.
Rename the parameter and all of its uses before checking again.
## Complete keyword map
| Word | Current use or reservation, owner and trigger |
| --- | --- |
| `func` | Function declaration or parenthesized function literal. |
| `pure` | Pure function marker. |
| `let` | Immutable binding. |
| `letrec` | Recursive binding. |
| `in` | Binding scope. |
| `if` | Conditional or match guard. |
| `then` | Conditional true branch. |
| `else` | Conditional false branch. |
| `match` | Pattern matching with braces (no `with`). |
| `with` | PAR019 dialect detection and planned effect-handler syntax. Handler lane: consume or release at syntax freeze. Alternatives: `using`, `given`, `w`. |
| `type` | Type declaration. |
| `class` | Type-class declaration syntax. |
| `instance` | Type-class instance syntax. |
| `module` | Module declaration. |
| `import` | Import declaration. |
| `export` | Export marker. |
| `extern` | External declaration. |
| `forall` | Universal type quantification. |
| `exists` | Reserved for existential type quantification; the type-system lane must consume or release it at syntax freeze. |
| `test` | Contextual named test marker; also a legal identifier. |
| `tests` | Contextual test block marker; also a legal identifier. |
| `property` | Contextual property marker; also a legal identifier. |
| `properties` | Contextual property block marker; also a legal identifier. |
| `assert` | Reserved for planned assertion builtin; assertion/testing lane must consume or release at syntax freeze. Alternatives: `check`, `verify`. |
| `spawn` | Reserved for planned concurrency lane (CSP/session types). Consume or release at concurrency syntax freeze. Alternatives: `chan`, `fork`, `concurrent`, `pick`. |
| `parallel` | Reserved for planned concurrency lane (CSP/session types). Consume or release at concurrency syntax freeze. Alternatives: `chan`, `fork`, `concurrent`, `pick`. |
| `select` | Reserved for planned concurrency lane (CSP/session types). Consume or release at concurrency syntax freeze. Alternatives: `chan`, `fork`, `concurrent`, `pick`. |
| `channel` | Reserved for planned concurrency lane (CSP/session types). Consume or release at concurrency syntax freeze. Alternatives: `chan`, `fork`, `concurrent`, `pick`. |
| `send` | Reserved for planned CSP/session-types lane. CSP lane: consume or release at syntax freeze. Alternatives: `tx`, `rx`, `deadline`, `expires`. |
| `recv` | Reserved for planned CSP/session-types lane. CSP lane: consume or release at syntax freeze. Alternatives: `tx`, `rx`, `deadline`, `expires`. |
| `timeout` | Reserved for planned CSP/session-types lane. CSP lane: consume or release at syntax freeze. Alternatives: `tx`, `rx`, `deadline`, `expires`. |
| `as` | Import alias. |
| `deriving` | Type-class derivation. |
| `requires` | Precondition contract. |
| `ensures` | Postcondition contract. |
| `invariant` | Invariant contract. |
| `true` | Boolean literal. |
| `false` | Boolean literal. |
| `not` | Boolean negation. |
| `and` | Boolean conjunction. |
| `or` | Boolean disjunction. |
## Reservation decisions
The [effect-handler lane](https://github.com/sunholo-data/ailang/blob/dev/design_docs/planned/v1_1_0/m-effect-handlers.md)
owns the proposed `handle ... with` syntax. Its syntax freeze must explicitly
consume `with` as syntax or release it. This is separate from its current PAR019
role: `match x with { ... }` is rejected with the existing dialect diagnostic;
AILANG uses `match x { ... }` and guards use `if`.
The [CSP/session-types lane](https://github.com/sunholo-data/ailang/blob/dev/design_docs/planned/v1_1_0/m-csp-session-types.md)
owns channel communication and concurrency reservations. Its examples are
unfrozen sketches, including function-form receive/send; they do not establish
that those words need keyword status. At syntax freeze the lane must explicitly
consume or release each listed word. The assertion/testing lane has the same
obligation for its planned assertion builtin (see the
[named-test assertion plan](https://github.com/sunholo-data/ailang/blob/dev/design_docs/planned/v0_33_1/m-named-test-assert-lowering-sprint-plan.md)).
No syntax-freeze decision is made here. Until its owner rules, each reservation
remains. A release must update the keyword map, token enum, token strings,
`IsKeyword`, and this reference together.
## Working rename example
```ailang
module examples/runnable/reserved_keyword_names
pure func combine(a: int, rx: int, b: int) -> int = a + rx + b
pure func usingValue(using: int) -> int = using
export pure func main() -> int = combine(1, 2, 3) + usingValue(4)
```
Use `ailang check examples/runnable/reserved_keyword_names.ail` to check the
corrected three-parameter call. Recovery of an invalid parameter is not a
successful type check and does not preserve a three-argument call's arity.
See [Language Syntax](./language-syntax.md), or run `ailang prompt` for the
current teaching prompt and `ailang check file.ail` for diagnostics.
---
# AI Teaching Prompt (Latest: v0.16.12)
# AILANG v0.16.12 - AI Teaching Prompt (with IFC Labels + Output Discipline)
AILANG is a **pure functional language** with Hindley-Milner type inference and algebraic effects. Write code using **recursion** (no loops), **pattern matching**, and **explicit effect declarations**.
**⚠️ AILANG is NOT Python!** Do not use Python syntax like `def`, `for`, `while`, `class`, or `if x:`. See "What AILANG Does NOT Have" below.
## Required Program Structure
```ailang
module myproject/mymodule
export func main() -> () ! {IO} {
println(show(42)) -- println is in prelude, no import needed!
}
```
**Rules:**
1. First line MUST be `module path/name` matching the file path
2. Use `println(show(value))` for numbers, `println(str)` for strings. **Prefer `show` over `intToStr`/`floatToStr`** — `show` works on ALL types
3. **Pick ONE function-body style per function and stick to it:**
- **Block body** `func f() ! {IO} { ... }` — one `let` per line, NO `in`. Statements are separated by newlines (canonical, what `ailang fmt` writes) or by `;` (also accepted)
- **Expression body** `func f() ! {IO} = let x = a in let y = b in finalExpr` — `in` keyword, NO semicolons
- **The `= {` trap**: `func f() = { a; b }` mixes both styles. `=` means expression body (no `;`); for multiple statements DROP the `=` → `func f() { a; b }`. (Error: `PAR017: ';' is only valid inside { } block bodies`.)
4. No loops — use recursion
5. **Output raw AILANG code only** — no markdown fences (`` ``` ``), no prose, no JSON wrappers. The first line must be `module ...`.
## println vs print
| Function | Import Required? | Behavior |
|----------|-----------------|----------|
| `println` | NO (prelude) | WITH newline |
| `print` | YES (`import std/io (print)`) | NO newline |
```ailang
-- println works without import (prelude provides it)
println("A"); println("B") -- Output: A\nB\n
-- print requires import for no-newline output
import std/io (print)
print("A"); print("B") -- Output: AB
```
**Use `println` for most output.** Use `print` ONLY when building output on one line.
**Real-time terminal output (games, progress bars, TUIs), v0.27.0+:** on a TTY, stdout is
line-buffered — `println` shows at once, but partial-line output (`\r` with no newline) needs
an explicit `flush()`. C-style escapes `\xHH` / `\u{..}` / `\e` / `\0` work in string literals.
```ailang
import std/io (print, flush)
print("\r\x1b[92m50%\x1b[0m"); -- \x1b = ESC (ANSI colour); "\x1b[2J" clears the screen
flush() -- force it to the screen NOW (no /dev/tty hack needed)
```
Runs under `--caps IO,Clock` — no `FS`. `printErr`/`eprintln` (std/io) write to stderr (unbuffered).
## Line input and native terminals (supporting runtime required)
Use `import std/io (readLineOpt)` for loops that must distinguish a blank line
from EOF: `readLineOpt() -> Option[string] ! {IO}` returns `Some("")` for a blank
line, `Some(text)` for a final unterminated line, then sticky `None` at EOF.
Legacy `readLine() -> string` returns `""` for both EOF and blank input; do not
use that equality to terminate a line service.
```ailang
module line_echo
import std/io (readLineOpt, println)
import std/option (Some, None)
func loop() -> () ! {IO} {
match readLineOpt() {
None => (),
Some(line) => { println(line); loop() }
}
}
export func main() -> () ! {IO} = loop()
```
Native immediate keys/resize use `std/terminal` under `IO`: `info()` queries
configured TTY endpoints, `withTerminal(options, body)` owns restoration, and
`readEvent(session, timeout_ms)` returns `Result[TerminalEvent, TerminalError]`.
Timeouts: `-1` blocks, `0` polls, `1..60000` waits in milliseconds. `Idle` is
separate from `EndOfInput`. Callback effects propagate as `{IO, e}`. Native
scopes require macOS/Linux and host signal lifecycle integration; other hosts
return `Unsupported`. Handle errors explicitly. Widgets and normalized replay
belong in `sunholo/terminal_ui`; no shell or `/dev/tty` workaround is required.
## Output Discipline (stdout is compared byte-for-byte)
Your program's printed output is graded by an **exact match** against the expected
result. Extra text — even when the computed value is correct — is a failure.
- **Print only what the task asks for, nothing more.** If the task wants `40`, print
`40` — never `Result: 40`, `Answer: 40`, or `The result is 40`.
- **No labels, captions, or prefixes** in front of values. If the task says to print
`a = TBool`, that exact string is the whole line — do not add `Types unify!` or any
other commentary before or after it.
- **No status / confirmation lines** ("Done", "Success", "They unify!", "Computing…")
unless the task explicitly tells you to print them.
- **Match the requested format exactly**: same capitalisation, same spacing, one value
per line as specified, and no extra trailing or leading blank lines.
- When the task says *"print the result"* or *"print if they unify"*, it means print
the bare value(s) it just defined — not a sentence describing them.
Treat the expected output as a contract: emit exactly those characters and stop.
## CLI Exploration (USE THIS!)
**Before writing helper functions, check if the stdlib already has what you need:**
```bash
ailang docs --list # List all stdlib modules
ailang docs std/string # Show ALL exports with signatures
ailang docs std/list # Show list operations
ailang builtins list # All registered builtins
ailang check file.ail # Type-check without running
ailang examples search "recursion" # Find working code examples
```
## Quick Reference Examples
**Print calculation:**
```ailang
module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} = println(show(5 % 3))
```
**If-then-else (NO braces!):**
```ailang
module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} {
let x = 10;
let msg = if x > 0 then "positive" else "not positive";
println(msg)
}
```
**HTTP GET:**
```ailang
module benchmark/solution
import std/net (httpGet)
export func main() -> () ! {IO, Net} = println(httpGet("https://example.com"))
```
**HTTP POST with JSON:**
```ailang
module benchmark/solution
import std/net (httpPost)
import std/json (encode, jo, kv, js, jnum)
export func main() -> () ! {IO, Net} {
let data = encode(jo([kv("message", js("hello")), kv("count", jnum(42.0))]));
println(httpPost("https://httpbin.org/post", data))
}
```
**Filter list recursively:**
```ailang
module benchmark/solution
export func filter(people: [{name: string, age: int}], minAge: int) -> [{name: string, age: int}] =
match people {
[] => [],
p :: rest => if p.age >= minAge then p :: filter(rest, minAge) else filter(rest, minAge)
}
```
**Record update (immutable):**
```ailang
module benchmark/solution
export func main() -> () ! {IO} {
let person = {name: "Alice", age: 30, city: "NYC"};
let older = {person | age: person.age + 1}; -- Update one field
let moved = {older | city: "SF"}; -- Chain updates
println("${person.name}, ${show(person.age)}, ${person.city}");
println("${older.name}, ${show(older.age)}, ${older.city}");
println("${moved.name}, ${show(moved.age)}, ${moved.city}")
}
```
**Open records (width subtyping):**
```ailang
module benchmark/solution
-- EXACT record: only accepts {name: string}, rejects extra fields
pure func getNameExact(p: {name: string}) -> string = p.name
-- OPEN record with | r: accepts extra fields
pure func getName(p: {name: string | r}) -> string = p.name
-- OPEN record with ... sugar (equivalent to | r with fresh variable)
pure func getEmail(u: {email: string, ...}) -> string = u.email
export func main() -> () ! {IO} {
-- Exact: only works with exact shape
println(getNameExact({name: "Alice"}));
-- Open: accepts any record with at least name field
println(getName({name: "Bob", age: 30}));
println(getName({name: "Charlie", age: 25, city: "NYC"}));
-- Ellipsis sugar: same behavior
println(getEmail({email: "test@example.com", name: "Test", id: 123}))
}
```
**AI effect (call LLM):**
```ailang
module benchmark/solution
import std/ai (call, callJson, callJsonSimple)
export func main() -> () ! {IO, AI} = println(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 |
| `range(n)` | `range(0, n)`: two args, end exclusive |
| `acc ++ [x]` / `A.set` / `M.insert` once per loop step | O(n²): build in one pass, see "Cost Model" |
| `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 multi-arg func | `f(a, b)` - multi-arg funcs use commas |
| `if x { ... }` | `if x then ... else ...` - NO braces! |
| mixing `let x = e in` with `;` | Use ONE style consistently |
| `let (x, y) = tuple` | Use `match tuple { (x, y) => ... }` |
| `\(a, b). body` or `\a, b. body` | Inline: `\a b. body` (space-separated, called `f(a, b)`). Named: `func(a: T, b: U) -> R { body }` |
| nested `func f(...) =` | Use `let f = \x. body` for nested functions |
| `!condition` | Both `!x` and `not x` work — prefer `not` for readability |
| `concat(a, b)` for strings | `"${a}${b}"` interpolation — `++` is list-only in v0.13.0+ |
| `concat(a, b)` for lists | `a ++ b` |
## Let Bindings: Block Style vs Expression Style
**Rule: Inside `{ }` blocks, one `let` per line (no `in`). With `=` bodies, use `in`.**
```ailang
-- Block style: curly braces, one statement per line (canonical `ailang fmt` form)
export func main() -> () ! {IO} {
let x = 1
let y = 2
println(show(x + y))
}
-- `;` between block statements is also accepted (fmt removes it)
export func main() -> () ! {IO} {
let x = 1;
let y = 2;
println(show(x + y))
}
-- ONE place `;` is required: a block whose final expression starts with `{`
-- (a record update or literal). Without it the parser reads `{` as continuing the let.
pure func bump(r: {a: int}) -> {a: int} {
let x = 1;
{r | a: x}
}
-- Expression style: equals + in
export func main() -> () ! {IO} =
let x = 1 in
let y = 2 in
println(show(x + y))
-- WRONG: Using `in` inside `{ }` block causes scope errors
export func main() -> () ! {IO} {
let x = 1 in -- DON'T use `in` inside blocks!
let y = 2; -- ERROR: x out of scope
println(show(x + y))
}
```
**Simple rule: See `{` -> one `let` per line, no `in`. See `=` -> use `in`.**
## Function Calls: Multi-arg vs Curried (CRITICAL)
**Two styles with DIFFERENT call syntax:**
```ailang
-- MULTI-ARG FUNC: Call with commas f(a, b)
func add(a: int, b: int) -> int = a + b
let result = add(3, 4) -- CORRECT: 7
-- let bad = add(3)(4) -- WRONG: arity mismatch!
-- CURRIED LAMBDA: Call with chained f(a)(b)
let addC = \a. \b. a + b
let result = addC(3)(4) -- CORRECT: 7
let add10 = addC(10) -- Partial application!
-- let bad = addC(3, 4) -- WRONG: arity mismatch!
```
**Rule: Match call style to definition style:**
| Definition | Call Style | Partial App? |
|------------|------------|--------------|
| `func f(a, b) = ...` | `f(a, b)` | No |
| `let f = \a. \b. ...` | `f(a)(b)` | Yes |
**Higher-order with curried lambdas:**
```ailang
module benchmark/solution
import std/io (println)
export func main() -> () ! {IO} {
let compose = \f. \g. \x. f(g(x));
let double = \x. x * 2;
let addOne = \x. x + 1;
let doubleThenAdd = compose(addOne)(double); -- Curried: chain calls
println(show(doubleThenAdd(5)))
}
```
**Using std/list foldl (IMPORTANT - use inline func syntax!):**
```ailang
module benchmark/solution
import std/io (println)
import std/list (foldl)
-- std/list foldl signature: foldl(f: (acc, elem) -> acc, initial, list)
-- RECOMMENDED: Use inline func syntax for reliable type inference
export func main() -> () ! {IO} {
-- MOST RELIABLE: inline func with explicit types
let sum = foldl(func(acc: int, x: int) -> int { acc + x }, 0, [1,2,3,4,5]);
println(show(sum))
}
```
**foldl syntax options:**
```ailang
-- RECOMMENDED: space-separated multi-param lambda (concise)
foldl(\acc x. acc + x, 0, xs)
-- VERBOSE: inline func with explicit types (also works)
foldl(func(acc: int, x: int) -> int { acc + x }, 0, xs)
-- ALSO WORKS: curried lambda (longer; prefer the multi-param form)
foldl(\acc. \x. acc + x, 0, xs)
```
**For callbacks passed to `foldl`, `sortBy`, `zipWith`, `foldlE`, etc., write `\a b. body`**
(one lambda, several params): it is the simplest form. Curried `\a. \b. body` also works.
**Sorting with sortBy:**
```ailang
import std/list (sortBy)
-- Comparator returns: negative if a < b, 0 if equal, positive if a > b
func cmpInt(a: int, b: int) -> int = a - b
let sorted = sortBy(cmpInt, [3, 1, 4, 1, 5]) -- [1, 1, 3, 4, 5]
-- Reverse sort: swap arguments
func cmpDesc(a: int, b: int) -> int = b - a
let descending = sortBy(cmpDesc, [3, 1, 4]) -- [4, 3, 1]
```
## Recursive Lambdas (letrec)
**Use `letrec` when a lambda needs to call itself:**
```ailang
module benchmark/solution
export func main() -> () ! {IO} {
-- WRONG: `let` for self-referential lambda
-- let factorial = \n. if n <= 1 then 1 else n * factorial(n - 1); -- ERROR: undefined 'factorial'
-- RIGHT: use `letrec` for recursive lambdas
letrec factorial = \n. if n <= 1 then 1 else n * factorial(n - 1);
println(show(factorial(5))) -- 120
}
```
**When to use `letrec` vs `func`:**
| Case | Use |
|------|-----|
| Top-level recursive function | `func f(...) = ...` (preferred) |
| Recursive lambda inside block | `letrec f = \x. ... f(...)` |
| Non-recursive lambda | `let f = \x. ...` |
**Mutual recursion: use top-level `func` declarations** (last example below). Two local
`letrec`s cannot see each other: `letrec isEven = ...isOdd(n - 1)` followed by `letrec isOdd = ...`
fails with `undefined variable: isOdd`, and `letrec a = ..., b = ...` does not parse.
**Where to put a `letrec`:** it needs a statement sequence, and both a `{...}`
block body and a `= ...` equation body give it one. Prefer the block — it is what
`ailang fmt` emits, so formatting will not rewrite what you wrote.
```ailang
-- ✅ PREFERRED: `{ ... }` block; the `;` separates statements
export func is_even(n: int) -> bool {
letrec is_odd = \m. if m == 0 then false else is_even(m - 1);
if n == 0 then true else is_odd(n - 1)
}
-- ✅ BEST for mutual recursion: top-level `func` declarations
func is_odd(n: int) -> bool =
if n == 0 then false else is_even(n - 1)
export func is_even(n: int) -> bool =
if n == 0 then true else is_odd(n - 1)
```
## Type Annotations for Higher-Order Functions
**Annotate function types with parentheses around arrow types:**
```ailang
-- Single-argument function type
let double: int -> int = \x. x * 2
-- Higher-order: function taking a function
let apply: (int -> int) -> int -> int = \f. \x. f(x)
-- Multiple function parameters
let compose: (int -> int) -> (int -> int) -> int -> int = \f. \g. \x. f(g(x))
```
**Type annotation on multi-arg named function:**
```ailang
func twice(f: int -> int, x: int) -> int = f(f(x))
```
## 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` |
| Recursive lambda | `letrec f = \x. ... f(x) ...` |
| Pattern match | `match x { 0 => a, n => b }` (use `=>`, commas between arms) |
| ADT | `type Tree = Leaf(int) \| Node(Tree, int, Tree)` |
| ADT with Eq | `type Color = Red \| Green \| Blue deriving (Eq)` |
| Record with Eq | `type Point = {x: int, y: int} deriving (Eq)` (anonymous records have no `==`) |
| `==` / `!=` support | int, float, string, bool, and any list, `Option`, `Result` or tuple whose parts support it (`xs == []`, `o == Some(3)`, `(a, b) == (1, "x")`), plus types declared `deriving (Eq)`. Functions have no `==`. Float `==` is IEEE: NaN is never equal to anything, itself included; test with `isNaN(x)` from `std/math`. Float `<` `<=` `>` `>=` are IEEE too: false whenever either side is NaN, so `if x > hi` does not catch a NaN |
| Record | `{name: "A", age: 30}` |
| Record update | `{base \| field: val}` |
| Open record type | `{name: string \| r}` or `{name: string, ...}` |
| 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 in the signature!**
Calling a function-valued record field performs its annotated effects. Passing a
function to a higher-order function with an unannotated or effect-polymorphic
callback parameter also requires the caller to declare the callback’s effects.
This includes a function stored in a list, tuple, Option or record argument.
Constructing a record that stores a callback does not perform those effects.
Callback **function types** distinguish these contracts:
| Callback type | Contract |
|---|---|
| `T -> U` | Implicit open row; hand-off charges the caller, including nested callback data |
| `T -> U ! {}` | Closed pure callback; an effectful value is rejected even if its caller declares that effect |
| `T -> U ! {IO}` | Concrete IO callback contract; the callee declares IO when it invokes it |
| `T -> U ! {e}` | Explicit row polymorphism; share `e` with the enclosing caller row |
For a store-only helper taking a mixed record of pure and effectful callbacks,
write `! {}` on pure callback fields and concrete rows on effectful fields.
Direct record/list/tuple literals still only store callback values.
```ailang
-- Pure (no effects) - use `pure func` or omit effect annotation
pure func add(x: int, y: int) -> int = x + y
-- IO effect - for print/println/readLine
func greet(name: string) -> () ! {IO} = println("Hello ${name}")
-- Single effect with return value
func ask(prompt: string) -> string ! {IO} {
println(prompt);
readLine()
}
-- Multiple effects
func process(path: string) -> () ! {IO, FS} {
let content = readFile(path);
println(content)
}
-- Main function typically needs effects
export func main() -> () ! {IO} {
println("Hello, World!")
}
-- Exit with explicit code (for CLI tools)
import std/io (println, exit)
export func main() -> () ! {IO} {
println("error: invalid argument");
exit(1)
}
-- Main with file system access
export func main() -> () ! {IO, FS} {
let content = readFile("data.txt");
println(content)
}
-- Main with network
export func main() -> () ! {IO, Net} {
let body = httpGet("https://example.com");
println(body)
}
-- Main with CLI arguments (getArgs returns [string], needs Env)
-- Run: ailang run --caps IO,FS,Env --entry main program.ail -- arg1 arg2
export func main() -> () ! {IO, FS, Env} {
let args = getArgs();
match args {
filename :: _ => {
let content = readFile(filename);
println(content)
},
_ => println("Usage: program <filename>")
}
}
-- Reading lines from stdin (readLine reads one line, returns "" at EOF)
-- Run: printf "hello\nworld\n" | ailang run --caps IO --entry main program.ail
func loop() -> () ! {IO} {
let line = readLine();
if line == "" then ()
else {
println("Got: ${line}");
loop()
}
}
```
**Common effect combinations:**
| Task | Required Effects |
|------|------------------|
| Print output | `! {IO}` |
| Read files | `! {FS}` (add IO if also printing) |
| HTTP requests | `! {Net}` (add IO if also printing) |
| Environment vars | `! {Env}` |
| CLI arguments | `! {Env}` (use `getArgs()` from `std/env`) |
| Read from stdin | `! {IO}` (use `readLine()` from `std/io`) |
| AI calls | `! {AI}` |
| Run external commands | `! {Process}` or `! {IO, Process}` |
| Exit with code | `! {IO}` (use `exit(code)` from `std/io`) |
| Full program | `! {IO, FS, Net}` as needed |
**Effect errors and how to fix:**
```
Error: Effect checking failed for function 'main'
Function uses effects not declared in signature
Missing effects: IO
```
**Fix:** Add the missing effect to your function signature:
```ailang
-- WRONG: missing IO effect
export func main() -> () { println("hi") }
-- CORRECT: declare IO effect
export func main() -> () ! {IO} { println("hi") }
```
```
| Effect | Functions | Import |
|--------|-----------|--------|
| `IO` | `print`, `println`, `readLine`, `writeBytes`, `exit` | `std/io` (print is builtin) |
| `FS` | `readFile`, `writeFile`, `fileExists`, `listDir`, `walk`, `glob`, `mkdir`, `mkdirAll`, `isDir`, `isFile`, `removeFile`, `_zip_*` | `std/fs`, `std/zip` |
| `Net` | `httpGet`, `httpPost`, `httpRequest` | `std/net` |
| `Env` | `getArgs`, `getEnv`, `getEnvOr`, `hasEnv` | `std/env` |
| `Clock` | `now`, `sleep` | `std/clock` |
| `AI` | `call`, `callJson`, `callJsonSimple` | `std/ai` |
| `Debug` | `log`, `check` | `std/debug` |
| `Process` | `exec` | `std/process` |
| `SharedMem` | `_sharedmem_get`, `_sharedmem_put`, `_sharedmem_cas` | builtins |
| `SharedIndex` | `_sharedindex_upsert`, `_sharedindex_find_simhash` | builtins |
### Parameterised Effects (v0.15.0+)
Effect rows can carry `[key=value]` parameters that ride alongside the
effect name. Phase 1 ships the syntax and unification rules; the
runtime currently treats all modes identically (mode-aware dispatch
lands later).
```ailang
-- Bare !{Rand} desugars to !{Rand[mode=os]} via the default-mode table.
func roll_d6() -> int ! {Rand} = rand_int(1, 6)
-- Same effect signature as bare !{Rand}; unifies via default-desugar.
func roll_d20() -> int ! {Rand[mode=os]} = rand_int(1, 20)
-- Distinct mode — does NOT unify with mode=os under invariant rules.
func deterministic() -> int ! {Rand[mode=seeded]} = {
rand_seed(42);
rand_int(1, 100)
}
```
**Rules:**
- Bare `!{Rand}` is shorthand for `!{Rand[mode=os]}`.
- Different param values do NOT unify: `!{Rand[mode=os]}` and
`!{Rand[mode=seeded]}` are distinct effect rows.
- v0.15.0 ships two default-mode entries: `Rand → mode=os` and
`AI → mode=fixed`. Other effects (`IO`, `FS`, `Net`, `Env`, ...)
have no params yet — write `!{IO}`, not `!{IO[...]}`.
- Mode set is closed (compiler-known per effect) and **enforced** at
type-check. Only these forms are legal:
`Rand[mode=os|seeded|crypto]`, `AI[mode=fixed|routeable|replay-only]`,
`AI[scope=byok]`. Anything else is a hard error:
- `EFF_UNKNOWN_MODE` — value not in the effect's allowed set
(e.g. `Rand[mode=banana]`).
- `EFF_UNKNOWN_PARAM_KEY` — key not on the effect
(e.g. `Rand[flavor=hot]`).
- `EFF_PARAMS_NOT_SUPPORTED` — an effect with no params was given one
(e.g. `Clock[mode=pinned]`); write the bare effect instead.
Each error lists the legal keys/values — read them and pick one.
See [Parameterised Effects guide](https://ailang.sunholo.com/docs/guides/parameterised-effects).
**AI modes (v0.15.0+):**
```ailang
-- Bare !{AI} desugars to !{AI[mode=fixed]} (direct provider call).
func summarize(text: string) -> string ! {AI} = call(text)
-- !{AI[mode=routeable]} opts into runtime provider routing
-- (e.g. OpenRouter). Skips the --allow-routing CLI gate.
func summarize_routed(text: string) -> string ! {AI[mode=routeable]} = call(text)
```
Rules:
- Different param values do NOT unify: `!{AI[mode=fixed]}` and
`!{AI[mode=routeable]}` are distinct.
- Bare `!{AI}` continues to work; existing programs unchanged.
- See [AI Routing guide](https://ailang.sunholo.com/docs/guides/ai-routing) for routing flags.
### Debug Effect — True Ghost Effect for Logging
**Debug is a ghost effect** — fully invisible to callers, zero-cost in release mode. Use `Debug` instead of `IO` (println) for logging in packages. No effect declarations, no --caps, no config needed:
```ailang
import std/debug (log, check)
-- No ! {Debug} needed — ghost effect is invisible to callers
func process(data: string) -> string ! {Net} {
log("processing: ${data}");
check(length(data) > 0, "data must not be empty");
httpGet("https://example.com/${data}")
}
```
Or use the `sunholo/logging` package for structured JSON logging:
```ailang
import pkg/sunholo/logging/logger (info, warn, infoWith)
import std/json (kv, js, jnum)
func handleRequest(path: string) -> Response ! {Net} {
infoWith("Request", [kv("path", js(path))]);
httpGet("https://api.example.com${path}")
}
```
**Key rules:**
- **Ghost effect**: No `! {Debug}` in signatures, no `[effects].max`, no `--caps Debug` needed
- **Write-only**: `log` and `check` write to host context; AILANG code cannot read it
- **Assertions don't throw**: `check(false, msg)` records failure but continues
- **Release mode erases**: `--release` flag removes all Debug calls (zero cost)
- **Log level filtering**: `--log-level warn` suppresses INFO/DEBUG output
- **Output**: Debug logs printed to stderr as structured JSON after execution
## Standard Library
**Auto-imported (no import needed):**
- `println` (entry modules only) — `print` (no newline) is NOT auto-imported: `import std/io (print)`
- `show(x)` - convert to string
- Comparison operators: `<`, `>`, `<=`, `>=`, `==`, `!=`
- **`Option`/`Some`/`None` and `Result`/`Ok`/`Err`** (entry modules only, v0.30.0+) —
the types AND their constructors resolve without `import std/option` / `import std/result`.
You can construct and pattern-match them directly. (Their helper functions — `map`,
`getOrElse`, etc. — still need an explicit import, e.g. `import std/option (map)`.
Library modules with no exported `main` still require the explicit import.)
**Common imports:**
```ailang
import std/io (println, readLine)
import std/fs (readFile, writeFile, fileExists, listDir, walk, glob, mkdir, mkdirAll, isDir, isFile, removeFile)
-- walk(root) -> [string] ! {FS}: every file under root, recursively, sorted, paths prefixed with root.
-- glob(root, ".ail") -> [string] ! {FS}: walk filtered by suffix. Never hand-roll a recursive listDir/isDir walker.
import std/env (getArgs, getEnv, getEnvOr)
import std/net (httpGet, httpPost, httpRequest)
import std/json (encode, decode, get, getString, getNumber, getInt, getBool, getArray, getObject, asString, asNumber, asArray)
import std/json (filterStrings, filterNumbers, allStrings, allNumbers, getStringArrayOrEmpty, decodeFloatArray)
import std/list (map, filter, foldl, length, concat, sortBy, take, drop, range, nth, last, any, findIndex, flatMap, zipWith, mapAccumL, mapE, filterE, foldlE, flatMapE, forEachE)
-- range(start, end): TWO args, end exclusive. range(n) is an arity error.
import std/map as M -- O(1) lookup, O(n) insert: see "Cost Model"
import std/set as S -- Set[a], e.g. a visited Set[(int, int)]: empty, fromList, insert, remove, member, size, toList, union, intersect, difference
import std/set (Set) -- needed to WRITE the type Set[a] in a signature (`S.Set[...]` does not parse)
import std/regex as R -- compile, isMatch, findFirst, findAll, replaceAll, split (qualified: split clashes with std/string)
-- [float] vector math, native loops (never foldl(zipWith(...)) for a dot product):
import std/embedding (dot, cosine, magnitude, normalize, scale, add_vectors, axpy, euclidean_distance)
-- Array[float] when memory or bulk updates matter: unboxed (8 B/element), strict-length kernels, one-copy batched writes.
-- Its dot/axpy/scale clash with std/embedding's: import one module qualified (import std/array as A).
import std/array (make, fromList, toList, get, getOpt, set, length, dot, axpy, scale, add, sub, mul, sum, argmax, updateMany, scatterAdd, encodeF64LE, decodeF64LE)
import std/string (split, chars, trim, stringToInt, stringToFloat, contains, find, substring, intToStr, floatToStr, join, startsWith, endsWith, length, toUpper, toLower, compare, repeat)
import std/math (floatToInt, intToFloat, floor, ceil, round, sqrt, pow, abs_Float, abs_Int)
import std/ai (call, callJson, callJsonSimple)
import std/sem (make_frame_at, store_frame, load_frame, update_frame)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)
import std/bytes (fromString, toString, toBase64, fromBase64, toBase64URL, fromBase64URL, length, slice, fromInts, toInts, byteAt)
import std/stream (connect, transmit, transmitBinary, onEvent, runEventLoop, disconnect, sseConnect, ssePost, withSSE, sourceOfConn, asyncReadStdinLines, asyncExecProcess, selectEvents, StreamConn, StreamSource, StreamEvent, Message, Binary, Opened, Closed, StreamError, Ping, SSEData, SourceText, SourceBytes)
import std/process (exec, spawnProcess, writeProcessStdin, closeProcessStdin, ProcessHandle)
import std/zip (listEntries, readEntry, readEntryBytes)
import std/xml (parse, findAll, findFirst, getText, getAttr, getChildren, getTag)
```
**List functions** (std/list) — all polymorphic, fully stable, **use these instead of hand-rolling recursion**:
- `map(f, xs)` - Apply function to each element
- `filter(p, xs)` - Keep elements matching predicate
- `foldl(f, acc, xs)` - Left fold (use `func(acc: T, x: U) -> T { ... }` for f)
- `foldr(f, acc, xs)` - Right fold
- `length(xs)` - List length
- `head(xs) -> Option[a]` - First element (None if empty)
- `tail(xs)` - All elements except first
- `reverse(xs)` - Reverse a list
- `concat(xs, ys)` - Concatenate two lists
- `zip(xs, ys)` - Pair elements from two lists
- `sortBy(cmp, xs)` - Sort with comparator
- `take(n, xs)` - Take first n elements
- `drop(n, xs)` - Drop first n elements
- `nth(xs, idx) -> Option[a]` - Get element at index (0-based), None if out of bounds
- `last(xs) -> Option[a]` - Get last element, None if empty
- `any(p, xs) -> bool` - Check if any element satisfies predicate
- `findIndex(p, xs) -> Option[int]` - Find index of first matching element
- `flatMap(f, xs)` - Apply f to each element, flatten results (pure)
- `zipWith(f, xs, ys)` - Combine two lists element-wise with function f
- `range(start, end) -> [int]` - `[start, ..., end-1]`; `range(0, 3) == [0, 1, 2]`
- `mapAccumL(f, s0, xs) -> ([b], s)` - Map with running state, `f(s, x) -> (out, s2)`, one O(n) pass. Use it instead of a foldl that appends to a list
- `dedup(xs)` - Remove consecutive duplicates (preserves first occurrence)
- `union(xs, ys)`, `intersect(xs, ys)`, `difference(xs, ys)` - Set ops on lists
- `maximumInt(xs) -> Option[int]`, `minimumInt(xs) -> Option[int]` - Typed min/max (None on empty)
- `maximumFloat(xs) -> Option[float]`, `minimumFloat(xs) -> Option[float]` - Float variants
- `maximumString(xs) -> Option[string]`, `minimumString(xs) -> Option[string]` - String variants (lex order)
**Effectful list combinators** (v0.7.3) — use these instead of hand-rolling recursive traversals:
- `mapE(f, xs)` - Effectful map: apply effectful function to each element
- `filterE(p, xs)` - Effectful filter: keep elements matching effectful predicate
- `foldlE(f, acc, xs)` - Effectful left fold
- `flatMapE(f, xs)` - Effectful flatMap: apply f, flatten results
- `forEachE(f, xs)` - Effectful forEach: apply f for side-effects, discard results
All effectful combinators are **effect-polymorphic** — they work with any effect (`IO`, `FS`, `AI`, etc.) and guarantee **left-to-right evaluation order**.
**Prefer `map`/`filter`/`foldl` over manual recursion:**
```ailang
import std/list (map, filter, foldl, mapE, filterE, foldlE)
-- Pure: use map/filter/foldl
let doubled = map(\x. x * 2, [1, 2, 3]) -- [2, 4, 6]
let evens = filter(\x. x % 2 == 0, [1, 2, 3, 4]) -- [2, 4]
let sum = foldl(func(acc: int, x: int) -> int { acc + x }, 0, [1, 2, 3]) -- 6
-- Effectful: use mapE/filterE/foldlE (replaces hand-rolled recursion!)
let results = mapE(\x. { println("processing ${show(x)}"); x * 2 }, [1, 2, 3]);
let checked = filterE(\x. { println("checking ${show(x)}"); x > 2 }, [1, 2, 3, 4]);
let total = foldlE(func(acc: int, x: int) -> int ! {IO} { println("fold"); acc + x }, 0, [10, 20]);
```
**Note on foldlE:** Write the callback as `\acc x. ...` (or `func(acc: T, x: U) -> T ! {Effect} { ... }` when it needs annotations).
**String functions** (std/string) — run `ailang docs std/string` for full list:
- `length(s) -> int` - String length
- `contains(hay, needle) -> bool` - Check if string contains substring
- `startsWith(s, prefix) -> bool` - Check if string starts with prefix
- `endsWith(s, suffix) -> bool` - Check if string ends with suffix
- `find(hay, needle) -> int` - Index of substring, or -1 if not found
- `substring(s, start, end) -> string` - Extract substring
- `split(s, delim) -> [string]` - Split string by delimiter
- `chars(s) -> [string]` - Convert string to list of single-character strings (Unicode-aware)
- `trim(s) -> string` - Trim whitespace
- `toUpper(s) -> string` / `toLower(s) -> string` - Case conversion
- `stringToInt(s) -> Option[int]` - Parse integer
- `stringToFloat(s) -> Option[float]` - Parse float
- `intToStr(n) -> string` - Convert int to string (prefer `show` for simple cases)
- `floatToStr(f) -> string` - Convert float to string (prefer `show` for simple cases)
- `join(delim, xs) -> string` - Join list of strings with delimiter
- `repeat(s, n) -> string` - Repeat string n times
- `words(s) -> [string]` - Split by any whitespace run (handles tabs/newlines)
- `splitAny(s, delims: [string]) -> [string]` - Split by any of multiple delimiter strings
- `charCode(c: string) -> int` - Unicode codepoint of a single-char string (use with `chars(s)`)
- `foldChars(f, acc, s) -> a` - Fold over each character of a string
**Math functions** (std/math):
- `floatToInt(x) -> int` - Convert float to int (truncates toward zero)
- `intToFloat(x) -> float` - Convert int to float
- `floor(x) -> float`, `ceil(x) -> float`, `round(x) -> float` - Rounding
- `sqrt(x)`, `pow(x, y)`, `abs_Float(x)`, `abs_Int(x)` - Math operations
- `sin(x)`, `cos(x)`, `tan(x)` - Trigonometric (radians)
- `asin(x)`, `acos(x)`, `atan(x)`, `atan2(y, x)` - Inverse trig
- `exp(x)`, `log(x)`, `log10(x)` - Exponential and logarithm (natural + base-10)
- `PI() -> float`, `E() -> float` - Constants (note: CALL them with `()` — they're nullary functions, not bindings)
**Bytes functions** (std/bytes) — pure binary data operations:
- `fromString(s) -> bytes` - UTF-8 encode string to bytes
- `toString(b) -> string` - Decode bytes to UTF-8 string
- `toBase64(b) -> string` - Base64 encode (standard alphabet, padded)
- `fromBase64(s) -> Option[bytes]` - Base64 decode (None if invalid; REQUIRES padding)
- `toBase64URL(b) -> string` - base64url encode (RFC 4648 §5: URL-safe alphabet, NO padding). Required by the Gmail API's `raw` field and by JWT segments. Do NOT hand-roll it from `toBase64` + `replaceMany` — a missed substitution still looks like valid base64 and is rejected at the far end: `toBase64(fromString("a+b/c?"))` is `"YStiL2M/"`, `toBase64URL` of the same bytes is `"YStiL2M_"`
- `fromBase64URL(s) -> Option[bytes]` - base64url decode (None if invalid; REJECTS padding). Exact inverse of `toBase64URL`
- `length(b) -> int` - Byte length
- `slice(b, start, len) -> Option[bytes]` - Extract subsequence (None if out of bounds)
- `fromInts(xs: [int]) -> bytes` - Construct bytes from list of integers (0-255)
- `byteAt(b, i) -> Option[int]` - Get byte value (0-255) at index, or None if out of bounds. Use for ASCII char codes: `byteAt(fromString("A"), 0) == Some(65)`
- `toInts(b) -> [int]` - All byte values as a list, the inverse of `fromInts`. Use this (not `byteAt` in a loop) to walk bytes with `map`/`filter`/`foldl`. NOTE: to inspect the raw bytes behind a string, go through `fromString` — `toInts(fromString(s))`. `charCode`/string indexing give codepoints and return U+FFFD for bytes that are not valid UTF-8 on their own; `toInts` is the only way to reach those bytes (needed for charset transcoding, e.g. Latin-1 -> UTF-8)
- `concat(a, b) -> bytes` - Concatenate two byte slices
- `concatList(xs: [bytes]) -> bytes` - Concatenate a list of byte slices (single allocation)
**Streaming functions** (std/stream) — requires `--caps Stream`:
*WebSocket & SSE:*
- `connect(url, config) -> Result[StreamConn, StreamErrorKind] ! {Stream}` - Open WebSocket
- `transmit(conn, msg) -> Result[unit, StreamErrorKind] ! {Stream}` - Send text message
- `transmitBinary(conn, data) -> Result[unit, StreamErrorKind] ! {Stream}` - Send binary bytes
- `onEvent(conn, handler) -> unit ! {Stream}` - Register event handler (handler: `StreamEvent -> bool`)
- `runEventLoop(conn) -> unit ! {Stream}` - Block until handler returns false
- `disconnect(conn) -> unit ! {Stream}` - Close connection
- `sseConnect(url, config) -> Result[StreamConn, StreamErrorKind] ! {Stream}` - Open SSE (GET)
- `ssePost(url, body, config) -> Result[StreamConn, StreamErrorKind] ! {Stream}` - POST then stream SSE
- `withStream(url, handler) -> Result[StreamConn, StreamErrorKind] ! {Stream}` - Full WebSocket lifecycle
- `withSSE(url, handler) -> Result[StreamConn, StreamErrorKind] ! {Stream}` - Full SSE lifecycle
- Config: `{headers: [{name: string, value: string}]}` — use `{headers: []}` for no custom headers
*Multi-source multiplexing (v0.9.0):*
- `sourceOfConn(conn, name, priority) -> StreamSource ! {Stream}` - Wrap connection as named source
- `asyncReadStdinLines(name, priority) -> StreamSource ! {Stream}` - Stdin line reader source
- `asyncExecProcess(cmd, args, name, priority, chunkSize) -> StreamSource ! {Stream}` - Subprocess stdout as byte chunks (requires `--caps Stream,Process`)
- `selectEvents(sources, handler) -> unit ! {Stream}` - Priority-ordered multi-source event loop
*Subprocess stdin writing (v0.9.0 Phase 3):*
- `spawnProcess(cmd, args) -> ProcessHandle ! {Process}` - Spawn subprocess with writable stdin pipe
- `writeProcessStdin(handle, data) -> Result[(), string] ! {Process}` - Write bytes to subprocess stdin
- `closeProcessStdin(handle) -> () ! {Process}` - Close stdin pipe (signals EOF, subprocess exits)
*Event types (StreamEvent ADT):*
`Message(string)`, `Binary(string)`, `Opened(string)`, `Closed(int, string)`, `StreamError(StreamErrorKind)`, `Ping(string)`, `SSEData(string, string)`, `SourceText(string, string)`, `SourceBytes(string, bytes)`
*Multi-source example:*
```ailang
import std/stream (asyncReadStdinLines, asyncExecProcess, selectEvents,
StreamEvent, SourceText, SourceBytes)
import std/bytes (toString)
export func main() -> unit ! {Stream, Process, IO} {
let proc = asyncExecProcess("echo", ["hello"], "echo", 5, 4096);
let stdin = asyncReadStdinLines("stdin", 10);
selectEvents([proc, stdin], \event. match event {
SourceBytes(src, data) => { println("[${src}] ${toString(data)}"); true },
SourceText(src, text) => { println("[${src}] ${text}"); false },
_ => true
})
}
```
*Subprocess stdin writing example (v0.9.0 Phase 3):*
```ailang
import std/process (spawnProcess, writeProcessStdin, closeProcessStdin, ProcessHandle)
import std/bytes (fromString)
import std/result (Result, Ok, Err)
export func main() -> () ! {Process, IO} {
let handle = spawnProcess("cat", []);
match writeProcessStdin(handle, fromString("hello\n")) {
Ok(_) => println("wrote line"),
Err(e) => println("write error: ${e}")
};
closeProcessStdin(handle)
}
```
- `spawnProcess` spawns subprocess with writable stdin; stdout/stderr discarded
- `writeProcessStdin` writes bytes (non-blocking, 256-slot buffer with backpressure)
- `closeProcessStdin` signals EOF — subprocess sees end-of-input and exits
- Requires `--caps Process` (NOT Stream — stdin writing uses Process effect only)
## String Parsing (Returns Option)
`stringToInt` returns `Option[int]`, NOT an int. You MUST pattern match:
```ailang
module benchmark/solution
import std/string (stringToInt)
export func main() -> () ! {IO} {
match stringToInt("42") {
Some(n) => println("Parsed: ${show(n)}"),
None => println("Invalid number")
}
}
```
**With file reading (effect composition):**
```ailang
module benchmark/solution
import std/fs (readFile)
import std/string (stringToInt)
-- Pure function (no effects)
pure func formatMessage(name: string, count: int) -> string =
"User ${name} has ${show(count)} items"
-- FS effect only
func readCount(filename: string) -> int ! {FS} {
let content = readFile(filename);
match stringToInt(content) {
Some(n) => n,
None => 0
}
}
-- Combined effects: IO + FS
export func main() -> () ! {IO, FS} {
let count = readCount("data.txt");
let msg = formatMessage("Alice", count);
println(msg)
}
```
## Constructing Option Values
To RETURN an Option from a function, use `Some(value)` or `None`:
```ailang
module benchmark/solution
import std/option (Option, Some, None) -- explicit import (this is a LIBRARY module — no `main`)
-- NOTE (v0.30.0+): in an ENTRY module (one with an exported `main`) this import is
-- OPTIONAL — Option/Some/None resolve as prelude. Library modules like this one
-- still import explicitly so the dependency is self-documenting.
-- Return Some(value) for success, None for failure
pure func safeDivide(a: int, b: int) -> Option[int] =
if b == 0 then None else Some(a / b)
-- Return Option from validation
pure func validateAge(age: int) -> Option[int] =
if age >= 0 && age <= 150 then Some(age) else None
```
**Option helper functions** (std/option) — prefer these over re-pattern-matching:
- `isSome(opt) -> bool`, `isNone(opt) -> bool` - Discriminators
- `getOrElse(opt, default) -> a` - Unwrap with fallback (most common idiom!)
- `map(f, opt) -> Option[b]` - Apply function inside Some, pass through None
- `flatMap(f, opt) -> Option[b]` - Chain Option-returning functions
- `filter(pred, opt) -> Option[a]` - Drop to None if predicate fails
```ailang
import std/option (Option, Some, None, getOrElse, isSome, map)
import std/string (stringToInt)
-- Idiomatic: getOrElse instead of pattern match
let n = getOrElse(stringToInt("42"), 0); -- 42
let bad = getOrElse(stringToInt("oops"), -1); -- -1
-- map: transform the value inside Some
let doubled = map(\x. x * 2, Some(21)); -- Some(42)
let stillNone = map(\x. x * 2, None); -- None
```
## Error Handling with Result Type
Use the polymorphic `Result[a]` type for error handling:
```ailang
module benchmark/solution
import std/string (stringToInt)
-- Polymorphic Result type - works with any success type!
-- Ok(a) takes the polymorphic type, Err(string) is always a string
type Result[a] = Ok(a) | Err(string)
-- Parse integer, returning Result
pure func parseIntResult(s: string) -> Result[int] =
match stringToInt(s) {
Some(n) => Ok(n),
None => Err("Invalid integer")
}
-- Safe division
pure func divSafe(a: int, b: int) -> Result[int] =
if b == 0 then Err("Division by zero") else Ok(a / b)
-- Chain results: parse then divide
pure func parseAndDivide(s: string, divisor: int) -> Result[int] =
match parseIntResult(s) {
Ok(n) => divSafe(n, divisor),
Err(msg) => Err(msg)
}
-- Format Result for output
pure func showResult(r: Result[int]) -> string =
match r {
Ok(v) => "Result: ${show(v)}",
Err(msg) => "Error: ${msg}"
}
export func main() -> () ! {IO} {
println(showResult(parseAndDivide("10", 2))); -- Result: 5
println(showResult(parseAndDivide("10", 0))); -- Error: Division by zero
println(showResult(parseAndDivide("abc", 2))) -- Error: Invalid integer
}
```
**Result helper functions** (std/result) — note: `std/result.Result` has TWO type parameters (`Result[a, e]`). The simplified `Result[a] = Ok(a) | Err(string)` examples above use a locally-defined ADT. For the stdlib version, import `Result, Ok, Err` from `std/result` and use:
- `isOk(r) -> bool`, `isErr(r) -> bool` - Discriminators
- `unwrap(r) -> a` - Extract Ok value (panics on Err — use only after isOk check)
- `map(f, r) -> Result[b, e]` - Apply function to Ok value
- `mapErr(f, r) -> Result[a, e2]` - Apply function to Err value
- `flatMap(f, r) -> Result[b, e]` - Chain Result-returning functions
**Polymorphic ADTs with mixed field types:**
```ailang
-- Ok(a) uses the type parameter, Err(string) uses a concrete type
type Result[a] = Ok(a) | Err(string)
-- This works correctly:
-- Ok : forall a. a -> Result[a] (polymorphic field)
-- Err : forall a. string -> Result[a] (concrete field)
-- Both constructors can be used with any Result[a] type:
let r1: Result[int] = Ok(42)
let r2: Result[int] = Err("failed")
let r3: Result[string] = Ok("hello")
let r4: Result[string] = Err("also failed")
```
## Number Conversions (std/math)
```ailang
module benchmark/solution
import std/math (floatToInt, intToFloat, round)
import std/io (println)
export func main() -> () ! {IO} {
let f = 3.7;
let i = floatToInt(f); -- 3 (truncates toward zero)
let r = floatToInt(round(f)); -- 4 (round first, then convert)
let back = intToFloat(i); -- 3.0
println(show(i));
println(show(r))
}
```
## Time and Dates (std/clock, std/datetime)
**std/clock** — current time + sleep, requires `--caps Clock`:
- `now() -> int ! {Clock}` - Current Unix timestamp (milliseconds since epoch)
- `sleep(ms: int) -> () ! {Clock}` - Sleep for N milliseconds
```ailang
import std/clock (now, sleep)
import std/io (println)
export func main() -> () ! {Clock, IO} {
let t = now();
println("timestamp: ${show(t)}");
sleep(500) -- pause 0.5s
}
```
**std/datetime** — pure date arithmetic on Unix timestamps (int milliseconds). Pure (no effect needed):
- Accessors: `year(ts)`, `month(ts)`, `day(ts)`, `weekday(ts)`, `hour(ts)`, `minute(ts)`, `second(ts)` — all return `int`
- Arithmetic: `addDays(ts, n)`, `addMonths(ts, n)`, `addYears(ts, n)`, `diffDays(a, b)` — all return `int`
- Boundaries: `startOfDay(ts)`, `startOfWeek(ts)`, `startOfMonth(ts)` — return `int`
- Construction: `makeDate(y, m, d) -> int`, `makeDateTime(y, m, d, h, mi, s) -> int`
- Formatting: `formatISODate(ts) -> string`, `formatRFC3339(ts) -> string`, `formatMonthShort(ts)`, `formatWeekdayFull(ts)`
- Parsing: `parseISODate(s) -> Option[int]`, `parseRFC3339(s) -> Option[int]`
```ailang
import std/clock (now)
import std/datetime (formatRFC3339, year, addDays, makeDate)
import std/io (println)
export func main() -> () ! {Clock, IO} {
let t = now();
println(formatRFC3339(t)); -- e.g. "2026-05-21T12:00:00Z"
println("year: ${show(year(t))}");
println(formatRFC3339(addDays(t, 7))) -- one week from now
}
```
## Hashing & Crypto (std/crypto)
Pure hashing primitives (no capability needed):
- `sha256Hex(s: string) -> string` - SHA-256 hash, hex-encoded
- `sha256Bytes(b: bytes) -> string` - SHA-256 of raw bytes, hex-encoded
- `hmacSha256(message, key) -> string` - HMAC-SHA-256, hex-encoded
- `constantTimeEqual(a, b) -> bool` - Timing-safe string comparison
```ailang
import std/crypto (sha256Hex, hmacSha256)
let h = sha256Hex("hello"); -- 64-char hex digest
let mac = hmacSha256("payload", "secret-key"); -- HMAC hex
```
## Arrays — Construction Primitives (std/array)
Costs are in "Cost Model" below. `import std/array as A`; `#[10, 20, 30]` is an array literal.
- `empty() -> Array[a]` - Empty array
- `make(size: int, default: a) -> Array[a]` - Fixed-size array initialized to `default`
- `append(arr, val) -> Array[a]` - Return new array with `val` appended
- `unsafeGet(arr, idx) -> a` - Get; out of bounds is a runtime error — use `getOpt` for safety
- `get`/`set` out of bounds are errors naming the index and length (`set` never returns the array unchanged)
**Float arrays.** An `Array[float]` is stored unboxed (8 bytes per element; a 10M-element array is ~130 MB,
the same data as `[float]` is ~2.5 GB). Native kernels: `dot`, `axpy`, `scale`, `add`, `sub`, `mul`, `sum`,
`argmax`; two-array kernels require equal lengths. Never update weights one `set` at a time (see Cost
Model): `axpy` updates a whole vector, `scatterAdd(arr, idx, xs)` adds many values with one copy.
Ingest without a `Json` tree: `std/json.decodeFloatArray(s)` for a flat JSON number array, and
`decodeF64LE(bytes)` (exact) / `decodeF32LE(bytes)` (**lossy**: float32 precision) from `std/array`,
all returning `Result[Array[float], string]`.
## Character Processing
Use `chars(s)` to convert a string to a list of single-character strings:
```ailang
module benchmark/solution
import std/string (chars)
import std/list (filter, length)
-- Count vowels in a string
pure func countVowels(s: string) -> int {
let cs = chars(s);
let vowels = filter(func(c: string) -> bool {
c == "a" || c == "e" || c == "i" || c == "o" || c == "u"
}, cs);
length(vowels)
}
export func main() -> () ! {IO} {
println(show(countVowels("hello"))) -- 2
}
```
**Note:** `chars` is Unicode-aware - emoji and accented characters are handled correctly.
## High-Performance String Builtins (v0.10.4)
For email/HTML/CSV-style processing where naive accumulation is O(n²),
prefer these single-pass O(n) builtins from `std/string`:
| Function | Use case |
|----------|----------|
| `decodeQuotedPrintable(s)` | RFC 2045 §6.7 decode (`=20` → space, soft-line-breaks) |
| `replaceMany(s, pairs)` | Multi-pattern replace, e.g. HTML entity decode in one pass |
| `foldSlices(s, delim, acc, f)` | Fold over `split(s, delim)` without allocating the list |
| `mapSlicesJoin(s, delim, f)` | `split → map → join` in O(n), no intermediate list |
| `startsWithIgnoreCase(s, p)` | Case-insensitive prefix check (header parsing) |
```ailang
import std/string (replaceMany, foldSlices, mapSlicesJoin)
-- Decode 23 HTML entities in one pass
let html = replaceMany(raw, [("&", "&"), ("<", "<"), (">", ">")])
-- Sum line lengths without materializing the list of lines
let total = foldSlices(text, "\n", 0, \acc line. acc + length(line))
-- Uppercase each CSV field. The pieces are joined with NO separator:
-- mapSlicesJoin("a,b,c", ",", toUpper) == "ABC". Re-add the delimiter yourself if needed.
let upper = mapSlicesJoin(csv, ",", \field. "${toUpper(field)},")
```
`substring`/`find` have an ASCII fast-path: 24× faster on pure-ASCII input
(common for headers, JSON, XML).
## Regular Expressions (std/regex)
Use a regex instead of hand-rolled `split`/`find`/`chars` parsing for tokens, validation and
extraction. RE2 engine: linear time, no backreferences or lookaround. `compile` is the only
fallible call; matching is total. A `RegexMatch` is `{start, end, text, groups}` (rune indices;
`groups` includes the whole match first). Escape backslashes in string literals: `"\\d+"`
(`"\d"` is a parse error). In `replaceAll`, refer to groups as `$1`.
```ailang
module benchmark/solution
import std/regex as R
import std/list (map)
export func main() -> () ! {IO} {
match R.compile("([a-z]+)=(\\d+)") {
Err(e) => println("bad pattern: ${e}"),
Ok(re) => {
println(show(R.isMatch(re, "x=1"))); -- true
println(show(map(\m. m.text, R.findAll(re, "a=1, bb=22")))); -- [a=1, bb=22]
match R.findFirst(re, "key=42") {
Some(m) => println(show(map(\g. g.text, m.groups))), -- [key=42, key, 42]
None => println("no match")
};
println(R.replaceAll(re, "a=1 b=2", "$2:$1")); -- 1:a 2:b
println(show(R.split(re, "x a=1 y"))) -- [x , y]
}
}
}
```
## Polymorphic Comparison Lambdas (v0.11.4)
Let-bound lambdas using only `<`/`>`/`<=`/`>=`/`==`/`!=` are now properly
polymorphic — they no longer monomorphize to `Int`. This works:
```ailang
let max = \x. \y. if x > y then x else y in {
let a = max(3.14)(2.71); -- Float
let b = max(10)(20); -- Int
let c = max("foo")("bar"); -- String
...
}
```
Before v0.11.4, calling `max(3.14)(2.71)` crashed with `gt_Int: expected
IntValue, got *eval.FloatValue` because the lambda was prematurely defaulted
to `Int`. The constraint is now preserved through generalization, so the
correct dictionary (`gt_Int` / `gt_Float` / `gt_String`) is selected at
each call site.
`Num`/`Fractional` defaulting is unchanged — only `Ord`-only / `Eq`-only /
`Show`-only constraints stay polymorphic.
## Operators
| Type | Operators |
|------|-----------|
| Arithmetic | `+`, `-`, `*`, `/`, `%` (no `**`: use `pow(x, y)` from `std/math`) |
| Comparison | `<`, `>`, `<=`, `>=`, `==`, `!=` |
| Logical | `&&`, `\|\|`, `not` |
| Bitwise | `&` (AND), `^` (XOR), `~` (NOT), `<<` (shift left), `>>` (shift right) |
| List | `++` (concatenation, list-only); `::` (cons/prepend) |
| String | `"${expr}"` interpolation; `concat([parts])`; `join(sep, parts)` |
**Bitwise operators** (int only, C-standard precedence bands):
```ailang
-- AND, XOR, shifts, complement
println(show(12 & 10)) -- 8
println(show(12 ^ 10)) -- 6
println(show(1 << 4)) -- 16
println(show(16 >> 2)) -- 4
println(show(~0)) -- -1
-- XOR is self-inverse (encryption pattern)
let encrypted = 42 ^ 137
let decrypted = encrypted ^ 137 -- 42
-- Note: bitwise OR (|) is not an operator (conflicts with ADT syntax).
-- Use bitwiseOr(a, b) from std/math instead.
```
**Bitwise precedence** (loosest → tightest): `|| && ^ & == < << + *`
- `&` and `^` bind LOOSER than `==` (C convention: use parens in `(x & mask) == 0`)
- `<<`/`>>` bind LOOSER than `+` (C convention: use parens in `1 << (n + 1)`)
- `~` is prefix (same level as unary `-`)
**Signed hash semantics**: AILANG integers are signed 64-bit values. Bitwise and shift operations act on the 64-bit bit pattern. Hash functions may print negative int results even when the underlying bit pattern matches unsigned reference implementations.
- Hex/binary/octal literals are 64-bit patterns: write reference constants verbatim (`0x9e3779b97f4a7c15`, `0xcbf29ce484222325`). One with the top bit set reads as its two's-complement value (`0xffffffffffffffff == -1`). Decimal literals above `9223372036854775807` are an error.
- `>>` is ARITHMETIC (sign-extending: `-1 >> 1 == -1`). The logical/unsigned `>>>` of reference hash code is `shiftRightLogical(x, n)` from std/math (`shiftRightLogical(-1, 1) == 9223372036854775807`). Never emulate it with a mask.
## Boolean Operations
**Boolean operators:**
```ailang
-- AND: &&
if x > 0 && y > 0 then "both positive" else "no"
-- OR: ||
if x == 0 || y == 0 then "has zero" else "no"
-- NOT: both `not` and `!` work
if not isEmpty(list) then process(list) else []
if !done then retry() else finish()
```
**Short-circuit evaluation (v0.11.3):** `&&` and `||` are lazy — the RHS is NOT
evaluated when the LHS determines the result. This makes guarded expressions safe:
```ailang
-- SAFE: charAt(s, i-1) is only called when i > 0
if i > 0 && charAt(s, i - 1) == "\\" then "escaped" else "normal"
-- SAFE: lookup never runs when key is missing
if member(key, m) && lookup(key, m) == target then "match" else "no"
-- || short-circuits: the RHS effect runs only if LHS is false
if cached || expensiveFetch() then "ok" else "fail"
```
`&&` and `||` desugar to `if`: `a && b` → `if a then b else false`,
`a || b` → `if a then true else b`. Both are equivalent to nested `if` but
much more readable.
## String Interpolation (v0.12.1+, PREFERRED)
**Use `"${expr}"` for building strings with embedded values. This is the preferred, idiomatic form.**
```ailang
-- Basic substitution: any expression inside ${...}
let name = "Alice" in
let age = 30 in
println("Hello, ${name}! You are ${age} years old.")
-- → Hello, Alice! You are 30 years old.
-- Arithmetic and function calls inside ${...}
println("Next year: ${age + 1}, square: ${age * age}")
println("First letter: ${substring(name, 0, 1)}")
-- Nested braces work (record literals, field access, let-blocks)
println("Point: ${show({x: 1, y: 2}.x)}")
println("Squared: ${let sq = age * age in sq}")
-- Escape with backslash to get a literal ${
println("Use \${var} to interpolate.")
-- → Use ${var} to interpolate.
```
**How it works:** `"Hello, ${x}!"` desugars to `concat_String(concat_String("Hello, ", show(x)), "!")`.
`show_String` is identity, so string-typed values are spliced without quoting. Any `Show`-able type
works automatically (int, float, bool, records, ADTs).
## String and List Concatenation
**`++` is for lists only (v0.13.0+). For strings, use `"${...}"` interpolation,
`concat([parts])` from `std/string`, or `join(sep, parts)`.**
```ailang
-- Strings: use interpolation
let msg = "Count: ${length(items)}"
let greeting = "Hello ${name}"
-- Strings: concat a list of parts
import std/string (concat)
let s = concat(["Hello", " ", "World"])
-- Lists: ++ concatenates
let combined = [1, 2] ++ [3, 4] -- [1, 2, 3, 4]
```
## Pattern Matching
**CRITICAL: When a match arm needs multiple let bindings, wrap in `{ }`:**
```ailang
-- WRONG: multiple lets without braces causes parse error
match queue {
[] => result,
node :: rest =>
let x = process(node);
let y = update(x); -- ERROR: expected => got let
recurse(rest, y)
}
-- CORRECT: wrap multiple lets in { }
match queue {
[] => result,
node :: rest => {
let x = process(node);
let y = update(x);
recurse(rest, y)
}
}
```
**On lists:**
```ailang
match xs {
[] => 0,
x :: rest => x + sum(rest)
}
```
**On ADTs:**
```ailang
match tree {
Leaf(n) => n,
Node(l, v, r) => countNodes(l) + 1 + countNodes(r)
}
```
**On Option:**
```ailang
match result {
Some(x) => x,
None => defaultValue
}
```
**On Records (destructuring):**
```ailang
match person {
{name, age} => "${name} is ${show(age)}"
}
-- Record with renaming
match config {
{host, port: p} => "${host}:${show(p)}"
}
-- Nested record patterns (`...` ignores the other fields at each level)
match data {
{user: {email: e, ...}, ...} => e
}
```
**Record pattern syntax:**
- `{name}` - shorthand, binds field "name" to variable "name"
- `{name: n}` - renaming, binds field "name" to variable "n"
- `{name, age}` - multiple fields
- `{user: {name}}` - nested records
- `{name, ...}` - rest pattern (matches some fields, ignores others)
- **A record pattern must name EVERY field of the record or end with `...`.** `{name}` on a
record that also has `age` is a type error ("record field mismatch: expected 2 fields, got 1").
## Deriving Eq for ADT Types
Use `deriving (Eq)` to auto-generate `==` and `!=` for ADT types:
```ailang
module benchmark/solution
-- Enum-style ADT with derived equality
type Color = Red | Green | Blue deriving (Eq)
-- ADT with fields also works
type Shape = Circle(int) | Rectangle(int, int) deriving (Eq)
export func main() -> () ! {IO} {
let sameColor = Red == Red -- true
let diffColor = Red != Blue -- true
let sameShape = Circle(5) == Circle(5) -- true
let diffShape = Circle(5) != Rectangle(5, 10) -- true
println("Color test: ${show(sameColor)}")
println("Shape test: ${show(sameShape)}")
}
```
**Note:** Derived equality compares by constructor and field values (structural equality).
## Newtype Record Access (v0.8.2)
Single-constructor ADTs wrapping a record support direct field access:
```ailang
type Item = Item({name: string, value: int})
let item = Item({name: "hello", value: 42})
let n = item.name -- "hello" (auto-unwraps the ADT wrapper)
-- Works with map too:
import std/list (map)
let items = [Item({name: "a", value: 1}), Item({name: "b", value: 2})]
let names = map(\t. t.name, items) -- ["a", "b"]
```
**Note:** This only works for single-constructor ADTs with exactly one record field. For multi-constructor ADTs, use pattern matching to access fields.
## Polymorphic ADTs with Mixed Field Types
ADT constructors can mix polymorphic and concrete field types:
```ailang
module benchmark/solution
import std/string (stringToInt)
-- Result type: Ok uses type parameter, Err always takes string
type Result[a] = Ok(a) | Err(string)
-- Either type: both sides polymorphic
type Either[a, b] = Left(a) | Right(b)
-- Validation: success is polymorphic, errors are string list
type Validation[a] = Valid(a) | Invalid([string])
-- Usage examples
pure func safeDivide(a: int, b: int) -> Result[int] =
if b == 0 then Err("division by zero") else Ok(a / b)
pure func parsePositive(s: string) -> Result[int] =
match stringToInt(s) {
Some(n) => if n > 0 then Ok(n) else Err("must be positive"),
None => Err("not a number")
}
export func main() -> () ! {IO} {
let r1 = safeDivide(10, 2); -- Ok(5)
let r2 = safeDivide(10, 0); -- Err("division by zero")
match r1 {
Ok(v) => println("Got: ${show(v)}"),
Err(msg) => println("Error: ${msg}")
};
match r2 {
Ok(v) => println("Got: ${show(v)}"),
Err(msg) => println("Error: ${msg}")
}
}
```
**Key pattern:** When a constructor has a concrete type like `Err(string)`, that type is preserved regardless of the ADT's type parameter. This enables idiomatic error handling where errors are always strings but success values can be any type.
**Option type with findFirst and mapOption:**
```ailang
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: one let per line, 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)
println(match doubled { Some(v) => "Found: ${show(v)}", None => "Not found" })
}
```
**ADT state machine (traffic light):**
```ailang
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)
println(showState(s2))
}
```
## JSON
**Common JSON imports (use all you need):**
```ailang
-- For building JSON
import std/json (encode, jo, ja, kv, js, jnum, jint, jb, jn)
-- For parsing JSON
import std/json (decode, get, getString, getInt, getArray, asString, asNumber, asArray)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)
```
**Build and encode JSON:**
```ailang
import std/json (encode, jo, ja, kv, js, jnum, jb, jn)
-- JSON constructors: js(string), jnum(float), jint(int), jb(bool), jn() for null
-- jint(n) is the integer convenience for jnum(intToFloat(n))
-- jo([kv(k,v)...]) for objects, ja([...]) for arrays
-- Build JSON object with all types
let json = encode(jo([
kv("name", js("Alice")),
kv("age", jnum(30.0)),
kv("active", jb(true))
]))
-- Result: "{\"name\":\"Alice\",\"age\":30,\"active\":true}"
-- Build JSON array
let arr = encode(ja([jnum(1.0), jnum(2.0), jnum(3.0)]))
-- Result: "[1,2,3]"
```
**Decode JSON (returns Result):**
```ailang
import std/json (decode, Json, JObject, JString)
let result = decode("{\"name\":\"Alice\"}");
match result {
Ok(json) => print(show(json)),
Err(msg) => print("Parse error: ${msg}")
}
```
**Parse JSON array directly (IMPORTANT - use asArray!):**
```ailang
module benchmark/solution
import std/json (decode, asArray, getString)
import std/option (Some, None)
import std/result (Ok, Err)
-- When decode returns an array, use asArray to convert it
export func main() -> () ! {IO} {
let json = "[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]";
match decode(json) {
Ok(parsed) => match asArray(parsed) {
Some(items) => {
-- items is now [Json], iterate with recursion
printNames(items)
},
None => println("Not an array")
},
Err(e) => println("Parse error: ${e}")
}
}
func printNames(items: [Json]) -> () ! {IO} =
match items {
[] => (),
item :: rest => {
match getString(item, "name") {
Some(name) => println(name),
None => ()
};
printNames(rest)
}
}
```
**Access JSON values (IMPORTANT for parsing tasks):**
```ailang
import std/json (decode, get, has, getOr, asString, asNumber, asBool, asArray)
import std/option (Option, Some, None)
-- Decode and extract values
match decode("{\"name\":\"Alice\",\"age\":30}") {
Ok(obj) => {
-- get(obj, key) -> Option[Json]
match get(obj, "name") {
Some(j) => match asString(j) {
Some(name) => print("Name: ${name}"),
None => print("name is not a string")
},
None => print("no name field")
};
-- asNumber returns Option[float]
match get(obj, "age") {
Some(j) => match asNumber(j) {
Some(age) => print("Age: ${show(age)}"),
None => print("age is not a number")
},
None => print("no age field")
}
},
Err(msg) => print("Parse error: ${msg}")
}
```
**JSON convenience functions (RECOMMENDED for parsing):**
Use these to reduce nested Option matching from 4 levels to 2:
```ailang
import std/json (decode, getString, getNumber, getInt, getBool, getArray, getObject)
import std/string (intToStr)
import std/option (Option, Some, None)
import std/result (Result, Ok, Err)
-- Extract string field with single match
func getName(obj: Json) -> string =
match getString(obj, "name") {
Some(name) => name,
None => "unknown"
}
-- Extract int field (JSON numbers are floats, getInt truncates)
func getAge(obj: Json) -> int =
match getInt(obj, "age") {
Some(age) => age,
None => 0
}
-- Full example
export func main() -> () ! {IO} =
match decode("{\"name\":\"Alice\",\"age\":30}") {
Ok(obj) => print("${getName(obj)} is ${intToStr(getAge(obj))}"),
Err(e) => print("Error: ${e}")
}
```
**JSON accessor functions:**
- `getString(obj, key)` -> `Option[string]` - Get string value directly
- `getNumber(obj, key)` -> `Option[float]` - Get float value directly
- `getInt(obj, key)` -> `Option[int]` - Get int value (truncates float)
- `getBool(obj, key)` -> `Option[bool]` - Get boolean value directly
- `getArray(obj, key)` -> `Option[[Json]]` - Get array value by key
- `getObject(obj, key)` -> `Option[Json]` - Get nested object directly
- `asString(j)` -> `Option[string]` - Convert Json to string
- `asNumber(j)` -> `Option[float]` - Convert Json to number
- `asBool(j)` -> `Option[bool]` - Convert Json to bool
- `asArray(j)` -> `Option[[Json]]` - Convert Json to array (USE THIS for top-level arrays!)
**JSON array type extraction:**
Extract typed arrays from JSON with two variants: permissive (skip non-matching) and strict (fail on mismatch).
```ailang
import std/json (decode, getArray, filterStrings, allStrings, getStringArrayOrEmpty)
import std/option (Some, None)
import std/result (Ok, Err)
-- Permissive: filterStrings skips non-strings
let mixed = [JString("a"), JNumber(1.0), JString("b")];
filterStrings(mixed) -- ["a", "b"]
-- Strict: allStrings fails if ANY element is not a string
allStrings(mixed) -- None (has JNumber)
allStrings([JString("a"), JString("b")]) -- Some(["a", "b"])
-- Convenience wrappers for common patterns:
match decode("{\"tags\": [\"a\", \"b\"]}") {
Ok(obj) => {
-- getStringArrayOrEmpty: returns [] if missing or invalid
let tags = getStringArrayOrEmpty(obj, "tags"); -- ["a", "b"]
let missing = getStringArrayOrEmpty(obj, "nope"); -- []
println("Got ${show(length(tags))} tags")
},
Err(e) => println("Error: ${e}")
}
```
## HTTP Requests
**Simple GET/POST** (return body string directly):
```ailang
import std/net (httpGet, httpPost)
let body = httpGet("https://example.com") -- returns string
let resp = httpPost("https://api.example.com", jsonData) -- returns string
```
**Advanced with headers** (returns `Result[HttpResponse, NetError]`):
```ailang
import std/net (httpRequest)
import std/json (decode)
-- httpRequest returns Result[HttpResponse, NetError]
-- HttpResponse = {status: int, headers: [...], body: string, ok: bool}
-- IMPORTANT: Ok(resp) captures the FULL HttpResponse record
-- Use resp.body to get the body string, NOT resp directly
let headers = [{name: "Authorization", value: "Bearer token"}];
match httpRequest("POST", url, headers, body) {
Ok(resp) => decode(resp.body), -- resp.body is the string
Err(Transport(msg)) => Err("Error: ${msg}"),
Err(_) => Err("Other error")
}
```
## AI Effect
Call external LLMs with string->string or structured JSON interfaces:
```ailang
import std/ai (call, callJson, callJsonSimple)
import std/json (decode)
import std/result (Result, Ok, Err)
-- Unstructured: string -> string
func ask_ai(question: string) -> string ! {AI} = call(question)
-- Structured JSON (no schema): provider returns valid JSON
func ask_json(prompt: string) -> string ! {AI} = callJsonSimple(prompt)
-- Structured JSON (schema-enforced): provider validates against schema
func ask_person(prompt: string) -> string ! {AI} =
callJson(prompt, "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}")
```
**Parse JSON responses** with `std/json.decode`:
```ailang
let raw = callJsonSimple("Return a JSON array");
match decode(raw) {
Ok(json) => println("Valid JSON!")
Err(msg) => println("Parse error: ${msg}")
}
```
**Run with providers:**
```bash
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
ailang run --caps IO,AI --ai ollama:llama3 --entry main file.ail # Ollama (local)
ailang run --caps IO,AI --ai openrouter/auto --entry main file.ail # OpenRouter (~100 models)
ailang run --caps IO,AI --ai-stub --entry main file.ail # Testing stub
```
**Model strings**: plain provider names (`claude-sonnet-4-5`, `gemini-2-5-flash`)
hit that vendor directly. `vendor/model` strings (`anthropic/claude-sonnet-4.5`,
`openai/gpt-5-mini`, `openrouter/auto`) route through OpenRouter — needs
`OPENROUTER_API_KEY`.
**Authentication setup (required before using `--ai`):**
| Provider | Env Variable | How to get it |
|----------|-------------|---------------|
| Anthropic (`claude-*`) | `ANTHROPIC_API_KEY` | https://console.anthropic.com/ |
| OpenAI (`gpt-*`) | `OPENAI_API_KEY` | https://platform.openai.com/api-keys |
| Ollama (`ollama:*`) | None needed | Local server at http://localhost:11434 |
**Google Gemini (`gemini-*`) has two auth options:**
Option 1 — API Key (AI Studio, simplest for getting started):
```bash
export GOOGLE_API_KEY=AIza...
# Get one at: https://aistudio.google.com/apikey
ailang run --caps IO,AI --ai gemini-2-5-flash --entry main file.ail
```
Option 2 — Application Default Credentials (Vertex AI, for GCP projects):
```bash
# One-time setup:
gcloud auth application-default login
# IMPORTANT: GOOGLE_API_KEY must be UNSET for Vertex AI to be used.
# AILANG checks GOOGLE_API_KEY first — if set, it always uses AI Studio.
unset GOOGLE_API_KEY
ailang run --caps IO,AI --ai gemini-2-5-flash --entry main file.ail
```
**Gemini auth selection logic:**
```
GOOGLE_API_KEY is set (non-empty) → AI Studio (API key auth)
GOOGLE_API_KEY is unset or empty → Vertex AI (ADC via gcloud)
```
**Common gotcha:** If `GOOGLE_API_KEY` is in your shell profile for other tools,
AILANG will always use AI Studio even if you want Vertex AI. Fix:
```bash
# Unset just for this command:
GOOGLE_API_KEY= ailang run --caps IO,AI --ai gemini-2-5-flash --entry main file.ail
```
**Quick test that auth works:**
```bash
# Test with stub (no auth needed):
ailang run --caps IO,AI --ai-stub --entry main file.ail
# Then swap --ai-stub for --ai <model> once env vars are set.
```
## ZIP Archives (std/zip)
Read ZIP archives (including .docx, .xlsx, .pptx files). Requires `FS` effect:
```ailang
module benchmark/solution
import std/zip (listEntries, readEntry, readEntryBytes)
import std/list (length)
-- listEntries(path) -> Result[[string], string] ! {FS}
-- readEntry(path, entry) -> Result[string, string] ! {FS} (UTF-8 text)
-- readEntryBytes(path, entry) -> Result[string, string] ! {FS} (base64)
export func main() -> () ! {IO, FS} {
match listEntries("document.docx") {
Ok(entries) => println("Found ${show(length(entries))} entries"),
Err(msg) => println("Error: ${msg}")
}
}
```
Run: `ailang run --entry main --caps IO,FS file.ail`
## Tar + Gzip (std/tar, std/gzip) — v0.12.0+
Native reading of `.tar`, `.tar.gz`, and raw gzip streams. No shell-out, no temp files. Matches `std/zip` conventions: binary data crosses as base64.
```ailang
import std/tar (readFromGzip, extractAll)
import std/gzip (decompress, decompressFile)
-- Pull one file straight from a .tar.gz (primary use: arXiv bundles)
match readFromGzip("paper.tar.gz", "main.tex") {
Ok(tex) => println(tex),
Err(msg) => println("read failed: ${msg}")
}
-- Safe extraction: rejects ../ entries, symlinks, absolute paths
match extractAll("archive.tar", "./dest") {
Ok(paths) => println("wrote ${show(length(paths))} files"),
Err(msg) => println("blocked: ${msg}")
}
```
- `std/gzip`: `decompress(b64)`, `compress(b64, level)` — **pure**; `decompressFile(path) ! {FS}`
- `std/tar`: `listEntries`, `readEntry`, `readEntryBytes`, `extractAll`, `readFromGzip`, `readFromGzipBytes` — all `! {FS}`
- Caps: 10K entries, 100MB decompressed per entry (bomb defence). Respects `AILANG_FS_SANDBOX`.
## Raw Deflate / Zlib (std/deflate) — v0.16.0+
For raw zlib/deflate streams *without* the gzip or zip wrapper. Use this for **PDF FlateDecode** (PDF /ObjStm in PDF 1.5+), HTTP `Content-Encoding: deflate`, PNG IDAT chunks, WebSocket permessage-deflate.
```ailang
import std/deflate (inflateZlib)
import std/result (Result, Ok, Err)
-- Decode a PDF FlateDecode payload (zlib-wrapped, RFC 1950)
match inflateZlib(b64_of_objstm) {
Ok(decompressedB64) => println(decompressedB64),
Err(msg) => println("inflate failed: ${msg}")
}
```
- **All four functions are pure** (no effect row): `inflate(b64)`, `inflateZlib(b64)`, `deflate(b64, level)`, `deflateZlib(b64, level)`
- `inflate`/`deflate` = raw RFC 1951 (no header, no trailer); `inflateZlib`/`deflateZlib` = RFC 1950 (2-byte zlib header + adler32). **For PDF/HTTP/PNG: use the Zlib variants.**
- Same base64-string boundary as `std/gzip` and `std/zip.readEntryBytes`. Same 100MB output cap.
## XML Parsing (std/xml)
Parse and query XML documents. **Pure functions** (no effect needed):
```ailang
module benchmark/solution
import std/result (Result, Ok, Err)
import std/option (Option, Some, None)
export func main() -> () ! {IO} {
let xml = "<root><item id=\"1\">Hello</item><item id=\"2\">World</item></root>";
match _xml_parse(xml) {
Ok(doc) => {
-- Find all <item> elements
let items = _xml_findAll(doc, "item");
-- Get text content
let text = _xml_getText(doc);
-- Find first match
match _xml_findFirst(doc, "item") {
Some(item) => {
println("Tag: ${_xml_getTag(item)}");
println("Text: ${_xml_getText(item)}");
match _xml_getAttr(item, "id") {
Some(id) => println("ID: ${id}"),
None => ()
}
},
None => println("No items found")
}
},
Err(msg) => println("Parse error: ${msg}")
}
}
```
**XML builtins (all pure):**
| Function | Type | Description |
|----------|------|-------------|
| `_xml_parse` | `string -> Result[XmlNode, string]` | Parse XML string |
| `_xml_findAll` | `(XmlNode, string) -> [XmlNode]` | Find all elements by tag |
| `_xml_findFirst` | `(XmlNode, string) -> Option[XmlNode]` | Find first element by tag |
| `_xml_getText` | `XmlNode -> string` | Extract text content |
| `_xml_getAttr` | `(XmlNode, string) -> Option[string]` | Get attribute value |
| `_xml_getChildren` | `XmlNode -> [XmlNode]` | Get child nodes |
| `_xml_getTag` | `XmlNode -> string` | Get tag name (empty for non-Element) |
**Combine ZIP + XML to parse .docx files:**
```ailang
-- Read XML from inside a .docx (which is a ZIP archive)
match _zip_readEntry("report.docx", "word/document.xml") {
Ok(xml) => match _xml_parse(xml) {
Ok(doc) => {
let paragraphs = _xml_findAll(doc, "w:p");
println("Found ${show(length(paragraphs))} paragraphs")
},
Err(e) => println("XML error: ${e}")
},
Err(e) => println("ZIP error: ${e}")
}
```
## Streaming XML / Bounded Folds (v0.10.1, v0.11.3)
For large XML (multi-MB) inside ZIP archives, `parseFold` and `scanFold`
fold over `<tag>` elements **without materializing the whole document**:
```ailang
import std/xml (parseFold, getText, getAttr)
import std/zip (scanFold)
import std/iter (FoldStep, Continue, Stop)
import std/option (Some, None)
-- Pure: fold over rows in an XML string
let total = parseFold(xml, "row", 0, \acc node.
acc + 1
)
-- Effectful: fold over rows directly from a ZIP entry (never materializes XML)
let count = scanFold("data.xlsx", "xl/sharedStrings.xml", "si", 0,
\acc node. acc + 1
)
```
**Bounded prefix scans (v0.11.3):** When you only need the first N rows of a
50K-row sheet, return `Stop(acc)` to halt the scan immediately:
```ailang
import std/xml (parseFoldStep)
import std/iter (FoldStep, Continue, Stop)
-- Take first 5000 rows then stop — the rest of the document isn't scanned
let first5k = parseFoldStep(xml, "row", [], \acc node.
if length(acc) >= 5000
then Stop(acc)
else Continue(acc ++ [getText(node)])
)
```
`parseFoldStep` and `scanFoldStep` mirror `parseFold`/`scanFold` but the
handler returns `FoldStep[a] = Continue(a) | Stop(a)`. Use them whenever
the document is much larger than what you need.
## Maps (std/map, v0.10.1)
`Map[k, v]` — immutable hash map: O(1) `lookup`/`member`, but `insert`/`remove` copy the whole map (O(n)).
Build with `M.fromList` (O(n); a duplicate key keeps the LAST value). Keys: int, string, bool, float,
unit, and tuples, records and ADT constructors of those, so `M.lookup(m, (r, c))` works (a function or
list key is a runtime error). Example: `ailang examples show map_tuple_record_keys`.
```ailang
import std/map as M
let m0 = M.empty()
let m1 = M.insert(m0, "alice", 30)
let m2 = M.insert(m1, "bob", 25)
match M.lookup(m2, "alice") {
Some(age) => println(show(age)), -- 30
None => println("missing")
}
println(show(M.size(m2))) -- 2
println(show(M.member(m2, "bob"))) -- true
-- Iteration is sorted (deterministic)
let xs = M.toList(m2) -- [("alice", 30), ("bob", 25)]
let m3 = M.fromList([("c", 1), ("d", 2)])
```
Builtins: `empty`, `insert`, `lookup`, `member`, `remove`, `size`,
`keys`, `values`, `fromList`, `toList`. Iteration order is by encoded key, so int keys
come out lexically: `M.keys(M.fromList([(9, "a"), (10, "b"), (2, "c")]))` is `[10, 2, 9]`.
## JWT Verification (std/jwt, v0.10.0)
Pure JWT parsing and RS256 signature verification (e.g., Firebase ID tokens):
```ailang
import std/jwt (decodeJWT, verifyRS256, verifyWithKid, isExpired, checkIssuer)
import std/json (encode, jo, kv, js)
-- Decode without verification (for inspection). The Ok value is a record
-- {header: Json, payload: Json, signature: bytes, signed: string}; use field access
-- (or a pattern that names all four fields / ends with `...`).
match decodeJWT(token) {
Ok(jwt) => println(encode(jwt.payload)),
Err(msg) => println("decode failed: ${msg}")
}
-- Verify RS256 signature against a PEM public key
match verifyRS256(token, pemPublicKey) {
Ok(claims) => if isExpired(claims, nowUnix) then "expired" else "valid",
Err(msg) => "invalid: ${msg}"
}
-- Firebase/OAuth pattern: select the key by the token's 'kid' header.
-- The second argument is a Json OBJECT mapping kid -> PEM string (not a function).
let keys = jo([kv("key-2026", js(pemPublicKey))])
verifyWithKid(token, keys) -- Result[Json, string]; an unknown kid is Err
```
Backed by `rsaVerifyPKCS1v15(message, signature, publicKeyPEM)` in
`std/crypto`, plus `fromBase64URL`/`toBase64URL` in `std/bytes` (RFC 4648 §5,
no padding — the pair round-trips exactly).
JWT functions are pure — fetch keys yourself via `std/net`.
## Custom Tracing (std/trace, v0.11.1)
Emit OTEL-compatible spans and events from AILANG code. Requires `Trace` effect:
```ailang
import std/trace (spanStart, spanEnd, event)
export func processBatch(items: [Item]) -> int ! {Trace, IO} {
spanStart("batch.process");
event("batch.size", show(length(items)));
let n = doWork(items);
spanEnd("batch.process");
n
}
```
Run: `ailang run --caps IO,Trace --emit-trace jsonl --entry main file.ail`
In WASM, register a JS callback via `ailangSetTraceHandler` to receive
events live (function_enter/exit, effect, contract_check, budget_delta).
## Process Exit (v0.10.1)
`exit(code: int) -> ()` in `std/io` terminates the process with a specific
exit code. Required for CLI tools that need to signal failure:
```ailang
import std/io (println, exit)
export func main() -> () ! {IO} {
match validate(args) {
Ok(()) => println("ok"),
Err(msg) => {
println("error: ${msg}");
exit(1)
}
}
}
```
Telemetry/traces are flushed before the process exits.
## Cost Model: Lists, Arrays (std/array), Maps (std/map), Sets (std/set)
Values are immutable: an "update" returns a copy. Reads are cheap; growing one element at a time is not.
| Operation | Cost |
|---|---|
| `nth(xs, i)`, `length(xs)`, matching `x :: rest` (`rest` shares storage) | O(1) |
| `x :: xs`, `xs ++ ys` | O(n): copies the list |
| `map`, `filter`, `flatMap`, `zip`, `range`, `mapAccumL` / `sortBy` | O(n) / O(n log n) |
| `A.get`, `A.getOpt` (None out of bounds), `A.length` | O(1) |
| `A.set`, `A.append` | O(n): copies the array |
| `A.make`, `A.fromList`, `A.updateMany(arr, [(i, v), ...])`, `A.accumulate(arr, [(i, x), ...], combine)` | O(n + k), one copy |
| `M.lookup`, `M.member`, `M.size`, `S.member`, `S.size` | O(1) |
| `M.insert`, `M.remove`, `S.insert`, `S.remove` | O(n): copies the map / set |
| `M.fromList`, `S.fromList` / `M.keys`, `M.toList`, `S.toList`, `S.union`, `S.intersect`, `S.difference` | O(n) / O(n log n) |
**One `acc ++ [x]`, `x :: acc`, `A.set`, `M.insert` or `S.insert` per loop step is O(n²) overall**
(20,000 `M.insert`s or `S.insert`s take ~5 s, ×4 per doubling): fine for a few thousand steps, too
slow for 10^5. Instead:
- Build the whole list in one pass (`map`/`flatMap`/`range`; `mapAccumL` for running state), then
convert ONCE: `A.fromList` for indexed access, `M.fromList` for lookup by key.
- A 2D grid is one flat Array: cell `(r, c)` at `r * w + c`.
- Many writes: collect `(index, value)` pairs, apply ONE `A.updateMany` (a later write wins) or ONE
`A.accumulate(arr, writes, \a b. a + b)` to combine (difference array, histogram, counts).
- Visited set: `S.member(seen, (r, c))` is O(1). For a few thousand states, `S.insert` per state is
fine; for more, add a whole BFS level at once with `S.union(seen, S.fromList(level))` (one copy
per level, not per state). A
deterministic walk (state → next state) that may loop needs no set: Floyd's tortoise and hare
(one cursor steps once, the other twice, until they meet) uses O(1) memory.
- Worked examples: `ailang examples show visited_set_bfs` (also `cycle_detection_visited_set`,
`map_tuple_record_keys`, `difference_array_accumulate`).
```ailang
module benchmark/solution
import std/list (map, flatMap, range, mapAccumL)
import std/array as A
import std/map as M
export func main() -> () ! {IO} {
let h = 3;
let w = 4;
let grid = A.fromList(flatMap(\r. map(\c. r * 10 + c, range(0, w)), range(0, h)));
println(show(A.get(grid, 2 * w + 1))); -- 21: cell (2, 1)
let g2 = A.updateMany(grid, [(0, 99), (5, 7), (0, 1)]);
println(show(A.get(g2, 0))); -- 1: the later write to index 0 wins
let byName = M.fromList(map(\i. ("k${show(i)}", i), range(0, 100)));
println(show(M.lookup(byName, "k42"))); -- Some(42)
match mapAccumL(\s x. (s + x, s + x), 0, [1, 2, 3]) {
(prefix, total) => println("${show(prefix)} ${show(total)}") -- [1, 3, 6] 6
}
}
```
## List Operations
```ailang
-- Recursive sum: matching x :: rest does not copy, so this is O(n)
func sum(xs: [int]) -> int =
match xs {
[] => 0,
x :: rest => x + sum(rest)
}
```
**List index access:**
```ailang
import std/list (nth, last, any, findIndex)
import std/option (Some, None)
let xs = [10, 20, 30];
-- nth: get element by index (0-based)
match nth(xs, 1) {
Some(x) => print("Element at 1: ${show(x)}"), -- 20
None => print("Index out of bounds")
};
-- last: get last element
match last(xs) {
Some(x) => print("Last: ${show(x)}"), -- 30
None => print("Empty list")
};
-- any: check if any element matches predicate
let hasEven = any(\x. x % 2 == 0, xs); -- true (20 is even)
-- findIndex: find index of first matching element
match findIndex(\x. x > 15, xs) {
Some(i) => print("First > 15 at index: ${show(i)}"), -- 1
None => print("Not found")
}
```
## Contracts & Verification (USE THIS — your biggest advantage!)
**Contracts let you PROVE your code is correct for ALL inputs, not just test cases.** This is the single most powerful feature for AI agents: write a function with contracts, run `ailang verify`, and Z3 mathematically proves it correct or gives you the exact counterexample to fix.
**Agent workflow:** Write contracts FIRST as your specification, implement the function, then verify:
1. `requires { ... }` — what the caller must guarantee (precondition)
2. `ensures { ... }` — what the function guarantees back (postcondition, `result` = return value)
3. `ailang verify file.ail` — Z3 proves it or shows the exact failing input
```ailang
module myapp/billing
import std/string (length as strLength, startsWith)
import std/list (length as listLength)
-- Enum ADT + contract: Z3 checks ALL tax brackets automatically
export type TaxBracket = STANDARD | REDUCED | EXEMPT
export func calculateTax(income: int, bracket: TaxBracket) -> int ! {}
requires { income >= 0 }
ensures { result >= 0 }
{
match bracket {
EXEMPT => 0,
REDUCED => income / 10,
STANDARD => income / 5
}
}
-- Cross-function verification: Z3 inlines calculateTax to prove this
export func netIncome(gross: int, bracket: TaxBracket) -> int ! {}
requires { gross >= 0 }
ensures { result >= 0 }
{
gross - calculateTax(gross, bracket)
}
-- String verification using stdlib imports
export func isValidPromo(code: string) -> bool ! {}
ensures { result == (startsWith(code, "PROMO-") && strLength(code) >= 8) }
{
startsWith(code, "PROMO-") && strLength(code) >= 8
}
-- Record verification: field-level contracts
export func netFromSummary(inv: {subtotal: int, tax: int, discount: int}) -> int ! {}
requires { inv.subtotal >= 0, inv.tax >= 0, inv.discount >= 0, inv.discount <= inv.subtotal }
ensures { result >= 0 }
{
inv.subtotal - inv.discount + inv.tax
}
-- List verification using stdlib
export func addItem(price: int, items: [int]) -> int ! {}
ensures { result == listLength(items) + 1 }
{
listLength(price :: items)
}
```
**Verify with Z3** (proves contracts correct for ALL inputs at compile time):
```bash
ailang verify file.ail # Prove contracts for all functions
ailang verify --verbose file.ail # Show generated SMT-LIB
ailang verify --json file.ail # Machine-readable output
ailang verify --strict file.ail # Exit 1 if any function can't be verified
```
**Example output** — Z3 proves 5 functions and catches a bug in the 6th:
```
✓ VERIFIED calculateTax 6ms
✓ VERIFIED netIncome 8ms # cross-function: inlines calculateTax
✓ VERIFIED isValidPromo 5ms # string theory
✓ VERIFIED netFromSummary 7ms # record fields
✓ VERIFIED addItem 5ms # list/sequence theory
✗ VIOLATION brokenDiscount
Counterexample:
price: Int = 0
discount: Int = 1
```
When Z3 finds a violation, it gives you the EXACT inputs that break the contract. Fix the function, re-verify — no guessing.
**Runtime contract checking** (alternative to static verification):
```bash
ailang run --verify-contracts --caps IO --entry main file.ail
```
**What can be verified** (decidable fragment):
- Types: `int`, `bool`, `string`, enum ADT, record, `[int]` lists
- Arithmetic (`+`, `-`, `*`, `/`), comparison (`>=`, `<=`, `==`, `!=`), logical (`&&`, `||`), bitwise (`&`, `^`, `~`, `<<`, `>>`)
- `if`/`else`, `let` bindings, `match` on enums/ADTs
- String ops (use `std/string`): `length`, `startsWith`, `endsWith`, `find`, `substring`, `contains`; `concat` (list of strings); `"${expr}"` interpolation
- List ops: `length` (from `std/list`), `_list_head`, `_list_nth`, cons (`::`), concat (`++`), literals
- Records: field access (`r.field`), construction (`{x: 1, y: 2}`), ensures with `result.x`
- Cross-function calls: Z3 inlines callees to reason about full call chains
- Must be pure: `! {}` (no effects), non-recursive, non-higher-order
**What gets skipped:** Recursive functions, higher-order functions, effectful functions, float parameters.
**Rules:**
- `requires { expr }` — precondition; `ensures { expr }` — postcondition
- `result` refers to the return value (only in `ensures`)
- Contracts go BETWEEN the effect annotation `! {}` and the function body `{ ... }`
- Use stdlib imports in contracts (`std/string`, `std/list`) — Z3 resolves them natively
- For list `head`/`nth` in contracts, use builtins `_list_head`/`_list_nth` (stdlib versions return `Option`)
- Install Z3: `brew install z3` (macOS) or `apt install z3` (Linux)
### Structure Code for Z3 Verification
**Whenever possible, structure your code so Z3 can verify it.** This means preferring the decidable fragment (pure, non-recursive, non-higher-order) for core logic, even when the outer program uses effects and recursion.
**Pattern: Pure core + effectful shell.** Extract pure computation into small, contract-bearing functions. Wrap them in effectful entry points:
```ailang
-- GOOD: Z3 can verify the pure core
pure func applyDiscount(price: int, pct: int) -> int ! {}
requires { price >= 0, pct >= 0, pct <= 100 }
ensures { result >= 0, result <= price }
{
price - (price * pct) / 100
}
-- Effectful shell just calls the verified core
export func main() -> () ! {IO} {
let finalPrice = applyDiscount(1000, 15);
println(show(finalPrice))
}
```
**Guidelines:**
- **Split logic from effects** — Keep arithmetic, validation, and decision logic in pure `! {}` functions with contracts. Push IO/Net/FS to the outer layer.
- **Prefer enums over strings for decisions** — `match bracket { STANDARD => ..., REDUCED => ... }` is fully decidable; string parsing is not.
- **Use `int` over `float` where possible** — Integer arithmetic is fully decidable in Z3; floats are not verifiable.
- **Keep verified functions non-recursive** — If you need recursion, have the recursive function call a pure verified helper for its core logic.
- **Add contracts to ALL pure functions** — Even simple ones. `ensures { result >= 0 }` catches edge cases you won't think to test.
- **Decompose complex logic into small verifiable pieces** — Instead of one 20-line function, write 3-4 small functions each with tight contracts. Z3 inlines across calls automatically.
**Think of it this way:** Every function you can mark `! {}` with a contract is a function that is *mathematically proven correct* — not just tested. Maximize the surface area of verified code.
## Testing
**Inline tests on functions (recommended):**
```ailang
-- 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
```
```ailang
-- myapp/data.ail
module myapp/data
export type User = { name: string, age: int }
-- myapp/main.ail
module myapp/main
import myapp/data (User)
export func main() -> () ! {IO} = print("Hello")
```
Run: `ailang run --entry main --caps IO myapp/main.ail`
## Reserved Keywords
AILANG reserves 43 keywords. You **cannot** use these as variable or function names.
| Category | Keywords |
|----------|----------|
| Control Flow | `if`, `then`, `else`, `match`, `with`, `select`, `timeout` |
| Definitions | `func`, `pure`, `let`, `letrec`, `in` |
| Type System | `type`, `class`, `instance`, `forall`, `exists`, `deriving` |
| Modules | `module`, `import`, `export`, `extern` |
| Testing | `test`, `tests`, `property`, `properties`, `assert` |
| Verification | `requires`, `ensures`, `invariant` |
| Concurrency | `spawn`, `parallel`, `channel`, `send`, `recv` |
| Boolean | `true`, `false`, `and`, `or`, `not` |
**Common mistake:**
```ailang
-- WRONG: 'exists' is reserved (for existential types)
let exists = fileExists(path)
-- CORRECT: use alternative name
let found = fileExists(path)
```
## External Package Imports
Use `pkg/` prefix for external packages:
```ailang
import std/io (println) -- Stdlib (bundled)
import myproject/utils (helper) -- Local module
import pkg/sunholo/gcp_auth/token (getAccessToken) -- External package
import pkg/sunholo/auth/keys (validateKeyHash) -- External package
```
Package names use **underscores**, never hyphens. A hyphen in an import path parses as
subtraction and fails with `PAR_HYPHEN_IN_IMPORT`. The registry name is canonical
(`sunholo/gcp_auth`); the repo directory may be spelled `packages/gcp-auth`, but that is
a folder name, never an import path.
### Registry Workflow (recommended)
```bash
ailang search "auth" # Discover packages by keyword
ailang search --tag gcp # Filter by tag
ailang install sunholo/auth@0.1.0 # Download, verify hash, add to ailang.toml
ailang pkg-docs sunholo/auth # Read AGENT.md (structured AI usage guide)
```
Search results include `ai_summary`, `tags`, and `effects` per package — use effects to know what capabilities (`IO`, `Net`, `FS`, etc.) a dependency requires.
### Alternative: Git or path dependencies
```bash
ailang add --git https://github.com/sunholo-data/ailang-packages --subdir packages/auth --tag v0.1.0
ailang add --path ../ailang-packages/packages/auth # Local development
ailang lock # Resolve and lock all deps
```
### Publishing
```bash
ailang init package --name myorg/mylib # Create ailang.toml with exports, effects, metadata
ailang publish --dry-run # Preview: compile check, effect validation, contract verification
ailang publish # Upload to registry (immutable — versions cannot be overwritten)
```
Include an `AGENT.md` in your package root for AI agent consumers — it's served via `ailang pkg-docs` and indexed in registry search results.
### Shipping a command (`[bin]`)
A package can ship a CLI, not just modules. Declare it in `ailang.toml`; `ailang install`
then puts a shim on PATH (`~/.ailang/bin`) that runs the module with the package as program
root — no clone, no wrapper script, no hand-written runtime directory.
```toml
[bin]
eparse = "cli" # module <pkg>/cli, entry main, caps auto (inferred)
docparse = { module = "docparse/main", caps = "IO,FS,Env,AI", run_flags = ["--max-recursion-depth", "50000"] }
```
```bash
ailang install sunholo/email # library + `eparse` on PATH
ailang install --path packages/email # developer loop: shim this checkout without publishing
ailang bin list # installed shims, with package@version
```
The command reads its arguments with `getArgs()` from `std/env` (`--caps Env`); flag-like
arguments reach it untouched. `ailang publish` refuses a `[bin]` whose module is missing or
whose entry is not exported. Do NOT write a `scripts/` launcher that `cd`s into a checkout and
`ailang run`s a file — that is what `[bin]` replaces.
**Available packages**: `sunholo/gcp-auth` (OAuth2), `sunholo/auth` (API keys), `sunholo/http-helpers` (request builders), `sunholo/logging` (JSON logs), `sunholo/config` (env loading), `sunholo/testing-utils` (assertions).
## Module Import Rules
**Imports are NOT transitive.** When module A imports module B, A does **not** get B's imports.
```ailang
-- Module B
module myapp/db
import std/fs (readFile)
export func loadConfig() -> string ! {FS} = readFile("config.json")
-- Module A - WRONG: std/fs not available just because B uses it
module myapp/main
import myapp/db (loadConfig)
let data = readFile("other.txt") -- ERROR: module std/fs not imported
-- Module A - CORRECT: explicitly import what you use
module myapp/main
import std/fs (readFile)
import myapp/db (loadConfig)
let data = readFile("other.txt") -- Works!
```
**Rule: Import everything you directly use in your module.**
## Common Mistakes
### Strict `take` after `flatMap` or allocating `map`
- `take(n, flatMap(f, xs))` materialises every output under strict evaluation.
- Use `takeFlatMap(n, f, xs)` to skip unvisited inputs' outputs.
- It does not shrink the source list or a single `f(x)`; bound a huge `f(x)` inside `f`.
- Likewise, when `f` allocates, replace `take(n, map(f, xs))`
with `takeMap(n, f, xs)`.
| 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 |
| Multiple lets in match arm without `{ }` | Wrap in `{ }` block (see Pattern Matching) |
| `module benchmark/myname` | Use exactly `module benchmark/solution` |
| Using reserved keyword as name | See Reserved Keywords section above |
| Expecting transitive imports | Each module must import what it uses directly |
| No output from println | Check: `--caps IO` flag present AND before filename, effect sig is `! {IO}` not `IO[Unit]` |
| `let x = e` then `println(x)` in `{ }` | Put each statement on its own line (or separate with `;`) — or use `let x = e in println(x)` without braces |
| `x = x + 1` inside a block | No assignment: bind a new name, `let x2 = x + 1` (or shadow with `let x = x + 1`) |
| `2 ** 10` | No `**` operator: `pow(2.0, 10.0)` (`std/math`, floats) |
| `import std/math (max)` / `min` | No int `max`/`min` exists anywhere: define `pure func maxInt(a: int, b: int) -> int = if a > b then a else b` |
| A `match` that leaves out a constructor | `ailang check` does NOT catch it; it fails at run time ("no pattern matched"). Cover every constructor or end with `_ =>` |
| `{user: {email: e}}` on a record with more fields | Record patterns must name every field or end with `...`: `{user: {email: e, ...}, ...}` |
## Running Programs
```bash
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!**
### FS Sandbox
Restrict all file system operations to a directory with `AILANG_FS_SANDBOX`:
```bash
AILANG_FS_SANDBOX=/tmp/work ailang run --entry main --caps IO,FS file.ail
```
When set, all paths are resolved relative to the sandbox root:
- `readFile("data.txt")` reads `/tmp/work/data.txt`
- `writeFile("out.txt", s)` writes `/tmp/work/out.txt`
- `_zip_listEntries("archive.zip")` opens `/tmp/work/archive.zip`
Applies to **all FS builtins**: `readFile`, `writeFile`, `readFileBytes`, `writeFileBytes`, `appendFile`, `appendFileBytes`, `fileExists`, `listDir`, `mkdir`, `mkdirAll`, `isDir`, `isFile`, `removeFile`, and all `std/zip` operations. Also sets the working directory for `std/process` `exec`.
If unset (default), paths resolve normally from the process working directory.
---
# Information-Flow Control with IFC Labels (v0.16.0+)
AILANG supports **information-flow control (IFC) labels** that mark
strings (and other types) as carrying source-level taint. Labels
travel with values through the type system and document where text
is allowed to flow. This is the language-level equivalent of
"parameterised SQL" — code is separated from data, and the verifier
proves that untrusted data never reaches a sensitive sink.
## Syntax
| Form | Meaning |
|--------------------|------------------------------------------------------|
| `T<label>` | A `T` whose data is tainted with constant `<label>`. |
| `T{not LABEL}` | A `T` REFINED to refuse values labelled `<LABEL>`. |
| `! {Declassify}` | Function effect: this function changes a label. |
Examples:
```ailang
-- A string carrying the <email> label (untrusted source)
let body: string<email> = fetchMailBody()
-- A function whose body parameter rejects any <email>-tainted input
pure func sendEmail(to: string, body: string{not email}) -> () ! {Net}
-- A declassifier: input <email>, output <sanitized>, declared via Declassify
pure func sanitize(raw: string<email>) -> string<sanitized> ! {Declassify}
ensures { result == "[sanitized]" }
{ "[sanitized]" }
```
## Label Naming Convention
- **lowercase**, single-word labels: `<email>`, `<pii>`, `<secret>`.
- **snake_case** for compounds: `<user_input>`, `<sql_text>`, `<raw_html>`. A `-` in a label does not parse.
- One label per value and one `not` per parameter: `string{not email, not web}` does not parse. To forbid several sources at one sink, give them a shared label (`<untrusted>`) and write `{not untrusted}`.
- Labels are **structural**, not nominal — `<email>` from one module is
the same label as `<email>` from another module.
- The bottom label `⊥` (untainted) is implicit on any value with no
annotation. `<L> ⊔ ⊥ = <L>` (identity), `<L> ⊔ <L> = <L>` (idempotent).
## The DECLASS Rule
To LOWER a value's label (e.g. `<email>` → `<sanitized>`), write a
function whose **input** has the higher label and whose **output** has
the lower label, AND declare `! {Declassify}` in the effect row. The
type checker rejects any other transition.
```ailang
-- ✅ Allowed: explicit declassifier
pure func sanitize(raw: string<email>) -> string<sanitized> ! {Declassify}
{ "[sanitized]" }
-- ❌ Rejected: function changes label without Declassify
pure func sneakySanitize(raw: string<email>) -> string<sanitized> ! {}
{ "[sanitized]" }
```
## The Sink Rule
A function parameter typed `T{not LABEL}` REJECTS any argument carrying
the `<LABEL>` annotation in its type. This is how you build sinks that
provably refuse tainted input. Within one module, sink requirements follow function values through aliases, higher-order calls and wrappers.
```ailang
-- A network sink that refuses anything carrying <email>
pure func sendEmail(to: string, body: string{not email}) -> () ! {Net}
-- ✅ Caller passes a declassified value
sendEmail("alice@company.com", sanitize(rawBody))
-- ❌ Caller passes the raw body — refused at the type level
sendEmail("alice@company.com", rawBody) -- string<email> ↛ string{not email}
```
## Identity Bypass Is Structurally Impossible
Pure operations propagate labels through their result via APP-PURE:
the output label is the JOIN of all input labels. Identity functions
(or any function that returns its input) carry the input's label
forward. There is no way to "wash" a label by routing through a
trivial function.
```ailang
let laundered = rawBody; -- laundered: string<email> (label preserved)
sendEmail(addr, laundered); -- still rejected at the sink
```
## Worked Example: Inbox-Injection
```ailang
module myapp/agent
import std/string (endsWith)
type SendAction = { to: string, body: string }
-- Declassify: explicit gate from <email> to <sanitized>
pure func sanitize(rawBody: string<email>) -> string<sanitized> ! {Declassify}
ensures { result == "[sanitized]" }
{ "[sanitized]" }
-- ✅ Verified: declassifies before sending
pure func safeForward(rawBody: string<email>, recipient: string) -> SendAction ! {Declassify}
requires { endsWith(recipient, "@company.com") }
ensures { result.body == "[sanitized]" }
{ { to: recipient, body: sanitize(rawBody) } }
-- ❌ Rejected: forwards raw body verbatim — Z3 catches it now,
-- the type system will catch it once full label enforcement lands.
pure func injectedForward(rawBody: string<email>, recipient: string) -> SendAction ! {}
requires { endsWith(recipient, "@company.com") }
ensures { result.body == "[sanitized]" }
{ { to: recipient, body: rawBody } }
```
`ailang verify` on this module reports: 2 verified (sanitize, safeForward),
1 violation (injectedForward).
## When to Use Labels
- **Mark sources** of untrusted data: email bodies, HTTP request payloads,
user input, file contents from the network.
- **Mark sinks** that must NOT receive specific labels: `sendEmail`,
`executeShell`, `evalSql`, anything that puts data on the wire or into
an interpreter.
- **Always pair declassification with `! {Declassify}`** so reviewers can
audit every place a label is lowered.
## What Labels Do NOT Do
- Labels are NOT a runtime check — they have zero runtime cost.
- Labels do NOT affect the value's representation; `string<email>` is
still a plain `string` at runtime.
- Labels do NOT propagate through impure operations (`! {IO}`,
`! {Net}`) — those are assumed to clear the label, since IO sinks
are the boundary between in-process trust and the outside world.
## Formatting
`ailang fmt --write file.ail` rewrites a file into canonical form (idempotent; preserves
comments). Use it after your edits type-check. `ailang fmt --check file.ail` reports drift
without writing. Formatting never changes program meaning: `Parse(fmt(x)) ≡ Parse(x)`.
---
# Examples Status
## Example Status
### Working Examples ✅
- `adt_option.ail`
- `adt_simple.ail`
- `ai_call.ail` ← ✨ NEW (v0.3.9) - OpenAI API integration
- `arithmetic.ail`
- `block_recursion.ail` ← ✨ NEW (v0.3.0-alpha2)
- `claude_haiku_call.ail` ← ✨ NEW (v0.3.9) - Anthropic API integration
- `demos/adt_pipeline.ail` ← ✅ FIXED (M-R5 Day 1)
- `demos/hello_io.ail`
- `effects_basic.ail`
- `effects_pure.ail`
- `guards_basic.ail`
- `hello.ail`
- `micro_block_if.ail` ← ✨ NEW (v0.3.0-alpha2)
- `micro_block_seq.ail` ← ✨ NEW (v0.3.0-alpha2)
- `micro_io_echo.ail` ← ✅ FIXED (M-R5 Day 1)
- `micro_option_map.ail` ← ✅ FIXED (M-R5 Day 1)
- `micro_record_person.ail` ← ✨ NEW (v0.3.0-alpha3 M-R5 Day 3)
- `recursion_error.ail`
- `recursion_factorial.ail`
- `recursion_fibonacci.ail`
- `recursion_mutual.ail`
- `recursion_quicksort.ail`
- `showcase/01_type_inference.ail`
- `showcase/02_lambdas.ail`
- `showcase/03_type_classes.ail`
- `showcase/04_closures.ail`
- `simple.ail`
- `test_effect_annotation.ail`
- `test_effect_capability.ail`
- `test_effect_fs.ail`
- `test_effect_io.ail`
- `test_exhaustive_bool_complete.ail`
- `test_exhaustive_bool_incomplete.ail`
- `test_exhaustive_wildcard.ail`
- `test_guard_bool.ail`
- `test_guard_debug.ail`
- `test_guard_false.ail`
- `test_import_ctor.ail` ← ✅ FIXED (M-R5 Day 1)
- `test_import_func.ail` ← ✅ FIXED (M-R5 Day 1)
- `test_invocation.ail`
- `test_io_builtins.ail`
- `test_module_minimal.ail`
- `test_no_import.ail`
- `test_record_subsumption.ail` ← ✨ NEW (v0.3.0-alpha3 M-R5 Day 3)
- `test_single_guard.ail`
- `test_use_constructor.ail` ← ✅ FIXED (M-R5 Day 1)
- `test_with_import.ail`
- `type_classes_working_reference.ail`
- `v3_3/imports.ail` ← ✅ FIXED (M-R5 Day 1)
- `v3_3/imports_basic.ail` ← ✅ FIXED (M-R5 Day 1)
### Failing Examples ❌
- `demos/effects_pure.ail`
- `experimental/ai_agent_integration.ail`
- `experimental/concurrent_pipeline.ail`
- `experimental/factorial.ail`
- `experimental/quicksort.ail`
- `experimental/web_api.ail`
- `lambda_expressions.ail`
- `list_patterns.ail`
- `patterns.ail`
- `records.ail`
- `showcase/03_lists.ail`
- `test_effect_io_simple.ail`
- `typeclasses.ail`
- `v3_3/math/gcd.ail`
### Skipped Examples ⏭️
- `block_demo.ail`
- `option_demo.ail`
- `stdlib_demo.ail`
- `stdlib_demo_simple.ail`
**Summary:** 50 passed, 14 failed, 4 skipped (Total: 68)
**Recent improvements:**
- ✅ **v0.3.9 (Oct 2025)**: 2 new AI API integration examples!
- `ai_call.ail`: OpenAI GPT-4o-mini integration with JSON encoding
- `claude_haiku_call.ail`: Anthropic Claude Haiku integration (verified with real API)
- ✅ **M-R5 (v0.3.0-alpha3)**: 11 examples fixed/added via records & row polymorphism!
- Day 1: 9 examples fixed (demos/adt_pipeline, micro_io_echo, micro_option_map, test_import_ctor, test_import_func, test_use_constructor, v3_3/imports, v3_3/imports_basic)
- Day 3: 2 new examples (micro_record_person, test_record_subsumption)
- ✅ **M-R8 (v0.3.0-alpha2)**: `micro_block_*.ail`, `block_recursion.ail` (3 files) - Block expressions with recursion
- ✅ **M-R4 (v0.3.0-alpha1)**: `recursion_*.ail` (5 files) - Recursion support with RefCell indirection
---
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

