perf: replace O(n²) String.append loops with StringBuf#1
Conversation
join-children, join-segments, and escape-string built strings byte-by-byte using String.append, causing quadratic allocation for large ASTs and strings. Switch to StringBuf for amortised O(n) appends. Adds strbuf@0.1.0 dependency.
There was a problem hiding this comment.
Build & Tests
Build: pass (ARM Linux, local).
Tests: all 50 pass — round-trip assertions for integers, floats, strings, symbols, arrays, dicts, reader macros, comments, positions, and edge cases all green.
CI: Both macOS and Ubuntu pass (tests, lint, format check, docs).
Findings
1. Correctness: all three replacements are clean (no issues)
Traced through each function:
join-children:StringBuf.create+ loop withappend-strfor space separator and childForm.str. Semantically identical to the oldString.appendloop. Thewhen (Int.> i 0)guard correctly skips the leading separator.join-segments: Same pattern with.separator. Identical semantics.escape-string: Thewhile-doloop with CRLF detection is preserved exactly.StringBuf.append-strreplacesset! out (String.append ...)in both branches. The byte-walking logic andescape-string-bytecalls are untouched.
2. StringBuf API usage is correct
StringBuf.append-str takes (&StringBuf, &String) and mutates in-place. StringBuf.to-string &sb returns an owned String and resets the buffer; sb is then dropped at scope end. No ownership issues.
3. CHANGELOG updated
The Unreleased section correctly documents the change and the new strbuf@0.1.0 dependency.
4. No edge cases found
Empty inputs (empty child list, empty segment list, empty string) all work correctly — the loops simply don't execute, and StringBuf.to-string on a fresh buffer returns "".
Verdict: merge
Mechanical O(n²) → O(n) replacement. Correct, well-tested, CHANGELOG updated. No issues found.
Summary
join-children,join-segments, andescape-stringinFormall built strings usingset! out (String.append &out ...)inside loops. EachString.appendallocates a new string and copies the accumulated content, making these O(n²) in the output length. For large ASTs or long strings (e.g. formatter/linter processing big files), this causes quadratic allocation.This PR replaces all three loops with
StringBuffromstrbuf@0.1.0, which grows geometrically and gives amortised O(1) appends — making the overall functions O(n).Changes
strbuf@0.1.0dependencyjoin-children:StringBuf.create+append-strper child,to-stringat endjoin-segments: same pattern with.separatorescape-string: same pattern, byte-walking logic unchangedNo behavioural changes — all existing round-trip tests pass identically.
Test plan
test/carp-reader.carp) passes — all round-trip assertions exerciseForm.strwhich calls these three functionscarp-fmt/angleron this fileOpened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.