Commit Graph

19 Commits

Author SHA1 Message Date
Brummel 14a91f0ae5 iter boehm-retirement.1 (DONE 10/10): retire the transitional Boehm GC backend
Closes Gitea #4. Removes the Boehm-Demers-Weiser conservative GC
backend wholesale across six layers in one atomic iteration. After
this iter, `AllocStrategy` has two variants (`Rc`, `Bump`),
`--alloc=gc` is rejected at CLI parse with `unknown --alloc value`,
the libgc link arm is gone, and the design ledger describes RC
(canonical) + bump (raw-alloc bench-floor) as the only allocators.

Layer-by-layer summary:

  CLI surface — `crates/ail/src/main.rs`:
    `parse_alloc_strategy` arm `"gc" => Ok(AllocStrategy::Gc)`
    removed; error wording updated to `(expected `rc` or `bump`)`;
    clap-derive `value_parser = ["gc","bump","rc"]` allowlist on
    BOTH `Build` and `Run` subcommands DROPPED so that
    `parse_alloc_strategy` remains the sole gatekeeper for the
    unknown-value diagnostic (otherwise clap shadows the runtime
    diagnostic with `invalid value 'gc' for '--alloc'`, which would
    miss the milestone-pin's stderr substring check). The
    `default_value = "rc"` stays.

  Codegen — `crates/ailang-codegen/src/lib.rs`:
    `AllocStrategy::Gc` variant + `Default` derive removed (no
    caller of `AllocStrategy::default()` existed in the workspace,
    so the trait derivation was dead). `fn_name` (spec called it
    `runtime_alloc_fn` loosely; actual identifier is `fn_name`)
    drops the `Gc => "GC_malloc"` arm. `lower_workspace` and
    `lower_workspace_staticlib` defaults flip from `Gc` to `Rc`.
    In-source negative-complement codegen test (mod tests, lib.rs:3571ff)
    retargets from `AllocStrategy::Gc` to `AllocStrategy::Bump`
    (bump also doesn't emit per-type drop fns; the test's semantic
    "no drop fns under non-RC" is preserved).

  Link branch — `crates/ail/src/main.rs:2389ff`:
    The `match strategy { AllocStrategy::Gc => { ... cmd.arg("-lgc"); ... } }`
    arm and its libgc-link block are entirely gone. The surviving
    match exhausts on `Bump` and `Rc` (Rust's exhaustiveness check
    confirms; no `error[E0004]`). Staticlib-guard diagnostic
    rewritten to drop the "shared Boehm collector" phrasing while
    preserving the prefix `staticlib (swarm) artefact is RC-only`
    verbatim (the surviving `staticlib_bump_is_rejected` test
    depends on that substring).

  Test suite — 3 pure-differential e2e tests deleted
    (`gc_handles_recursive_list_construction`,
    `alloc_rc_produces_same_stdout_as_gc`,
    `alloc_rc_matches_gc_on_std_list_demo`); 9 RC-feature tests
    stripped of their `stdout_gc` build call and differential
    `assert_eq!(stdout_gc, stdout_rc, ...)` (absolute
    `assert_eq!(stdout_rc.trim(), "<n>")` pin retained as
    correctness oracle); `staticlib_gc_is_rejected` deleted; new
    milestone-pin `crates/ail/tests/boehm_retirement_pin.rs`
    asserts `ail build --alloc=gc` exits ≠ 0 with stderr containing
    `unknown --alloc value` and `\`gc\``; `examples/gc_stress.ail`
    fixture deleted (no remaining references).

    Implementer expansion (not in plan): `iter17a_local_box_alloca`
    (in `e2e.rs`) carried an IR-shape assertion against
    `@GC_malloc`-absence as the witness for non-escaping
    allocation. After the Task-2 codegen default flip, the witness
    shifts to `@ailang_rc_alloc`-absence in escape-targeted
    positions; assertion + doc-comment updated. Property
    protected ("no heap allocation in non-escaping contexts") is
    unchanged; only the named allocator shifts.

  Bench harness — `bench/run.sh` 9→6 column compaction
    (workload + bump(s) + rc(s) + rc/bump + bump RSS + rc RSS);
    gc-arm `bench_latency_implicit_gc` build call + harness
    invocation dropped from latency block; header comment reframed
    from "GC-overhead bench harness" to "RC-overhead bench
    harness"; "Decision 10's Boehm-retirement target (1.3x)"
    rewording to "RC-overhead-vs-bump bench-health regression gate".

    `bench/check.py:62` header-sentinel changes from
    `"gc(s)" in line` to `"bump(s)" in line`; column-count check
    at `:72` flips from `!= 9` to `!= 6`; per-workload field set
    drops `gc_s`/`gc_over_bump`/`gc_rss_kb`; `ARM_LABEL_TO_KEY`
    drops the `"implicit @ gc": "implicit_at_gc"` entry.
    `bench/baseline.json` regenerated via `--update-baseline`.

    Implementer note (planner-defect): `write_new_baseline`
    iterated over the *existing* baseline's metric list when
    emitting the regenerated file, so even after parser-level
    `gc_*` removal, the fallback emitted them back into the JSON.
    Scrubbed post-update; the cleaner fix (have
    `write_new_baseline` emit only keys present in
    `parsed_throughput[workload]`) is a follow-up if the script
    becomes load-bearing for further allocator changes.

  Design ledger — `design/models/rc-uniqueness.md` excises the
    `## Dual allocator — RC canonical, Boehm parity oracle`
    section and the `Boehm-Demers-Weiser conservative GC` choice
    block + rationale + trade-offs; the per-fn-alloca section
    generalises Boehm-specific language to allocator-agnostic;
    the memory-model section's `## Choice.` paragraph reframes the
    1.3× target from "Boehm-retirement gate" to "bench-health
    regression gate".

    `design/models/pipeline.md` drops the `--alloc=gc → links libgc`
    arm of the pipeline diagram and replaces it with
    `--alloc=bump → links bump-floor`; the accompanying prose
    rewrites accordingly.

    `design/contracts/scope-boundaries.md` rewrites the
    "Memory management via Boehm conservative GC" bullet to
    describe RC + per-fn-arena present-tense; the dead reference
    to `examples/gc_stress.ail.json` (file never existed; the
    fixture only ever had a `.ail` form, deleted by this iter) is
    dropped along with the `examples/std_list_stress.ail.json`
    reference whose purpose was Boehm-only soak testing.
    `:67`'s `@printf` / `@GC_malloc` parenthetical updated.

    `design/contracts/memory-model.md:232` drops the
    "leaks like the pre-Boehm era" phrase; the RC inc/dec
    instrumentation is wired up, so the "until then" conditional
    that referenced pre-Boehm is closed.

    `design/contracts/embedding-abi.md:42-44` rewrites the
    staticlib-guard prose to drop the `--alloc=gc` clause (gc is
    now a CLI-parser-level unknown-value, not a staticlib-guard
    rejection) and reframe the swarm-safety justification around
    `--alloc=bump` (leak-only bench instrument) rather than the
    historical Boehm collector.

  Honesty pin — `crates/ailang-core/tests/docs_honesty_pin.rs`
    inverts the polarity: the present-tense Boehm-anchor assertion
    on `pipeline.md` (`:116-117`) is deleted, and four
    absence-pins are added to `design_md_has_no_wunschdenken`
    against the Boehm-zombie strings `transitional Boehm`,
    `parity oracle`, `GC_malloc`, `libgc`. The
    `design_corpus()` already includes `rc-uniqueness.md` so no
    path-list change was needed for the new pins to scan.

    `crates/ailang-core/tests/design_index_pin.rs:166` drops the
    `"pre-Boehm"` token from the protected-exception comment list
    (the phrase no longer appears in `memory-model.md` after this
    iter, so the exception is dead).

  Runtime docs — `runtime/bump.c`, `runtime/rc.c`, `runtime/str.c`
    header comments scrubbed of Boehm/`GC_malloc`/`libgc`
    references. `bump.c`'s function signature description still
    documents `void *bump_malloc(size_t)` as the bench-floor
    allocator interface, but no longer cross-references libgc.

  Example fixtures — `examples/bench_latency_implicit.ail`,
    `bench_latency_explicit.ail`, `escape_local_demo.ail`,
    `reuse_as_demo.ail`, `rc_pin_recurse_implicit.ail` doc-comment
    headers scrubbed of `--alloc=gc` / Boehm references. The
    `.ail` surface (AST) is untouched in every case; round-trip
    invariant holds (`cargo test -p ailang-surface --test round_trip`
    green).

  Skill / agent prompts — `skills/audit/agents/ailang-bencher.md`
    rewritten to use an RC-vs-bump worked example pattern for the
    hypothesis-driven bench tutorial, replacing the recurring
    "RC vs Boehm under heap pressure" example.
    `skills/implement/agents/ailang-implementer.md` Decision-10 /
    Boehm references replaced with present-tense RC-commitment
    framing.

  IR snapshots — the 5 checked-in snapshots
    (`crates/ail/tests/snapshots/{hello,list,max3,sum,ws_main}.ll`)
    regenerated via `UPDATE_SNAPSHOTS=1 cargo test -p ail --test
    ir_snapshot`. Each previously contained
    `declare ptr @GC_malloc(i64)` and (for `list.ll`) a `call ptr
    @GC_malloc(...)` invocation; post-flip the snapshots contain
    `declare ptr @ailang_rc_alloc(i64)` plus the rc inc/dec runtime
    declarations.

Spec-vs-acceptance addendum (caught at orchestrator end-report,
absorbed here rather than in a follow-up spec edit): spec §6
acceptance criteria said "Boehm-grep returns matches ONLY in
docs_honesty_pin.rs". The plan itself prescribed historical Boehm
references in 3 additional files: (a) the new milestone-pin
`boehm_retirement_pin.rs` (must literally invoke `--alloc=gc` to
assert its rejection), (b) `embed_staticlib_alloc_guard.rs` file
doc-comment historical note ("`--alloc=gc` no longer exists as a
CLI value"), (c) `embedding-abi.md:44-45` contract historical
clause ("see the Boehm-retirement iter"). All three are
prescribed; the spec's grep wording was too narrow. The four
absence-pins in `docs_honesty_pin.rs` catch the actual zombies
(Boehm-narrative re-emerging in the design ledger), which is the
substantive intent the spec was aiming at — the four extra
documented-by-design exceptions are the cost of having an
explicit milestone-pin and contract-level historical anchors.

Net delta:
  - 32 files modified, 2 new (boehm_retirement_pin.rs + stats),
    1 deleted (gc_stress.ail);
  - workspace tests: every binary `0 failed`. Pass-count delta:
    -3 net (4 e2e tests deleted, 1 new milestone-pin test added);
  - boehm-grep state: hits only in the four by-design exceptions
    documented above;
  - `bench/check.py` exit 0 against regenerated baseline;
  - CLI must-fail fixture: `ail build --alloc=gc examples/hello.ail`
    exits non-zero with stderr containing `unknown --alloc value`
    and `\`gc\``;
  - design ledger present-tense honest (Boehm-narrative gone from
    `rc-uniqueness.md` + `pipeline.md`; the few historical
    references in `embedding-abi.md` / `boehm_retirement_pin.rs` /
    `embed_staticlib_alloc_guard.rs` are explicit milestone-pins
    or contract anchors, not silent ledger residue).

Bench measurement variance noted: closure-chain and hof-pipeline
are ±1-5% jittery between runs; one regeneration flagged 2
metrics as `regressed` before a second run returned 0. The
captured baseline is within self-comparison range. Existing
per-metric tolerances absorb the jitter.

Stats file:
`bench/orchestrator-stats/2026-05-20-iter-boehm-retirement.1.json`.

closes #4
2026-05-20 20:51:53 +02:00
Brummel 13946219f5 fix(runtime): mirror surface .0-fallback in ailang_float_to_str
`runtime/str.c::ailang_float_to_str` now applies the `.0`-fallback
already enforced by the surface printer's `write_float_lit`
(crates/ailang-surface/src/print.rs lines 624-637): after the
existing `%g` snprintf, if `x` is finite and the rendered buffer
contains neither `.` nor `e`/`E`, append `.0` so a whole-valued
Float renders with Float-shaped text. The `isfinite(x)` fence
keeps `nan`/`inf`/`-inf` from matching the predicate and turning
into `nan.0`/`inf.0`. The 64-byte stack buffer's truncation guard
is extended by 2 bytes for the fallback path so the defensive
`abort()` discipline holds.

The alternative (document `%g`-without-fallback in the design/
ledger as a deliberate human-friendly contract) was rejected: the
language's stated ethos is machine-readability and round-trip
identity; the surface printer is the reference, runtime drift
from it is a defect not a feature.

Golden updates flow from the contract change. Three fixtures
encoded the old Int-shaped output for Float values:
- `crates/ail/tests/floats_e2e.rs`: `"4\n42\n-1.5\n"` →
  `"4.0\n42.0\n-1.5\n"` (1.5+2.5=4.0, int_to_float(42)=42.0).
- `crates/ail/tests/e2e.rs::mut_sum_floats_prints_55`: `"55"` →
  `"55.0"`. The accompanying doc comment used to canonicalise the
  bug ("`%g` strips the trailing `.0` — so the canonical stdout
  for Float 55.0 is `55`"); rewritten to reflect the new contract.
- `examples/mut_sum_floats.ail` docstring: same correction.

Two doc comments are updated where the post-fix contract differs
from the old text but the fixture golden does not change:
- `examples/float_to_str_smoke.ail` docstring (3.5 still renders
  as `3.5` because `.` is already present — the fallback does not
  fire).
- `crates/ail/tests/e2e.rs::float_to_str_smoke` doc comment (same
  observation, made explicit so the trip-wire's intent is clear).

Verified: `cargo test -p ail --test print_float_whole_e2e` green
(was RED at f19b0dd); `cargo test --workspace` 0 failed across
all crates and integration suites.

closes #7
2026-05-20 18:24:57 +02:00
Brummel 99df14c792 workflow: scrub residual JOURNAL/journal refs missed in the first sweep
A re-grep found 15 live references (excluding docs/specs/ and
docs/plans/, which stay historical) the previous two commits
missed — all in source comments, doctests, bench helpers, and one
agent file.

Per-file:
- crates/ail/src/main.rs: typeclass-coherence diagnostic comment
  pointed at "the JOURNAL queue's wording" — points at
  design/contracts/typeclasses.md alone.
- crates/ailang-prose/src/lib.rs: `//!` header referred to
  docs/JOURNAL.md "Pinned: human-readable prose surface" —
  retargeted to design/contracts/authoring-surface.md.
- crates/ailang-check/src/lib.rs: bugfix tag + "see iter
  method-dispatch-refactor journal" prose collapsed to a clean
  rationale paragraph; the canonical shape is stated inline.
- crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs:
  "(d) record the rationale in a per-iter journal" →
  "(d) record the rationale in the commit body".
- crates/ailang-surface/tests/prelude_module_hash_pin.rs: header
  collapsed (pd.2/pd.3 milestone narrative dropped — the test's
  purpose is self-evident from its body); both "per-iter journal"
  drift-instructions point at the commit body.
- runtime/rc.c: "bench numbers in JOURNAL 18f.2" →
  "original profiling bench numbers".
- bench/run.sh: two "JOURNAL entry" comments → "commit body".
- bench/architect_sweeps.sh: header rewritten (cross-ref to the
  design-md-consolidation milestone commits, not a JOURNAL entry).
  Sweep-4 extended with two new anti-regrowth phrases
  ("see the per-iter journal", "in a per-iter journal") so journal
  prose can't grow back into design/contracts/ silently.
- skills/audit/agents/ailang-architect.md: "unlike a journal it
  lives on main" → "since it lives on main".
- ail-embed/src/bin/timeshard_runner.rs: "records it in the
  close-out journal" → "prints it to stderr".
- ail-embed/tests/timeshard.rs: two "journal-only friction timing"
  → "stderr-only friction timing".

Verification (after the edit):
- `grep -rin '\bjournal\b'` against live tree returns exactly two
  hits: the Sweep-4 regex itself (intentional — TABU phrases that
  prevent regrowth) and the PHRASES array in design_index_pin.rs
  (intentional — the same regrowth guard at test level). Both are
  load-bearing negative assertions.
- `bash bench/architect_sweeps.sh` exits 0 ("All five sweeps
  clean") — no design/-side regrowth.
- `cargo build --workspace` green; `cargo test --workspace` green.
2026-05-20 11:32:37 +02:00
Brummel 176821c2e7 iter design-md-rolesplit.1 (DONE 9/9): DESIGN.md -> design/ ledger role-split
The 3020-line docs/DESIGN.md is replaced by the design/ ledger:
design/INDEX.md (sole addressable spine, typed Contracts+Models tables,
polymorphic links — prose file OR authoritative source //!), 14
design/contracts/*.md test-linked invariants + 3 source-link-only
contracts (mangling/env-construction/qualified-xref, no prose file —
code is SoT), 5 design/models/*.md whitepapers, and
docs/journals/2026-05-19-design-decision-records.md (the
relitigation-guard archive — every why/rejected/does-not-do/rollback/
empirical ### moved out at ###-granularity). Clean cut: git rm
docs/DESIGN.md, no stub.

RED-first crates/ailang-core/tests/design_index_pin.rs — the 4-clause
anti-regrowth spine (DESIGN.md-gone / every-INDEX-link-resolves /
every-contract-names-a-resolvable-ratifier /
contracts-carry-no-decision-record-prose) — demonstrably RED before,
GREEN after. Build-atomic by task ordering: design_schema_drift.rs's
include_str! (the only compile-time consumer) retargeted to
design/contracts/data-model.md BEFORE the deletion; its
## Data model/## Pipeline slicer dropped (a simplification the split
enables). 2 NoInstance diagnostics + 2 lockstep E2Es retargeted to
design/contracts/{float-semantics,typeclasses}.md. ~12 agent reading
lists + 5 SKILL bodies + CLAUDE.md + skills/README.md + ~25
code/C/.ail/spec comment xrefs retargeted; OQ7 dangling 'Iter 13b'
cite deleted (no forward target — a pointer would be fiction).
honesty-rule.md rewritten so the rule names the new home
(rationale->journals), resolving the recon-found internal
contradiction; the two docs_honesty_pin.rs:70,72 pinned phrases kept
verbatim+contiguous.

Boss-verified independently: cargo test --workspace 646 passed /
0 failed; design_index_pin 4/4; acceptance grep CLEAN of live
DESIGN.md refs (residuals = only the spec-mandated clause-4
deletion-enforcer). 2 DONE_WITH_CONCERNS routed to the mandatory
milestone-close audit: (a) str-abi.md:23 '(iter str-concat,
2026-05-13)' provenance stamp trips advisory architect_sweeps Sweep-1
— Boss-confirmed byte-identical to DESIGN.md@deeffb1:2062-2065, a
faithfully-migrated PRE-EXISTING anchor (regexes verbatim, only path
retargeted), NOT split-introduced — RATIFY-or-tidy at audit; (b) a
now stale-direction intra-prose 'see Str ABI below' cross-ref in
float-semantics.md — audit-adjudication candidate. Plan defect noted:
Task 9 Step 4's verbatim acceptance grep used a ^./ anchor not
matching the system's grep -rIn output; substance re-verified CLEAN.

Spec grounding-check PASS x2. Journals INDEX + decision-records
pointer appended (Boss-only).
2026-05-19 13:04:22 +02:00
Brummel 70f2a318e0 iter embedding-abi-m5.tidy (DONE 3/3): milestone-close doc-honesty drift — pin-safe, doc/comment-only
Resolves the M5 milestone-close audit DRIFT (audit journal
28ab56a). Single cohesive commit, M2.tidy a80d495 / M3.tidy
63d7d60 precedent (pins are the coverage, no RED, no
audit/fieldtest gate).

- docs/DESIGN.md edit-1 §"Free (host side)": dropped the retired-M4
  forward-reference ("an additive M4 concern, not a contradiction
  of this freeze"; M4 retired 2026-05-18) -> present-tense current
  fact (a boxed-field record is not an M3 embedding type,
  export-gate-rejected; the freeze covers exactly the all-scalar
  single-ctor record).
- docs/DESIGN.md edit-2 §"Embedding ABI": reconciled the now-
  inaccurate "(no shared mutable runtime state - ... data-race-free,
  sanitiser-verified)" blanket with the real post-7bfa11e state:
  the per-allocation hot path (per-ctx counters + per-object
  refcount header) is non-atomic by design and never shared
  (Ctx:!Send, single-thread-per-ctx); the one shared datum is the
  atomic-relaxed global RC-stats fallback counter; "data-race-free,
  sanitiser-verified" retained (still true).
- runtime/rc.c:88 comment-only: stale "two unconditional ++
  operations" -> accurate "one counter bump per alloc/free ...
  relaxed atomic add on the global null-ctx fallback, a plain ++
  on the per-ctx path" (consistent with the adjacent already-
  correct :93-106 atomic block + :45-55 Threading header).

Boss-verified independently: all pins green (design_schema_drift
8/0, docs_honesty_pin 5/0, effect_doc_honesty_pin 4/0,
embed_record_layout_pin 1/0) = mechanical proof no pinned/hashed
byte moved; the adjacent separately-pinned bare-scalar sentence is
byte-identical (shifted 2299->2305 by net-added lines; substring
pin, passes); rc.c strictly comment-only (filter empty);
cargo build --workspace Finished; git scope = only docs/DESIGN.md
+ runtime/rc.c + journal/stats. Clears the M5-audit doc-honesty
debt; no new debt.

FINAL M5 iteration. The M1-M5 Embedding ABI arc is functionally
complete, audited, ratified, and doc-honest. Includes the per-iter
journal, stats, and INDEX line.
2026-05-19 03:07:40 +02:00
Brummel 7bfa11e838 fix(rc): GREEN — atomic global g_rc_* stats fallback counters (swarm-safe)
bugfix-rc-global-stats-race, GREEN stage. RED is the separate
audit-trail commit 427b687.

g_rc_alloc_count / g_rc_free_count (runtime/rc.c:90-91) were plain
static uint64_t; the __ail_tls_ctx == NULL fallback at rc.c:161
(alloc) and rc.c:212 (dec-to-zero) did a non-atomic ++. A
multi-threaded host using the global fallback path (no
ailang_ctx_new) raced the read-modify-write and lost increments, so
the AILANG_RC_STATS atexit Σ under-counted non-deterministically.
Not a memory bug — Ctx:!Send keeps every box on one thread; only
the global statistics Σ was wrong (the M5 iter-2 symbol-fan
leak-proof finding, b724cd1; user-approved Option A bounce-back
resolution).

Fix (runtime/rc.c only, 37+/14-): the two globals are now
_Atomic uint64_t; atomic_fetch_add_explicit(.., memory_order_relaxed)
at the two null-ctx fallback ++ sites; atomic_load_explicit(..,
relaxed) snapshot in ailang_rc_stats_atexit (its sole reader).
Relaxed is correct and sufficient — pure statistics, no
happens-before obligation, atexit reader runs after all worker
threads joined. Per-ctx counters and the per-object refcount header
stay non-atomic BY DESIGN (single-thread-per-ctx; boxes never cross
threads) — out of scope, untouched. Frozen value layout / ABI
offsets / host-free rule untouched. The genuinely-still-
single-threaded drop-worklist doc note was correctly left unchanged
(false correction refused); the two stale atomicity doc blocks
corrected for honesty.

Boss-verified independently: RED -> GREEN deterministically 3/3
(jitter gone, allocs==frees==16_000_000); full cargo test -p ail
green (every binary 0 failed — per-ctx tsan harnesses + embed
e2e unaffected; rc.c links into every ail binary, strong regression
gate); scope = runtime/rc.c + journal + stats only; RED files
unchanged. Unblocks resuming the M5 leak-proof.

Includes the per-iter journal, stats, and the INDEX.md line.
2026-05-19 01:57:06 +02:00
Brummel 4ea8bc5faf iter embedding-abi-m3.1 (DONE 6-7): freeze the value layout — DESIGN.md frozen-layout SSOT + lockstep pointers + enforceable byte-pin
Tasks 6-7 of the Boss-repaired split dispatch (Tasks 1-5 committed d5c565d).
Task 6: DESIGN.md §"Embedding ABI" gains the `### Frozen value layout
(M3 — one-way commitment)` SSOT subsection; both "provisional until M3"
sentences rewritten to one-way-freeze wording (docs_honesty_pin-safe — the
pinned "written **bare**" sentence stayed byte-verbatim, :2297->:2299
content-asserted); // FROZEN ABI lockstep pointers at runtime/rc.c +
match_lower.rs lower_ctor + drop.rs; 3 plan-named stale rustdocs fixed +
the Boss-caught stale // gate-comment block (check/lib.rs:1917-1922,
M1/scalar-only -> M1/M2/M3/C-ABI-permitted-incl-record); byte-pin
enforceability demonstrated (RED on a local offset perturbation, GREEN on
git-checkout restore — the only legitimate git op, working-tree only).
Task 7: milestone-close verification gate.

Boss-verified independently (agent report = intent, not outcome): workspace
639 passed / 0 failed; embed_record_e2e 2/2 (own+borrow, global
leak-freedom model); byte-pin 1/1; gate 10/10; forwarder 3/3;
docs_honesty_pin 5/5 pin-safe; design_schema_drift + embed_export_hash_stable
+ embed_e2e + M2 swarm regression-green-unmodified.

INDEX.md line added (deferred from the PARTIAL — one line per iter, final
title). Milestone-close audit next (architect Invariant 1 + bench trio,
spec Testing items 8/9 — audit-owned, not the implement run's job).
2026-05-18 21:29:50 +02:00
Brummel c9a84b33b3 iter embedding-abi-m2.1 (PARTIAL 4/9 + Boss spec-defect repair): ctx ABI + de-globalisation
Tasks 1-4 land fully review-green and are the cohesive shippable
subset (the per-thread embedding ABI, fully wired + proven):
- T1: FIXED-FIRST alloc-guard baseline pin (RED captured -> GREEN at T4).
- T2: runtime/rc.c gains ailang_ctx_t {alloc_count,free_count} +
  ailang_ctx_new/_free + __thread __ail_tls_ctx; the two increment
  sites become 'if (_ctx) _ctx->... else g_rc_...'. g_rc_* statics +
  ailang_rc_stats_atexit + the constructor RETAINED VERBATIM as the
  null-ctx (single-threaded executable) fallback. rc_accounting_tsan.c
  + driver: per-ctx 8x200000 tsan-clean (de-globalisation positive proof).
- T3: codegen Target::StaticLib forwarder gains a leading 'ptr %ctx'
  + one '@__ail_tls_ctx = external thread_local global ptr' decl +
  TLS save/store/restore around the BYTE-UNCHANGED internal call;
  _adapter/_clos + internal arg vector untouched (M1 decision held);
  doc-comment provisionality narrowed to the value/record layout.
- T4: build_staticlib rejects --alloc != rc (RC-only swarm artefact).

Boss adjudication of the orchestrator's Task-5 BLOCKED (spec-defect,
correctly surfaced not hacked): swarm.c's -DSHARED_CTX negative
control is structurally impossible — examples/embed_backtest_step.ail
is a non-allocating scalar kernel that never writes a ctx field, and
__ail_tls_ctx is __thread, so a shared-ctx scalar swarm has no
shared-memory write to race on (the spec's OWN item-1 honesty point).
The de-globalisation teeth belong to the item-1 rc_accounting harness
(shared ctx genuinely races on ctx->alloc_count; orchestrator
independently confirmed tsan exit 66). Spec amended in lockstep
(Goal coherent-stop, must-fail-axis item 2, Testing item 3,
Acceptance) so item 3's negative control is item-1's by the
de-globalisation-proof / capability-demo split; plan Task 5
restructured (swarm.c per-ctx capability demo only; negative-control
standing test relocated into the item-1 harness driver) + Task 3's
plan-transcription error (@ail_backtest_step ->
@ail_embed_backtest_step_step) corrected. This is a Boss
consistency-repair of an internally-contradictory clause whose intent
is already correctly realised in the same spec — not a redesign; no
brainstorm bounce. Task-5 working files discarded (corrected plan
reproduces them; a structurally-RED test must not enter main).
INDEX.md + final journal land at iter completion (re-dispatch [5,9]).
2026-05-18 17:16:40 +02:00
Brummel 6fdb45d2f2 iter rpe.1: retire per-type print effect-ops
Single iter shipping the post-milestone-24 follow-up named in
docs/specs/2026-05-14-retire-per-type-print-effects.md. After this
iter the only surviving direct-output effect-op is `io/print_str`;
all per-type print primitives are replaced by the polymorphic
`print` helper (prelude, iter 24.3).

Components:
- 92 examples/*.ail fixtures migrated (do io/print_<T> x) →
  (app print x); 6 .prose.txt snapshots regenerated via `ail prose`.
- Four-site lockstep compiler deletion: crates/ailang-check/src/builtins.rs
  (3 effect_ops.insert blocks + 3 list() rows + the
  install_io_print_float_signature test + module + EffectOpSig
  doc-comments); crates/ailang-codegen/src/lib.rs lower_app
  (3 arms + lowers_io_print_float test); crates/ailang-codegen/src/synth.rs
  builtin_effect_op_ret match-arm pattern. Dead `intern_string`
  helper removed as a follow-up.
- Five incidental test-body migrations (ailang-check x2, ailang-core
  spec_drift + design_schema_drift, ailang-surface/src/lex.rs,
  ailang-prose/src/lib.rs round-trip test).
- Cat B test-harness patch: six IR-shape tests in
  crates/ail/tests/e2e.rs gained a monomorphise_workspace call
  before lower_workspace_with_alloc so they follow the same
  pipeline as `ail build` (the home-rolled desugar+lift loop
  stayed because mono's precondition is "already lifted"; mono
  inserts after lift).
- Six doc-comment touch-ups (lex.rs module doc, parse.rs
  diagnostic example, ail/src/main.rs x2, runtime/str.c %g anchor,
  crates/ailang-core/specs/form_a.md surface-spec example).
- DESIGN.md seven-site sweep (Decision 11 example, Polymorphic
  print past-tense, Heap-Str output sentence, effect-op
  invocation comment, Float NaN paragraph re-anchored on
  float_to_str, two "What is supported" lists).
- Three E2E test-comment polish + four IR-snapshot refresh + one
  canonical-hash pin update (plan-unanticipated downstream
  consequences of the corpus migration).
- bench/{check,compile_check,cross_lang}.py: all exit 0; no
  ratification needed.
- Roadmap entry struck through; per-iter journal at
  docs/journals/2026-05-14-iter-rpe.1.md.

Tests 564/0/3. cargo clippy and cargo doc: zero warnings.

Two upstream codegen bugs surfaced during the first BLOCKED
attempt and were fixed in separate iters before this retry:
- 1fb225e bugfix: mono cursor misalignment at poly-free-fn Var
  with class-constrained Forall.
- feb9413 bugfix: print leak — propagate ret_mode through rigid
  substitution + prelude Show.show ret_mode.

Known debt (carried forward for next /audit):
- Emitter.strings field is functionally dead post-iter (orphan
  after intern_string removal); cycles over empty map harmlessly.
2026-05-14 02:12:34 +02:00
Brummel e7e67e1a40 iter str-concat: heap-Str concatenation primitive in four-site lockstep
Closes fieldtest-form-a friction finding #4. `str_concat : (borrow Str,
borrow Str) -> Str` ships in the four-site-lockstep pattern established
by `str_clone` / `int_to_str` / `bool_to_str` (iter 24.1). The
LLM-natural Show-MyType body
`(app str_concat "label=" (app int_to_str x))` now parses, checks,
builds, and runs end-to-end.

Sites touched (lockstep):
- runtime/str.c — `ailang_str_concat(a, b)` slab-allocates and
  memcpys both source payloads into a new heap-Str.
- ailang-check/src/builtins.rs — `env.globals.insert("str_concat",
  Fn { 2x Str borrow, ret Str own, effects [] })` + `list()` row +
  `install_str_concat_signature` unit test.
- ailang-codegen/src/lib.rs — `declare ptr @ailang_str_concat(ptr,
  ptr)` extern + `lower_app` arm after str_clone + `is_builtin_callable`
  extension + IR-pin unit test `str_concat_emits_call_to_ailang_str_concat`.
- examples/show_user_adt_with_label.ail (new) + crates/ail/tests/
  str_concat_e2e.rs (new) — corpus fixture exercising the LLM-natural
  Show body shape + E2E pin asserting check + build + run produce
  `Item 42\n`.

Lockstep collision repaired: examples/bug_unbound_in_instance_method.ail
used `str_concat` as its UNBOUND name (because that was the literal
fieldtester repro). Renamed to `format_label` (LLM-author-realistic
helper name that will never become a builtin) and updated the pin test
`crates/ail/tests/unbound_in_instance_method_pin.rs` accordingly,
preserving the regression guard's intent (instance-method-body walked
through unbound-var check).

DESIGN.md amended: new §"Heap-Str primitives" subsection between the
milestone-24 Show-backer enumeration and the existing
`Primitive output goes through ...` paragraph, cataloguing all five
heap-Str primitives (`int_to_str`, `bool_to_str`, `float_to_str`,
`str_clone`, `str_concat`) with signatures, iter origins, and the
user-visible-vs-prelude-internal distinction. Show-backer block
unchanged.

IR snapshots regenerated (hello, list, max3, sum, ws_main) to absorb
the new `declare ptr @ailang_str_concat(ptr, ptr)` line in the
unconditional extern header — same upkeep pattern as hs.4 which
regenerated the same 5 snapshots for the same reason
(unconditional declares dead-stripped by clang -O2 when unused).

Tests: 559 + 3 = 562 green (E2E pin + builtin-signature test + IR-pin
test). Zero re-loops across all 7 tasks.
2026-05-13 12:43:10 +02:00
Brummel f38bad8c2b iter 24.1: bool_to_str + str_clone runtime + codegen wiring
First iter of milestone 24 (Show + print rewire). Wires two new
heap-Str-producing primitives parallel to hs.4's int_to_str /
float_to_str:

- runtime/str.c gains ailang_bool_to_str(bool) → heap-Str "true" /
  "false" and ailang_str_clone(const char *) → memcpy'd heap-Str
  copy. Both use the existing str_alloc slab helper.
- builtins.rs + synth.rs install the two signatures lockstep with
  ret_mode: Own; str_clone carries param_modes: [Borrow].
- IR-header preamble gains two unconditional `declare ptr @...`
  lines; Emitter::lower_app gets two new arms; is_static_callee
  whitelist extends with the two names.
- Five IR snapshots regenerate for the two new declares.
- Pre-existing-drift fix: int_to_str row added to builtins.rs::list()
  (hs.4 installed env.globals entry but missed the list() row).

Substantive deviation flagged by orchestrator (DONE_WITH_CONCERNS):
builtin signatures registered in uniqueness.rs::infer_module and
linearity.rs::check_module_with_visible (8 LOC × 2 files), symmetric
to iter 23.4-prep's class-method registration in the same globals
maps. Without this fix str_clone's param_modes: [Borrow] is invisible
to the App-arg walker, src_heap walks as Position::Consume, the
scope-close ailang_rc_dec is gated off, and the
str_clone_cross_realisation_uniform_abi test's plan-literal
`frees == 3` assertion does not hold. The fix is the substantively
correct repair, not a design departure.

9 new tests: 2 builtins-install unit, 2 IR-shape unit pins, 5 E2E
(2 RC-stats, 2 stdout-smoke for both Bool branches, 1 cross-
realisation). 4 new .ail.json fixtures.

Full cargo test --workspace: 513 passed, 0 failed.
bench/compile_check.py: 24/24 stable. bench/cross_lang.py: 25/25
stable.
2026-05-12 23:47:13 +02:00
Brummel 1cf281b217 iter hs.3: heap-Str runtime additions — str_alloc + int_to_str + float_to_str
Append three new symbols to runtime/str.c: a private str_alloc(uint64_t)
slab helper that allocates [rc_header | len | bytes... | NUL] via
ailang_rc_alloc and writes the len prefix, plus two extern formatters
ailang_int_to_str(int64_t) and ailang_float_to_str(double) that compose
str_alloc with snprintf("%lld") / snprintf("%g"). Defensive abort() on
truncation; 64-byte stack buffer comfortably oversized for either
formatter.

The extern declaration of ailang_rc_alloc carries __attribute__((weak)).
First regression sweep without it surfaced 11 e2e link failures under
--alloc=gc and --alloc=bump: public T-visible symbols (int_to_str /
float_to_str) pull their transitive callees (rc_alloc) into every
binary's symbol set, but rc.c is not linked under gc/bump in hs.3.
The weak attribute makes the cross-allocator link safe in isolation;
under rc the strong definition wins as usual. Retained after hs.4
(when rc.c becomes unconditionally linked) as a no-op that keeps
str.c link-safe in isolation.

No IR-side caller wired yet — hs.4 lands the IR-header declare lines,
the int_to_str/float_to_str codegen lowering, the checker install,
and the unconditional rc.c link.

cargo test --workspace + cross_lang.py + compile_check.py + check.py
all green on re-sweep.
2026-05-12 18:03:46 +02:00
Brummel 94893bfb9d iter 23.3.1: runtime ail_str_compare + IR header declaration 2026-05-10 23:00:22 +02:00
Brummel cc2d6944c1 iter 23.2.1: runtime/str.c with ail_str_eq + unconditional link 2026-05-10 21:49:45 +02:00
Brummel fc5f4590f8 rc: opt-in alloc/free stats counter for diagnosing leaks
Two non-atomic uint64_t counters in runtime/rc.c, incremented from
ailang_rc_alloc and the to-zero branch of ailang_rc_dec. An
__attribute__((constructor)) registers an atexit handler IFF the
AILANG_RC_STATS env var is non-empty at startup; the handler prints

    ailang_rc_stats: allocs=N frees=M live=K

to stderr. Default-disabled so production binaries stay quiet.

Used by the e2e test infrastructure for assertions about RC
correctness (e.g. tail-recursive list-sum must not leak outer cells)
and by the bencher / debugger when diagnosing leak shape from a
fixture run. Single-threaded; non-atomic — same scope as the rest
of runtime/rc.c. The two unconditional increments on the hot paths
are negligible relative to the libc malloc/free already there.
2026-05-08 14:18:02 +02:00
Brummel 6b3ff3bbed tidy: rewrite stale 18b/18c.x doc-headers to match shipped state
First half of the post-18-arc tidy-iter (per the new CLAUDE.md
iter-cycle rule). Architect's drift review flagged module-doc
headers describing 18b's leak-everything snapshot or 18c.x's
"deferred" debt that has since shipped. Doc-only changes; cargo
build clean, cargo test --workspace green at e2e=61, no
behavioural change.

- runtime/rc.c top-of-file header: rewrote from "Iter 18b
  deliberately stops at the layout and the alloc... programs
  leak every allocation" (false post-18c.3) to a stage summary
  spanning 18b–18e. Fixed `--memory=rc` reference (renamed to
  `--alloc=rc` in 18b's CLI work). Updated ailang_rc_inc /
  ailang_rc_dec block comments to point at `drop_<m>_<T>` and
  the worklist as the cascade owners, not at "18c will wire
  this up".

- ailang-check uniqueness.rs module-doc: replaced the "deferred
  to later iters" block (which named 18c.4 + 18d as future
  work, both shipped) with a current "what this pass does NOT
  do" block. Cross-fn reasoning is still genuinely deferred;
  per-type drop fns and recursive cascades are NOT this pass's
  job by design (codegen does them, not the inference).

- ailang-codegen emit_drop_fn_for_type doc + in-body comment:
  rewrote "Iter 18e replaces the recursive call with an
  iterative worklist free" to describe the actual shipped
  behaviour — the 18e (drop-iterative) annotation routes
  annotated types through emit_iterative_drop_fn_for_type;
  unannotated types stay recursive by orchestrator design
  (cheaper IR, no worklist alloc).

Held back for the second half of the tidy-iter (pending the
ailang-bencher determinism result):

- DESIGN.md Decision 10 line 700 says modes are "mandatory"
  but lines 940–952 admit they're opt-in with deferred
  mandatoriness. The bench result either supports tightening
  the mandatoriness claim or backs down to "opt-in with
  performance benefit" — orchestrator-level decision blocked
  on the bench data.
- Dynamic-tag partial-drop debt is captured in JOURNAL but
  should be surfaced in DESIGN as a known precision gap.
2026-05-08 13:34:32 +02:00
Brummel ce6ab8ee44 Iter 18e: (drop-iterative) annotation + worklist allocator
Closes the 18-arc's stack-recursion limit. Recursive drop
cascades from 18c.4 overflow on long ADT chains (Linux's 8 MB
default stack maxes out around 1M cells of List). The new
opt-in (drop-iterative) annotation on a Def::Type switches the
synthesised drop_<m>_<T> body from recursive to iterative-with-
explicit-worklist for that type.

Schema:
- TypeDef.drop_iterative: bool. Default false; serde-skip
  when false so existing fixtures' canonical JSON hashes stay
  stable.
- Form-A: (drop-iterative) clause inside (data T ...).

Worklist runtime (4 new ABI symbols in runtime/rc.c):
- ailang_drop_worklist_new(initial_capacity)
- ailang_drop_worklist_push(wl, ptr)
- ailang_drop_worklist_pop(wl) -> ptr
- ailang_drop_worklist_free(wl)

Heap stretchy buffer, doubling on overflow, null-filtering on
push. Lean 4 / Roc precedent documented in the runtime; the
slot-repurposing strategy was considered and rejected because
not every box has a free pointer-typed slot to thread the
worklist through (Cons head is i64, slot 1 is ptr but it's
the field we're following — no free slot).

Codegen (emit_iterative_drop_fn_for_type): for a
drop_iterative type, drop_<m>_<T>(ptr %p) emits a worklist
loop. Fields of the SAME annotated type push onto the
worklist (mono-typed); fields of DIFFERENT types call their
own drop fn directly (recursive on those, but only if THEY
are themselves recursive — i.e. one level of cascade jump
maximum). Mono-typed-worklist is sound for the deep-self-
recursion case the iter targets (List of List of T just
needs the spine flattened).

Tests:
- examples/rc_drop_iterative_long_list — 1M-cell List of Int
  with (drop-iterative) annotation.
- alloc_rc_drop_iterative_handles_million_cell_list E2E —
  builds + runs under --alloc=rc, asserts clean exit. With
  annotation: exits 0. Without annotation (control): SIGSEGV
  at exit code 139 (verified by hand). Worklist is load-
  bearing.
- iter18e_drop_iterative_emits_worklist_body_no_self_recursion
  IR-shape: worklist body has br to loop_head AND no direct
  recursive call into drop_<m>_<T>.
- iter18e_no_annotation_keeps_recursive_drop_body — control:
  unannotated variant still emits the 18c.4 recursive shape.
- 3 surface parse-tests for the annotation round-trip.

Test deltas: e2e 58 -> 61 (+3), surface 18 -> 21 (+3). All
other buckets unchanged. cargo test --workspace green.

Known debt (deliberate):
- Mono-typed worklist: cross-type drop-iterative fields call
  the other type's drop fn directly. A heterogeneous
  worklist would be more general but adds tag tracking
  complexity for a case (drop-iterative T containing
  drop-iterative T') that's narrower than the deep-self-
  recursion target. Documented in
  emit_iterative_drop_fn_for_type's doc.
- Closure / Type::Var / Type::Forall fields fall back to
  shallow ailang_rc_dec via field_drop_call — same as the
  recursive variant.
- Dynamic-tag partial-drop fallback (head_or_zero epilogue
  shallow dec when moved_slots non-empty) — out of scope per
  brief.
2026-05-08 12:53:09 +02:00
Brummel 1eed78c41e Iter 18b: RC runtime + --alloc=rc routing
Wires up reference-counting allocator end-to-end without any
inc/dec emission. Programs run under --alloc=rc and produce
correct stdout (validated against --alloc=gc); they leak every
allocation, exactly like the pre-Boehm era. The point of 18b is
to establish the runtime contract before 18c adds the
inc/dec-emission codegen pass.

runtime/rc.c — new file. 8-byte uint64 refcount header
prepended to every payload; ailang_rc_alloc(size) returns a
ptr to the payload (header at ptr-8). ailang_rc_inc / dec are
declared but never called by codegen yet; they exist so 18c
can wire codegen against a stable runtime ABI. dec frees on
zero refcount but does NOT recursively dec child references —
that's 18c's job once it has per-ctor type info.

crates/ailang-codegen/src/lib.rs — AllocStrategy::Rc variant
added; fn_name() returns "ailang_rc_alloc". Single-line
extension because Iter 18a's bump path had already centralised
the allocator-symbol decision on fn_name() for all four
allocation sites.

crates/ail/src/main.rs — --alloc=rc accepted by Build/Run;
parse_alloc_strategy extended; locate_rc_runtime() helper
mirrors locate_bump_runtime; new Rc arm in build_to compiles
runtime/rc.c with clang -O2 -c and links the resulting .o
into the final binary (no -lgc).

E2E coverage: alloc_rc_produces_same_stdout_as_gc on
list.ail.json (42), alloc_rc_matches_gc_on_std_list_demo for
broader allocation-site coverage. Total e2e bundle: 51 tests
(was 49).

Hand-verified on:
  sum.ail.json --alloc=rc → 55
  list.ail.json --alloc=rc → 42
  borrow_own_demo.ail.json --alloc=rc → 3 then 6
  std_list_demo.ail.json --alloc=rc → matches --alloc=gc

cargo build/test --workspace green; git diff examples/ empty.
2026-05-08 02:13:08 +02:00
Brummel 65e280bb70 Bench: GC overhead via bump-allocator comparison
Adds --alloc=<gc|bump> to ail build/run. Bump path links a 256MB
no-free arena C stub instead of libgc; IR is byte-identical except
for the @GC_malloc → @bump_malloc symbol swap. Bench harness times
two allocation-heavy workloads (list cons/sum and balanced tree
build/walk) under both modes.

Numbers (RUNS=5, median of 4):
  bench_list_sum   gc 0.141s  bump 0.048s  +194%
  bench_tree_walk  gc 0.103s  bump 0.041s  +151%

Bucket: large. ~60% of runtime is Boehm on these workloads —
upper bound for any realistic program. Both fixtures hold the
heap fully live, so the cost we're seeing is Boehm's allocate
path itself, not collection work; that fact narrows the design
space for the GC discussion.

- crates/ailang-codegen: AllocStrategy enum, three callsites and
  the IR header parameterised.
- crates/ail/src/main.rs: --alloc flag plumbed; bump runtime
  located + compiled on demand.
- runtime/bump.c: 256MB static arena, abort-on-overflow.
- examples/bench_list_sum, bench_tree_walk: accumulator-form
  fixtures (textbook recursive sum was constructor-blocked).
- bench/run.sh: harness with Python timing helper (Arch's
  /usr/bin/time isn't part of the base install).

No language-level changes; default --alloc=gc, all 141 workspace
tests green, all 5 IR snapshots unchanged, 11 prior fixtures
produce identical stdout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:06:20 +02:00