v35.1.0 — FJARR_LEAK Phase 1 + Phase 18 CALL_INDEX + T4 dup-fn
[v35.1.0] — 2026-05-08 🎯 FJARR_LEAK Phase 1 — _FjArr realloc-leak class CLOSED — minor bump
The residual _FjArr heap-leak class documented in docs/FJARR_LEAK_B0_FINDINGS.md
is closed. Every [i64] / [str] value in fj-emitted code previously leaked
88 bytes per array (24-byte struct + 64-byte initial buffer) through
malloc(sizeof(_FjArr)) and realloc(...) calls in stdlib/codegen.fj's
_fj_arr_new / _fj_arr_grow. B0 measured this at 2.73 MB / 53,818 blocks
per fjc-stage1 self-compile run (54,125 allocs vs 307 frees, 176:1 leak
ratio, exactly linear at 88 bytes × N arrays). Phase 1 / Strategy A migrates
both functions to the existing R15 arena (_fj_arena_alloc, freed at exit
via atexit(_fj_arena_free_all)); valgrind now reports
definitely lost: 0 AND indirectly lost: 0 for the regression baseline.
The fix in 6 lines (stdlib/codegen.fj emit_preamble)
_fj_arr_new:malloc(sizeof(_FjArr))→_fj_arena_alloc(sizeof(_FjArr))._fj_arr_grow:realloc(a->data, ...)replaced with copy-grow:size_t new_cap = a->cap == 0 ? 8 : a->cap * 2; void** new_data = (void**)_fj_arena_alloc(new_cap * sizeof(void*)); if (a->data) memcpy(new_data, a->data, a->len * sizeof(void*)); a->data = new_data; a->cap = new_cap;
- Doubling-cap strategy preserved → amortized O(1) push, identical asymptotic
shape. Old buffer slot is abandoned to the arena (reaped at process exit
via the existing_fj_arena_free_allatexit handler).
Stage 2 byte-equality preserved (no md5 rebase)
Decision file claim "Phase 1 (A) is text-only and deterministic → preserves
byte-equality (md5 unchanged)" VINDICATED. phase17_stage2_native_triple_test
4/4 PASS (~112s) post-commit; both Stage 1 and Stage 2 emit the same new
arena-using preamble text; the test compares stage1.c == stage2.c directly
without any hardcoded md5 constants.
Honest scope (per CLAUDE.md §6.6 R3)
- ✅ Default fj-source array allocations (
[1, 2, 3],["a", "b"], push chains, fn returns of[T]): 0 bytes definitely+indirectly lost. ⚠️ Heap-still-heap caveat: the arena IS heap memory, just freed at process exit. Compass §4.1 (@kernel must reject heap at compile time) is not yet satisfied. Phase 1 is "compatible-by-deferral" — a future @kernel mode forbids_fj_arr_newcalls entirely (user code uses[T; N]fixed arrays).⚠️ Long-running embedded consumer (STM32N6 / Cortex-M55 niche): between v35.1.0 and v36.x, arena retention grows monotonically until process exit. Mitigation today: use[T; N]fixed-size arrays in firmware loops; reserve dynamic[T]for setup / one-shot tasks.- ⏸️ Phase 2 (Strategy D / linear-types-lite — affine
[T], SE017 UseAfterMove,.clone()builtin, codegen emitsfreeat last-use) deferred to v36.x roadmap. ~14h estimate per FJARR_LEAK_PLAN §5. One-way-door per decision file §Reverse-cost; deliberately not auto-chained.
Added
tests/selfhost_fjarr_leak_baseline.rs(NEW) —_FjArrleak regression
GREEN gate. Compilesfn main() { let v: [i64] = [1, 2, 3]; ... }via
cargo run -- run --emit-c, links withgcc -O0, runs under
valgrind --leak-check=full, assertsdefinitely_lost + indirectly_lost == 0.
Auto-skips whenvalgrindbinary absent (macOS / sandbox CI runners) — no
false-RED. Default-#[ignore]d (chain + gcc + valgrind ~30-50s); pre-push
hook + per-PR CI gate exercise via--include-ignored.scripts/check_decision_file.sh(NEW) — structural validator for
docs/decisions/*.md. Greps for required headers per FJARR_LEAK_PLAN §1.3:
Choice / Rationale / @kernel-future-compat / Migration path / Surprise budget
/ Rejected candidates / Reverse-cost. Exit 0 → pass; exit 1 → missing
required header. Used by pre-commit hook + future decision-file gates.scripts/git-hooks/pre-commitFJARR_LEAK gate — detects
+...(_fj_arr_new|_fj_arr_grow|emit_preamble.*malloc|emit_preamble.*realloc)
in staged diff; requiresdocs/decisions/2026-05-07-fjarr-leak-strategy.md
to exist AND passscripts/check_decision_file.sh. Blocks ad-hoc reversal
of the arena strategy without a follow-up decision-file amendment. Fired
correctly during the 18.A.1+A.2 commit, validating end-to-end.docs/FJARR_LEAK_PHASE_1_FINDINGS.md(NEW) — Phase 1 closure findings
doc. §0 B0 recap, §1 decision recap, §2 sub-task closure (B0 → 18.0.2 →
18.0.4 → 18.A.1+A.2 → 18.Z.*), §3 test additions, §4 effort recap (~3.5h
actual vs 7.5h ceiling, -53%), §5 prevention layer, §6 honest scope,
§7 cumulative state, §8 decision gate.
Changed
stdlib/codegen.fjemit_preamble_fj_arr_new+_fj_arr_grow
(lines ~388–401): malloc/realloc → arena copy-grow per §The fix in 6 lines.
+15 / -5 LOC text-only change. Stage 2 byte-equality preserved.tests/selfhost_fjarr_leak_baseline.rsassertion flipped from
assert!(lost >= 88)(RED baseline per commitf13ac484) to
assert_eq!(lost, 0)(GREEN gate). RemovedMIN_LEAK_BYTES_PRE_FIX
constant. Doc-comment rewritten as lifecycle-history form (pre-18.A.1 vs
post-18.A.1 expected behavior). Parser fix: when valgrind reports
"All heap blocks were freed", per-classdefinitely lost:/
indirectly lost:lines are OMITTED —parse_valgrind_lostnow checks
forHEAP SUMMARY:marker and defaults to 0 when class lines absent.
Missing HEAP SUMMARY → panic (valgrind didn't run cleanly).
Stats
- Self-host tests: 101 → 102 (+1
fjarr_leak_baseline_minimal_array) - Stage1-full: 86 (unchanged) | phase17_self_compile: 4/4 (unchanged, byte-equality preserved)
- Per-fjc-self-compile leak: 2.73 MB → 0 bytes definitely+indirectly lost ✅
- Heap-leak classes closed: R15 string-arena → +1
_FjArrrealloc (88 bytes/array → 0) - Pre-commit gates: + FJARR_LEAK decision-file gate (script + hook)
- Cumulative effort v33.4.0..v35.1.0: ~38h Claude time across 24 self-host phases (0..18 + FJARR_LEAK Phase 1)
Source of truth
docs/FJARR_LEAK_PHASE_1_FINDINGS.md— Phase 1 closure (this release)docs/FJARR_LEAK_B0_FINDINGS.md— B0 pre-flight evidence (commitf5448b03)docs/decisions/2026-05-07-fjarr-leak-strategy.md— Choice F (A-now + D-Phase-19)docs/FJARR_LEAK_PLAN.md— full plan w/ 5 strategy candidates + risk register
Full Changelog: v35.0.0...v35.1.0