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]
ailconly 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.ailstripped (fib.ail→fib). --keep-c/-kkeeps 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 Rustailangc run/compileare 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).ailcthen shells out toclang, auto-adding-lgcand (when used) OpenSSL / libpq /-lm.
echo 'println("hi")' > hi.ail
ailc hi.ail && ./hi # hi
ailc hi.ail /tmp/hi && /tmp/hi
ailc -k hi.ail && cat hi.c # inspect generated 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 arei64. - All keywords are short (
fn,lp,mt,rt,el,en,st,mu,im,ex). Do not substitutefor/while/match/return/else/enum/struct/let/var. - Top-level statements form an implicit
main— don't writefn mainfor a script. - The trailing expression of a function/block body is its return value — no
rtneeded at the end. ifandmtand{...}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(...)->Rparameter and returns the result of calling it must annotate its return type (-> i64) or use an explicitrt— implicit-return inference fails there and defaults tovoid. - The builtins
map/filter/reducewant 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 amap/filter/reduceargument.
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 UFCSobj.m(args). fn= static dispatch (by the receiver's static type);vt fn= virtual (runtime dispatch via a vtable). A same-namedvt fnin 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!Nameall 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 returnNonefrom 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/f64are bothdouble. Don't rely on wraparound. stris an immutable string;bytesis a binary buffer (may contain NUL).- Composite:
[T]array,{K:V}map (open-addressing hash),*Tpointer,!Tresult,(A, B, ...)tuple,fn(A,B)->Rclosure 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/compilesubcommand.ailc src.ail [out], then run the binary yourself. The oldailangc runsyntax is dead. elnotelse;rtnotreturn;lpnotfor/while;ennotenum;stnotstruct.- Reassignment needs
mu.x := vis const; usemu x := vthenx = 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!Tfn. - Enum variants are call-style (
Add(l,r), bareRed) and matched withmt x { V(b) => ...; }using;separators. - Lambdas are
fn(x) body— no|x|/=>/->arrow forms. - Implicit
main— don't wrap a top-level script infn main. - Integer widths are cosmetic (stored 64-bit). The type checker is conservative but real — it reports confident mistakes at the
.ailline:col(type/!Tmismatches,mtexhaustiveness/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/reduceneed an inline lambda —map(xs, fn(x) x*2), not a lambda stored in a variable. (std/seq.ail'skeep/map_to/foldaccept a passed/stored closure where the builtins won't.) And a non-generic fn that returns the result of calling afn(...)->Rparameter must annotate its return type (-> i64) or use explicitrt.
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.