29625e7262
The cma authoring-form harness corpus had gone dead against the language as it evolved since May. The plan modelled it as merely schema-dead (missing param_modes/ret_mode); it was also drift-dead in the example BODIES. Fixed in place, with `ail check` + both test suites as the oracle: - Schema: param_modes/ret_mode completed on every fn type; existing borrow annotations preserved (data_with_match's borrow over List). - Symbol drift: `<`/`==` -> `lt`/`eq` (operator-routing); the removed `io/print_int` op -> print_str(int_to_str n) followed by a newline print, preserving the trailing newline the references' expected_stdout needs. - Ownership/ADT restructures: data_simple's reuse-as now wraps the source in a match arm (ctor must be statically visible); data_with_match's count_via_letrec + local go switched borrow->own (consume-while-borrowed under the tightened ownership analysis; head_or_zero still exercises borrow over the boxed List). - param_modes_all rewritten to own (Int) + borrow over a boxed ADT -- (borrow Int) is now a borrow-over-value error. - Two new author-facing examples (loop_sum: Loop/Recur; new_rawbuf: New) cover the Term variants that landed since May. - spec_completeness.rs: drop the deleted ParamMode::Implicit; cover Loop/Recur/New; allowlist the non-authorable Term::Intrinsic out. - spec.md section 4 rewritten to own/borrow (mandatory, no implicit) with the borrow-over-value rule; rendered/ regenerated. - mock_full_run fixture's t3 turn-2 program migrated so the harness score assertions hold; usage fields untouched. Both render/ and harness/ cargo test suites green in mock mode; no live IONOS call. Run the harness budget/reference tests with AIL_BIN pointing at target/debug/ail (ail is not on PATH in the test env).
1108 lines
35 KiB
Markdown
1108 lines
35 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 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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "fn_returns_int",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "answer",
|
|
"doc": "The simplest possible fn — no params, returns a fixed Int.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [],
|
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 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: `{"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).
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "forall_polymorphic",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "id",
|
|
"doc": "The polymorphic identity function. Exercises Type::Forall and Type::Var.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [ { "k": "var", "name": "a" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "var", "name": "a" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x"],
|
|
"body": { "t": "var", "name": "x" }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## 4. Mode annotations
|
|
|
|
Every function parameter carries a mode, and so does the return
|
|
position. There are exactly two modes. `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. `borrow` is legal only over a boxed type (an algebraic
|
|
data type): unboxed value types (`Int`, `Bool`, `Float`, `Unit`)
|
|
are always `own`, and borrowing one is a `borrow-over-value` error.
|
|
|
|
The return position is always `own` — a function returns a fresh
|
|
owned value; borrow-returns are not permitted.
|
|
|
|
Modes are mandatory: every parameter and the return position carries
|
|
exactly one mode. There is no default or omitted mode.
|
|
|
|
On `Type::Fn`, two required fields carry modes:
|
|
|
|
- `param_modes`: an array of strings, each `"own"` or `"borrow"`,
|
|
one per parameter, in the same order as `params`.
|
|
- `ret_mode`: a single string, always `"own"`.
|
|
|
|
Both are present on every fn type. Example — a fn that borrows a
|
|
boxed list and returns an int: `"param_modes": ["borrow"]`,
|
|
`"ret_mode": "own"`. A fn that consumes an int and returns an int:
|
|
`"param_modes": ["own"]`, `"ret_mode": "own"`.
|
|
|
|
|
|
```json
|
|
{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"List"}],"name":"Cons"}],"kind":"type","name":"List"},{"body":{"name":"x","t":"var"},"doc":"(own Int) — unboxed value types (Int/Bool/Float/Unit) are always own; borrow over them is a borrow-over-value error.","kind":"fn","name":"f_own_value","params":["x"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"},"ret_mode":"own"}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"f_borrow_boxed","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"(borrow List) — boxed types may be borrowed; the callee reads without consuming.","kind":"fn","name":"f_borrow_boxed","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["borrow"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"},"ret_mode":"own"}}],"imports":[],"name":"param_modes_all","schema":"ailang/v0"}
|
|
```
|
|
|
|
## 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: `{"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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "fn_calls_prelude",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "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": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "con", "name": "Int" },
|
|
{ "k": "con", "name": "Int" }
|
|
],
|
|
"param_modes": ["own", "own"],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "let",
|
|
"name": "s",
|
|
"value": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "+" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
},
|
|
"body": { "t": "clone", "value": { "t": "var", "name": "s" } }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "fn_with_lambda",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "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": {
|
|
"k": "fn",
|
|
"params": [ { "k": "con", "name": "Int" } ],
|
|
"param_modes": ["own"],
|
|
"ret": {
|
|
"k": "fn",
|
|
"params": [ { "k": "con", "name": "Int" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": ["x"],
|
|
"body": {
|
|
"t": "lam",
|
|
"params": ["y"],
|
|
"param-types": [ { "k": "con", "name": "Int" } ],
|
|
"ret-type": { "k": "con", "name": "Int" },
|
|
"effects": [],
|
|
"body": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "+" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "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: `{"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": []`.
|
|
|
|
|
|
```json
|
|
{"defs":[{"ctors":[{"fields":[{"k":"var","name":"a"}],"name":"MkBox"}],"kind":"type","name":"Box","vars":["a"]},{"body":{"arms":[{"body":{"body":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"ctor":"MkBox","t":"ctor","type":"Box"},"source":{"name":"src","t":"var"},"t":"reuse-as"},"pat":{"ctor":"MkBox","fields":[{"name":"v","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"src","t":"var"},"t":"match"},"kind":"fn","name":"wrap_one","params":["src"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"args":[{"k":"con","name":"Int"}],"k":"con","name":"Box"}],"ret":{"args":[{"k":"con","name":"Int"}],"k":"con","name":"Box"},"ret_mode":"own"}}],"imports":[],"name":"data_simple","schema":"ailang/v0"}
|
|
```
|
|
|
|
## 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: `{"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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "data_with_match",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "type",
|
|
"name": "List",
|
|
"doc": "Monomorphic singly-linked Int list \u2014 boxed, recursive.",
|
|
"ctors": [
|
|
{
|
|
"name": "Nil",
|
|
"fields": []
|
|
},
|
|
{
|
|
"name": "Cons",
|
|
"fields": [
|
|
{
|
|
"k": "con",
|
|
"name": "Int"
|
|
},
|
|
{
|
|
"k": "con",
|
|
"name": "List"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"kind": "fn",
|
|
"name": "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": {
|
|
"k": "fn",
|
|
"params": [
|
|
{
|
|
"k": "con",
|
|
"name": "List"
|
|
}
|
|
],
|
|
"param_modes": [
|
|
"borrow"
|
|
],
|
|
"ret": {
|
|
"k": "con",
|
|
"name": "Int"
|
|
},
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [
|
|
"xs"
|
|
],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "var",
|
|
"name": "xs"
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": {
|
|
"p": "ctor",
|
|
"ctor": "Nil",
|
|
"fields": []
|
|
},
|
|
"body": {
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "int",
|
|
"value": 0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"pat": {
|
|
"p": "ctor",
|
|
"ctor": "Cons",
|
|
"fields": [
|
|
{
|
|
"p": "var",
|
|
"name": "h"
|
|
},
|
|
{
|
|
"p": "wild"
|
|
}
|
|
]
|
|
},
|
|
"body": {
|
|
"t": "var",
|
|
"name": "h"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"kind": "fn",
|
|
"name": "count_via_letrec",
|
|
"doc": "Walk xs counting nodes using a local recursive let. Exercises TermLetRec and TermVar; the recursive `go` is bound locally.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [
|
|
{
|
|
"k": "con",
|
|
"name": "List"
|
|
}
|
|
],
|
|
"param_modes": [
|
|
"own"
|
|
],
|
|
"ret": {
|
|
"k": "con",
|
|
"name": "Int"
|
|
},
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [
|
|
"xs"
|
|
],
|
|
"body": {
|
|
"t": "letrec",
|
|
"name": "go",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [
|
|
{
|
|
"k": "con",
|
|
"name": "List"
|
|
}
|
|
],
|
|
"param_modes": [
|
|
"own"
|
|
],
|
|
"ret": {
|
|
"k": "con",
|
|
"name": "Int"
|
|
},
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [
|
|
"ys"
|
|
],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "var",
|
|
"name": "ys"
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": {
|
|
"p": "ctor",
|
|
"ctor": "Nil",
|
|
"fields": []
|
|
},
|
|
"body": {
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "int",
|
|
"value": 0
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"pat": {
|
|
"p": "ctor",
|
|
"ctor": "Cons",
|
|
"fields": [
|
|
{
|
|
"p": "wild"
|
|
},
|
|
{
|
|
"p": "var",
|
|
"name": "t"
|
|
}
|
|
]
|
|
},
|
|
"body": {
|
|
"t": "app",
|
|
"fn": {
|
|
"t": "var",
|
|
"name": "+"
|
|
},
|
|
"args": [
|
|
{
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "int",
|
|
"value": 1
|
|
}
|
|
},
|
|
{
|
|
"t": "app",
|
|
"fn": {
|
|
"t": "var",
|
|
"name": "go"
|
|
},
|
|
"args": [
|
|
{
|
|
"t": "var",
|
|
"name": "t"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"in": {
|
|
"t": "app",
|
|
"fn": {
|
|
"t": "var",
|
|
"name": "go"
|
|
},
|
|
"args": [
|
|
{
|
|
"t": "var",
|
|
"name": "xs"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "match_literal_pattern",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "classify",
|
|
"doc": "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [ { "k": "con", "name": "Int" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": ["n"],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": { "t": "var", "name": "n" },
|
|
"arms": [
|
|
{
|
|
"pat": { "p": "lit", "lit": { "kind": "int", "value": 0 } },
|
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 100 } }
|
|
},
|
|
{
|
|
"pat": { "p": "wild" },
|
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 200 } }
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"kind": "fn",
|
|
"name": "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": {
|
|
"k": "fn",
|
|
"params": [ { "k": "con", "name": "Int" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": ["n"],
|
|
"body": {
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "lt" },
|
|
"args": [
|
|
{ "t": "var", "name": "n" },
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": -1 } },
|
|
"else": {
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "eq" },
|
|
"args": [
|
|
{ "t": "var", "name": "n" },
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 0 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 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 `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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "fn_with_do_seq",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "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": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": {
|
|
"k": "con",
|
|
"name": "Unit"
|
|
},
|
|
"ret_mode": "own",
|
|
"effects": [
|
|
"IO"
|
|
]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "seq",
|
|
"lhs": {
|
|
"t": "seq",
|
|
"lhs": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [
|
|
{
|
|
"t": "app",
|
|
"fn": {
|
|
"t": "var",
|
|
"name": "int_to_str"
|
|
},
|
|
"args": [
|
|
{
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "int",
|
|
"value": 1
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"rhs": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [
|
|
{
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "str",
|
|
"value": "\n"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"rhs": {
|
|
"t": "seq",
|
|
"lhs": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [
|
|
{
|
|
"t": "app",
|
|
"fn": {
|
|
"t": "var",
|
|
"name": "int_to_str"
|
|
},
|
|
"args": [
|
|
{
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "int",
|
|
"value": 2
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"rhs": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [
|
|
{
|
|
"t": "lit",
|
|
"lit": {
|
|
"kind": "str",
|
|
"value": "\n"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## 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 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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "floats",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "const",
|
|
"name": "pi",
|
|
"doc": "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).",
|
|
"type": { "k": "con", "name": "Float" },
|
|
"value": { "t": "lit", "lit": { "kind": "float", "bits": "40091eb851eb851f" } }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "bool_str",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "is_true",
|
|
"doc": "Trivial Bool literal. Exercises Literal::Bool.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [],
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
|
},
|
|
{
|
|
"kind": "fn",
|
|
"name": "greeting",
|
|
"doc": "Trivial Str literal. Exercises Literal::Str.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Str" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [],
|
|
"body": { "t": "lit", "lit": { "kind": "str", "value": "hi" } }
|
|
},
|
|
{
|
|
"kind": "fn",
|
|
"name": "unit_value",
|
|
"doc": "Trivial Unit literal — needed so Literal::Unit appears at least once in the corpus.",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Unit" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
},
|
|
"params": [],
|
|
"body": { "t": "lit", "lit": { "kind": "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: `{"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.
|
|
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "class_def",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "class",
|
|
"name": "MyShow",
|
|
"param": "a",
|
|
"doc": "A toy single-method class. The instance in instance_def.ail.json provides a body for `show` at type Int.",
|
|
"methods": [
|
|
{
|
|
"name": "show",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [ { "k": "var", "name": "a" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "con", "name": "Str" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "instance_def",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "class",
|
|
"name": "MyShow",
|
|
"param": "a",
|
|
"doc": "Re-declared here so this fixture is self-contained; class_def.ail.json declares the same class verbatim.",
|
|
"methods": [
|
|
{
|
|
"name": "show",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [ { "k": "var", "name": "a" } ],
|
|
"param_modes": ["own"],
|
|
"ret": { "k": "con", "name": "Str" },
|
|
"ret_mode": "own",
|
|
"effects": []
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"kind": "instance",
|
|
"class": "MyShow",
|
|
"type": { "k": "con", "name": "Int" },
|
|
"doc": "Trivial MyShow Int — show always returns the fixed string \"int\".",
|
|
"methods": [
|
|
{
|
|
"name": "show",
|
|
"body": { "t": "lit", "lit": { "kind": "str", "value": "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.
|