iter cma.1: master mini-spec + render binary for cross-model authoring experiment
New top-level experiments/2026-05-12-cross-model-authoring/ (out of
root Cargo workspace; nested-crate standalone via empty [workspace]
table per Cargo idiom). The renderer projects one canonical
master/spec.md into two form-specific files (rendered/json.md and
rendered/ailx.md) by walking three directive forms ({form-only:
json}, {form-only: ailx}, {example: <id>}) and emitting per-example
fenced blocks sourced from 13 curated .ail.json fixtures.
Four test gates green (10/10 total):
- splitter_unit (7) — directive parser, malformed inputs panic
- spec_completeness (1) — AST-variant visitor lifted verbatim from
schema_coverage.rs; 34/34 variants covered by the curated subset
- example_roundtrip (1) — every fixture roundtrips through
ailang_surface::{print, parse} to identical canonical bytes
- token_balance (1) — form-only block totals balanced to 3.22% under
tiktoken-rs::cl100k_base (gate is ±5%)
Six of the 34 variants did not have a dedicated fixture topic in the
plan and were absorbed into the closest existing fixture per the
plan's Step 4.14 escape hatch (Let/Clone → fn_calls_prelude; LetRec
→ data_with_match; If → match_literal_pattern; ReuseAs →
data_simple; Const → floats). Surfaced as a journal note, not a
plan revision.
Two ancillary additions vs. plan, both structural Cargo necessities,
spec-compatible: render/Cargo.toml carries an empty [workspace] table
so the nested crate refuses parent-workspace discovery, and
render/.gitignore drops /target + Cargo.lock. Both surfaced inline
in the per-iter journal's Concerns section.
cma.2 (harness) and cma.3 (live IONOS run + DESIGN.md addendum +
journal + roadmap edits) remain out of scope per the spec.
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"iter_id": "cma.1",
|
||||||
|
"date": "2026-05-12",
|
||||||
|
"mode": "standard",
|
||||||
|
"outcome": "DONE",
|
||||||
|
"tasks_total": 9,
|
||||||
|
"tasks_completed": 9,
|
||||||
|
"reloops_per_task": {
|
||||||
|
"1": 0,
|
||||||
|
"2": 0,
|
||||||
|
"3": 0,
|
||||||
|
"4": 0,
|
||||||
|
"5": 0,
|
||||||
|
"6": 0,
|
||||||
|
"7": 0,
|
||||||
|
"8": 1,
|
||||||
|
"9": 0
|
||||||
|
},
|
||||||
|
"review_loops_spec": 0,
|
||||||
|
"review_loops_quality": 0,
|
||||||
|
"blocked_reason": null,
|
||||||
|
"notes": "Task 8 token_balance failed on first run (json=1800, ailx=1692, ratio 6.00%); one substantive AILX-side spec.md edit brought it to 3.22%. Counts as 1 re-loop on Task 8 (implementer-phase repair, not a review-loop)."
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# iter cma.1 — Master mini-spec + renderer for the cross-model authoring experiment
|
||||||
|
|
||||||
|
**Date:** 2026-05-12
|
||||||
|
**Started from:** fd6efdbc
|
||||||
|
**Status:** DONE
|
||||||
|
**Tasks completed:** 9 of 9
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Stands up the master mini-spec source and the renderer binary under a
|
||||||
|
new top-level `experiments/2026-05-12-cross-model-authoring/` tree
|
||||||
|
that lives outside the root Cargo workspace. The renderer projects
|
||||||
|
one form-agnostic `master/spec.md` into two form-specific files
|
||||||
|
(`rendered/json.md` and `rendered/ailx.md`) by walking three directive
|
||||||
|
forms (`{form-only: json}`, `{form-only: ailx}`, `{example: <id>}`)
|
||||||
|
and emitting per-example fenced blocks from 13 curated `.ail.json`
|
||||||
|
fixtures. Four test gates land green: a 7-test splitter unit suite,
|
||||||
|
a `spec_completeness` test borrowing the AST-variant visitor verbatim
|
||||||
|
from `crates/ailang-core/tests/schema_coverage.rs` (34/34 variants
|
||||||
|
observed across the curated subset), an `example_roundtrip` test
|
||||||
|
mirroring the surface roundtrip contract, and a `token_balance` test
|
||||||
|
that gates form-only block tokens to ±5% under `tiktoken-rs::cl100k_base`
|
||||||
|
(final ratio: json=1800, ailx=1742, ratio 3.22%). Two follow-on iters
|
||||||
|
(cma.2: harness, cma.3: live IONOS run + DESIGN.md addendum) are out
|
||||||
|
of scope per the spec.
|
||||||
|
|
||||||
|
## Per-task notes
|
||||||
|
|
||||||
|
- cma.1.1: Bootstrap experiment dir + Cargo skeleton — created
|
||||||
|
`experiments/.../{README.md,render/Cargo.toml,render/src/main.rs}`
|
||||||
|
plus the four target directories. Spec-pinned dependency paths
|
||||||
|
resolve `ailang-core` and `ailang-surface` via relative
|
||||||
|
`../../../crates/...` references.
|
||||||
|
- cma.1.2: Directive splitter (TDD) — 7 unit tests RED first, then
|
||||||
|
implementation; added a `[lib]` target so the integration tests
|
||||||
|
can reach `xmodel_render::splitter`. GREEN on first impl run.
|
||||||
|
- cma.1.3: spec_completeness test — visitor + EXPECTED_VARIANTS
|
||||||
|
list lifted verbatim from `crates/ailang-core/tests/schema_coverage.rs`;
|
||||||
|
only `examples_dir()` body and the test name changed. Confirmed RED
|
||||||
|
on the empty master/examples/ directory (failure mode: "no .ail.json
|
||||||
|
fixtures found").
|
||||||
|
- cma.1.4: 13 curated `.ail.json` fixtures (one per file as planned).
|
||||||
|
The plan's 13 fixture topics naturally covered 28 of the 34 AST
|
||||||
|
variants; the remaining 6 (Def::Const, Term::Let, Term::LetRec,
|
||||||
|
Term::If, Term::Clone, Term::ReuseAs) were folded into existing
|
||||||
|
fixtures per the plan's Step 4.14 instruction ("Extend the fixture
|
||||||
|
closest in scope … do not weaken the test"). Concrete absorptions:
|
||||||
|
Let + Clone into `fn_calls_prelude`; LetRec into `data_with_match`
|
||||||
|
(as a recursive `count_via_letrec` helper); If into
|
||||||
|
`match_literal_pattern` (as a `sign_if` helper); ReuseAs into
|
||||||
|
`data_simple` (the `wrap_one` body uses `(reuse-as src ...)`);
|
||||||
|
Const into `floats` (a `const pi : Float = 3.14`). All 34 variants
|
||||||
|
green on first run.
|
||||||
|
- cma.1.5: example_roundtrip test — verbatim from plan; all 13
|
||||||
|
fixtures roundtrip through `ailang_surface::print` and
|
||||||
|
`ailang_surface::parse` to the same canonical bytes on first
|
||||||
|
implementation run.
|
||||||
|
- cma.1.6: master/spec.md — 13 sections of form-agnostic prose with
|
||||||
|
parallel JSON/AILX form-only blocks in §2-5, §7, §9, §10 plus the
|
||||||
|
prelude table in §11 (no form-only there; the table itself is the
|
||||||
|
content). Section §11's prelude tabulation reflects the live
|
||||||
|
`crates/ailang-check/src/builtins.rs` symbol set: `+`, `-`, `*`,
|
||||||
|
`/`, `%`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `not`, `neg`,
|
||||||
|
`int_to_float`, `float_to_int_truncate`, `float_to_str`, `is_nan`,
|
||||||
|
`nan`/`inf`/`neg_inf`, `__unreachable__`, plus the effect ops
|
||||||
|
`io/print_{int,bool,str,float}`. `int_to_str` is explicitly named
|
||||||
|
as OUT (heap-Str ABI milestone).
|
||||||
|
- cma.1.7: Wire renderer end-to-end — examples.rs and main.rs filled
|
||||||
|
in verbatim from the plan. Renderer ran clean on first invocation;
|
||||||
|
json.md (33409 bytes) and ailx.md (23964 bytes) emit with the
|
||||||
|
expected 13 fenced blocks each (and zero of the wrong fence type).
|
||||||
|
- cma.1.8: token_balance test — first run reported json=1800,
|
||||||
|
ailx=1692, ratio 6.00% (above the 5% threshold). One substantive
|
||||||
|
AILX-side extension to §4 (mode annotations) added authoring
|
||||||
|
guidance on `over-strict-mode` and hash stability under future
|
||||||
|
default shifts; final ratio json=1800, ailx=1742, 3.22%. The
|
||||||
|
rebalancing is content, not filler.
|
||||||
|
- cma.1.9: Final render + full sweep — `cargo test --manifest-path
|
||||||
|
experiments/.../render/Cargo.toml` ends with 10 passed; 0 failed
|
||||||
|
across four test suites; `git status` shows the entire experiment
|
||||||
|
tree as untracked.
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
- Task 1: Cargo's automatic workspace discovery hits the root
|
||||||
|
manifest, so the experiment crate refuses to build on its own. The
|
||||||
|
fix is the standard Cargo idiom for a nested-but-out-of-workspace
|
||||||
|
crate: add an empty `[workspace]` table to the experiment's
|
||||||
|
`Cargo.toml`. The plan did not enumerate this; I added it with a
|
||||||
|
comment pointing at the spec authority (§Architecture lines 90-95)
|
||||||
|
for the standalone constraint. Boss should confirm this is the
|
||||||
|
intended mechanism vs. e.g. adding the experiment to a
|
||||||
|
`workspace.exclude` list in the root manifest (the spec is explicit
|
||||||
|
that the root Cargo.toml is not touched, which the chosen mechanism
|
||||||
|
respects).
|
||||||
|
- Task 1 ancillary: a `.gitignore` was added under
|
||||||
|
`experiments/.../render/` ignoring `/target` and `Cargo.lock`. Not
|
||||||
|
enumerated in the plan but a structural necessity to keep cargo's
|
||||||
|
build output and lockfile out of the repo. Boss can keep, replace
|
||||||
|
with a top-level rule, or move to the root `.gitignore`.
|
||||||
|
|
||||||
|
## Known debt
|
||||||
|
|
||||||
|
- None surfaced inside the iter. Two named deferrals remain out of
|
||||||
|
scope (cma.2 harness, cma.3 live run + DESIGN.md addendum), per
|
||||||
|
the spec.
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
27 new files, all under `experiments/2026-05-12-cross-model-authoring/`:
|
||||||
|
|
||||||
|
- `README.md`
|
||||||
|
- `master/spec.md`
|
||||||
|
- `master/examples/*.ail.json` × 13
|
||||||
|
- `render/Cargo.toml`
|
||||||
|
- `render/.gitignore`
|
||||||
|
- `render/src/{main,lib,splitter,examples}.rs`
|
||||||
|
- `render/tests/{splitter_unit,spec_completeness,example_roundtrip,token_balance}.rs`
|
||||||
|
- `rendered/json.md`
|
||||||
|
- `rendered/ailx.md`
|
||||||
|
|
||||||
|
`git diff --name-only HEAD | wc -l`: 0 (untracked tree; use
|
||||||
|
`git status --porcelain --untracked-files=all experiments/...` to
|
||||||
|
enumerate).
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
|
||||||
|
`bench/orchestrator-stats/2026-05-12-iter-cma.1.json`
|
||||||
@@ -20,3 +20,4 @@
|
|||||||
- 2026-05-12 — iter rt.2: DESIGN.md anchor for Roundtrip Invariant (top-level section + Decision 6 Constraint 2 upward cross-reference) → 2026-05-12-iter-rt.2.md
|
- 2026-05-12 — iter rt.2: DESIGN.md anchor for Roundtrip Invariant (top-level section + Decision 6 Constraint 2 upward cross-reference) → 2026-05-12-iter-rt.2.md
|
||||||
- 2026-05-12 — audit-rt: milestone close (Roundtrip Invariant) — architect drift fixed in rt.tidy (3 items: Direction-2 5th test, roadmap P1 removed, wording sync); bench all-green → 2026-05-12-audit-rt.md
|
- 2026-05-12 — audit-rt: milestone close (Roundtrip Invariant) — architect drift fixed in rt.tidy (3 items: Direction-2 5th test, roadmap P1 removed, wording sync); bench all-green → 2026-05-12-audit-rt.md
|
||||||
- 2026-05-12 — iter boss: `/boss` skill — autonomous-mode discipline (Direction freedom, Notifications, WhatsNew procedure) extracted from CLAUDE.md into a user-invoked skill → 2026-05-12-iter-boss.md
|
- 2026-05-12 — iter boss: `/boss` skill — autonomous-mode discipline (Direction freedom, Notifications, WhatsNew procedure) extracted from CLAUDE.md into a user-invoked skill → 2026-05-12-iter-boss.md
|
||||||
|
- 2026-05-12 — iter cma.1: master mini-spec + render binary for cross-model authoring experiment (13 fixtures cover 34/34 AST variants, 4 test gates green, rendered/{json,ailx}.md checked in; out-of-workspace nested crate) → 2026-05-12-iter-cma.1.md
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Cross-model authoring-form test
|
||||||
|
|
||||||
|
Empirical measurement of whether `.ail.json` or `.ailx` is the form a
|
||||||
|
foreign LLM author reaches for and succeeds with. Single subject for
|
||||||
|
v1: Qwen3-Coder-Next via IONOS. Two blind cohorts; same four tasks.
|
||||||
|
|
||||||
|
Parent spec: `docs/specs/2026-05-12-cross-model-authoring-form-test.md`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `master/spec.md` — canonical mini-spec source (form-agnostic prose +
|
||||||
|
`{form-only: X}` blocks + `{example: id}` markers).
|
||||||
|
- `master/examples/*.ail.json` — AST source-of-truth; each example
|
||||||
|
prints to either form via the existing roundtrip machinery.
|
||||||
|
- `master/tasks/*.task.json` — task definitions consumed by the
|
||||||
|
harness. **Authored in cma.2**, not cma.1.
|
||||||
|
- `render/` — standalone Cargo crate, outside the root workspace,
|
||||||
|
builds the renderer binary.
|
||||||
|
- `rendered/json.md`, `rendered/ailx.md` — projected mini-specs,
|
||||||
|
checked into the repo for review.
|
||||||
|
- `harness/` — **Authored in cma.2**.
|
||||||
|
- `runs/<date>-<hash>/` — populated by `harness` during a live run.
|
||||||
|
|
||||||
|
## Running the renderer
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --manifest-path experiments/2026-05-12-cross-model-authoring/render/Cargo.toml -- \
|
||||||
|
--master experiments/2026-05-12-cross-model-authoring/master \
|
||||||
|
--rendered experiments/2026-05-12-cross-model-authoring/rendered
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo test --manifest-path experiments/2026-05-12-cross-model-authoring/render/Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Three integration tests: `example_roundtrip` (each example loads,
|
||||||
|
prints to AILX, reparses to the same canonical bytes), `spec_completeness`
|
||||||
|
(every AST variant in `ailang_core::ast` is exercised by at least
|
||||||
|
one example), `token_balance` (form-only blocks balanced within ±5%
|
||||||
|
across the two rendered files).
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "bool_str",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "is_true",
|
||||||
|
"doc": "Trivial Bool literal. Exercises Literal::Bool.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [],
|
||||||
|
"ret": { "k": "con", "name": "Bool" },
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Unit" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": [],
|
||||||
|
"body": { "t": "lit", "lit": { "kind": "unit" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"effects": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "data_simple",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "type",
|
||||||
|
"name": "Box",
|
||||||
|
"vars": ["a"],
|
||||||
|
"doc": "A unary box around a polymorphic value.",
|
||||||
|
"ctors": [
|
||||||
|
{
|
||||||
|
"name": "MkBox",
|
||||||
|
"fields": [ { "k": "var", "name": "a" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "wrap_one",
|
||||||
|
"doc": "Construct a Box Int via MkBox. The body uses (reuse-as src body) — a hint that the freshly-allocated Box should reuse the source's memory slot under --alloc=rc. Exercises TermCtor, ReuseAs, and Type::Var inside a ctor field position.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [
|
||||||
|
{ "k": "con", "name": "Box", "args": [ { "k": "con", "name": "Int" } ] }
|
||||||
|
],
|
||||||
|
"param_modes": ["own"],
|
||||||
|
"ret": { "k": "con", "name": "Box", "args": [ { "k": "con", "name": "Int" } ] },
|
||||||
|
"ret_mode": "own",
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["src"],
|
||||||
|
"body": {
|
||||||
|
"t": "reuse-as",
|
||||||
|
"source": { "t": "var", "name": "src" },
|
||||||
|
"body": {
|
||||||
|
"t": "ctor",
|
||||||
|
"type": "Box",
|
||||||
|
"ctor": "MkBox",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 1 } } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "data_with_match",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "type",
|
||||||
|
"name": "List",
|
||||||
|
"doc": "Monomorphic singly-linked Int list — 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" },
|
||||||
|
"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": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["xs"],
|
||||||
|
"body": {
|
||||||
|
"t": "letrec",
|
||||||
|
"name": "go",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "List" } ],
|
||||||
|
"param_modes": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"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" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"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" }
|
||||||
|
],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": [],
|
||||||
|
"body": { "t": "lit", "lit": { "kind": "int", "value": 42 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Unit" },
|
||||||
|
"effects": ["IO"]
|
||||||
|
},
|
||||||
|
"params": [],
|
||||||
|
"body": {
|
||||||
|
"t": "seq",
|
||||||
|
"lhs": {
|
||||||
|
"t": "do",
|
||||||
|
"op": "io/print_int",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 1 } } ]
|
||||||
|
},
|
||||||
|
"rhs": {
|
||||||
|
"t": "do",
|
||||||
|
"op": "io/print_int",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 2 } } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"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" } ],
|
||||||
|
"ret": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": {
|
||||||
|
"t": "lam",
|
||||||
|
"params": ["y"],
|
||||||
|
"paramTypes": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"retType": { "k": "con", "name": "Int" },
|
||||||
|
"effects": [],
|
||||||
|
"body": {
|
||||||
|
"t": "app",
|
||||||
|
"fn": { "t": "var", "name": "+" },
|
||||||
|
"args": [
|
||||||
|
{ "t": "var", "name": "x" },
|
||||||
|
{ "t": "var", "name": "y" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "var", "name": "a" },
|
||||||
|
"effects": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"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" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["n"],
|
||||||
|
"body": {
|
||||||
|
"t": "if",
|
||||||
|
"cond": {
|
||||||
|
"t": "app",
|
||||||
|
"fn": { "t": "var", "name": "<" },
|
||||||
|
"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": "==" },
|
||||||
|
"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 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "param_modes_all",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_implicit",
|
||||||
|
"doc": "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_own",
|
||||||
|
"doc": "(own T) — caller transfers ownership; callee consumes.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"param_modes": ["own"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_borrow",
|
||||||
|
"doc": "(borrow T) — caller retains ownership; callee may not consume.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"param_modes": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
# 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: ailx}
|
||||||
|
The AILX 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 AILX form is the printed dual of the JSON form: `ail parse
|
||||||
|
file.ailx` produces the same canonical bytes as the JSON form would,
|
||||||
|
and `ail render file.ail.json` produces the AILX 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: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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 AILX 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 AILX 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 AILX, the parser emits
|
||||||
|
`"bits": "40091eb851eb851f"` in the JSON form.
|
||||||
|
{/form-only}
|
||||||
|
|
||||||
|
{form-only: ailx}
|
||||||
|
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: ailx}
|
||||||
|
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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Empty `[workspace]` table declares this crate as a standalone
|
||||||
|
# Cargo workspace of its own, so it is NOT auto-discovered as a member
|
||||||
|
# of the root workspace at /home/brummel/dev/ailang/Cargo.toml. The
|
||||||
|
# spec (`docs/specs/2026-05-12-cross-model-authoring-form-test.md`
|
||||||
|
# §Architecture lines 90-95) requires this experiment to live outside
|
||||||
|
# the root workspace; the root Cargo.toml is intentionally not modified.
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "xmodel-render"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "xmodel-render"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "xmodel_render"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ailang-core = { path = "../../../crates/ailang-core" }
|
||||||
|
ailang-surface = { path = "../../../crates/ailang-surface" }
|
||||||
|
serde_json = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tiktoken-rs = "0.6"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
//! Per-form rendering of a single master example.
|
||||||
|
//!
|
||||||
|
//! - JSON form: read the on-disk canonical bytes verbatim, emit a
|
||||||
|
//! ```json``` fenced block.
|
||||||
|
//! - AILX form: load the example via ailang_core::load_module, print
|
||||||
|
//! via ailang_surface::print, emit a ```ailx``` fenced block.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Form {
|
||||||
|
Json,
|
||||||
|
Ailx,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<String> {
|
||||||
|
let path = examples_dir.join(format!("{id}.ail.json"));
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"example id `{id}` references missing fixture at {}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match form {
|
||||||
|
Form::Json => {
|
||||||
|
let bytes = std::fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("reading {}", path.display()))?;
|
||||||
|
Ok(format!("```json\n{}\n```\n", bytes.trim_end()))
|
||||||
|
}
|
||||||
|
Form::Ailx => {
|
||||||
|
let module = ailang_core::load_module(&path)
|
||||||
|
.with_context(|| format!("load_module on {}", path.display()))?;
|
||||||
|
let text = ailang_surface::print(&module);
|
||||||
|
Ok(format!("```ailx\n{}\n```\n", text.trim_end()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! Library surface for xmodel-render's internal modules — exposed so
|
||||||
|
//! integration tests can reach them.
|
||||||
|
|
||||||
|
pub mod splitter;
|
||||||
|
pub mod examples;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! xmodel-render — projects master/spec.md into rendered/json.md and rendered/ailx.md.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use xmodel_render::splitter::{self, Segment};
|
||||||
|
use xmodel_render::examples;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let (master_dir, rendered_dir) = parse_args()?;
|
||||||
|
let spec_path = master_dir.join("spec.md");
|
||||||
|
let examples_dir = master_dir.join("examples");
|
||||||
|
let spec_text = std::fs::read_to_string(&spec_path)
|
||||||
|
.with_context(|| format!("reading {}", spec_path.display()))?;
|
||||||
|
let segments = splitter::split(&spec_text);
|
||||||
|
|
||||||
|
let json_out = render_for(&segments, &examples_dir, examples::Form::Json, splitter::Form::Json)?;
|
||||||
|
let ailx_out = render_for(&segments, &examples_dir, examples::Form::Ailx, splitter::Form::Ailx)?;
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&rendered_dir)
|
||||||
|
.with_context(|| format!("creating {}", rendered_dir.display()))?;
|
||||||
|
let json_path = rendered_dir.join("json.md");
|
||||||
|
let ailx_path = rendered_dir.join("ailx.md");
|
||||||
|
std::fs::write(&json_path, json_out)
|
||||||
|
.with_context(|| format!("writing {}", json_path.display()))?;
|
||||||
|
std::fs::write(&ailx_path, ailx_out)
|
||||||
|
.with_context(|| format!("writing {}", ailx_path.display()))?;
|
||||||
|
eprintln!("xmodel-render: wrote {} and {}", json_path.display(), ailx_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_for(
|
||||||
|
segments: &[Segment],
|
||||||
|
examples_dir: &Path,
|
||||||
|
example_form: examples::Form,
|
||||||
|
keep_form: splitter::Form,
|
||||||
|
) -> Result<String> {
|
||||||
|
let mut out = String::new();
|
||||||
|
for seg in segments {
|
||||||
|
match seg {
|
||||||
|
Segment::Prose(text) => out.push_str(text),
|
||||||
|
Segment::FormOnly { form, body } if *form == keep_form => out.push_str(body),
|
||||||
|
Segment::FormOnly { .. } => {} // drop the other form's block
|
||||||
|
Segment::Example(id) => {
|
||||||
|
let rendered = examples::render_example(examples_dir, id, example_form)?;
|
||||||
|
out.push_str(&rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> Result<(PathBuf, PathBuf)> {
|
||||||
|
let mut master: Option<PathBuf> = None;
|
||||||
|
let mut rendered: Option<PathBuf> = None;
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"--master" => master = Some(PathBuf::from(args.next().ok_or_else(|| anyhow!("--master needs a value"))?)),
|
||||||
|
"--rendered" => rendered = Some(PathBuf::from(args.next().ok_or_else(|| anyhow!("--rendered needs a value"))?)),
|
||||||
|
other => return Err(anyhow!("unknown argument: {}", other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let master = master.ok_or_else(|| anyhow!("--master <dir> is required"))?;
|
||||||
|
let rendered = rendered.ok_or_else(|| anyhow!("--rendered <dir> is required"))?;
|
||||||
|
Ok((master, rendered))
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! Directive splitter for master/spec.md.
|
||||||
|
//!
|
||||||
|
//! Recognises three directives, one per line, no nesting:
|
||||||
|
//! {form-only: json} … {/form-only}
|
||||||
|
//! {form-only: ailx} … {/form-only}
|
||||||
|
//! {example: <id>}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Form {
|
||||||
|
Json,
|
||||||
|
Ailx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Form {
|
||||||
|
fn parse(name: &str) -> Option<Form> {
|
||||||
|
match name {
|
||||||
|
"json" => Some(Form::Json),
|
||||||
|
"ailx" => Some(Form::Ailx),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Segment {
|
||||||
|
Prose(String),
|
||||||
|
FormOnly { form: Form, body: String },
|
||||||
|
Example(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a master/spec.md text into a sequence of segments.
|
||||||
|
///
|
||||||
|
/// Panics on malformed input (unknown form name, unclosed form-only block).
|
||||||
|
/// The renderer treats these as authoring bugs that should fail the test
|
||||||
|
/// suite rather than silently produce diverging mini-specs.
|
||||||
|
pub fn split(input: &str) -> Vec<Segment> {
|
||||||
|
let mut out: Vec<Segment> = Vec::new();
|
||||||
|
let mut current_prose = String::new();
|
||||||
|
let mut current_block: Option<(Form, String)> = None;
|
||||||
|
|
||||||
|
for line in input.split_inclusive('\n') {
|
||||||
|
let trimmed = line.trim_end_matches('\n');
|
||||||
|
|
||||||
|
// Inside a form-only block: collect until the closing marker.
|
||||||
|
if let Some((form, body)) = current_block.as_mut() {
|
||||||
|
if trimmed == "{/form-only}" {
|
||||||
|
let body_owned = std::mem::take(body);
|
||||||
|
out.push(Segment::FormOnly { form: *form, body: body_owned });
|
||||||
|
current_block = None;
|
||||||
|
} else {
|
||||||
|
body.push_str(line);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {form-only: <name>}
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("{form-only: ").and_then(|r| r.strip_suffix("}")) {
|
||||||
|
flush_prose(&mut current_prose, &mut out);
|
||||||
|
let form = Form::parse(rest)
|
||||||
|
.unwrap_or_else(|| panic!("unknown form name in directive: {}", rest));
|
||||||
|
current_block = Some((form, String::new()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {example: <id>}
|
||||||
|
if let Some(id) = trimmed.strip_prefix("{example: ").and_then(|r| r.strip_suffix("}")) {
|
||||||
|
flush_prose(&mut current_prose, &mut out);
|
||||||
|
out.push(Segment::Example(id.to_string()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stray closer with no opener.
|
||||||
|
if trimmed == "{/form-only}" {
|
||||||
|
panic!("stray {{/form-only}} without matching opener");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain prose line.
|
||||||
|
current_prose.push_str(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_block.is_some() {
|
||||||
|
panic!("unclosed form-only block at end of input");
|
||||||
|
}
|
||||||
|
flush_prose(&mut current_prose, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_prose(buf: &mut String, out: &mut Vec<Segment>) {
|
||||||
|
if !buf.is_empty() {
|
||||||
|
out.push(Segment::Prose(std::mem::take(buf)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! Roundtrip gate for master/examples/.
|
||||||
|
//!
|
||||||
|
//! Mirrors crates/ailang-surface/tests/round_trip.rs but scoped to
|
||||||
|
//! the master examples directory. For each .ail.json fixture: load,
|
||||||
|
//! print via ailang_surface::print, reparse, canonicalise both sides,
|
||||||
|
//! assert byte equality.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn examples_dir() -> PathBuf {
|
||||||
|
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
manifest.parent().unwrap().join("master").join("examples")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_examples() -> Vec<PathBuf> {
|
||||||
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(examples_dir())
|
||||||
|
.expect("master/examples/ should exist")
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.filter(|path| path.extension().and_then(|s| s.to_str()) == Some("json"))
|
||||||
|
.filter(|path| {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|name| name.ends_with(".ail.json"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
fn round_trip_one(path: &Path) -> Result<(), String> {
|
||||||
|
let original = ailang_core::load_module(path)
|
||||||
|
.map_err(|e| format!("{}: load_module failed: {e}", path.display()))?;
|
||||||
|
let text = ailang_surface::print(&original);
|
||||||
|
let parsed = ailang_surface::parse(&text)
|
||||||
|
.map_err(|e| format!("{}: surface::parse failed: {e}", path.display()))?;
|
||||||
|
let bytes_original = ailang_core::canonical::to_bytes(&original);
|
||||||
|
let bytes_parsed = ailang_core::canonical::to_bytes(&parsed);
|
||||||
|
if bytes_original != bytes_parsed {
|
||||||
|
return Err(format!(
|
||||||
|
"{}: roundtrip diverged.\n--- printed AILX ---\n{}\n--- original bytes len {} vs parsed bytes len {} ---",
|
||||||
|
path.display(),
|
||||||
|
text,
|
||||||
|
bytes_original.len(),
|
||||||
|
bytes_parsed.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_example_roundtrips_via_ailx() {
|
||||||
|
let examples = list_examples();
|
||||||
|
assert!(
|
||||||
|
!examples.is_empty(),
|
||||||
|
"master/examples/ should contain at least one .ail.json fixture"
|
||||||
|
);
|
||||||
|
let mut failures: Vec<String> = Vec::new();
|
||||||
|
for path in &examples {
|
||||||
|
if let Err(msg) = round_trip_one(path) {
|
||||||
|
failures.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !failures.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"{} of {} examples failed roundtrip:\n\n{}",
|
||||||
|
failures.len(),
|
||||||
|
examples.len(),
|
||||||
|
failures.join("\n\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
//! Schema-coverage audit (scoped to master/examples/): every AST enum
|
||||||
|
//! variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`, `ParamMode`
|
||||||
|
//! is exercised by at least one master example.
|
||||||
|
//!
|
||||||
|
//! Mirrors `crates/ailang-core/tests/schema_coverage.rs` — the visitor
|
||||||
|
//! and the expected-variants list are copied verbatim. The only
|
||||||
|
//! adjustment is the `examples_dir()` resolver (one parent up rather
|
||||||
|
//! than two, because this crate's manifest sits at
|
||||||
|
//! `experiments/.../render/`).
|
||||||
|
//!
|
||||||
|
//! Pure reader: this test loads fixtures but does not modify any
|
||||||
|
//! committed content. A failure means a master example must be added
|
||||||
|
//! (in a separate iteration); the test must not be relaxed.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use ailang_core::ast::{
|
||||||
|
Def, InstanceMethod, Literal, Module, ParamMode, Pattern, Term, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
enum VariantTag {
|
||||||
|
// Def
|
||||||
|
DefFn,
|
||||||
|
DefConst,
|
||||||
|
DefType,
|
||||||
|
DefClass,
|
||||||
|
DefInstance,
|
||||||
|
// Term
|
||||||
|
TermLit,
|
||||||
|
TermVar,
|
||||||
|
TermApp,
|
||||||
|
TermLet,
|
||||||
|
TermLetRec,
|
||||||
|
TermIf,
|
||||||
|
TermDo,
|
||||||
|
TermCtor,
|
||||||
|
TermMatch,
|
||||||
|
TermLam,
|
||||||
|
TermSeq,
|
||||||
|
TermClone,
|
||||||
|
TermReuseAs,
|
||||||
|
// Pattern
|
||||||
|
PatternWild,
|
||||||
|
PatternVar,
|
||||||
|
PatternLit,
|
||||||
|
PatternCtor,
|
||||||
|
// Literal
|
||||||
|
LiteralInt,
|
||||||
|
LiteralBool,
|
||||||
|
LiteralStr,
|
||||||
|
LiteralUnit,
|
||||||
|
LiteralFloat,
|
||||||
|
// Type
|
||||||
|
TypeCon,
|
||||||
|
TypeFn,
|
||||||
|
TypeVar,
|
||||||
|
TypeForall,
|
||||||
|
// ParamMode
|
||||||
|
ParamModeImplicit,
|
||||||
|
ParamModeOwn,
|
||||||
|
ParamModeBorrow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists every expected variant. Stored as a const array so that
|
||||||
|
/// adding a new `VariantTag` here and forgetting to add the
|
||||||
|
/// corresponding match arm in the visitor is a Rust compile error
|
||||||
|
/// (and vice versa).
|
||||||
|
const EXPECTED_VARIANTS: &[VariantTag] = &[
|
||||||
|
VariantTag::DefFn,
|
||||||
|
VariantTag::DefConst,
|
||||||
|
VariantTag::DefType,
|
||||||
|
VariantTag::DefClass,
|
||||||
|
VariantTag::DefInstance,
|
||||||
|
VariantTag::TermLit,
|
||||||
|
VariantTag::TermVar,
|
||||||
|
VariantTag::TermApp,
|
||||||
|
VariantTag::TermLet,
|
||||||
|
VariantTag::TermLetRec,
|
||||||
|
VariantTag::TermIf,
|
||||||
|
VariantTag::TermDo,
|
||||||
|
VariantTag::TermCtor,
|
||||||
|
VariantTag::TermMatch,
|
||||||
|
VariantTag::TermLam,
|
||||||
|
VariantTag::TermSeq,
|
||||||
|
VariantTag::TermClone,
|
||||||
|
VariantTag::TermReuseAs,
|
||||||
|
VariantTag::PatternWild,
|
||||||
|
VariantTag::PatternVar,
|
||||||
|
VariantTag::PatternLit,
|
||||||
|
VariantTag::PatternCtor,
|
||||||
|
VariantTag::LiteralInt,
|
||||||
|
VariantTag::LiteralBool,
|
||||||
|
VariantTag::LiteralStr,
|
||||||
|
VariantTag::LiteralUnit,
|
||||||
|
VariantTag::LiteralFloat,
|
||||||
|
VariantTag::TypeCon,
|
||||||
|
VariantTag::TypeFn,
|
||||||
|
VariantTag::TypeVar,
|
||||||
|
VariantTag::TypeForall,
|
||||||
|
VariantTag::ParamModeImplicit,
|
||||||
|
VariantTag::ParamModeOwn,
|
||||||
|
VariantTag::ParamModeBorrow,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn visit_def(def: &Def, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match def {
|
||||||
|
Def::Fn(fd) => {
|
||||||
|
observed.insert(VariantTag::DefFn);
|
||||||
|
visit_type(&fd.ty, observed);
|
||||||
|
visit_term(&fd.body, observed);
|
||||||
|
}
|
||||||
|
Def::Const(cd) => {
|
||||||
|
observed.insert(VariantTag::DefConst);
|
||||||
|
visit_type(&cd.ty, observed);
|
||||||
|
visit_term(&cd.value, observed);
|
||||||
|
}
|
||||||
|
Def::Type(td) => {
|
||||||
|
observed.insert(VariantTag::DefType);
|
||||||
|
for ctor in &td.ctors {
|
||||||
|
for field_ty in &ctor.fields {
|
||||||
|
visit_type(field_ty, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Def::Class(cd) => {
|
||||||
|
observed.insert(VariantTag::DefClass);
|
||||||
|
for m in &cd.methods {
|
||||||
|
visit_type(&m.ty, observed);
|
||||||
|
if let Some(body) = &m.default {
|
||||||
|
visit_term(body, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Def::Instance(id) => {
|
||||||
|
observed.insert(VariantTag::DefInstance);
|
||||||
|
visit_type(&id.type_, observed);
|
||||||
|
for m in &id.methods {
|
||||||
|
visit_instance_method(m, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_instance_method(m: &InstanceMethod, observed: &mut HashSet<VariantTag>) {
|
||||||
|
visit_term(&m.body, observed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match t {
|
||||||
|
Term::Lit { lit } => {
|
||||||
|
observed.insert(VariantTag::TermLit);
|
||||||
|
visit_literal(lit, observed);
|
||||||
|
}
|
||||||
|
Term::Var { .. } => {
|
||||||
|
observed.insert(VariantTag::TermVar);
|
||||||
|
}
|
||||||
|
Term::App { callee, args, .. } => {
|
||||||
|
observed.insert(VariantTag::TermApp);
|
||||||
|
visit_term(callee, observed);
|
||||||
|
for a in args {
|
||||||
|
visit_term(a, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Let { value, body, .. } => {
|
||||||
|
observed.insert(VariantTag::TermLet);
|
||||||
|
visit_term(value, observed);
|
||||||
|
visit_term(body, observed);
|
||||||
|
}
|
||||||
|
Term::LetRec { ty, body, in_term, .. } => {
|
||||||
|
observed.insert(VariantTag::TermLetRec);
|
||||||
|
visit_type(ty, observed);
|
||||||
|
visit_term(body, observed);
|
||||||
|
visit_term(in_term, observed);
|
||||||
|
}
|
||||||
|
Term::If { cond, then, else_ } => {
|
||||||
|
observed.insert(VariantTag::TermIf);
|
||||||
|
visit_term(cond, observed);
|
||||||
|
visit_term(then, observed);
|
||||||
|
visit_term(else_, observed);
|
||||||
|
}
|
||||||
|
Term::Do { args, .. } => {
|
||||||
|
observed.insert(VariantTag::TermDo);
|
||||||
|
for a in args {
|
||||||
|
visit_term(a, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Ctor { args, .. } => {
|
||||||
|
observed.insert(VariantTag::TermCtor);
|
||||||
|
for a in args {
|
||||||
|
visit_term(a, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Match { scrutinee, arms } => {
|
||||||
|
observed.insert(VariantTag::TermMatch);
|
||||||
|
visit_term(scrutinee, observed);
|
||||||
|
for arm in arms {
|
||||||
|
visit_pattern(&arm.pat, observed);
|
||||||
|
visit_term(&arm.body, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Term::Lam { param_tys, ret_ty, body, .. } => {
|
||||||
|
observed.insert(VariantTag::TermLam);
|
||||||
|
for ty in param_tys {
|
||||||
|
visit_type(ty, observed);
|
||||||
|
}
|
||||||
|
visit_type(ret_ty, observed);
|
||||||
|
visit_term(body, observed);
|
||||||
|
}
|
||||||
|
Term::Seq { lhs, rhs } => {
|
||||||
|
observed.insert(VariantTag::TermSeq);
|
||||||
|
visit_term(lhs, observed);
|
||||||
|
visit_term(rhs, observed);
|
||||||
|
}
|
||||||
|
Term::Clone { value } => {
|
||||||
|
observed.insert(VariantTag::TermClone);
|
||||||
|
visit_term(value, observed);
|
||||||
|
}
|
||||||
|
Term::ReuseAs { source, body } => {
|
||||||
|
observed.insert(VariantTag::TermReuseAs);
|
||||||
|
visit_term(source, observed);
|
||||||
|
visit_term(body, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_pattern(p: &Pattern, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match p {
|
||||||
|
Pattern::Wild => {
|
||||||
|
observed.insert(VariantTag::PatternWild);
|
||||||
|
}
|
||||||
|
Pattern::Var { .. } => {
|
||||||
|
observed.insert(VariantTag::PatternVar);
|
||||||
|
}
|
||||||
|
Pattern::Lit { lit } => {
|
||||||
|
observed.insert(VariantTag::PatternLit);
|
||||||
|
visit_literal(lit, observed);
|
||||||
|
}
|
||||||
|
Pattern::Ctor { fields, .. } => {
|
||||||
|
observed.insert(VariantTag::PatternCtor);
|
||||||
|
for f in fields {
|
||||||
|
visit_pattern(f, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_literal(l: &Literal, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match l {
|
||||||
|
Literal::Int { .. } => {
|
||||||
|
observed.insert(VariantTag::LiteralInt);
|
||||||
|
}
|
||||||
|
Literal::Bool { .. } => {
|
||||||
|
observed.insert(VariantTag::LiteralBool);
|
||||||
|
}
|
||||||
|
Literal::Str { .. } => {
|
||||||
|
observed.insert(VariantTag::LiteralStr);
|
||||||
|
}
|
||||||
|
Literal::Unit => {
|
||||||
|
observed.insert(VariantTag::LiteralUnit);
|
||||||
|
}
|
||||||
|
Literal::Float { .. } => {
|
||||||
|
observed.insert(VariantTag::LiteralFloat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_type(t: &Type, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match t {
|
||||||
|
Type::Con { args, .. } => {
|
||||||
|
observed.insert(VariantTag::TypeCon);
|
||||||
|
for a in args {
|
||||||
|
visit_type(a, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::Fn { params, param_modes, ret, ret_mode, .. } => {
|
||||||
|
observed.insert(VariantTag::TypeFn);
|
||||||
|
for p in params {
|
||||||
|
visit_type(p, observed);
|
||||||
|
}
|
||||||
|
for m in param_modes {
|
||||||
|
visit_param_mode(m, observed);
|
||||||
|
}
|
||||||
|
visit_type(ret, observed);
|
||||||
|
visit_param_mode(ret_mode, observed);
|
||||||
|
}
|
||||||
|
Type::Var { .. } => {
|
||||||
|
observed.insert(VariantTag::TypeVar);
|
||||||
|
}
|
||||||
|
Type::Forall { body, .. } => {
|
||||||
|
observed.insert(VariantTag::TypeForall);
|
||||||
|
visit_type(body, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_param_mode(m: &ParamMode, observed: &mut HashSet<VariantTag>) {
|
||||||
|
match m {
|
||||||
|
ParamMode::Implicit => {
|
||||||
|
observed.insert(VariantTag::ParamModeImplicit);
|
||||||
|
}
|
||||||
|
ParamMode::Own => {
|
||||||
|
observed.insert(VariantTag::ParamModeOwn);
|
||||||
|
}
|
||||||
|
ParamMode::Borrow => {
|
||||||
|
observed.insert(VariantTag::ParamModeBorrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_module(m: &Module, observed: &mut HashSet<VariantTag>) {
|
||||||
|
for def in &m.defs {
|
||||||
|
visit_def(def, observed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples_dir() -> std::path::PathBuf {
|
||||||
|
// CARGO_MANIFEST_DIR for this test is
|
||||||
|
// experiments/2026-05-12-cross-model-authoring/render/
|
||||||
|
// Master examples live one parent up + master/examples/.
|
||||||
|
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
manifest.parent().unwrap().join("master").join("examples")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_json_fixtures() -> Vec<PathBuf> {
|
||||||
|
let dir = examples_dir();
|
||||||
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||||
|
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| {
|
||||||
|
p.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.map(|n| n.ends_with(".ail.json"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_ast_variant_is_observed_in_master_examples() {
|
||||||
|
let fixtures = list_json_fixtures();
|
||||||
|
assert!(
|
||||||
|
!fixtures.is_empty(),
|
||||||
|
"no .ail.json fixtures found under {}",
|
||||||
|
examples_dir().display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut observed: HashSet<VariantTag> = HashSet::new();
|
||||||
|
let mut load_failures = Vec::<String>::new();
|
||||||
|
|
||||||
|
for path in &fixtures {
|
||||||
|
match ailang_core::load_module(path) {
|
||||||
|
Ok(m) => visit_module(&m, &mut observed),
|
||||||
|
Err(e) => load_failures.push(format!("{}: {e}", path.display())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load failures fail the test on their own — a fixture that
|
||||||
|
// does not load is a corpus bug, surface it loud.
|
||||||
|
if !load_failures.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"{} fixture(s) failed to load during coverage scan:\n{}",
|
||||||
|
load_failures.len(),
|
||||||
|
load_failures.join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let missing: Vec<VariantTag> = EXPECTED_VARIANTS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|v| !observed.contains(v))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
let names: Vec<String> = missing.iter().map(|v| format!("{v:?}")).collect();
|
||||||
|
panic!(
|
||||||
|
"{} AST variant(s) have NO fixture coverage in {}:\n {}\n\n\
|
||||||
|
every variant must be exercised by at least one fixture; \
|
||||||
|
add a fixture that uses each missing variant (do NOT \
|
||||||
|
weaken this test).",
|
||||||
|
missing.len(),
|
||||||
|
examples_dir().display(),
|
||||||
|
names.join("\n ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"schema coverage ok: {} variants observed across {} fixtures",
|
||||||
|
observed.len(),
|
||||||
|
fixtures.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use xmodel_render::splitter::{split, Form, Segment};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_text_passes_through_unchanged() {
|
||||||
|
let input = "hello\nworld\n";
|
||||||
|
let segments = split(input);
|
||||||
|
assert_eq!(segments, vec![Segment::Prose("hello\nworld\n".to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_only_block_is_isolated() {
|
||||||
|
let input = "before\n{form-only: json}\nschema rule\n{/form-only}\nafter\n";
|
||||||
|
let segments = split(input);
|
||||||
|
assert_eq!(
|
||||||
|
segments,
|
||||||
|
vec![
|
||||||
|
Segment::Prose("before\n".to_string()),
|
||||||
|
Segment::FormOnly { form: Form::Json, body: "schema rule\n".to_string() },
|
||||||
|
Segment::Prose("after\n".to_string()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ailx_form_only_block_uses_ailx_form() {
|
||||||
|
let input = "{form-only: ailx}\ngrammar rule\n{/form-only}\n";
|
||||||
|
let segments = split(input);
|
||||||
|
assert_eq!(
|
||||||
|
segments,
|
||||||
|
vec![
|
||||||
|
Segment::FormOnly { form: Form::Ailx, body: "grammar rule\n".to_string() },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example_marker_is_isolated() {
|
||||||
|
let input = "before\n{example: empty_module}\nafter\n";
|
||||||
|
let segments = split(input);
|
||||||
|
assert_eq!(
|
||||||
|
segments,
|
||||||
|
vec![
|
||||||
|
Segment::Prose("before\n".to_string()),
|
||||||
|
Segment::Example("empty_module".to_string()),
|
||||||
|
Segment::Prose("after\n".to_string()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_directives_in_sequence() {
|
||||||
|
let input = "p1\n{example: a}\n{form-only: json}\njs\n{/form-only}\n{form-only: ailx}\nax\n{/form-only}\n{example: b}\np2\n";
|
||||||
|
let segments = split(input);
|
||||||
|
assert_eq!(
|
||||||
|
segments,
|
||||||
|
vec![
|
||||||
|
Segment::Prose("p1\n".to_string()),
|
||||||
|
Segment::Example("a".to_string()),
|
||||||
|
Segment::FormOnly { form: Form::Json, body: "js\n".to_string() },
|
||||||
|
Segment::FormOnly { form: Form::Ailx, body: "ax\n".to_string() },
|
||||||
|
Segment::Example("b".to_string()),
|
||||||
|
Segment::Prose("p2\n".to_string()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_form_name_is_an_error() {
|
||||||
|
let input = "{form-only: yaml}\nx\n{/form-only}\n";
|
||||||
|
let result = std::panic::catch_unwind(|| split(input));
|
||||||
|
assert!(result.is_err(), "split should panic on unknown form name");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unclosed_form_only_is_an_error() {
|
||||||
|
let input = "{form-only: json}\nopen\n";
|
||||||
|
let result = std::panic::catch_unwind(|| split(input));
|
||||||
|
assert!(result.is_err(), "split should panic on unclosed form-only block");
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//! Token-balance gate for master/spec.md form-only blocks.
|
||||||
|
//!
|
||||||
|
//! The mini-spec is the experimental treatment; if the JSON-cohort's
|
||||||
|
//! form-only scaffolding is meaningfully longer than the AILX-cohort's
|
||||||
|
//! (or vice versa), the experiment is biased before the model ever
|
||||||
|
//! sees the prompts. This test catches gross imbalance.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use xmodel_render::splitter::{self, Form, Segment};
|
||||||
|
|
||||||
|
fn master_dir() -> PathBuf {
|
||||||
|
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
manifest.parent().unwrap().join("master")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_only_block_token_totals_within_five_percent() {
|
||||||
|
let spec_text = std::fs::read_to_string(master_dir().join("spec.md"))
|
||||||
|
.expect("master/spec.md must exist");
|
||||||
|
let segments = splitter::split(&spec_text);
|
||||||
|
|
||||||
|
let mut json_buf = String::new();
|
||||||
|
let mut ailx_buf = String::new();
|
||||||
|
for seg in &segments {
|
||||||
|
if let Segment::FormOnly { form, body } = seg {
|
||||||
|
match form {
|
||||||
|
Form::Json => json_buf.push_str(body),
|
||||||
|
Form::Ailx => ailx_buf.push_str(body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bpe = tiktoken_rs::cl100k_base().expect("cl100k_base tokenizer should load");
|
||||||
|
let json_count = bpe.encode_with_special_tokens(&json_buf).len();
|
||||||
|
let ailx_count = bpe.encode_with_special_tokens(&ailx_buf).len();
|
||||||
|
assert!(json_count > 0, "JSON-form blocks produced zero tokens");
|
||||||
|
assert!(ailx_count > 0, "AILX-form blocks produced zero tokens");
|
||||||
|
|
||||||
|
let max = json_count.max(ailx_count) as f64;
|
||||||
|
let diff = (json_count as i64 - ailx_count as i64).unsigned_abs() as f64;
|
||||||
|
let ratio = diff / max;
|
||||||
|
assert!(
|
||||||
|
ratio <= 0.05,
|
||||||
|
"form-only block token imbalance: json={json_count}, ailx={ailx_count}, ratio={ratio:.4} > 0.05",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
# 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 AILX 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 AILX form is the printed dual of the JSON form: `ail parse
|
||||||
|
file.ailx` produces the same canonical bytes as the JSON form would,
|
||||||
|
and `ail render file.ail.json` produces the AILX text. Round-trip
|
||||||
|
through this pair is gated by an integration test.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module fn_returns_int
|
||||||
|
(fn answer
|
||||||
|
(doc "The simplest possible fn — no params, returns a fixed Int.")
|
||||||
|
(type (fn-type (params) (ret (con Int))))
|
||||||
|
(params)
|
||||||
|
(body 42)))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Types
|
||||||
|
|
||||||
|
A type is one of four shapes. Every type position in a function
|
||||||
|
signature must be filled in; there is no implicit type inference at
|
||||||
|
the binding form. Constructor types may carry type arguments.
|
||||||
|
Functions are first-class — function types appear as parameter and
|
||||||
|
return types.
|
||||||
|
|
||||||
|
A type constructor names a base type or a user-defined ADT. The
|
||||||
|
prelude provides `Int`, `Bool`, `Str`, `Float`, `Unit`. User ADTs are
|
||||||
|
introduced with the `data` form (see section 6); after declaration
|
||||||
|
their name is usable wherever a type constructor is expected.
|
||||||
|
|
||||||
|
A type variable is a placeholder introduced by `forall` at the top
|
||||||
|
of a function signature. Within the signature the variable behaves
|
||||||
|
as an opaque type; the toolchain instantiates it at each call site.
|
||||||
|
|
||||||
|
A function type names parameter types, parameter modes (see section
|
||||||
|
4), a return type, a return mode, and an effect row (see section 8).
|
||||||
|
|
||||||
|
A `forall` is universal quantification: it lists the type variables
|
||||||
|
bound and the body type they appear in. Forall is only valid at the
|
||||||
|
top of a function or constant signature.
|
||||||
|
|
||||||
|
|
||||||
|
Type::Con: `Int`, `Bool`, `Str`, `Float`, `Unit`, or a user type
|
||||||
|
name. For parameterised types: `(con List Int)` reads "List of Int";
|
||||||
|
nested: `(con Map (con Str) (con Int))`.
|
||||||
|
|
||||||
|
Type::Var: a bare identifier alone in type position (no wrapper),
|
||||||
|
e.g. `a` inside a `(forall (a) ...)`. Type variables are
|
||||||
|
syntactically distinguished from type constructors by being declared
|
||||||
|
in the enclosing `forall`.
|
||||||
|
|
||||||
|
Type::Fn: `(fn-type (params T1 T2) (ret RET) (effects E1 E2))`. The
|
||||||
|
parameter and effect lists may be empty. Modes wrap the relevant
|
||||||
|
type: `(own T)` or `(borrow T)` — see section 4.
|
||||||
|
|
||||||
|
Type::Forall: `(forall (a b) BODY-TYPE)` quantifies over `a` and
|
||||||
|
`b`. Class constraints attach as `(forall (a) (constraints (Eq a))
|
||||||
|
BODY-TYPE)`.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module forall_polymorphic
|
||||||
|
(fn id
|
||||||
|
(doc "The polymorphic identity function. Exercises Type::Forall and Type::Var.")
|
||||||
|
(type (forall (vars a) (fn-type (params a) (ret a))))
|
||||||
|
(params x)
|
||||||
|
(body x)))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Mode annotations
|
||||||
|
|
||||||
|
Every function parameter carries a mode. The mode says how the
|
||||||
|
callee may treat the value: `implicit` is the legacy default
|
||||||
|
(treated as `own`), `own` means the caller transfers ownership and
|
||||||
|
the callee consumes the value, `borrow` means the caller retains
|
||||||
|
ownership and the callee may inspect but not consume.
|
||||||
|
|
||||||
|
The mode also applies to the return value: `ret_mode` says whether
|
||||||
|
the function transfers ownership to the caller (`own`) or returns a
|
||||||
|
borrow (`borrow`). For most functions the return mode is `own` or
|
||||||
|
implicit; `borrow` returns are rare and used only for accessor-style
|
||||||
|
functions.
|
||||||
|
|
||||||
|
Modes are mandatory on every parameter and on the return position.
|
||||||
|
The schema rejects unannotated fn signatures only when annotations
|
||||||
|
are explicitly requested; for compatibility the legacy form (no
|
||||||
|
mode field at all) is treated as all-implicit.
|
||||||
|
|
||||||
|
|
||||||
|
Modes are surface wrappers around the type in parameter and return
|
||||||
|
position. Inside a `(fn-type ...)`:
|
||||||
|
|
||||||
|
- `(params (own T1) (borrow T2))` — `T1` is owned, `T2` is borrowed.
|
||||||
|
- `(ret (own T))` — the function returns an owned value.
|
||||||
|
- A bare type with no mode wrapper is implicit (legacy default).
|
||||||
|
|
||||||
|
When all parameters and the return are implicit, the surface omits
|
||||||
|
the mode wrappers entirely; this is the canonical form for fns that
|
||||||
|
do not yet need explicit annotations. Adding modes is a forward
|
||||||
|
change: an existing fn keeps the same canonical bytes as long as no
|
||||||
|
annotation is added.
|
||||||
|
|
||||||
|
Writing `(borrow T)` on a parameter the typechecker wants `own`
|
||||||
|
for surfaces as the `over-strict-mode` diagnostic — advisory, and
|
||||||
|
suppressable via the fn def's `suppress` clause. The mode is part
|
||||||
|
of the fn's signature and contributes to its content hash, so
|
||||||
|
making the choice explicit at authoring time avoids surprise
|
||||||
|
hash changes when the typechecker's defaults shift.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module param_modes_all
|
||||||
|
(fn f_implicit
|
||||||
|
(doc "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(params x)
|
||||||
|
(body x))
|
||||||
|
(fn f_own
|
||||||
|
(doc "(own T) — caller transfers ownership; callee consumes.")
|
||||||
|
(type (fn-type (params (own (con Int))) (ret (con Int))))
|
||||||
|
(params x)
|
||||||
|
(body x))
|
||||||
|
(fn f_borrow
|
||||||
|
(doc "(borrow T) — caller retains ownership; callee may not consume.")
|
||||||
|
(type (fn-type (params (borrow (con Int))) (ret (con Int))))
|
||||||
|
(params x)
|
||||||
|
(body x)))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Functions
|
||||||
|
|
||||||
|
A function definition binds a name to a body of code. The signature
|
||||||
|
declares the parameter types, the return type, and the effect set
|
||||||
|
(if any). The body is an expression — there is no statement form.
|
||||||
|
Functions are first-class values: they may be passed to other
|
||||||
|
functions, returned, stored in data structures.
|
||||||
|
|
||||||
|
Inside a function body, four term forms appear most often: a
|
||||||
|
variable reference (the name of a parameter, a local binding, or a
|
||||||
|
top-level definition); a function application (a callee plus
|
||||||
|
positional arguments); a lambda (an anonymous function with its
|
||||||
|
own signature); a literal value (see section 9).
|
||||||
|
|
||||||
|
Lambdas carry their own parameter list, parameter types, return
|
||||||
|
type, and effect set. Lambdas may close over names from the
|
||||||
|
enclosing lexical scope; the closure is captured by value (the
|
||||||
|
captured values are reference-counted under `--alloc=rc`).
|
||||||
|
|
||||||
|
A `let` binds a name to the value of one expression for the
|
||||||
|
duration of another expression. A `letrec` is the recursive variant:
|
||||||
|
the bound name is visible inside its own body. Both are
|
||||||
|
expressions, not statements — they have a value.
|
||||||
|
|
||||||
|
|
||||||
|
Function definition: `(fn NAME (doc STRING)? (type TYPE) (params
|
||||||
|
NAME*) (body TERM))`. The `doc` clause is optional but recommended.
|
||||||
|
|
||||||
|
Term forms relevant here:
|
||||||
|
|
||||||
|
- Variable reference: a bare identifier, e.g. `x` or `+`.
|
||||||
|
- Application: `(app CALLEE ARG*)` — head is `app`, then the callee
|
||||||
|
term, then positional arguments.
|
||||||
|
- Lambda: `(lam (params NAME*) (paramTypes TYPE*) (ret TYPE)
|
||||||
|
(effects EFFECT*) BODY)`.
|
||||||
|
- Let: `(let (NAME VALUE) BODY)` — binds `NAME` to `VALUE` in
|
||||||
|
`BODY`.
|
||||||
|
- Letrec: `(letrec NAME (type FN-TYPE) (params NAME*) BODY IN-BODY)`
|
||||||
|
— `NAME` is recursively visible inside `BODY`.
|
||||||
|
|
||||||
|
Inside any term position, an integer literal is a bare decimal
|
||||||
|
number, a string is a double-quoted string, a bool is `true` or
|
||||||
|
`false`, unit is `(unit)`.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module fn_calls_prelude
|
||||||
|
(fn add
|
||||||
|
(doc "Add two Ints via the polymorphic prelude `+`. Also exercises TermLet by binding the sum to a local name before returning it, and TermClone by re-using the let-bound value.")
|
||||||
|
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
|
||||||
|
(params x y)
|
||||||
|
(body (let s (app + x y) (clone s)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module fn_with_lambda
|
||||||
|
(fn make_adder
|
||||||
|
(doc "Curried adder: takes an Int and returns an Int -> Int closure. Exercises TermLam and Type::Fn appearing as a return type.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (fn-type (params (con Int)) (ret (con Int))))))
|
||||||
|
(params x)
|
||||||
|
(body (lam (params (typed y (con Int))) (ret (con Int)) (body (app + x y))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Algebraic data types
|
||||||
|
|
||||||
|
A data type declaration introduces a new named type with one or
|
||||||
|
more constructors. Each constructor takes zero or more positional
|
||||||
|
argument types and produces a value of the declared type. ADTs may
|
||||||
|
be parameterised by type variables; the variables are listed before
|
||||||
|
the constructors and may appear in the constructor argument types.
|
||||||
|
|
||||||
|
A constructor is invoked at the term level by naming the type and
|
||||||
|
the constructor plus the positional arguments. The runtime
|
||||||
|
representation under `--alloc=rc` is a tagged heap cell with one
|
||||||
|
slot per constructor field; the tag identifies which constructor
|
||||||
|
was used.
|
||||||
|
|
||||||
|
Constructor names live in their own namespace, separate from
|
||||||
|
function names. The same identifier may be a function and a
|
||||||
|
constructor without conflict, though for readability the convention
|
||||||
|
is constructors are capitalised and functions are lower-case.
|
||||||
|
|
||||||
|
|
||||||
|
Type definition: `(data NAME (vars TYVAR*)? (doc STRING)? (ctor
|
||||||
|
CTOR-NAME ARG-TYPE*)*)`. The `vars` clause is optional; nullary
|
||||||
|
constructors have an empty argument list.
|
||||||
|
|
||||||
|
Example: `(data List (ctor Nil) (ctor Cons Int (con List)))` is a
|
||||||
|
monomorphic Int list with a Nil and a Cons constructor.
|
||||||
|
|
||||||
|
Constructor invocation: `(ctor TYPE-NAME CTOR-NAME ARG*)`. The
|
||||||
|
`TYPE-NAME` precedes the constructor name so the parser does not
|
||||||
|
need a separate registry to resolve overlapping constructor names
|
||||||
|
across ADTs. Example: `(ctor List Cons 1 (ctor List Nil))` builds a
|
||||||
|
single-element list.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module data_simple
|
||||||
|
(data Box (vars a)
|
||||||
|
(doc "A unary box around a polymorphic value.")
|
||||||
|
(ctor MkBox a))
|
||||||
|
(fn wrap_one
|
||||||
|
(doc "Construct a Box Int via MkBox. The body uses (reuse-as src body) — a hint that the freshly-allocated Box should reuse the source's memory slot under --alloc=rc. Exercises TermCtor, ReuseAs, and Type::Var inside a ctor field position.")
|
||||||
|
(type (fn-type (params (own (con Box (con Int)))) (ret (own (con Box (con Int))))))
|
||||||
|
(params src)
|
||||||
|
(body (reuse-as src (term-ctor Box MkBox 1)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Pattern matching
|
||||||
|
|
||||||
|
Pattern matching is the way to inspect an ADT value. A `match`
|
||||||
|
form takes a scrutinee expression plus one or more arms; each arm
|
||||||
|
pairs a pattern with a body. Arms are tried top-to-bottom; the
|
||||||
|
first matching arm's body is evaluated. The toolchain checks
|
||||||
|
exhaustiveness: every constructor of the scrutinee's type must be
|
||||||
|
reachable through some arm, otherwise the typechecker errors.
|
||||||
|
|
||||||
|
There are four pattern shapes. A wildcard matches anything and
|
||||||
|
binds nothing. A variable pattern matches anything and binds the
|
||||||
|
matched value to the named identifier for use inside the arm body.
|
||||||
|
A literal pattern matches only when the scrutinee is bit-equal to
|
||||||
|
the literal value; literal patterns work on Int, Bool, Str, and
|
||||||
|
Unit (Float patterns are rejected at typecheck — see section 9).
|
||||||
|
A constructor pattern matches when the scrutinee is built by the
|
||||||
|
named constructor and recursively matches the constructor's
|
||||||
|
fields.
|
||||||
|
|
||||||
|
|
||||||
|
Match expression: `(match SCRUT (case PAT BODY) (case PAT BODY)
|
||||||
|
...)`. Arms are introduced by `(case ...)`; the order matters (top-
|
||||||
|
to-bottom).
|
||||||
|
|
||||||
|
Pattern shapes:
|
||||||
|
- Wildcard: `_` — bare underscore is the wildcard pattern.
|
||||||
|
- Variable: a bare identifier alone, e.g. `h`, binds the scrutinee.
|
||||||
|
- Literal: a literal value alone, e.g. `0`, `true`, `"abc"`,
|
||||||
|
`(unit)`, matches bit-equal scrutinees.
|
||||||
|
- Constructor: `(ctor CTOR-NAME SUBPAT*)` — note no `type` slot
|
||||||
|
here, because the constructor name is resolved against the
|
||||||
|
scrutinee's type.
|
||||||
|
|
||||||
|
Pattern variables introduced by a `(case PAT BODY)` are in scope
|
||||||
|
inside `BODY` and bind there only. They do not leak into sibling
|
||||||
|
arms or into code outside the `match`.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module data_with_match
|
||||||
|
(data List
|
||||||
|
(doc "Monomorphic singly-linked Int list — boxed, recursive.")
|
||||||
|
(ctor Nil)
|
||||||
|
(ctor Cons (con Int) (con List)))
|
||||||
|
(fn head_or_zero
|
||||||
|
(doc "Return the head of xs, or 0 if empty. Exercises TermMatch with two arms, PatternCtor (both nullary Nil and binary Cons), and PatternWild on the tail field.")
|
||||||
|
(type (fn-type (params (borrow (con List))) (ret (con Int))))
|
||||||
|
(params xs)
|
||||||
|
(body (match xs
|
||||||
|
(case (pat-ctor Nil) 0)
|
||||||
|
(case (pat-ctor Cons h _) h))))
|
||||||
|
(fn count_via_letrec
|
||||||
|
(doc "Walk xs counting nodes using a local recursive let. Exercises TermLetRec and TermVar; the recursive `go` is bound locally.")
|
||||||
|
(type (fn-type (params (borrow (con List))) (ret (con Int))))
|
||||||
|
(params xs)
|
||||||
|
(body (let-rec go (params ys) (type (fn-type (params (borrow (con List))) (ret (con Int)))) (body (match ys
|
||||||
|
(case (pat-ctor Nil) 0)
|
||||||
|
(case (pat-ctor Cons _ t) (app + 1 (app go t))))) (in (app go xs))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module match_literal_pattern
|
||||||
|
(fn classify
|
||||||
|
(doc "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(params n)
|
||||||
|
(body (match n
|
||||||
|
(case (pat-lit 0) 100)
|
||||||
|
(case _ 200))))
|
||||||
|
(fn sign_if
|
||||||
|
(doc "Return -1 / 0 / 1 for the sign of n using nested TermIf. Exercises TermIf in both then/else positions plus TermApp on the polymorphic comparison.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(params n)
|
||||||
|
(body (if (app < n 0) -1 (if (app == n 0) 0 1)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Effects
|
||||||
|
|
||||||
|
Every function type carries an effect row — a set of effect labels
|
||||||
|
that the function may raise. The two currently wired effects are
|
||||||
|
`IO` (required to call effect operations like `io/print_int`) and
|
||||||
|
`Diverge` (used for functions that may not terminate). An empty
|
||||||
|
effect row marks the function as pure.
|
||||||
|
|
||||||
|
Effect operations are reached through the `do` term, not through a
|
||||||
|
regular function application. The `do` form names the operation
|
||||||
|
(e.g. `io/print_int`) and lists the operation's arguments; the
|
||||||
|
typechecker resolves the operation against the prelude's effect-op
|
||||||
|
table, checks the arguments, and accumulates the operation's
|
||||||
|
effect label into the enclosing function's effect row.
|
||||||
|
|
||||||
|
A `seq` term sequences two effectful expressions: the left-hand
|
||||||
|
side is evaluated for its effects and its result discarded, then
|
||||||
|
the right-hand side is evaluated and its value is the value of the
|
||||||
|
whole expression. `seq` is the canonical way to perform multiple
|
||||||
|
IO operations one after the other inside a function body.
|
||||||
|
|
||||||
|
|
||||||
|
Effect row on a fn type: `(effects E1 E2 ...)` inside the
|
||||||
|
`(fn-type ...)`. Empty row: omit the `(effects)` clause or write
|
||||||
|
`(effects)`. Common effectful row: `(effects IO)`. Order is sorted
|
||||||
|
in canonical output.
|
||||||
|
|
||||||
|
Effect operation: `(do OP-NAME ARG*)` — the leading `do` head is
|
||||||
|
the parser's signal that the next identifier is an effect op, not a
|
||||||
|
function. Example: `(do io/print_int 42)` prints the integer 42
|
||||||
|
and raises the `IO` effect.
|
||||||
|
|
||||||
|
Sequencing: `(seq LHS RHS)`. The value of `(seq X Y)` is `Y`'s
|
||||||
|
value; `X`'s value is discarded. Chains of sequencing are written
|
||||||
|
nested: `(seq A (seq B C))` runs `A`, then `B`, then yields `C`'s
|
||||||
|
value.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module fn_with_do_seq
|
||||||
|
(fn main
|
||||||
|
(doc "Print two Ints in sequence. Exercises TermDo, TermSeq, and the IO effect on a fn type. The trailing unit return is implicit in the last Do (op returns Unit).")
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body (seq (do io/print_int 1) (do io/print_int 2)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Literals
|
||||||
|
|
||||||
|
There are five literal shapes. An integer literal is a signed
|
||||||
|
64-bit decimal. A boolean literal is `true` or `false`. A string
|
||||||
|
literal is a UTF-8 sequence in double quotes; AILang restricts the
|
||||||
|
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
|
||||||
|
ASCII bytes are an authoring error. A unit literal is the value of
|
||||||
|
type Unit, written `(unit)` in AILX 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 AILX parser converts
|
||||||
|
to the 64-bit bit pattern and emits the canonical 16-character
|
||||||
|
lowercase hex string into the JSON. You never author the hex
|
||||||
|
encoding by hand; the toolchain owns that representation. NaN and
|
||||||
|
±Inf are representable through the prelude constants `nan`, `inf`,
|
||||||
|
and `neg_inf` (see section 11).
|
||||||
|
|
||||||
|
Float pattern matching is rejected at typecheck. IEEE-`==` makes
|
||||||
|
Float patterns semantically dubious (NaN never matches; equality
|
||||||
|
is bit-exact, not approximate), so the language hard-rejects them
|
||||||
|
to push authors toward comparison-based dispatch.
|
||||||
|
|
||||||
|
|
||||||
|
Literal surface forms:
|
||||||
|
|
||||||
|
- Int: bare signed decimal, e.g. `42`, `-7`, `0`.
|
||||||
|
- Bool: `true` or `false` as bare keywords.
|
||||||
|
- Str: double-quoted with `\"`, `\\`, `\n`, `\t` escapes, e.g.
|
||||||
|
`"hello"`. Bytes outside ASCII printable are rejected by the
|
||||||
|
authoring rule (Decision 6 Constraint 3).
|
||||||
|
- Unit: the keyword `(unit)`.
|
||||||
|
- Float: bare decimal with at least one digit on each side of the
|
||||||
|
point, e.g. `3.14`, `0.5`, `-2.0`. The parser converts to the
|
||||||
|
bit-pattern hex string when emitting JSON; you never author the
|
||||||
|
hex form.
|
||||||
|
|
||||||
|
There is no separate syntax for exponent-form floats in the v1
|
||||||
|
authoring surface; for non-finite values use the prelude constants
|
||||||
|
`nan`, `inf`, `neg_inf`.
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module floats
|
||||||
|
(const pi
|
||||||
|
(doc "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).")
|
||||||
|
(type (con Float))
|
||||||
|
(body 3.14)))
|
||||||
|
```
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module bool_str
|
||||||
|
(fn is_true
|
||||||
|
(doc "Trivial Bool literal. Exercises Literal::Bool.")
|
||||||
|
(type (fn-type (params) (ret (con Bool))))
|
||||||
|
(params)
|
||||||
|
(body true))
|
||||||
|
(fn greeting
|
||||||
|
(doc "Trivial Str literal. Exercises Literal::Str.")
|
||||||
|
(type (fn-type (params) (ret (con Str))))
|
||||||
|
(params)
|
||||||
|
(body "hi"))
|
||||||
|
(fn unit_value
|
||||||
|
(doc "Trivial Unit literal — needed so Literal::Unit appears at least once in the corpus.")
|
||||||
|
(type (fn-type (params) (ret (con Unit))))
|
||||||
|
(params)
|
||||||
|
(body (lit-unit))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Typeclasses
|
||||||
|
|
||||||
|
A typeclass is a named bundle of operations that may be
|
||||||
|
implemented for multiple types. The class declaration lists the
|
||||||
|
methods; each instance declaration provides the bodies for a
|
||||||
|
specific type. The compiler resolves method calls at the call site
|
||||||
|
and monomorphises the implementation: there is no runtime dispatch.
|
||||||
|
|
||||||
|
A class is parameterised by a single type variable (multi-parameter
|
||||||
|
classes are not supported in v1). Methods are declared with their
|
||||||
|
full type signature; the class parameter appears as a type variable
|
||||||
|
inside the method type. Methods may carry an optional default body
|
||||||
|
that instances inherit unless they explicitly override.
|
||||||
|
|
||||||
|
Class constraints attach to polymorphic function types: a function
|
||||||
|
`forall a. (Eq a) => (a, a) -> Bool` requires its callers to
|
||||||
|
provide an `Eq` instance for `a` at the call site. The constraint
|
||||||
|
appears inside the `forall` quantifier, in the `constraints` slot.
|
||||||
|
|
||||||
|
The prelude provides four built-in classes: `Eq` (equality), `Ord`
|
||||||
|
(ordering with `<`, `<=`, `>`, `>=`), `Num` (arithmetic — `+`, `-`,
|
||||||
|
`*`, `/`, `neg`), and `Bounded`. Their instances for Int, Bool,
|
||||||
|
Str, Float, and Unit are built in; you do not declare them.
|
||||||
|
|
||||||
|
|
||||||
|
Class declaration: `(class NAME (param TYVAR) (method NAME (type
|
||||||
|
METHOD-TYPE) (default BODY)?)*)`. The `default` clause is
|
||||||
|
optional; methods without a default must be supplied by every
|
||||||
|
instance.
|
||||||
|
|
||||||
|
Instance declaration: `(instance NAME (type CONCRETE-TYPE) (method
|
||||||
|
NAME BODY)*)`. The method bodies must match the class's method
|
||||||
|
signatures with the class parameter substituted to the concrete
|
||||||
|
type.
|
||||||
|
|
||||||
|
Constraints inside a forall: `(forall (a) (constraints (Eq a) (Ord
|
||||||
|
a)) BODY-TYPE)`. Each constraint is `(CLASS-NAME TYPE)` — usually
|
||||||
|
the type is a single bound type variable but a concrete type is
|
||||||
|
also legal (typically a typecheck error unless an instance exists).
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module class_def
|
||||||
|
(class MyShow
|
||||||
|
(param a)
|
||||||
|
(doc "A toy single-method class. The instance in instance_def.ail.json provides a body for `show` at type Int.")
|
||||||
|
(method show
|
||||||
|
(type (fn-type (params a) (ret (con Str)))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
```ailx
|
||||||
|
(module instance_def
|
||||||
|
(class MyShow
|
||||||
|
(param a)
|
||||||
|
(doc "Re-declared here so this fixture is self-contained; class_def.ail.json declares the same class verbatim.")
|
||||||
|
(method show
|
||||||
|
(type (fn-type (params a) (ret (con Str))))))
|
||||||
|
(instance
|
||||||
|
(class MyShow)
|
||||||
|
(type (con Int))
|
||||||
|
(doc "Trivial MyShow Int — show always returns the fixed string \"int\".")
|
||||||
|
(method show
|
||||||
|
(body "int"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. The prelude
|
||||||
|
|
||||||
|
The prelude provides a fixed set of operators, conversion functions,
|
||||||
|
and effect operations that are in scope without any import. The
|
||||||
|
operators are polymorphic over a small set of types and resolve at
|
||||||
|
the call site by the resolved argument type.
|
||||||
|
|
||||||
|
| Name | Type | Notes |
|
||||||
|
|------|------|------|
|
||||||
|
| `+` `-` `*` `/` | `forall a. (a, a) -> a` | Int and Float; codegen filters at use site |
|
||||||
|
| `%` | `(Int, Int) -> Int` | Int only |
|
||||||
|
| `==` `!=` `<` `<=` `>` `>=` | `forall a. (a, a) -> Bool` | Int, Bool, Str, Float, Unit |
|
||||||
|
| `not` | `(Bool) -> Bool` | Boolean negation |
|
||||||
|
| `neg` | `forall a. (a) -> a` | Int and Float negation |
|
||||||
|
| `int_to_float` | `(Int) -> Float` | Widening; codegen uses `sitofp` |
|
||||||
|
| `float_to_int_truncate` | `(Float) -> Int` | Saturating truncation to zero |
|
||||||
|
| `float_to_str` | `(Float) -> Str` | Formatted decimal |
|
||||||
|
| `is_nan` | `(Float) -> Bool` | True only for NaN bit patterns |
|
||||||
|
| `nan` `inf` `neg_inf` | `Float` | Bare-value constants |
|
||||||
|
| `__unreachable__` | `forall a. a` | Polymorphic bottom; LLVM `unreachable` |
|
||||||
|
|
||||||
|
Effect operations (reached through `do`, not `app`):
|
||||||
|
|
||||||
|
| Op | Signature | Effect |
|
||||||
|
|----|----------|-------|
|
||||||
|
| `io/print_int` | `(Int) -> Unit` | `IO` |
|
||||||
|
| `io/print_bool` | `(Bool) -> Unit` | `IO` |
|
||||||
|
| `io/print_str` | `(Str) -> Unit` | `IO` |
|
||||||
|
| `io/print_float` | `(Float) -> Unit` | `IO` |
|
||||||
|
|
||||||
|
`int_to_str` is intentionally NOT in the prelude — it is type-
|
||||||
|
installed in a future milestone but codegen-deferred pending the
|
||||||
|
heap-Str ABI work. Do not call it.
|
||||||
|
|
||||||
|
## 12. Content addressing
|
||||||
|
|
||||||
|
AILang has content-addressed identity: every top-level definition
|
||||||
|
has a 16-character hash derived from its canonical bytes. The
|
||||||
|
toolchain computes hashes on `ail parse` and `ail check`; the
|
||||||
|
author writes no hash literal in either form, and the JSON form
|
||||||
|
has no `hash` field. This is symmetric across forms and removes
|
||||||
|
hash as a form-distinguishing factor. You author code; the
|
||||||
|
toolchain owns the identity.
|
||||||
|
|
||||||
|
## 13. Out of scope, and closing directive
|
||||||
|
|
||||||
|
This reference does NOT teach: cross-module imports (every task is
|
||||||
|
a single self-contained module with an empty `imports` array),
|
||||||
|
refinement types (reserved in the schema but pass-through in this
|
||||||
|
version), point-free style (cut by Decision 6), operator
|
||||||
|
overloading (cut), syntactic sugar of any kind (cut). The four
|
||||||
|
tasks you will be given do not need any of these features.
|
||||||
|
|
||||||
|
When asked to write a module, return the complete module text and
|
||||||
|
nothing else. No markdown fences. No prose explanation. No
|
||||||
|
preamble. The harness parses your response verbatim; any wrapping
|
||||||
|
text is treated as a parse error.
|
||||||
@@ -0,0 +1,997 @@
|
|||||||
|
# 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": [],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "var", "name": "a" },
|
||||||
|
"effects": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Mode annotations
|
||||||
|
|
||||||
|
Every function parameter carries a mode. The mode says how the
|
||||||
|
callee may treat the value: `implicit` is the legacy default
|
||||||
|
(treated as `own`), `own` means the caller transfers ownership and
|
||||||
|
the callee consumes the value, `borrow` means the caller retains
|
||||||
|
ownership and the callee may inspect but not consume.
|
||||||
|
|
||||||
|
The mode also applies to the return value: `ret_mode` says whether
|
||||||
|
the function transfers ownership to the caller (`own`) or returns a
|
||||||
|
borrow (`borrow`). For most functions the return mode is `own` or
|
||||||
|
implicit; `borrow` returns are rare and used only for accessor-style
|
||||||
|
functions.
|
||||||
|
|
||||||
|
Modes are mandatory on every parameter and on the return position.
|
||||||
|
The schema rejects unannotated fn signatures only when annotations
|
||||||
|
are explicitly requested; for compatibility the legacy form (no
|
||||||
|
mode field at all) is treated as all-implicit.
|
||||||
|
|
||||||
|
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"`.
|
||||||
|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "param_modes_all",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_implicit",
|
||||||
|
"doc": "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_own",
|
||||||
|
"doc": "(own T) — caller transfers ownership; callee consumes.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"param_modes": ["own"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "f_borrow",
|
||||||
|
"doc": "(borrow T) — caller retains ownership; callee may not consume.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"param_modes": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": { "t": "var", "name": "x" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Functions
|
||||||
|
|
||||||
|
A function definition binds a name to a body of code. The signature
|
||||||
|
declares the parameter types, the return type, and the effect set
|
||||||
|
(if any). The body is an expression — there is no statement form.
|
||||||
|
Functions are first-class values: they may be passed to other
|
||||||
|
functions, returned, stored in data structures.
|
||||||
|
|
||||||
|
Inside a function body, four term forms appear most often: a
|
||||||
|
variable reference (the name of a parameter, a local binding, or a
|
||||||
|
top-level definition); a function application (a callee plus
|
||||||
|
positional arguments); a lambda (an anonymous function with its
|
||||||
|
own signature); a literal value (see section 9).
|
||||||
|
|
||||||
|
Lambdas carry their own parameter list, parameter types, return
|
||||||
|
type, and effect set. Lambdas may close over names from the
|
||||||
|
enclosing lexical scope; the closure is captured by value (the
|
||||||
|
captured values are reference-counted under `--alloc=rc`).
|
||||||
|
|
||||||
|
A `let` binds a name to the value of one expression for the
|
||||||
|
duration of another expression. A `letrec` is the recursive variant:
|
||||||
|
the bound name is visible inside its own body. Both are
|
||||||
|
expressions, not statements — they have a value.
|
||||||
|
|
||||||
|
Function definition: `{"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" }
|
||||||
|
],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["x"],
|
||||||
|
"body": {
|
||||||
|
"t": "lam",
|
||||||
|
"params": ["y"],
|
||||||
|
"paramTypes": [ { "k": "con", "name": "Int" } ],
|
||||||
|
"retType": { "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
|
||||||
|
{
|
||||||
|
"schema": "ailang/v0",
|
||||||
|
"name": "data_simple",
|
||||||
|
"imports": [],
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"kind": "type",
|
||||||
|
"name": "Box",
|
||||||
|
"vars": ["a"],
|
||||||
|
"doc": "A unary box around a polymorphic value.",
|
||||||
|
"ctors": [
|
||||||
|
{
|
||||||
|
"name": "MkBox",
|
||||||
|
"fields": [ { "k": "var", "name": "a" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "fn",
|
||||||
|
"name": "wrap_one",
|
||||||
|
"doc": "Construct a Box Int via MkBox. The body uses (reuse-as src body) — a hint that the freshly-allocated Box should reuse the source's memory slot under --alloc=rc. Exercises TermCtor, ReuseAs, and Type::Var inside a ctor field position.",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [
|
||||||
|
{ "k": "con", "name": "Box", "args": [ { "k": "con", "name": "Int" } ] }
|
||||||
|
],
|
||||||
|
"param_modes": ["own"],
|
||||||
|
"ret": { "k": "con", "name": "Box", "args": [ { "k": "con", "name": "Int" } ] },
|
||||||
|
"ret_mode": "own",
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["src"],
|
||||||
|
"body": {
|
||||||
|
"t": "reuse-as",
|
||||||
|
"source": { "t": "var", "name": "src" },
|
||||||
|
"body": {
|
||||||
|
"t": "ctor",
|
||||||
|
"type": "Box",
|
||||||
|
"ctor": "MkBox",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 1 } } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Pattern matching
|
||||||
|
|
||||||
|
Pattern matching is the way to inspect an ADT value. A `match`
|
||||||
|
form takes a scrutinee expression plus one or more arms; each arm
|
||||||
|
pairs a pattern with a body. Arms are tried top-to-bottom; the
|
||||||
|
first matching arm's body is evaluated. The toolchain checks
|
||||||
|
exhaustiveness: every constructor of the scrutinee's type must be
|
||||||
|
reachable through some arm, otherwise the typechecker errors.
|
||||||
|
|
||||||
|
There are four pattern shapes. A wildcard matches anything and
|
||||||
|
binds nothing. A variable pattern matches anything and binds the
|
||||||
|
matched value to the named identifier for use inside the arm body.
|
||||||
|
A literal pattern matches only when the scrutinee is bit-equal to
|
||||||
|
the literal value; literal patterns work on Int, Bool, Str, and
|
||||||
|
Unit (Float patterns are rejected at typecheck — see section 9).
|
||||||
|
A constructor pattern matches when the scrutinee is built by the
|
||||||
|
named constructor and recursively matches the constructor's
|
||||||
|
fields.
|
||||||
|
|
||||||
|
Match expression: `{"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 — 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" },
|
||||||
|
"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": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["xs"],
|
||||||
|
"body": {
|
||||||
|
"t": "letrec",
|
||||||
|
"name": "go",
|
||||||
|
"type": {
|
||||||
|
"k": "fn",
|
||||||
|
"params": [ { "k": "con", "name": "List" } ],
|
||||||
|
"param_modes": ["borrow"],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Int" },
|
||||||
|
"effects": []
|
||||||
|
},
|
||||||
|
"params": ["n"],
|
||||||
|
"body": {
|
||||||
|
"t": "if",
|
||||||
|
"cond": {
|
||||||
|
"t": "app",
|
||||||
|
"fn": { "t": "var", "name": "<" },
|
||||||
|
"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": "==" },
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Unit" },
|
||||||
|
"effects": ["IO"]
|
||||||
|
},
|
||||||
|
"params": [],
|
||||||
|
"body": {
|
||||||
|
"t": "seq",
|
||||||
|
"lhs": {
|
||||||
|
"t": "do",
|
||||||
|
"op": "io/print_int",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 1 } } ]
|
||||||
|
},
|
||||||
|
"rhs": {
|
||||||
|
"t": "do",
|
||||||
|
"op": "io/print_int",
|
||||||
|
"args": [ { "t": "lit", "lit": { "kind": "int", "value": 2 } } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Literals
|
||||||
|
|
||||||
|
There are five literal shapes. An integer literal is a signed
|
||||||
|
64-bit decimal. A boolean literal is `true` or `false`. A string
|
||||||
|
literal is a UTF-8 sequence in double quotes; AILang restricts the
|
||||||
|
authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
|
||||||
|
ASCII bytes are an authoring error. A unit literal is the value of
|
||||||
|
type Unit, written `(unit)` in AILX 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 AILX 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 AILX, 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": [],
|
||||||
|
"ret": { "k": "con", "name": "Bool" },
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"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": [],
|
||||||
|
"ret": { "k": "con", "name": "Unit" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"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" } ],
|
||||||
|
"ret": { "k": "con", "name": "Str" },
|
||||||
|
"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.
|
||||||
Reference in New Issue
Block a user