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"); +}