| name | ailang |
| description | Write, read, and compile AiLang (`.ail`) source with the self-hosted `ailc` compiler. Use whenever the user asks for AiLang code, references AiLang syntax, mentions an `.ail` file, asks about the `ailc` compiler, or works inside an AiLang project. AiLang is a compiled, statically-typed language with a deliberately minimal-token syntax — 2-char keywords (`fn`/`lp`/`mt`/`rt`/`el`/`en`/`st`), optional type annotations (default `i64`), implicit `main`, implicit return. It compiles to C and links via `clang -O2`. Supports structs, classes (single inheritance + virtual methods), enums/recursive ADTs, real generics (generic structs `Box<T>` + enums `Option<T>` + multi-param `<A,B>` + `tr` trait bounds), operator overloading, closures with capture, `!T`/`?` error propagation, string interpolation `"${e}"`, UFCS, module namespacing (`im "…" as m`), C++ library interop via `csrc`, a multi-error type checker with *"did you mean?"* suggestions, and a 16-module stdlib (sockets/HTTP/TLS/Postgres/Redis/WebSocket/JSON/CSV/time/str/math/threads/seq/web/jwt/mysql). |
AiLang Quick Reference (self-hosted ailc)
AiLang's compiler is written in AiLang itself (ailc). It lexes, parses, type-tracks, and lowers .ail → C, then drives clang -O2 to a native binary. The source syntax is optimized for low token count when generated by an LLM; the binary runs at C speed because it is C, generated.
CLI — this is different from older docs
ailc [--keep-c|-k] <input.ail> [output-binary]
ailc only PRODUCES a binary — it does not run it. To run: ailc foo.ail && ./foo.
- Positional only. Arg 1 = input
.ail; optional arg 2 = output path. Default output = input with .ail stripped (fib.ail → fib).
--keep-c / -k keeps the generated <output>.c (deleted by default after a successful compile).
- There is NO
run, compile, --emit-c, --backend, or any subcommand. (Old docs for the Rust ailangc run/compile are obsolete — that's a different, archived compiler.)
im "..." imports resolve relative to the source file; std/ modules resolve via $AILANG_STD (set by the installer — see "Standard library" below). ailc then shells out to clang, auto-adding -lgc and (when used) OpenSSL / libpq / -lm.
echo 'println("hi")' > hi.ail
ailc hi.ail && ./hi
ailc hi.ail /tmp/hi && /tmp/hi
ailc -k hi.ail && cat hi.c
On Windows
ailc runs natively and writes a .exe: ailc hi.ail hi produces hi.exe, run it
with .\hi.exe (not ./hi). The .exe is self-contained (depends only on
KERNEL32/msvcrt). Networking/regex
programs (sockets/HTTP/TLS/Postgres/Redis/regex_*) are POSIX-only — build and run
those under WSL, not native ailc.
Mental model
- Default type is
i64. Unannotated params and locals are i64.
- All keywords are short (
fn, lp, mt, rt, el, en, st, mu, im, ex). Do not substitute for/while/match/return/else/enum/struct/let/var.
- Top-level statements form an implicit
main — don't write fn main for a script.
- The trailing expression of a function/block body is its return value — no
rt needed at the end.
if and mt and {...} blocks are expressions — they yield a value.
Keywords
| kw | meaning | kw | meaning |
|---|
fn | function / lambda | mt | match |
rt | return (early only) | st | struct declaration |
if / el | if / else (el if chains) | en | enum / ADT declaration |
lp | loop (while + for-in + range + k,v) | im | import a file |
br / ct | break / continue | ex | extern C function |
mu | mutable binding | cinc | include a C header |
in | iterator separator in lp | true false | bool literals |
cl | class (single inheritance) | vt | virtual-method marker |
csrc | compile + link a C++ shim | super | parent-method call (in a method) |
tr | trait declaration (generic bound) | <T> <A,B> | generic type params |
println / print are builtins, not keywords. Full-word forms do not exist — use the short keyword: cl (not class), st (not struct), en (not enum), lp (not for/while), el (not else), rt (not return), mt (not match). Also no def / let / var.
Operators
declare := introduce a NEW binding
assign = reassign an existing `mu` binding
compound += -= *= /= %= (binding must be mu)
arithmetic + - * / % (- x is unary negate)
concat + (both str) or ++ (explicit string concat)
compare == != < <= > >=
logical && || !
bitwise & | ^ << >> (prefix & is address-of)
pipe |> x |> f ⇒ f(x); x |> f(b) ⇒ f(x, b)
coalesce ?? m[k] ?? default
error prop expr? postfix — propagate err inside an !T fn
range .. ..= ONLY inside `lp i in lo..hi` (exclusive / inclusive)
interp "${expr}" string interpolation
No ternary cond ? a : b — use an if expression. ? is only postfix error-propagation.
Bindings
x := 10 // immutable (cannot reassign)
mu n := 5 // mutable
n = 7 // ok — n is mu
n += 1 // compound needs mu too
Empty literals need a type annotation so element types are known:
mu xs:[i64] := []
mu m:{str:i64} := {}
Functions, generics, closures
fn add(a, b) a + b // expression body; params default i64
fn fib(n) { // block body
if n < 2 rt n // braceless single-stmt if + early return
fib(n-1) + fib(n-2) // trailing expr = return value
}
fn greet(name:str) -> str "Hello, " + name + "!" // annotate non-i64
fn id<T>(x:T) -> T x // real generic, monomorphized to a real C fn per call
fn pick<A,B>(a:A, b:B) -> A a // multi-param — A and B inferred independently
fn dump<T: Show>(x:T) -> str x.fmt() // constrained — T must satisfy trait Show (see Traits)
fn map2<T,U>(xs:[T], f:fn(T)->U) -> [U] { map(xs, fn(x) f(x)) } // generic HOF — takes a closure
inc := fn(x) x + 1 // lambda (closure) — fn(params) body
println(inc(41)) // 42 — stored lambda, direct call: fine
fn apply(f:fn(i64)->i64, a:i64) -> i64 { f(a) } // fn-type param: annotate -> ret
println(apply(inc, 41)) // 42 — stored lambda → user fn: fine
threshold := 3
println(filter([1,2,3,4,5], fn(x) x > threshold)) // [4, 5] — capture by value
Lambda syntax is fn(x) body or fn(x) { ... }. Not |x| ..., (x) => ..., or lambda x:.
Generic functions monomorphize into real C functions (one per type instantiation), so a generic fn may take a closure parameter (fn map2<T,U>(xs:[T], f:fn(T)->U)) and have a full multi-statement body with loops, locals, and early return. std/seq.ail is a combinator library built this way. (When you pass a lambda to such a fn over non-i64 elements, annotate the lambda's param to match: keep(words, fn(s:str) starts_with(s,"a")).)
Two real constraints of the current self-hosted compiler:
- A fn that takes a
fn(...)->R parameter and returns the result of calling it must annotate its return type (-> i64) or use an explicit rt — implicit-return inference fails there and defaults to void.
- The builtins
map/filter/reduce want the lambda inline (map(xs, fn(x) x*2)). A lambda stored in a variable works for direct calls and for your own fn-type params, but not as a map/filter/reduce argument.
Control flow
// lp has FOUR forms, one keyword:
lp i in 1..10 { print(i) } // for-in range (exclusive; ..= inclusive)
lp x in nums { total += x } // for-in collection
lp (k, v) in m { ... } // map iteration, tuple-destructured
mu n := 5
lp n > 0 { n -= 1 } // while
lp { ...; if done br } // infinite loop
// if / el — also usable as an expression
grade := if s >= 90 { "A" } el if s >= 80 { "B" } el { "C" }
Structs
st Point { x:i64; y:i64 } // fields separated by ; or ,
p := Point(3, 4) // positional construction
q := Point{ x: 0, y: 1 } // named (order-independent)
println(p.x) // field access
Classes (single inheritance + explicit virtual)
cl Shape {
tag:i64
fn describe(self) -> i64 { self.tag } // method: implicit self (*Shape)
vt fn area(self) -> i64 { 0 } // `vt` = virtual (vtable dispatch)
}
cl Circle : Shape { // `: Base` = single inheritance
r:i64
vt fn area(self) -> i64 { 3*self.r*self.r } // override
vt fn name(self) -> str { "c/" ++ super.name() } // override + super call
}
c := Circle(0, 5) // ctor: inherited fields first, then own → (tag, r)
println(c.area()) // 75 — UFCS call; virtual → Circle::area
println(c.describe()) // 0 — inherited static method
- Methods take an implicit
self (typed *ClassName); call with UFCS obj.m(args).
fn = static dispatch (by the receiver's static type); vt fn = virtual (runtime dispatch via a vtable). A same-named vt fn in a subclass overrides it; super.m() calls the parent's impl.
- A class IS a struct under the hood —
Name(...) construction, println(obj), [Name] arrays, {str:Name} maps and !Name all work for free.
- Single inheritance only. Lowers to plain C (vtables = function-pointer tables) → works on macOS, Linux, and Windows.
Operator overloading (structural). A class that defines a conventionally-named method gets the operator — no trait/keyword needed:
cl Vec2 { x:i64 y:i64
fn add(self, o:Vec2) -> Vec2 { Vec2(self.x+o.x, self.y+o.y) } // enables a + b
fn eq(self, o:Vec2) -> bool { self.x==o.x && self.y==o.y } // enables a == b
}
c := a + b // → a.add(b)
Map: + - * / % → add sub mul div mod; == != < > <= >= → eq ne lt gt le ge. Dispatch is on the LEFT operand's class; bind intermediates (c := a+b) before chaining .m().
Traits & generic bounds (structural — no impl)
tr Show { fn fmt(self) -> str; } // a bundle of required method names
cl Dog { nm:str fn fmt(self) -> str { "dog:" + self.nm } } // satisfies Show by HAVING fmt
fn dump<T: Show>(x:T) -> str { x.fmt() } // bound: T must provide Show's methods
println(dump(Dog("rex"))) // dog:rex
A type satisfies a trait just by defining its methods (Go-interface style). The checker enforces the bound at the call site: dump(5) → type 'i64' does not satisfy bound 'Show': missing method 'fmt'.
Generic data types (monomorphized per use)
st Box<T> { val: T } // generic struct
st Pair<A,B> { a: A b: B } // multi-param
en Option<T> { Some(v:T), None } // generic enum
en Result<T> { Ok(v:T), Err(e:str) }
b := Box(5) // → Box_i64 (T inferred from the ctor arg)
p := Pair(3, "hi") // → Pair_i64_str (order matters: Pair_i64_str ≠ Pair_str_i64)
fn lookup(k:i64) -> Option<i64> { if k>0 { Some(k*10) } el { None } }
fn opt_get(o:Option<i64>) -> i64 { mt o { Some(v) => v; None => 0-1 } }
Some(x) / Ok(x) infer the type param from the payload. A payload-less / non-T variant (None, Err) infers it from the enclosing fn's declared return type — so return None from a -> Option<i64> fn; f(None) at a call site (no context) is not supported.
- Use an explicit annotation (
Box<i64> in a param/field/return) for instantiations over a non-scalar type (e.g. Box<Vec2>).
Enums / ADTs (recursive OK — self-references are heap-boxed)
en Color { Red, Green, Blue } // nullary variants = bare names
c := Blue
en Expr { // recursive ADT
Num(v:i64),
Add(l:Expr, r:Expr),
Neg(x:Expr),
}
e := Add(Num(2), Neg(Num(3))) // variant = call-style constructor
fn eval(x:Expr) -> i64 {
mt x { // match — `;` separates arms, `=>` per arm
Num(v) => v;
Add(l,r) => eval(l) + eval(r);
Neg(y) => 0 - eval(y);
}
}
mt is an expression; variant patterns bind positionally. Also supported: a _ wildcard arm, per-arm guards (Circle(r) if r > 10 => …), and one-level nested destructuring (Some(Pair(a,b)) => …). The checker verifies exhaustiveness (guard-aware — a guarded arm doesn't count as covering), variant validity, and binding arity, reported at the .ail line.
Types
- Primitives:
i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 bool str bytes void.
Caveat: integer widths are cosmetic — most lower to 64-bit; f32/f64 are both double. Don't rely on wraparound.
str is an immutable string; bytes is a binary buffer (may contain NUL).
- Composite:
[T] array, {K:V} map (open-addressing hash), *T pointer, !T result, (A, B, ...) tuple, fn(A,B)->R closure type.
*T pointers: fn bump(c:*Counter) { c.n = c.n + 1 } // p.f auto-derefs
bump(&ctr) // &x = address-of
tuples: a, b := divmod(17, 5) // multi-return + destructure
Error handling: !T + ?
fn parse(s:str) -> !i64 {
if regex_match("^-?[0-9]+$", s) rt ok(str_to_int(s))
err_i64("not a number: " + s) // err_<type>(msg): err_i64/err_str/err_f64/...
}
fn sum2(a:str, b:str) -> !i64 {
x := parse(a)? // ? propagates the err to this fn's return
y := parse(b)?
ok(x + y)
}
r := sum2("3", "4")
if is_ok(r) println(unwrap(r)) el println(err_msg(r))
? only works inside a fn whose return type is !T. The implicit top-level main is not !T — wrap fallible code in a fn run() -> !i64 { ... }. No generic err() — the constructor name encodes the type.
String interpolation, UFCS, C interop
name := "Ada"; n := 6
println("lang=${name} sq=${n * n}") // ${expr} holes; required braces
s.f(a) // UFCS: sugar for f(s, a)
s.cstr // property sugar for cstr(s)
cinc "math.h" // pull a C header into scope
ex fn sqrt(x:f64) -> f64 // then bind its symbols
ex fn printf(fmt:str, ...) -> i32 // variadic extern
ex "z" fn zlibVersion() -> str // "lib" → adds -lz
csrc "shim.cpp" // C++ interop (POSIX only): compile a C++
ex fn Acc_new() -> i64 // shim with clang++ & link it; bind its
ex fn Acc_free(h:i64) // extern "C" fns. C++ objects = i64 handles
csrc links a C++ shim: expose extern "C" functions, declare them with ex fn, pass opaque C++ objects across as i64 handles (reinterpret_cast). macOS/Linux only — on Windows a csrc program errors clearly. Two forms — external file csrc "shim.cpp", or inline in one file with a backtick block (the C++ is extracted, compiled with clang++, and cleaned up):
csrc `
#include <set>
extern "C" { int64_t set_new(){ return (int64_t)new std::set<int64_t>(); } }
`
ex fn set_new() -> i64
Builtins (always in scope — no im)
| Name | Notes |
|---|
print(x) / println(x) | type-dispatched; arrays of scalars and scalar maps print directly; bools print true/false in every form — the true/false literal, a comparison (== < …), a logical (&& || !), an overloaded ==, a bool variable, or a bool-returning call |
len(x) | str / bytes / [T] / {K:V} |
has(m, k) | map membership |
push/pop/sort/reverse/slice | return a fresh array |
keys(m) / values(m) | |
map(arr,f) / filter(arr,pred) / reduce(arr,init,f) | pass the lambda inline (map(xs, fn(x) x*2)) — a stored-in-a-variable lambda is not accepted here |
ok(v) / err_i64/err_str/err_f64/err_bool / unwrap/is_ok/is_err/err_msg | !T result |
str_to_int/int_to_str/str_to_float/float_to_str | conversions |
regex_match(pat,s) / regex_find(pat,s) | POSIX extended |
to_str(x) | used by ${...} interpolation |
time / io: now_ms(), mono_ms(), time_iso(ms), sleep_ms(ms), flush(), read_line(), read_stdin(), get_env(name) | time_iso takes a ms timestamp — current ISO time is time_iso(now_ms()), not time_iso(). read_line() reads one line (returns "" at EOF and for a blank line); read_stdin() reads all of stdin (use it for multi-line/JSON input). flush() flushes stdout: lp { print(time_iso(now_ms()) + "\r"); flush(); sleep_ms(1000) } |
socket/net builtins: tcp_*, sock_*, tls_*, pg_*, sha1, ... | baked into codegen — no extern decls needed; POSIX-only (need WSL on Windows) |
Don't name your own functions after a builtin (e.g. unwrap, split, len, keys, to_str) — the builtin wins and your fn is silently misrouted. Pick a distinct name (opt_get, not unwrap).
Standard library (im "std/<name>.ail")
| module | purpose | key functions |
|---|
std/math.ail | libm + helpers | sqrt/pow/sin/cos/log/floor/ceil(f64), min/max/ipow/gcd, rand/srand |
std/str.ail | string utils | eq(a,b), parse_int(s), strcmp, atoi |
std/time.ail | timing | tick(), elapsed_ms(t), since(t), sleep_s(s), now_iso() |
std/sock.ail | TCP | must_listen(host,port,banner), sock_send_str_all(fd,s), env_int(name,def) |
std/http.ail | HTTP/1.1 | http_recv_request(fd), http_method/http_path/http_header(req[,name]), http_text/http_json/http_html(status,body) |
std/json.ail | JSON (flat + nested) | flat: parse_flat_obj_str/parse_flat_obj_int; nested: json_parse(s) -> Json, json_str(j), accessors obj_get(j,k)/arr_at(j,i)/arr_len/as_int/as_float/as_str/as_bool/is_null/json_keys |
std/csv.ail | CSV reader/writer | csv_parse(text) -> [Row] (quoted fields, CRLF/LF), csv_emit(rows), csv_field(f), row_map(header,r) -> {str:str} (a Row wraps cells:[str]) |
std/tls.ail | TLS I/O | tls_send_str_all(ssl,s), tls_send_all(ssl,bytes) |
std/pg.ail | Postgres | pg_must_connect(dsn), pg_one(conn,sql), pg_first_col(conn,sql) -> [str], pg_print_table(res) |
std/redis.ail | Redis | redis_connect(host,port), redis_get/redis_set, redis_incr, redis_del, redis_ping |
std/ws.ail | WebSocket | ws_handshake_response(key), ws_send_text(fd,p), ws_recv_text(fd), b64_encode(bytes) |
std/thread.ail | OS threads (pthread, POSIX) | spawn(fn()->i64)/wait(h)/wait_all(hs), mutex()/lock/unlock, channel(cap)/send/recv/close (bounded blocking) |
std/seq.ail | generic combinators (|>-friendly) | any/all/count/find_index/take/drop/keep/map_to/flat_map/fold/sort_by/for_each/zip_with — each takes a passed closure; annotate the lambda param when elements aren't i64 |
std/web.ail | Express-style web framework (POSIX) | web_new(), web_get/web_post/web_put/web_delete(&app, pat, fn(r:Req)->str), web_use(&app, mw) middleware, :id path params via req_param(r,"id"), web_handle(&app, raw)->resp (socket-free, testable), web_listen(&app, host, port) (live server). Handlers are closures in the routes table. |
std/jwt.ail | JWT HS256 (POSIX) | jwt_sign(payload_json, secret)->str, jwt_verify(token, secret)->bool, jwt_payload(token)->str, jwt_claim(token, key)->str; b64url_encode/b64url_decode_str. Real interoperable tokens (byte-identical to PyJWT). |
std/mysql.ail | MySQL/MariaDB (libmysqlclient, POSIX) | mysql_must_connect(host,user,pass,db,port), mysql_one(c,sql)->str, mysql_rows(c,sql)->[MRow], mysql_exec/mysql_escape/mysql_close. Opt-in (only programs that use it link -lmysqlclient); needs the client lib + a server. (Postgres: std/pg.ail.) |
std/math.ail and std/sock.ail are auto-imported. The net/TLS/PG/Redis/thread builtins are baked into codegen, so the modules are thin convenience wrappers.
Resolving std/. im "std/…" is searched in three places, in order: (1) beside your source file, (2) $AILANG_STD/std/…, (3) beside the ailc binary. The installer sets AILANG_STD — and it must point at the directory that contains std/ (e.g. the repo root), not at std/ itself (a common mistake: AILANG_STD=…/std makes it look for …/std/std/math.ail). So once AILANG_STD is set, im "std/math.ail" resolves from any directory; otherwise put a std/ next to your .ail or next to ailc. An import that resolves nowhere is a hard error (it no longer silently drops).
Namespaced imports. im "path" as m aliases a module; call its fns qualified as m.fn(...). The module's functions are isolated under the alias, so two modules can define the same name without colliding (im "a.ail" as a + im "b.ail" as b → a.run() / b.run()). Plain im "path" still splices unqualified.
Top gotchas (what an LLM gets wrong)
- No
run/compile subcommand. ailc src.ail [out], then run the binary yourself. The old ailangc run syntax is dead.
el not else; rt not return; lp not for/while; en not enum; st not struct.
- Reassignment needs
mu. x := v is const; use mu x := v then x = v.
- String interpolation is
"${expr}" — braces required. Not {}, %s, or $var.
- No ternary — use
if c { a } el { b } as an expression.
- Errors are
!T + ok()/err_<type>()/?, not exceptions. ? only inside an !T fn.
- Enum variants are call-style (
Add(l,r), bare Red) and matched with mt x { V(b) => ...; } using ; separators.
- Lambdas are
fn(x) body — no |x| / => / -> arrow forms.
- Implicit
main — don't wrap a top-level script in fn main.
- Integer widths are cosmetic (stored 64-bit). The type checker is conservative but real — it reports confident mistakes at the
.ail line:col (type/!T mismatches, mt exhaustiveness/variants/arity, call & generic arity, <T: Trait> bounds, generic-instance mismatches), all errors in one run, with "did you mean?" spelling suggestions. It's not a full type system, so some mistakes still surface as C-compiler errors.
map/filter/reduce need an inline lambda — map(xs, fn(x) x*2), not a lambda stored in a variable. (std/seq.ail's keep/map_to/fold accept a passed/stored closure where the builtins won't.) And a non-generic fn that returns the result of calling a fn(...)->R parameter must annotate its return type (-> i64) or use explicit rt.
Worked examples
// hello — implicit main
println("hello, AiLang")
// fizzbuzz — lp range + mt tuple patterns
lp i in 1..16 {
mt (i%3, i%5) {
(0,0) => println("FizzBuzz");
(0,_) => println("Fizz");
(_,0) => println("Buzz");
_ => println(i);
}
}
// recursive fib
fn fib(n) {
if n < 2 rt n
fib(n-1) + fib(n-2)
}
println(fib(30)) // 832040
// arrays, maps, higher-order
nums := [5, 2, 8, 1, 9]
println(len(nums)) // 5
println(reduce(nums, 0, fn(a, b) a + b)) // 25
mu counts:{str:i64} := {}
lp w in ["a", "b", "a"] { counts[w] = counts[w] + 1 }
println(counts["a"]) // 2
// recursive ADT + match expression
en Tree { Leaf(v:i64), Node(l:Tree, r:Tree) }
fn sum(t:Tree) -> i64 {
mt t {
Leaf(v) => v;
Node(l,r) => sum(l) + sum(r);
}
}
println(sum(Node(Leaf(1), Node(Leaf(2), Leaf(3))))) // 6
// !T result + ? propagation
fn half(n) -> !i64 {
if n % 2 == 0 rt ok(n / 2)
err_i64("odd: ${n}")
}
fn run() -> !i64 {
a := half(8)?
b := half(a)?
ok(a + b)
}
r := run()
if is_ok(r) println(unwrap(r)) el println(err_msg(r)) // 6
// tiny HTTP server (std/sock auto-imported; im http)
im "std/http.ail"
fd := must_listen("127.0.0.1", 8080, "listening on :8080")
lp {
cli := tcp_accept(fd)
req := http_recv_request(cli)
sock_send_str_all(cli, http_text(200, "you asked for ${http_path(req)}\n"))
sock_close(cli)
}
When unsure, mimic the shape of programs in examples-selfhost/*.ail rather than translating literally from another language.