Correctness and stability fixes from the maturity audit (findings #1, #2, #3, #6) - #12
Merged
Conversation
…cies, and antipatterns 39 findings across four tiers (correctness/safety, performance, redundancy, hygiene) with impact and difficulty estimates, pinned to v0.5.0 / main @ 3ebb79d. This branch tracks the maturity work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MCP patch_symbol tool rewrites whole source files through PrettyExpr, so any printed construct the parser cannot re-read silently corrupted patched files. Fixed (verified against the lexer/parser): - DeBruijn printed `D0` instead of `^0` (hit every bound variable) - ju arg order was flipped (shape before expr) - Panic/Expand glyphs were swapped (`!` vs `\``) - Eq printed `==`, Gt printed `>` (which lexes as Move) - export prefix printed `*` (Mul) instead of `X ` - expand params printed `!name` instead of `name\`` - format_token emitted `???` for BitAnd/BitOr/BitXor - Import emitted a trailing arity the parser rejects - whole floats printed without `.0` and re-lexed as Integer - string escaping missed backslash/newline/tab/CR Also corrects the inverted `!`/`\`` doc-comments in ast/mod.rs that display.rs had been written against, and adds an 11-test parse->print->parse round-trip suite covering every construct plus real files from tests/lang/. Resolves maturity report finding #1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The codebase index kept three name-keyed maps that analyze_path only
ever appended to: every re-analysis (including the one patch_symbol
triggers after each write) duplicated fingerprint entries, and
renamed or deleted symbols persisted forever. Names were also global,
so patch_symbol("main") silently rewrote whichever file last won the
index — observed corrupting otel_async_test.llm during the display.rs
validation.
- Restructure the index: call_graph/fingerprints side maps folded
into per-symbol metadata, keyed name -> one entry per defining
file; re-indexing a file purges its old entries first, so stale or
duplicate data is impossible by design
- patch_symbol now rejects ambiguous names with the candidate file
list and accepts an optional 'path' argument to disambiguate
- Replace the unwrap-based trailing-newline append (a server-killing
panic under panic=abort if the file races away) with a single write
- run_semantic_patch.py asserts the ambiguity rejection and the
disambiguated patch, and now runs in CI on both platforms
Resolves maturity report finding #6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fgets over a fresh fdopen per call had three defects: the FILE* was never fclosed (one leak per read), its readahead buffered up to 4096 bytes and dropped everything after the first line when the stream was abandoned (subsequent reads saw EOF), and a NULL fdopen return went straight into fgets (crash on a bad fd). Replace the raw-fd path with a byte-at-a-time read() loop (EINTR retried): nothing to leak, nothing consumed past the newline — consecutive reads return consecutive lines — and a bad fd returns 0. The managed LlmFile path already cached its FILE* and is unchanged. Adds tests/lang/stdin_multiline_test.llm (three piped lines + EOF), run by llm-test with piped stdin; it fails with 'line 2 mismatch' against the old code and passes now. Resolves maturity report finding #2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
http_live_test.llm and https_json_test.llm called httpbin.org, so any outage of that service turned CI red (observed 2026-07-17: both tests failing with 503s on every platform while the rest of the suite was green). The llm-test mock server now emulates the two httpbin behaviors the tests need — /json/get returns a JSON body echoing the request URL, /json/post echoes a form-encoded body back as a JSON 'form' object — on paths distinct from the plain-text /get and /post that http_test.llm asserts. Both tests now target the mock; the full suite passes with httpbin.org unreachable (verified during the outage). TLS client coverage is unchanged: it lives in https_test.llm / run_https_test.sh, not these tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ref_cnt was a plain unsigned short mutated with ++/--, while llm_fork
hands the same managed pointers to pool threads: racing dup/drop lost
updates, freeing objects that still had references (use-after-free),
and the 16-bit counter could wrap.
- ref_cnt is now _Atomic unsigned int: dup uses fetch_add (relaxed),
drop uses fetch_sub (acq_rel) and only the releaser of the last
reference destroys the object
- LlmRtHeader reordered to {magic: u32, type: u32, ref_cnt: u32} —
three 4-byte fields, no padding — and gen_string_constant, which
materializes this header for string constants, updated in lockstep
({i32,i32,i32} instead of {i32,i16,i16})
- new tests/runtime/refcount_race_test.c (8 threads x 200k balanced
dup/drop, wired into llm-test): fails against the old code with
'object was freed while references remained', passes now
- llm-clang now rebuilds runtime objects when common.h is newer than
the object, so header layout changes cannot silently link stale
objects built against the old struct
Resolves maturity report finding #3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mach-O ld does not resolve undefined weak references to NULL the way GNU ld does, so the standalone test link failed on the runtime's weak drop hooks. Provide no-op stubs — the test only allocates strings, so they are never invoked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
paulprogrammer
added a commit
that referenced
this pull request
Jul 17, 2026
- #12: trap/parallel IDs from a monotonic counter instead of get_functions().count() per site (O(n^2) codegen on trap-heavy code) - #13: module name and mangle prefix computed once in CodeGen::new instead of re-parsing/re-hashing on every identifier resolution - #18: pthread_once curl_global_init shared by http.c/http_server.c, removing the lazy per-easy init race across pool threads - #22: rust-cache + concurrency groups in ci.yml and release.yml - #36: set -eo pipefail in llm-clang/llm-test; runtime compile and ar failures abort with a message instead of linking stale objects; brew --prefix instead of hardcoded Homebrew paths; libllm_opencl.so copied only when stale; llm-test fails fast with diagnostics when the mock server cannot bind port 8080 Validated: cargo test (47 passed), llm-test (25 passed, fresh runtime objects), run_semantic_patch.py, and a forced port-8080 conflict now exits 1 with the bind error surfaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First tranche of fixes from the codebase maturity audit (
maturity_report.md, included in this PR with per-finding status entries). Covers the report's recommended items 1 and 2: the pretty-printer round-trip fixes and the runtime safety trio, plus a CI-flakiness fix that the work surfaced.Finding #1 —
PrettyExpremitted source the parser cannot re-read (fec93f0)The MCP
patch_symboltool rewrites whole files throughPrettyExpr, so every printed construct that couldn't re-parse silently corrupted patched files. Fixed all 8 audited bugs (DeBruijnD0→^0,juarg order, swapped!/`,Eq/Gtglyphs, export prefix, expand params) plus 4 more found while writing tests (bitwise ops printed???,Importemitted a trailing arity, whole floats re-lexed as integers, string escaping missed\/\n/\t/\r). Adds an 11-test parse→print→parse round-trip suite; corrects the inverted doc-comments inast/mod.rsthat caused the bug.Finding #6 — MCP index corruption and wrong-file patching (
7afff91)Re-analysis appended to the fingerprint index without clearing (duplicates on every patch; deleted symbols persisted), the trailing-newline write could abort the server via bare
.unwrap(), andpatch_symbol("main")silently rewrote whichever file last won the name-keyed index. The index is now per-file and purged on re-analysis (staleness impossible by design), ambiguous names are rejected with the candidate list, and a new optionalpathargument disambiguates.run_semantic_patch.pyasserts both behaviors and now runs in CI on both platforms.Finding #2 —
llm_readraw-fd path: leak, data loss, null-deref (88394b1)A fresh
fdopenper read leaked theFILE*and its read-ahead swallowed every line after the first; NULL return crashed. Now a byte-at-a-timeread()loop: nothing leaks, consecutive reads return consecutive lines, bad fds return 0. Regression test reads three piped lines + EOF; verified to fail against the old code.Finding #3 — non-atomic refcounts (
487633f,c697784)ref_cntwas a plainunsigned shortmutated with++/--whilellm_forkshares managed pointers across pool threads: lost updates → premature free → use-after-free. Now_Atomic unsigned int(fetch_add/fetch_sub, acq_rel on release), withLlmRtHeaderreordered to three padding-free u32 fields andgen_string_constantupdated in lockstep. New 8-thread × 200k dup/drop race test (fails against old code, passes now, both platforms). Also:llm-clangnow rebuilds runtime objects whencommon.hchanges instead of silently linking stale layouts.Test flakiness — httpbin.org dependence removed (
386de70)http_live_test.llmandhttps_json_test.llmcalled httpbin.org, whose outage turned CI red. Thellm-testmock server now provides httpbin-style JSON echo endpoints (/json/get,/json/post) and both tests target it; verified green during the outage.Testing
cargo test: 67 passing (includes 15 new round-trip/index tests)./llm-test: 25 passing, 0 failed (includes new stdin, refcount-race, and mocked HTTP tests); no live-internet dependence remains in the default loopThe
maturity-workbranch stays open — remaining findings continue there.🤖 Generated with Claude Code