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.
614 lines
25 KiB
Markdown
614 lines
25 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.
|
|
|
|
{form-only: json}
|
|
The on-disk form is a single JSON object with the following required
|
|
top-level fields, in canonical key order:
|
|
|
|
- `schema`: the string `"ailang/v0"`. Any other value is rejected at
|
|
load time.
|
|
- `name`: the module name (matches the file stem on disk).
|
|
- `imports`: array of import objects. May be empty.
|
|
- `defs`: array of definition objects. Order is the declared order;
|
|
the hash is computed over the canonical-keys-sorted byte form so
|
|
inserting fields in source order is safe.
|
|
|
|
Canonical key order means object keys are emitted in sorted order
|
|
when the toolchain computes the canonical bytes for hashing; for
|
|
authoring you can write keys in whatever order is clearest. Numeric
|
|
literals are bare JSON numbers; strings are JSON strings. Every
|
|
definition object carries a `kind` discriminator (`fn`, `const`,
|
|
`type`, `class`, `instance`). Every term object carries a `t`
|
|
discriminator. Every type object carries a `k` discriminator. Every
|
|
pattern object carries a `p` discriminator. Every literal object
|
|
carries a `kind` discriminator.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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.
|
|
{/form-only}
|
|
|
|
{example: fn_returns_int}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Type::Con: `{"k": "con", "name": "Int"}` for a base type; for a
|
|
parameterised type, `{"k": "con", "name": "List", "args": [...]}`.
|
|
The `args` field is omitted when empty (canonical-bytes stable).
|
|
|
|
Type::Var: `{"k": "var", "name": "a"}`. Variable names are bare
|
|
identifiers; conventionally lower-case single letters.
|
|
|
|
Type::Fn: `{"k": "fn", "params": [...], "ret": ..., "effects": [...]}`
|
|
with optional `param_modes` and `ret_mode` fields. The `effects`
|
|
array is a set; sort and dedup at authoring time for readability.
|
|
|
|
Type::Forall: `{"k": "forall", "vars": [...], "body": ...}` with an
|
|
optional `constraints` array for class constraints (see section 10).
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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)`.
|
|
{/form-only}
|
|
|
|
{example: forall_polymorphic}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
On `Type::Fn`, two optional fields carry modes:
|
|
|
|
- `param_modes`: array of strings `"implicit"`, `"own"`, or
|
|
`"borrow"`, one per parameter, in the same order as `params`. The
|
|
array is omitted from the canonical bytes when every entry is
|
|
`"implicit"`; this is purely a serialisation optimisation. Author
|
|
the field explicitly when at least one parameter is `own` or
|
|
`borrow`.
|
|
- `ret_mode`: a single string of the same vocabulary. Omitted when
|
|
it would be `"implicit"`.
|
|
|
|
Example: a fn that borrows two ints and returns an int:
|
|
`"param_modes": ["borrow", "borrow"]`, `ret_mode` omitted (implicit).
|
|
A fn that consumes a list and returns a new list:
|
|
`"param_modes": ["own"]`, `"ret_mode": "own"`.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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.
|
|
{/form-only}
|
|
|
|
{example: param_modes_all}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Function definition: `{"kind": "fn", "name": ..., "type": ...,
|
|
"params": [...], "body": ...}` plus optional `doc`.
|
|
|
|
Term variants relevant here:
|
|
|
|
- Variable reference: `{"t": "var", "name": "x"}`.
|
|
- Application: `{"t": "app", "fn": ..., "args": [...]}`.
|
|
- Lambda: `{"t": "lam", "params": [...], "paramTypes": [...],
|
|
"retType": ..., "effects": [...], "body": ...}`.
|
|
- Let: `{"t": "let", "name": "x", "value": ..., "body": ...}`.
|
|
- Letrec: `{"t": "letrec", "name": "go", "type": ..., "params":
|
|
[...], "body": ..., "in": ...}`.
|
|
|
|
The `paramTypes` and `retType` keys in `lam` use camelCase; the rest
|
|
use snake_case. This is historical and the canonical bytes preserve
|
|
the difference.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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)`.
|
|
{/form-only}
|
|
|
|
{example: fn_calls_prelude}
|
|
|
|
{example: fn_with_lambda}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Type definition: `{"kind": "type", "name": ..., "vars": [...],
|
|
"doc": ..., "ctors": [...]}`. The `vars` array is the list of type
|
|
parameter names; omitted when empty.
|
|
|
|
Each ctor: `{"name": "Cons", "fields": [TYPE, TYPE, ...]}`.
|
|
`fields` is a positional list of field types.
|
|
|
|
Constructor invocation: `{"t": "ctor", "type": "List", "ctor":
|
|
"Cons", "args": [...]}`. The `type` field disambiguates which ADT
|
|
the constructor belongs to (necessary because constructor names are
|
|
not globally unique). `args` is positional and matches the
|
|
constructor's declared `fields` in order; nullary constructors
|
|
carry `"args": []`.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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.
|
|
{/form-only}
|
|
|
|
{example: data_simple}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Match expression: `{"t": "match", "scrutinee": ..., "arms": [...]}`.
|
|
Each arm: `{"pat": PATTERN, "body": TERM}`.
|
|
|
|
Pattern variants:
|
|
- Wildcard: `{"p": "wild"}` — no other fields.
|
|
- Variable: `{"p": "var", "name": "h"}` — binds the scrutinee to
|
|
`h`.
|
|
- Literal: `{"p": "lit", "lit": LITERAL}` — same `lit` shape as a
|
|
term literal.
|
|
- Constructor: `{"p": "ctor", "ctor": "Cons", "fields": [PATTERN,
|
|
PATTERN, ...]}` — positional sub-patterns matching the
|
|
constructor's declared field types.
|
|
|
|
Pattern variables are linear: each name appears at most once in a
|
|
single pattern. Repeating a name is a typecheck error.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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`.
|
|
{/form-only}
|
|
|
|
{example: data_with_match}
|
|
|
|
{example: match_literal_pattern}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Effect row on `Type::Fn`: the `effects` field is a JSON array of
|
|
strings (the effect labels). `[]` means pure. `["IO"]` is the
|
|
common effectful case. The toolchain compares effect rows modulo
|
|
order; for readability sort the array alphabetically.
|
|
|
|
Effect operation invocation: `{"t": "do", "op": "io/print_int",
|
|
"args": [INT-TERM]}`. The `op` is a string naming the prelude
|
|
operation. The `args` list matches the operation's declared
|
|
parameter types.
|
|
|
|
Sequencing: `{"t": "seq", "lhs": EFFECT-TERM, "rhs": EFFECT-TERM}`.
|
|
The value of the `seq` is the value of `rhs`; `lhs`'s value is
|
|
discarded but its effects count toward the enclosing fn's row.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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.
|
|
{/form-only}
|
|
|
|
{example: fn_with_do_seq}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Literal variants and their JSON shapes:
|
|
|
|
- Int: `{"kind": "int", "value": 42}` — value is a JSON number.
|
|
- Bool: `{"kind": "bool", "value": true}` — value is a JSON bool.
|
|
- Str: `{"kind": "str", "value": "hi"}` — value is a JSON string.
|
|
- Unit: `{"kind": "unit"}` — no value payload.
|
|
- Float: `{"kind": "float", "bits": "400921fb54442d18"}` — the
|
|
`bits` field is a 16-character lowercase hex string of the IEEE-
|
|
754 binary64 bit pattern. The hex-string path keeps NaN and ±Inf
|
|
representable (JSON numbers cannot encode them) and avoids
|
|
serialisation drift across `serde_json` versions.
|
|
|
|
Float bit patterns are computed by the toolchain on `ail parse`,
|
|
not by the author. When you write `3.14` in AIL, the parser emits
|
|
`"bits": "40091eb851eb851f"` in the JSON form.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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`.
|
|
{/form-only}
|
|
|
|
{example: floats}
|
|
|
|
{example: bool_str}
|
|
|
|
## 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.
|
|
|
|
{form-only: json}
|
|
Class declaration: `{"kind": "class", "name": "Show", "param": "a",
|
|
"methods": [...]}`. Each method: `{"name": "show", "type": ...,
|
|
"default": ...}` — `type` is the method's full signature with the
|
|
class parameter appearing as a `Type::Var`; `default` is an
|
|
optional body (omit when the method is abstract-required).
|
|
|
|
Instance declaration: `{"kind": "instance", "class": "Show",
|
|
"type": ..., "methods": [...]}`. The `type` field is the concrete
|
|
type the class is applied to. Each method: `{"name": "show",
|
|
"body": TERM}`.
|
|
|
|
Constraint on a polymorphic fn: inside `Type::Forall`, the
|
|
`constraints` field is an array of constraint objects: `{"class":
|
|
"Eq", "type": TYPE}`. Empty when the polymorphic fn has no class
|
|
constraints; omitted from canonical bytes when empty.
|
|
{/form-only}
|
|
|
|
{form-only: ail}
|
|
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).
|
|
{/form-only}
|
|
|
|
{example: class_def}
|
|
|
|
{example: instance_def}
|
|
|
|
## 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.
|