Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
8e50716
Add BroadcastChannel and the remaining Node 24 worker_threads surfaces
ctate Oct 17, 2026
69320be
Declare scr_worker_broadcast_channel in the runtime header
ctate Oct 17, 2026
4530547
Return owned copies of the worker thread name
ctate Oct 17, 2026
3fcef83
Carry lifetime regions across await and reuse freed region chunks
cramforce Oct 17, 2026
1689404
Keep records, tuples and spread updates that own references in frame …
cramforce Oct 17, 2026
63dae87
Define the region fiber hooks in an always-linked runtime unit
cramforce Oct 17, 2026
83517ec
Accept a frame-stored tuple receiver in the tuple for-of test
cramforce Oct 17, 2026
d762393
Keep declared methods of generic class instances after a computed-key…
ctate Oct 17, 2026
7689d4b
Merge main
ctate Oct 17, 2026
0580460
Match Node 24 in URLSearchParams, querystring, string_decoder and os
ctate Oct 17, 2026
2ba6bfa
Merge main
ctate Oct 17, 2026
1ad47f1
Match Node 24 in util.format, util.types and util.inspect
ctate Oct 17, 2026
5e020e9
Regenerate the surface manifest for the new util.types brand checks
ctate Oct 17, 2026
50d101f
Record corpus costs for the worker programs and order 5805's reads
ctate Oct 17, 2026
694d677
Add corpus costs for recently added programs
ctate Oct 18, 2026
a5c7c09
Add mock.timers to node:test
ctate Oct 18, 2026
8d8b96f
Merge main
ctate Oct 18, 2026
aad9b4f
Merge main
ctate Oct 18, 2026
de4e10d
Shift the runtime in bench:runtime layouts
ctate Oct 18, 2026
eb0d09f
Keep capture analyses linear in the size of a closure environment
cramforce Oct 18, 2026
8c8ee28
Compile bodies with thousands of capture boxes unoptimized
cramforce Oct 18, 2026
84b74c0
Keep declared methods of function-local classes after a computed-key …
ctate Oct 18, 2026
3d2c834
Add corpus costs for the mock programs
ctate Oct 18, 2026
0d300f2
Shift the runtime in bench:runtime layouts (#1245)
ctate Oct 18, 2026
43fb612
Compile CommonJS requires placed after code that can reach them, and …
cramforce Oct 18, 2026
f4daa00
Merge main
ctate Oct 18, 2026
e0998f0
Prove generic classes acyclic like other classes
ctate Oct 18, 2026
bb04c21
Merge main
ctate Oct 18, 2026
4fb38f0
Merge main
ctate Oct 18, 2026
82c699b
Add DisposableStack
ctate Oct 18, 2026
eeffa29
Merge main
ctate Oct 18, 2026
43d9b02
Merge main
ctate Oct 18, 2026
32061e8
Add Float16Array
ctate Oct 18, 2026
33cab6f
Prove generic classes acyclic like other classes (#1246)
ctate Oct 18, 2026
2f93c46
Merge main
ctate Oct 18, 2026
ed817c0
Name the rejected value in the Object prototype TypeError
ctate Oct 18, 2026
dd44148
Merge main
ctate Oct 18, 2026
9c90086
Add corpus costs for recently added programs (#1242)
ctate Oct 18, 2026
f5c87fb
Merge main
ctate Oct 18, 2026
982316d
Merge main
ctate Oct 18, 2026
aa19f94
Settle async generator completions on Node's microtask turns
ctate Oct 18, 2026
97f2875
Merge main
ctate Oct 18, 2026
a3a4c72
Merge main
ctate Oct 18, 2026
23ce40c
Keep JavaScript lets open when a later object literal adds properties
ctate Oct 18, 2026
ccd65e2
Merge main
ctate Oct 18, 2026
924e485
Merge main
ctate Oct 18, 2026
62bf385
Merge main
ctate Oct 18, 2026
b072969
Name the rejected value in the Object prototype TypeError (#1250)
ctate Oct 18, 2026
db18d45
Merge main
ctate Oct 18, 2026
370bf59
Add the cstring class to library profiles
ctate Oct 18, 2026
044b7ae
Regenerate the surface manifest
ctate Oct 18, 2026
b6cfe07
Merge main
ctate Oct 18, 2026
87abacc
Merge main
ctate Oct 18, 2026
298e674
Match Node 24 in URLSearchParams, querystring, string_decoder and os …
ctate Oct 18, 2026
ca23c11
Add the cstring class to library profiles (#1253)
ctate Oct 18, 2026
6f9acfd
Read fields of a class instance asserted to a wider union
ctate Oct 18, 2026
3dcb8a1
Lower Date.prototype.toJSON with Node's null for invalid dates
ctate Oct 18, 2026
0f50a7b
Lower mapped types over a generic function's type parameters
ctate Oct 18, 2026
bce093c
Keep JavaScript lets open when a later object literal adds properties…
ctate Oct 18, 2026
5eb65cf
Merge main
ctate Oct 18, 2026
86f5bc3
Merge main
ctate Oct 18, 2026
6774fe5
Merge main
ctate Oct 18, 2026
b64ef0a
Read fields of a class instance asserted to a wider union (#1254)
ctate Oct 18, 2026
421e98f
Merge main
ctate Oct 18, 2026
d6bb761
Merge main
ctate Oct 18, 2026
7804e62
Merge main
ctate Oct 18, 2026
58e6396
Merge main
ctate Oct 18, 2026
add07db
Return a worker's memory when it exits without sharing any of it
ctate Oct 18, 2026
b152260
Settle async generator completions on Node's microtask turns (#1251)
ctate Oct 18, 2026
3f84d64
Merge main
ctate Oct 18, 2026
8077a9b
Add DisposableStack (#1248)
ctate Oct 18, 2026
b06e06f
Merge main
ctate Oct 18, 2026
dc1cb7c
Merge main
ctate Oct 18, 2026
54ab14d
Keep declared methods of generic class instances after a computed-key…
ctate Oct 18, 2026
0309ff6
Window long source lines in diagnostic code frames
ctate Oct 18, 2026
d60978a
Match Node 24 in util.format, util.types and util.inspect (#1229)
ctate Oct 18, 2026
98cbdb9
Merge main
ctate Oct 18, 2026
aa9c6d2
Answer `in` for class methods and accessors on instances with runtime…
ctate Oct 18, 2026
9498d93
Merge main
ctate Oct 18, 2026
9750d39
Merge main
ctate Oct 18, 2026
5d56b57
Read .groups on matchAll rows in JavaScript callbacks
ctate Oct 18, 2026
53a8b0a
Window long source lines in diagnostic code frames (#1260)
ctate Oct 18, 2026
6cdff93
Merge main
ctate Oct 18, 2026
8a2fbee
Give querystring.parse results a null prototype
ctate Oct 18, 2026
3999311
Give the discard test's trim run an array of its own
ctate Oct 18, 2026
588e16d
Skip WebAssembly irreducible control flow analysis on reducible funct…
ctate Oct 18, 2026
d8f707a
Drop the util.types.isFloat16Array entry main already adds
ctate Oct 18, 2026
752d58e
Answer `in` for class methods and accessors on instances with runtime…
ctate Oct 18, 2026
3414144
Lower mapped types over a generic function's type parameters (#1256)
ctate Oct 18, 2026
24096c6
Remove the leftover always-false isFloat16Array probe
ctate Oct 18, 2026
c7ffd12
Give querystring.parse results a null prototype (#1263)
ctate Oct 18, 2026
2385fc4
Return a worker's memory when it exits without sharing any of it (#1258)
ctate Oct 18, 2026
8e677e5
Merge main
ctate Oct 18, 2026
fe47b38
Add Float16Array (#1249)
ctate Oct 18, 2026
7c0db7d
Merge main
ctate Oct 18, 2026
f7a9fa9
Free large dead cycle sets in address order
ctate Oct 18, 2026
0a5e244
Read .groups on matchAll rows in JavaScript callbacks (#1262)
ctate Oct 18, 2026
85ed5fd
Lower Date.prototype.toJSON with Node's null for invalid dates (#1255)
ctate Oct 18, 2026
86e3354
Free large dead cycle sets in address order (#1265)
ctate Oct 18, 2026
d7ac6d3
Merge main
ctate Oct 18, 2026
1899b5f
Key the differential oracle cache on the inherited environment
ctate Oct 18, 2026
3fd1d2b
Revert "Return a worker's memory when it exits without sharing any of…
ctate Oct 18, 2026
84916f2
Support the regex v flag
ctate Oct 18, 2026
d375212
Add BroadcastChannel and the remaining Node 24 worker_threads surface…
ctate Oct 18, 2026
51777de
Add markAsUncloneable and worker[Symbol.asyncDispose]
ctate Oct 18, 2026
4f3bf56
Key the differential oracle cache on the inherited environment (#1266)
ctate Oct 18, 2026
a0b1e93
Merge main
ctate Oct 18, 2026
a18335e
Revert "Return a worker's memory when it exits without sharing any of…
ctate Oct 18, 2026
ab1c271
Support the regex v flag (#1268)
ctate Oct 18, 2026
f9682f3
Skip WebAssembly irreducible control flow analysis on reducible funct…
ctate Oct 18, 2026
6d80207
Convert errors held as any with Error.prototype.toString
ctate Oct 18, 2026
58096a7
Define scr_uncloneable_count with an initializer
ctate Oct 18, 2026
c88981a
Describe why mock.timers' tick leaves a fired timer queued
ctate Oct 18, 2026
5e069c6
Convert errors held as any with Error.prototype.toString (#1270)
ctate Oct 18, 2026
5093ab9
Add markAsUncloneable and worker[Symbol.asyncDispose] (#1269)
ctate Oct 18, 2026
ffd760b
Merge main
ctate Oct 18, 2026
4fe392c
Return a worker's memory once it and the other workers are done
ctate Oct 18, 2026
0df712e
Add mock.timers to node:test (#1244)
ctate Oct 18, 2026
89b5bae
Add t.assert.snapshot and t.assert.fileSnapshot
ctate Oct 18, 2026
d09b380
Serialize checked-dynamic members of typed records like JSON.stringify
ctate Oct 18, 2026
b779ef7
Return a worker's memory once it and the other workers are done (#1273)
ctate Oct 18, 2026
85fe87f
Convert Date values into unknown slots as Dates
ctate Oct 18, 2026
74702ac
Run a full cycle pass when a structure dies outside passes
ctate Oct 18, 2026
0dac6c5
Serialize checked-dynamic members of typed records like JSON.stringif…
ctate Oct 18, 2026
04da3a6
Run a full cycle pass when a structure dies outside passes (#1277)
ctate Oct 18, 2026
f676f5a
Select a new property bag's prototype through one helper per class
ctate Oct 18, 2026
14c1cdd
Merge main
ctate Oct 18, 2026
f62275d
Add the process.execArgv surface to the manifest
ctate Oct 18, 2026
30a7e61
Select a new property bag's prototype through one helper per class (#…
ctate Oct 18, 2026
60829fa
Convert Date values into unknown slots as Dates (#1276)
ctate Oct 18, 2026
0ee069f
Answer typeof for builtin module functions and classes
ctate Oct 18, 2026
4d95b28
Add t.assert.snapshot and t.assert.fileSnapshot (#1274)
ctate Oct 18, 2026
572b26e
Answer typeof for builtin module functions and classes (#1279)
ctate Oct 18, 2026
1cd06fd
Read events.defaultMaxListeners from the ES module namespace copy
ctate Oct 18, 2026
201857e
Read the class base after the callable-base case in super calls
ctate Oct 18, 2026
fc68a58
Remove the last element of a packed array inline on pop
ctate Oct 18, 2026
aba3c3a
Read the class base after the callable-base case in super calls (#1281)
ctate Oct 18, 2026
31b52e2
Remove the last element of a packed array inline on pop (#1282)
ctate Oct 18, 2026
10c5e26
Merge origin/main into opt/06-regions
cramforce Oct 18, 2026
ed5aa10
Merge origin/main into opt/02-value-records
cramforce Oct 18, 2026
9118cbc
Record corpus 5973's preflight baseline and skip the scaling test und…
cramforce Oct 18, 2026
ae91df9
Admit require cycles whose modules settle their exports first
cramforce Oct 18, 2026
b143778
Read events.defaultMaxListeners from the ES module namespace copy (#1…
ctate Oct 18, 2026
f2e03bd
Keep records, tuples and spread updates that own references in frame …
ctate Oct 18, 2026
8255315
Merge origin/main into opt/cjs-interop
cramforce Oct 18, 2026
4ed7511
Carry lifetime regions across await and reuse freed region chunks (#1…
ctate Oct 18, 2026
8c1d94e
Compile large closure environments in linear time (#1284)
ctate Oct 18, 2026
04297f3
Keep the preflight batching test on a fenced require binding
cramforce Oct 18, 2026
9d0012d
Lower child_process.exec's callback forms
ctate Oct 18, 2026
27f78a9
Read fields as undefined before their assignment
ctate Oct 18, 2026
0d4e6a0
Compile classic CommonJS packages: late requires, JSON requires, nest…
ctate Oct 18, 2026
6c4e4c3
Keep object-literal computed keys out of the method replacement census
ctate Oct 18, 2026
0bdccce
Lower child_process.exec's callback forms (#1290)
ctate Oct 18, 2026
ff90245
Read fields as undefined before their assignment (#1292)
ctate Oct 18, 2026
2fa4b19
Keep object-literal computed keys out of the method replacement censu…
ctate Oct 18, 2026
41ec7b7
Merge remote-tracking branch 'origin/main' into opt/overlay-t13
cramforce Oct 18, 2026
6984133
Give source overlays their package's type environment and class-field…
cramforce Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions benchmarks/regions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Programs that build generations of `ProgramLifetime` objects and free each gener
| `generations.ts` | Per-file syntax trees and binder symbols in one region per file, checker state (types, signatures, node and symbol links in paged tables, literal and union caches) in one region per generation. `edit` re-parses one file per generation, `all` re-parses every file. |
| `compiler.ts` | A tokenizer, recursive-descent parser, binder and checker over generated sources, rebuilt from scratch in one region per build, like a watch mode that does not reuse syntax trees. |
| `emit.ts` | A large live graph of ordinary objects with parent links, standing in for a type checker, and a per-file emit that builds its output in a region released right after. |
| `requests.ts` | Batches of concurrent async request handlers, each in its own region across its awaits: tokenize a JSON body, parse it into a syntax tree that points at its tokens, validate it into issues and an index, render a response, release the region. |

```console
$ node benchmarks/regions/run.mjs --baseline=../scriptc-main --runs=7
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/regions/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ let level: Type[] = [new Type(id++, undefined)];
while (id < typeCount) {
const next: Type[] = [];
for (const t of level) for (let k = 0; k < 4 && id < typeCount; k++) next.push(new Type(id++, t));
roots.push(...next);
for (const t of next) roots.push(t);
level = next;
}

Expand Down
221 changes: 221 additions & 0 deletions benchmarks/regions/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// An async request handler: each request tokenizes a JSON body, awaits (as
// if reading more of it), parses the tokens into a syntax tree whose nodes
// point back at their tokens, awaits again (as if loading reference data),
// validates the tree into issues held in an index, and renders a response.
// Requests run in batches of concurrent handlers, so the handlers of one
// batch interleave at every await.
//
// requests <requests> <concurrency> <items-per-body> [plain]
//
// Each request runs in its own LifetimeRegion, released when its response
// is rendered; `plain` never constructs a region.
import { readFileSync } from "node:fs";
import type { ProgramLifetime } from "@scriptc/types";
import { LifetimeRegion } from "@scriptc/experimental";

const T_PUNCT = 0;
const T_STR = 1;
const T_NUM = 2;
const T_WORD = 3;

class Token implements ProgramLifetime {
readonly kind: number;
readonly text: string;
readonly pos: number;
constructor(kind: number, text: string, pos: number) {
this.kind = kind;
this.text = text;
this.pos = pos;
}
}

const N_OBJECT = 0;
const N_ARRAY = 1;
const N_STRING = 2;
const N_NUMBER = 3;
const N_LITERAL = 4;

class JsonNode implements ProgramLifetime {
readonly kind: number;
readonly token: Token;
parent: JsonNode | undefined = undefined;
readonly keys: string[] = [];
readonly children: JsonNode[] = [];
constructor(kind: number, token: Token) {
this.kind = kind;
this.token = token;
}
}

class Issue implements ProgramLifetime {
readonly path: string;
readonly node: JsonNode;
readonly message: string;
constructor(path: string, node: JsonNode, message: string) {
this.path = path;
this.node = node;
this.message = message;
}
}

// ---- request bodies (deterministic) ----

function body(id: number, items: number): string {
let seed = 7 + id * 7919;
const rand = (n: number): number => {
seed = (seed * 1103515245 + 12345) % 2147483648;
return seed % n;
};
const parts: string[] = [];
for (let i = 0; i < items; i++) {
const qty = rand(12) - 1;
const sku = rand(9) === 0 ? "" : `sku-${rand(5000)}`;
const tags = Array.from({ length: rand(4) }, (_, k) => `"t${k}${rand(10)}"`).join(",");
parts.push(`{"sku":"${sku}","qty":${qty},"price":${rand(10000) / 100},"gift":${rand(2) === 0},"tags":[${tags}]}`);
}
return `{"order":${id},"customer":"c${id % 97}","items":[${parts.join(",")}]}`;
}

// ---- handler ----

function tokenize(text: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < text.length) {
const c = text.charCodeAt(i);
const start = i;
if (c === 34) {
i++;
while (text.charCodeAt(i) !== 34) i++;
i++;
tokens.push(new Token(T_STR, text.slice(start + 1, i - 1), start));
} else if (c === 45 || (c >= 48 && c <= 57)) {
i++;
while (i < text.length && (text.charCodeAt(i) === 46 || (text.charCodeAt(i) >= 48 && text.charCodeAt(i) <= 57))) i++;
tokens.push(new Token(T_NUM, text.slice(start, i), start));
} else if (c >= 97 && c <= 122) {
while (i < text.length && text.charCodeAt(i) >= 97 && text.charCodeAt(i) <= 122) i++;
tokens.push(new Token(T_WORD, text.slice(start, i), start));
} else {
i++;
tokens.push(new Token(T_PUNCT, text[start]!, start));
}
}
return tokens;
}

class Parser {
private at = 0;
private readonly tokens: Token[];
constructor(tokens: Token[]) {
this.tokens = tokens;
}
value(): JsonNode {
const token = this.tokens[this.at++]!;
if (token.kind === T_STR) return new JsonNode(N_STRING, token);
if (token.kind === T_NUM) return new JsonNode(N_NUMBER, token);
if (token.kind === T_WORD) return new JsonNode(N_LITERAL, token);
const node = new JsonNode(token.text === "{" ? N_OBJECT : N_ARRAY, token);
const close = token.text === "{" ? "}" : "]";
while (this.tokens[this.at]!.text !== close) {
if (node.kind === N_OBJECT) {
node.keys.push(this.tokens[this.at]!.text);
this.at += 2; // the key and the colon
}
const child = this.value();
child.parent = node;
node.children.push(child);
if (this.tokens[this.at]!.text === ",") this.at++;
}
this.at++;
return node;
}
}

function member(node: JsonNode, key: string): JsonNode | undefined {
const i = node.keys.indexOf(key);
return i < 0 ? undefined : node.children[i];
}

function validate(root: JsonNode, issues: Issue[], index: Map<string, JsonNode>): number {
let total = 0;
const items = member(root, "items")!;
for (let i = 0; i < items.children.length; i++) {
const item = items.children[i]!;
const path = `items[${i}]`;
const sku = member(item, "sku")!;
if (sku.token.text === "") issues.push(new Issue(`${path}.sku`, sku, "empty sku"));
else if (index.has(sku.token.text)) issues.push(new Issue(`${path}.sku`, sku, `duplicate of ${index.get(sku.token.text)!.token.pos}`));
else index.set(sku.token.text, item);
const qty = Number(member(item, "qty")!.token.text);
if (qty <= 0) issues.push(new Issue(`${path}.qty`, member(item, "qty")!, "quantity must be positive"));
else total += qty * Number(member(item, "price")!.token.text);
const tags = member(item, "tags")!;
for (const tag of tags.children) if (tag.token.text.endsWith("0")) issues.push(new Issue(`${path}.tags`, tag, "reserved tag"));
}
return total;
}

const hop = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));

async function handle(text: string): Promise<string> {
const tokens = tokenize(text);
await hop();
const root = new Parser(tokens).value();
await hop();
const issues: Issue[] = [];
const index = new Map<string, JsonNode>();
const total = validate(root, issues, index);
await hop();
const first = issues[0];
return `${member(root, "order")!.token.text} ${tokens.length} ${index.size} ${issues.length} ${total.toFixed(2)}` +
(first === undefined ? "" : ` ${first.path}@${first.node.token.pos}`);
}

// ---- driver ----

function rssMb(key: string): number {
const m = new RegExp(`${key}:\\s*(\\d+) kB`).exec(readFileSync("/proc/self/status", "utf8"));
return m === null ? 0 : Number(m[1]) / 1024;
}

const args = process.argv.slice(2);
const requests = Number(args[0] ?? "4000");
const concurrency = Number(args[1] ?? "16");
const items = Number(args[2] ?? "100");
const useRegions = args[3] !== "plain";

async function serve(id: number): Promise<string> {
const text = body(id, items);
if (!useRegions) return await handle(text);
const region = new LifetimeRegion();
const response = await region.run(() => handle(text));
region.release();
return response;
}

async function main(): Promise<void> {
let hash = 0;
let lines = 0;
let sample = "";
let rssFirst = 0;
const t0 = performance.now();
for (let next = 0; next < requests; next += concurrency) {
const batch: Promise<string>[] = [];
for (let k = next; k < next + concurrency && k < requests; k++) batch.push(serve(k));
for (const response of await Promise.all(batch)) {
for (let i = 0; i < response.length; i++) hash = (hash * 31 + response.charCodeAt(i)) % 1000000007;
lines++;
if (lines === 1) sample = response;
}
if (next === 0) rssFirst = rssMb("VmRSS");
}
const ms = performance.now() - t0;
console.log(`${lines} responses, hash ${hash}, first ${sample}`);
console.log(
`total ${ms.toFixed(1)} ms per-request ${((ms * 1000) / requests).toFixed(1)} us` +
` rss-first ${rssFirst.toFixed(1)} MB rss-last ${rssMb("VmRSS").toFixed(1)} MB peak ${rssMb("VmHWM").toFixed(1)} MB`,
);
}

main();
4 changes: 3 additions & 1 deletion benchmarks/regions/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const CONFIGS = [
{ name: "compiler-one-shot", program: "compiler.ts", args: ["400", "60", "1"] },
// Per-file emit regions next to a large live heap of ordinary objects.
{ name: "emit-files", program: "emit.ts", args: ["400000", "200", "2000"] },
// Concurrent async request handlers, one region per request across its awaits.
{ name: "requests-async", program: "requests.ts", args: ["4000", "16", "100"] },
];

const work = join(tmpdir(), `scriptc-region-bench-${process.pid}`);
Expand Down Expand Up @@ -93,7 +95,7 @@ for (const config of CONFIGS) {
const lines = r.stdout.trim().split("\n");
outputs.set(name, lines[0].replace(/ gen0 .*/, ""));
const metrics = {};
for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|MB)/g))
for (const [, key, value] of lines.at(-1).matchAll(/([a-z0-9-]+) ([0-9.]+) (?:ms|us|MB)/g))
metrics[key] = Number(value);
samples[name].push(metrics);
}
Expand Down
1 change: 1 addition & 0 deletions benchmarks/runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields |
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
Expand Down
7 changes: 7 additions & 0 deletions benchmarks/runtime/workloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@
"2"
]
},
{
"name": "value-records",
"entry": "workloads/value-records.ts",
"args": [
"1"
]
},
{
"name": "region-builds",
"entry": "workloads/region-builds.ts",
Expand Down
93 changes: 93 additions & 0 deletions benchmarks/runtime/workloads/value-records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Records that hold references: a tokenizer that builds token records with
// string fields and passes them to helpers, an immutable reducer that
// updates its state with object spread, and tuple-returning helpers whose
// results are destructured at once. None of these temporaries outlives
// the statement or loop iteration that builds it.
interface Token {
kind: string;
text: string;
pos: number;
}
const KEYWORDS = ["let", "const", "if", "else", "for", "while", "return", "function"];
function classify(text: string): string {
return KEYWORDS.includes(text) ? "keyword" : "ident";
}
function token(kind: string, text: string, pos: number): Token {
return { kind, text, pos };
}
function weight(t: Token): number {
return t.kind === "keyword" ? t.text.length * 3 : t.text.length + (t.pos & 7);
}
function tokenize(source: string): number {
let sum = 0;
let start = -1;
for (let i = 0; i <= source.length; i++) {
const c = i < source.length ? source.charCodeAt(i) : 32;
const word = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95;
if (word) {
if (start < 0) start = i;
continue;
}
if (start >= 0) {
const text = source.slice(start, i);
const t = token(classify(text), text, start);
sum += weight(t);
start = -1;
}
if (c !== 32 && c !== 10) {
const punct = { kind: "punct", text: source[i]!, pos: i };
sum += weight(punct) + (punct.text === ";" ? 1 : 0);
}
}
return sum;
}

interface State {
mode: string;
depth: number;
count: number;
last: string;
}
function step(s: State, text: string): State {
if (text === "{") return { ...s, depth: s.depth + 1, last: text };
if (text === "}") return { ...s, depth: s.depth - 1, last: text };
return s.mode === "code" ? { ...s, count: s.count + 1 } : { ...s, mode: "code", last: text };
}
function reduce(texts: string[], rounds: number): number {
let total = 0;
for (let r = 0; r < rounds; r++) {
let state: State = { mode: "start", depth: 0, count: 0, last: "" };
for (const text of texts) {
state = step(state, text);
if (state.depth > 3) state = { ...state, depth: 3 };
}
total += state.count + state.depth + state.last.length + state.mode.length;
}
return total;
}

function divmod(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b];
}
function split(text: string, at: number): [string, string] {
return [text.slice(0, at), text.slice(at)];
}
function tuples(n: number, texts: string[]): number {
let sum = 0;
for (let i = 0; i < n; i++) {
const [q, r] = divmod(i * 31, 17);
const [head, tail] = split(texts[i % texts.length]!, r % 3);
sum += q + r + head.length * 2 + tail.length;
}
return sum;
}

const scaleArg = Number(process.argv[2] ?? "1");
const line = "function step ( s , text ) { if ( text ) { return s ; } let x = s ; while ( x ) { x = x ; } }\n";
const source = line.repeat(Math.floor(400 * scaleArg));
const texts = source.split(" ").filter((t) => t.length > 0);
let lexed = 0;
for (let i = 0; i < 240; i++) lexed += tokenize(source);
const reduced = reduce(texts, Math.floor(600 * scaleArg));
const pairs = tuples(Math.floor(2_000_000 * scaleArg), texts);
console.log("tokens", lexed, "reduce", reduced, "tuples", pairs);
2 changes: 1 addition & 1 deletion docs/content/docs/contracts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ $ scriptc build graph.ts

## ProgramLifetime

`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed.
`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).

```ts:tree.ts
import type { ProgramLifetime } from "@scriptc/types";
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/dependencies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null

Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.

## CommonJS packages

CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.

## Unsupported code inside a package

A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses.
Expand Down
Loading
Loading