Commit Graph
8 Commits
Author SHA1 Message Date
Chris Tate a88fdacbe2 Iterate strings over their bytes
for-of, spread, Array.from, yield* and [Symbol.iterator]() over a string
walked a UTF-16 index. Each step mapped the index to a byte offset, built
the character, and measured its UTF-16 length to advance.

They now walk a byte cursor over the WTF-8 data:
- The cpAtByte intrinsic reads the character that starts at the cursor.
  ASCII bytes take the one-character table inline; other characters call
  scr_str_cp_at_byte. byteLength advances the cursor by the character's
  byte count. A lone surrogate is its own three-byte character, so it
  stays a one-unit character, as in Node.
- Spread and Array.from of a string call scr_str_code_points, which
  counts the characters, allocates the array once and fills it.
- The one-character strings of U+0080..U+07FF are made immortal on first
  use and shared, as the ASCII ones are static.

The cpAt intrinsic, which no lowering emits any more, is removed.
2026-10-25 13:00:18 -05:00
Chris Tate 63a99cc4d9 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it
A versioned loop over a non-ASCII string passes the range of indexes it
reads: a counted loop's start and limit, or a while loop's counter and
bound. The runtime converts the string at once when that range has at
least one unit per 16 bytes of it, and otherwise counts the loop as one
indexed read, so a loop over a line of a long source text no longer
keeps a UTF-16 copy of all of it. The publish call is weighted as
unlikely, since loops re-entered over one string skip it.
2026-10-25 07:37:50 -05:00
Chris Tate add5f4869b Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string 2026-10-24 22:53:15 -05:00
Chris Tate 87e51e6c71 Read non-ASCII scan receivers inline in worker executables and keep units for longer texts
Indexed reads of a non-ASCII string (charCodeAt, length) become loads
once the runtime holds its UTF-16 units, but emitted code read the
published scan receiver only where runtime state is ordinary global
storage, so worker executables called into the runtime for every read.
On ELF targets their thread-locals live in the executable's static TLS
block, so the receiver is now declared thread-local there and read with
one thread-pointer-relative load. The unit array was also limited to
receivers of 2^20 units; source texts of several megabytes, which
scanners read in full, now get one too.

Adds the unicode-lexer runtime benchmark workload: a lexer over a
2.7M-unit module whose comments and strings are in several languages.
2026-10-20 14:11:26 -05:00
Malte UblandClaude Opus 5.5 ba17241220 Run String and Number methods on dynamic values and add substr, locale case and toPrecision
string-width's `character.codePointAt(0)`, camelcase's
`string.toLocaleLowerCase(locale)`, toml's `input.substr(...)` and
pretty-bytes' `number.toPrecision(3)` threw "x.y is not a function": the
dynamic method dispatch had no answer for these names on strings and
numbers.

A checked-dynamic receiver calling a String or Number method the runtime
dispatch cannot answer (codePointAt, substr, toLocaleLowerCase,
toLocaleUpperCase, isWellFormed, toWellFormed, trimLeft, trimRight,
toPrecision, toExponential(digits)) now evaluates the receiver and
arguments once and dispatches on the runtime kind: a string or number runs
the typed lowering, every other kind keeps the runtime dispatch's answer.
Names other dyn kinds share (at, concat, slice) and the Annex B HTML names
chalk-style builders define (bold, link, ...) keep their existing
dispatch.

Typed receivers gain substr, the locale case pair, the HTML methods,
toPrecision and toExponential(fractionDigits), all previously compile-time
refusals. toPrecision/toExponential digits come from exact big-integer
arithmetic over the double (ties up on the exact binary value). Locale
case mapping follows V8: the first requested locale decides, Turkic
dotted/dotless I is implemented, and Lithuanian, Greek uppercasing,
objects and malformed tags throw a 'not supported yet' Error.

A checker-untyped JS binding holding `string | undefined` boxes into the
dyn receiver path for String methods (string-width's baseVisible result).
tests/diagnostics/retired-engine-1112-number-methods.ts compiles now and
moves back to tests/corpus/1112-number-methods.ts.

New runtime functions are reached only from the new lowerings; the
existing runtime objects' sections are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:55:12 -07:00
Malte UblandClaude Opus 5.5 06b0bb2931 Write borrowed string slices into stack buffers
A slice read only by lending consumers (string equality and ordering,
length/charCodeAt/indexOf/includes/startsWith/endsWith, Number/unary
plus/parseInt/parseFloat, typed string-keyed Map/Set get/has/set/add/
delete keys, string switch discriminants, and string parameters their
callee only lends) is written by scr_str_slice_scratch into a per-site
stack buffer as an immortal ASCII ScrStr marked as scratch (cap < len),
instead of a heap allocation. Stable locals initialized by such a slice
qualify when every read lends. Map/Set insertion stores a heap copy of a
scratch key, and probes cache the key hash in the scratch header, so the
counting idiom allocates only for new keys. Longer, non-ASCII, and
interned results keep the owned slice; coroutines and debug builds are
unchanged. Slice bounds are normalized without a libm trunc call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:13:11 -07:00
Malte Ubl b99e2d2558 Visit only selected captures when building regex exec results
A spans result (the compiler proved only some captures are read) used to
run the full per-capture decision for every group. Slots now start as
present undefined with a zero payload, and only the selected and lazy
capture bits are visited; full results visit every capture as before.
The hole-free prefix is measured after the fill.

String-array teardown keeps its storage pointers in registers across the
release calls (releasing a string runs no user code), and regex group
parseInt takes the short-decimal path inline (shared with
scr_parse_int_bytes as scr_parse_int_short_digits) with signed
conversions where the values are known to be far below 2^53.
2026-10-15 04:47:04 -07:00
Chris Tate 4d54f6c4d8 Split scr_runtime.h into per-module runtime headers
scr_runtime.h now includes 48 module headers (scr_core.h, scr_rc.h,
scr_string.h, scr_dyn.h, scr_http.h, ...) in dependency order and holds no
declarations. Each module header holds whole sections of the former file,
includes the headers whose names it uses, and compiles on its own.

Declarations, macros, struct layouts and inline bodies are unchanged. Five
blocks move to join their module: the class-hierarchy vtable header (into
scr_error.h, after the string core), the node:timers/promises pair (into
scr_async.h), the JSON.parse entry points and the JSON output buffer (into
scr_json.h, after the dyn declarations) and the EventTarget block (after
scr_json.h). No moved block uses or declares a name that a block it moves
past uses or declares. The string methods keep their position after the
class objects in scr_string_methods.h, because their struct ScrDyn
parameters rely on the file-scope struct ScrDyn declaration there.

The runtime ABI scan reads scr_runtime.h with its module headers expanded
in include order. A runtime test compiles every module header on its own.
scripts/runtime-header-hunks.mjs retargets a diff of the single-file
header onto the module headers.
2026-10-15 02:06:23 -05:00