Support array keys in InternTable and free replaced indexes (#1072)

This commit is contained in:
Chris Tate
2026-10-15 22:50:08 -05:00
committed by GitHub
22 changed files with 813 additions and 85 deletions
+2 -2
View File
@@ -159,7 +159,7 @@ worker: square: 4 lines, 3 shapes
<dl>
<dt><code>new InternTable&lt;K, V&gt;(name)</code></dt>
<dd>Opens the table called <code>name</code>. Within one thread every construction with the same name refers to the same table. In scriptc builds every thread of the process shares it; under Node.js each thread has its own. Keys are strings or numbers and compare like <code>Map</code> keys.</dd>
<dd>Opens the table called <code>name</code>. Within one thread every construction with the same name refers to the same table. In scriptc builds every thread of the process shares it; under Node.js each thread has its own. Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like <code>Map</code> keys; arrays compare by their elements in the same way and can have at most 65536 elements.</dd>
<dt><code>table.get(key)</code>, <code>table.has(key)</code>, <code>table.size</code></dt>
<dd>Read the table.</dd>
<dt><code>table.getOrInsert(key, value)</code></dt>
@@ -180,7 +180,7 @@ No lock is held while a callback runs. Two threads that miss the same key at the
2. It does not compare identities or ids across threads.
3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js.
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value: module state is per thread in both runtimes.
State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
### Memory
@@ -8833,7 +8833,8 @@ export class LlEmitter {
/** InternTable's runtime calls (scr_itable.c). Table and entry handles
* are f64 values carrying pointer bits; entry values are 64-bit slots
* (double bits, a bool, or a pointer to an immortal value) that the
* table neither retains nor releases. */
* table neither retains nor releases. Lookups and inserts of array keys
* throw a TypeError for keys longer than a table accepts. */
private emitInternTableIntrinsic(e: ExprOf<"intrinsic">): LlValue {
const B = this.B;
// The runtime copies keys and stores published values without counts,
@@ -8855,8 +8856,9 @@ export class LlEmitter {
return { name: handle, type: e.type };
};
const keyCall = (op: "find" | "insert"): string => {
const key = e.args[1]!.type.kind === "string" ? "str" : "f64";
const keyTy = key === "str" ? "ptr" : "double";
const kind = e.args[1]!.type.kind;
const key = kind === "string" ? "str" : kind === "array" ? "arr" : "f64";
const keyTy = key === "f64" ? "double" : "ptr";
const value = op === "insert" ? ", i64" : "";
this.declare(`declare ptr @scr_itable_${op}_${key}(ptr, ${keyTy}${value})`);
return `@scr_itable_${op}_${key}(ptr ${toPtr(args[0]!.name)}, ${keyTy} ${args[1]!.name}`;
@@ -8874,6 +8876,8 @@ export class LlEmitter {
const call = keyCall("find");
const entry = B.tmp();
B.line(`${entry} = call ptr ${call})`);
// Array keys longer than a table accepts throw a TypeError.
if (e.args[1]!.type.kind === "array") this.emitPendingCheck();
return toHandle(entry);
}
case "itable.insert": {
@@ -8893,6 +8897,7 @@ export class LlEmitter {
const call = keyCall("insert");
const entry = B.tmp();
B.line(`${entry} = call ptr ${call}, i64 ${slot})`);
if (e.args[1]!.type.kind === "array") this.emitPendingCheck();
return toHandle(entry);
}
case "itable.value": {
@@ -42,6 +42,19 @@ test("InternTable's intrinsics call the runtime and read value slots directly",
intrinsic("itable.value", [inserted], STRING),
intrinsic("itable.size", [table], F64),
intrinsic("itable.find", [intrinsic("itable.open", [str("n"), num(1)], F64), num(1)], F64),
intrinsic(
"itable.find",
[
intrinsic("itable.open", [str("a"), num(2)], F64),
{
kind: "arrayLit",
elems: [num(1), num(2)],
type: { kind: "array", elem: F64 },
loc,
} as IrExpr,
],
F64,
),
];
expect(validateModule(module(calls))).toEqual([]);
const main = body(emitLlvmModule(module(calls), { pointerBits: 64 }), "sc_f_main");
@@ -50,6 +63,7 @@ test("InternTable's intrinsics call the runtime and read value slots directly",
expect(main).toMatch(/call ptr @scr_itable_insert_str\(ptr %\S+, ptr @\S+, i64 %\S+\)/);
expect(main).toMatch(/call ptr @scr_itable_find_f64\(ptr %\S+, double \S+\)/);
expect(main).toMatch(/call double @scr_itable_size\(ptr %\S+\)/);
expect(main).toMatch(/call ptr @scr_itable_find_arr\(ptr %\S+, ptr %\S+\)/);
// Entry handles reach their load through memory (x86 FastISel folds
// inttoptr of a bitcast double into the address without moving it out
// of its XMM register at -O0).
@@ -68,7 +82,7 @@ test("the validator checks InternTable intrinsic operands", () => {
];
expect(validateModule(module(bad)).map((e) => e.message)).toEqual([
"in main: itable.open takes a string name and an f64 key kind",
"in main: itable.find key must be string or f64",
"in main: itable.find key must be string, f64, or an array of either",
"in main: itable.insert takes 3 arguments",
"in main: itable.size has type f64",
]);
@@ -196,6 +196,12 @@ export function computeMayThrow(
if (rec.name === "module.tdzCheck") f.throws = true;
// publish() throws on an unpublishable dyn value.
if (rec.name === "threads.publish") f.throws = true;
// An InternTable array key longer than a table accepts.
if (
(rec.name === "itable.find" || rec.name === "itable.insert") &&
rec.args[1]?.type.kind === "array"
)
f.throws = true;
break;
case "yieldExpr":
// A consumer .throw() surfaces at the yield (and .return()'s
@@ -18,7 +18,8 @@
* publication analysis guards stores into the value's types), and reads
* the entry's value (itable.value). `getOrInsertComputed` with a
* function expression lowers in place instead, and creates the callback
* only when the key has no entry.
* only when the key has no entry; an arrow function with an expression
* body is not created at all: its body is lowered in place.
* The entry handles that itable.find and itable.insert answer stay inside
* these lowerings: f64 locals that only reach toBool and itable.value.
* Any other use of the class or its members as values is refused. */
@@ -41,8 +42,15 @@ const TABLE_FIELD = "#table";
const BUILTIN_LOC: SrcLoc = { file: "<builtin>", start: 0, end: 0 };
/** Runtime key kinds (scr_itable.h SCR_ITABLE_KEY_*). */
const KEY_KIND: Record<string, number> = { string: 0, f64: 1 };
/** The runtime key kind of a key type (scr_itable.h SCR_ITABLE_KEY_*):
* strings, numbers, and arrays of numbers or of strings. */
function keyKind(key: IrType): number | null {
if (key.kind === "string") return 0;
if (key.kind === "f64") return 1;
if (key.kind === "array" && key.elem.kind === "f64") return 2;
if (key.kind === "array" && key.elem.kind === "string") return 3;
return null;
}
/** Value types a table stores: the publishable Map value types. */
const VALUE_KINDS = new Set<IrType["kind"]>([
@@ -138,12 +146,12 @@ function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes
const args = table ? lowerer.checker.getTypeArguments(table as ts.TypeReference) : [];
if (args.length !== 2) lowerer.noLowering("InternTable without type arguments", blame);
const key = lowerer.mapTypeOf(args[0]!);
if (key === null || (key.kind !== "string" && key.kind !== "f64"))
if (key === null || keyKind(key) === null)
lowerer.unsupported(
"SC1090",
blame,
`'${lowerer.checker.typeToString(args[0]!)}' as an InternTable key type`,
"use string or number keys",
"use string or number keys, or arrays of numbers or of strings",
);
const value = lowerer.mapTypeOf(args[1]!);
if (value === null || !VALUE_KINDS.has(value.kind))
@@ -189,7 +197,7 @@ export function lowerInternTableNew(lowerer: Lowerer, expr: ts.NewExpression): I
className: INTERN_TABLE_CLASS,
args: [
{ kind: "strLit", value: name, type: STRING, loc },
{ kind: "numLit", value: KEY_KIND[types.key.kind]!, type: F64, loc },
{ kind: "numLit", value: keyKind(types.key)!, type: F64, loc },
],
type: internTableType(lowerer),
loc,
@@ -300,7 +308,9 @@ export function lowerInternTableCall(lowerer: Lowerer, expr: ts.CallExpression):
* e ? itable.value(e) : itable.value(itable.insert(t.#table, k, publish(callback(k))))
*
* Creating a function has no observable effect, so the callback's closure
* is created only when the key has no entry, and a hit allocates nothing. */
* is created only when the key has no entry, and a hit allocates nothing.
* An arrow function with an expression body is lowered in place instead
* (inlineArrowBody). */
function lowerComputedInline(
lowerer: Lowerer,
table: IrExpr,
@@ -316,10 +326,14 @@ function lowerComputedInline(
const entryLocal = lowerer.declareHiddenLocal("%internEntry", F64);
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
const handle = (): IrExpr => handleOf(ref(tableLocal.id, tableT), loc);
const callback = lowerer.coerceInto(callbackNode, lowerer.lowerExpr(callbackNode), callbackType);
const computed: IrExpr = {
const computed: IrExpr = inlineArrowBody(
lowerer,
callbackNode,
ref(keyLocal.id, types.key),
types.value,
) ?? {
kind: "callValue",
callee: callback,
callee: lowerer.coerceInto(callbackNode, lowerer.lowerExpr(callbackNode), callbackType),
args: [ref(keyLocal.id, types.key)],
type: types.value,
loc,
@@ -367,6 +381,64 @@ function lowerComputedInline(
};
}
/** Whether `node` contains a function or class, whose captures of the
* inlined parameter would need a closure after all. */
function containsFunction(node: ts.Node): boolean {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return true;
return ts.forEachChild(node, (child) => (containsFunction(child) ? true : undefined)) === true;
}
/** The body of `() => value` or `(k) => value`, lowered where the callback
* would be called, with the parameter bound to the key. Calling a fresh
* arrow function at once is evaluating its body there with its parameter
* bound, so no closure is created. Other callbacks answer null. */
function inlineArrowBody(
lowerer: Lowerer,
node: ts.Expression,
key: IrExpr,
value: IrType,
): IrExpr | null {
if (!ts.isArrowFunction(node) || ts.isBlock(node.body)) return null;
if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
const param = node.parameters[0];
if (
node.parameters.length > 1 ||
(param !== undefined &&
(!ts.isIdentifier(param.name) ||
param.initializer !== undefined ||
param.dotDotDotToken !== undefined))
)
return null;
if (containsFunction(node.body)) return null;
const body = node.body;
const loc = locOf(node);
lowerer.scopes.push(new Map());
try {
const stmts: IrStmt[] = [];
if (param !== undefined) {
const paramTs = lowerer.typeOf(param);
const paramType = lowerer.mapTypeOf(paramTs);
if (paramType === null) lowerer.badType(param, paramTs);
const local = lowerer.declareLocal(
param.name,
(param.name as ts.Identifier).text,
paramType!,
false,
);
stmts.push({
kind: "varDecl",
localId: local.id,
init: lowerer.coerceToExpected(key, paramType!),
loc,
});
}
const result = lowerer.coerceInto(body, lowerer.lowerExpr(body), value);
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: value, loc };
} finally {
lowerer.scopes.pop();
}
}
function helperName(lowerer: Lowerer, member: InternTableMember, signature: string): string | null {
return lowerer.internTableHelpers.get(`${member}|${signature}`) ?? null;
}
+6 -3
View File
@@ -3481,9 +3481,12 @@ export type IrExpr =
* has no entry; itable.insert (f64 table, K key, V value) → f64 the
* key's entry, storing the already published value when the key has
* none; itable.value (f64 entry) → V, the entry's immortal value;
* itable.size (f64 table) → f64. K is string or f64. Handles carry
* raw pointer bits and only reach toBool and these intrinsics.
* None of them throws. */
* itable.size (f64 table) → f64. K is string, f64, or an array of
* f64 or of strings (compared by elements); find and insert throw
* a TypeError for array keys of more than 65536 elements or whose
* encoding exceeds 2^31 - 1 bytes; nothing else throws. Handles
* carry raw pointer bits and only reach toBool and these
* intrinsics. */
| "itable.open"
| "itable.find"
| "itable.insert"
+6 -2
View File
@@ -3819,9 +3819,13 @@ function validateFunction(
else if (
(e.name === "itable.find" || e.name === "itable.insert") &&
key!.type.kind !== "string" &&
key!.type.kind !== "f64"
key!.type.kind !== "f64" &&
!(
key!.type.kind === "array" &&
(key!.type.elem.kind === "f64" || key!.type.elem.kind === "string")
)
)
err(`${e.name} key must be string or f64`, e.loc);
err(`${e.name} key must be string, f64, or an array of either`, e.loc);
else if (e.name === "itable.insert" && !isSupportedMapValue(value!.type))
err(`itable.insert value ${typeKey(value!.type)} is not a table value type`, e.loc);
if (e.name === "itable.value") {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5724-intern-table-array-keys.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5725-intern-table-callbacks.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5726-intern-table-key-limits.ts"
],
"diags": []
}
+2 -2
View File
@@ -55,7 +55,7 @@ function unionOf(members: readonly Type[]): Type {
```
- `new InternTable<K, V>(name)` opens the table called `name`. Within one thread, every construction with the same name refers to the same table. In scriptc builds the table is shared by every thread of the process; under Node.js each thread has its own.
- Keys are strings or numbers and compare like `Map` keys: strings by content, numbers by SameValueZero.
- Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like `Map` keys: strings by content, numbers by SameValueZero. Arrays compare by their elements in the same way, so a freshly built `[a.id, b.id]` finds the value stored under an equal array; holes are equal to each other and to no number or string. An array key can have at most 65536 elements; longer ones make every method throw a `TypeError`.
- `get(key)`, `has(key)` and `size` read the table.
- `getOrInsert(key, value)` returns the value stored under `key`. When there is none, it publishes `value`, stores it and returns it.
- `getOrInsertComputed(key, callback)` returns the value stored under `key`. When there is none, it calls `callback(key)`, publishes the result and stores it, unless a value was stored under `key` meanwhile (by the callback, or in scriptc builds by another thread), and returns the stored value.
@@ -70,6 +70,6 @@ Programs that behave identically under scriptc and Node.js follow the `@scriptc/
2. Identity and ids are not compared across threads. Ids assigned inside callbacks are unique in both runtimes but differ between them.
3. A thread does not depend on another thread's inserts. `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js.
Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a module-level `Map` keyed by the value: module state is per thread in both runtimes.
Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a module-level `Map` keyed by the value, or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these.
In scriptc builds stored values are immortal: they stay allocated until the process exits, including values a callback computed for a key that another thread stored first.
+9 -3
View File
@@ -24,13 +24,19 @@ export declare class LifetimeRegion {
* publishes it, so stored values and everything they reach are immutable.
* scriptc requires the name to be a string literal and every construction
* of one name to use the same type arguments. */
export declare class InternTable<K extends string | number, V> {
export declare class InternTable<
K extends string | number | readonly number[] | readonly string[],
V,
> {
/** Opens the table called `name`. */
constructor(name: string);
/** The number of entries. */
readonly size: number;
/** The value stored under `key`, or undefined. Keys compare like Map
* keys: strings by content, numbers by SameValueZero. */
/** The value stored under `key`, or undefined. String and number keys
* compare like Map keys: strings by content, numbers by SameValueZero.
* Array keys compare by their elements in the same way; a hole and an
* undefined element are equal. Every method throws a TypeError for an
* array key of more than 65536 elements. */
get(key: K): V | undefined;
/** Whether `key` has a value. */
has(key: K): boolean;
+27 -7
View File
@@ -22,6 +22,24 @@ export class LifetimeRegion {
/** 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;
@@ -40,21 +58,22 @@ export class InternTable {
/** The value stored under `key`, or undefined. */
get(key) {
return this.#entries.get(key);
return this.#entries.get(keyOf(key));
}
/** Whether `key` has a value. */
has(key) {
return this.#entries.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;
if (entries.has(key)) return entries.get(key);
const k = keyOf(key);
if (entries.has(k)) return entries.get(k);
publish(value);
entries.set(key, value);
entries.set(k, value);
return value;
}
@@ -63,10 +82,11 @@ export class InternTable {
* `key` itself. Returns the stored value. */
getOrInsertComputed(key, callback) {
const entries = this.#entries;
if (entries.has(key)) return entries.get(key);
const k = keyOf(key);
if (entries.has(k)) return entries.get(k);
const value = publish(callback(key));
if (entries.has(key)) return entries.get(key);
entries.set(key, value);
if (entries.has(k)) return entries.get(k);
entries.set(k, value);
return value;
}
}
+320 -45
View File
@@ -5,9 +5,13 @@
* key's 64-bit hash pick the segment and the low bits the first slot of a
* linear probe in the segment's index. Number tables keep each key and its
* value in the index slot itself, so a lookup touches one slot per probe.
* String tables keep entries (value, hash and key bytes) in chunks owned by
* the segment; their slots hold the entry pointer with a tag of hash bits
* 50..57 in bits 48..55, so a probe reads an entry only when the tags match.
* String and array tables keep entries (value, hash and key bytes) in
* chunks owned by the segment; their slots hold the entry pointer with a
* tag of hash bits 50..57 in bits 48..55, so a probe reads an entry only
* when the tags match. An array key is stored as one byte string that
* encodes its elements (scr_itable_encode), so array keys compare by
* their elements; keys longer or larger than a table accepts are refused
* with a TypeError before anything is encoded.
*
* Readers load the segment's index pointer, then slots with acquire order,
* and never write. Writers hold the segment's lock: they probe again, grow
@@ -15,13 +19,12 @@
* value (and the entry), and store the slot's last word with release order.
* A grown index is complete before its pointer is stored with release
* order, and a reader that still holds the previous one sees every key
* stored before the growth. Replaced indexes are kept on the segment's
* retired list, because readers may still be probing them (and number
* handles point into them); their total size stays below the current
* index's.
* stored before the growth. A replaced index is retired and freed once no
* thread can still be probing it (see "retired indexes" below).
*
* Nothing here is freed. Every allocation is reachable from the static
* registry, so leak checkers report none of it. */
* Tables, their entries and their current indexes are never freed. Every
* such allocation is reachable from the static registry, so leak checkers
* report none of it. */
#include "scr_runtime.h"
#include "scr_itable.h"
@@ -42,6 +45,12 @@
#define SCR_ITABLE_FIRST_SLOTS 8u
#define SCR_ITABLE_FIRST_CHUNK ((size_t)512)
#define SCR_ITABLE_MAX_CHUNK ((size_t)64 << 10)
/* Keys whose bytes (a string's, or an array key's encoding) exceed this
* are refused, which keeps every entry size representable on 32-bit
* targets. Array keys of more than SCR_ITABLE_MAX_KEY_ELEMENTS elements are
* refused too; the Node.js implementation applies the same limit. */
#define SCR_ITABLE_MAX_KEY_BYTES ((size_t)SCR_STR_MAX_CAP)
#define SCR_ITABLE_MAX_KEY_ELEMENTS 65536u
/* String slots: 0 is empty; otherwise the entry pointer with the tag in
* bits 48..55, which no supported target uses for user-space addresses
@@ -50,17 +59,22 @@
#define SCR_ITABLE_TAG_MASK ((uint64_t)0xff << SCR_ITABLE_TAG_SHIFT)
/* Number slots are { value, key }, with the key stored as its normalized
* bits XOR this NaN pattern, which normalization never produces: a zero
* key word is an empty slot. */
* bits XOR this NaN pattern, which normalization never produces (so no
* stored key is 0), and a tag byte of hash bits 48..55 (0 mapped to 1). */
#define SCR_ITABLE_F64_FLIP UINT64_C(0x7ff4000000000001)
typedef struct ScrITableIndex {
uint64_t mask; /* slots - 1 */
struct ScrITableIndex *retired; /* the index this one replaced */
struct ScrITableIndex *retired; /* the retired list's next index */
uint64_t epoch; /* the epoch it was retired in */
/* Number indexes: one tag byte per slot (0: empty), after the words. A
* probe reads the dense tags first, so a miss rarely touches a pair. */
_Atomic uint8_t *tags;
_Atomic uint64_t words[]; /* one word per string slot, two per number slot */
} ScrITableIndex;
/* A string table entry: the value slot first (scr_itable.h). */
/* A string or array table entry: the value slot first (scr_itable.h),
* then the key's bytes. */
typedef struct {
uint64_t value;
uint64_t hash;
@@ -125,6 +139,92 @@ static void scr_itable_unlock(atomic_uint *lock) {
atomic_store_explicit(lock, 0, memory_order_release);
}
/* ── retired indexes ─────────────────────────────────────────────────
* A thread that loaded a segment's index pointer before the index grew may
* still be probing the old index (or about to read a number entry handle
* that points into it). Every thread that uses a table has a reader record:
* at the start of each lookup and insert it records the global epoch it
* reads there, with release order. Growth publishes the new index, retires
* the old one under the current epoch and advances the epoch. A retired
* index is freed once every registered reader has recorded a later epoch:
* such a reader began its current operation after the growth, so it reads
* the new pointer, and the operations before, which may have read the old
* one, are complete. Entry handles are used before the thread's next lookup
* or insert, so the same rule covers them. A thread that stops using tables
* keeps its last epoch, and with it later retirements, until it uses a
* table again or exits (scr_itable_thread_exit). */
typedef struct ScrITableReader {
_Atomic uint64_t seen; /* SCR_ITABLE_IDLE: the record is unused */
struct ScrITableReader *next;
} ScrITableReader;
#define SCR_ITABLE_IDLE UINT64_MAX
static _Atomic uint64_t scr_itable_epoch = 1;
static ScrITableReader *_Atomic scr_itable_readers;
static SCR_TL ScrITableReader *scr_itable_reader;
static atomic_uint scr_itable_retired_lock;
static ScrITableIndex *scr_itable_retired; /* newest first */
static ScrITableReader *scr_itable_reader_new(void) {
/* Reuse the record of an exited thread, else add one. */
for (ScrITableReader *r = atomic_load_explicit(&scr_itable_readers, memory_order_acquire); r; r = r->next) {
uint64_t idle = SCR_ITABLE_IDLE;
if (atomic_compare_exchange_strong_explicit(&r->seen, &idle, 0, memory_order_acq_rel, memory_order_relaxed))
return r;
}
ScrITableReader *r = calloc(1, sizeof *r);
if (!r) scr_itable_oom();
r->next = atomic_load_explicit(&scr_itable_readers, memory_order_relaxed);
while (!atomic_compare_exchange_weak_explicit(&scr_itable_readers, &r->next, r, memory_order_release,
memory_order_relaxed)) {
}
return r;
}
/* The start of a lookup or insert: the calling thread holds nothing it
* read before. */
static inline void scr_itable_enter(void) {
ScrITableReader *r = scr_itable_reader;
if (__builtin_expect(r == NULL, 0)) r = scr_itable_reader = scr_itable_reader_new();
uint64_t epoch = atomic_load_explicit(&scr_itable_epoch, memory_order_acquire);
if (atomic_load_explicit(&r->seen, memory_order_relaxed) != epoch)
atomic_store_explicit(&r->seen, epoch, memory_order_release);
}
void scr_itable_thread_exit(void) {
ScrITableReader *r = scr_itable_reader;
if (!r) return;
scr_itable_reader = NULL;
atomic_store_explicit(&r->seen, SCR_ITABLE_IDLE, memory_order_release);
}
/* Retires `index` (the caller has published its replacement) and frees
* the retired indexes no reader can still hold. */
static void scr_itable_retire(ScrITableIndex *index) {
scr_itable_lock(&scr_itable_retired_lock);
index->epoch = atomic_fetch_add_explicit(&scr_itable_epoch, 1, memory_order_acq_rel);
index->retired = scr_itable_retired;
scr_itable_retired = index;
uint64_t oldest = SCR_ITABLE_IDLE;
for (ScrITableReader *r = atomic_load_explicit(&scr_itable_readers, memory_order_acquire); r; r = r->next) {
uint64_t seen = atomic_load_explicit(&r->seen, memory_order_acquire);
if (seen < oldest) oldest = seen;
}
/* Free every index retired before the oldest recorded epoch. */
ScrITableIndex **link = &scr_itable_retired;
while (*link) {
ScrITableIndex *old = *link;
if (old->epoch < oldest) {
*link = old->retired;
free(old);
} else {
link = &old->retired;
}
}
scr_itable_unlock(&scr_itable_retired_lock);
}
/* ── keys and hashes ─────────────────────────────────────────────────── */
static inline uint64_t scr_itable_f64_bits(double k) {
@@ -161,6 +261,11 @@ static inline ScrITableSegment *scr_itable_segment(ScrITable *t, uint64_t hash)
return &t->segments[hash >> (64 - SCR_ITABLE_SEGMENT_BITS)];
}
static inline uint8_t scr_itable_tag8(uint64_t hash) {
uint8_t tag = (uint8_t)(hash >> 48);
return tag ? tag : 1;
}
static inline uint64_t scr_itable_tag(uint64_t hash) {
return ((hash >> 50) & 0xff) << SCR_ITABLE_TAG_SHIFT;
}
@@ -171,8 +276,8 @@ static inline const ScrITableStrEntry *scr_itable_slot_entry(uint64_t slot) {
/* ── lookups ─────────────────────────────────────────────────────────── */
static const ScrITableStrEntry *scr_itable_probe_str(const ScrITableIndex *index, uint64_t hash,
const ScrStr *key) {
static const ScrITableStrEntry *scr_itable_probe_bytes(const ScrITableIndex *index, uint64_t hash,
const char *bytes, size_t len) {
if (!index) return NULL;
uint64_t tag = scr_itable_tag(hash);
for (uint64_t i = hash & index->mask;; i = (i + 1) & index->mask) {
@@ -180,28 +285,32 @@ static const ScrITableStrEntry *scr_itable_probe_str(const ScrITableIndex *index
if (slot == 0) return NULL;
if ((slot & SCR_ITABLE_TAG_MASK) != tag) continue;
const ScrITableStrEntry *e = scr_itable_slot_entry(slot);
if (e->hash == hash && e->len == key->len && scr_key_equal(e->data, key->data, key->len)) return e;
if (e->hash == hash && e->len == len && scr_key_equal(e->data, bytes, len)) return e;
}
}
/* The { value, key } pair of a stored number key, or NULL. */
static _Atomic uint64_t *scr_itable_probe_f64(ScrITableIndex *index, uint64_t hash, uint64_t stored) {
if (!index) return NULL;
uint8_t tag = scr_itable_tag8(hash);
for (uint64_t i = hash & index->mask;; i = (i + 1) & index->mask) {
uint64_t key = atomic_load_explicit(&index->words[2 * i + 1], memory_order_acquire);
if (key == stored) return &index->words[2 * i];
if (key == 0) return NULL;
uint8_t seen = atomic_load_explicit(&index->tags[i], memory_order_acquire);
if (seen == 0) return NULL;
if (seen == tag && atomic_load_explicit(&index->words[2 * i + 1], memory_order_relaxed) == stored)
return &index->words[2 * i];
}
}
const ScrITableEntry *scr_itable_find_str(ScrITable *t, const ScrStr *key) {
scr_itable_enter();
uint64_t hash = scr_itable_str_hash(key);
ScrITableSegment *seg = scr_itable_segment(t, hash);
return (const ScrITableEntry *)scr_itable_probe_str(
atomic_load_explicit(&seg->index, memory_order_acquire), hash, key);
return (const ScrITableEntry *)scr_itable_probe_bytes(
atomic_load_explicit(&seg->index, memory_order_acquire), hash, key->data, key->len);
}
const ScrITableEntry *scr_itable_find_f64(ScrITable *t, double key) {
scr_itable_enter();
uint64_t stored = scr_itable_f64_bits(key) ^ SCR_ITABLE_F64_FLIP;
uint64_t hash = scr_itable_f64_hash(stored);
ScrITableSegment *seg = scr_itable_segment(t, hash);
@@ -211,46 +320,58 @@ const ScrITableEntry *scr_itable_find_f64(ScrITable *t, double key) {
/* ── inserts (the segment lock held) ─────────────────────────────────── */
static ScrITableIndex *scr_itable_index_new(uint64_t slots, unsigned words) {
ScrITableIndex *index = calloc(1, sizeof(ScrITableIndex) + slots * words * sizeof(uint64_t));
static ScrITableIndex *scr_itable_index_new(uint64_t slots, bool numbers) {
/* Bytes per slot: the words and, for numbers, the tag byte. */
size_t per = (numbers ? 2 : 1) * sizeof(uint64_t) + (numbers ? 1 : 0);
if (slots > (SIZE_MAX - sizeof(ScrITableIndex)) / per) scr_itable_oom();
size_t words = (size_t)slots * (numbers ? 2 : 1) * sizeof(uint64_t);
ScrITableIndex *index = calloc(1, sizeof(ScrITableIndex) + words + (numbers ? slots : 0));
if (!index) scr_itable_oom();
index->mask = slots - 1;
if (numbers) index->tags = (_Atomic uint8_t *)((char *)index->words + words);
return index;
}
/* Whether slot `i` holds a key (the caller holds the segment lock). */
static inline bool scr_itable_used(const ScrITableIndex *index, uint64_t i, bool numbers) {
if (numbers) return atomic_load_explicit(&index->tags[i], memory_order_relaxed) != 0;
return atomic_load_explicit(&index->words[i], memory_order_relaxed) != 0;
}
/* Makes room for one more key: the segment's index, created or grown so
* that it stays at most half full. Growth rehashes every stored key. */
static ScrITableIndex *scr_itable_reserve(ScrITableSegment *seg, bool numbers) {
ScrITableIndex *index = atomic_load_explicit(&seg->index, memory_order_relaxed);
uint64_t count = atomic_load_explicit(&seg->count, memory_order_relaxed);
unsigned words = numbers ? 2 : 1;
if (index && (count + 1) * 2 <= index->mask + 1) return index;
ScrITableIndex *grown = scr_itable_index_new(index ? (index->mask + 1) * 2 : SCR_ITABLE_FIRST_SLOTS, words);
ScrITableIndex *grown = scr_itable_index_new(index ? (index->mask + 1) * 2 : SCR_ITABLE_FIRST_SLOTS, numbers);
if (index) {
for (uint64_t i = 0; i <= index->mask; i++) {
uint64_t last = atomic_load_explicit(&index->words[i * words + words - 1], memory_order_relaxed);
if (last == 0) continue;
if (!scr_itable_used(index, i, numbers)) continue;
uint64_t last = atomic_load_explicit(&index->words[numbers ? 2 * i + 1 : i], memory_order_relaxed);
uint64_t hash = numbers ? scr_itable_f64_hash(last) : scr_itable_slot_entry(last)->hash;
uint64_t j = hash & grown->mask;
while (atomic_load_explicit(&grown->words[j * words + words - 1], memory_order_relaxed) != 0)
j = (j + 1) & grown->mask;
if (numbers)
while (scr_itable_used(grown, j, numbers)) j = (j + 1) & grown->mask;
if (numbers) {
atomic_store_explicit(&grown->words[2 * j],
atomic_load_explicit(&index->words[2 * i], memory_order_relaxed),
memory_order_relaxed);
atomic_store_explicit(&grown->words[j * words + words - 1], last, memory_order_relaxed);
atomic_store_explicit(&grown->words[2 * j + 1], last, memory_order_relaxed);
atomic_store_explicit(&grown->tags[j], scr_itable_tag8(hash), memory_order_relaxed);
} else {
atomic_store_explicit(&grown->words[j], last, memory_order_relaxed);
}
}
grown->retired = index;
}
atomic_store_explicit(&seg->index, grown, memory_order_release);
if (index) scr_itable_retire(index);
return grown;
}
/* The first empty slot of the key's probe (the key is known to be absent). */
static uint64_t scr_itable_free_slot(const ScrITableIndex *index, uint64_t hash, unsigned words) {
static uint64_t scr_itable_free_slot(const ScrITableIndex *index, uint64_t hash, bool numbers) {
uint64_t i = hash & index->mask;
while (atomic_load_explicit(&index->words[i * words + words - 1], memory_order_relaxed) != 0)
i = (i + 1) & index->mask;
while (scr_itable_used(index, i, numbers)) i = (i + 1) & index->mask;
return i;
}
@@ -279,21 +400,23 @@ static void *scr_itable_alloc(ScrITableSegment *seg, size_t size) {
return p;
}
const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uint64_t value) {
if (key->len > UINT32_MAX) scr_trap("scriptc: InternTable key is too long\n");
uint64_t hash = scr_itable_str_hash(key);
/* Stores `value` under a byte key (a string's bytes or an encoded array)
* unless the key has an entry. */
static const ScrITableEntry *scr_itable_insert_bytes(ScrITable *t, uint64_t hash, const char *bytes,
size_t len, uint64_t value) {
if (len > SCR_ITABLE_MAX_KEY_BYTES) scr_trap("scriptc: InternTable key is too long\n");
ScrITableSegment *seg = scr_itable_segment(t, hash);
scr_itable_lock(&seg->lock);
const ScrITableStrEntry *found =
scr_itable_probe_str(atomic_load_explicit(&seg->index, memory_order_relaxed), hash, key);
scr_itable_probe_bytes(atomic_load_explicit(&seg->index, memory_order_relaxed), hash, bytes, len);
if (!found) {
ScrITableIndex *index = scr_itable_reserve(seg, false);
ScrITableStrEntry *entry = scr_itable_alloc(seg, sizeof(ScrITableStrEntry) + key->len);
ScrITableStrEntry *entry = scr_itable_alloc(seg, sizeof(ScrITableStrEntry) + len);
entry->value = value;
entry->hash = hash;
entry->len = (uint32_t)key->len;
memcpy(entry->data, key->data, key->len);
uint64_t i = scr_itable_free_slot(index, hash, 1);
entry->len = (uint32_t)len;
memcpy(entry->data, bytes, len);
uint64_t i = scr_itable_free_slot(index, hash, false);
atomic_store_explicit(&index->words[i], (uint64_t)(uintptr_t)entry | scr_itable_tag(hash),
memory_order_release);
scr_itable_counted(seg);
@@ -303,7 +426,13 @@ const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uin
return (const ScrITableEntry *)found;
}
const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uint64_t value) {
scr_itable_enter();
return scr_itable_insert_bytes(t, scr_itable_str_hash(key), key->data, key->len, value);
}
const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t value) {
scr_itable_enter();
uint64_t stored = scr_itable_f64_bits(key) ^ SCR_ITABLE_F64_FLIP;
uint64_t hash = scr_itable_f64_hash(stored);
ScrITableSegment *seg = scr_itable_segment(t, hash);
@@ -312,9 +441,10 @@ const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t v
scr_itable_probe_f64(atomic_load_explicit(&seg->index, memory_order_relaxed), hash, stored);
if (!found) {
ScrITableIndex *index = scr_itable_reserve(seg, true);
uint64_t i = scr_itable_free_slot(index, hash, 2);
uint64_t i = scr_itable_free_slot(index, hash, true);
atomic_store_explicit(&index->words[2 * i], value, memory_order_relaxed);
atomic_store_explicit(&index->words[2 * i + 1], stored, memory_order_release);
atomic_store_explicit(&index->words[2 * i + 1], stored, memory_order_relaxed);
atomic_store_explicit(&index->tags[i], scr_itable_tag8(hash), memory_order_release);
scr_itable_counted(seg);
found = &index->words[2 * i];
}
@@ -322,6 +452,151 @@ const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t v
return (const ScrITableEntry *)found;
}
/* ── array keys ──────────────────────────────────────────────────────── */
/* A walk over the elements of an array in index order. Sparse entries
* are sorted by index (scr_array.h), so one pass visits each once. */
typedef struct {
const ScrArr *a;
size_t sparse;
} ScrITableElements;
/* The state and slot of element `i` (i < len, increasing across calls). */
static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t *slot) {
const ScrArr *a = w->a;
if (i < a->filled) {
*slot = a->data[i];
return SCR_ARR_VALUE;
}
if (i < a->cap) {
*slot = a->data[i];
return (ScrArrState)a->present[i];
}
if (a->ext) {
while (w->sparse < a->ext->sparse_len && a->ext->sparse[w->sparse].index < i) w->sparse++;
if (w->sparse < a->ext->sparse_len && a->ext->sparse[w->sparse].index == i) {
*slot = a->ext->sparse[w->sparse].slot;
return (ScrArrState)a->ext->sparse[w->sparse].state;
}
}
return SCR_ARR_HOLE;
}
/* An array key as bytes: per number element its normalized bits, per
* string element a 32-bit length and the bytes; a hole or undefined
* element is the bits of a NaN that normalization never produces, or the
* length 0xffffffff. */
#define SCR_ITABLE_ABSENT_F64 UINT64_C(0x7ff4000000000002)
#define SCR_ITABLE_ABSENT_STR UINT32_C(0xffffffff)
static void scr_itable_refuse(const char *message) {
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
}
/* The encoded size of `key`, or false (with a pending TypeError) when the
* key is longer or larger than a table accepts. Every sum stays at most
* SCR_ITABLE_MAX_KEY_BYTES, so none can wrap. */
static bool scr_itable_encoded_size(const ScrArr *key, int kind, size_t *out) {
if (key->len > SCR_ITABLE_MAX_KEY_ELEMENTS) {
scr_itable_refuse("InternTable array keys can have at most 65536 elements");
return false;
}
size_t n = 0;
ScrITableElements walk = {key, 0};
for (size_t i = 0; i < key->len; i++) {
size_t add = kind == SCR_ITABLE_KEY_F64S ? 8 : 4;
if (kind != SCR_ITABLE_KEY_F64S) {
uint64_t slot = 0;
if (scr_itable_element(&walk, i, &slot) == SCR_ARR_VALUE) {
const ScrStr *s = (const ScrStr *)(uintptr_t)slot;
if (s->len > SCR_ITABLE_MAX_KEY_BYTES - add) add = SCR_ITABLE_MAX_KEY_BYTES + 1;
else add += s->len;
}
}
if (add > SCR_ITABLE_MAX_KEY_BYTES - n) {
scr_itable_refuse("InternTable array keys can encode at most 2147483647 bytes");
return false;
}
n += add;
}
*out = n;
return true;
}
/* Writes the encoding of `key` into `out`, which holds exactly the size
* scr_itable_encoded_size answered. */
static void scr_itable_encode(const ScrArr *key, int kind, char *out) {
size_t n = 0;
ScrITableElements walk = {key, 0};
for (size_t i = 0; i < key->len; i++) {
uint64_t slot = 0;
bool present = scr_itable_element(&walk, i, &slot) == SCR_ARR_VALUE;
if (kind == SCR_ITABLE_KEY_F64S) {
uint64_t bits = SCR_ITABLE_ABSENT_F64;
if (present) {
double d;
memcpy(&d, &slot, sizeof d);
bits = scr_itable_f64_bits(d);
}
memcpy(out + n, &bits, 8);
n += 8;
} else {
const ScrStr *s = present ? (const ScrStr *)(uintptr_t)slot : NULL;
uint32_t len = s ? (uint32_t)s->len : SCR_ITABLE_ABSENT_STR;
memcpy(out + n, &len, 4);
n += 4;
if (s) {
memcpy(out + n, s->data, s->len);
n += s->len;
}
}
}
}
static inline uint64_t scr_itable_bytes_hash(const char *bytes, size_t len) {
return scr_key_mix(scr_key_hash(bytes, len) ^ ((uint64_t)len << 1));
}
/* Encodes `key` into `stack` when it fits, else into a heap buffer the
* caller frees; NULL (with a pending TypeError) for a refused key. */
static char *scr_itable_encoded(const ScrITable *t, const ScrArr *key, char *stack, size_t cap, size_t *len) {
if (!scr_itable_encoded_size(key, t->key_kind, len)) return NULL;
char *bytes = stack;
if (*len > cap) {
bytes = malloc(*len);
if (!bytes) scr_itable_oom();
}
scr_itable_encode(key, t->key_kind, bytes);
return bytes;
}
#define SCR_ITABLE_STACK_KEY 256
const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key) {
scr_itable_enter();
char stack[SCR_ITABLE_STACK_KEY];
size_t len;
char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len);
if (!bytes) return NULL;
uint64_t hash = scr_itable_bytes_hash(bytes, len);
ScrITableSegment *seg = scr_itable_segment(t, hash);
const ScrITableEntry *found = (const ScrITableEntry *)scr_itable_probe_bytes(
atomic_load_explicit(&seg->index, memory_order_acquire), hash, bytes, len);
if (bytes != stack) free(bytes);
return found;
}
const ScrITableEntry *scr_itable_insert_arr(ScrITable *t, const ScrArr *key, uint64_t value) {
scr_itable_enter();
char stack[SCR_ITABLE_STACK_KEY];
size_t len;
char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len);
if (!bytes) return NULL;
const ScrITableEntry *entry = scr_itable_insert_bytes(t, scr_itable_bytes_hash(bytes, len), bytes, len, value);
if (bytes != stack) free(bytes);
return entry;
}
double scr_itable_size(const ScrITable *t) {
uint64_t total = 0;
for (unsigned i = 0; i < SCR_ITABLE_SEGMENTS; i++)
+14 -5
View File
@@ -3,7 +3,7 @@
#ifndef SCR_ITABLE_H
#define SCR_ITABLE_H
#include "scr_string.h"
#include "scr_array.h"
/* ── shared intern tables (scr_itable.c) ──────────────────────────────
* InternTable (@scriptc/experimental): named, append-only tables of
@@ -23,7 +23,10 @@
* which emitted code reads directly. A handle stays valid, and its slot
* unchanged, for the life of the process. */
enum { SCR_ITABLE_KEY_STR = 0, SCR_ITABLE_KEY_F64 = 1 };
/* Key kinds: strings, numbers, and arrays of numbers or of strings, which
* compare by their elements (holes and undefined elements are equal to
* each other and to nothing else). */
enum { SCR_ITABLE_KEY_STR = 0, SCR_ITABLE_KEY_F64 = 1, SCR_ITABLE_KEY_F64S = 2, SCR_ITABLE_KEY_STRS = 3 };
typedef struct ScrITable ScrITable;
@@ -35,16 +38,22 @@ typedef struct ScrITableEntry {
/* The table called `name`, created on first use. Traps when the name is
* already open with another key kind. Never returns NULL. */
ScrITable *scr_itable_open(const ScrStr *name, int key_kind);
/* The entry of `key`, or NULL. */
/* The entry of `key`, or NULL. The array variants refuse keys of more
* than 65536 elements, or whose encoding exceeds 2^31 - 1 bytes, with a
* pending TypeError (and answer NULL). */
const ScrITableEntry *scr_itable_find_str(ScrITable *t, const ScrStr *key);
const ScrITableEntry *scr_itable_find_f64(ScrITable *t, double key);
const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key);
/* Stores `value` under `key` unless the key has an entry already; answers
* the key's entry either way. `value` must already be immortal (or a
* scalar): the table neither retains nor releases it. String keys are
* copied. */
* scalar): the table neither retains nor releases it. Keys are copied. */
const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uint64_t value);
const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t value);
const ScrITableEntry *scr_itable_insert_arr(ScrITable *t, const ScrArr *key, uint64_t value);
/* The number of entries. */
double scr_itable_size(const ScrITable *t);
/* The calling thread uses no table again (worker exit): replaced indexes
* no longer wait for it. */
void scr_itable_thread_exit(void);
#endif /* SCR_ITABLE_H */
+102 -1
View File
@@ -8,12 +8,20 @@
#include <stdlib.h>
#include <string.h>
/* The table unit needs only scr_trap from the rest of the runtime. */
/* The table unit needs only scr_trap and scr_throw_error_msg from the
* rest of the runtime; refusals are recorded here. */
_Noreturn void scr_trap(const char *msg) {
fputs(msg, stderr);
abort();
}
static char refused[128];
void scr_throw_error_msg(int kind, const char *msg, size_t len) {
assert(kind == SCR_ERR_TYPE && len < sizeof refused);
memcpy(refused, msg, len);
refused[len] = 0;
}
enum { THREADS = 8, KEYS = 20000, ROUNDS = 3 };
/* A stand-in for a published value: written by its creator before the
@@ -34,6 +42,29 @@ static ScrStr *make_str(const char *text) {
return s;
}
static ScrArr *make_f64s(const double *values, size_t n) {
ScrArr *a = calloc(1, sizeof(ScrArr));
assert(a);
a->rc = 1;
a->len = a->cap = n;
a->filled = (uint32_t)n;
a->elem = SCR_ELEM_F64;
a->data = calloc(n ? n : 1, sizeof(uint64_t));
for (size_t i = 0; i < n; i++) memcpy(&a->data[i], &values[i], sizeof(double));
return a;
}
static ScrArr *make_strs(ScrStr *const *values, size_t n) {
ScrArr *a = make_f64s(NULL, 0);
a->len = a->cap = n;
a->filled = (uint32_t)n;
a->elem = SCR_ELEM_STR;
free(a->data);
a->data = calloc(n, sizeof(uint64_t));
for (size_t i = 0; i < n; i++) a->data[i] = (uint64_t)(uintptr_t)values[i];
return a;
}
static void key_text(char *out, size_t size, uint64_t k) { snprintf(out, size, "key-%llu", (unsigned long long)k); }
static ScrITable *strings, *numbers;
@@ -81,6 +112,7 @@ static void *run(void *arg) {
assert(!scr_itable_find_f64(numbers, (double)KEYS + 0.5 + n));
}
}
scr_itable_thread_exit();
return NULL;
}
@@ -111,6 +143,75 @@ int main(void) {
free(other);
free(name);
/* Array keys compare by their elements: numbers by SameValueZero,
* strings by content, holes like undefined elements. */
ScrStr *lists_name = make_str("lists"), *paths_name = make_str("paths");
ScrITable *lists = scr_itable_open(lists_name, SCR_ITABLE_KEY_F64S);
ScrArr *one = make_f64s((double[]){1, -0.0, 0.0 / 0.0}, 3);
ScrArr *same = make_f64s((double[]){1, 0.0, -(0.0 / 0.0)}, 3);
ScrArr *shorter = make_f64s((double[]){1, 0.0}, 2);
ScrArr *holed = make_f64s((double[]){1, 0.0, 0.0}, 3);
holed->filled = 1;
holed->present = calloc(3, 1);
holed->present[0] = SCR_ARR_VALUE;
holed->present[1] = SCR_ARR_VALUE;
holed->present[2] = SCR_ARR_HOLE;
ScrArr *empty_list = make_f64s(NULL, 0);
const ScrITableEntry *l1 = scr_itable_insert_arr(lists, one, 1);
assert(scr_itable_find_arr(lists, same) == l1 && scr_itable_insert_arr(lists, same, 2) == l1);
assert(!scr_itable_find_arr(lists, shorter) && !scr_itable_find_arr(lists, holed));
assert(scr_itable_insert_arr(lists, holed, 3)->value == 3 && scr_itable_insert_arr(lists, shorter, 4)->value == 4);
assert(scr_itable_insert_arr(lists, empty_list, 5)->value == 5 && scr_itable_find_arr(lists, empty_list)->value == 5);
assert(scr_itable_size(lists) == 4);
ScrArr *big = make_f64s(NULL, 0);
big->len = big->cap = big->filled = 100;
free(big->data);
big->data = calloc(100, sizeof(uint64_t));
for (unsigned i = 0; i < 100; i++) memcpy(&big->data[i], &(double){i}, sizeof(double));
assert(scr_itable_insert_arr(lists, big, 6)->value == 6 && scr_itable_find_arr(lists, big)->value == 6);
/* Keys of more than 65536 elements are refused before anything is read,
* however long their sparse length. */
ScrArr *longest = make_f64s(NULL, 0);
longest->len = 65536;
assert(scr_itable_insert_arr(lists, longest, 7)->value == 7 && !refused[0]);
ScrArr *huge = make_f64s(NULL, 0);
huge->len = (size_t)1 << 29;
assert(!scr_itable_find_arr(lists, huge) && !strcmp(refused, "InternTable array keys can have at most 65536 elements"));
refused[0] = 0;
longest->len = 65537;
assert(!scr_itable_insert_arr(lists, longest, 8) && refused[0]);
refused[0] = 0;
assert(scr_itable_size(lists) == 6);
ScrITable *paths = scr_itable_open(paths_name, SCR_ITABLE_KEY_STRS);
ScrStr *ab = make_str("ab"), *c = make_str("c"), *a = make_str("a"), *bc = make_str("bc");
ScrArr *p1 = make_strs((ScrStr *[]){ab, c}, 2), *p2 = make_strs((ScrStr *[]){a, bc}, 2);
ScrArr *p3 = make_strs((ScrStr *[]){ab, c}, 2);
const ScrITableEntry *e1 = scr_itable_insert_arr(paths, p1, 7);
assert(scr_itable_insert_arr(paths, p2, 8) != e1 && scr_itable_find_arr(paths, p3) == e1);
assert(scr_itable_size(paths) == 2);
/* String keys whose encoding exceeds 2^31 - 1 bytes are refused. */
ScrStr *wide = calloc(1, sizeof(ScrStr) + 1);
assert(wide);
wide->rc = 1;
wide->len = (size_t)0x7fffffff - 2;
ScrArr *heavy = make_strs((ScrStr *[]){wide, wide}, 2);
assert(!scr_itable_find_arr(paths, heavy) &&
!strcmp(refused, "InternTable array keys can encode at most 2147483647 bytes"));
refused[0] = 0;
free(wide);
ScrArr *arrays[] = {one, same, shorter, holed, empty_list, big, longest, huge, p1, p2, p3, heavy};
for (unsigned i = 0; i < sizeof arrays / sizeof *arrays; i++) {
free(arrays[i]->data);
free(arrays[i]->present);
free(arrays[i]);
}
free(ab);
free(c);
free(a);
free(bc);
free(lists_name);
free(paths_name);
pthread_t threads[THREADS];
for (unsigned i = 0; i < THREADS; i++)
assert(pthread_create(&threads[i], NULL, run, (void *)(uintptr_t)i) == 0);
+2
View File
@@ -228,6 +228,8 @@ static void scr_worker_thread_run(ScrWorkerThread *worker) {
* only once none of those operations can touch the worker's script heap. */
scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_EXIT, worker->id, code, NULL));
scr_worker_thread_release(worker);
/* Replaced InternTable indexes no longer wait for this thread. */
scr_itable_thread_exit();
/* The next script thread continues this thread's allocator arena. */
scr_sa_thread_park();
}
@@ -0,0 +1,79 @@
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { InternTable } from "@scriptc/experimental";
// InternTable (@scriptc/experimental) with array keys: arrays of numbers or
// of strings compare by their elements (numbers by SameValueZero, strings
// by content), so a freshly built key finds the value stored under an equal
// one. Workers intern list types keyed by their member ids. Node.js prints
// the same.
class List {
readonly members: readonly number[];
readonly label: string;
constructor(members: readonly number[], label: string) {
this.members = members;
this.label = label;
}
}
const lists = new InternTable<readonly number[], List>("arrays.lists");
const paths = new InternTable<readonly string[], number>("arrays.paths");
function listOf(members: readonly number[]): List {
return lists.getOrInsertComputed(members, (key) => new List([...key], key.join("|")));
}
if (isMainThread) {
const a = listOf([1, 2, 3]);
const b = listOf([1, 2, 3]);
const c = listOf([1, 2]);
const d = listOf([3, 2, 1]);
console.log(a === b, a === c, a === d, a.label, c.label, d.label, lists.size);
console.log(lists.get([1, 2, 3]) === a, lists.get([1, 2, 3, 4]), lists.has([1, 2]), lists.has([2, 1]));
// SameValueZero elements.
const z = listOf([-0, NaN]);
console.log(listOf([0, 0 / 0]) === z, lists.has([0, NaN]), lists.has([NaN, 0]), lists.size);
// Empty and long keys.
const empty = listOf([]);
const long: number[] = [];
for (let i = 0; i < 500; i++) long.push(i * 1.5);
const big = listOf(long);
console.log(listOf([]) === empty, empty.label === "", listOf([...long]) === big, big.members.length);
long.push(1);
console.log(lists.has(long), lists.size);
// Holes differ from numbers.
const holed = new Array<number>(3);
holed[0] = 1;
const again = new Array<number>(3);
again[0] = 1;
console.log(listOf(holed).members.length, listOf(again) === listOf(holed), lists.has([1, 0, 0]), lists.has([1]));
// String arrays compare by content, element by element.
console.log(paths.getOrInsert(["ab", "c"], 1), paths.getOrInsert(["a", "bc"], 2), paths.getOrInsert(["ab", "c"], 3));
console.log(paths.get(["a" + "b", "c"]), paths.get(["abc"]), paths.get([]), paths.getOrInsert([], 4), paths.get([""]));
console.log(paths.getOrInsert([""], 5), paths.getOrInsert(["", ""], 6), paths.get([""]), paths.size);
const results: string[] = [];
let pending = 3;
for (let w = 0; w < 3; w++) {
const worker = new Worker(new URL(import.meta.url), { workerData: { seed: w } });
worker.on("message", (message: unknown) => {
results[w] = message as string;
});
worker.on("exit", () => {
if (--pending === 0) for (const line of results) console.log(line);
});
}
} else {
const { seed } = workerData as { seed: number };
let total = 0;
let bad = 0;
for (let n = 0; n < 5000; n++) {
const k = (n * 13 + seed * 1667) % 5000;
const members = [k % 7, k % 11, k];
const list = listOf(members);
if (list.label !== members.join("|")) bad++;
total += list.members[2]!;
}
parentPort!.postMessage(`worker ${seed}: total=${total} bad=${bad}`);
}
@@ -0,0 +1,73 @@
import { InternTable } from "@scriptc/experimental";
// InternTable.getOrInsertComputed callbacks (@scriptc/experimental): the
// callback runs only when the key has no value, sees the key and the
// enclosing scope, and its side effects happen in the order Node.js runs
// them. Node.js prints the same.
class Item {
readonly label: string;
readonly n: number;
constructor(label: string, n: number) {
this.label = label;
this.n = n;
}
}
const items = new InternTable<string, Item>("callbacks.items");
const log: string[] = [];
let made = 0;
function make(label: string): Item {
log.push(`make ${label}`);
return new Item(label, ++made);
}
// Expression arrows, with and without the key parameter.
console.log(items.getOrInsertComputed("a", (k) => make(k)).n, items.getOrInsertComputed("a", (k) => make(k)).n);
console.log(items.getOrInsertComputed("b", () => make("bee")).label, made);
// A parameter that shadows an outer binding, and a wider parameter type.
const k = "outer";
console.log(items.getOrInsertComputed("c", (k) => new Item(k + "!", 0)).label, k);
console.log(items.getOrInsertComputed("d", (key: string | number) => new Item(`${typeof key}:${key}`, 1)).label);
// The enclosing scope: reads and writes of outer variables.
let counter = 10;
const e = items.getOrInsertComputed("e", () => new Item("e", counter++));
console.log(e.n, counter, items.getOrInsertComputed("e", () => new Item("e", counter++)).n, counter);
// Nested interning inside a callback.
function chain(depth: number): Item {
return items.getOrInsertComputed(`chain${depth}`, (key) =>
depth === 0 ? new Item(key, 0) : new Item(`${key}>${chain(depth - 1).label}`, depth),
);
}
console.log(chain(3).label, chain(3) === chain(3), items.has("chain0"));
// A callback that stores its own key, and one that throws.
const nested = items.getOrInsertComputed("self", (key) => (items.getOrInsert(key, new Item("inner", 1)), new Item("outer", 2)));
console.log(nested.label, items.get("self")?.label);
try {
items.getOrInsertComputed("boom", (key) => (key.length > 0 ? (() => { throw new Error(`no ${key}`); })() : new Item(key, 0)));
} catch (error) {
console.log((error as Error).message, items.has("boom"));
}
// Block bodies and function expressions.
console.log(
items.getOrInsertComputed("f", (key) => {
log.push(`block ${key}`);
return new Item(key, 3);
}).n,
items.getOrInsertComputed("g", function (key: string) {
return new Item(key.toUpperCase(), 4);
}).label,
);
// Methods that intern with `this` in the callback.
class Factory {
readonly prefix: string;
constructor(prefix: string) {
this.prefix = prefix;
}
item(name: string): Item {
return items.getOrInsertComputed(`${this.prefix}${name}`, (key) => new Item(`${key}/${this.prefix}`, 5));
}
}
const factory = new Factory("p-");
console.log(factory.item("x").label, factory.item("x") === factory.item("x"));
console.log(log.join("; "), items.size);
@@ -0,0 +1,39 @@
import { InternTable } from "@scriptc/experimental";
// InternTable (@scriptc/experimental) array keys can have at most 65536
// elements: longer keys, including sparse arrays with a large length, make
// every method throw a TypeError and store nothing. Node.js prints the same.
const lists = new InternTable<readonly number[], number>("limits.lists");
const names = new InternTable<readonly string[], number>("limits.names");
function attempt(what: string, run: () => unknown): void {
try {
console.log(what, run());
} catch (error) {
console.log(what, (error as Error).name, (error as Error).message);
}
}
const longest = new Array<number>(65536);
longest[0] = 1;
attempt("longest", () => lists.getOrInsert(longest, 1));
attempt("longest again", () => lists.get(longest));
const longer = new Array<number>(65537);
attempt("longer get", () => lists.get(longer));
attempt("longer has", () => lists.has(longer));
attempt("longer insert", () => lists.getOrInsert(longer, 2));
attempt("longer computed", () => lists.getOrInsertComputed(longer, () => 3));
const sparse: number[] = [1, 2];
sparse.length = 2 ** 29;
attempt("sparse get", () => lists.get(sparse));
attempt("sparse insert", () => lists.getOrInsert(sparse, 4));
attempt("sparse computed", () => lists.getOrInsertComputed(sparse, () => 5));
const words: string[] = ["a"];
words.length = 2 ** 30;
attempt("words get", () => names.get(words));
attempt("words insert", () => names.getOrInsert(words, 6));
console.log(lists.size, names.size, lists.get([1]), names.get(["a"]));
@@ -23,7 +23,7 @@ intern-table-fences.ts:11:15 - error SC1090: 'string | number' as an InternTable
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12 | const callbacks = new InternTable<string, () => number>("fences.callbacks");
hint: use string or number keys
hint: use string or number keys, or arrays of numbers or of strings
intern-table-fences.ts:12:19 - error SC1090: '() => number' as an InternTable value type is not supported yet
+2
View File
@@ -305,6 +305,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
// InternTable handles carry 32-bit pointers in f64 slots.
"5720-intern-table-basics.ts",
"5721-intern-table-values.ts",
// Array key sizes are checked in 32-bit size_t arithmetic.
"5726-intern-table-key-limits.ts",
])("the LLVM target runs %s byte-identically to Node", async (fixture) => {
const entry = join(repoRoot, "tests/corpus", fixture);
const outDir = await mkdtemp("/tmp/scriptc-wasi-");