LambLisp 01 Red Fox Alpha
From AI to Actuator
Loading...
Searching...
No Matches
LL_String Class Reference

Public Member Functions

 LL_String (const char *other)
 LL_String (const char *other, int n)
 LL_String (const LL_String &other)
void set (const char *s)
void set (const char *s, int n)
LL_String concat (const char *s1, const char *s2)
LL_String & append (const char *s1)
LL_String & append (char c)
LL_String substring (int start, int len_)
LL_String & operator= (const char *other)
LL_String & operator= (const LL_String &other)
bool operator== (const char *other)
bool operator!= (const char *other)
bool operator== (const LL_String &other)
bool operator!= (const LL_String &other)
char & operator[] (int ix)
LL_String operator+ (const char *other)
LL_String operator+ (const LL_String &other)
LL_String operator+ (char c)
LL_String & operator+= (const char *other)
LL_String & operator+= (const LL_String &other)
LL_String & operator+= (char c)
char * c_str () const
int length () const

Detailed Description

A small owning string. [B834] AMORTISED GROWTH – it used to reallocate and full-copy on EVERY append, which made the REPL's per-character input accumulation quadratic.

WHAT IT COST, measured before the change. ll_vm_term.cpp's commandLine += c runs once per RECEIVED CHARACTER, and append did strlen + new char[n0+n1+1] + copy-all + delete[] – so every byte of an input line cost a heap allocation and a full copy, O(n^2) per line. A 150-byte LLIP frame meant ~150 allocations and ~11,000 byte copies on a 240 MHz part. That is the ~100 B/s ceiling _w3_llip_stage_tests derives its timeouts from (r7rs-tests.scm: 904.7 s for 91,848 B) on a line whose wire rate is ~11.5 KB/s – under 1% of the link. push_file's comment named the symptom ("O(n) String rebuild per char") without naming this as the cause.

NOW: a stored length and capacity, geometric growth, in-place append. += is amortised O(1) and length() is O(1) instead of a strlen.

MEASURED, same benchmark against both classes (10 correctness arms + a growth curve): 20,000 single-char appends old 201 ms new 0.13 ms ~1,500x 10x the work old 41x time new 13x time (quadratic -> near-linear) The old timing was first measured as 0.17 ms, which is 8.5 ns per append and impossible for a heap allocation plus a full copy – the harness had compiled the NEW class twice, because a quoted #include searches the including file's own directory first. An impossible number is the only reason that was caught, so the arm now verifies which class it built before trusting the figure.

THREE THINGS THAT MADE THIS NOT A ONE-LINE CHANGE, each checked rather than assumed:

  1. ALIASING – AND THE OLD CODE WAS NOT SAFE HERE, WHICH I ASSUMED BEFORE TESTING IT. append WAS accidentally safe (it allocated a fresh buffer, then read the old one, then freed). But set did delete[] ptr FIRST and copied afterwards, so a source inside our own buffer was read AFTER being freed. Two arms fail against the old class, both use-after-free: a.set(a.c_str() + 6, 5) -> '' (self-set from own buffer) a = a -> '\xefq' (operator= forwards to set(other.ptr)) Both now pass: set detects a source within [ptr, ptr+len] and memmoves within the buffer without reallocating (no growth can be needed, since n <= len), append re-derives the source after growth, and operator= short-circuits on this == &other. So this change fixes two latent use-after-frees as well as the quadratic cost.
  2. operator[] RETURNS char&, so a caller writing '\0' into the middle used to truncate the string – length() was a strlen and would simply see less. With a stored length it would not, and the two would silently disagree. Checked before relying on it: NO LL_String subscript assignment exists anywhere in src/ (every hit is a raw char array – buf, _b, ll_lla_fmtbuf, line). If one is ever added, it must call set() instead.
  3. concat LEAKED, and still would have. It did char *s3 = new char[...]; ... return s3; – which converts through LL_String(const char*), COPIES s3 into a fresh buffer, and never frees s3. Every operator+ leaked its own result length. Now it builds the result in place.

A NOTE ON WHAT GOT SAFER RATHER THAN RISKIER. c_str() hands out ptr, and a caller caching it across a mutation was ALWAYS reading freed memory before (the buffer was replaced every time); now it survives every append that does not grow. That is strictly fewer dangling reads, not more – but it also means such a misuse would now be intermittent rather than constant, so do not cache c_str() across a mutation.

Member Function Documentation

◆ set()

void LL_String::set ( const char * s,
int n )
inline

< aliases us: no growth can be needed

◆ append() [1/2]

LL_String & LL_String::append ( const char * s1)
inline

< may move ptr; preserves [0,len]

◆ append() [2/2]

LL_String & LL_String::append ( char c)
inline

Appending ONE character is the hot path (the terminal's per-char input accumulation), so it does not route through the 2-byte-cstr + strlen detour the operator used to take.

◆ length()

int LL_String::length ( ) const
inline

O(1) since [B834]; was a strlen on every call.