mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Relational string operators (`<`, `<=`, `>`, `>=`) compared the UTF-8
bytes, which is code-point order. Node compares UTF-16 code units, so the
results differed for characters above U+FFFF against U+E000..U+FFFF and
for lone surrogates against surrogate pairs. The default `sort()`
comparator, `util.inspect` with `sorted: true` and dynamic comparisons
already used UTF-16 order; the default comparator of a dynamic array's
`sort()` did not.
Typed comparisons and the dynamic array sort now use `scr_str_cmp_u16`,
and the IR's `strCmp` node no longer has a `utf16` flag. The two orders
agree unless both of the first differing bytes are at least 0xED, so:
- the inline wrapper decides differing first words, or last words of
equal lengths up to 16 bytes, in byte order when neither word has a
byte of at least 0xED (a SWAR test). Other differing words, and the
first bytes of shorter strings, decide by their first differing byte:
in byte order when one of the two is below 0xED, and by plane when both
are lead bytes from 0xEE to 0xF4 (a supplementary character orders
below U+E000..U+FFFF);
- otherwise a proven-ASCII string on either side compares with `memcmp`
(`scr_str_cmp`), and other pairs call `scr_str_cmp_u16_bytes`. It finds
the first differing byte with words up to byte 32, then with 32-byte
SSE2 or NEON blocks where the target has them. The call is marked as
only reading its arguments, so repeated comparisons of one pair merge.
- `scr_str_cmp_u16` itself takes the same `memcmp` path for proven-ASCII
strings.
The native compiler sorts record fields with `<` and checks the order
with `sort()`, so it reported SC9001 for 5888-inspect-sorted.ts, whose
object has the keys "\u{1F600}" and "\uffff". 5888 runs in the
native-frontend self-host check. Adds corpus 6023.
309 lines
14 KiB
C
309 lines
14 KiB
C
/* scriptc runtime — strings: layout, lone surrogates (WTF-8), concatenation
|
|
* and comparison (scr_string.c). Included by scr_runtime.h, which says where
|
|
* new declarations go. */
|
|
#ifndef SCR_STRING_H
|
|
#define SCR_STRING_H
|
|
|
|
#include "scr_rc.h"
|
|
|
|
/* ── strings ──────────────────────────────────────────────────────────
|
|
* UTF-8 bytes, refcounted, immutable. rc == SIZE_MAX marks an immortal
|
|
* interned literal (emitted as a static object; retain/release are no-ops).
|
|
* data is NUL-terminated for C convenience; len excludes the NUL.
|
|
*
|
|
* cap is the usable byte capacity of data[] excluding the NUL (allocation
|
|
* is sizeof(ScrStr) + cap + 1); cap == len for interned literals and plain
|
|
* allocations. Builders and concat results may have spare capacity so
|
|
* scr_str_concat can append in place when the left operand is uniquely
|
|
* owned (rc == 1) — observable immutability is preserved: a string with
|
|
* rc > 1 or rc == SIZE_MAX is never mutated.
|
|
*
|
|
* On 64-bit little-endian targets the capacity word is split: the low half
|
|
* is cap (strings never exceed SCR_STR_MAX_CAP bytes; JavaScript strings are
|
|
* far shorter) and the high half caches the string's Map key hash (0 = not
|
|
* computed). Emitted literals and runtime statics still initialize one
|
|
* size-sized word to len, which leaves the hash half zero. Only uniquely
|
|
* owned heap strings are ever mutated, and every such path clears the hash
|
|
* (scr_str_hash_forget); immortal strings are never written, so they always
|
|
* hash on demand.
|
|
*
|
|
* Bit 31 of the capacity word (on every target) is the proven-ASCII fact:
|
|
* when set, every byte is below 0x80, so UTF-16 indices equal byte offsets
|
|
* and .length == len. A clear bit means "not known", never "non-ASCII":
|
|
* constructors set it only where it is cheap, and string reads that prove
|
|
* ASCII set it later. Capacity therefore stops at 2^31 - 1 bytes, which
|
|
* still holds every string V8 can represent (2^29 - 24 UTF-16 units, at
|
|
* most three UTF-8 bytes each). Generated code reads the bit for inline
|
|
* charCodeAt/length (llvm/strings.ts); emitted literals and runtime
|
|
* statics set it in their capacity word. scr_str_hash_forget clears it
|
|
* with the hash: every in-place mutation already forgets cached facts.
|
|
*/
|
|
#if SIZE_MAX > UINT32_MAX && defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
|
#define SCR_STR_HASH_CACHE 1
|
|
#else
|
|
#define SCR_STR_HASH_CACHE 0
|
|
#endif
|
|
#define SCR_STR_MAX_CAP ((size_t)0x7fffffff)
|
|
typedef struct ScrStr {
|
|
size_t rc;
|
|
size_t len;
|
|
#if SCR_STR_HASH_CACHE
|
|
uint32_t cap : 31;
|
|
uint32_t ascii : 1;
|
|
uint32_t hash;
|
|
#else
|
|
size_t cap : 31;
|
|
size_t ascii : 1;
|
|
#endif
|
|
char data[];
|
|
} ScrStr;
|
|
|
|
/* The capacity word of a static string: cap == len, the proven-ASCII bit,
|
|
* plus the precomputed 32-bit key hash where the header caches one (0
|
|
* leaves it to be computed). */
|
|
#define SCR_STR_ASCII_BIT ((size_t)1 << 31)
|
|
#if SCR_STR_HASH_CACHE
|
|
#define SCR_STR_CAP_WORD(len, hash32, ascii) \
|
|
((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0) | \
|
|
((size_t)(uint32_t)(hash32) << 32))
|
|
#else
|
|
#define SCR_STR_CAP_WORD(len, hash32, ascii) ((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0))
|
|
#endif
|
|
|
|
void scr_str_hash_prime(ScrStr *s); /* scr_map.c; before a string becomes immortal */
|
|
/* Forget every cached content fact (key hash, proven ASCII) before a
|
|
* uniquely owned string's bytes change. */
|
|
static inline void scr_str_hash_forget(ScrStr *s) {
|
|
#if SCR_STR_HASH_CACHE
|
|
s->hash = 0;
|
|
#endif
|
|
s->ascii = 0;
|
|
}
|
|
|
|
/* True only when every byte is proven below 0x80 (see above). */
|
|
static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; }
|
|
|
|
/* Record a proven-ASCII fact. Immortal strings may live in read-only
|
|
* memory and are shared across threads, so they are never written. */
|
|
static inline void scr_str_note_ascii(ScrStr *s) {
|
|
if (s->rc != SIZE_MAX) s->ascii = 1;
|
|
}
|
|
|
|
/* memcpy for the short, variable-length copies of string construction
|
|
* (identifiers, keys, number text): fixed-size overlapping moves instead of
|
|
* a libc call. Longer spans keep the platform's bulk copy. */
|
|
static inline void scr_copy_bytes(char *dst, const char *src, size_t n) {
|
|
if (n <= 16) {
|
|
if (n >= 8) {
|
|
uint64_t a, b;
|
|
memcpy(&a, src, 8);
|
|
memcpy(&b, src + n - 8, 8);
|
|
memcpy(dst, &a, 8);
|
|
memcpy(dst + n - 8, &b, 8);
|
|
} else if (n >= 4) {
|
|
uint32_t a, b;
|
|
memcpy(&a, src, 4);
|
|
memcpy(&b, src + n - 4, 4);
|
|
memcpy(dst, &a, 4);
|
|
memcpy(dst + n - 4, &b, 4);
|
|
} else if (n) {
|
|
dst[0] = src[0];
|
|
dst[n / 2] = src[n / 2];
|
|
dst[n - 1] = src[n - 1];
|
|
}
|
|
return;
|
|
}
|
|
memcpy(dst, src, n);
|
|
}
|
|
|
|
/* Whether a byte span is all ASCII: 32 bytes per step, stopping at the
|
|
* first chunk with a high bit. */
|
|
static inline bool scr_bytes_all_ascii(const char *p, size_t n) {
|
|
const uint64_t high = UINT64_C(0x8080808080808080);
|
|
size_t i = 0;
|
|
for (; n - i >= 32; i += 32) {
|
|
uint64_t w[4];
|
|
memcpy(w, p + i, sizeof w);
|
|
if ((w[0] | w[1] | w[2] | w[3]) & high) return false;
|
|
}
|
|
uint64_t acc = 0;
|
|
for (; n - i >= 8; i += 8) {
|
|
uint64_t w;
|
|
memcpy(&w, p + i, 8);
|
|
acc |= w;
|
|
}
|
|
for (; i < n; i++) acc |= (unsigned char)p[i];
|
|
return (acc & high) == 0;
|
|
}
|
|
|
|
/* Length of a byte span's ASCII prefix: 32 bytes per step through long
|
|
* ASCII runs, then the chunk holding a high bit word by word. */
|
|
static inline size_t scr_bytes_ascii_span(const char *p, size_t n) {
|
|
const uint64_t high = UINT64_C(0x8080808080808080);
|
|
size_t i = 0;
|
|
while (n - i >= 32) {
|
|
uint64_t w[4];
|
|
memcpy(w, p + i, sizeof w);
|
|
if ((w[0] | w[1] | w[2] | w[3]) & high) break;
|
|
i += 32;
|
|
}
|
|
while (n - i >= 8) {
|
|
uint64_t w;
|
|
memcpy(&w, p + i, 8);
|
|
if (w & high) break;
|
|
i += 8;
|
|
}
|
|
while (i < n && (unsigned char)p[i] < 0x80) i++;
|
|
return i;
|
|
}
|
|
|
|
ScrStr *scr_str_new(const char *bytes, size_t len); /* returns +1 */
|
|
/* Native callback boundary: copy and WHATWG-decode a UTF-8 span, replacing
|
|
* malformed subsequences with U+FFFD. NULL with len == 0 is an empty span. */
|
|
ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len); /* +1 */
|
|
/* The same decode in place for bytes read straight into a uniquely owned
|
|
* raw string (file contents) whose first `ascii` bytes the caller already
|
|
* proved ASCII (0 when unknown): consumes `raw`, returns +1 (`raw` itself
|
|
* when it is well formed). */
|
|
ScrStr *scr_str_adopt_utf8_lossy(ScrStr *raw, size_t ascii);
|
|
/* A runtime encoding name Node decodes as UTF-8 ("utf8"/"utf-8", any
|
|
* ASCII case). */
|
|
bool scr_bytes_encoding_is_utf8(const ScrStr *enc);
|
|
|
|
/* Internal allocators for buffer builders: a +1 string with
|
|
* UNINITIALIZED data (the builder fills bytes, then len and the NUL), and
|
|
* an rc==1-only realloc that grows capacity in place. Both keep the RC
|
|
* audit's live count exact. */
|
|
ScrStr *scr_str_alloc_raw(size_t len, size_t cap);
|
|
ScrStr *scr_str_regrow(ScrStr *s, size_t newcap);
|
|
|
|
static inline ScrStr *scr_str_retain(ScrStr *s) {
|
|
if (s->rc != SIZE_MAX) s->rc++;
|
|
return s;
|
|
}
|
|
|
|
void scr_str_release(ScrStr *s); /* NULL-tolerant (uninitialized locals) */
|
|
|
|
/* A transient stack copy of a slice (scr_str_slice_scratch): immortal for
|
|
* retain and release, and valid only while its borrowing consumer runs. No
|
|
* other string has a capacity below its length. Containers that keep a
|
|
* string they were lent store scr_str_scratch_keep's copy instead. */
|
|
static inline bool scr_str_is_scratch(const ScrStr *s) {
|
|
return s->rc == SIZE_MAX && s->cap < s->len;
|
|
}
|
|
|
|
/* ── lone surrogates: canonical WTF-8 storage ─────────────────────────
|
|
* String storage is UTF-8 extended to lone UTF-16 surrogates: an unpaired
|
|
* surrogate is its generalized 3-byte encoding ED A0..BF 80..BF, while a
|
|
* high surrogate directly followed by a low one is always stored as the
|
|
* pair's 4-byte UTF-8 sequence. Canonical storage keeps byte equality,
|
|
* hashing and searching exact. Builders preserve it by fusing a trailing
|
|
* lone high surrogate with a leading lone low one (scr_wtf8_joins). A lone
|
|
* surrogate is exactly as long as U+FFFD, so byte sinks transcode to UTF-8
|
|
* (scr_str_utf8) without changing any length or offset. */
|
|
|
|
/* Set once a lone surrogate may exist anywhere in the process: program
|
|
* startup stamps it when a literal holds one, and every runtime producer
|
|
* notes it (scr_wtf8_put, scr_wtf8_note). Until then no string can hold a
|
|
* lone surrogate, so builders and byte sinks skip all surrogate work. It is
|
|
* process-wide (strings cross threads) and never cleared. */
|
|
extern bool scr_wtf8_lone;
|
|
static inline bool scr_wtf8_any(void) { return __atomic_load_n(&scr_wtf8_lone, __ATOMIC_RELAXED); }
|
|
void scr_wtf8_note(void);
|
|
|
|
/* Whether p starts a lone surrogate (p[1] must be readable: string data
|
|
* is NUL-terminated). */
|
|
static inline bool scr_wtf8_surrogate_at(const char *p) {
|
|
return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xA0;
|
|
}
|
|
|
|
/* Whether p starts a lone low surrogate (p[1] readable, as above). */
|
|
static inline bool scr_wtf8_low_at(const char *p) {
|
|
return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xB0;
|
|
}
|
|
|
|
/* Whether appending b to a puts a lone high surrogate directly before a
|
|
* lone low surrogate. Tests the first byte of b before anything else, so
|
|
* ordinary text costs one compare. */
|
|
static inline bool scr_wtf8_joins(const char *a, size_t alen, const char *b, size_t blen) {
|
|
return __builtin_expect(scr_wtf8_any() && (unsigned char)b[0] == 0xED && blen >= 3 &&
|
|
(unsigned char)b[1] >= 0xB0 && alen >= 3 &&
|
|
(unsigned char)a[alen - 3] == 0xED &&
|
|
((unsigned char)a[alen - 2] & 0xF0) == 0xA0,
|
|
0);
|
|
}
|
|
|
|
/* The first lone surrogate in a span, or NULL. memchr-driven. */
|
|
const char *scr_wtf8_find_surrogate(const char *p, size_t n);
|
|
/* Fuse every adjacent lone high/low pair in place; returns the new length. */
|
|
size_t scr_wtf8_fuse(char *p, size_t n);
|
|
/* Restore canonical storage after a builder joined a lone high surrogate
|
|
* to a lone low one: fuses in place and drops cached content facts. The
|
|
* _from form starts at a byte offset before which storage is canonical. */
|
|
void scr_str_fuse_surrogates(ScrStr *s);
|
|
void scr_str_fuse_from(ScrStr *s, size_t from);
|
|
/* Replace every lone surrogate with U+FFFD in place (same length). */
|
|
void scr_wtf8_replace_surrogates(char *p, size_t n);
|
|
static inline void scr_wtf8_to_utf8(char *p, size_t n) {
|
|
if (__builtin_expect(scr_wtf8_any(), 0)) scr_wtf8_replace_surrogates(p, n);
|
|
}
|
|
/* A string's bytes as well-formed UTF-8 for a byte sink: s->data itself,
|
|
* or (only when s holds a lone surrogate) a malloc'd NUL-terminated copy
|
|
* with each one replaced by U+FFFD, returned in *owned for the caller to
|
|
* release with scr_str_utf8_end. *owned is NULL on the fast path. */
|
|
const char *scr_str_utf8_copy(const ScrStr *s, char **owned);
|
|
static inline const char *scr_str_utf8(const ScrStr *s, char **owned) {
|
|
*owned = NULL;
|
|
if (s->ascii || !scr_wtf8_any()) return s->data;
|
|
return scr_str_utf8_copy(s, owned);
|
|
}
|
|
static inline void scr_str_utf8_end(char *owned) {
|
|
if (owned) free(owned);
|
|
}
|
|
static inline bool scr_str_has_surrogate(const ScrStr *s) {
|
|
return scr_wtf8_any() && !s->ascii && scr_wtf8_find_surrogate(s->data, s->len) != NULL;
|
|
}
|
|
/* Encode one UTF-16 code unit or code point as WTF-8 (1-4 bytes). */
|
|
size_t scr_wtf8_put(char *dst, uint32_t cp);
|
|
|
|
/* Borrow both args, return +1. */
|
|
ScrStr *scr_str_concat(ScrStr *a, ScrStr *b);
|
|
/* Borrow each part without mutation; return one independently owned result. */
|
|
ScrStr *scr_str_concat_parts(ScrStr *const *parts, size_t count);
|
|
/* Concatenate up to 16 string or number operands (parts[i] == NULL selects
|
|
* nums[i], formatted like String(n)) onto an optional borrowed head, +1
|
|
* result. The head may be appended in place under scr_str_concat's rule for
|
|
* its left operand; parts are never mutated. */
|
|
ScrStr *scr_str_concat_mixed(ScrStr *head, ScrStr *const *parts, const double *nums, size_t count);
|
|
/* The same with the left operand / head MOVED in (`s = s + x` when nothing
|
|
* else can observe `s`): one call instead of a snapshot retain, the
|
|
* binding's release and the snapshot's release around a borrowing concat. */
|
|
ScrStr *scr_str_concat_move(ScrStr *a, ScrStr *b);
|
|
/* scr_str_concat_move(a, String.fromCharCode(code)) without building the
|
|
* one-character string: an ASCII character appends to a uniquely owned a
|
|
* in place. */
|
|
ScrStr *scr_str_concat_move_char_code(ScrStr *a, double code);
|
|
ScrStr *scr_str_concat_mixed_move(ScrStr *head, ScrStr *const *parts, const double *nums,
|
|
size_t count);
|
|
/* a + String(x): scr_str_concat's contract with a number right operand. */
|
|
ScrStr *scr_str_concat_f64(ScrStr *a, double x);
|
|
|
|
bool scr_str_eq(ScrStr *a, ScrStr *b);
|
|
|
|
/* memcmp byte order == code-point order, which differs from JS UTF-16
|
|
* code-unit order for non-BMP characters against U+E000..U+FFFF and for
|
|
* lone surrogates. Returns <0, 0, >0. Relational string operators use
|
|
* scr_str_cmp_u16. */
|
|
int scr_str_cmp(ScrStr *a, ScrStr *b);
|
|
/* a.localeCompare(b) with no locales/options: Node's root-locale order for
|
|
* Latin-script text, code-point order outside it (scr_string.c). */
|
|
double scr_str_locale_compare(ScrStr *a, ScrStr *b);
|
|
|
|
/* ECMAScript string ordering: compare UTF-16 code units even though ScrStr
|
|
* stores WTF-8. Returns <0, 0, >0. */
|
|
int scr_str_cmp_u16(ScrStr *a, ScrStr *b);
|
|
/* scr_str_cmp_u16 for two distinct strings that are not proven ASCII. */
|
|
int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b);
|
|
|
|
#endif /* SCR_STRING_H */
|