The CLI help text claimed `render` was symmetric to `parse`, but
`render` was wired to ailang_core::pretty::module (an older
human-pretty form) while `parse` consumed form (A). Two different
"text projections" coexisted under one name.
Fix: Cmd::Render and Cmd::Describe (both branches) now call
ailang_surface::print, the actual inverse of `parse`. Round-trip
gate via `ail render | ail parse` is now an e2e test in
crates/ail/tests/e2e.rs in addition to the existing unit-level
round-trip across all 25 fixtures in ailang-surface/tests.
Cleanup: `ailang_core::pretty::module` and its three private
helpers (`def_block`, `term_block`, `term_inline`) deleted —
~260 LOC of duplicate-purpose code removed. pretty.rs goes
from 457 to 196 LOC. Surviving surface is intentionally narrow:
manifest, type_to_string, pattern_to_string — the diagnostic
stringification used in error messages, where one-line ML
notation reads better than form (A)'s nested S-expressions.
Module-level doc rewritten to nail down the single-purpose
framing.
Tests: 89/89 (e2e +1, ailang-core unit -1 since the deleted
test exercised the deleted fn).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third stdlib module. Either<e, a> is the first 2-type-var data def,
and the eliminator `either : (e -> c) -> (a -> c) -> Either<e, a> -> c`
the first fn with three type vars on top of the data — the deepest
polymorphism shipped end-to-end so far.
Five combinators: from_right, is_left, is_right, map_right, either.
Demo exercises every one and runs to deterministic output. IR shows
six distinct monomorphisations including two `from_right` variants
(Left=Int vs Left=$u depending on call site) and `either__I_I_I`.
No new compiler bugs surfaced: 14a / 14h / 15b coverage was wide
enough to handle 2-type-var data plus 3-type-var fns out of the box.
Discovered (queued as 15e): `ail render` and `ail parse` are not
symmetric. Form-(A) printer in ailang_surface::print is the actual
inverse of `parse` (round-trip test covers it across all 25 fixtures
including std_either) but is not exposed via the CLI; `render` still
calls the older ailang_core::pretty::module printer.
Tests: 89/89 (was 88, +1 e2e for std_either_demo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small empirical iter confirming the 14e tail-call story works
dogfood-practical. New fixture std_list_stress.ailx builds a
1000-element List<Int> via recursive build, folds it with both
fold_left (tail-marked in std_list) and fold_right (constructor-
blocked, unmarked), prints both sums (500500 each).
IR evidence at the monomorphised fold-recursion sites:
fold_left__I_I: musttail call i64 @ail_std_list_fold_left__I_I(...)
fold_right__I_I: plain call i64 @ail_std_list_fold_right__I_I(...)
The tail: true marker on std_list.fold_left's recursive call
survives monomorphisation through the __I_I specialisation.
fold_right correctly emits a plain call (recursive call is
second arg to f, not tail position).
Empirical findings at N=1000:
- fold_left runs in constant stack (musttail).
- fold_right runs in ~1000 frames. No segfault; default 8MB
Linux stack absorbs it comfortably.
- build (recursive, unmarked) likewise fits.
- End-to-end ~40ms wall (build + clang link); program <1ms.
- Boehm GC handles 2 × 1000 Cons allocations without symptom.
Tests 87 -> 88. Cumulative 30 e2e tests. cargo doc 0 warnings.
Cumulative state, post-15c: 2 stdlib modules (std_maybe,
std_list), 14 combinators, cross-module recursive ADTs working,
TCO surviving monomorphisation, GC at 1000-element scale. Four
compiler bugs surfaced + fixed in dogfood since 14a.
Natural pause point. Queue for future iters: 15d (std_either),
15e (std_pair), 16a (nested patterns), 16b (local rec let),
17a (per-fn arena, Decision 9 future-iter). None block further
stdlib work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second stdlib module. Tester wrote std_list.ailx (10 combinators,
164 LOC) and a consumer demo. std_list typechecked standalone;
demo did not, surfacing three compiler bugs:
1. Check-side: Iter 14h's qualify_local_types was applied to
Term::Var cross-module lookup but not to ctor-field types in
Term::Ctor synth or Pattern::Ctor resolution. First recursive
cross-module ADT (List has Cons a (List a) — recursive Con
self-ref) triggers the bug. std_maybe slipped through because
Maybe's ctors have no recursive Con field.
2. Codegen-side: same gap mirrored across 4 sites in codegen
(Term::Ctor synth, lower_ctor, lower_match) plus a tweak to
unify_for_subst (recurse on re-bind instead of strict equality
so sibling-derived List<Int> accepts nullary-ctor's List<$u>
wildcard).
3. Const codegen: emit_const rejected non-literal const bodies.
The demo's xs : List<Int> = Cons 1 (...) requires it. Fix:
per-module const table, Term::Var resolution loads literal
consts from global, inlines non-literal bodies. Bare and
qualified refs both supported.
All three fixes carry an "Iter 15b" code comment at their site.
~349/25 LOC across ailang-check, ailang-codegen, e2e.rs.
Tests 85 -> 87. New e2e std_list_demo asserts 11-line stdout:
length 5, is_empty false/true, head via from_maybe, tail length,
append length, reverse head, map double head, filter is_even
length, fold_left sum, fold_right sum. New ailang-check unit
test cross_module_recursive_adt_term_and_pat_ctor covers both
the original bug and the symmetric pat-ctor latent twin.
Hash invariance: all pre-15b fixtures + std_maybe defs
bit-identical. 14a / 14e / 14h regressions all green.
Cumulative state: 2 stdlib modules (std_maybe, std_list), 14
combinators, cross-module recursive ADT working end-to-end.
Three compiler bugs surfaced + fixed in dogfood since 14a (each
dogfood iter has surfaced ≥1).
Authoring observation: form (A) at 10 combinators is fine; main
friction is paren-counting in nested seq chains, not the form
itself. n-ary seq would help but is sugar.
Plan 15c: 1000-element list stress test for fold_left (tail-call-
marked) vs fold_right (constructor-blocked).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 15a tester surfaced a real compiler limitation: cross-module
type and ctor references were not implemented. Iter 5b only
carried fns + consts via module_globals; types/ctors stayed
module-local with an explicit DESIGN comment. This iter completes
the cross-module mechanism using the Iter-5b convention:
qualified-only access via module.Name.
Implementation:
- ailang-check: Env.module_types populated by build_module_types.
Qualified resolution in Type::Con, Term::Ctor, with cross-module
fallback for pat-ctor (local wins, multi-import collision -> new
ambiguous-ctor diagnostic). Four new unit tests.
- ailang-codegen: workspace-level module_ctor_index replaces
per-Emitter table. lookup_ctor_by_type / lookup_ctor_in_pattern
thread qualified type names through box-tag and field-type
resolution.
- examples/std_maybe_demo.{ailx,ail.json}: type-name slots now
qualified (std_maybe.Maybe).
- New e2e test cross_module_maybe_demo asserts the demo prints
["7","99","true","true","42"].
Net diff ~550 LOC. Tests 80 -> 85. All Iter 14a regressions
(parameterised_box_round_trip, parameterised_maybe_match,
list_map_poly_inc_then_prints, polymorphic_id_at_int_and_bool)
verified green — the 14h derive_substitution change (default
unpinned forall vars to Unit for monomorphiser) sits on a
different layer than 14a's $u-wildcard fix and they coexist.
Hash invariance: all five std_maybe def hashes unchanged. All
80-test-suite fixtures retain bit-identical hashes — cross-module
support is purely additive at the language level.
Process note: the std_maybe.ailx file landed in the 14g commit
via a sloppy git-add-A; should have spotted it before staging.
Not a correctness issue but a hygiene one.
Implementer flagged Unit-default monomorphisation as wasteful-
but-correct; rethink if stdlib grows toward overload-resolution-
style cases needing distinct unconstrained instantiations.
std_maybe stdlib effectively ships: module + four combinators +
e2e-tested consumer demo. Plan 15b: std_list importing std_maybe,
exercising Maybe-returning head/tail and tail-call-marked
fold_left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconsidered 14d's removal of Term::If. The decision was wrong.
"No redundancies" requires judgment; reducibility (if -> match)
is not redundancy in the strong sense. Term::If is a primitive
control-flow shape; bool branching is the second most common
shape after sequencing, and removing it cost 3x tokens on every
branch site (`(if c a b)` 4 tokens vs the match-on-Bool form
12 tokens).
Meta-pattern fixed: I had been treating user observations as
directives. User said "if is a subset of match"; I jumped to
remove it citing CLAUDE.md, with no independent conviction.
The leak appeared in 14f's JOURNAL prose ("three lines for what
if used to do in one"), which read as regret. Two feedback
memories saved (memory/feedback_user_suggestions_not_directives,
memory/feedback_no_nostalgia_for_removed_features) to head this
off.
Implementation: mechanical reverse-application of 14d's diff at
every site (AST, check including the 14e tail-position arm,
codegen 4 sites, surface parser/printer, pretty, CLI walker,
e2e test mutation). Removed lower_bool_match helper — it existed
only because 14d's migration shape needed codegen for non-ptr
match scrutinees; with Term::If back, match-on-Bool returns to
its pre-14d unsupported state. Three fixtures (sum, sort, max3)
restored to pre-14d shape. gc_stress (added in 14f) also
migrated back to (if ...) since it was authored under the wrong
constraint.
14e (musttail) and 14f (GC_malloc) verified intact in IR.
Hashes restored to pre-14d values:
- sum.sum: db33f57cb329935e
- sort.insert: 697fcb9f30f8633a
- max3.max: 65c45d6a45dd0a72
- max3.max3: 624b14429bf302f5
All other defs across all 18 fixtures keep their post-14f
hashes. Tests 80/80 green; cargo doc 0 warnings. LOC delta
+265/-295 net -30.
DESIGN.md Decision 7 preserved with a "Status: REVERTED" header
for audit trail. Form-(A) `if-term` production restored.
Plan: back to 15a (std_maybe stdlib module).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decision 9 ships. Through Iter 14e every ADT box, lambda env, and
closure pair was leaked. This iter substitutes GC_malloc for malloc
in all four IR allocation sites and links -lgc. No language change,
no AST change, no schema change.
Diff: 5 files modified, ~30 LOC net.
- codegen/lib.rs: 4 substitutions @malloc -> @GC_malloc.
- ail/main.rs: .arg("-lgc") in the clang invocation.
- 5 IR snapshot files: mechanical s/@malloc/@GC_malloc/, 9
occurrences. IR is bit-identical to pre-14f modulo this
substitution — exactly Decision 9's promise.
- e2e.rs: new test gc_handles_recursive_list_construction.
- examples/gc_stress.{ailx,ail.json}: new fixture, builds a 50-
element list via recursive Cons, sums it (1275).
Hash invariance verified: every existing fixture def hash
unchanged (codegen and link line are downstream of canonical
bytes; AST didn't move).
Tests 79 -> 80, all green. Existing 79 byte-identical stdout.
gc_stress -> 1275. list_map_poly -> 2/3/4 unchanged. sort
sorted-list unchanged. cargo doc 0 warnings.
GC notes (pertinent to future work):
- GC_INIT() not needed on Arch libgc 1.5.6 (auto-init via
__attribute__((constructor))).
- No conservative-scan over-retention observed.
- -lgc alone sufficient for link (pthread/dl transitive).
Pattern-shape note from gc_stress fixture writing: the post-14d
"if-then-else" replacement is `(match (app == n 0) (case
(pat-lit true) ...) (case (pat-wild) ...))`. Three lines for
what `if` used to do in one, but uniform with the language.
Worth flagging for the stdlib brief.
Language is feature-complete enough for stdlib. The three
blockers identified at the 14b boundary (redundancy 14d, tail
calls 14e, GC 14f) are all done. Plan 15a: first stdlib module
std_list.ailx with length/append/reverse/map/filter/fold_left/
fold_right/head/tail/is_empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decision 8 ships. Term::App and Term::Do gain tail: bool with
serde-default false and skip-when-false serialisation. New
typecheck pass verify_tail_positions enforces tail-position rules
(Scheme-style propagation through match arms, seq.rhs, let body,
lam body). Codegen emits musttail call for marked App calls.
Hash invariance verified: only the two migrated print_list defs
(list_map_poly.print_list, sort.print_list) changed hashes; all
other defs across all 18 fixtures kept bit-identical hashes —
confirms the skip-when-false serialisation rule works.
Tests 76 -> 79: tail_call_in_non_tail_position_is_rejected,
tail_call_in_tail_position_is_accepted, plus an IR-grep e2e test
asserting that print_list's recursive call site emits musttail
in the lowered IR. Existing 25 e2e tests unchanged in behaviour
(map -> [2,3,4], sort -> sorted list).
IR evidence at the recursive site:
%v7 = musttail call i8 @ail_list_map_poly_print_list(ptr %v6)
ret i8 %v7
Two deviations called out in the implementer report and JOURNAL:
1. tail-do uses tail call, not musttail. Cross-type return
(runtime helpers return i32, AILang Unit is i8) would have
LLVM reject musttail. Path is implemented but not exercised
by any current fixture; proper fix is runtime-helper signature
change, punted.
2. block_terminated flag in codegen so tail-call emit
(musttail call + ret) doesn't get a duplicate trailing ret
from surrounding code (match-arm phi, fn-body, lambda thunk).
Internal plumbing; required for IR well-formedness.
Form (A) productions now at ~30, exactly the constraint-1
budget. Future surface additions need to retire something or
explicit-budget-rebalance in DESIGN.md.
GC notes from implementer survey land in JOURNAL:
- Allocations cluster in lower_ctor; every term-ctor does
malloc(8+8n).
- Tail recursion does not reduce alloc pressure, only stack.
For map-style ctor-blocked recursions, allocation IS the
bottleneck.
- Per-fn arena is sound only when fn return type contains no
boxed ADT. Most current fixtures violate this.
Plan 14f: Boehm conservative GC (GC_malloc, -lgc) as a first
cut. Single-iter integration, no AST/schema change. Stress
test: build a 100k Cons list, observe RSS doesn't blow up.
After 14f the language is feature-complete enough for stdlib
work (15a).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Term::If was semantically a subset of Term::Match on Bool. Per
CLAUDE.md the language must contain no redundancies; two AST nodes
for the same operation produces an authoring decision with no
semantic content and a duplicate codegen path. Removed.
Migration shape (applied to sum, sort, max3 fixtures):
(if c a b) -> (match c (case (lit-bool true) a) (case _ b))
No schema version bump (per user direction): no third-party consumes
ailang/v0, so version ceremony is pure overhead. Edited AST and
fixtures in place; pinned hashes in hash.rs updated.
Implementer deviation, called out and justified: a tightly-scoped
lower_bool_match helper (~95 LOC) was needed in codegen because
the existing match path rejects i1 scrutinees and Pattern::Lit.
Helper accepts only the canonical two-arm migration shape, errors
on anything else, emits the same br/phi IR Term::If used to. No
generalisation of the ADT-match codegen.
Diff: 13 files, +286/-221 (net +65 LOC). AST got smaller
(one variant gone), form-(A) got smaller (one production gone),
typecheck got smaller (one branch gone). Codegen got slightly
larger by the bool-match helper.
Hash deltas: sum.sum, sort.insert, max3.max, max3.max3 changed.
All other defs (e.g. sum.main, sort.IntList, sort.sort,
sort.print_list, max3.main) kept bit-identical hashes — confirms
canonical-JSON byte format intact.
Verification: 76/76 tests green; sum->55, max3->17, sort->[1,1,2,
3,3,4,5,5,5,6,9] (identical to pre-migration). cargo doc 0 warnings.
Tail-call survey by implementer (informs 14e): print_list
recursions are already in tail position (rhs of seq inside match
arm); map/sort/insert recursions are NOT (constructor-blocked
inside Cons applications). 14e annotation will benefit terminal
recursions; ctor-blocked ones need accumulator-form rewrites in
source, not a compiler-side transform.
Decision 7 added to DESIGN.md. JOURNAL entry has the language-
completion sequence (14d done, 14e tail-calls, 14f GC, 15a stdlib).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strictly additive new crate per Decision 6 architectural pin.
JSON-AST stays the source of truth; ailang-surface is one
producer/consumer of ailang_core::ast::Module values. ailang-check
and ailang-codegen unchanged.
Crate contents:
- src/lex.rs (~264 LOC): 3-rule lexical core. Whitespace and parens
delimit tokens; semicolon to EOL is comment; first-character
classifier (digit -> int, " -> string, else -> ident).
- src/parse.rs (~1041 LOC): hand-written recursive descent. One Rust
fn per EBNF production. No parser-combinator dep.
- src/print.rs (~371 LOC): deterministic pretty-printer. Round-trip
contract with parse() is the surface's correctness gate.
- tests/round_trip.rs (~128 LOC): integration test runs every
examples/*.ail.json fixture through print -> parse -> canonical
JSON, asserts canonical-byte equality with the original.
Two AST-driven form widenings beyond the 14b sketch (both folded
into DESIGN.md Decision 6):
- lam-term carries (typed name type) params, ret type, and
optional effects (Term::Lam has parallel param_tys/ret_ty/
effects fields).
- import-clause admits (import name (as alias)?) (Import.alias
is Option<String>).
Production count ~28, under 30-rule budget. Constraint 1
(formalisable for foreign LLM) intact.
Verification:
- cargo build --workspace green.
- cargo test --workspace: 76 tests green (was 64; +9 surface unit,
+2 round-trip integration). All 17 fixtures round-trip
byte-identical at canonical level. 3 hand-written .ailx
exhibits parse to canonical JSON identical to their .ail.json
siblings.
- cargo doc --no-deps zero warnings (DESIGN.md item 6 invariant).
Manual smoke test (ail parse → ail run): hello, box,
list_map_poly all produce expected output through the form-(A)
authoring lane end-to-end.
CLI: ail parse <file.ailx> [-o <file.ail.json>]. .ail.json
remains a first-class input to every existing subcommand.
Plan 14d: stdlib (std_list.ailx with length/filter/fold/concat/
reverse/head/tail), authored in form (A) from day one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User clarified the architectural intent: the data structure is
the source of truth. The textual surface is one projection
among potentially many — visual / graphical front-ends are an
explicitly anticipated future lane.
The previous draft of Decision 6 said "JSON-AST demoted to
storage and exchange only", which was the wrong framing. The
JSON-AST stays canonical; form (A) is the AI authoring
projection but does not displace the AST.
Changes:
- New "Architectural pin: data structure is the source of truth"
subsection.
- Implementation outline reworded: parser is "strictly additive",
no schema changes, .ail.json remains a first-class input to
every existing subcommand.
- New "What this Decision deliberately does not do" subsection
to lock in the architectural commitments.
No code change. Pure documentation correction to the 14b
design pass before 14c starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User redirected at iter boundary: writing a stdlib in JSON was
the wrong move. The language is supposed to be the one I program
*best* in, and JSON-AST authoring is rationalisation, not
strength.
DESIGN.md Decision 6 captures the constraints that fall out of
the "formalisable for a foreign LLM" hard requirement (no
precedence, no semantic indentation, ASCII only, every AST node
a uniquely-tagged form), sketches three candidate notations with
the same `map` encoded in each, and picks form (A) — fully-tagged
S-expressions — as the first attempt with explicit rollback path
to form (C) if (A) hurts authoring.
Form (A) shape:
- 3-rule lexical core: sexpr / atom / token-classified-by-
first-character.
- Every AST node has a unique head keyword. No case-rule (no
"capitalised head means ctor"); ctors are explicit via
`(term-ctor TypeName CtorName args)` and `(pat-ctor CtorName
fields)`.
- Bare atoms get their sort from the parent slot (type-var
inside `(con NAME args)`, term-var inside `(app HEAD args)`,
pat-var inside `(pat-ctor CTOR fields)`, integer literal in
term position, etc.).
Empirical exhibits, hand-encoded:
- examples/hello.ailx 5 LOC (JSON was 36 pretty / 21 canonical)
- examples/box.ailx 25 LOC (JSON was 160 / 88)
- examples/list_map_poly.ailx 50 LOC (JSON was 394 / 230)
4-8x line reduction, ~4x character reduction. Bigger gains on
bigger programs since overhead is proportional to AST depth.
None are parseable yet — header comments say "Iter 14b design
exhibit, parser lands in 14c".
Two small spec issues caught while writing the exhibits and
folded back into DESIGN.md before committing:
- Operator idents (`+`, `==`) need the token-by-first-char
classification rule, not a word-shaped regex.
- Bool literals (`true`/`false`) reserved in term context;
unit is explicit `(lit-unit)`.
Tests unchanged (this iter is paper). 25/25 e2e green.
cargo doc --no-deps zero warnings.
Plan 14c: new crate `ailang-surface` with PEG parser, round-trip
hash-equivalence gate against every existing `examples/*.ail.json`,
CLI subcommand `ail parse`. If round-trip holds, stdlib starts
in `.ailx` form (Iter 14d).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dogfood payoff for parameterised ADTs (13a/b/c). Adds the first
program to nest a nullary ctor of a parameterised ADT inside a
parent ctor (Cons(Int, Nil)) and to call a polymorphic recursive
higher-order fn over a recursive parameterised ADT.
Fixture (examples/list_map_poly.ail.json):
- data List a = Nil | Cons a (List a)
- inc : (Int) -> Int = \x. x + 1
- map : forall a b. ((a) -> b, List<a>) -> List<b>
recursive, instantiated at (Int, Int)
- print_list : (List<Int>) -> Unit !{IO}, recursive
- main builds [1,2,3], maps inc, prints each: "2", "3", "4"
E2E test list_map_poly_inc_then_prints in crates/ail/tests/e2e.rs.
Bug fixed (crates/ailang-codegen/src/lib.rs, +30/-9):
synth_arg_type used Type::unit() as placeholder for ADT type
vars that the ctor's args couldn't pin (Nil for List<a>).
Inside Cons(Int, Nil), unify_for_subst then bound a=Int from
the head and collided with a=Unit from the tail. Replaced the
placeholder with a synth-only wildcard Type::Var{name:"$u"}
mirroring the checker's $m metavar convention; unify_for_subst
short-circuits on $u-prefixed arg-side vars (accept without
binding, let a sibling pin the var).
No schema or API change. No new variant. Tester's recursion
hypothesis was refuted by debugger via a non-recursive
Cons(7, Nil) repro before the fix landed.
Tests: 25/25 e2e (was 24). All Iter 12/13 regressions green.
cargo doc --no-deps zero warnings (workspace invariant from
13d/e/f preserved).
Three-agent flow (tester -> debugger, no implementer needed
since the fix was inside debugger's <50 LOC scope). Process
note in JOURNAL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third docwriter mission (combined). Pure rustdoc additions, no
API or behaviour change.
ailang-codegen (113 LOC of doc):
- Crate root: intra-doc links (emit_ir, lower_workspace,
CodegenError::MissingEntryMain) + precondition note (both
entry points assume type-checked input).
- /// on CodegenError + every variant, naming AST trigger.
- /// on emit_ir and lower_workspace (single vs multi-module
split, cross-linked).
ail CLI (49 LOC of doc):
- Module-level //! expanded from 5 lines to full subcommand
list with one-liners (11 subcommands verified against Cmd
enum), clang-on-PATH note, design-intent paragraph.
Verification: cargo doc --no-deps zero warnings (also under
RUSTDOCFLAGS='-D rustdoc::broken_intra_doc_links'); build green;
tests 64/64 + 3 ignored doctests green. Diff is 100% doc lines
(verified by filtering).
Findings (not fixed; orchestrator-deferred):
- CodegenError::Internal is one catch-all variant for ~30
invariant-violation sites; splitting would help test
ergonomics but is out of doc scope.
- emit_ir synthesises a Workspace with root_dir="."; harmless
today, surfaces if codegen ever reads root_dir.
Process note: my brief said the CLI had 9 subcommands; agent
found and documented 11. Useful counter-pressure on orchestrator
sloppiness — recorded in JOURNAL.
Workspace-wide rustdoc invariant (DESIGN.md item 6) now
load-bearing across all four crates. Docwriter shifts from
sweep to maintenance mode going forward.
Next: 14a (polymorphic list_map using Iter-13a parameterised
ADTs) is unblocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second docwriter mission. Pure rustdoc additions (no API or
behaviour change) across the typechecker crate:
- builtins.rs: module root expanded; EffectOpSig (struct + 3
fields) and install() got /// strings.
- diagnostic.rs: Severity (+ both variants), Diagnostic (+
severity/code/message fields) and the error/with_def/with_ctx
helpers got /// strings; super:: link rewritten to crate::.
- lib.rs: CheckError + every variant, to_diagnostic, CheckedModule
(+ symbols), check, Env (+ globals/effect_ops/types/
module_globals/current_module), CtorRef (+ type_name) got ///
strings; crate-root prose upgraded with intra-doc link to
check_module.
Verification: cargo doc --no-deps zero warnings; cargo build
--workspace green; cargo test --workspace 64/64 + 3 ignored
doctests green. Diff is 188 LOC, all in /// or //! lines (verified
by filtering).
Findings (not fixed; orchestrator-deferred):
- Env is pub but only privately constructable.
- CheckError::CtorArity and ::ArityMismatch share the public
diagnostic code "arity-mismatch" by design.
- Diagnostic / Severity reachable via two paths because the
diagnostic module is pub.
JOURNAL.md updated with the Iter 13e entry. 13f (ailang-codegen
+ ail) and 14a (List a rewrite) remain queued.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ailang-docwriter to /agents/ — a recurring role for keeping
crate-, module-, and pub-item-level rustdoc accurate. First mission:
ailang-core. Crate root, every module root, every pub item documented;
intra-doc links throughout; Iter-13a additions (TypeDef.vars, Type::Con.args)
get an explicit backwards-compat note. Two stale broken-link warnings in
ailang-check fixed in passing. cargo doc --no-deps now warning-free across
the workspace; promoted to verification invariant 6 in DESIGN.md.
DESIGN.md: removes "No parameterised ADTs" from the gap list,
adds an entry under "what is supported" that names the per-use-
site substitution scheme and the `llvm_type(Type::Var)` hard-error
defence. Smoke-test list extended with `box.ail.json` and
`maybe_int.ail.json`. Boundary snapshot moved from "end of Iter 12"
to "end of Iter 13".
JOURNAL.md: single Iter 13 entry covering 13a/b/c. Records the
hash-invariant regression test, the architect-flagged debt I
deliberately did not touch (poly-fn-as-value asymmetry, builtins
triple-source, `synth_arg_type` shortcuts on If/Match), the
KISS observation that 13b's "no mono-queue for types" was the
right call, and the process note that this was the first iter
worked strictly through `/agents/` after the role pin in 3df943d.
Plan iteration 14 queued: list_map-as-`List a` rewrite, GC/arena,
poly-fn-as-value.
Closes the gap between Iter 13a (parameterised ADTs in the
checker) and end-to-end execution. ADT ctor code stays inlined at
every use site — no mono-queue for types, no new symbols — but
LLVM field types are now derived per use site via substitution.
`CtorRef` gains `type_vars: Vec<String>` so use sites can identify
which fields reference rigid vars. `cref.fields` (precomputed
LLVM strings) is documented as monomorphic-only and read past for
parameterised ADTs.
`lower_ctor`: derives a `BTreeMap<String, Type>` substitution from
`synth_arg_type` of each arg via `unify_for_subst`, then maps every
`ail_field` through `apply_subst_to_type` + `llvm_type` for the
per-store types. Monomorphic ADTs hit the original fast path.
`lower_match`: builds `arm_subst` from `s_ail.args` ↔ `cref.type_vars`,
substitutes through each `cref.ail_fields[idx]`, and uses the
substituted type both for the LLVM `load` AND as the AILang slot
of the local — so a downstream `unbox(b)` sees `b: Int`, not
`b: a`.
`synth_arg_type` for `Term::Ctor`: returns concrete type-args
derived from the ctor's term-args. Vars left unpinned (e.g. `None`
for `Maybe a`) fall back to `Type::unit()`.
`llvm_type(Type::Var)` now hard-errors. Earlier this silently
fell through to `ptr` (the ADT-via-ptr fallback), producing
garbage IR. Failing loudly here surfaces missed substitutions in
the test suite.
Two e2e tests (`box.ail.json`, `maybe_int.ail.json`) cover ctor
lower with substituted field types and match-arm field
substitution. 64 tests green; clippy clean (two pre-existing
warnings untouched). Hash invariant holds.
Out of scope per agent assignment: poly-fn-as-value, higher-rank
polymorphism, DESIGN/JOURNAL updates (deferred to 13c).
Adds an explicit "My role: orchestrator" section so the discipline
doesn't drift across sessions. Three concrete contracts:
- What I do myself (planning, design, JOURNAL/DESIGN) vs. what gets
delegated (implementation, testing, debugging, architectural
drift review). Trivial mechanical edits stay inline.
- Authority over /agents/: I may add, edit, retire, or replace agent
definitions when orchestration needs change, with the change going
through git like any other code.
- When NOT to delegate: direct user questions, single judgement
calls, and cases where context is already loaded.
Triggered by a session where I quietly took over implementer work
on Iter 13a instead of routing it through ailang-implementer.
Adds type parameters to ADTs. `TypeDef` gets a `vars: Vec<String>` and
`Type::Con` gets an `args: Vec<Type>`, both `skip_serializing_if =
"Vec::is_empty"` so canonical-JSON hashes of every pre-13a definition
stay bit-identical (regression test in `hash::tests`).
Checker:
- `check_type_def` installs `td.vars` as rigid vars while validating
ctor field types — `Cons(a, List a)` now resolves both occurrences
of `a` and the recursive `List a` use.
- `check_type_well_formed` accepts `Type::Con { name, args }` only
when the type is in scope and `args.len()` matches its declared
arity; primitives stay zero-arg.
- `Term::Ctor` synth instantiates `td.vars` with fresh metavars, so
`MkBox(42)` synthesises to `Box<$m0>` and unifies field types
through the surrounding context.
- `type_check_pattern` substitutes the scrutinee's concrete type-args
through ctor field types, so a `MkBox(x)` arm against a `Box<Int>`
scrutinee binds `x : Int`.
- `check_fn` validates declared param/return types via
`check_type_well_formed` so arity mismatches on parameterised
ADTs surface before the body is checked.
`unify`, `occurs`, `Subst::apply`, `substitute_rigids`, and codegen's
`unify_for_subst` / `apply_subst_to_type` recurse into `args`.
Pretty-print:
- `type_to_string` renders `Box<Int>` for parameterised cons.
- `def_block`/`manifest` carry the `[a b ...]` vars list.
Three new check unit tests cover ctor instantiation at a concrete
arg, the polymorphic-`unbox` round trip at two distinct
instantiations, and arity mismatch on `Box<Int, Bool>`. All 62 tests
green; clippy clean (two pre-existing warnings untouched). Hashes
db33f57cb329935e (sum) and b082192bd0c99202 (IntList) verified
unchanged.
Codegen still synthesises `Type::Con` with `args: vec![]` from
`Term::Ctor` — full ADT monomorphisation lands in 13b.
DESIGN.md:
- "What is not (yet) supported" updated for end-of-Iter-12 boundary:
parameterised ADTs replace HM-inside-bodies as the headline gap;
polymorphism limitations (direct-call-only, no higher-rank) are
spelled out so future me doesn't trip over them.
- "What is supported" gains the polymorphism + monomorphisation
bullet, with the descriptor scheme written out.
- Smoke-test list extended with poly_id and poly_apply.
JOURNAL.md: Iter 12a/b retrospective. Notes the metavar-encoding
choice (Type::Var{name:"$m<n>"} vs new variant) and why the
codegen-side type tracker was preferable to a typechecker
sidetable for now. Architecture self-check confirms the language
is now usable for poly-flavoured programs over primitives. Plan 13:
parameterised ADTs as the natural next step (without them, generic
map remains hand-monomorphised).
Iter 12c was originally to include a polymorphic map rewrite —
dropped because parameterised ADTs are the prerequisite. The two
new examples (poly_id, poly_apply, both already in 12b) cover the
real e2e proof.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Polymorphic defs are now actually emitted: each unique instantiation
gets its own specialised LLVM fn, mangled @ail_<m>_<def>__<descriptor>.
The typechecker's Forall instantiations from Iter 12a now have a real
backend.
Strategy:
- Pass 1 of lower_workspace splits fn-typed defs into mono and poly:
module_user_fns keeps LLVM-typed FnSig only for monomorphic fns;
module_polymorphic_fns holds the full FnDef for poly defs;
module_def_ail_types is a unified AILang-type lookup for both.
- Direct calls to poly defs (current-module or qualified cross-module)
flow through lower_polymorphic_call: it derives the type
substitution from the actual arg AILang types, mangles the symbol,
and queues (def, subst) for specialisation.
- emit_module drains the queue after the regular defs. Each entry
produces a specialised FnDef with rigid vars substituted in both
the type and the body (incl. Lam param/ret annotations), then
emits via the existing emit_fn pipeline. The synthetic name embeds
the descriptor; standard mangling produces the right symbol.
Codegen-side type tracking:
- locals tuple grew from (name, ssa, llvm_type) to 4-tuple with
ail_type. Updated all 6 push sites: fn entry, Let, match-ctor field,
match-open-arm, lambda capture (cap_meta also extended), lambda
param.
- CtorRef gained ail_fields parallel to fields, used by match-arm
bindings to inherit AILang types.
- New synth_arg_type / synth_with_extras: a small recursive walker
that derives the AILang type of an expression in the current scope.
Used at poly call sites for arg-type inference. The `extras`
parameter shadows locals during Let recursion without &mut self.
Helpers added:
- derive_substitution(vars, params, arg_tys): unifies the param
shapes against the actual args, binding rigid vars; reports
unbound vars as an internal error.
- apply_subst_to_type / apply_subst_to_term: substitute rigid vars
throughout a Type or Term (the Term variant only matters for Lam,
the only Term arm carrying types).
- descriptor_for_subst / type_descriptor: stable identifier-safe
name suffix. `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT
`Foo → FFoo`, `Fn → Fn_<...>__r_<ret>`. So `id__I` for id at Int,
`apply__I_I` for apply at (Int, Int).
- builtin_ail_type / builtin_effect_op_ret: AILang types for the
builtin operators and effect ops, used by the codegen-side type
tracker.
Examples + tests:
- examples/poly_id.ail.json: id used at Int (42) and Bool (true).
Output: 42 / true. Two specialised fns + adapters + static
closures get emitted (visible in the IR).
- examples/poly_apply.ail.json: apply(succ, 41) == 42. Exercises
the harder case where one of the polymorphic params is itself
a function value — the closure-pair ABI survives substitution.
- crates/ail/tests/e2e.rs: polymorphic_id_at_int_and_bool and
polymorphic_apply_with_fn_param.
Tests: 58/58 (was 56/56). Hash invariant holds: sum.ail.json keeps
db33f57cb329935e / d9a916a0ed10a3d3.
Limitations (known, deferred):
- Polymorphic fns can only be DIRECTLY called. Passing a poly fn as
a value (let f = id in f(42)) fails — resolve_top_level_fn looks
in module_user_fns which doesn't include poly defs. Adding this
needs per-instantiation closure-pair globals.
- No higher-rank polymorphism: a polymorphic arg passed to another
polymorphic call (apply(id, 42)) trips the simple unify_for_subst
which doesn't recurse into Forall. Acceptable for the MVP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Polymorphism is now opt-in via Type::Forall at top-level def types.
Implementation is the textbook ML rule: peel the Forall when checking
the def body, instantiate fresh metavars at every use site, unify.
Mechanics:
- New Subst struct + unify(): standard occurs-check unification, with
effects compared as a set.
- Metavars encoded inside the existing Type::Var as Type::Var{name:
"$m<id>"}. The "$" prefix can't collide with source-level identifiers,
so the AST schema stays untouched and existing module hashes don't
shift (verified: sum.ail.json keeps db33f57cb329935e / d9a916a0ed10a3d3).
- check_fn peels an outer Forall and installs the rigid vars in
Env.rigid_vars, where check_type_well_formed accepts them. Inner
forall vars unify only with themselves.
- Term::Var lookup runs maybe_instantiate on the resolved type, so
polymorphic globals get fresh metavars at each occurrence. Two
use sites of the same `id` get independent ($m0)→$m0 and ($m1)→$m1
shapes — no bleed.
- All expect_eq calls inside synth replaced with unify; expect_eq is
still used for the App-arity / etc. structural checks where no
metavars can appear.
Constraints kept tight on purpose:
- Const types may not be Forall (rejected outright).
- Forall in ADT field positions still rejected.
- No higher-rank polymorphism: App's callee can be Forall (defensive
instantiate) but the body doesn't introduce new quantifiers.
- No let-generalisation of lambdas — all polymorphism is at top-level
defs only.
Codegen still rejects Forall types, so polymorphic programs typecheck
but don't yet compile. Iter 12b adds monomorphisation.
Tests: 14/14 (was 10/10) — added polymorphic_id_def_typechecks,
polymorphic_id_can_be_used_at_int_and_bool,
polymorphic_id_consistency_is_enforced, polymorphic_apply_with_two_vars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small cleanups, no semantic change. Workspace is now clippy-
clean (default lints):
- ailang-check: collapse `if-same-then-else` for primitive vs ADT
type lookup into one branch via a local boolean.
- ailang-codegen: iterate `all_strings.values()` directly instead of
`for (_mname, entries) in &all_strings`.
- ailang-codegen: drop `import_map` parameter from `collect_captures`
— was threaded speculatively through every recursive call but
never read.
- ailang-codegen: `s.len()` instead of `s.as_bytes().len()` in
c_byte_len (the warning predates Iter 6).
Tests: 52 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/sort.ail.json: insertion sort over IntList. Defines `insert
:: Int -> IntList -> IntList` and `sort :: IntList -> IntList`
recursively, plus print_list using Iter 10's seq. Sorts an 11-element
input and prints `1 1 2 3 3 4 5 5 5 6 9` one-per-line.
Validates that the language handles deeper ADT recursion + branching
(if + <=) + ctor construction + IO sequencing without surprises.
Wrote, typechecked, ran first try.
The Iter 11 plan called for polymorphism, but on reflection the right
move was one more validation cycle before disturbing the pipeline.
Polymorphism is now queued explicitly as Iter 12 (12a typechecker
substitution, 12b codegen monomorphisation, 12c docs + generic
example).
Tests: 52 green (was 51). New e2e `insertion_sort_orders_list`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `Term::Seq { lhs, rhs }` (serde tag "seq") as a first-class
AST node for sequencing effectful expressions. Equivalent in
behaviour to `let _ = lhs in rhs`, but the dedicated node gives the
pretty-printer and diagnostics a cleaner shape and surfaces the
intent ("run for effect, then yield rhs") to future tooling.
Typecheck: lhs must be Unit; rhs's type is the result; effects
accumulate.
Codegen: lower lhs (drop SSA), lower rhs (return).
Capture / deps walkers: recurse into both sides.
Refactored examples/list_map.ail.json's print_list to use seq
instead of `let _ = ...`. Output unchanged (2/4/6).
Hash stability: existing examples without Term::Seq serialise
bit-identical; only list_map.ail.json's hashes changed (deliberate
refactor).
Tests: 51 green (was 50). New unit test
`ailang_check::tests::seq_lhs_must_be_unit` covers the type-error
path; existing list_map e2e covers the happy path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESIGN.md:
- CLI block: add `ail run`.
- Smoke-test list: add list_map.ail.json (the dogfood example).
JOURNAL: append Iter 9 entry covering the dogfood result, two
friction points surfaced (single-arg fn-type pretty-print parens;
absence of a sequencing operator), the `ail run` CLI helper, and a
ranked Iter 10 plan with three candidates (polymorphism, `;`, GC).
Tentative pick is (2) `;` next, (1) polymorphism for Iter 11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iter 9a — dogfood. examples/list_map.ail.json exercises everything
Iter 1-8 shipped in one program: ADTs (IntList = Nil | Cons Int IntList),
recursive fns over the ADT (map_int, print_list), pattern matching with
nested Var fields, a closure (`\\x. x * 2`, no captures), fn-typed
parameters, IO effects propagating through recursion, and `let`-
sequencing of an effectful sub-expression inside a match arm.
Result: nothing broke. The full pipeline (typecheck → IR emit →
clang → run) produces "2\\n4\\n6\\n". The language is now sufficient
for "small but real" programs.
Iter 9b — `ail run`. Convenience subcommand that builds into a
tempdir and executes, propagating the binary's exit code. Equivalent
to `ail build && ./bin` in one step. Build logic factored out of
`Cmd::Build` into a shared `build_to` helper used by both Build and
Run.
Tests: 50 green (was 49). New e2e `list_map_doubles_then_prints`.
No test for `ail run` itself — the existing build_and_run helper in
e2e.rs already exercises the same build+exec sequence path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESIGN.md:
- Term schema: add `ctor`, `match`, `lam` rows. The schema fragment is
now exhaustive for the supported language; prior versions silently
dropped Iter 3/Iter 8 additions.
- "What is not (yet) supported": closures-with-capture moves out of
pending. New positive bullet for anonymous lambdas. Polymorphism
added as an explicit pending bullet (Forall is parseable but not
inferred). Smoke-test list extended with closure.ail.json.
JOURNAL: append Iter 8 entry covering both 8a (closure-pair ABI) and
8b (Term::Lam + capture). Includes the rationale for skipping TIR
(KISS won — `synth` already provides enough type info), the closure-
conversion sketch, hash stability check (existing examples produce
the same fn hashes pre vs post Iter 8), and the Iter 9 plan with
two candidates ranked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds anonymous functions to AILang. A lambda value is constructed by
malloc'ing an env struct that holds its captured locals, plus a
closure pair `{ thunk_ptr, env_ptr }` (Iter 8a's ABI). The lambda's
body lifts to a top-level thunk `@ail_<m>_<def>_lam<id>(ptr %env,
params...)` that unpacks captures back into named locals before
running.
AST: new Term::Lam { params, paramTypes, retType, effects, body }.
Serde tag is "lam"; existing modules (no Lam) serialize identically,
so all current hashes stay stable.
Pretty-printer: renders `(\\ (x: T ...) -> R . body)`.
Typecheck (ailang-check): a lambda's type is the declared Type::Fn.
Body's effect set must be a subset of declared effects (no row
polymorphism). Constructing a lambda is pure — only calling it picks
up the declared effects, via the existing App branch.
Codegen (ailang-codegen):
- collect_captures: free-var walk; excludes builtins, current-module
top-level fns, and qualified names.
- lower_lambda: per-fn lam counter, save/restore emitter state, emit
thunk into a deferred queue (flushed after the parent fn), pack env
+ closure pair on heap, register the resulting closure-pair SSA in
the sidetable so subsequent indirect calls find it.
- Capture sigs propagate: a fn-typed capture keeps its FnSig in the
thunk's sidetable so `f(args)` inside the body still indirect-calls.
- Env layout: 8 bytes per capture (typed load/store reads only the
needed bytes; padding wasted but uniform).
Tests: 49 green (was 48). New examples/closure.ail.json + e2e
`closure_captures_let_n` exercises `let n = 3 in apply(\\x. x + n, 39)`
end-to-end and asserts the binary prints 42.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every fn-value is now a `ptr` to a closure pair `{ ptr thunk, ptr env }`,
not a raw fn-pointer. Top-level fns get an auto-generated adapter
`@ail_<m>_<f>_adapter(ptr %_env, params...)` that ignores env and
forwards to the real fn, plus a static closure constant
`@ail_<m>_<f>_clos = { @adapter, null }`. References to a fn as a
value return the closure-pair address.
Indirect calls now GEP the thunk + env slots, load both, and call
`thunk(env, args...)`. Direct calls to statically-known callees stay
on the original fast path (no adapter).
This is the ABI groundwork for Iter 8b lambdas: a captured-env closure
will reuse the same value shape, just with a non-null env produced by
malloc.
Tests: 48 still green. IR snapshots refreshed (every fn now carries an
adapter + static closure pair). Iter 7's hof.ail.json prints 42
unchanged — `apply(inc, 41)` now hands `@ail_hof_inc_clos` to apply,
which unpacks and indirect-calls as designed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-level fn names are now usable as values, fn-typed parameters can
be called as `f(args)`. KISS slice of "closures + HOFs + TIR": no
TIR, no heap, no ABI shift — that bundle stays in Iter 8 where
closure-with-capture and lambdas land together.
Codegen:
- Type::Fn lowers to LLVM `ptr`; sig travels via a per-fn-body
`ssa_fn_sigs` sidetable on `Emitter`.
- Term::Var falls through to `resolve_top_level_fn` when the name is
not a local; emits `@ail_<m>_<def>` and registers the sig.
- Term::App splits: static callee (builtin / current-module / qualified)
keeps the existing direct path; otherwise lower the callee, look up
the sig, emit indirect `call <ret> (<param-tys>) %fn(args...)`.
- emit_fn registers fn-typed params in the sidetable on entry.
- If branches propagate a fn-pointer sig to their phi when both arms
match.
Typechecker: unchanged. It already accepted `synth(callee)` against
Type::Fn; the only blocker was codegen rejecting non-Var callees.
Tests: 48 green (was 47). New `examples/hof.ail.json` and e2e
`higher_order_apply_inc` exercise `apply(inc, 41) == 42` end-to-end.
DESIGN.md: "What is not (yet) supported" rewritten — closures-with-
capture and lambdas remain pending, first-class fn-refs added as
positive bullet. Smoke-test list extended with `hof.ail.json`.
JOURNAL.md: Iter 7 entry with rationale, scope choice, architecture
self-check, Iter 8 plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ail deps now filters builtins, fn params, and let/match-pattern
bindings. New value_names() in ailang-check::builtins is the single
source of truth shared with the typechecker install path. walk_term
threads a scope set; qualified `prefix.def` refs pass through.
- check_in_workspace returns Vec<CheckError>; check_workspace
accumulates body diagnostics across defs and modules. Pass-1
(top-level symbol table) and per-module type-def setup stay
fail-fast — corrupt env would taint later diagnostics.
- DESIGN.md "What the MVP is NOT" was lying (ADTs, strings landed
in Iter 2/3). Renamed to "What is not (yet) supported" and split
into "not yet" + supported/smoke-tested. JOURNAL Iter 6 entry
records the architecture self-check.
Tests: 47 green (was 44). +2 deps filter tests in e2e,
+1 multi-diagnose test in ailang-check workspace integration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Finishes the project-wide English convention:
- CLAUDE.md (the user opted in to translate it too).
- examples/hello.ail.json: "Hallo, AILang." -> "Hello, AILang.".
- E2E test assertion and IR snapshot for hello regenerated to match.
- DESIGN.md "Project language: English" section: the previous
CLAUDE.md exception is dropped.
After this commit, no German remains in any committed file
(grep -P '[äöüÄÖÜß]' is empty across .rs / .md / .json / .ll).
Verified: cargo test --workspace passes (44/44).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make English the project-wide language: all comments, string literals,
CLI help text, design docs, journal, agent prompts, and README. Only
CLAUDE.md (the user's own instruction file) stays German, and the live
conversation between user and Claude continues in German.
Adds a new "Project language: English" section to docs/DESIGN.md as
the durable convention. No logic changes — translation only. Four
internal error strings in the typechecker were retranslated; no
test asserts on their wording.
Verified: cargo test --workspace passes (44/44).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JOURNAL bekommt fehlenden Iter-5d-Eintrag und Iter-5-Architektur-
Review-Notiz mit Iter-6-Plan (Aufräumarbeiten). DESIGN.md korrigiert
zwei Drifts vom Architekt-Befund: @main ist i32 (C-/LLVM-ABI), nicht
i64; String-Globals tragen Hint @.str_<modul>_<hint>_<idx>.
Vier Tooling-Subkommandos können jetzt auf dem ganzen Workspace
arbeiten. Default bleibt single-modul (Rückwärtskompat). Manifest
listet alphabetisch nach (modul, name) mit module-Feld; describe
nimmt Punktnotation ws_lib.add an, ambig-Erkennung fallback;
deps gibt Cross-Module-Edges {from_module,from_def,to_module,to_def}
aus, Effekt-Ops separat; diff vergleicht workspace-übergreifend mit
added/removed/changed/unchanged_modules und nested Sub-Diff pro
changed_module. Helper diff_def_lists wird von Single- und
Workspace-Diff geteilt.
Symbol-Mangling-Schema einheitlich auf @ail_<modul>_<def> umgestellt
(auch für Single-Modul-Programme), String-Globals als
@.str_<modul>_<idx>. main bleibt LLVM-/C-ABI-Eintrittspunkt und ist
ein Trampoline auf @ail_<entry>_main. lower_workspace emittiert eine
einzige .ll für den ganzen Workspace, alphabetisch nach Modulname,
Cross-Module-Calls über Import-Map aufgelöst. ail build / ail emit-ir
laufen jetzt durch den Workspace-Pfad. IR-Snapshots regeneriert,
neuer ws_main-Snapshot. E2E-Test workspace_build_runs_imported_fn
prüft, dass das Binary die importierte Funktion korrekt aufruft.
Schuld #19 (source_filename) durch einheitliches <entry>.ail-Schema
geschlossen.
Neue API check_workspace(&Workspace) -> Vec<Diagnostic>; check_module
hebt das Modul intern in einen Trivial-Workspace. Term::Var mit genau
einem Punkt im Namen ist ein qualifizierter Verweis <prefix>.<def>,
aufgelöst über Import-Map (alias-oder-modulname → echter Modulname).
Drei neue Diagnostic-Codes: unknown-module, unknown-import,
invalid-def-name. ail check lädt jetzt immer via load_workspace;
Loader-Fehler werden im JSON-Modus zu strukturierten Diagnostics
(module-not-found, module-cycle, …). DESIGN.md und JOURNAL.md
dokumentieren die Konvention. Hash-Stabilität: alle ir_snapshot_*-
Tests bitidentisch grün, kein neuer AST-Knoten.
Architekt-Befund umgesetzt: Iter-4-Eintrag dokumentiert die drei
Sub-Commits (4a/4b/4c), den Test-Stand von 28, das Schließen der
Iter-2-Block-Tracking-Schuld und die zwei neuen Schulden
(single-shot check_module, hartkodiertes source_filename).
Iter-5-Plan steht: Modulsystem zuerst, Multi-Diagnose-Refactor danach.
Vier Snapshots in crates/ail/tests/snapshots/ (sum, max3, hello, list)
sichern den erzeugten LLVM-IR. Test-Helper normalisiert target triple
und trailing whitespace, sonst byte-für-byte-Vergleich. Update via
UPDATE_SNAPSHOTS=1 cargo test ir_snapshot_. Mismatch erzeugt eine
.actual-Datei mit dem aktuellen Output für Diff-Inspektion.
ail diff <a> <b> [--json] vergleicht zwei Module strukturell per
Def-Hash. Vier Kategorien (added/removed/changed/unchanged),
alphabetisch sortiert, deterministisches JSON-Schema. Exit 1 bei
Unterschieden, Exit 0 bei Identität — skript-tauglich. Kein
Typcheck-Zwang, damit man auch kaputte Module diffen kann. Helper
def_name/def_kind in ailang-core für stabile Def-Identität.
Neue Top-Level-API check_module(&Module) -> Vec<Diagnostic> in
ailang-check, plus stabile Codes (unbound-var, type-mismatch,
arity-mismatch, non-exhaustive-match, unknown-ctor-in-pattern,
duplicate-def, …). CLI bekommt --json-Flag für maschinenlesbares
Output, Exit 1 bei Errors. Text-Modus unverändert. E2E-Test
check_json_unbound_var sichert das Format ab.
Vier spezialisierte Agent-Definitionen (implementer, architect, tester,
debugger) im sichtbaren agents/-Verzeichnis. DESIGN.md erweitert um den
Abschnitt "Projekt-Ökosystem" — AILang ist Sprache + CLI + Examples +
Agents + Doku + Tests gleichermaßen. JOURNAL hält den Workflow-Wechsel
auf Orchestrator-Modus fest und ordnet Iteration 4 neu.
- AST: Def::Type mit Ctors; Term::Ctor (Konstruktion) und Term::Match
mit Arm/Pattern. Patterns: Wild, Var, Lit, Ctor { ctor, fields } —
Sub-Patterns im MVP auf Var/Wild beschränkt.
- Typchecker: Type-Registry, ctor_index für O(1)-Resolution, Pattern-
Bindings, Exhaustiveness-Check gegen volle Konstruktormenge plus
Negativ-Tests.
- Codegen: Boxed-Heap-Layout via malloc; Tag in Offset 0, Felder ab
Offset 8 in 8-Byte-Slots. Match lowert zu load tag + switch + Phi
am Join. Default-Block ist unreachable, wenn vom Typchecker geprüft.
- examples/list.ail.json: rekursive Int-Liste mit sum_list via match.
E2E-Test + Exhaustiveness-Tests. 19/19 Tests grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Codegen: current_block-Tracking ersetzt die Heuristik im phi-Lowering;
verschachtelte if-Ausdrücke produzieren jetzt korrekte LLVM IR.
examples/max3.ail.json + Test schützt gegen Regression.
- Strings: Lit::Str / Type Str / io/print_str Effekt-Op; Strings sind im
MVP immutable Konstanten. examples/hello.ail.json als zweiter E2E-Test.
- CLI: --json für manifest und builtins; neuer deps-Subcommand listet
statische Symbol-Referenzen pro Definition. Effekt-Ops mit Prefix
effect: markiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Erste lauffähige Iteration. examples/sum.ail.json wird zu nativem Binary
kompiliert und druckt 55 (Summe 1..10) als End-to-End-Test.
Architektur:
- ailang-core: hashbares JSON-AST + canonical-form + pretty-printer
- ailang-check: monomorpher HM-Subset + Effekt-Set-Tracking
- ailang-codegen: LLVM-IR-Text-Emitter (kein libllvm-link)
- ail: CLI mit check/manifest/render/describe/emit-ir/build/builtins
Designentscheidungen sind in docs/DESIGN.md dokumentiert; der Verlauf
in docs/JOURNAL.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>