@scriptc/experimental
Experimental scriptc APIs. Each export has a Node.js implementation, so programs that use them run unchanged under Node.js.
LifetimeRegion
Instances of ProgramLifetime classes (@scriptc/types) are never freed in scriptc builds. A LifetimeRegion gives a long-running program a way to free a whole generation of them at once, for example the previous program of a compiler's watch mode.
import { LifetimeRegion } from "@scriptc/experimental";
let previous: LifetimeRegion | undefined;
function rebuild(): Program {
const region = new LifetimeRegion();
const program = region.run(() => buildProgram());
previous?.release();
previous = region;
return program;
}
run(fn)callsfnwith the region as the current region and returns its result.ProgramLifetimeinstances created during the call belong to the region; calls nest.- Instances created outside every
runcall belong to the default region, which is never freed. - Async work started inside
fnkeeps its region: what it creates after anawaitbelongs to the region too, and the region is not freed before that work has finished. release()requests that the region be freed. scriptc frees it once nothing outside the region refers to any of its instances; until then the region stays allocated. Laterruncalls on a released region allocate in the default region.- A region that holds a published object (
@scriptc/threads) is never freed.
Under Node.js, run(fn) returns fn() and release() does nothing.
In scriptc builds, SCRIPTC_REGION_STATS=1 reports every release attempt on stderr.
InternTable
An InternTable is a named, append-only table of published values (@scriptc/threads) that every thread of a scriptc program shares. Threads that build the same cache (interned types, strings, resolved modules) fill and read one table instead of one copy each.
import { InternTable } from "@scriptc/experimental";
class Type {
readonly id: number;
readonly members: readonly Type[];
constructor(id: number, members: readonly Type[]) {
this.id = id;
this.members = members;
}
}
// Module scope: every thread that evaluates this line opens the same table.
const unions = new InternTable<string, Type>("checker.unions");
function unionOf(members: readonly Type[]): Type {
const key = members.map((m) => m.id).join(",");
return unions.getOrInsertComputed(key, () => new Type(nextId(), members));
}
new InternTable<K, V>(name)opens the table calledname. 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, numbers, or arrays of numbers or of strings. Strings and numbers compare like
Mapkeys: 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 aTypeError. get(key),has(key)andsizeread the table.getOrInsert(key, value)returns the value stored underkey. When there is none, it publishesvalue, stores it and returns it.getOrInsertComputed(key, callback)returns the value stored underkey. When there is none, it callscallback(key), publishes the result and stores it, unless a value was stored underkeymeanwhile (by the callback, or in scriptc builds by another thread), and returns the stored value.- A key's value never changes once stored, and there is no
setordelete. Stored values and everything they reach are immutable; a value thatpublishrefuses makes the call throw the sameTypeErrorand stores nothing. - No lock is held while a callback runs. Threads that miss one key at the same time may each call the callback; one result is stored and every call returns it.
scriptc requires the name to be a string literal, and every construction of one name in a program to use the same type arguments. Values must be numbers, booleans, strings, bigints, arrays, Maps, Sets, objects or class instances.
Programs that behave identically under scriptc and Node.js follow the @scriptc/threads rules and three more:
- A stored value does not depend on which thread created it. In scriptc a value may come from another thread's callback.
- Identity and ids are not compared across threads. Ids assigned inside callbacks are unique in both runtimes but differ between them.
- A thread does not depend on another thread's inserts.
sizeandhasreflect 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 ThreadSlots family (below), 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.
ThreadSlots
A ThreadSlots family holds one value per key and thread, like a WeakMap that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares, such as values read from an InternTable, and setting a value never writes the key.
import { InternTable, ThreadSlots } from "@scriptc/experimental";
class Type {
readonly id: number;
constructor(id: number) {
this.id = id;
}
}
class TypeState {
resolved = false;
}
const types = new InternTable<number, Type>("checker.types");
// Module scope: every thread that evaluates this line opens the same family.
const states = new ThreadSlots<Type, TypeState>("checker.typeStates");
function stateOf(t: Type): TypeState {
let state = states.get(t);
if (state === undefined) {
state = new TypeState();
states.set(t, state);
}
return state;
}
new ThreadSlots<K, V>(name)opens the family calledname. Within one thread, every construction with the same name refers to the same family.get(key)andhas(key)read the calling thread's value forkey.set(key, value)sets the calling thread's value and returns the slots.delete(key)removes the calling thread's value and returns whether there was one.
scriptc requires the name to be a string literal, every construction of one name to use the same type arguments, the key type to be a class declared in the program (keys may be instances of its subclasses), and the value type to be a class, an object type, an array, a Map or a Set. The methods must be called through a module-level const initialized with new ThreadSlots<K, V>("name"), since a family's storage is found by its name when the program is compiled.
In scriptc builds every instance of a key class's hierarchy carries a 4-byte slot index, assigned by the first set for it and never reused. get reads the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family holds a reference to each of the thread's values and releases it when it is replaced or deleted, when its key is freed, and when the thread exits. Keys freed with a LifetimeRegion keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive.
Under Node.js each family is a WeakMap per thread.
InlineArray
InlineArray<T> marks an array field of a ProgramLifetime class (@scriptc/types) whose elements scriptc keeps in the instance itself, after its other fields, instead of in an array object of their own. It suits a program with millions of short arrays that never change once built, such as the children of each node of a syntax tree.
import type { ProgramLifetime } from "@scriptc/types";
import type { InlineArray } from "@scriptc/experimental";
class NodeList implements ProgramLifetime {
readonly pos: number;
readonly nodes: InlineArray<Node>;
constructor(pos: number, nodes: readonly Node[]) {
this.pos = pos;
this.nodes = nodes;
}
}
function visit(list: NodeList): void {
const nodes = list.nodes;
for (let i = 0; i < nodes.length; i++) visitNode(nodes[i]!);
}
Tis aProgramLifetimeclass, and the field is an instance field of aProgramLifetimeclass.- The constructor of the class that declares the field writes it once, from one of its parameters. A subclass constructor passes one of its own parameters there through
super(...). Nothing else writes the field. - Indexing the field, reading its
lengthand iterating it withfor...ofread the elements from the instance, directly or through aconstlocal used only in those ways. Any other use, such as passing the field to a function or calling one of its methods, gets an array of the elements, which is frozen in scriptc builds. - The array passed to the constructor is one that nothing else keeps: a new array (a literal, a copy, or the result of a function that returns only such arrays or its own argument), a local array at its last use, a parameter whose callers each pass such an array, an array that an InlineArray field already holds, or a
constarray that is never written. - The arrays fields hold are never written, through a read of the field (a cast) or through any other reference. scriptc follows them through locals, parameters, return values, fields, the elements of arrays, Maps and Sets, and closures, and reports one that reaches a write, a dynamic value (also inside the fields of an object that becomes one), a call through a function value (also inside an array, Map or Set passed there), or a library call other than printing and
JSON.stringifywithout a replacer. - Two reads of the field need not give the same array, so the arrays fields hold are never compared by identity: with
===or!==, as anindexOforincludesargument, or as a Map key or Set element. Compare the elements instead, or keep a copy made withslice()where identity matters. - Instances of the class are not constructed through a class value, and do not become dynamic values (for example through a conversion to
unknown), directly or inside another value that can hold one: an instance of a class with such a field, a record, array, Map or Set, or a value typed as a base class. A dynamic view reads the field as a new array each time, so a class whose instances can become dynamic values cannot hold an InlineArray field. Printing,JSON.stringifywithout a replacer,publishand a worker'sworkerDataonly read or copy the value and are allowed.
scriptc reports a field that does not keep these rules (SC7009). Under Node.js the field holds the array the constructor stored. In builds with sanitizers, in programs that use LifetimeRegion, and on platforms other than 64-bit Linux, the elements stay in an ordinary array object.