Commit Graph

72 Commits

Author SHA1 Message Date
Brummel e68d927e5a design-md-consolidation 4.4 fixup: drop 'Sharpened later the same day' temporal anchor + supplementary 'see closure conversion in JOURNAL' pointer 2026-05-10 13:10:45 +02:00
Brummel 49fc6738ba design-md-consolidation 4.4: cross-ref audit — JOURNAL queue → roadmap.md; bench pointers retained as binding-evidence anchors 2026-05-10 13:09:16 +02:00
Brummel 1605be75cb design-md-consolidation 4.3: drop 'Recently lifted gates' history paragraph from §What-is-not-yet-supported 2026-05-10 13:07:38 +02:00
Brummel 127a34a21b design-md-consolidation 4.2: strip ailang-docwriter agent workflow detail from Verification section; rule survives 2026-05-10 13:06:10 +02:00
Brummel 96270ced35 design-md-consolidation 4.1: fix Project-ecosystem stale agents/ path; expand Docs bullet to name roadmap.md + specs/ + plans/ 2026-05-10 13:04:03 +02:00
Brummel 934a6e18e6 design-md-consolidation 3.4 fixup: align DESIGN.md ClassDef/InstanceDef JSON tags with ast.rs lowercase serde rename 2026-05-10 12:53:36 +02:00
Brummel ac16100319 design-md-consolidation 3.2: invert §Data-model SoT — DESIGN.md canonical, ast.rs projection, drift test enforces 2026-05-10 12:46:15 +02:00
Brummel a100389c2f design-md-consolidation 3.1: remove 2 Rust code blocks from Decision 10 (Type::Fn + Suppress) — schema lives in §Data-model 2026-05-10 12:43:49 +02:00
Brummel 4f47dbfeb1 design-md-consolidation 2.5: future-iter speculations — remove aspirations, tighten binding prohibitions 2026-05-10 12:34:21 +02:00
Brummel 774886bb37 design-md-consolidation 2.4: condense Decision 11 mono-vs-vdisp correction history to state-only rationale 2026-05-10 12:31:37 +02:00
Brummel 38d357d58a design-md-consolidation 2.3: delete Decision 10 §Migration plan (7-point completed-iter list) 2026-05-10 12:29:54 +02:00
Brummel 2539b0e728 design-md-consolidation 2.2: condense Decision 9 narrative-of-changes opener + drop pre-Decision-9 history paragraphs 2026-05-10 12:27:51 +02:00
Brummel 86fd5dec0b design-md-consolidation 2.1 fixup: replace orphaned Decision 7 cross-reference at DESIGN.md:1141-1144 2026-05-10 12:25:26 +02:00
Brummel a3406394c2 design-md-consolidation 2.1: delete Decision 7 (Term::If REVERTED block) 2026-05-10 12:23:49 +02:00
Brummel 172b974c9a design-md-consolidation 1.5 fixup: untangle line 863 run-on, deduplicate JOURNAL attribution, idiomatic 'larger still' 2026-05-10 12:14:56 +02:00
Brummel 44fe4384fc design-md-consolidation 1.5: condense historical bench data-points in DESIGN.md 2026-05-10 12:11:40 +02:00
Brummel 152e4efa28 design-md-consolidation 1.4 nit: deduplicate 'fixture' word in user-class parenthetical 2026-05-10 12:09:36 +02:00
Brummel 613d4d8c0f design-md-consolidation 1.4 fixup: strip bare iter-id references (~16 sites) 2026-05-10 12:07:22 +02:00
Brummel 008b7924e3 design-md-consolidation 1.4 fixup: strip non-Iter-N iter anchors (14b sketch, pre-19b, pre-22a) 2026-05-10 12:00:03 +02:00
Brummel ba94d7240f design-md-consolidation 1.4: drop Iter-N tags from DESIGN.md 2026-05-10 11:58:22 +02:00
Brummel 18fc014377 design-md-consolidation 1.3 fixup: repair two date-strip quality issues 2026-05-10 11:48:39 +02:00
Brummel 4b10ee4a5f design-md-consolidation 1.3: drop date anchors from DESIGN.md 2026-05-10 11:46:14 +02:00
Brummel ef8c983720 design-md-consolidation 1.2 fixup: trim trailing whitespace on DESIGN.md:33 2026-05-10 11:43:41 +02:00
Brummel e6821d873d design-md-consolidation 1.2: drop Family-N tags from DESIGN.md 2026-05-10 11:42:07 +02:00
Brummel f2c4b400a8 design-md-consolidation 1.1: drop **Status:** markers from DESIGN.md 2026-05-10 11:40:03 +02:00
Brummel 4cacfcbdac bench: mono-vs-vdisp micro-benchmark + revised Decision 11 rationale
Hypothesis-driven measurement of "did monomorphisation actually
buy us performance?" on a 100M-iter LCG hot loop, AILang mono'd
code vs. four C reference variants (direct-inlinable, direct-
noinline, indirect-monomorphic, indirect-polymorphic). Zen 3,
clang -O2, median-of-15.

