Commit Graph

15 Commits

Author SHA1 Message Date
Brummel 77b28ad64d iter form-a.1 (Tasks 1-5): additive phase + relocation
First half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All five tasks DONE; cargo
test --workspace green at every per-task boundary.

T1 — Add three new tests:
- parse_is_deterministic_over_every_ail_fixture (round_trip.rs)
- cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs)
- carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion)

T2 — Bulk-render the 99 missing examples/<stem>.ail files via
`ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json
carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved.
One re-loop triggered: load_workspace prefers .ail siblings since
ext-cli.1, so the newly-rendered imports broke seven Group-A entries
whose JSON entry-paths now resolved imports to fresh .ail. Repair:
pre-emptive forward-pull of five T3 migrations + 4 transient
#[ignore]s on workspace.rs mod tests (cleanly relocated in T5).

T3 — 14 Group-A test files migrated from ailang_core::load_workspace
to ailang_surface::load_workspace + .ail paths. Carve-out lines
preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs).

T4 — 12 Group-B test files: bulk regex flip on build_and_run /
build_and_run_with_alloc / build_and_run_with_rc_stats call sites
(~70 e2e.rs invocations + 11 subprocess sites). Four files
mis-classified Group-A as Group-B in plan recon (mono_hash_stability,
prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two
files mis-classified Group-B as Group-A (mono_recursive_fn,
mono_xmod_qualified_ref). Migrated per actual shape, not plan label.

T5 — Relocated #[cfg(test)] mod tests from production source to
integration test crates with ailang-surface dev-dependency:
- crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs)
- crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests
  from workspace.rs)
- crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from
  codegen/src/lib.rs:3717-3799)
- ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to
  .ail + ailang_surface::load_module
Carve-out tests in workspace.rs mod tests preserved in-place
(3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests
(3 sites) also preserved — they don't consume examples/.

Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new -
1 transit carve_out_inventory #[ignore] = 560 active).

Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4
additional raw-JSON-inspect handlers, .ail.json deletion, retiring
obsolete roundtrip tests + schema_coverage corpus flip, §C3
DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap
strike) ship in the next dispatch on this iter ID.

Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs
(borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical
/ ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form
smoke) need rewrite or #[ignore] before T8 deletion; recorded in
journal Concerns + Known debt sections.
2026-05-13 11:12:48 +02:00
Brummel 3286117605 iter 24.2: class Show + 4 primitive instances + 22b TShow migration
Ships class Show a where show : (a borrow) -> Str in the prelude
plus primitive Show Int / Bool / Str / Float instances. Each
instance body is a single-application lambda invoking the
corresponding runtime primitive (int_to_str, bool_to_str,
str_clone, float_to_str) — first prelude instance bodies to call
runtime primitives directly. Float is included in Show (unlike
Eq/Ord) because textual representation is well-defined modulo the
NaN-spelling caveat at DESIGN.md §Float semantics.

The 22b typeclass test corpus is migrated preemptively: 21 fixture
files and 6 consumer files (typeclass_22b{2,3}.rs +
hash.rs ct4 pin + workspace.rs iter22b1 tests + the
instance_present.prose.txt snapshot) rename class Show / show
to class TShow / tshow, analogous to the existing TEq/TOrd
convention. Migration runs before the prelude additions so the
workspace stays green throughout.

Three new tests pin the post-mono shape: show_mono_synthesis.rs
(existence of show__Int/Bool/Str/Float as Def::Fn entries in the
prelude post-mono module), show_dispatch_pin.rs (bare show and
tshow both Step-2 singletons workspace-globally),
mono_hash_stability.rs::primitive_show_mono_symbol_hashes_stay_bit_identical
(body hashes pinned for the 4 new mono symbols).

DESIGN.md §Prelude (built-in) classes drops Show from the
deferred-features list and appends a milestone-24 paragraph
naming the class signature + the 4 instances + the body shape.
print rewire stays deferred to iter 24.3.

Tests: 552 passed (was 548 + 4 new). bench/compile_check +
bench/cross_lang exit 0. bench/check exits 1 on the recurring
noise envelope per the conservative-call lineage.
2026-05-13 03:31:40 +02:00
Brummel 0eb33235eb iter mq.1: class-ref canonical-form extension + workspace-internal class-name qualification
Three schema fields move bare → canonical (bare for same-module,
<module>.<Class> for cross-module): InstanceDef.class,
Constraint.class, SuperclassRef.class. Symmetric to ct.1's
Type::Con.name rule. ClassDef.name stays bare.

validate_canonical_type_names gains three field-walks via new
check_class_ref helper + two sibling diagnostics
BareCrossModuleClassRef / BadCrossModuleClassRef.
check_class_name_fields narrowed to ClassDef.name-only.

Workspace-internal class-name keys all qualified:
class_def_module, class_by_name, registry entries.0,
ClassMethodEntry.class_name, class_superclasses, mono's
class_index, Origin::Class.class_name. New qualify_class_ref
helper at workspace.rs scope plus sibling
qualify_class_ref_in_check in ailang-check.

Two new positive on-disk fixtures (mq1_xmod_constraint_class{,_dep})
exercise the qualified Constraint.class shape.

Recon claim that all existing fixtures' class-refs are intra-module
was empirically wrong — 10 fixtures required minimal canonical-form
migration. Duplicate-instance test architecturally restructured:
post-mq.1 orphan-freedom makes two-modules-on-same-(class,type)
structurally impossible; new test_22b1_dup_same_module fixture
lands both instances in the class's module.

Plan defects fixed inline: Origin::Class.class_name single
construction site (plan recon off-by-one); build_class_index in
mono.rs needed qualifying; build_module_globals needed Def::Instance
carve-out; check_fn declared-constraint matching needed inline
canonical-form lifting; NoInstance Float-aware message arm needed
prelude.Eq/prelude.Ord match; coherence type-leg needed qualified-
head split-and-lookup. Tasks 3-6 ran as one coherent fix.

7/7 tasks, 520 tests green. bench/compile_check.py + cross_lang.py
exit 0; bench/check.py exit 1 (4 improvements-beyond-tolerance,
audit-ratifiable, not a regression).

MethodNameCollision + dispatch path unchanged; iters mq.2 + mq.3
land them.
2026-05-13 01:11:56 +02:00
Brummel 1d039b782e iter ct.4.2: pin canonical-form hashes for migrated fixtures + unmigrated sanity 2026-05-11 09:51:19 +02:00
Brummel b636b883a3 iter 22b.2.1: add Constraint struct, Type::Forall.constraints 2026-05-09 17:34:39 +02:00
Brummel eb4db9dafc iter 22b.1.2: workspace registry skeleton + coherence checks
Adds the workspace-global typeclass instance registry per Decision 11
"Resolution and monomorphisation". Built at the end of load_workspace
after the DFS over imports completes; keyed by (class-name, type-hash)
where type-hash uses the new canonical::type_hash (16-hex prefix,
parallel to def_hash and module_hash).

Three coherence checks fire from build_registry, each surfacing as a
distinct WorkspaceLoadError variant:

- OrphanInstance: `instance C T` not in C's or T's defining module.
- DuplicateInstance: two entries share the same (class, type-hash) key.
- MissingMethod: instance omits a required (non-default) method.

The CLI's workspace_error_to_diagnostic is extended with the three new
codes (orphan-instance / duplicate-instance / missing-method).

All existing Workspace { ... } construction sites get the new
`registry` field, defaulting to Registry::default() at synthetic /
test-only paths and threading through ws.registry.clone() at codepath
sites that already hold a real workspace.

Includes hash-stability regression tests (iter22b1_schema_extension_*
and iter22b1_classdef_empty_optionals_hash_stable) and the empty-
registry positive test against examples/sum.ail.json. Test count:
288 → 291 (3 new tests, all pass).
2026-05-09 12:37:00 +02:00
Brummel 50b68267fe check: mode-strict-because suppression (iter 19b)
Closes the 19a/19a.1/19b arc. Corpus signal from 19a.1 (5/65
fixtures fire over-strict-mode, all deliberate RC codegen-test
fixtures) justified shipping the suppress mechanism end-to-end.

Schema: Suppress { code, because } on FnDef. Pre-19b hashes
bit-identical via skip_serializing_if. Typechecker drops matching
diagnostics; empty 'because' is Error severity; wrong codes are
silent no-ops (open-set registry).

Form-A: (suppress (code "...") (because "...")) clause, parser
+ printer round-trip clean. Form-B: '// @suppress <code>: <reason>'
above the doc string, lossless contract metadata.

5 RC fixtures migrated (.ail.json + .ailx + .prose.txt). Corpus
signal: 5/65 -> 0/65. Lint still fires on any future fn that's
accidentally over-strict without an authored reason.

Test counts: ailang-check 55->61, ailang-core 26->28, ailang-surface
21->26, ailang-prose 49->52, e2e 70 unchanged.

Known debt: .ailx comment headers lost on regen (ail render's
contract excludes comments); parse_suppress_attr accepts
duplicate code/because attrs without diagnose (bounded by canonical
print order).
2026-05-08 19:36:04 +02:00
Brummel b9ca8894a5 Iter 18a: (borrow T) / (own T) mode annotations on fn signatures
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:

  Type::Fn { params, param_modes, ret, ret_mode, effects }
  enum ParamMode { Implicit, Own, Borrow }

This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).

Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.

Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.

New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.

Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.

Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
2026-05-08 02:07:07 +02:00
Brummel 41d406bcbb Iter 14g: Term::If restored (revert of 14d)
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>
2026-05-07 17:45:33 +02:00
Brummel d64031c234 Iter 14e: explicit, verified tail calls
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>
2026-05-07 17:16:28 +02:00
Brummel 8d97a924de Iter 14d: remove Term::If as a redundancy
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>
2026-05-07 16:56:02 +02:00
Brummel c90926dbba Iter 13d: rustdoc polish for ailang-core + new docwriter agent
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.
2026-05-07 15:02:35 +02:00
Brummel 078262271a Iter 13a: parameterised ADT schema + checker
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.
2026-05-07 14:25:58 +02:00
Brummel 7577ab8a90 Translate project content to English
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>
2026-05-07 12:17:48 +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