Files
scriptc/packages/experimental/index.js
T
Chris Tate 8dc5dc1249 Refuse InternTable array keys that are too long to encode
Encoding an array key added each element's size to a running length
with unchecked arithmetic, and wrote into a fixed stack buffer whenever
the sum fit. A sparse array with a large length (2^29 elements or more)
wrapped the sum on 32-bit targets, so the write landed before the buffer;
on every target the encoder also visited every index of such an array.

Array keys now have at most 65536 elements, and their encoding at most
2^31 - 1 bytes. The size is computed first with sums that cannot wrap,
the buffer is sized to it, and keys over either limit make the lookup or
insert throw a TypeError before anything is read or written. The Node.js
implementation throws the same TypeError for keys of more than 65536
elements. Sparse elements are walked in one pass in index order, and the
index growth computation checks its size arithmetic too.
2026-10-15 21:44:38 -05:00

93 lines
2.9 KiB
JavaScript

// @scriptc/experimental — the Node.js implementation.
//
// scriptc builds lower these exports to compiler intrinsics. Under Node.js
// a LifetimeRegion is an ordinary object: run() calls its callback and
// release() does nothing, since the garbage collector reclaims what the
// program no longer references. An InternTable is a Map per name and
// thread: Node.js threads share no objects, so every thread has its own
// tables, and stored values are published (deep-frozen) as in scriptc.
import { publish } from "@scriptc/threads";
export class LifetimeRegion {
/** Calls `fn` and returns its result. */
run(fn) {
return fn();
}
/** Does nothing under Node.js. */
release() {}
}
/** This thread's tables, by name. */
const tables = new Map();
/** The Map key of a table key: strings and numbers as they are, arrays as a
* string that encodes each element (numbers by SameValueZero, strings by
* content, holes and undefined elements alike). Arrays of more than 65536
* elements are refused, as in scriptc builds. */
function keyOf(key) {
if (!Array.isArray(key)) return key;
if (key.length > 65536)
throw new TypeError("InternTable array keys can have at most 65536 elements");
let text = "";
for (let i = 0; i < key.length; i++) {
const element = key[i];
if (typeof element === "number") text += `n${Object.is(element, -0) ? 0 : element};`;
else if (typeof element === "string") text += `s${element.length}:${element}`;
else text += "u;";
}
return text;
}
export class InternTable {
#entries;
/** Opens the table called `name`. */
constructor(name) {
if (typeof name !== "string") throw new TypeError("The table name must be a string");
let entries = tables.get(name);
if (entries === undefined) tables.set(name, (entries = new Map()));
this.#entries = entries;
}
/** The number of entries. */
get size() {
return this.#entries.size;
}
/** The value stored under `key`, or undefined. */
get(key) {
return this.#entries.get(keyOf(key));
}
/** Whether `key` has a value. */
has(key) {
return this.#entries.has(keyOf(key));
}
/** The value stored under `key`; when there is none, publishes `value`,
* stores it and returns it. */
getOrInsert(key, value) {
const entries = this.#entries;
const k = keyOf(key);
if (entries.has(k)) return entries.get(k);
publish(value);
entries.set(k, value);
return value;
}
/** The value stored under `key`; when there is none, publishes
* `callback(key)` and stores it, unless the callback stored a value under
* `key` itself. Returns the stored value. */
getOrInsertComputed(key, callback) {
const entries = this.#entries;
const k = keyOf(key);
if (entries.has(k)) return entries.get(k);
const value = publish(callback(key));
if (entries.has(k)) return entries.get(k);
entries.set(k, value);
return value;
}
}