Files
AILang/experiments/2026-05-12-cross-model-authoring/rendered/json.md
T
Brummel 29625e7262 feat(cma): revive cross-model harness corpus to current language (refs #68)
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).
2026-06-02 17:08:30 +02:00

35 KiB

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.

{
  "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).

{
  "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".

{"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.

{
  "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" } }
      }
    }
  ]
}
{
  "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": [].

{"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.

{
  "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"
            }
          ]
        }
      }
    }
  ]
}
{
  "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.

{
  "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.

{
  "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" } }
    }
  ]
}
{
  "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.

{
  "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": []
          }
        }
      ]
    }
  ]
}
{
  "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.