72e54f4fd3
The surface-form file extension changes from .ailx to .ail. AILang's
authoring surface now uses the same .ail stem as its canonical JSON
form (.ail.json), giving the language a single coherent extension
family: .ail is the LLM-authored Form A, .ail.json is the canonical
JSON-AST Form B.
Scope (touched):
- 61 example renames examples/**/*.ailx → .ail (git mv)
- 1 rename experiments/.../rendered/ailx.md → ail.md
- 35 content-edited live-toolchain files (crates/, docs/DESIGN.md,
docs/roadmap.md, docs/PROSE_ROUNDTRIP.md, skills/, bench/reference/*.c,
experiment crates under experiments/.../{render,harness,master})
- Experiment-crate cohort rename Cohort::Ailx → Cohort::Ail,
Form::Ailx → Form::Ail, per_cohort/ailx → per_cohort/ail,
{form-only: ailx} → {form-only: ail}, ```ailx → ```ail
Out of scope (deliberately untouched, to preserve honest history):
- docs/journal-archive.md (content-frozen per CLAUDE.md)
- docs/journals/, docs/specs/, docs/plans/, bench/orchestrator-stats/
- experiments/.../runs/ (frozen LLM-output artefacts; models actually
saw .ailx — renaming would falsify the experimental record)
Verification: cargo build/test --workspace green; experiment crate
cargo test green; bench/check.py + compile_check.py + cross_lang.py
all 0-regressed; negative grep for ailx|Ailx|AILX outside the
out-of-scope paths returns zero matches.
Opens immediate follow-up: roadmap.md P2 todo `ail check`/build/run
accept .ail extension — after this rename, .ail is canonical
authoring surface but the CLI still produces a misleading JSON-parse
error on `ail check foo.ail`. That's the next iter.
582 lines
24 KiB
Markdown
582 lines
24 KiB
Markdown
# AILang authoring reference
|
|
|
|
## 1. Header
|
|
|
|
AILang is a small data-as-source functional language designed for an
|
|
LLM author. This document is the complete authoring reference. Every
|
|
well-formed program is a module. Read the whole thing once before you
|
|
write code; the order of sections is the order of dependencies.
|
|
|
|
## 2. Modules and the on-disk form
|
|
|
|
A module is the top-level container. It has a name (matching the file
|
|
stem), an optional list of imports, and a flat list of definitions.
|
|
Every program you write is exactly one module. Cross-module imports
|
|
are out of scope for the tasks in this experiment — every task is a
|
|
single self-contained module with an empty `imports` array.
|
|
|
|
A definition is the unit that gets a content hash. There are five
|
|
kinds of definition: function, constant, data type, class, and
|
|
instance. Their order within the module is preserved on disk but is
|
|
not load-bearing semantically — forward references are legal.
|
|
|
|
The schema of the on-disk form is fixed. The toolchain rejects any
|
|
module that does not conform.
|
|
|
|
|
|
The AIL surface is parenthesised tagged-head Lisp-style notation.
|
|
Every form begins with `(` followed by a tag identifier; positional
|
|
arguments follow; the form closes with `)`. There are no infix
|
|
operators, no operator precedence, no implicit conversions.
|
|
|
|
A module starts with `(module NAME ...)` and contains import forms
|
|
followed by definition forms. Whitespace and `;`-prefixed line
|
|
comments are insignificant; indentation is for readability only.
|
|
|
|
Identifier characters allowed: letters, digits, `_`, `-`, `+`, `*`,
|
|
`/`, `<`, `>`, `=`, `!`, `?`, `%`. An identifier may not start with a
|
|
digit. String literals are double-quoted with `\"`, `\\`, `\n`, `\t`
|
|
escapes; integer literals are signed decimal; the `(unit)` keyword
|
|
denotes the unit value.
|
|
|
|
The AIL form is the printed dual of the JSON form: `ail parse
|
|
file.ail` produces the same canonical bytes as the JSON form would,
|
|
and `ail render file.ail.json` produces the AIL text. Round-trip
|
|
through this pair is gated by an integration test.
|
|
|
|
```ail
|
|
(module fn_returns_int
|
|
(fn answer
|
|
(doc "The simplest possible fn — no params, returns a fixed Int.")
|
|
(type (fn-type (params) (ret (con Int))))
|
|
(params)
|
|
(body 42)))
|
|
```
|
|
|
|
## 3. Types
|
|
|
|
A type is one of four shapes. Every type position in a function
|
|
signature must be filled in; there is no implicit type inference at
|
|
the binding form. Constructor types may carry type arguments.
|
|
Functions are first-class — function types appear as parameter and
|
|
return types.
|
|
|
|
A type constructor names a base type or a user-defined ADT. The
|
|
prelude provides `Int`, `Bool`, `Str`, `Float`, `Unit`. User ADTs are
|
|
introduced with the `data` form (see section 6); after declaration
|
|
their name is usable wherever a type constructor is expected.
|
|
|
|
A type variable is a placeholder introduced by `forall` at the top
|
|
of a function signature. Within the signature the variable behaves
|
|
as an opaque type; the toolchain instantiates it at each call site.
|
|
|
|
A function type names parameter types, parameter modes (see section
|
|
4), a return type, a return mode, and an effect row (see section 8).
|
|
|
|
A `forall` is universal quantification: it lists the type variables
|
|
bound and the body type they appear in. Forall is only valid at the
|
|
top of a function or constant signature.
|
|
|
|
|
|
Type::Con: `Int`, `Bool`, `Str`, `Float`, `Unit`, or a user type
|
|
name. For parameterised types: `(con List Int)` reads "List of Int";
|
|
nested: `(con Map (con Str) (con Int))`.
|
|
|
|
Type::Var: a bare identifier alone in type position (no wrapper),
|
|
e.g. `a` inside a `(forall (a) ...)`. Type variables are
|
|
syntactically distinguished from type constructors by being declared
|
|
in the enclosing `forall`.
|
|
|
|
Type::Fn: `(fn-type (params T1 T2) (ret RET) (effects E1 E2))`. The
|
|
parameter and effect lists may be empty. Modes wrap the relevant
|
|
type: `(own T)` or `(borrow T)` — see section 4.
|
|
|
|
Type::Forall: `(forall (a b) BODY-TYPE)` quantifies over `a` and
|
|
`b`. Class constraints attach as `(forall (a) (constraints (Eq a))
|
|
BODY-TYPE)`.
|
|
|
|
```ail
|
|
(module forall_polymorphic
|
|
(fn id
|
|
(doc "The polymorphic identity function. Exercises Type::Forall and Type::Var.")
|
|
(type (forall (vars a) (fn-type (params a) (ret a))))
|
|
(params x)
|
|
(body x)))
|
|
```
|
|
|
|
## 4. Mode annotations
|
|
|
|
Every function parameter carries a mode. The mode says how the
|
|
callee may treat the value: `implicit` is the legacy default
|
|
(treated as `own`), `own` means the caller transfers ownership and
|
|
the callee consumes the value, `borrow` means the caller retains
|
|
ownership and the callee may inspect but not consume.
|
|
|
|
The mode also applies to the return value: `ret_mode` says whether
|
|
the function transfers ownership to the caller (`own`) or returns a
|
|
borrow (`borrow`). For most functions the return mode is `own` or
|
|
implicit; `borrow` returns are rare and used only for accessor-style
|
|
functions.
|
|
|
|
Modes are mandatory on every parameter and on the return position.
|
|
The schema rejects unannotated fn signatures only when annotations
|
|
are explicitly requested; for compatibility the legacy form (no
|
|
mode field at all) is treated as all-implicit.
|
|
|
|
|
|
Modes are surface wrappers around the type in parameter and return
|
|
position. Inside a `(fn-type ...)`:
|
|
|
|
- `(params (own T1) (borrow T2))` — `T1` is owned, `T2` is borrowed.
|
|
- `(ret (own T))` — the function returns an owned value.
|
|
- A bare type with no mode wrapper is implicit (legacy default).
|
|
|
|
When all parameters and the return are implicit, the surface omits
|
|
the mode wrappers entirely; this is the canonical form for fns that
|
|
do not yet need explicit annotations. Adding modes is a forward
|
|
change: an existing fn keeps the same canonical bytes as long as no
|
|
annotation is added.
|
|
|
|
Writing `(borrow T)` on a parameter the typechecker wants `own`
|
|
for surfaces as the `over-strict-mode` diagnostic — advisory, and
|
|
suppressable via the fn def's `suppress` clause. The mode is part
|
|
of the fn's signature and contributes to its content hash, so
|
|
making the choice explicit at authoring time avoids surprise
|
|
hash changes when the typechecker's defaults shift.
|
|
|
|
```ail
|
|
(module param_modes_all
|
|
(fn f_implicit
|
|
(doc "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.")
|
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
|
(params x)
|
|
(body x))
|
|
(fn f_own
|
|
(doc "(own T) — caller transfers ownership; callee consumes.")
|
|
(type (fn-type (params (own (con Int))) (ret (con Int))))
|
|
(params x)
|
|
(body x))
|
|
(fn f_borrow
|
|
(doc "(borrow T) — caller retains ownership; callee may not consume.")
|
|
(type (fn-type (params (borrow (con Int))) (ret (con Int))))
|
|
(params x)
|
|
(body x)))
|
|
```
|
|
|
|
## 5. Functions
|
|
|
|
A function definition binds a name to a body of code. The signature
|
|
declares the parameter types, the return type, and the effect set
|
|
(if any). The body is an expression — there is no statement form.
|
|
Functions are first-class values: they may be passed to other
|
|
functions, returned, stored in data structures.
|
|
|
|
Inside a function body, four term forms appear most often: a
|
|
variable reference (the name of a parameter, a local binding, or a
|
|
top-level definition); a function application (a callee plus
|
|
positional arguments); a lambda (an anonymous function with its
|
|
own signature); a literal value (see section 9).
|
|
|
|
Lambdas carry their own parameter list, parameter types, return
|
|
type, and effect set. Lambdas may close over names from the
|
|
enclosing lexical scope; the closure is captured by value (the
|
|
captured values are reference-counted under `--alloc=rc`).
|
|
|
|
A `let` binds a name to the value of one expression for the
|
|
duration of another expression. A `letrec` is the recursive variant:
|
|
the bound name is visible inside its own body. Both are
|
|
expressions, not statements — they have a value.
|
|
|
|
|
|
Function definition: `(fn NAME (doc STRING)? (type TYPE) (params
|
|
NAME*) (body TERM))`. The `doc` clause is optional but recommended.
|
|
|
|
Term forms relevant here:
|
|
|
|
- Variable reference: a bare identifier, e.g. `x` or `+`.
|
|
- Application: `(app CALLEE ARG*)` — head is `app`, then the callee
|
|
term, then positional arguments.
|
|
- Lambda: `(lam (params NAME*) (paramTypes TYPE*) (ret TYPE)
|
|
(effects EFFECT*) BODY)`.
|
|
- Let: `(let (NAME VALUE) BODY)` — binds `NAME` to `VALUE` in
|
|
`BODY`.
|
|
- Letrec: `(letrec NAME (type FN-TYPE) (params NAME*) BODY IN-BODY)`
|
|
— `NAME` is recursively visible inside `BODY`.
|
|
|
|
Inside any term position, an integer literal is a bare decimal
|
|
number, a string is a double-quoted string, a bool is `true` or
|
|
`false`, unit is `(unit)`.
|
|
|
|
```ail
|
|
(module fn_calls_prelude
|
|
(fn add
|
|
(doc "Add two Ints via the polymorphic prelude `+`. Also exercises TermLet by binding the sum to a local name before returning it, and TermClone by re-using the let-bound value.")
|
|
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
|
|
(params x y)
|
|
(body (let s (app + x y) (clone s)))))
|
|
```
|
|
|
|
```ail
|
|
(module fn_with_lambda
|
|
(fn make_adder
|
|
(doc "Curried adder: takes an Int and returns an Int -> Int closure. Exercises TermLam and Type::Fn appearing as a return type.")
|
|
(type (fn-type (params (con Int)) (ret (fn-type (params (con Int)) (ret (con Int))))))
|
|
(params x)
|
|
(body (lam (params (typed y (con Int))) (ret (con Int)) (body (app + x y))))))
|
|
```
|
|
|
|
## 6. Algebraic data types
|
|
|
|
A data type declaration introduces a new named type with one or
|
|
more constructors. Each constructor takes zero or more positional
|
|
argument types and produces a value of the declared type. ADTs may
|
|
be parameterised by type variables; the variables are listed before
|
|
the constructors and may appear in the constructor argument types.
|
|
|
|
A constructor is invoked at the term level by naming the type and
|
|
the constructor plus the positional arguments. The runtime
|
|
representation under `--alloc=rc` is a tagged heap cell with one
|
|
slot per constructor field; the tag identifies which constructor
|
|
was used.
|
|
|
|
Constructor names live in their own namespace, separate from
|
|
function names. The same identifier may be a function and a
|
|
constructor without conflict, though for readability the convention
|
|
is constructors are capitalised and functions are lower-case.
|
|
|
|
|
|
Type definition: `(data NAME (vars TYVAR*)? (doc STRING)? (ctor
|
|
CTOR-NAME ARG-TYPE*)*)`. The `vars` clause is optional; nullary
|
|
constructors have an empty argument list.
|
|
|
|
Example: `(data List (ctor Nil) (ctor Cons Int (con List)))` is a
|
|
monomorphic Int list with a Nil and a Cons constructor.
|
|
|
|
Constructor invocation: `(ctor TYPE-NAME CTOR-NAME ARG*)`. The
|
|
`TYPE-NAME` precedes the constructor name so the parser does not
|
|
need a separate registry to resolve overlapping constructor names
|
|
across ADTs. Example: `(ctor List Cons 1 (ctor List Nil))` builds a
|
|
single-element list.
|
|
|
|
```ail
|
|
(module data_simple
|
|
(data Box (vars a)
|
|
(doc "A unary box around a polymorphic value.")
|
|
(ctor MkBox a))
|
|
(fn wrap_one
|
|
(doc "Construct a Box Int via MkBox. The body uses (reuse-as src body) — a hint that the freshly-allocated Box should reuse the source's memory slot under --alloc=rc. Exercises TermCtor, ReuseAs, and Type::Var inside a ctor field position.")
|
|
(type (fn-type (params (own (con Box (con Int)))) (ret (own (con Box (con Int))))))
|
|
(params src)
|
|
(body (reuse-as src (term-ctor Box MkBox 1)))))
|
|
```
|
|
|
|
## 7. Pattern matching
|
|
|
|
Pattern matching is the way to inspect an ADT value. A `match`
|
|
form takes a scrutinee expression plus one or more arms; each arm
|
|
pairs a pattern with a body. Arms are tried top-to-bottom; the
|
|
first matching arm's body is evaluated. The toolchain checks
|
|
exhaustiveness: every constructor of the scrutinee's type must be
|
|
reachable through some arm, otherwise the typechecker errors.
|
|
|
|
There are four pattern shapes. A wildcard matches anything and
|
|
binds nothing. A variable pattern matches anything and binds the
|
|
matched value to the named identifier for use inside the arm body.
|
|
A literal pattern matches only when the scrutinee is bit-equal to
|
|
the literal value; literal patterns work on Int, Bool, Str, and
|
|
Unit (Float patterns are rejected at typecheck — see section 9).
|
|
A constructor pattern matches when the scrutinee is built by the
|
|
named constructor and recursively matches the constructor's
|
|
fields.
|
|
|
|
|
|
Match expression: `(match SCRUT (case PAT BODY) (case PAT BODY)
|
|
...)`. Arms are introduced by `(case ...)`; the order matters (top-
|
|
to-bottom).
|
|
|
|
Pattern shapes:
|
|
- Wildcard: `_` — bare underscore is the wildcard pattern.
|
|
- Variable: a bare identifier alone, e.g. `h`, binds the scrutinee.
|
|
- Literal: a literal value alone, e.g. `0`, `true`, `"abc"`,
|
|
`(unit)`, matches bit-equal scrutinees.
|
|
- Constructor: `(ctor CTOR-NAME SUBPAT*)` — note no `type` slot
|
|
here, because the constructor name is resolved against the
|
|
scrutinee's type.
|
|
|
|
Pattern variables introduced by a `(case PAT BODY)` are in scope
|
|
inside `BODY` and bind there only. They do not leak into sibling
|
|
arms or into code outside the `match`.
|
|
|
|
```ail
|
|
(module data_with_match
|
|
(data List
|
|
(doc "Monomorphic singly-linked Int list — boxed, recursive.")
|
|
(ctor Nil)
|
|
(ctor Cons (con Int) (con List)))
|
|
(fn head_or_zero
|
|
(doc "Return the head of xs, or 0 if empty. Exercises TermMatch with two arms, PatternCtor (both nullary Nil and binary Cons), and PatternWild on the tail field.")
|
|
(type (fn-type (params (borrow (con List))) (ret (con Int))))
|
|
(params xs)
|
|
(body (match xs
|
|
(case (pat-ctor Nil) 0)
|
|
(case (pat-ctor Cons h _) h))))
|
|
(fn count_via_letrec
|
|
(doc "Walk xs counting nodes using a local recursive let. Exercises TermLetRec and TermVar; the recursive `go` is bound locally.")
|
|
(type (fn-type (params (borrow (con List))) (ret (con Int))))
|
|
(params xs)
|
|
(body (let-rec go (params ys) (type (fn-type (params (borrow (con List))) (ret (con Int)))) (body (match ys
|
|
(case (pat-ctor Nil) 0)
|
|
(case (pat-ctor Cons _ t) (app + 1 (app go t))))) (in (app go xs))))))
|
|
```
|
|
|
|
```ail
|
|
(module match_literal_pattern
|
|
(fn classify
|
|
(doc "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.")
|
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
|
(params n)
|
|
(body (match n
|
|
(case (pat-lit 0) 100)
|
|
(case _ 200))))
|
|
(fn sign_if
|
|
(doc "Return -1 / 0 / 1 for the sign of n using nested TermIf. Exercises TermIf in both then/else positions plus TermApp on the polymorphic comparison.")
|
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
|
(params n)
|
|
(body (if (app < n 0) -1 (if (app == n 0) 0 1)))))
|
|
```
|
|
|
|
## 8. Effects
|
|
|
|
Every function type carries an effect row — a set of effect labels
|
|
that the function may raise. The two currently wired effects are
|
|
`IO` (required to call effect operations like `io/print_int`) and
|
|
`Diverge` (used for functions that may not terminate). An empty
|
|
effect row marks the function as pure.
|
|
|
|
Effect operations are reached through the `do` term, not through a
|
|
regular function application. The `do` form names the operation
|
|
(e.g. `io/print_int`) and lists the operation's arguments; the
|
|
typechecker resolves the operation against the prelude's effect-op
|
|
table, checks the arguments, and accumulates the operation's
|
|
effect label into the enclosing function's effect row.
|
|
|
|
A `seq` term sequences two effectful expressions: the left-hand
|
|
side is evaluated for its effects and its result discarded, then
|
|
the right-hand side is evaluated and its value is the value of the
|
|
whole expression. `seq` is the canonical way to perform multiple
|
|
IO operations one after the other inside a function body.
|
|
|
|
|
|
Effect row on a fn type: `(effects E1 E2 ...)` inside the
|
|
`(fn-type ...)`. Empty row: omit the `(effects)` clause or write
|
|
`(effects)`. Common effectful row: `(effects IO)`. Order is sorted
|
|
in canonical output.
|
|
|
|
Effect operation: `(do OP-NAME ARG*)` — the leading `do` head is
|
|
the parser's signal that the next identifier is an effect op, not a
|
|
function. Example: `(do io/print_int 42)` prints the integer 42
|
|
and raises the `IO` effect.
|
|
|
|
Sequencing: `(seq LHS RHS)`. The value of `(seq X Y)` is `Y`'s
|
|
value; `X`'s value is discarded. Chains of sequencing are written
|
|
nested: `(seq A (seq B C))` runs `A`, then `B`, then yields `C`'s
|
|
value.
|
|
|
|
```ail
|
|
(module fn_with_do_seq
|
|
(fn main
|
|
(doc "Print two Ints in sequence. Exercises TermDo, TermSeq, and the IO effect on a fn type. The trailing unit return is implicit in the last Do (op returns Unit).")
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body (seq (do io/print_int 1) (do io/print_int 2)))))
|
|
```
|
|
|
|
## 9. Literals
|
|
|
|
There are five literal shapes. An integer literal is a signed
|
|
64-bit decimal. A boolean literal is `true` or `false`. A string
|
|
literal is a UTF-8 sequence in double quotes; AILang restricts the
|
|
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
|
|
ASCII bytes are an authoring error. A unit literal is the value of
|
|
type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
|
|
|
|
A Float literal is an IEEE-754 binary64 value. When you author
|
|
Floats, write the decimal form (`3.14`) — the AIL parser converts
|
|
to the 64-bit bit pattern and emits the canonical 16-character
|
|
lowercase hex string into the JSON. You never author the hex
|
|
encoding by hand; the toolchain owns that representation. NaN and
|
|
±Inf are representable through the prelude constants `nan`, `inf`,
|
|
and `neg_inf` (see section 11).
|
|
|
|
Float pattern matching is rejected at typecheck. IEEE-`==` makes
|
|
Float patterns semantically dubious (NaN never matches; equality
|
|
is bit-exact, not approximate), so the language hard-rejects them
|
|
to push authors toward comparison-based dispatch.
|
|
|
|
|
|
Literal surface forms:
|
|
|
|
- Int: bare signed decimal, e.g. `42`, `-7`, `0`.
|
|
- Bool: `true` or `false` as bare keywords.
|
|
- Str: double-quoted with `\"`, `\\`, `\n`, `\t` escapes, e.g.
|
|
`"hello"`. Bytes outside ASCII printable are rejected by the
|
|
authoring rule (Decision 6 Constraint 3).
|
|
- Unit: the keyword `(unit)`.
|
|
- Float: bare decimal with at least one digit on each side of the
|
|
point, e.g. `3.14`, `0.5`, `-2.0`. The parser converts to the
|
|
bit-pattern hex string when emitting JSON; you never author the
|
|
hex form.
|
|
|
|
There is no separate syntax for exponent-form floats in the v1
|
|
authoring surface; for non-finite values use the prelude constants
|
|
`nan`, `inf`, `neg_inf`.
|
|
|
|
```ail
|
|
(module floats
|
|
(const pi
|
|
(doc "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).")
|
|
(type (con Float))
|
|
(body 3.14)))
|
|
```
|
|
|
|
```ail
|
|
(module bool_str
|
|
(fn is_true
|
|
(doc "Trivial Bool literal. Exercises Literal::Bool.")
|
|
(type (fn-type (params) (ret (con Bool))))
|
|
(params)
|
|
(body true))
|
|
(fn greeting
|
|
(doc "Trivial Str literal. Exercises Literal::Str.")
|
|
(type (fn-type (params) (ret (con Str))))
|
|
(params)
|
|
(body "hi"))
|
|
(fn unit_value
|
|
(doc "Trivial Unit literal — needed so Literal::Unit appears at least once in the corpus.")
|
|
(type (fn-type (params) (ret (con Unit))))
|
|
(params)
|
|
(body (lit-unit))))
|
|
```
|
|
|
|
## 10. Typeclasses
|
|
|
|
A typeclass is a named bundle of operations that may be
|
|
implemented for multiple types. The class declaration lists the
|
|
methods; each instance declaration provides the bodies for a
|
|
specific type. The compiler resolves method calls at the call site
|
|
and monomorphises the implementation: there is no runtime dispatch.
|
|
|
|
A class is parameterised by a single type variable (multi-parameter
|
|
classes are not supported in v1). Methods are declared with their
|
|
full type signature; the class parameter appears as a type variable
|
|
inside the method type. Methods may carry an optional default body
|
|
that instances inherit unless they explicitly override.
|
|
|
|
Class constraints attach to polymorphic function types: a function
|
|
`forall a. (Eq a) => (a, a) -> Bool` requires its callers to
|
|
provide an `Eq` instance for `a` at the call site. The constraint
|
|
appears inside the `forall` quantifier, in the `constraints` slot.
|
|
|
|
The prelude provides four built-in classes: `Eq` (equality), `Ord`
|
|
(ordering with `<`, `<=`, `>`, `>=`), `Num` (arithmetic — `+`, `-`,
|
|
`*`, `/`, `neg`), and `Bounded`. Their instances for Int, Bool,
|
|
Str, Float, and Unit are built in; you do not declare them.
|
|
|
|
|
|
Class declaration: `(class NAME (param TYVAR) (method NAME (type
|
|
METHOD-TYPE) (default BODY)?)*)`. The `default` clause is
|
|
optional; methods without a default must be supplied by every
|
|
instance.
|
|
|
|
Instance declaration: `(instance NAME (type CONCRETE-TYPE) (method
|
|
NAME BODY)*)`. The method bodies must match the class's method
|
|
signatures with the class parameter substituted to the concrete
|
|
type.
|
|
|
|
Constraints inside a forall: `(forall (a) (constraints (Eq a) (Ord
|
|
a)) BODY-TYPE)`. Each constraint is `(CLASS-NAME TYPE)` — usually
|
|
the type is a single bound type variable but a concrete type is
|
|
also legal (typically a typecheck error unless an instance exists).
|
|
|
|
```ail
|
|
(module class_def
|
|
(class MyShow
|
|
(param a)
|
|
(doc "A toy single-method class. The instance in instance_def.ail.json provides a body for `show` at type Int.")
|
|
(method show
|
|
(type (fn-type (params a) (ret (con Str)))))))
|
|
```
|
|
|
|
```ail
|
|
(module instance_def
|
|
(class MyShow
|
|
(param a)
|
|
(doc "Re-declared here so this fixture is self-contained; class_def.ail.json declares the same class verbatim.")
|
|
(method show
|
|
(type (fn-type (params a) (ret (con Str))))))
|
|
(instance
|
|
(class MyShow)
|
|
(type (con Int))
|
|
(doc "Trivial MyShow Int — show always returns the fixed string \"int\".")
|
|
(method show
|
|
(body "int"))))
|
|
```
|
|
|
|
## 11. The prelude
|
|
|
|
The prelude provides a fixed set of operators, conversion functions,
|
|
and effect operations that are in scope without any import. The
|
|
operators are polymorphic over a small set of types and resolve at
|
|
the call site by the resolved argument type.
|
|
|
|
| Name | Type | Notes |
|
|
|------|------|------|
|
|
| `+` `-` `*` `/` | `forall a. (a, a) -> a` | Int and Float; codegen filters at use site |
|
|
| `%` | `(Int, Int) -> Int` | Int only |
|
|
| `==` `!=` `<` `<=` `>` `>=` | `forall a. (a, a) -> Bool` | Int, Bool, Str, Float, Unit |
|
|
| `not` | `(Bool) -> Bool` | Boolean negation |
|
|
| `neg` | `forall a. (a) -> a` | Int and Float negation |
|
|
| `int_to_float` | `(Int) -> Float` | Widening; codegen uses `sitofp` |
|
|
| `float_to_int_truncate` | `(Float) -> Int` | Saturating truncation to zero |
|
|
| `float_to_str` | `(Float) -> Str` | Formatted decimal |
|
|
| `is_nan` | `(Float) -> Bool` | True only for NaN bit patterns |
|
|
| `nan` `inf` `neg_inf` | `Float` | Bare-value constants |
|
|
| `__unreachable__` | `forall a. a` | Polymorphic bottom; LLVM `unreachable` |
|
|
|
|
Effect operations (reached through `do`, not `app`):
|
|
|
|
| Op | Signature | Effect |
|
|
|----|----------|-------|
|
|
| `io/print_int` | `(Int) -> Unit` | `IO` |
|
|
| `io/print_bool` | `(Bool) -> Unit` | `IO` |
|
|
| `io/print_str` | `(Str) -> Unit` | `IO` |
|
|
| `io/print_float` | `(Float) -> Unit` | `IO` |
|
|
|
|
`int_to_str` is intentionally NOT in the prelude — it is type-
|
|
installed in a future milestone but codegen-deferred pending the
|
|
heap-Str ABI work. Do not call it.
|
|
|
|
## 12. Content addressing
|
|
|
|
AILang has content-addressed identity: every top-level definition
|
|
has a 16-character hash derived from its canonical bytes. The
|
|
toolchain computes hashes on `ail parse` and `ail check`; the
|
|
author writes no hash literal in either form, and the JSON form
|
|
has no `hash` field. This is symmetric across forms and removes
|
|
hash as a form-distinguishing factor. You author code; the
|
|
toolchain owns the identity.
|
|
|
|
## 13. Out of scope, and closing directive
|
|
|
|
This reference does NOT teach: cross-module imports (every task is
|
|
a single self-contained module with an empty `imports` array),
|
|
refinement types (reserved in the schema but pass-through in this
|
|
version), point-free style (cut by Decision 6), operator
|
|
overloading (cut), syntactic sugar of any kind (cut). The four
|
|
tasks you will be given do not need any of these features.
|
|
|
|
When asked to write a module, return the complete module text and
|
|
nothing else. No markdown fences. No prose explanation. No
|
|
preamble. The harness parses your response verbatim; any wrapping
|
|
text is treated as a parse error.
|