Commit Graph

7 Commits

Author SHA1 Message Date
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