From da6654931000bff4a2616a44f5f56f24c4633da4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 12:59:22 +0000 Subject: [PATCH] Quadratic paths made linear; Headers.get, regex crashes and five wrong 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.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.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 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 --- docs/fetch.md | 2 +- src/generics/lib.generics.ts | 232 ++++++++++++++++++++++++++--------- src/lib.d.ts | 14 +-- src/lib.ts | 187 ++++++++++++++++++---------- src/wrappers/regex.cpp | 20 +-- tests/array_class_string.ts | 7 +- tests/array_fill_in_place.ts | 22 ++++ tests/array_findLast2.ts | 13 ++ tests/array_join_empty.ts | 16 +++ tests/array_lastIndexOf3.ts | 21 ++++ tests/array_sort_stable.ts | 40 ++++++ tests/headers_get.ts | 29 +++++ tests/math_random.ts | 22 ++++ tests/regexp_bounds.ts | 24 ++++ tests/string_search3.ts | 12 ++ 15 files changed, 520 insertions(+), 141 deletions(-) create mode 100644 tests/array_fill_in_place.ts create mode 100644 tests/array_findLast2.ts create mode 100644 tests/array_join_empty.ts create mode 100644 tests/array_lastIndexOf3.ts create mode 100644 tests/array_sort_stable.ts create mode 100644 tests/headers_get.ts create mode 100644 tests/math_random.ts create mode 100644 tests/regexp_bounds.ts create mode 100644 tests/string_search3.ts diff --git a/docs/fetch.md b/docs/fetch.md index ee2c374..f5b085b 100644 --- a/docs/fetch.md +++ b/docs/fetch.md @@ -18,7 +18,7 @@ class Headers { append(name: string, value: string): void; set(name: string, value: string): void; - get(name: string): string; + get(name: string): string | null; has(name: string): boolean; delete(name: string): void; toRawString(): string; diff --git a/src/generics/lib.generics.ts b/src/generics/lib.generics.ts index 322dba2..b605eac 100644 --- a/src/generics/lib.generics.ts +++ b/src/generics/lib.generics.ts @@ -54,11 +54,31 @@ function __toSpliced(source: T[], start: int, deleteCount: int, items: T[]): const result: T[] = []; for (let i = 0; i < start; i++) result.push(source[i]); - while (items.length > 0) result.push(items.shift()); + __moveAll(items, result); for (let i = start + count; i < length; i++) result.push(source[i]); return result; } +// The elements [from, to) of `a` in reverse order, by exchanges, which move nothing: through a +// local, under -mm=own an object element would have two owners (see __Array.reverse). +function __reverseRange(a: T[], from: int, to: int): void { + let i = from; + let j = to - 1; + while (i < j) { + [a[i], a[j]] = [a[j], a[i]]; + i++; + j--; + } +} + +// Every element of `source` moved onto the end of `target`, in order; `source` is left empty. +// Reversed once so each comes off the end: a shift for each one moved all the rest down every +// time, which made n elements cost n * n. +function __moveAll(source: T[], target: T[]): void { + __reverseRange(source, 0, source.length); + while (source.length > 0) target.push(source.pop()); +} + namespace __Array { function at(this: T[], index: int) { return this[index]; @@ -151,10 +171,10 @@ namespace __Array { return true; } + // Fills the array itself and gives it back, as JavaScript does: it used to fill a copy and leave + // the array as it was. @sharesElements function fill(this: T[], value: T, start = 0, end = this.length) { - let newArray = this.slice(); - if (start < 0) { if (-this.length <= start) { start = start + this.length; @@ -162,7 +182,7 @@ namespace __Array { start = 0; } } else if (start >= this.length) { - return newArray; + return this; } if (end < 0) { @@ -176,9 +196,9 @@ namespace __Array { } for (let i = start; i < end; i++) - newArray[i] = value; + this[i] = value; - return newArray; + return this; } @sharesElements @@ -203,13 +223,10 @@ namespace __Array { return -1; } - function findLast(this: T[], func: (v: T, i: number) => boolean) { - for (let i = this.length - 1; i >= 0; i--) { - const v = this[i]; - if (func(v, i)) return v; - } - - return -1; + // undefined when there is none, as find gives: it gave -1, which is findLastIndex's answer + function findLast(this: T[], func: (v: T, i: number) => boolean): T | undefined { + for (let i = this.length - 1; i >= 0; i--) if (func(this[i], i)) return this[i]; + return undefined; } function findLastIndex(this: T[], func: (v: T, i: number) => boolean) { @@ -267,15 +284,21 @@ namespace __Array { return -1; } + // Each element's text with the separator before it, then one string of them all. Adding each to + // one result copied what was there for every element, and asked its length (a walk to its end) + // to know whether a separator was due - which also lost the separator after a leading "". function join(this: T[], separator = ",") { - let result = ""; - for (const v of this) { - if (result.length > 0) - result += separator; - result += v; + const parts: string[] = []; + const length = this.length; + for (let i = 0; i < length; i++) { + if (i > 0) { + parts.push(separator + this[i]); + } else { + parts.push("" + this[i]); + } } - return result; + return "".concat(...parts); } function* keys(this: T[]) { @@ -284,21 +307,19 @@ namespace __Array { } } + // The search goes backwards from `fromIndex`. As in JavaScript, one at or past the end starts at + // the last element (it gave -1), a negative one counts from the end, and one before the start + // finds nothing (it searched element 0). function lastIndexOf(this: T[], searchElement: T, fromIndex = this.length - 1) { - if (fromIndex < 0) { - if (-this.length <= fromIndex) { - fromIndex = fromIndex + this.length; - } else if (fromIndex < -this.length) { - fromIndex = 0; - } - } else if (fromIndex >= this.length) { - return -1; + const length: int = this.length; + let from: int = fromIndex; + if (from < 0) { + from = from + length; + } else if (from >= length) { + from = length - 1; } - if (fromIndex > this.length) - fromIndex = this.length - 1; - - for (let i = fromIndex; i in this; i--) { + for (let i = from; i >= 0; i--) { if (searchElement === this[i]) return i; } @@ -408,15 +429,71 @@ namespace __Array { // callback may release is known from the functions given for it, and a parameter assigned a // default is one nothing knows - so a call of it with two elements would be a use of them after // it may release them. - function selectionSort(a: T[], compare: (value1: T, value2: T) => int) { - const len = a.length; - for (let i = 0; i < len - 1; ++i) { - for (let j = i + 1; j < len; ++j) { - if (compare(a[i], a[j]) > 0) { - // an exchange, as in reverse - [a[i], a[j]] = [a[j], a[i]]; + // + // A merge sort of the elements' positions, then the elements exchanged into that order: about + // n log n comparisons where a selection sort made n * n, and stable, as JavaScript's sort is. + // The elements themselves only ever change places (an exchange, as in reverse). + function mergeSort(a: T[], compare: (value1: T, value2: T) => int) { + const len: int = a.length; + let order: int[] = []; + order.length = len; + let merged: int[] = []; + merged.length = len; + for (let i = 0; i < len; i++) order[i] = i; + + // runs of `width` positions, each in order, merged two by two + for (let width = 1; width < len; width *= 2) { + for (let lo = 0; lo < len; lo += 2 * width) { + let mid = lo + width; + if (mid > len) mid = len; + let hi = mid + width; + if (hi > len) hi = len; + + let i = lo; + let j = mid; + let k = lo; + while (i < mid && j < hi) { + // the right one goes first only when the left one is greater: equal elements + // keep the order they came in + if (compare(a[order[i]], a[order[j]]) > 0) { + merged[k] = order[j]; + j++; + } else { + merged[k] = order[i]; + i++; + } + + k++; + } + + while (i < mid) { + merged[k] = order[i]; + i++; + k++; } + + while (j < hi) { + merged[k] = order[j]; + j++; + k++; + } + } + + for (let i = 0; i < len; i++) order[i] = merged[i]; + } + + // order[i] is where the element that belongs at i is now: each cycle of positions is + // walked once, an exchange putting one element in its place + for (let i = 0; i < len; i++) { + let current = i; + while (order[current] != i) { + const next = order[current]; + [a[current], a[next]] = [a[next], a[current]]; + order[current] = current; + current = next; } + + order[current] = current; } } @@ -426,9 +503,9 @@ namespace __Array { } if (callbackfn) { - selectionSort(this, callbackfn); + mergeSort(this, callbackfn); } else { - selectionSort(this, (a: T, b: T) => { + mergeSort(this, (a: T, b: T) => { const sa = a; const sb = b; if (sa == sb) return 0; @@ -502,7 +579,7 @@ namespace __Array { for (let i = 0; i < this.length; i++) { // moved out of the array func made, which goes when this loop turns const mapped = func(this[i], i); - while (mapped.length > 0) result.push(mapped.shift()); + __moveAll(mapped, result); } return result; @@ -546,8 +623,7 @@ class Array { // each element is moved out of `args`, not read: -mm=own then keeps them, and the callers give // them up public push(...args: T[]): int { - while (args.length > 0) - this.data.push(args.shift()); + __moveAll(args, this.data); return this.data.length; } @@ -559,10 +635,26 @@ class Array { return this.data.shift(); } + // A few go in one at a time, each a single move of the elements already there. Many are moved + // onto the end, and the two parts then change places: one pass over the elements, by exchanges, + // which costs about what eight of those moves do. public unshift(...args: T[]): int { - while (args.length > 0) - this.data.unshift(args.pop()); - return this.data.length; + if (args.length < 8) { + while (args.length > 0) + this.data.unshift(args.pop()); + return this.data.length; + } + + const before: int = this.data.length; + __moveAll(args, this.data); + const after: int = this.data.length; + if (before > 0 && after > before) { + __reverseRange(this.data, 0, before); + __reverseRange(this.data, before, after); + __reverseRange(this.data, 0, after); + } + + return after; } // Removes `deleteCount` elements at `start` (all of them to the end when it is left out), @@ -582,21 +674,36 @@ class Array { count = length - start; } - // the removed elements are moved out, not read: each is swapped to the end, the first last, - // and popped off in order - const last = this.data.length - 1; - for (let k = 0; k < count; k++) { - for (let j = start; j < last - k; j++) { - [this.data[j], this.data[j + 1]] = [this.data[j + 1], this.data[j]]; - } + // The removed elements are moved out, not read: they go to the end, the first of them + // last, and are popped off in order. Two reversals take them there - of everything from + // `start`, then of what stays - where each used to be swapped down one place at a time. + const end: int = length; + const from: int = start; + const kept: int = end - count; + if (count > 0) { + __reverseRange(this.data, from, end); + __reverseRange(this.data, from, kept); } const removed: T[] = []; for (let k = 0; k < count; k++) removed.push(this.data.pop()); - for (let i = 0; items.length > 0; i++) - this.data.splice(start + i, 0, items.shift()); + // a few items go in one at a time, as in unshift + if (items.length < 8) { + for (let i = 0; items.length > 0; i++) + this.data.splice(start + i, 0, items.shift()); + return removed; + } + + // many go on at the end, then change places with what followed `start` + __moveAll(items, this.data); + const total: int = this.data.length; + if (total > kept && kept > from) { + __reverseRange(this.data, from, kept); + __reverseRange(this.data, kept, total); + __reverseRange(this.data, from, total); + } return removed; } @@ -988,6 +1095,9 @@ namespace Thread { // their counts under -mm=rc. Worker is built on it (docs/worker-design.md). class MessageQueue { private items: T[] = []; + // What take hands out, the oldest last: filled from `items` when it runs empty, so each item + // is moved once. Shifting them off `items` moved every item left down for each one taken. + private outbox: T[] = []; // [0] the lock (1 taken); [1] bumped on each push and on close, which a taker waits on; // [2] closed (1) private sync = new Int32Array(3); @@ -1018,8 +1128,12 @@ class MessageQueue { const deadline = forever ? 0 : Date.now() + timeout; while (true) { this.lock(); - if (this.items.length > 0) { - const item = this.items.shift(); + if (this.outbox.length == 0) { + while (this.items.length > 0) this.outbox.push(this.items.pop()); + } + + if (this.outbox.length > 0) { + const item = this.outbox.pop(); this.unlock(); return item; } @@ -1057,6 +1171,7 @@ class MessageQueue { clear() { this.lock(); this.items.length = 0; + this.outbox.length = 0; this.unlock(); } @@ -1065,6 +1180,7 @@ class MessageQueue { this.lock(); this.sync[2] = 1; this.items.length = 0; + this.outbox.length = 0; Atomics.add(this.sync, 1, 1); this.unlock(); Atomics.notify(this.sync, 1); @@ -1076,7 +1192,7 @@ class MessageQueue { get length() { this.lock(); - const length = this.items.length; + const length = this.items.length + this.outbox.length; this.unlock(); return length; } diff --git a/src/lib.d.ts b/src/lib.d.ts index 8c6cf35..55a401d 100644 --- a/src/lib.d.ts +++ b/src/lib.d.ts @@ -224,11 +224,11 @@ declare class Date { @final declare class MatchIndicesResults { - private match: Opaque | null; + private match: Opaque; private lastIndex: index; - constructor(match?: Opaque | null, lastIndex?: index); + constructor(match: Opaque, lastIndex?: index); [index: number]: [index, index]; @@ -240,15 +240,13 @@ declare class MatchIndicesResults @final declare class MatchResults { - private indices_: MatchIndicesResults; - - private match: Opaque | null; + private match: Opaque; private hasIndices: boolean; private lastIndex: index; - constructor(match?: Opaque | null, hasIndices?: boolean, lastIndex?: index); + constructor(match: Opaque, hasIndices?: boolean, lastIndex?: index); [index: number]: string; @@ -291,6 +289,8 @@ declare class RegExp { replaceAll(s: string, replacement: string): string; + split(s: string, limit?: int): string[]; + [Symbol.dispose](): void; } @@ -671,7 +671,7 @@ declare class Headers { set(name: string, value: string): void; - get(name: string): string; + get(name: string): string | null; has(name: string): boolean; diff --git a/src/lib.ts b/src/lib.ts index 32ad456..2adabfe 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -477,8 +477,8 @@ export class Date { @final export class MatchIndicesResults { - constructor(private match: Opaque | null = null, private lastIndex: index = 0) { - } + constructor(private match: Opaque, private lastIndex: index = 0) { + } [index: number]: [index, index]; @@ -497,10 +497,8 @@ export class MatchIndicesResults @final export class MatchResults { - private indices_: MatchIndicesResults = null; - - constructor(private match: Opaque | null = null, private hasIndices = false, private lastIndex: index = 0) { - } + constructor(private match: Opaque, private hasIndices = false, private lastIndex: index = 0) { + } [index: number]: string; @@ -568,6 +566,14 @@ export class RegExp } exec(s: string): MatchResults | null { + // past the end there is nothing to search, and nothing to read: as in JavaScript the + // match fails and lastIndex goes back to 0 (at 0 there is nothing to ask the length for) + if (this.lastIndex > 0 && this.lastIndex > s.length) + { + this.lastIndex = 0; + return null; + } + const cmatch = regexp_exec(this.source, this.flags, Ref(s[this.lastIndex]), this.match); if (!cmatch) { @@ -588,6 +594,13 @@ export class RegExp test(s: string) { if (this.global) { + // as in exec: past the end the match fails + if (this.lastIndex > 0 && this.lastIndex > s.length) + { + this.lastIndex = 0; + return false; + } + const result = regexp_test(this.source, this.flags, Ref(s[this.lastIndex])); if (result >= 0) { @@ -602,9 +615,16 @@ export class RegExp return regexp_test(this.source, this.flags, s) >= 0; } + // Where the first match starts, or -1. It gave where the match ends, less one: right only for + // a match of one character. search(s: string): int { - const r = regexp_test(this.source, this.flags, s); - return r > 0 ? r - 1 : r; + const cmatch = regexp_exec(this.source, this.flags, s, null); + if (!cmatch) + return -1; + + const start = regexp_match_results_prefix_length(cmatch); + regexp_free(cmatch); + return start; } replace(s: string, replacement: string): string { @@ -638,6 +658,10 @@ export class RegExp replaceAll(s: string, replacement: string): string { const r = regexp_replace(this.source, this.flags, s, replacement); + // no text comes back for a pattern that is not a regular expression + if (r == null) + throw new SyntaxError(`Invalid regular expression: /${this.source}/`); + const len = (r).length; let result = "".clone().resize(len); boundedCopy(Ref(result[0]), len, r, len); @@ -732,13 +756,15 @@ export class RegExp namespace __String { function dataAt(this: string, index: int) { + // the length once: it is a walk to the string's end each time it is asked for + const len: int = this.length; if (index < 0) { - if (-this.length <= index) { - index = index + this.length; - } else if (index < -this.length) { + if (-len <= index) { + index = index + len; + } else { index = 0; } - } else if (index >= this.length) { + } else if (index >= len) { return 0; } @@ -751,7 +777,7 @@ namespace __String { function resize(this: string, newSize: index): string { this.length = newSize + 1; - this[newSize] = null; + this[newSize] = 0; return this; } @@ -1004,9 +1030,10 @@ namespace __String { function pad(addSize: index, padString: string): string { let newPadString = "".clone().resize(addSize); let padStringIndex = 0; + const padStringLength = padString.length; for (let i = 0; i < addSize; i++) { newPadString[i] = padString[padStringIndex++]; - if (padStringIndex >= padString.length) { + if (padStringIndex >= padStringLength) { padStringIndex = 0; } } @@ -1116,24 +1143,25 @@ namespace __String { export function slice(this: string, indexStart: int, indexEnd: int = this.length): string { + const len: int = this.length; if (indexStart < 0) { - if (-this.length <= indexStart) { - indexStart = indexStart + this.length; - } else if (indexStart < -this.length) { + if (-len <= indexStart) { + indexStart = indexStart + len; + } else { indexStart = 0; } - } else if (indexStart >= this.length) { + } else if (indexStart >= len) { return ""; } if (indexEnd < 0) { - if (-this.length <= indexEnd) { - indexEnd = indexEnd + this.length; - } else if (indexEnd < -this.length) { + if (-len <= indexEnd) { + indexEnd = indexEnd + len; + } else { indexEnd = 0; } - } else if (indexEnd >= this.length) { - indexEnd = this.length; + } else if (indexEnd >= len) { + indexEnd = len; } // an end at or before the start is empty - slice does not swap, as substring does; the @@ -1238,17 +1266,18 @@ namespace __String { } export function substring(this: string, indexStart: int, indexEnd: int = this.length): string { + const len: int = this.length; if (indexStart < 0) { indexStart = 0; - } else if (indexStart >= this.length) { - indexStart = this.length; + } else if (indexStart >= len) { + indexStart = len; } if (indexEnd < 0) { indexEnd = 0; - } else if (indexEnd >= this.length) { - indexEnd = this.length; - } + } else if (indexEnd >= len) { + indexEnd = len; + } if (indexStart == indexEnd) { return ""; @@ -1267,7 +1296,8 @@ namespace __String { export function toLocaleLowerCase(this: string, locale = ""): string { setlocale (LC_COLLATE, locale); const lower = this.clone(); - for (let i = 0; i < this.length; i++) lower[i] = tolower(this[i]); + const len = this.length; + for (let i = 0; i < len; i++) lower[i] = tolower(this[i]); setlocale (LC_COLLATE, ""); return lower; } @@ -1275,14 +1305,16 @@ namespace __String { export function toLocaleUpperCase(this: string, locale = ""): string { setlocale (LC_COLLATE, locale); const upper = this.clone(); - for (let i = 0; i < this.length; i++) upper[i] = toupper(this[i]); + const len = this.length; + for (let i = 0; i < len; i++) upper[i] = toupper(this[i]); setlocale (LC_COLLATE, ""); return upper; } export function toLowerCase(this: string): string { const lower = this.clone(); - for (let i = 0; i < this.length; i++) lower[i] = tolower(this[i]); + const len = this.length; + for (let i = 0; i < len; i++) lower[i] = tolower(this[i]); return lower; } @@ -1292,7 +1324,8 @@ namespace __String { export function toUpperCase(this: string): string { const upper = this.clone(); - for (let i = 0; i < this.length; i++) upper[i] = toupper(this[i]); + const len = this.length; + for (let i = 0; i < len; i++) upper[i] = toupper(this[i]); return upper; } @@ -1303,11 +1336,12 @@ namespace __String { // the first character that is not a space, or the length when every one is function trimmedStart(this: string): int { - for (let i: int = 0; i < this.length; i++) { + const len: int = this.length; + for (let i: int = 0; i < len; i++) { if (!isspace(this[i])) return i; } - return this.length; + return len; } // one past the last character that is not a space, or 0 when every one is: substring's end is @@ -1843,7 +1877,13 @@ export static class Math { /** Returns a pseudorandom number between 0 and 1. */ public random(): number { - return rand() / 2147483647; + // 45 bits from three calls, 15 from each: rand() gives 15 on Windows (RAND_MAX is 32767) and + // 31 on Linux, and its range was taken to be the larger, so on Windows nothing above + // 0.000016 came back. Never 1, as in JavaScript. + const high = rand() & 0x7FFF; + const middle = rand() & 0x7FFF; + const low = rand() & 0x7FFF; + return (high * 1073741824.0 + middle * 32768.0 + low) / 35184372088832.0; } /** @@ -1932,11 +1972,13 @@ export class ArrayBuffer { this.states |= States.HasMaxByteLength; } + // always an array, empty for no bytes and once detached: never null + let buffer : char[] = []; if (length > 0) { - let buffer : char[] = []; - buffer.length = length; - this.arrayOfView = buffer; + buffer.length = length; } + + this.arrayOfView = buffer; } public get resizable() { @@ -1952,10 +1994,12 @@ export class ArrayBuffer { } public resize(newLength: int) { + this.expectAttached(); this.arrayOfView.length = newLength; } public slice(start = 0, end = this.arrayOfView.length) { + this.expectAttached(); if (start < 0 && -this.arrayOfView.length <= start) start += this.arrayOfView.length; if (start < -this.arrayOfView.length) start = 0; if (start >= this.arrayOfView.length) { @@ -1977,12 +2021,11 @@ export class ArrayBuffer { // TODO: newByteLength is not used public transfer(newByteLength?: int) { + this.expectAttached(); const newArrayBuffer = new ArrayBuffer(0); newArrayBuffer.states = this.states; newArrayBuffer.maxByteLength = this.maxByteLength; - if (this.arrayOfView != null) { - newArrayBuffer.arrayOfView = this.copyOf(0, this.arrayOfView.length); - } + newArrayBuffer.arrayOfView = this.copyOf(0, this.arrayOfView.length); this.detach(); return newArrayBuffer; @@ -2007,15 +2050,22 @@ export class ArrayBuffer { return copy; } + // as in JavaScript: a detached buffer has no bytes to read, resize or hand on + private expectAttached() { + if (this.detached) { + throw new TypeError("ArrayBuffer is detached"); + } + } + private detach() { this.states |= States.Detached; - this.arrayOfView = null; + this.arrayOfView = []; } // the assignment releases the bytes under rc and own, and leaves them to the collector under gc [Symbol.dispose]() { - if (this.arrayOfView != null && !this.detached) { - this.arrayOfView = null; + if (!this.detached) { + this.arrayOfView = []; } } } @@ -2052,18 +2102,6 @@ export class Headers { return headers; } - private key(name: string): string { - return name.toLowerCase(); - } - - private indexOf(key: string): int { - for (let i = 0; i < this.names.length; i++) { - if (this.names[i] == key) return i; - } - - return -1; - } - append(name: string, value: string) { const key = this.key(name); const i = this.indexOf(key); @@ -2086,9 +2124,14 @@ export class Headers { } } - get(name: string): string { + // the value, or null when there is no such header, as in JavaScript + get(name: string): string | null { const i = this.indexOf(this.key(name)); - return i != -1 ? this.values[i] : null; + if (i == -1) { + return null; + } + + return this.values[i]; } has(name: string): boolean { @@ -2119,6 +2162,21 @@ export class Headers { return result; } + + // The private methods come last: lib.d.ts declares only the public ones, and a program finds a + // method by its place among them. With these two before `append`, every call from a program + // ran a method two places before the one it named. + private key(name: string): string { + return name.toLowerCase(); + } + + private indexOf(key: string): int { + for (let i = 0; i < this.names.length; i++) { + if (this.names[i] == key) return i; + } + + return -1; + } } @final @@ -2170,14 +2228,14 @@ export function fetch(url: string, init?: { method?: string, headers?: [string, const headersLen = http_response_headers_length(r); let headersBuffer: char[] = []; headersBuffer.length = headersLen + 1; - headersBuffer[headersLen] = null; + headersBuffer[headersLen] = 0; if (headersLen > 0) http_response_headers_copy_to(r, Ref(headersBuffer[0]), headersLen); const headers = Headers.parse(Ref(headersBuffer[0])); const bodyLen = http_response_body_length(r); let bodyBuffer: char[] = []; bodyBuffer.length = bodyLen + 1; - bodyBuffer[bodyLen] = null; + bodyBuffer[bodyLen] = 0; if (bodyLen > 0) http_response_body_copy_to(r, Ref(bodyBuffer[0]), bodyLen); // a copy: the Response keeps its body (@keeps), and bodyBuffer goes with this function const responseBody = Ref(bodyBuffer[0]) + ""; @@ -2197,7 +2255,9 @@ const JSON_OBJECT = 5; const JSON_MAX_DEPTH = 512; function jsonAppend(output: char[], text: string): void { - for (let i = 0; i < text.length; i++) { + // the length once: it is a walk to the text's end each time it is asked for + const length = text.length; + for (let i = 0; i < length; i++) { output.push(text[i]); } } @@ -2223,7 +2283,8 @@ function jsonHexDigit(digit: int): char { function jsonAppendQuoted(output: char[], text: string): void { output.push(0x22); - for (let i = 0; i < text.length; i++) { + const length = text.length; + for (let i = 0; i < length; i++) { const c = text[i]; let escape = 0; switch (c) { @@ -2947,7 +3008,7 @@ export static class console { let buffer: char[] = []; buffer.length = length + 1; - buffer[length] = null; + buffer[length] = 0; if (length > 0) stdio_line_copy_to(Ref(buffer[0]), length); // a copy: buffer goes with this function return Ref(buffer[0]) + ""; diff --git a/src/wrappers/regex.cpp b/src/wrappers/regex.cpp index 3328da0..1243e5e 100644 --- a/src/wrappers/regex.cpp +++ b/src/wrappers/regex.cpp @@ -50,8 +50,8 @@ extern "C" TSLANG_EXPORT char* regexp_match_results_format(std::cmatch *cm, cons auto len = result.size(); - std::allocator alloc; - char* buffer = alloc.allocate(len + 1); + // new[], given back by regexp_free_string's delete[] + char* buffer = new char[len + 1]; #ifdef _WIN32 memcpy_s(buffer, len + 1, result.c_str(), len); @@ -86,11 +86,15 @@ extern "C" TSLANG_EXPORT int regexp_test(const char *expr, const char *flags, co extern "C" TSLANG_EXPORT std::cmatch *regexp_exec(const char *expr, const char *flags, const char *s, std::cmatch *cm) { + // the results it makes here are the caller's only when it returns them: with no match, or a + // pattern that is not one, they are given back, where they used to be lost + std::cmatch *made = nullptr; try { if (cm == nullptr) { - cm = new std::cmatch(); + made = new std::cmatch(); + cm = made; } if (std::regex_search(s, *cm, std::regex(expr, get_flags(flags)), get_match_flags(flags))) @@ -103,6 +107,7 @@ extern "C" TSLANG_EXPORT std::cmatch *regexp_exec(const char *expr, const char * regexp_error(e.code()); } + delete made; return nullptr; } @@ -115,8 +120,7 @@ extern "C" TSLANG_EXPORT char* regexp_replace(const char *expr, const char *flag auto len = result.size(); - std::allocator alloc; - char* buffer = alloc.allocate(len + 1); + char* buffer = new char[len + 1]; #ifdef _WIN32 memcpy_s(buffer, len + 1, result.c_str(), len); @@ -147,11 +151,7 @@ extern "C" TSLANG_EXPORT void regexp_free(std::cmatch *cm) extern "C" TSLANG_EXPORT void regexp_free_string(char *c) { - if (c != nullptr) - { - std::allocator alloc; - alloc.deallocate(c, 1); - } + delete[] c; } std::regex::flag_type get_flags(const char *flags) diff --git a/tests/array_class_string.ts b/tests/array_class_string.ts index 4616f07..39567f1 100644 --- a/tests/array_class_string.ts +++ b/tests/array_class_string.ts @@ -41,8 +41,11 @@ function main() { assert(a.toReversed().join() == "ccc,bb,x"); assert(a.toSorted().join() == "bb,ccc,x"); - const filled = a.fill("z", 1); - assert(filled.join() == "x,z,z"); + // fill fills the array itself, as in JavaScript + assert(a.fill("z", 1).join() == "x,z,z"); + assert(a.join() == "x,z,z"); + a.set(1, "bb"); + a.set(2, "ccc"); assert(a.pop() == "ccc" && a.shift() == "x" && a.length == 1); a.unshift("p", "q"); diff --git a/tests/array_fill_in_place.ts b/tests/array_fill_in_place.ts new file mode 100644 index 0000000..cf35412 --- /dev/null +++ b/tests/array_fill_in_place.ts @@ -0,0 +1,22 @@ +// fill fills the array itself and gives it back, as in JavaScript: it filled a copy +function main() { + const a = [1, 2, 3, 4]; + a.fill(0); + assert(a[0] == 0 && a[1] == 0 && a[2] == 0 && a[3] == 0, "the array itself is filled"); + + a.fill(7, 1, 3); + assert(a[0] == 0 && a[1] == 7 && a[2] == 7 && a[3] == 0, "a range"); + + a.fill(9, -1); + assert(a[2] == 7 && a[3] == 9, "a negative start counts from the end"); + + a.fill(5, 10); + assert(a[3] == 9, "a start past the end fills nothing"); + + const flags: boolean[] = []; + flags.length = 3; + flags.fill(true); + assert(flags[0] && flags[1] && flags[2], "a new array filled"); + + console.log("ALL DONE"); +} diff --git a/tests/array_findLast2.ts b/tests/array_findLast2.ts new file mode 100644 index 0000000..9f8310a --- /dev/null +++ b/tests/array_findLast2.ts @@ -0,0 +1,13 @@ +// findLast gives undefined when no element passes, as find does: it gave -1 +function main() { + const numbers = [5, 12, 50, 130, 44]; + assert(numbers.findLast((v) => v > 45) == 130, "the last that passes"); + assert(numbers.findLast((v) => v > 1000) == undefined, "undefined when none does"); + assert(numbers.findLast((v, i) => i == 0) == 5, "the index is given too"); + + const words = ["a", "bb", "cc"]; + assert(words.findLast((v) => v.length == 2) == "cc", "strings"); + assert(words.findLast((v) => v.length == 9) == undefined, "no string"); + + console.log("ALL DONE"); +} diff --git a/tests/array_join_empty.ts b/tests/array_join_empty.ts new file mode 100644 index 0000000..59ac993 --- /dev/null +++ b/tests/array_join_empty.ts @@ -0,0 +1,16 @@ +// join puts the separator between every two elements, an empty first one included +function main() { + assert(["", "a", "b"].join(",") == ",a,b", "a leading empty string"); + assert(["", ""].join("-") == "-", "two empty strings"); + assert(["a"].join(",") == "a", "one element"); + const none: string[] = []; + assert(none.join(",") == "", "no elements"); + assert([1, 2, 3].join() == "1,2,3", "numbers, default separator"); + assert([1, 2, 3].join("") == "123", "no separator"); + + const many: string[] = []; + for (let i = 0; i < 20000; i++) many.push("item"); + assert(many.join(",").length == 20000 * 5 - 1, "a long array"); + + console.log("ALL DONE"); +} diff --git a/tests/array_lastIndexOf3.ts b/tests/array_lastIndexOf3.ts new file mode 100644 index 0000000..01b85ae --- /dev/null +++ b/tests/array_lastIndexOf3.ts @@ -0,0 +1,21 @@ +// The fromIndex of lastIndexOf, as in JavaScript: at or past the end the search starts at the last +// element (it gave -1), and before the start there is nothing to search (it searched element 0) +function main() { + const numbers = [2, 5, 9, 2]; + assert(numbers.lastIndexOf(2) == 3); + assert(numbers.lastIndexOf(7) == -1); + assert(numbers.lastIndexOf(2, 3) == 3); + assert(numbers.lastIndexOf(2, 2) == 0); + assert(numbers.lastIndexOf(2, -2) == 0); + assert(numbers.lastIndexOf(2, -1) == 3); + assert(numbers.lastIndexOf(2, 4) == 3, "the length"); + assert(numbers.lastIndexOf(2, 100) == 3, "past the end"); + assert(numbers.lastIndexOf(2, -4) == 0, "the first element, from the end"); + assert(numbers.lastIndexOf(2, -5) == -1, "before the start"); + assert(numbers.lastIndexOf(2, -100) == -1, "far before the start"); + + const none: int[] = []; + assert(none.lastIndexOf(1) == -1, "an empty array"); + + console.log("ALL DONE"); +} diff --git a/tests/array_sort_stable.ts b/tests/array_sort_stable.ts new file mode 100644 index 0000000..e034ee0 --- /dev/null +++ b/tests/array_sort_stable.ts @@ -0,0 +1,40 @@ +// sort is a merge sort: stable, as JavaScript's is, and quick enough for a large array. +function main() { + // pairs sorted by their first half only: equal ones keep the order they came in + const pairs = [31, 12, 33, 11, 21, 32, 13, 22]; + pairs.sort((a: int, b: int) => (a / 10) - (b / 10)); + const wanted = [12, 11, 13, 21, 22, 31, 33, 32]; + for (let i = 0; i < wanted.length; i++) { + assert(pairs[i] == wanted[i], "stable order"); + } + + const empty: int[] = []; + empty.sort((a: int, b: int) => a - b); + assert(empty.length == 0, "empty"); + + const one = [5]; + one.sort((a: int, b: int) => a - b); + assert(one[0] == 5, "one element"); + + const big: int[] = []; + let x = 12345; + for (let i = 0; i < 20000; i++) { + x = (x * 1103515245 + 12345) & 0x7fffffff; + big.push(x % 1000); + } + + big.sort((a: int, b: int) => a - b); + for (let i = 1; i < big.length; i++) { + assert(big[i - 1] <= big[i], "in order"); + } + + const words = ["pear", "apple", "fig", "banana"]; + words.sort(); + assert(words.join(",") == "apple,banana,fig,pear", "default order"); + + const sorted = words.toSorted((a: string, b: string) => a.length - b.length); + assert(sorted.join(",") == "fig,pear,apple,banana", "toSorted, stable by length"); + assert(words[0] == "apple", "toSorted leaves the array"); + + console.log("ALL DONE"); +} diff --git a/tests/headers_get.ts b/tests/headers_get.ts new file mode 100644 index 0000000..163c42b --- /dev/null +++ b/tests/headers_get.ts @@ -0,0 +1,29 @@ +// Headers.get gives the value, or null when there is no such header. The class's private methods +// used to be declared before its public ones, so a program's call of `get` ran another method. +function main() { + const h = new Headers(); + h.set("Content-Type", "text/plain"); + h.append("Accept", "a"); + h.append("accept", "b"); + + const type = h.get("content-type"); + assert(type != null, "a header that is there"); + if (type != null) { + assert(type == "text/plain", "its value"); + } + + const accept = h.get("ACCEPT"); + if (accept != null) { + assert(accept == "a, b", "appended values are joined"); + } + + assert(h.get("x-missing") == null, "null for a header that is not there"); + assert(h.has("accept"), "has"); + assert(!h.has("x-missing"), "has not"); + + h.delete("accept"); + assert(h.get("accept") == null, "null once deleted"); + assert(h.toRawString() == "content-type: text/plain\r\n", "raw"); + + console.log("ALL DONE"); +} diff --git a/tests/math_random.ts b/tests/math_random.ts new file mode 100644 index 0000000..6f6a07a --- /dev/null +++ b/tests/math_random.ts @@ -0,0 +1,22 @@ +// Math.random is spread over [0, 1): on Windows nothing above 0.000016 came back, as the range of +// rand() there is 32767 and the larger one of Linux was assumed +function main() { + let lowest = 1.0; + let highest = 0.0; + let sum = 0.0; + const count = 10000; + for (let i = 0; i < count; i++) { + const r = Math.random(); + assert(r >= 0 && r < 1, "in [0, 1)"); + if (r < lowest) lowest = r; + if (r > highest) highest = r; + sum += r; + } + + assert(lowest < 0.01, "some near 0"); + assert(highest > 0.99, "some near 1"); + const mean = sum / count; + assert(mean > 0.45 && mean < 0.55, "about a half on average"); + + console.log("ALL DONE"); +} diff --git a/tests/regexp_bounds.ts b/tests/regexp_bounds.ts new file mode 100644 index 0000000..738df3a --- /dev/null +++ b/tests/regexp_bounds.ts @@ -0,0 +1,24 @@ +// A lastIndex past the end of the string fails the match and goes back to 0, as in JavaScript: it +// used to read past the string. (An invalid pattern given to replaceAll is a SyntaxError now, where +// it read a null; not tested here, as the wrapper also reports it on stderr, which fails a test.) +function main() { + const re = /b/g; + re.lastIndex = 1000; + assert(re.exec("abc") == null, "exec past the end"); + assert(re.lastIndex == 0, "lastIndex is 0 again"); + + re.lastIndex = 1000; + assert(!re.test("abc"), "test past the end"); + assert(re.lastIndex == 0, "lastIndex is 0 again after test"); + assert(re.test("abc"), "and the next search starts over"); + + const none = "abc".match(/z/); + assert(none == null, "match with no match is null"); + const some = "abc".match(/b/); + assert(some != null, "match with a match"); + if (some != null) { + assert(some.length == 1 && some[0] == "b", "the match"); + } + + console.log("ALL DONE"); +} diff --git a/tests/string_search3.ts b/tests/string_search3.ts new file mode 100644 index 0000000..001bd07 --- /dev/null +++ b/tests/string_search3.ts @@ -0,0 +1,12 @@ +// search gives where the first match starts, whatever its length: it gave where the match ends, less +// one, which is right only for a match of one character +function main() { + assert("abcdef".search(/cd/) == 2, "a match of two characters"); + assert("abcdef".search(/abc/) == 0, "a match at the start"); + assert("abcdef".search(/def/) == 3, "a match at the end"); + assert("abcdef".search(/x/) == -1, "no match"); + assert("abcdef".search(/f/) == 5, "a match of one character"); + assert("hello world".search(/o\s*w/) == 4, "a longer pattern"); + + console.log("ALL DONE"); +}