Headline: H1 supported, but the mechanism is inlining, not
dispatch shape. AILang mono = hand-C direct (1.000x). Indirect-
monomorphic = direct-noinline (1.000x) — saturating branch
predictor makes the indirect-call cost vanish on this hardware.
Inlining is the actual 3.31x win; polymorphic indirect adds
another 21% predictor-miss penalty.

DESIGN.md Decision 11 gains a rationale paragraph reframing mono
as inlining-enabler rather than indirect-call-eliminator, with
explicit pointer to the bench. JOURNAL entry records the full
methodology, ratios, limitations, and the side-effect mono-pass
env.globals-seeding bug surfaced while building the AILang fixture
(separate RED-first debug iter to follow).
2026-05-10 01:03:21 +02:00
Brummel e6fe2dfd81 iter 22-tidy.1: DESIGN.md — reconcile Decision 11 with milestone-22 outcome (no Prelude, '__' separator) 2026-05-09 22:51:17 +02:00
Brummel 0709e319ab iter 22b.4a.6: spec + DESIGN amendments — 22b.4 split, terminology fix, '__' separator, forall-constraints gap 2026-05-09 22:28:17 +02:00
Brummel f6cb900082 design: 22a — Decision 11 typeclasses (Haskell-lite, monomorphised, coherent) 2026-05-09 12:16:48 +02:00
Brummel 338a4cd3fe docs: codify feature-acceptance criterion (LLM-author utility)
Adds DESIGN.md "Feature-acceptance criterion" as a top-level section:
a feature ships only if (1) an LLM author naturally produces code
that uses it, and (2) it measurably improves correctness or removes
redundancy. Aesthetic appeal and human ergonomics do not count.

Mirrored in CLAUDE.md as "Feature acceptance: LLM utility", paired
with the existing "Design rationale != implementation effort". The
two together narrow valid feature rationales to one thing: what the
LLM author gets out of the feature.

Trigger: the typeclass-design conversation around 22a. Rule was
implicit in many past decisions (Decision 1's JSON-over-text choice,
Decision 10's "what LLMs are good at" reasoning) but never stated
as a feature-gate. Codifying it now means future feature proposals
get evaluated against an articulated criterion instead of being
re-derived each time.

Documentation-only; no Rust, schema, or bench changes. Test state
288/0/3 unchanged.
2026-05-09 10:59:28 +02:00
Brummel aea3758742 tidy: 21'g — resolve drift after 21'-arc close
Three drift items from ailang-architect, plus one false-positive
surfaced during verification:

  1. DESIGN.md silent on closure-pair 4.14x finding (21'b).
     Decision-10 ratified: "Workload scope of the 1.3x target"
     paragraph scopes the retirement gate to linear/tree/poly-ADT
     workloads; closure-pair carve-out documented as
     representational cost (closure cell + env struct = 2 allocs
     per step) until a slab/pool answer ships.

  2. bench/compile_check.py corpus drift. Three fixtures added
     (bench_compute_intsum, bench_compute_collatz,
     bench_list_sum_explicit), re-baselined. Now 12 fixtures x
     2 ops = 24 compile-time metrics.

  3. baseline.json convention not codified. Note field gains
     "max-of-distribution gets wider band than percentile"
     convention discovered in 21'd.

  4. (verification finding) bench_compute_intsum cross_lang
     tolerances at 15%/12% fire on subprocess-spawn jitter for
     sub-millisecond fixtures. Widened to 35% across all five
     intsum metrics; convention recorded in baseline_cross_lang
     note field (sub-ms fixtures need looser bands).

All three bench gates re-run sequentially after edits:
  bench/check.py        — 63 metrics; 63 stable
  bench/compile_check.py — 24 metrics; 24 stable
  bench/cross_lang.py    — 25 metrics; 25 stable

Total: 112 metrics under regression coverage, all green.

288 tests passing, 3 ignored. No Rust changes.
2026-05-09 01:40:36 +02:00
Brummel 7e5c95f6c6 runtime: half-retirement of Boehm — flip CLI default to --alloc=rc
The dual-allocator policy from 2026-05-08 made Boehm the CLI default
"for general workloads", with rc selected explicitly for real-time-
sensitive code. That asymmetry contradicted Decision 10: rc + uniqueness
inference is the canonical memory model AILang's typechecker enforces,
and the CLI was teaching the opposite mental model to users and prompt-
fed LLMs.

This iter flips the asymmetry without removing Boehm:

  - main.rs: default_value gc -> rc for build/run; help text rewritten
    so rc is the canonical path and gc is the parity oracle.
  - DESIGN.md Decision 9 retitled "RC canonical, Boehm parity oracle"
    and rewritten end-to-end. Migration plan step 6 updated to a
    two-step retirement (default-flip done, full removal gated).
  - DESIGN.md pipeline diagram: rc-first, gc-as-oracle.
  - JOURNAL: 2026-05-09 entry recording the call, what shipped,
    the bug the flip surfaced, the gating condition for full
    Boehm removal.

Bug surfaced by the flip and fixed in the same iter:
codegen/drop.rs build_pair_drop_fn emitted `getelementptr inbounds
{{ ptr, ptr }}, ...` via push_str (not format!), so the doubled
braces leaked verbatim into the IR. LLVM parsed it as a struct-of-
struct, the GEP referenced a non-existent field, and clang failed.
Invisible under the old gc default (no per-type drop fns) and
invisible to the 8 explicit _with_alloc("rc") tests (none of them
escaped a closure-with-captures). Caught immediately by two corpus
tests once rc became the baseline: closure_captures_let_n,
local_rec_as_value_capture_demo. Fix: single braces. Tests stay
as the regression guard.

The episode is the canonical justification for keeping the GC arm
as a parity oracle for now — a months-old codegen bug found by
flipping the baseline column.

288 passed / 0 failed / 3 ignored, unchanged.
2026-05-09 00:14:56 +02:00
Brummel c79f7e2f00 design: 20-family tidy-iter — refresh Data model + ecosystem inventory
Architect drift review after Family 20 surfaced four items; (1) and
(2) are doc drift fixed here, (3) and (4) are recorded as acceptable.

DESIGN.md changes:

  - Data model section rewritten from "Data model (MVP)" claiming
    only fn/const exist to "Data model" reflecting all three Def
    kinds. Added the missing schema fields the architect named:
    `tail` flag on app/do, the `seq` / `clone` / `reuse-as` / `letrec`
    terms, `paramModes` / `retMode` on Type::Fn, `suppress` on FnDef,
    `drop-iterative` on TypeDef, `args` on Type::Con, `vars` on
    TypeDef. Each additive field's hash-stable serialisation is
    called out once at the top so individual mentions stay terse.
    `Suppress`, `ParamMode`, `Literal`, `Pattern` get their own
    sub-blocks.
  - Pipeline diagram now shows `--alloc=gc` (links libgc;
    transitional) and `--alloc=rc` (emits ailang_rc_inc / _dec;
    canonical) as two backends sharing the same MIR.
  - Ecosystem inventory gained a "Surface forms" bullet for
    ailang-surface (Iter 14c, lossless Form-A printer/parser) and
    ailang-prose (Family 20, lossy Form-B projection). CLI bullet
    now lists parse, render, prose, merge-prose.

JOURNAL entry records the architect findings, the resolutions for
items (1) and (2), and the acceptable-drift status of (3)
FnDef::synthetic factor-out (defer to next schema-additive
FnDef field) and (4) linearity.rs spurious warning (not
corpus-triggered).

Pure documentation iter — no code changes. 288 workspace tests
unchanged and green.
2026-05-08 23:47:48 +02:00
Brummel 72626fa94f ail: embed Form-A spec in merge-prose prompt (iter 20f)
Closes a design hole shipped in 20d: the merge-prose prompt instructed
the LLM to emit JSON-AST and gave a 12-line schema-essentials reminder.
JSON-AST is the canonical hashable artefact, not a writing surface; the
reminder was not a language spec. Foreign LLMs had no realistic shot.

20f makes two coupled changes:

1) The LLM now emits Form-A (the canonical authoring surface fixed by
   Decision 6). merge-prose loads the original via ailang_core::load_module
   and re-renders via ailang_surface::print before embedding (round-trip
   is a gating contract on the surface crate, so this is lossless). The
   user runs `ail parse foo.new.ailx` to recover JSON, then `ail check`.

2) crates/ailang-core/specs/form_a.md is the complete LLM-targeted
   Form-A specification — grammar, every term/pattern/type/def keyword,
   schema invariants, pitfall catalogue, four few-shot modules from
   examples/*.ailx. Exported as ailang_core::FORM_A_SPEC via include_str!
   and embedded verbatim in every merge-prose prompt.

Drift detection in crates/ailang-core/tests/spec_drift.rs: every variant
of Term, Pattern, Type, Def, Literal is reached via exhaustive `match`.
The arms are not the assertion — adding a new variant without updating
the match is a compile error before the test runs. Once matched, an
anchor string is asserted to appear in FORM_A_SPEC. 8 tests, all green.

The hand-written + mechanical-drift-test combo addresses the user's
"distance to code is too big" concern about a docs-only spec. Generator
overkill rejected: structural drift is mechanically caught, but prose
explanation, schema-invariant catalogue, pitfall list, and few-shot
corpus cannot be emitted from AST shape alone.

Tests:
  - ailang-core: +8 spec_drift tests; existing 12 unchanged
  - ail unit (3): rewritten in lockstep — assert (own T)/(effects IO)
    landmarks, FORM-A SPECIFICATION header, FORM_A_SPEC body verbatim
  - ail e2e merge_prose_prints_framed_prompt: rewritten — assert
    `(module foo` + `FORM-A SPECIFICATION` instead of `ailang/v0`
  - Workspace: all green

Richer integration paths (LLM tool-use, MCP server, LSP) were named
in the design discussion and deferred per "kiss". All three layer
additively on the static-prompt path; static prompt remains the
lowest-common-denominator fallback.
2026-05-08 23:31:27 +02:00
Brummel bfe49084de design: ratify 19a-arc in DESIGN.md (tidy)
Per CLAUDE.md tidy-iter at family boundaries. ailang-architect
flagged the canonical 'DESIGN.md silent' finding for the entire
19a-arc — same shape as family-20's 20e tidy.

Decision 10 schema-additions block gained 'Iter 19b —
FnDef.suppress' entry. New subsection 'Advisory diagnostics —
Iter 19a-arc' documents the over-strict-mode lint rule (incl.
heap-type filter rationale), Severity::Warning introduction, CLI
exit-on-Error gating, and the suppress mechanism. New subsection
'Why advisory + suppress instead of inference' pins the three
substantive reasons (annotation-states-intent, drift-bremse,
LLM-authoring forcing function) so future iters can't reopen the
decision without engaging the recorded rationale.

Migration plan extended with step 7 covering 19a/19a.1/19b.
Decision 10's mandatory-annotation rule is explicitly reaffirmed
as unchanged.

No code changes. Two architect findings deferred (codegen FnDef
fan-out, snapshot-fixture coupling). Data model (MVP) drift
predates 19b — separate iter.
2026-05-08 19:41:43 +02:00
Brummel 2852326dd3 design: ratify form (B) prose projection (iter 20e tidy)
Family-20 tidy-iter: ailang-architect drift review surfaced
DESIGN.md silence on the new prose surface as the highest-leverage
gap. Decision 6 now has a 'Form (B) — human prose projection'
subsection naming the lossy-vs-lossless contract, the no-parser-
by-design choice, and the LLM-mediated round-trip. CLI block
gained 'ail prose' and 'ail merge-prose'.

No code changes. Form (B) does not weaken any Decision 6
invariant: JSON-AST remains the only hashable artefact, form (A)
the canonical authoring surface, 30-production grammar unchanged.

Two architect findings deferred (prompt-template lockstep test;
snapshot-fixture cross-family coupling). Family 20 closes.
JOURNAL queue: empty.
2026-05-08 18:27:34 +02:00
Brummel f10a77e8e5 DESIGN: re-frame Decision 9 as dual-allocator (not transitional)
The 18-arc validated the RC pipeline end-to-end; the orchestrator's
call (JOURNAL 2026-05-08) was to keep Boehm as default and RC as
the ready alternative rather than retire Boehm. Decision 9's
'transitional Boehm' framing was wrong shape for that policy —
this edit re-casts it as 'dual allocator: Boehm GC for general
workloads, RC for real-time-sensitive workloads', preserving the
rest of the section.

Decision 10 holds as the specification of the RC alternative; this
edit only changes the framing of how the two allocators relate.
2026-05-08 15:16:05 +02:00
Brummel 3113258680 tidy: 18g sub-arc — DESIGN ratification, latency bench, negative coverage
Resolves the architect's drift report on the 18g sub-arc:

- DESIGN.md: ratify mode-metadata's codegen role. param_modes /
  ret_mode were already on Type::Fn since 18a; with 18d.4 / 18g
  they became load-bearing for drop-emission decisions in
  codegen. The new 'Mode metadata is load-bearing for codegen'
  subsection records the four seams (Iter A + Iter B + 18g.1 +
  18g.2) and names the let-alias-of-borrow carve-out the gates
  do not propagate through.

- bench/run.sh: post-throughput, invoke bench/latency_harness.py
  on the three canonical arms (Implicit @ gc, explicit @ rc,
  Implicit @ rc control). The Boehm-retirement bench numbers
  are now reproducible by anyone running the harness, not just
  by hand on a specific host.

- Negative coverage: examples/rc_let_implicit_returning_app
  + alloc_rc_let_binder_for_implicit_returning_app_does_not_drop
  pin the asymmetry to the (own)-ret-mode test (live=0 there,
  live=1 here). The Borrow-ret-mode case is covered by the
  language design itself — typechecker rejects 'borrow-
  passthrough' shapes with consume-while-borrowed.

JOURNAL entry records four items as deferred known debt:
emit_inlined_partial_drop dynamic-tag fallback, carve-out
diagnostics surface, bench-number stat-of-N, and the
cross-family ordering observation about CLAUDE.md's tidy-iter
rule.

The 18-arc is formally closed with this tidy. Next iter is
the orchestrator's Boehm-retirement decision (Path A vs Path B
from JOURNAL 2026-05-08 18f entry, joined by the post-18g.2
re-bench numbers).
2026-05-08 14:37:36 +02:00
Brummel 8f40e8c31f DESIGN: substantive rationale for Term::ReuseAs wrapper form
The schema sketch in Decision 10 already committed to
Term::ReuseAs { source, body } as a wrapper, but did not record
WHY the wrapper form was chosen over a reuse_from: Option<String>
modifier on Term::Ctor. Adds the two substantive reasons:

- Compositional flexibility: wrapper generalises to record
  literals, opaque box wrappers, capability cells — anything
  future iters might add as an allocating construct. Modifier
  would replicate per Term variant.

- Source-locality: (reuse-as SRC NEW-CTOR) reads as a sentence
  with the source-binder at the head. Modifier scatters intent.

The trade-off accepted: schema permits Term::ReuseAs around a
non-allocating body. Caught at typecheck via
reuse-as-non-allocating-body diagnostic. Same shape as
Decision 7's Term::If composability accepts.

Recording before 18d.1 ships — per CLAUDE.md "design rationale
ne implementation effort", a schema choice has to name its
substantive reasons before code is written, not retroactively.
2026-05-08 10:40:35 +02:00
Brummel a91c3ffd94 CLAUDE/DESIGN/JOURNAL: design rationale ≠ implementation effort
User correction during the post-RC-overnight check-in: the 18a
"Type::Fn metadata vs. new Type variant" call was justified
across DESIGN.md, JOURNAL, and the commit message primarily by
"avoids ~250 match-arm sites". That is an observation about the
current state of the code, not a design rationale.

CLAUDE.md gains two binding rules:

- Design rationale ≠ implementation effort. Effort is at most a
  tiebreaker; a choice whose only stated reason is effort is
  suspect. The rule names the 18a misstep as the canonical
  anti-example so future sessions catch it earlier.
- Direction freedom + bounce-back conditions inlined (was
  previously a cross-reference to a private auto-memory file
  outside the repo, which the user couldn't see).

DESIGN.md Decision 10's Schema-additions block now leads with
the substantive reasons for per-position metadata: semantic
locality (modes belong to fn-parameter positions, not to types
in general — Decision 1 line); compositional clarity (type
identity vs. calling convention factor apart); future-proofing
(per-position metadata generalises; Type-variant approach
combinatoric blows up). The match-arm count remains parenthetical,
explicitly named a tiebreaker.

JOURNAL records the correction itself — the mistake stays as a
data point because how design discipline corrupts is informative.
2026-05-08 09:29:33 +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 52b129f558 DESIGN: clarify Iter 18a schema — modes are Type::Fn metadata, not Type variants
Avoids the ~190 Type match-arms in the typechecker that a new
Type variant would require. ParamMode enum (Implicit | Own |
Borrow) attaches per-param to Type::Fn, with serde defaults so
existing fixture hashes stay bit-identical.
2026-05-08 01:55:15 +02:00
Brummel e3ecf4f0cd DESIGN/JOURNAL: Decision 10 reframed — RC + LLM-author annotations
Replaces the earlier "RC + inference, no annotations" position
with a two-layer architecture: inference (intra-fn) + mandatory
LLM-author mode annotations on fn signatures (inter-fn). Adds
structured rationale for rejecting region inference (regions
assume stack-shaped lifetimes; real programs need
non-stack-shaped lifetimes for caches / memo tables) and linear
types as primary mechanism.

Schema additions enumerated for the 18-series:
  Type::Borrow / Type::Own (Iter 18a)
  Term::Clone (Iter 18c)
  Term::ReuseAs (Iter 18d)
  TypeDef.drop_iterative (Iter 18e)

Migration plan extended from 18a-d (4 iters) to 18a-f (6 iters).

JOURNAL records the regions excursion, the scope-shape reduction,
and the LLM-author lever as the structural advantage Decision 10
cashes in on.
2026-05-08 01:52:40 +02:00
Brummel aee6e9d3bf Iter 17a: per-fn arena via alloca for non-escaping allocations
Adds a conservative escape analysis that flags Term::Ctor and
Term::Lam allocations as non-escaping when they are bound by a
let, never flow to a fn arg / ctor field / tail-return / lambda
capture, and remain inside the allocating fn's frame. Codegen
lowers flagged sites to LLVM `alloca` instead of `@GC_malloc`;
escaping sites continue to use Boehm.

- escape.rs (new): name-based taint propagation; let-only
  candidates; tail-position / app-arg / ctor-field / lam-capture
  all taint.
- codegen: three sites switched (lower_ctor, lam env, closure
  pair). Save/restore non_escape across lambda thunk emit.
- examples/escape_local_demo.{ailx,ail.json}: focused fixture
  exercising both branches (peek and recursive count).
- e2e + escape unit tests: 133 → 141 (+8).

Stop-gate: 17a closes the queue. Memory-management discussion
(GC scope) is the next user-driven step. Per-fn arena observations
appended to JOURNAL — including the headline finding that 0/270
shipped allocations are flagged non-escaping under this rule
(real AILang threads ctors directly between fns; build-locally /
consume-locally is not idiomatic).

All existing fixture stdouts and content hashes bit-identical;
IR snapshots unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:31:35 +02:00
Brummel 937782e36d Iter 16e: == polymorphic over Int / Bool / Str / Unit
Lifts the Int-only restriction on `==`. Declared type becomes
forall a. Fn(a, a) -> Bool; codegen monomorphises and dispatches:

  Int  → icmp eq i64
  Bool → icmp eq i1
  Str  → call @strcmp + icmp eq i32 0
  Unit → constant true (operands still emitted for side effects)
  ADT / Fn / other → CodegenError::Internal

This unblocks 16c's build_eq for non-Int lit patterns. == joins
__unreachable__ as the second polymorphic builtin (same Forall
machinery).

- check/builtins.rs: == registered as Forall(a, Fn(a, a) -> Bool).
- codegen: lower_eq dispatch table; @strcmp declared in IR header
  alongside @printf/@GC_malloc/@puts.
- examples/eq_demo.{ailx,ail.json}: covers all four supported
  scalars including a Str-lit-pattern match.
- IR snapshots refreshed: only +declare i32 @strcmp(ptr, ptr) in
  the header; every define body bit-identical.
- e2e + check + codegen tests: 124 → 133 (+9, of which +1 is the
  e2e fixture and the rest exercise the new dispatch / typecheck
  surface).

Other comparison ops (<, <=, >, >=, !=) remain Int-only — out of
scope for this iter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:12:24 +02:00
Brummel af35612c1d Iter 16d: chain-terminator via __unreachable__ builtin
Eliminates the Unit-typed chain default that 16c left in place.
Adds __unreachable__ : forall a. a — codegen lowers to LLVM
unreachable. The 16a/16c chain machinery now uses it as the
deepest fall-through, so exhaustive matches with non-Unit arms
no longer need a (case _ ...) workaround.

- check/builtins.rs: register __unreachable__ as Forall(a, a).
- codegen: Term::Var "__unreachable__" emits LLVM unreachable
  and sets block_terminated; If/Match/fn-body gate downstream
  work on that flag.
- desugar: chain default switched from Lit{Unit} to Var.
- examples/lit_pat: categorize_first's trailing _ arm removed.
  Hash changes from c4faec3abc2ed388 to 644de0c0ec15fc17.
- examples/unreachable_demo: positive fixture using safe_div
  with __unreachable__ in the impossible branch.
- e2e + desugar tests: 122 → 124 (+2).

Path-a from 16b.2 planning: real primitive over a desugar-time
exhaustiveness pre-check (which would need cross-module type
registry access from desugar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:00:23 +02:00
Brummel ca30606aec Iter 16b.3: post-typecheck lift for LetRec with Let-bound captures
Adds path-2 from the 16b.2 planning entry. Desugar now defers
LetRecs whose captures include Term::Let-bound names; a new
ailang-check::lift_letrecs pass runs after typecheck and uses the
elaborated env to resolve capture types. ailang-check learns a real
Term::LetRec typing rule in synth and verify_tail_positions.

- desugar: Term::LetRec arm gains a defer-arm; helpers promoted to
  pub for reuse by the lift pass; find_non_callee_use moved before
  classification.
- ailang-check::synth/verify_tail_positions: real LetRec rules
  (effect-subset, locals install, recursive name in body+in_term).
- ailang-check::lift.rs (new, 720 LOC): post-typecheck lift with
  post-order traversal, env-walk for capture-type resolution,
  fast-path skip when no LetRec is present.
- ail::main.rs: build path now does load → check → desugar →
  lift_letrecs → codegen.
- examples/local_rec_let_capture.{ailx,ail.json}: new fixture
  capturing a let-bound `threshold` in a recursive helper.
- e2e + check unit tests: 106 → 110 (+4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:07:05 +02:00
Brummel 4683af6114 Iter 16c — literal patterns via desugar
Desugar `Pattern::Lit` (top-level and nested in Ctor) to
`Term::If { cond = (== sv lit), then = body, else_ = fall_k }`.
After 16c, no Pattern::Lit survives the desugar pass; codegen
and typechecker never see one. `is_flat` reclassifies Lit as
non-flat to force the chain machinery; new `build_eq` helper
constructs the equality test per literal kind.

Tests: 99 → 103 (+1 e2e lit_pat_demo, +3 desugar unit). Demo
fixture exercises top-level lit arms (classify) and nested
lit-in-Ctor (categorize_first) with a local IntList; output
100/200/999/-1/0/7.

Known limitations documented in DESIGN.md and JOURNAL: (a)
chain machinery's Unit terminator means non-Wild-terminated
exhaustive matches with lit arms still need a trailing `_`
catch-all to type-check; (b) `==` is currently Int-only, so
Bool/Str/Unit lit patterns desugar correctly but error at
typecheck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:47:21 +02:00
Brummel 20b412342d Iter 16a-aux — DESIGN.md drift audit (post-15f)
After six feature iters since the last docs sweep, DESIGN.md had
visible drift. Patched in place rather than queueing the next
codegen-heavy iter against a stale spec.

DESIGN.md changes (+72 LOC):
- Decision 6: form (A) marked shipped (Iter 14c, sole projection
  since 15e); body kept as audit trail.
- Pipeline: desugar pass added between resolve+hash and typecheck;
  invariant noted that CheckedModule.symbols hashes from the
  original module, not the desugared one (so ail diff/manifest
  preserve on-disk identity).
- CLI: added deps, diff, workspace, builtins (shipped earlier but
  never doc'd).
- "What is not (yet) supported": re-anchored from "end of Iter 13"
  to "as of Iter 16a"; removed lifted gates (cross-module ADTs,
  no-GC, flat-pattern-only); added tighter follow-up gates
  (literal sub-patterns, local recursive let).
- "What IS supported": promoted nested Ctor patterns (16a),
  cross-module ADTs (14h), form-(A) text surface (14b/14c/15e),
  Boehm GC (Decision 9 / 14f) into the smoke-test list.
- Smoke tests: added std_list_demo, std_maybe_demo, std_either_demo,
  std_pair_demo, nested_pat fixtures.

JOURNAL: 16a-aux entry recording the drift sites and what was
explicitly *not* changed (Goal, Decisions 1-5, 7-9, Mangling,
Convention, Data model, Verification — spot-checked, all current).

Tests: 93/93 unchanged (doc-only). Build clean.
2026-05-07 19:39:44 +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