Skip to content

Quadratic paths made linear; Headers.get, regex crashes and five wrong results fixed - #96

Merged
ASDAlexander77 merged 1 commit into
mainfrom
speed-and-null-fixes
Nov 14, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
speed-and-null-fixes

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Came out of a review of the library for slow paths and for null handling. Three groups of changes.

Speed

A string's .length is a strlen call and an array's shift() moves every remaining element; both sat inside loops. Measured under the release JIT (gc, Windows x64):

Operation Before After
toUpperCase, 200k chars 898 ms under 1 ms
JSON.stringify of a 200k-char string 901 ms under 1 ms
arr.join(","), 20k items 2671 ms 2 ms
arr.sort() default, 8k ints 6080 ms 15 ms
arr.sort(cmp), 8k ints 31 ms under 1 ms
flatMap, 80k inner elements 2394 ms under 1 ms
MessageQueue.take, 80k items 2375 ms 6 ms
Array<T>.splice removing 10, 200 times on 100k 207 ms 8 ms
  • String helpers read the length once.
  • join builds its parts and allocates once.
  • sort is a stable merge sort of positions, applied by exchanges (so -mm=own still sees elements only change places). It was a selection sort.
  • flatMap, toSpliced and Array<T>.push move elements out with one reverse and pops.
  • MessageQueue.take pops from an outbox refilled from the inbox.
  • Array<T>.unshift and splice inserts keep the native move for fewer than 8 items and rotate for more; a single-item insert costs what it did.

Null and crashes

  • Headers: the private methods now come after the public ones. lib.d.ts declares only the public ones and a program finds a method by its place, so every call ran a method two places off and get returned garbage. get now returns string | null.
  • RegExp.split is declared in lib.d.ts (the same kind of mismatch).
  • replaceAll with an invalid pattern throws a SyntaxError; it was an access violation.
  • exec and test with lastIndex past the end fail the match and reset it to 0; it was an access violation.
  • regexp_exec no longer leaks its results on a failed match.
  • NUL terminators are written as <char>0; MatchResults loses an unused field and its null defaults; ArrayBuffer never holds null and throws on a detached buffer.

Wrong results

  • search returns where the match starts ("abcdef".search(/cd/) was 3).
  • join keeps the separator after a leading empty string.
  • fill fills the array itself, as in JavaScript; it filled a copy. Behaviour change: tests/array_class_string.ts expected the copy and is updated.
  • findLast returns undefined when nothing matches, not -1.
  • lastIndexOf treats a fromIndex at or past the end as the last element, and one before the start finds nothing.
  • Math.random is spread over [0, 1) on Windows (it assumed a 31-bit rand()).

Not in this PR

  • JSON.parse of one object with thousands of keys is still quadratic (61 ms at 8,000 keys).
  • String.match still returns null through string[]: the nullable type broke two existing tests at compile time.
  • // @strict-null false stays in lib.ts.

Testing

  • Release, Windows x64: the whole suite passes for gc, rc, none and own, compiled and under the JIT, with nine new tests.
  • scripts/check_declarations.ps1 passes.
  • Not run locally: debug builds, Linux, x86 and wasm.
  • The ArrayBuffer changes compile but were not executed (programs cannot name the class yet).

🤖 Generated with Claude Code

…g results fixed

Speed. A string's length is a strlen call and an array's shift moves every
element left, and both sat inside loops:
- toLowerCase/toUpperCase, JSON.stringify of a string, charAt and the other
  string helpers ask the length once (200k characters: 898 ms to under 1 ms).
- Array join builds its parts and allocates once (20k items: 2671 ms to 2 ms).
- Array sort is a stable merge sort of positions applied by exchanges, where it
  was a selection sort (default sort of 8k ints: 6080 ms to 15 ms).
- flatMap, toSpliced and Array<T>.push move elements out with a reverse and
  pops (flatMap of 80k: 2394 ms to under 1 ms).
- MessageQueue.take pops from an outbox filled from the inbox when it runs
  empty (80k items: 2375 ms to 6 ms).
- Array<T>.splice removes by two reversals, and unshift and splice insert
  eight or more items by rotation; fewer keep the native move.

Null and crashes.
- Headers: the private methods come after the public ones. lib.d.ts declares
  only the public ones and a program finds a method by its place, so every call
  ran a method two places off and get returned garbage. get now returns
  string | null.
- RegExp.split is declared in lib.d.ts, the same kind of mismatch.
- replaceAll with an invalid pattern throws a SyntaxError where it read a null.
- exec and test with lastIndex past the end fail the match and reset it to 0
  where they read past the string.
- regexp_exec frees the results it made when there is no match; the C strings
  are new[]/delete[].
- NUL terminators are written as <char>0, MatchResults loses its unused
  indices_ field and its null defaults, and ArrayBuffer never holds null and
  throws on a detached buffer.

Wrong results.
- search returns where the match starts ("abcdef".search(/cd/) was 3).
- join keeps the separator after a leading empty string.
- fill fills the array itself, as in JavaScript, where it filled a copy;
  tests/array_class_string.ts expected the copy and is updated.
- findLast returns undefined when nothing matches, not -1.
- lastIndexOf takes a fromIndex at or past the end as the last element, and
  one before the start finds nothing.
- Math.random is spread over [0, 1) on Windows: it assumed a 31-bit rand(),
  and RAND_MAX there is 32767.

Tested in release on Windows x64 for gc, rc, none and own, compiled and under
the JIT. Debug, Linux, x86 and wasm were not built locally.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant