mirror of
https://github.com/fluffos/fluffos
synced 2026-08-12 18:26:06 -04:00
Propagate the ASCII string tag across concatenation
Each concatenation produced a fresh, untagged string, so a loop of `s += x` with an accompanying sizeof(s) re-derived the tag from scratch for every intermediate -- O(n^2) in scanning on top of the O(n^2) the copying already costs. All three concat macros now carry the tag through: ascii(a + b) == ascii(a) && ascii(b), computed from the operands before the result is built. That identity is exact only because the predicate excludes CR: with no CR on either side no CR-LF (one cluster, UAX #29 GB3) can form across the seam. It is exact in the NO direction too -- a non-ASCII operand's bytes survive into the result -- so neither case ever rescans the joined string. The third macro, SVALUE_STRING_ADD_LEFT (f_add's int/float/object + string paths), needs the same treatment as the two string+string ones. Note the operand there is a raw stack buffer rather than an svalue, so it is queried with counted=false: it has no block header to read a cached tag out of, and treating it as though it did would be a wild read. Pinned by regression tests that tag "a\r" and "\nb" separately and then join them: a propagated "both ASCII" would report 4 clusters where the CRLF makes 3. Covered on both the string+string path and the <non-string>+string one, the latter with the left operand held in a variable so the expression cannot be constant-folded before f_add runs. Verified to FAIL (2 checks) with the propagation forced to "ASCII". append+sizeof loop, 4000 iters 875,913 ns -> 548,144 ns The remainder is the string building itself, which is inherently quadratic in copying. Validated: LPC testsuite 3x, 334/334 GTest, Debug+ASan/UBSan with the DEBUGMALLOC ref-count checker, RelWithDebInfo+ASan/UBSan -- all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MN9sz4nvGgBZw9oZiei3XR
This commit is contained in:
parent
547ee319da
commit
f98d09bab7
2 changed files with 53 additions and 0 deletions
|
|
@ -157,6 +157,23 @@ inline void free_svalue_maybe_refed(svalue_t* v) {
|
|||
// commonly used svalue.
|
||||
extern svalue_t const0, const1, const0u;
|
||||
|
||||
/* Is this string svalue pure ASCII (and CR-free), i.e. byte offset == grapheme
|
||||
* cluster index? Kept as a macro rather than an inline function so it expands
|
||||
* where SVALUE_STRLEN/STRING_COUNTED are already visible. */
|
||||
#define SVALUE_STR_ASCII(sv) \
|
||||
u8_string_is_ascii_cached((sv)->u.string, SVALUE_STRLEN(sv), ((sv)->subtype & STRING_COUNTED) != 0)
|
||||
|
||||
/* Tag a freshly built concatenation. ascii(a + b) == ascii(a) && ascii(b) is
|
||||
* exact ONLY because the ASCII predicate excludes CR: with no CR on either
|
||||
* side, no CR-LF (one cluster, UAX #29 GB3) can form across the seam. If
|
||||
* either side is non-ASCII its bytes survive into the result, so NO likewise
|
||||
* propagates exactly -- neither case needs to rescan the joined string.
|
||||
*
|
||||
* Without this, `s += x` in a loop re-derives the tag from scratch for each
|
||||
* intermediate string, making an accompanying sizeof(s) O(n^2). */
|
||||
#define MSTR_TAG_JOIN(res, ascii_both) \
|
||||
(MSTR_ASCII(res) = (ascii_both) ? MSTR_ASCII_YES : MSTR_ASCII_NO)
|
||||
|
||||
/* These are not used anywhere */
|
||||
|
||||
/* Beek - add some sanity to joining strings */
|
||||
|
|
@ -169,6 +186,7 @@ extern svalue_t const0, const1, const0u;
|
|||
int ess_r; \
|
||||
ess_len = (ess_r = SVALUE_STRLEN(x)) + strlen(y); \
|
||||
if (ess_len > max_string_length) error("Maximum string length exceeded in concatenation.\n"); \
|
||||
bool ess_ascii = SVALUE_STR_ASCII(x) && u8_string_is_ascii_cached((y), ess_len - ess_r, false);\
|
||||
if ((x)->subtype == STRING_MALLOC && MSTR_REF((x)->u.string) == 1) { \
|
||||
ess_res = (char*)extend_string((x)->u.string, ess_len); \
|
||||
if (!ess_res) fatal("Out of memory!\n"); \
|
||||
|
|
@ -181,6 +199,7 @@ extern svalue_t const0, const1, const0u;
|
|||
(x)->subtype = STRING_MALLOC; \
|
||||
} \
|
||||
(x)->u.string = ess_res; \
|
||||
MSTR_TAG_JOIN(ess_res, ess_ascii); \
|
||||
})
|
||||
|
||||
/* <something that needs no free> + string svalue */
|
||||
|
|
@ -192,6 +211,7 @@ extern svalue_t const0, const1, const0u;
|
|||
int pss_len; \
|
||||
pss_len = SVALUE_STRLEN(sp) + (pss_r = strlen(y)); \
|
||||
if (pss_len > max_string_length) error("Maximum string length exceeded in concatenation.\n"); \
|
||||
bool pss_ascii = SVALUE_STR_ASCII(sp) && u8_string_is_ascii_cached((y), pss_r, false); \
|
||||
pss_res = new_string(pss_len, z); \
|
||||
strcpy(pss_res, y); \
|
||||
strcpy(pss_res + pss_r, sp->u.string); \
|
||||
|
|
@ -199,6 +219,7 @@ extern svalue_t const0, const1, const0u;
|
|||
sp->type = T_STRING; \
|
||||
sp->u.string = pss_res; \
|
||||
sp->subtype = STRING_MALLOC; \
|
||||
MSTR_TAG_JOIN(pss_res, pss_ascii); \
|
||||
})
|
||||
|
||||
/* basically, string + string; faster than using extend b/c of SVALUE_STRLEN */
|
||||
|
|
@ -211,6 +232,7 @@ extern svalue_t const0, const1, const0u;
|
|||
ssj_r = SVALUE_STRLEN(x); \
|
||||
ssj_len = ssj_r + SVALUE_STRLEN(y); \
|
||||
if (ssj_len > max_string_length) error("Maximum string length exceeded in concatenation.\n"); \
|
||||
bool ssj_ascii = SVALUE_STR_ASCII(x) && SVALUE_STR_ASCII(y); \
|
||||
if ((x)->subtype == STRING_MALLOC && MSTR_REF((x)->u.string) == 1) { \
|
||||
ssj_res = (char*)extend_string((x)->u.string, ssj_len); \
|
||||
if (!ssj_res) fatal("Out of memory!\n"); \
|
||||
|
|
@ -225,6 +247,7 @@ extern svalue_t const0, const1, const0u;
|
|||
(x)->subtype = STRING_MALLOC; \
|
||||
} \
|
||||
(x)->u.string = ssj_res; \
|
||||
MSTR_TAG_JOIN(ssj_res, ssj_ascii); \
|
||||
})
|
||||
|
||||
// Translate svalue into json summary, only suitable for
|
||||
|
|
|
|||
|
|
@ -49,6 +49,36 @@ void do_tests() {
|
|||
s += wide; // grows in place
|
||||
ASSERT_EQ(7, sizeof(s)); // 5 + 2 clusters, NOT 5 + 6 bytes
|
||||
|
||||
// --- the CR-LF seam: concatenation propagates the tag as
|
||||
// ascii(a+b) == ascii(a) && ascii(b), which is only exact because a CR
|
||||
// disqualifies a string outright. Joining "...\r" with "\n..." forms one
|
||||
// CRLF cluster ACROSS the seam, so a propagated "both ASCII" would be a
|
||||
// wrong answer here -- both sides must already be tagged non-ASCII. ---
|
||||
s = "a\r";
|
||||
ASSERT_EQ(2, sizeof(s)); // tag it before joining
|
||||
s += "\nb";
|
||||
ASSERT_EQ(3, sizeof(s)); // 'a', CRLF, 'b' -- not 4
|
||||
|
||||
// --- <non-string> + string takes a THIRD concat path in f_add
|
||||
// (SVALUE_STRING_ADD_LEFT, for int/float/object on the left), which builds
|
||||
// the result fresh instead of extending either operand. The left operand is
|
||||
// held in a variable so the expression cannot be constant-folded away before
|
||||
// f_add ever runs.
|
||||
//
|
||||
// Cluster counts here are correct whether or not the tag is propagated -- an
|
||||
// untagged result just gets rescanned. What this pins is the direction that
|
||||
// would NOT be self-correcting: propagating "ASCII" across a seam that forms
|
||||
// a CRLF, which would answer from the byte length and be wrong. ---
|
||||
int n = 12;
|
||||
s = n + "abc";
|
||||
ASSERT_EQ("12abc", s);
|
||||
ASSERT_EQ(5, sizeof(s));
|
||||
s = n + wide; // non-ASCII on the right must not come back tagged ASCII
|
||||
ASSERT_EQ(2 + sizeof(wide), sizeof(s));
|
||||
n = 1;
|
||||
s = n + "\r\nb"; // '1', CRLF, 'b' -- a propagated "ASCII" would say 4
|
||||
ASSERT_EQ(3, sizeof(s));
|
||||
|
||||
// --- non-ASCII, then reduced back to ASCII ---
|
||||
s = wide + "xy";
|
||||
ASSERT_EQ(4, sizeof(s));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue