Commit Graph

64 Commits

Author SHA1 Message Date
Brummel 1631f6065c Iter 13b: codegen for parameterised ADTs
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).
2026-05-07 14:45:35 +02:00
Brummel 1fd4763fad Iter 12b: codegen monomorphisation for polymorphic defs
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>
2026-05-07 14:10:51 +02:00
Brummel 1d24907f84 Iter 11: deeper dogfood — insertion sort
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>
2026-05-07 13:17:07 +02:00
Brummel c75517ac79 Iter 10: Term::Seq sequencing operator
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>
2026-05-07 13:14:28 +02:00
Brummel 849eca4fcf Iter 9a/9b: dogfood (list_map) + ail run
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>
2026-05-07 13:09:18 +02:00
Brummel ecde8fa7af Iter 8b: lambdas with capture (Term::Lam + closure conversion)
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>
2026-05-07 13:03:20 +02:00
Brummel c6c0a10788 Iter 7: first-class function references (no capture)
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>
2026-05-07 12:44:10 +02:00
Brummel efd209779c Translate remaining German content to English
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>
2026-05-07 12:20:18 +02:00
Brummel b2878fe655 Iter 5b: Cross-Module-Typcheck mit qualifizierten Verweisen
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.
2026-05-07 11:31:57 +02:00
Brummel 3451b5bd15 Iter 5a: Workspace-Loader mit Imports
ailang_core::Workspace + load_workspace folgt imports-Feld rekursiv
vom Eintrittsmodul. DFS mit Zyklus-Erkennung; Konvention: Modulname
== Dateiname (.ail.json). WorkspaceLoadError sammelt strukturiert:
Cycle, ModuleNotFound, ModuleNameMismatch, Schema, Io. Neues
ail workspace <entry> [--json] listet erreichbare Module mit
module_hash und Def-Count. Bestehende Subkommandos arbeiten weiter
pro Einzelmodul (Iter 5d).
2026-05-07 11:23:21 +02:00
Brummel 93fe7237e3 Iter 4a: ail check --json mit strukturierten Diagnostics
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.
2026-05-07 11:07:36 +02:00
Brummel 21606c9340 Iter 3: ADTs + Pattern Matching
- 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>
2026-05-07 10:38:07 +02:00
Brummel 6e6b6a14fb Iter 2: verschachteltes if, Strings, JSON-Output, deps
- 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>
2026-05-07 10:28:16 +02:00
Brummel 2fbcdba0b1 MVP: AILang-Sprache mit JSON-AST, Typchecker, LLVM-IR-Backend
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>
2026-05-07 10:18:32 +02:00