mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -360,7 +360,7 @@ A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked
|
||||
|
||||
Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`.
|
||||
|
||||
Static code has no `WeakRef` or `FinalizationRegistry`, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
|
||||
Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
|
||||
|
||||
### Lifetime regions
|
||||
|
||||
|
||||
@@ -3,6 +3,15 @@ Copied values, other native handles, and symbol keys are unsupported.
|
||||
Constructor seeds support arrays; custom iterators and detached methods are unsupported.
|
||||
Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release.
|
||||
|
||||
`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js.
|
||||
Its cleanup callbacks never run, which the ECMAScript specification allows.
|
||||
Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to.
|
||||
Memory that a package frees only from a cleanup callback is not reclaimed until the program exits.
|
||||
For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory.
|
||||
Call `free()` on wrappers the program no longer needs.
|
||||
Symbol targets and unregister tokens are unsupported.
|
||||
`WeakRef` is unsupported.
|
||||
|
||||
Memory uses reference counting.
|
||||
Acyclic values are freed deterministically, and cycles are collected at defined collection points.
|
||||
Native checked objects, arrays, Sets, and captured closures participate in cycle collection.
|
||||
|
||||
@@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () =
|
||||
expect(found.has(idle)).toBe(false);
|
||||
expect(found.has(returned)).toBe(false);
|
||||
|
||||
// Only `fill` builds arrays.
|
||||
// Only `fill` builds arrays. They may hold int32 slots (element kind 7),
|
||||
// which does not change the choice of constructor.
|
||||
const llvm = emitLlvmModule(module(body, ["xs", "ys"]));
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("a binding with the same id in another function does not count", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { compactRefLoadLines, compactRefStoreLines } from "./classes.js";
|
||||
import { elemAccess } from "./shapes.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { indexInteger } from "./integer-values.js";
|
||||
import { integerOrNaNIndex } from "./wrapping-sums.js";
|
||||
|
||||
const SCR_ARR_VALUE = 1;
|
||||
@@ -99,14 +99,17 @@ function emitDenseGuard(
|
||||
): DenseGuard {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const integer = exactInteger(host, index, indexExpr);
|
||||
const nanIndex = integer ? null : integerOrNaNIndex(host, index, indexExpr);
|
||||
const integer = indexInteger(host, index, indexExpr);
|
||||
const nanIndex = integer === null ? integerOrNaNIndex(host, index, indexExpr) : null;
|
||||
let wide: string;
|
||||
let integral: string | null = null;
|
||||
if (integer) {
|
||||
// A proven integer needs no double round-trip. Negative signed values
|
||||
// compare above any length or capacity as unsigned 64-bit integers.
|
||||
wide = widenInteger(host, integer);
|
||||
if (integer !== null) {
|
||||
// A proven integer, or a wide view that equals the double at every
|
||||
// index below a length, needs no double round-trip. Negative signed
|
||||
// values compare above any length or capacity as unsigned 64-bit
|
||||
// integers, and every index that fails the comparisons takes the
|
||||
// double's path.
|
||||
wide = integer;
|
||||
} else if (nanIndex) {
|
||||
// An exact integer or NaN is an index exactly when it is ordered.
|
||||
wide = nanIndex.wide;
|
||||
@@ -239,9 +242,10 @@ function statePointer(
|
||||
return statePtr;
|
||||
}
|
||||
|
||||
/** How an array's dense slots hold its elements: the access kind, or
|
||||
* `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte
|
||||
* slot holding the address over 8; LlEmitter.compactArrayElem). */
|
||||
/** How an array's dense slots hold its elements: the access kind (`i32`
|
||||
* for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact
|
||||
* class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8;
|
||||
* LlEmitter.compactArrayElem). */
|
||||
type SlotKind = ArrayAccess | "cref";
|
||||
|
||||
function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind {
|
||||
@@ -249,14 +253,14 @@ function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType):
|
||||
}
|
||||
|
||||
/** The dense slot type: boolean arrays store one byte per element, compact
|
||||
* references four, every other element kind an 8-byte slot. */
|
||||
* references and int32 arrays four, every other element kind an 8-byte slot. */
|
||||
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
|
||||
return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64";
|
||||
return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64";
|
||||
}
|
||||
|
||||
/** Bytes per dense slot (scr_arr_slot_width). */
|
||||
function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" ? 4 : 8;
|
||||
export function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8;
|
||||
}
|
||||
|
||||
/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */
|
||||
@@ -264,6 +268,17 @@ export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): numbe
|
||||
return slotWidth(slotKind(host, elemAccess(element), element));
|
||||
}
|
||||
|
||||
/** The runtime accessor family of an access: int32 arrays answer through
|
||||
* the double-valued entry points, which convert their slots. */
|
||||
export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" {
|
||||
return acc === "i32" ? "f64" : acc;
|
||||
}
|
||||
|
||||
/** The LLVM type of an access's values at the emitter's interface. */
|
||||
function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" {
|
||||
return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
}
|
||||
|
||||
function valuePointer(
|
||||
host: LlvmEmitterContext,
|
||||
guard: Pick<DenseGuard, "offset" | "data">,
|
||||
@@ -285,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string {
|
||||
return state;
|
||||
}
|
||||
|
||||
export type ArrayAccess = "f64" | "bool" | "ref";
|
||||
/** The ScrElemKind of int32 arrays (scr_array.h). */
|
||||
export const SCR_ELEM_I32 = 7;
|
||||
|
||||
/** How a dense array's slots are read and written: doubles, booleans,
|
||||
* references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts),
|
||||
* whose values are doubles at the emitter's interface. */
|
||||
export type ArrayAccess = "f64" | "bool" | "ref" | "i32";
|
||||
|
||||
/** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference
|
||||
* element: when `index` lies in the hole-free prefix and the slot still
|
||||
@@ -341,15 +362,33 @@ export function emitDenseArrayGet(
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven)
|
||||
.value;
|
||||
}
|
||||
|
||||
/** emitDenseArrayGet, with the int32 of an int32 array's element: the
|
||||
* slot itself on the dense path, the runtime getter's double converted
|
||||
* (exactly: the element is an int32) on the other. */
|
||||
export function emitDenseArrayRead(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): { value: string; int32: string | null } {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const ret = acc === "bool" ? "zeroext i1" : ty;
|
||||
const kind = slotKind(host, acc, element);
|
||||
host.declare(`declare ${ret} @${getter}(ptr, double)`);
|
||||
if (acc === "ref" && retain && !isRefCounted(element)) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`);
|
||||
return t;
|
||||
return { value: t, int32: null };
|
||||
}
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
@@ -380,10 +419,16 @@ export function emitDenseArrayGet(
|
||||
B.startBlock(valueLabel);
|
||||
const ptr = valuePointer(host, guard, kind);
|
||||
let fast: string;
|
||||
let fastInt: string | null = null;
|
||||
let fastLabel = valueLabel;
|
||||
if (acc === "f64") {
|
||||
fast = B.tmp();
|
||||
B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
} else if (acc === "i32") {
|
||||
fastInt = B.tmp();
|
||||
fast = B.tmp();
|
||||
B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
B.line(`${fast} = sitofp i32 ${fastInt} to double`);
|
||||
} else if (acc === "bool") {
|
||||
const raw = B.tmp();
|
||||
fast = B.tmp();
|
||||
@@ -410,11 +455,21 @@ export function emitDenseArrayGet(
|
||||
const slowValue = B.tmp();
|
||||
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
|
||||
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
|
||||
let slowInt: string | null = null;
|
||||
if (fastInt !== null) {
|
||||
slowInt = B.tmp();
|
||||
B.line(`${slowInt} = fptosi double ${slowValue} to i32`);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
|
||||
return result;
|
||||
let int32: string | null = null;
|
||||
if (fastInt !== null && slowInt !== null) {
|
||||
int32 = B.tmp();
|
||||
B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`);
|
||||
}
|
||||
return { value: result, int32 };
|
||||
}
|
||||
|
||||
/** `scr_arr_get_number`: a numeric element, or NaN for every missing,
|
||||
@@ -425,13 +480,14 @@ export function emitDenseArrayNumber(
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
acc: "f64" | "i32" = "f64",
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
indexExpr,
|
||||
"f64",
|
||||
acc,
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
@@ -486,7 +542,9 @@ export function emitDenseArrayState(
|
||||
* value's reference. A dense index stores the slot, marks it present, and
|
||||
* extends the length when it lands at or past it; a replaced present
|
||||
* reference is released after the new edge is published (a release can
|
||||
* collect cycles). Every other index takes the unchanged runtime call. */
|
||||
* collect cycles). Every other index takes the unchanged runtime call.
|
||||
* `int32`, for an int32 array, is the value as an i32 when the caller has
|
||||
* it at hand. */
|
||||
export function emitDenseArraySet(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
@@ -496,11 +554,12 @@ export function emitDenseArraySet(
|
||||
element: IrType,
|
||||
value: string,
|
||||
proven = false,
|
||||
int32: string | null = null,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const kind = slotKind(host, acc, element);
|
||||
const setter = `scr_arr_set_${acc}`;
|
||||
const setter = `scr_arr_set_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const call = (attrs: string): void =>
|
||||
B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`);
|
||||
@@ -508,6 +567,13 @@ export function emitDenseArraySet(
|
||||
call("");
|
||||
return;
|
||||
}
|
||||
// An int32 array's stores are proven int32s (ir/int32-arrays.ts); both
|
||||
// dense paths below store the converted value.
|
||||
let int32Value: string | null = int32;
|
||||
if (acc === "i32" && int32Value === null) {
|
||||
int32Value = B.tmp();
|
||||
B.line(`${int32Value} = fptosi double ${value} to i32`);
|
||||
}
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
@@ -526,6 +592,8 @@ export function emitDenseArraySet(
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
@@ -713,12 +781,21 @@ export function emitDenseArrayPop(
|
||||
B.line(`${wide} = zext i8 ${raw} to i64`);
|
||||
raw = wide;
|
||||
} else if (slotTy === "i32") {
|
||||
// The 8-byte slot of a compact reference is its address.
|
||||
const wide = B.tmp(),
|
||||
address = B.tmp();
|
||||
B.line(`${wide} = zext i32 ${raw} to i64`);
|
||||
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
|
||||
raw = address;
|
||||
if (kind === "cref") {
|
||||
// The 8-byte slot of a compact reference is its address.
|
||||
const wide = B.tmp(),
|
||||
address = B.tmp();
|
||||
B.line(`${wide} = zext i32 ${raw} to i64`);
|
||||
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
|
||||
raw = address;
|
||||
} else {
|
||||
// The slot bits of an int32 element's double, as the f64 path returns them.
|
||||
const number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${raw} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
raw = bits;
|
||||
}
|
||||
}
|
||||
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
// Only an array with states has a state byte to clear.
|
||||
@@ -755,8 +832,8 @@ export function emitDenseArrayPush(
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const kind = element === null ? acc : slotKind(host, acc, element);
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const pusher = `scr_arr_push_${acc}`;
|
||||
const ty = valueType(acc);
|
||||
const pusher = `scr_arr_push_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const header = (field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
@@ -794,6 +871,10 @@ export function emitDenseArrayPush(
|
||||
const valuePtr = valuePointer(host, { offset: len, data }, kind);
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
const int32 = B.tmp();
|
||||
B.line(`${int32} = fptosi double ${value} to i32`);
|
||||
B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
|
||||
@@ -61,6 +61,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
@@ -138,6 +139,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
emitter.int32Kinds = null;
|
||||
emitter.int32KindResume = null;
|
||||
emitter.finalFieldBorrows =
|
||||
!fn.async &&
|
||||
!fn.generator &&
|
||||
|
||||
@@ -34,9 +34,16 @@ import { dynHoleRead } from "./loop-array-borrows.js";
|
||||
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
|
||||
import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArraySet,
|
||||
SCR_ELEM_I32,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js";
|
||||
import { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js";
|
||||
import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js";
|
||||
import { withinInt32 } from "../../ir/integer-ranges.js";
|
||||
import { resultSlotStore } from "../result-slots.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
@@ -411,7 +418,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const v = emitter.emitExpr(s.value);
|
||||
if (s.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
if (emitter.checkedInt32Array(s.arr)) {
|
||||
// An int32 array with checked stores: the int32 arm stores an int32
|
||||
// value, and widens the array for any other first.
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
const elem = s.arr.type.elem;
|
||||
const proven = emitter.provenDenseIndex(s.arr, s.index);
|
||||
emitter.emitByElementKind(arr.name, null, (acc, widened) => {
|
||||
if (acc === "i32") emitter.emitCheckedInt32Store(arr.name, v.name, s.value, widened);
|
||||
emitDenseArraySet(emitter, arr.name, idx, s.index, acc, elem, v.name, proven);
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
const acc = emitter.arrayAccess(s.arr, s.arr.type.elem);
|
||||
const int32 = emitter.emitInt32KindStoreCheck(s.arr, v, s.value);
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") emitter.toHeap(v);
|
||||
emitDenseArraySet(
|
||||
@@ -423,6 +444,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.arr.type.elem,
|
||||
v.name,
|
||||
emitter.provenDenseIndex(s.arr, s.index),
|
||||
int32,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -478,12 +500,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(dropsLast, pop, call);
|
||||
B.startBlock(pop);
|
||||
const elem = s.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
|
||||
if (acc === "ref" && isRefCounted(elem)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
|
||||
emitter.releaseHeap(old, elem);
|
||||
if (emitter.checkedInt32Array(s.arr)) {
|
||||
emitter.emitByElementKind(arr.name, null, (acc) => {
|
||||
emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
const acc = emitter.arrayAccess(s.arr, elem);
|
||||
const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
|
||||
if (acc === "ref" && isRefCounted(elem)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
|
||||
emitter.releaseHeap(old, elem);
|
||||
}
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(call);
|
||||
@@ -1826,7 +1855,7 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch"
|
||||
* case, refcounted/boxed case-body locals are NULL-reset up front and
|
||||
* the scope-exit releases rely on NULL tolerance. */
|
||||
export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void {
|
||||
const unitSwitch = charAtUnitSwitch(s);
|
||||
const unitSwitch = charAtUnitSwitch(emitter, s);
|
||||
if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch);
|
||||
const B = emitter.B;
|
||||
const discKind = s.disc.type.kind;
|
||||
@@ -2098,9 +2127,26 @@ export function emitLoopCopies(
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
const proven = allowed ? planProvenLoop(emitter, loop) : null;
|
||||
if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
|
||||
else emitCopy(exit);
|
||||
const ordinary = (exit: string): void => {
|
||||
const proven = allowed ? planProvenLoop(emitter, loop) : null;
|
||||
if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
|
||||
else emitCopy(exit);
|
||||
};
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planInt32KindLoop(
|
||||
loop,
|
||||
(id) => emitter.checkedInt32Local(id),
|
||||
(e) => withinInt32(emitter.integerRanges.get(e)),
|
||||
)
|
||||
: null;
|
||||
if (kinds) emitInt32KindLoop(emitter, kinds, exit, kinds.writes.size > 0 ? emitCopy : ordinary);
|
||||
else ordinary(exit);
|
||||
return;
|
||||
}
|
||||
const B = emitter.B;
|
||||
@@ -2243,6 +2289,124 @@ export function emitLoopCopies(
|
||||
}
|
||||
}
|
||||
|
||||
/** An int32 kind loop (int32-kind-loops.ts). The entry test reads the kind
|
||||
* of each of the plan's arrays: when every one holds int32 slots the fast
|
||||
* copy runs, when every one holds doubles the wide copy, which reads and
|
||||
* writes them as double arrays, and otherwise (a plan with several arrays)
|
||||
* the mixed copy, which tests their kinds at each access. A failed store of
|
||||
* the fast copy re-executes in the third copy, which finishes its
|
||||
* iteration with kind tests and then enters the mixed copy, or the wide
|
||||
* copy when the plan has one array (it just widened). Each copy is emitted
|
||||
* by `ordinary`: the loop's plain copy when a store may fail, since a
|
||||
* proven integer loop keeps locals in shadows of its own copy that another
|
||||
* copy cannot resume from. */
|
||||
function emitInt32KindLoop(
|
||||
emitter: LlEmitter,
|
||||
plan: Int32KindLoopPlan,
|
||||
exit: string,
|
||||
ordinary: (exit: string) => void,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const fn = emitter.currentFunctionName;
|
||||
const keys = new Set([...plan.arrays.keys()].map((id) => `${fn}\0${id}`));
|
||||
const several = keys.size > 1;
|
||||
let allInt32 = "true",
|
||||
allWide = "true";
|
||||
const and = (all: string, next: string): string => {
|
||||
if (all === "true") return next;
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${all}, ${next}`);
|
||||
return both;
|
||||
};
|
||||
for (const ref of plan.arrays.values()) {
|
||||
const arr = emitter.emitStableReceiver(ref, []);
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
isInt32 = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.elem}`,
|
||||
);
|
||||
emitter.markMemoryPointer(kindPtr, "array:header");
|
||||
B.line(`${kind} = load i8, ptr ${kindPtr}${emitter.fieldAliasAttachment(kindPtr)}`);
|
||||
B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`);
|
||||
allInt32 = and(allInt32, isInt32);
|
||||
if (several) {
|
||||
const isWide = B.tmp();
|
||||
B.line(`${isWide} = xor i1 ${isInt32}, true`);
|
||||
allWide = and(allWide, isWide);
|
||||
}
|
||||
}
|
||||
const fast = B.newLabel("kind.fast"),
|
||||
wide = B.newLabel("kind.wide"),
|
||||
mixed = several ? B.newLabel("kind.mixed") : null;
|
||||
if (mixed === null) B.condBr(allInt32, fast, wide);
|
||||
else {
|
||||
const other = B.newLabel("kind.other");
|
||||
B.condBr(allInt32, fast, other);
|
||||
B.startBlock(other);
|
||||
B.condBr(allWide, wide, mixed);
|
||||
}
|
||||
const resume = new Map<IrStmt, { label: string; placed: boolean }>();
|
||||
for (const s of plan.writes) resume.set(s, { label: B.newLabel("kind.resume"), placed: false });
|
||||
// The header the third copy enters: the mixed copy's, or the wide copy's.
|
||||
let header: string | null = null;
|
||||
const capture = (label: string): void => {
|
||||
header = label;
|
||||
};
|
||||
B.startBlock(wide);
|
||||
if (resume.size > 0 && mixed === null) emitter.speculationHeader = capture;
|
||||
emitter.int32Kinds = { int32: new Set(), wide: keys, resume: new Map() };
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationHeader = null;
|
||||
emitter.int32Kinds = null;
|
||||
}
|
||||
if (mixed !== null) {
|
||||
B.startBlock(mixed);
|
||||
if (resume.size > 0) emitter.speculationHeader = capture;
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
}
|
||||
if (resume.size > 0) {
|
||||
if (header === null)
|
||||
throw new InternalCompilerError("llvm emitter bug: int32 kind loop copy without a header");
|
||||
// A failed store finishes its iteration here, as in a speculative
|
||||
// integer loop's third copy.
|
||||
B.startBlock(B.newLabel("kind.tail"));
|
||||
emitter.speculationResume = resume;
|
||||
emitter.speculationHeader = () => {
|
||||
B.br(header!);
|
||||
B.startBlock(B.newLabel("kind.unused"));
|
||||
};
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
}
|
||||
B.startBlock(fast);
|
||||
if (![...resume.values()].every((r) => r.placed)) {
|
||||
// A write the third copy never reached: this copy tests the kinds.
|
||||
ordinary(exit);
|
||||
return;
|
||||
}
|
||||
emitter.int32Kinds = {
|
||||
int32: keys,
|
||||
wide: new Set(),
|
||||
resume: new Map([...resume].map(([s, r]) => [s, r.label])),
|
||||
};
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.int32Kinds = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
@@ -2518,7 +2682,10 @@ function nullableStringSwitch(
|
||||
* one-unit string literals, as a switch on `s.charCodeAt(i)` with the units
|
||||
* as cases: no character string is built. An index out of range answers
|
||||
* NaN, which matches no case, as "" matches no one-unit literal. */
|
||||
function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null {
|
||||
function charAtUnitSwitch(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "switch" },
|
||||
): (IrStmt & { kind: "switch" }) | null {
|
||||
const disc = s.disc;
|
||||
if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1)
|
||||
return null;
|
||||
@@ -2531,7 +2698,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw
|
||||
return null;
|
||||
return {
|
||||
...s,
|
||||
disc: charAtUnitRead(disc),
|
||||
disc: charAtUnitRead(emitter, disc),
|
||||
cases: s.cases.map((c) =>
|
||||
c.test?.kind === "strLit"
|
||||
? {
|
||||
|
||||
@@ -79,8 +79,18 @@ import {
|
||||
} from "./emit-journal.js";
|
||||
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
|
||||
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
|
||||
import {
|
||||
allocatedLiteral,
|
||||
fillAllocationLength,
|
||||
selectInt32Arrays,
|
||||
} from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import {
|
||||
INT32_RANGE,
|
||||
withinInt32,
|
||||
type IntegerRange,
|
||||
type IntegerRanges,
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
@@ -100,7 +110,7 @@ import {
|
||||
} from "./int32-abi.js";
|
||||
import { type Int32Local } from "./int32-locals.js";
|
||||
import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js";
|
||||
import { exactInteger, narrowInteger } from "./integer-values.js";
|
||||
import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js";
|
||||
import { findInitializerBindings } from "../../ir/initializer-bindings.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
import {
|
||||
@@ -147,7 +157,12 @@ import {
|
||||
} from "./inline-unions.js";
|
||||
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
|
||||
import { CheckedNarrows } from "./checked-narrows.js";
|
||||
import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
|
||||
import {
|
||||
emitDenseArraySet,
|
||||
emitDenseSlotUnlink,
|
||||
SCR_ELEM_I32,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
@@ -168,7 +183,7 @@ import {
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import { emitFieldAbsentTest, f64Lit } from "./common.js";
|
||||
import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } from "./common.js";
|
||||
import { ffiExtendsNarrowIntegers } from "../targets.js";
|
||||
import {
|
||||
emitLiteralExpr,
|
||||
@@ -483,39 +498,65 @@ function versionParameters(mod: IrModule): IrModule {
|
||||
return versionLoopBoundParameters(mod, mangleFunction);
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and
|
||||
* checked), under the inputs LlEmitter gives it (selectInt32Arrays). */
|
||||
function provenInt32Arrays(mod: IrModule): ReadonlySet<string> {
|
||||
const slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores: dynamicFieldStores(mod),
|
||||
initializerBindings: findInitializerBindings(mod),
|
||||
});
|
||||
return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
|
||||
}
|
||||
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
provenInt32Arrays,
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emit();
|
||||
);
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
return moduleEmitter(input, options).emit();
|
||||
}
|
||||
|
||||
/** emitLlvmModule's text as parts that concatenate to it, without the copy
|
||||
* that joining them makes: a large program's text is 150 MB. */
|
||||
export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
|
||||
return moduleEmitter(input, options)
|
||||
.emitParts()
|
||||
.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
}
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
@@ -525,35 +566,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
// IR validation, contracts and the frontend's disposal run after lowering without checks.
|
||||
relieveHeapPressure(true);
|
||||
const mod = unshareNodes(input);
|
||||
const parts = new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emitParts();
|
||||
const parts = moduleEmitter(input, options).emitParts();
|
||||
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
|
||||
if (length <= 256 * 1024 * 1024) return parts.join("\n");
|
||||
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
@@ -734,6 +747,228 @@ export class LlEmitter {
|
||||
return `, !tbaa !${tag}`;
|
||||
}
|
||||
|
||||
/** The current function's int32 array allocations: the array literals
|
||||
* its int32 arrays are written with. They construct SCR_ELEM_I32 arrays. */
|
||||
int32Allocations = new Set<IrExpr>();
|
||||
/** The fill temporaries (`new Array(n).fill(v)`) of those allocations:
|
||||
* the same arrays, so int32 too. */
|
||||
int32ArrayTemps = new Set<string>();
|
||||
/** The same for the current function's int32 arrays with checked stores,
|
||||
* which are only allocated by fills (ir/int32-arrays.ts
|
||||
* checkableInt32Allocations): their temporaries' literals, with the fill's
|
||||
* length (a literal constructs SCR_ELEM_I32 storage when that length is at
|
||||
* least CHECKED_INT32_MIN_LENGTH, SCR_ELEM_F64 otherwise), and their
|
||||
* temporaries. */
|
||||
checkedAllocations = new Map<IrExpr, IrExpr>();
|
||||
checkedArrayTemps = new Set<string>();
|
||||
/** The fill calls (`.fill(v)`) of those allocations: a value that is not
|
||||
* an int32 widens the new array first. */
|
||||
checkedFills = new Set<IrExpr>();
|
||||
|
||||
/** Collect the int32 allocations of `fn`, about to be emitted. */
|
||||
findInt32Allocations(fn: IrFunction): void {
|
||||
this.int32Allocations.clear();
|
||||
this.int32ArrayTemps.clear();
|
||||
this.checkedAllocations.clear();
|
||||
this.checkedArrayTemps.clear();
|
||||
this.checkedFills.clear();
|
||||
const { int32Arrays, checkedInt32Arrays } = this.int32Slots;
|
||||
if (int32Arrays.size === 0 && checkedInt32Arrays.size === 0) return;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return true;
|
||||
const key = `${fn.name}\0${s.localId}`;
|
||||
const checked = checkedInt32Arrays.has(key);
|
||||
if (!checked && !int32Arrays.has(key)) return true;
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const allocation = value && allocatedLiteral(value);
|
||||
if (!allocation) return true;
|
||||
const length = checked ? fillAllocationLength(value) : null;
|
||||
if (length !== null) this.checkedAllocations.set(allocation.literal, length);
|
||||
else if (!checked) this.int32Allocations.add(allocation.literal);
|
||||
if (allocation.temporary !== null)
|
||||
(checked ? this.checkedArrayTemps : this.int32ArrayTemps).add(allocation.temporary);
|
||||
if (checked && value !== allocation.literal) this.checkedFills.add(value!);
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether `arr` is one of the current function's int32 arrays
|
||||
* (ir/int32-arrays.ts, kept by the int32 slot analysis). */
|
||||
int32Array(arr: IrExpr): boolean {
|
||||
if (arr.kind !== "varRef") return false;
|
||||
if (this.int32KindArray(arr)) return true;
|
||||
if (this.int32Slots.int32Arrays.size === 0) return false;
|
||||
return (
|
||||
this.int32ArrayTemps.has(arr.localId) ||
|
||||
this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `arr` is one of the current function's int32 arrays with
|
||||
* checked stores: int32 slots until a store widens it to doubles. Every
|
||||
* access tests its kind (emitByElementKind), except in the fast copy of
|
||||
* an int32 kind loop, which reads and writes it as an int32 array. */
|
||||
checkedInt32Array(arr: IrExpr): boolean {
|
||||
if (arr.kind !== "varRef" || this.int32KindArray(arr)) return false;
|
||||
if (this.int32Kinds?.wide.has(`${this.currentFunctionName}\0${arr.localId}`)) return false;
|
||||
return this.checkedInt32Local(arr.localId);
|
||||
}
|
||||
|
||||
/** Whether the local `localId` holds one of the current function's int32
|
||||
* arrays with checked stores. */
|
||||
checkedInt32Local(localId: string): boolean {
|
||||
if (this.int32Slots.checkedInt32Arrays.size === 0) return false;
|
||||
return (
|
||||
this.checkedArrayTemps.has(localId) ||
|
||||
this.int32Slots.checkedInt32Arrays.has(`${this.currentFunctionName}\0${localId}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `arr` is an int32 array with checked stores that the fast copy
|
||||
* of the int32 kind loop being emitted holds as an int32 array. */
|
||||
int32KindArray(arr: IrExpr): boolean {
|
||||
return (
|
||||
this.int32Kinds !== null &&
|
||||
arr.kind === "varRef" &&
|
||||
this.int32Kinds.int32.has(`${this.currentFunctionName}\0${arr.localId}`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Before a store of `value` (of `expr`) into `arr`, in the fast copy of
|
||||
* an int32 kind loop: continue when it is an int32 that is not -0, and
|
||||
* otherwise leave for the statement's resume point, where it re-executes
|
||||
* with kind tests. Returns the value as an i32 when it is at hand as an
|
||||
* integer, or null when the store converts the double itself. */
|
||||
emitInt32KindStoreCheck(arr: IrExpr, value: LlValue, expr: IrExpr): string | null {
|
||||
if (!this.int32KindArray(arr) || withinInt32(this.integerRanges.get(expr))) return null;
|
||||
const integer = exactInteger(this, value, expr);
|
||||
if (integer !== null && withinInt32(integer.range)) return narrowInteger(this, integer);
|
||||
const resume = this.int32KindResume;
|
||||
if (resume === null)
|
||||
throw new InternalCompilerError("llvm emitter bug: an int32 kind store without a resume");
|
||||
const B = this.B;
|
||||
const ok = B.tmp(),
|
||||
int = B.tmp();
|
||||
if (integer !== null) {
|
||||
// An exact integer: a range test on its 64-bit form.
|
||||
const wide = widenInteger(this, integer),
|
||||
low = B.tmp(),
|
||||
high = B.tmp();
|
||||
B.line(`${low} = icmp sge i64 ${wide}, -2147483648`);
|
||||
B.line(`${high} = icmp sle i64 ${wide}, 2147483647`);
|
||||
B.line(`${ok} = and i1 ${low}, ${high}`);
|
||||
const store = B.newLabel("arr.store.i32");
|
||||
B.condBr(ok, store, resume);
|
||||
B.startBlock(store);
|
||||
B.line(`${int} = trunc i64 ${wide} to i32`);
|
||||
return int;
|
||||
}
|
||||
this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)");
|
||||
const back = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
notNegativeZero = B.tmp();
|
||||
B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value.name})`);
|
||||
B.line(`${back} = sitofp i32 ${int} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${back}, ${value.name}`);
|
||||
B.line(`${bits} = bitcast double ${value.name} to i64`);
|
||||
B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`);
|
||||
B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`);
|
||||
const store = B.newLabel("arr.store.i32");
|
||||
B.condBr(ok, store, resume);
|
||||
B.startBlock(store);
|
||||
return int;
|
||||
}
|
||||
|
||||
/** Emit an access to the checked int32 array `array` once per element
|
||||
* kind: `arm("i32", widened)` while it holds int32 slots and
|
||||
* `arm("f64", widened)` after it widened, joined after both. `widened` is
|
||||
* the label of the double arm, where an int32 arm's store of a value that
|
||||
* is not an int32 continues after widening the array. Returns the phi of
|
||||
* the arms' results (of LLVM type `ty`), or null when `ty` is null. */
|
||||
emitByElementKind(
|
||||
array: string,
|
||||
ty: string | null,
|
||||
arm: (acc: "i32" | "f64", widened: string) => string | null,
|
||||
): string | null {
|
||||
const B = this.B;
|
||||
const narrow = B.newLabel("arr.kind.i32"),
|
||||
wide = B.newLabel("arr.kind.f64"),
|
||||
join = B.newLabel("arr.kind.join");
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
isInt32 = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.elem}`,
|
||||
);
|
||||
this.markMemoryPointer(kindPtr, "array:header");
|
||||
B.line(`${kind} = load i8, ptr ${kindPtr}${this.fieldAliasAttachment(kindPtr)}`);
|
||||
B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`);
|
||||
B.condBr(isInt32, narrow, wide);
|
||||
B.startBlock(narrow);
|
||||
const narrowValue = arm("i32", wide);
|
||||
const narrowEnd = B.isTerminated() ? null : B.currentLabel();
|
||||
if (narrowEnd !== null) B.br(join);
|
||||
B.startBlock(wide);
|
||||
const wideValue = arm("f64", wide);
|
||||
const wideEnd = B.isTerminated() ? null : B.currentLabel();
|
||||
if (wideEnd !== null) B.br(join);
|
||||
B.startBlock(join);
|
||||
// Arms that end in their own branches (a read's shared join) leave this
|
||||
// join without predecessors.
|
||||
if (narrowEnd === null && wideEnd === null) B.terminate("unreachable");
|
||||
if (ty === null) return null;
|
||||
const arms: string[] = [];
|
||||
if (narrowEnd !== null) arms.push(`[ ${narrowValue}, %${narrowEnd} ]`);
|
||||
if (wideEnd !== null) arms.push(`[ ${wideValue}, %${wideEnd} ]`);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ${ty} ${arms.join(", ")}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Before storing the double `value` (of `expr`) into an int32 arm
|
||||
* (emitByElementKind) of the checked int32 array `array`: continue in this
|
||||
* arm when it is an int32 that is not -0, or widen the array and continue
|
||||
* at `widened`. A value proven an int32 needs no test. */
|
||||
emitCheckedInt32Store(array: string, value: string, expr: IrExpr, widened: string): void {
|
||||
if (withinInt32(this.integerRanges.get(expr))) return;
|
||||
const B = this.B;
|
||||
this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)");
|
||||
this.declare("declare void @scr_arr_widen_i32(ptr)");
|
||||
const int = B.tmp(),
|
||||
back = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
notNegativeZero = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value})`);
|
||||
B.line(`${back} = sitofp i32 ${int} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${back}, ${value}`);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`);
|
||||
B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`);
|
||||
const store = B.newLabel("arr.store.i32"),
|
||||
widen = B.newLabel("arr.widen");
|
||||
B.condBr(ok, store, widen);
|
||||
B.startBlock(widen);
|
||||
B.line(`call void @scr_arr_widen_i32(ptr ${array})`);
|
||||
B.br(widened);
|
||||
B.startBlock(store);
|
||||
}
|
||||
|
||||
/** The access an element operation on `arr` (of element type `elem`)
|
||||
* takes: int32 arrays read and write their 4-byte slots. */
|
||||
arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess {
|
||||
if (this.checkedInt32Array(arr))
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: a checked int32 array without its kind test",
|
||||
);
|
||||
return this.int32Array(arr) ? "i32" : elemAccess(elem);
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
@@ -1180,6 +1415,14 @@ export class LlEmitter {
|
||||
* (`enterLoopHeader`): records the ordinary copy's header, or sends the
|
||||
* third copy's header there. */
|
||||
speculationHeader: ((header: string) => void) | null = null;
|
||||
/** A copy of an int32 kind loop (int32-kind-loops.ts) being emitted: the
|
||||
* int32 arrays with checked stores it reads and writes as int32 arrays
|
||||
* (the fast copy) or as double arrays (an ordinary copy entered only
|
||||
* after they widened), as `function\0local` keys, and where each of the
|
||||
* fast copy's writes resumes in the third copy. */
|
||||
int32Kinds: { int32: Set<string>; wide: Set<string>; resume: Map<IrStmt, string> } | null = null;
|
||||
/** The resume label of the fast-copy write being emitted. */
|
||||
int32KindResume: string | null = null;
|
||||
/** The function being emitted, for speculative globals. */
|
||||
speculativeFunction = "";
|
||||
/** Every function that writes each number global (computed on demand). */
|
||||
@@ -1201,6 +1444,10 @@ export class LlEmitter {
|
||||
* (findIntegerOrNaNLocals in wrapping-sums.ts). */
|
||||
integerOrNaNLocals: ReadonlyMap<string, IntegerRange> = new Map();
|
||||
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
/** The charCodeAt reads standing for charAt reads the emitter rewrote
|
||||
* (one-unit tests and switches, charAtUnitRead), with the charAt each
|
||||
* stands for: a loop version's proof for the charAt covers its read. */
|
||||
readonly charAtUnitReads = new Map<IrExpr, IrExpr>();
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
@@ -5268,10 +5515,14 @@ export class LlEmitter {
|
||||
const loop =
|
||||
s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
|
||||
if (loop) this.loopDepth++;
|
||||
// An int32 kind loop's fast-copy write leaves for its resume point.
|
||||
const kindResume = this.int32KindResume;
|
||||
this.int32KindResume = this.int32Kinds?.resume.get(s) ?? null;
|
||||
try {
|
||||
this.emitStmtBody(s);
|
||||
} finally {
|
||||
this.B.debugLocation = previous;
|
||||
this.int32KindResume = kindResume;
|
||||
if (loop) this.loopDepth--;
|
||||
}
|
||||
}
|
||||
@@ -6518,7 +6769,7 @@ export class LlEmitter {
|
||||
return tagInSet(this, uName, unionId, tags);
|
||||
}
|
||||
|
||||
arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string {
|
||||
arrPush(arr: string, acc: ArrayAccess, value: string, element: IrType | null): string {
|
||||
return arrPush(this, arr, acc, value, element);
|
||||
}
|
||||
|
||||
|
||||
@@ -1118,7 +1118,9 @@ export function emitInlineAtomicsOp(
|
||||
if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null;
|
||||
const elem = array.type.elem;
|
||||
const B = host.B;
|
||||
const receiver = host.emitExpr(array).name;
|
||||
// The instruction and scr_atomics_op borrow the array: an unchanged local
|
||||
// owns it across the later operands and the operation.
|
||||
const receiver = emitBorrowedInput(host, array).name;
|
||||
const idxValue = host.emitExpr(index);
|
||||
const operand = host.emitExpr(value);
|
||||
const swap = host.emitExpr(replacement);
|
||||
|
||||
@@ -60,7 +60,10 @@ import {
|
||||
emitDenseArrayNumber,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArrayPush,
|
||||
runtimeAccess,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
@@ -115,7 +118,7 @@ export function tagInSet(
|
||||
export function arrPush(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
value: string,
|
||||
element: IrType | null,
|
||||
): string {
|
||||
@@ -126,7 +129,7 @@ export function arrPush(
|
||||
export function emitArrayValues(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
values: LlValue[],
|
||||
prepend = false,
|
||||
@@ -142,12 +145,15 @@ export function emitArrayValues(
|
||||
}
|
||||
if (values.length === 1) {
|
||||
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const flat = runtimeAccess(acc);
|
||||
const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr";
|
||||
host.declare(
|
||||
`declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
|
||||
`declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`);
|
||||
B.line(
|
||||
`${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
const capacity = Math.min(values.length, 64);
|
||||
@@ -165,8 +171,9 @@ export function emitArrayValues(
|
||||
const slot = B.tmp(),
|
||||
pointer = B.tmp();
|
||||
const value = values[start + i]!.name;
|
||||
// The runtime takes double bits for int32 arrays too, and converts.
|
||||
const pack =
|
||||
acc === "f64"
|
||||
acc === "f64" || acc === "i32"
|
||||
? `bitcast double ${value} to i64`
|
||||
: acc === "bool"
|
||||
? `zext i1 ${value} to i64`
|
||||
@@ -286,14 +293,11 @@ export function emitArrayCopyLoop(
|
||||
host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`);
|
||||
}
|
||||
|
||||
/** The `charCodeAt` reads that stand for `charAt` reads the emitter
|
||||
* rewrote, such as one-unit tests and switches: a loop version's proof
|
||||
* for the charAt covers its read. */
|
||||
const unitReadOrigins = new WeakMap<IrExpr, IrExpr>();
|
||||
|
||||
/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a
|
||||
* code unit. */
|
||||
* code unit. The emitter records the charAt it stands for
|
||||
* (charAtUnitReads), so a loop version's proof for the charAt covers it. */
|
||||
export function charAtUnitRead(
|
||||
host: LlvmEmitterContext,
|
||||
charAt: IrExpr & { kind: "strIntrinsic" },
|
||||
): IrExpr & { kind: "strIntrinsic" } {
|
||||
const read: IrExpr & { kind: "strIntrinsic" } = {
|
||||
@@ -304,7 +308,7 @@ export function charAtUnitRead(
|
||||
type: F64,
|
||||
loc: charAt.loc,
|
||||
};
|
||||
unitReadOrigins.set(read, charAt);
|
||||
host.charAtUnitReads.set(read, charAt);
|
||||
return read;
|
||||
}
|
||||
|
||||
@@ -312,7 +316,7 @@ export function charAtUnitRead(
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (host.bytesBounds.has(e)) return true;
|
||||
const origin = unitReadOrigins.get(e);
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined && host.bytesBounds.has(origin);
|
||||
}
|
||||
|
||||
@@ -610,7 +614,15 @@ export function emitArrIntrinsic(
|
||||
if (e.receiver.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
|
||||
const elem = e.receiver.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
// Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others
|
||||
// take the runtime family's flat access. One with checked stores tests
|
||||
// its kind at each inline access (emitByElementKind) and otherwise takes
|
||||
// the double entry points, which serve both kinds.
|
||||
const checked = host.checkedInt32Array(e.receiver);
|
||||
const access = checked ? "f64" : host.arrayAccess(e.receiver, elem);
|
||||
if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method))
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`);
|
||||
const acc = runtimeAccess(access);
|
||||
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const accArg = acc === "bool" ? "i1 zeroext" : accTy;
|
||||
const method = e.method;
|
||||
@@ -631,6 +643,13 @@ export function emitArrIntrinsic(
|
||||
e.receiver.kind === "varRef"
|
||||
? host.constantNumericTables.get(e.receiver.localId)
|
||||
: undefined;
|
||||
if (!table && elem.kind === "f64" && checked) {
|
||||
const proven = host.provenDenseIndex(e.receiver, e.args[0]!);
|
||||
const value = host.emitByElementKind(r.name, "double", (kind) =>
|
||||
emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind),
|
||||
)!;
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (!table && elem.kind === "f64")
|
||||
return {
|
||||
name: emitDenseArrayNumber(
|
||||
@@ -639,6 +658,7 @@ export function emitArrIntrinsic(
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
access === "i32" ? "i32" : "f64",
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
@@ -675,7 +695,22 @@ export function emitArrIntrinsic(
|
||||
// no-op zero-argument call.
|
||||
const vs = e.args.map((a) => host.emitExpr(a));
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length));
|
||||
return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type };
|
||||
if (checked && vs.length > 0) {
|
||||
// Each value appends in its own kind test: one that is not an int32
|
||||
// widens the array before it is pushed.
|
||||
let length = "";
|
||||
vs.forEach((v, i) => {
|
||||
length = host.emitByElementKind(r.name, "double", (kind, widened) => {
|
||||
if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened);
|
||||
return arrPush(host, r.name, kind, v.name, elem);
|
||||
})!;
|
||||
});
|
||||
return { name: length, type: e.type };
|
||||
}
|
||||
// In an int32 kind loop's fast copy, every value is checked before
|
||||
// the first appends.
|
||||
vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!));
|
||||
return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type };
|
||||
}
|
||||
case "pushSpread": {
|
||||
// `a.push(...src)`: append src's elements in order (borrowed src,
|
||||
@@ -749,7 +784,13 @@ export function emitArrIntrinsic(
|
||||
let slowLabel = "";
|
||||
if (method === "pop") {
|
||||
slowLabel = B.newLabel("remove.slow");
|
||||
denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel);
|
||||
const empty = la,
|
||||
slow = slowLabel;
|
||||
denseRaw = checked
|
||||
? host.emitByElementKind(r.name, "i64", (kind) =>
|
||||
emitDenseArrayPop(host, r.name, kind, elem, empty, slow),
|
||||
)!
|
||||
: emitDenseArrayPop(host, r.name, access, elem, la, slowLabel);
|
||||
denseLabel = B.currentLabel();
|
||||
B.br(lp);
|
||||
B.startBlock(slowLabel);
|
||||
@@ -938,6 +979,14 @@ export function emitArrIntrinsic(
|
||||
case "fillUndefined": {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
|
||||
if (host.checkedFills.has(e)) {
|
||||
// A new int32 array with checked stores, filled with a value that
|
||||
// may not be an int32: widen it first then.
|
||||
const filled = B.newLabel("arr.fill.kind");
|
||||
host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled);
|
||||
B.br(filled);
|
||||
B.startBlock(filled);
|
||||
}
|
||||
const helper =
|
||||
e.method === "copyWithin"
|
||||
? "scr_arr_copy_within"
|
||||
|
||||
@@ -15,11 +15,23 @@ export interface LlValue {
|
||||
/** An exact integer snapshot, excluding negative zero, with no discarded
|
||||
* high bits and the proven range of its original value. */
|
||||
integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange };
|
||||
/** An i64 holding the exact integer that the arithmetic producing this
|
||||
* number computes, when that integer has no proven safe range: its
|
||||
* absolute value is at most `bound`, and the double equals it whenever
|
||||
* its absolute value is below `below` (a zero may be -0). Index consumers
|
||||
* use it in place of a round trip (integer-values.ts `indexInteger`). */
|
||||
wideInteger?: WideInteger;
|
||||
slot?: boolean;
|
||||
/** A sequence local held in a capture box; release the box itself. */
|
||||
boxed?: boolean;
|
||||
}
|
||||
|
||||
export interface WideInteger {
|
||||
name: string;
|
||||
below: number;
|
||||
bound: number;
|
||||
}
|
||||
|
||||
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
||||
export type LibCallExpr = ExprOf<"libCall">;
|
||||
type LibCallPrefixOf<T extends string> = T extends `${infer Prefix}.${string}` ? Prefix : never;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
|
||||
import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
@@ -16,10 +17,12 @@ import {
|
||||
int32Number,
|
||||
integerNumber,
|
||||
narrowInteger,
|
||||
wideArithmetic,
|
||||
widenInteger,
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
INT32_RANGE,
|
||||
integerArithmeticRange,
|
||||
isBitwiseOp,
|
||||
withinInt32,
|
||||
@@ -32,7 +35,15 @@ import {
|
||||
emitPowerOfTwoRemainder,
|
||||
emitSignedIntegerRemainder,
|
||||
} from "./integer-remainder.js";
|
||||
import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
|
||||
import {
|
||||
denseSlotWidth,
|
||||
emitDenseArrayRead,
|
||||
emitDenseArrayState,
|
||||
runtimeAccess,
|
||||
SCR_ELEM_I32,
|
||||
slotWidth,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
|
||||
|
||||
export function emitLiteralExpr(
|
||||
@@ -337,7 +348,14 @@ export function emitOperatorExpr(
|
||||
if (arith[e.op] !== undefined) {
|
||||
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
if (uint32 !== undefined) return { name: t, type: e.type, uint32 };
|
||||
// The exact integer beside the double, for index consumers.
|
||||
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
|
||||
if (uint32 !== undefined || wideInteger) {
|
||||
const value: LlValue = { name: t, type: e.type };
|
||||
if (uint32 !== undefined) value.uint32 = uint32;
|
||||
if (wideInteger) value.wideInteger = wideInteger;
|
||||
return value;
|
||||
}
|
||||
} else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
|
||||
} else if (bit[e.op] !== undefined) {
|
||||
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
|
||||
@@ -648,7 +666,7 @@ export function emitStringExpr(
|
||||
// equals no unit, as "" equals no one-unit literal.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr(charAtUnitRead(unit.charAt));
|
||||
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
@@ -682,7 +700,7 @@ export function emitStringExpr(
|
||||
// the code unit, as the equality above does.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr(charAtUnitRead(unit.charAt));
|
||||
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
@@ -886,9 +904,46 @@ export function emitContainerExpr(
|
||||
// the plain elements up to the next one (scr_arr_reserve_spread).
|
||||
const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i));
|
||||
const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread));
|
||||
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
// An int32 array's allocation (ir/int32-arrays.ts) constructs
|
||||
// SCR_ELEM_I32 storage; its elements are proven int32s. The empty
|
||||
// temporary of a fill that allocates an int32 array with checked
|
||||
// stores takes SCR_ELEM_I32 storage when the fill's length (which
|
||||
// evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and
|
||||
// SCR_ELEM_F64 storage otherwise.
|
||||
const fillLength = host.checkedAllocations.get(e);
|
||||
if (fillLength !== undefined && e.elems.length === 0) {
|
||||
const length = host.emitExpr(fillLength);
|
||||
const long = B.tmp(),
|
||||
kind = B.tmp();
|
||||
B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`);
|
||||
B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`);
|
||||
host.declare(
|
||||
small
|
||||
? `declare ptr @scr_arr_new_small(i32)`
|
||||
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
);
|
||||
B.line(
|
||||
small
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
return host.own({ name: arr, type: e.type });
|
||||
}
|
||||
const int32 = host.int32Allocations.has(e);
|
||||
if (int32) {
|
||||
host.declare(
|
||||
small
|
||||
? `declare ptr @scr_arr_new_small(i32)`
|
||||
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
);
|
||||
B.line(
|
||||
small
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc = elemAccess(elem);
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
let buffer: string | undefined;
|
||||
const flush = (): void => {
|
||||
@@ -909,7 +964,8 @@ export function emitContainerExpr(
|
||||
B.line(
|
||||
`call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`,
|
||||
);
|
||||
host.emitArrayCopyLoop(arr, v.name, acc);
|
||||
// A literal with spreads is never an int32 allocation.
|
||||
host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc));
|
||||
return;
|
||||
}
|
||||
pending.push(host.emitExpr(el));
|
||||
@@ -954,33 +1010,65 @@ export function emitContainerExpr(
|
||||
// Ref-element reads return +1 (the inline dense path retains like
|
||||
// the runtime getter); own registers the owned temp in the frame.
|
||||
const elem = e.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const t = emitDenseArrayGet(
|
||||
if (host.checkedInt32Array(e.arr)) {
|
||||
const proven = host.provenDenseIndex(e.arr, e.index);
|
||||
const value = host.emitByElementKind(
|
||||
arr.name,
|
||||
"double",
|
||||
(kind) =>
|
||||
emitDenseArrayRead(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
kind,
|
||||
elem,
|
||||
"scr_arr_get_f64",
|
||||
true,
|
||||
proven,
|
||||
).value,
|
||||
)!;
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
const acc = host.arrayAccess(e.arr, elem);
|
||||
const read = emitDenseArrayRead(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
acc,
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
`scr_arr_get_${runtimeAccess(acc)}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
if (read.int32 !== null)
|
||||
// An int32 array's element: its i32 is exact for index consumers.
|
||||
return {
|
||||
name: read.value,
|
||||
type: e.type,
|
||||
uint32: read.int32,
|
||||
integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE },
|
||||
};
|
||||
return host.own({ name: read.value, type: e.type });
|
||||
}
|
||||
case "arrayHas": {
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array");
|
||||
const t = emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"has",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "i1", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"has",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "arrayState": {
|
||||
@@ -988,14 +1076,18 @@ export function emitContainerExpr(
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
|
||||
const t = emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"state",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "double", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"state",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "arrIntrinsic":
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js";
|
||||
|
||||
/** Int32 kind loops.
|
||||
*
|
||||
* An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots
|
||||
* (or doubles, when its fill was short) until a store of a value that is
|
||||
* not an int32 widens it to doubles in place, so an access to it tests its
|
||||
* element kind first (LlEmitter.emitByElementKind). An innermost loop that
|
||||
* accesses such arrays tests their kinds once on entry instead
|
||||
* (emitLoopCopies). The fast copy runs when every one of them holds int32
|
||||
* slots and reads and writes them as int32 arrays; the wide copy runs when
|
||||
* every one holds doubles and accesses them as double arrays; a loop over
|
||||
* several arrays whose kinds differ runs the mixed copy, which tests the
|
||||
* kinds at each access. A store of the fast copy whose value is not an
|
||||
* int32 branches to the same statement in a third copy, which re-executes
|
||||
* it with kind tests (widening the array) and finishes the iteration, then
|
||||
* enters the mixed copy, or the wide copy when the loop has one array.
|
||||
* Re-execution is exact because such a store's statement evaluates nothing
|
||||
* but locals, literals, number arithmetic and element reads before it
|
||||
* writes.
|
||||
*
|
||||
* Nothing else can widen these arrays: they never reach code other than
|
||||
* the compiled element operations of their own function
|
||||
* (ir/int32-arrays.ts). */
|
||||
export interface Int32KindLoopPlan {
|
||||
/** The arrays the fast copy reads and writes as int32 arrays, by local,
|
||||
* with one of their references in the loop. */
|
||||
arrays: Map<string, IrExpr>;
|
||||
/** Element writes and pushes whose value may not be an int32: the
|
||||
* statements the fast copy may leave at. */
|
||||
writes: Set<IrStmt>;
|
||||
}
|
||||
|
||||
type KindLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
|
||||
|
||||
/** A number or boolean expression whose evaluation has no effect: locals,
|
||||
* literals, arithmetic, comparisons and element reads of local arrays. */
|
||||
function isPure(e: IrExpr): boolean {
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
case "varRef":
|
||||
return true;
|
||||
case "bin":
|
||||
return (
|
||||
(e.left.type.kind === "f64" || e.left.type.kind === "bool") &&
|
||||
isPure(e.left) &&
|
||||
isPure(e.right)
|
||||
);
|
||||
case "unary":
|
||||
return isPure(e.operand);
|
||||
case "ternary":
|
||||
return isPure(e.cond) && isPure(e.then) && isPure(e.else_);
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
return e.arr.kind === "varRef" && isPure(e.index);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The plan for `loop`, or null. `checked` names the int32 array locals
|
||||
* with checked stores of the function; `provenInt32` the values the static
|
||||
* range analysis proves int32s (stores that never leave the fast copy). */
|
||||
export function planInt32KindLoop(
|
||||
loop: KindLoop,
|
||||
checked: (localId: string) => boolean,
|
||||
provenInt32: (e: IrExpr) => boolean,
|
||||
): Int32KindLoopPlan | null {
|
||||
const arrays = new Map<string, IrExpr>();
|
||||
const writes = new Set<IrStmt>();
|
||||
const assigned = new Set<string>();
|
||||
let nodes = 0;
|
||||
let rejected = false;
|
||||
const reject = (): false => {
|
||||
rejected = true;
|
||||
return false;
|
||||
};
|
||||
const array = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && checked(e.localId) ? e.localId : null;
|
||||
const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null =>
|
||||
e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null;
|
||||
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId))
|
||||
arrays.set(e.localId, e);
|
||||
// A push the statement walk did not take as a write: its values must
|
||||
// be proven int32s, since it cannot leave the fast copy.
|
||||
const pushed = push(e);
|
||||
if (pushed && !pushed.args.every(provenInt32)) return reject();
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
// Statements inside expressions run mid-expression, where the fast copy
|
||||
// cannot leave: they may not write these arrays.
|
||||
const nested = (s: IrStmt): boolean => {
|
||||
nodes++;
|
||||
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
|
||||
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject();
|
||||
return everyStmtChild(s, expr, nested);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (!SIMPLE_STATEMENTS.has(s.kind)) return reject();
|
||||
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
|
||||
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) {
|
||||
if (!isPure(s.index) || !isPure(s.value)) return reject();
|
||||
writes.add(s);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
}
|
||||
const pushed = s.kind === "exprStmt" ? push(s.expr) : null;
|
||||
if (pushed && !pushed.args.every(provenInt32)) {
|
||||
if (!pushed.args.every(isPure)) return reject();
|
||||
writes.add(s);
|
||||
expr(pushed.receiver);
|
||||
pushed.args.forEach(expr);
|
||||
return !rejected;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
|
||||
if (loop.cond) expr(loop.cond);
|
||||
if (loop.kind === "for" && loop.update) stmt(loop.update);
|
||||
for (const s of loop.body) stmt(s);
|
||||
if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null;
|
||||
// A loop that rebinds one of the arrays could reach another array of
|
||||
// either kind.
|
||||
for (const id of arrays.keys()) if (assigned.has(id)) return null;
|
||||
return { arrays, writes };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js";
|
||||
|
||||
export type LlInteger = NonNullable<LlValue["integer"]>;
|
||||
|
||||
@@ -51,6 +51,89 @@ export function exactInteger(
|
||||
return { name, type: "i64", signed: true, range };
|
||||
}
|
||||
|
||||
/** Doubles hold every integer of smaller magnitude exactly. */
|
||||
const EXACT_DOUBLES = 2 ** 53;
|
||||
/** Wide views stay within this magnitude: a product of two int32s reaches
|
||||
* 2^62, one exact term of at most 2^53 may be added to it, and no i64
|
||||
* result wraps. */
|
||||
const WIDE_BOUND = 2 ** 62 + 2 ** 53;
|
||||
/** Array and string indexes are below 2^32 - 1: a wide view exact below
|
||||
* this many is exact at every index a length can admit. */
|
||||
const INDEX_LIMIT = 2 ** 32;
|
||||
|
||||
function magnitude(range: IntegerRange): number {
|
||||
return Math.max(Math.abs(range.min), Math.abs(range.max));
|
||||
}
|
||||
|
||||
/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands
|
||||
* have no proven result range (their product or sum may pass 2^53).
|
||||
*
|
||||
* - A product of two exact integers equals its double whenever its
|
||||
* magnitude is below 2^53: the double product of integers is exact there.
|
||||
* - A sum or difference of two exact integers likewise.
|
||||
* - A sum or difference of a wide view (exact below T) and an exact integer
|
||||
* of magnitude at most B equals its double below min(T - B, 2^53): the
|
||||
* view's magnitude is then below T, so its double is exact, and the
|
||||
* double sum of two exact integers below 2^53 is exact.
|
||||
*
|
||||
* Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view
|
||||
* exact below fewer than 2^32 helps no index and is not kept. */
|
||||
export function wideArithmetic(
|
||||
host: LlvmEmitterContext,
|
||||
op: string,
|
||||
l: LlValue,
|
||||
li: LlInteger | null,
|
||||
r: LlValue,
|
||||
ri: LlInteger | null,
|
||||
): WideInteger | null {
|
||||
if (op !== "+" && op !== "-" && op !== "*") return null;
|
||||
const view = (value: LlValue, integer: LlInteger | null): WideInteger | null =>
|
||||
integer
|
||||
? { name: "", below: Infinity, bound: magnitude(integer.range) }
|
||||
: (value.wideInteger ?? null);
|
||||
const lv = view(l, li),
|
||||
rv = view(r, ri);
|
||||
if (!lv || !rv) return null;
|
||||
let below: number, bound: number;
|
||||
if (op === "*") {
|
||||
if (lv.below !== Infinity || rv.below !== Infinity) return null;
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound * rv.bound;
|
||||
} else if (lv.below === Infinity && rv.below === Infinity) {
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound + rv.bound;
|
||||
} else if (lv.below === Infinity || rv.below === Infinity) {
|
||||
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
|
||||
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
|
||||
bound = exact.bound + wide.bound;
|
||||
} else return null;
|
||||
if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
|
||||
const operand = (value: LlValue, integer: LlInteger | null): string =>
|
||||
integer ? widenInteger(host, integer) : value.wideInteger!.name;
|
||||
const left = operand(l, li),
|
||||
right = operand(r, ri);
|
||||
const name = host.B.tmp();
|
||||
host.B.line(
|
||||
`${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
|
||||
);
|
||||
return { name, below, bound };
|
||||
}
|
||||
|
||||
/** An i64 index for a consumer that compares it, unsigned, with a length or
|
||||
* capacity below 2^32 and takes the double's path otherwise: the exact
|
||||
* integer of `value`, or its wide view, which equals the double at every
|
||||
* such index. Null when the index needs the double round trip. */
|
||||
export function indexInteger(
|
||||
host: LlvmEmitterContext,
|
||||
value: LlValue,
|
||||
expr?: IrExpr,
|
||||
): string | null {
|
||||
const integer = exactInteger(host, value, expr);
|
||||
if (integer) return widenInteger(host, integer);
|
||||
const wide = value.wideInteger;
|
||||
return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
|
||||
}
|
||||
|
||||
export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string {
|
||||
if (value.type === "i64") return value.name;
|
||||
const wide = host.B.tmp();
|
||||
|
||||
@@ -862,6 +862,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return host.emitGenericLibCall(e);
|
||||
case "weakMap":
|
||||
case "weakSet":
|
||||
case "finalizationRegistry":
|
||||
case "ffi":
|
||||
case "sharedArrayBuffer":
|
||||
case "arrayBuffer":
|
||||
|
||||
@@ -44,6 +44,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
"weakSet.new": "scr_weak_set_new",
|
||||
"finalizationRegistry.new": "scr_finalization_registry_new",
|
||||
"finalizationRegistry.is": "scr_finalization_registry_is",
|
||||
"dyn.fromEntries": "scr_dyn_from_entries",
|
||||
"bytes.constructor": "scr_bytes_constructor",
|
||||
"bytes.construct": "scr_bytes_construct",
|
||||
@@ -1598,6 +1600,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
"array.namedKeys": "scr_arr_named_keys",
|
||||
"array.hasNamedKey": "scr_arr_has_named_key",
|
||||
"array.hasProperties": "scr_arr_has_properties",
|
||||
"array.undefinedKey": "scr_arr_undefined_key",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
|
||||
@@ -13,7 +13,7 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { indexInteger } from "./integer-values.js";
|
||||
import { READ_FALLBACK } from "./dense-array-access.js";
|
||||
import { compactRefLoadLines } from "./classes.js";
|
||||
|
||||
@@ -476,7 +476,8 @@ function emitDenseReferenceArrayRead(
|
||||
no: string,
|
||||
slow: string,
|
||||
join: string,
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" = "ptr",
|
||||
/** `i32`: an int32 array's slot, presented as the int32. */
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr",
|
||||
): void {
|
||||
const B = host.B;
|
||||
const header = (field: number): string => {
|
||||
@@ -567,7 +568,7 @@ function emitDenseReferenceArrayRead(
|
||||
statePtr,
|
||||
data,
|
||||
cap,
|
||||
loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8,
|
||||
loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8,
|
||||
offset,
|
||||
);
|
||||
host.markMemoryPointer(statePtr, "array:present");
|
||||
@@ -578,9 +579,10 @@ function emitDenseReferenceArrayRead(
|
||||
B.startBlock(valueLabel);
|
||||
const valuePtr = B.tmp(),
|
||||
raw = B.tmp();
|
||||
// Boolean arrays store one byte per element, compact references four,
|
||||
// every other kind an 8-byte slot.
|
||||
const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64";
|
||||
// Boolean arrays store one byte per element, compact references and int32
|
||||
// arrays four, every other kind an 8-byte slot.
|
||||
const slotType =
|
||||
loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64";
|
||||
B.line(
|
||||
`${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`,
|
||||
);
|
||||
@@ -638,11 +640,11 @@ export function emitBorrowedArrayRead(
|
||||
return;
|
||||
}
|
||||
const index = host.emitExpr(read.index);
|
||||
// A proven integer index (an int32 local, say) needs no double
|
||||
// round-trip; a negative one compares above the capacity as unsigned.
|
||||
const exact =
|
||||
loopIndex === null && host.sizeType === "i64" ? exactInteger(host, index, read.index) : null;
|
||||
const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
|
||||
// A proven integer index (an int32 local, say), or a wide view exact at
|
||||
// every index below the capacity, needs no double round-trip; a negative
|
||||
// one compares above the capacity as unsigned.
|
||||
const integerIndex =
|
||||
loopIndex ?? (host.sizeType === "i64" ? indexInteger(host, index, read.index) : null);
|
||||
const slow = B.newLabel("borrow.array.slow"),
|
||||
join = B.newLabel("borrow.array.join");
|
||||
emitDenseReferenceArrayRead(
|
||||
@@ -680,13 +682,10 @@ export function emitLocalArrayRead(
|
||||
const array = host.emitStableReceiver(read.array, [read.index]);
|
||||
const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null;
|
||||
const index = host.emitExpr(read.index);
|
||||
// A proven integer index (a counter with a known range, say) needs no
|
||||
// double round trip, as in a borrowed read.
|
||||
const exact =
|
||||
inline && loopIndex === null && host.sizeType === "i64"
|
||||
? exactInteger(host, index, read.index)
|
||||
: null;
|
||||
const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
|
||||
// As in emitBorrowedArrayRead: an exact integer or wide view needs no
|
||||
// round trip, and only the loop's own counter is an induction variable.
|
||||
const integerIndex =
|
||||
loopIndex ?? (inline && host.sizeType === "i64" ? indexInteger(host, index, read.index) : null);
|
||||
const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null;
|
||||
if (nullable) {
|
||||
// A nullable union is the element pointer itself: borrowed from an
|
||||
@@ -743,8 +742,15 @@ export function emitLocalArrayRead(
|
||||
const no = B.newLabel("local.array.missing"),
|
||||
join = B.newLabel("local.array.join");
|
||||
const scalar = isScalarElement(read.element);
|
||||
// An int32 array's arms join on the int32 element, converted to the
|
||||
// payload's double once at the join, so a consumer converting the payload
|
||||
// back to an integer folds to the element.
|
||||
const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array);
|
||||
const element = int32 ? B.slot() : null;
|
||||
if (element) B.entryAllocas.push(`${element} = alloca i32`);
|
||||
const storeValue = (value: string): void => {
|
||||
if (scalar) {
|
||||
if (element) B.line(`store i32 ${value}, ptr ${element}`);
|
||||
else if (scalar) {
|
||||
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
|
||||
let bits = value;
|
||||
if (read.element.kind === "bool") {
|
||||
@@ -761,7 +767,37 @@ export function emitLocalArrayRead(
|
||||
}
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
};
|
||||
if (inline)
|
||||
if (inline && scalar && host.checkedInt32Array(read.array))
|
||||
// An int32 array with checked stores: its int32 arm stores the element's
|
||||
// double bits, as the double arm does.
|
||||
host.emitByElementKind(array.name, null, (kind) => {
|
||||
const present =
|
||||
kind === "i32"
|
||||
? (value: string): void => {
|
||||
const number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${value} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
storeValue(bits);
|
||||
}
|
||||
: storeValue;
|
||||
const loadType = kind === "i32" ? "i32" : "i64";
|
||||
const induction = loopIndex !== null;
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
integerIndex,
|
||||
induction,
|
||||
present,
|
||||
no,
|
||||
slow,
|
||||
join,
|
||||
loadType,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
else if (inline)
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
@@ -772,7 +808,13 @@ export function emitLocalArrayRead(
|
||||
no,
|
||||
slow,
|
||||
join,
|
||||
!scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64",
|
||||
!scalar
|
||||
? referenceLoad(host, read.array.type)
|
||||
: read.element.kind === "bool"
|
||||
? "i8"
|
||||
: int32
|
||||
? "i32"
|
||||
: "i64",
|
||||
);
|
||||
else B.br(slow);
|
||||
B.startBlock(slow);
|
||||
@@ -799,7 +841,8 @@ export function emitLocalArrayRead(
|
||||
B.line(
|
||||
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
|
||||
);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
if (element) B.line(`${bits} = fptosi double ${value} to i32`);
|
||||
else B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
|
||||
B.line(
|
||||
@@ -807,7 +850,8 @@ export function emitLocalArrayRead(
|
||||
);
|
||||
B.line(`${bits} = zext i1 ${value} to i64`);
|
||||
}
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 ${bits}, ptr ${element}`);
|
||||
else B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
} else {
|
||||
const value = B.tmp(),
|
||||
@@ -821,10 +865,20 @@ export function emitLocalArrayRead(
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 0, ptr ${element}`);
|
||||
else B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.missingTag}, ptr ${tag}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
if (element) {
|
||||
const value = B.tmp(),
|
||||
number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${value} = load i32, ptr ${element}`);
|
||||
B.line(`${number} = sitofp i32 ${value} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
}
|
||||
B.line(`store ptr ${box}, ptr ${localSlot}`);
|
||||
return read.borrow || scalar ? null : { slot: payload, type: read.element };
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"regex.resultProp",
|
||||
"array.namedKeys",
|
||||
"array.hasNamedKey",
|
||||
"array.hasProperties",
|
||||
"process.cwd",
|
||||
"process.argv",
|
||||
"process.execArgv",
|
||||
|
||||
@@ -35,12 +35,12 @@ export interface SpeculativeLoopPlan {
|
||||
type SpeculativeLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
const MAX_LOOP_NODES = 400;
|
||||
export const MAX_LOOP_NODES = 400;
|
||||
|
||||
/** Statements whose emission is plain control flow or a single write.
|
||||
* Nested loops, switches and try statements keep the loop on the ordinary
|
||||
* path (their emission keeps state that the two copies would not share). */
|
||||
const SIMPLE_STATEMENTS = new Set<string>([
|
||||
export const SIMPLE_STATEMENTS: ReadonlySet<string> = new Set<string>([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
|
||||
@@ -285,9 +285,10 @@ class ContractChecker {
|
||||
* the lifetimes of the objects they receive). Publication to other
|
||||
* threads needs no check: an instance carries a reference count of its
|
||||
* own until publication marks it like any other object it reaches
|
||||
* (classes.ts programLifetimeRc). Static code has no WeakRef or
|
||||
* FinalizationRegistry; weak collections stay correct because an entry
|
||||
* keyed by an object that is never freed is never observable as removed. */
|
||||
* (classes.ts programLifetimeRc). Static code has no WeakRef, and a
|
||||
* FinalizationRegistry never runs its cleanup callbacks; weak collections
|
||||
* stay correct because an entry keyed by an object that is never freed is
|
||||
* never observable as removed. */
|
||||
private checkProgramLifetime(): void {
|
||||
if (this.notes.classes.size === 0) return;
|
||||
const roots: string[] = [];
|
||||
|
||||
@@ -122,7 +122,8 @@ const COPIES = new Set([
|
||||
const COPY_IN = new Set(["pushSpread", "unshiftSpread", "spliceInsert"]);
|
||||
|
||||
/** Library calls that only read array arguments. */
|
||||
const READ_ONLY_LIB = /^(console\.|inspect\.|json\.stringify|math\.|insp\.)/;
|
||||
const READ_ONLY_LIB =
|
||||
/^(console\.|inspect\.|json\.stringify|math\.|insp\.|array\.(hasProperties|namedKeys|hasNamedKey)$)/;
|
||||
|
||||
/** Library calls that always throw (the value they stand for never exists). */
|
||||
const THROWING_LIB = /^error\.\w*Throw$/;
|
||||
@@ -872,6 +873,9 @@ export class Int32ElementContracts {
|
||||
};
|
||||
const isNumberArray = (t: IrType): boolean => t.kind === "array" && t.elem.kind === "f64";
|
||||
|
||||
// Live views handed straight to a read-only library call (util.inspect
|
||||
// of an array with properties) are only read through.
|
||||
const readOnlyViews = new Set<IrExpr>();
|
||||
const expr = (e: IrExpr): void => {
|
||||
switch (e.kind) {
|
||||
case "assignExpr":
|
||||
@@ -1000,6 +1004,7 @@ export class Int32ElementContracts {
|
||||
case "libCall":
|
||||
if (!READ_ONLY_LIB.test(e.fn))
|
||||
for (const a of e.args) escape(a, a.loc, `the library call ${e.fn}`);
|
||||
else for (const a of e.args) if (a.kind === "dynFrom") readOnlyViews.add(a);
|
||||
break;
|
||||
case "callValue":
|
||||
for (const a of e.args) escape(a, a.loc, "a call through a function value");
|
||||
@@ -1008,7 +1013,10 @@ export class Int32ElementContracts {
|
||||
case "dynFrom":
|
||||
// Plain arrays and records cross into dynamic code as deep copies;
|
||||
// class instances and live references stay connected.
|
||||
if (e.liveRef === true || this.containsObject(e.value.type, new Set()))
|
||||
if (
|
||||
!readOnlyViews.has(e) &&
|
||||
(e.liveRef === true || this.containsObject(e.value.type, new Set()))
|
||||
)
|
||||
escape(e.value, e.loc, "dynamic (any or unknown) code");
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -60,6 +60,28 @@ test("semantic validation reads non-source dependencies once and still invalidat
|
||||
expect(reads.mock.calls.filter(([path]) => path === dependency)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("an equivalent edit of a file whose raw text the frontend read does not match", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
const previous = "export const answer = 1;\n";
|
||||
await writeFile(source, previous);
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => {
|
||||
trackedReadFile(source);
|
||||
tracker.record({ op: "text", path: source });
|
||||
});
|
||||
const snapshot = tracker.snapshot();
|
||||
expect(validFrontendInputSnapshot(snapshot)).toBe(true);
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
await writeFile(source, `// comment\n${previous}`);
|
||||
const equivalent = vi.fn(() => true);
|
||||
expect(
|
||||
frontendInputsSemanticallyMatch(snapshot, new Map([[source, previous]]), equivalent),
|
||||
).toBeNull();
|
||||
expect(equivalent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("tracked frontend reads invalidate on byte edits", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { observeSourceText } from "./ts7/ast-node.js";
|
||||
|
||||
/**
|
||||
* Filesystem observations made while constructing and lowering one frontend
|
||||
@@ -16,7 +17,10 @@ export type FrontendInputProbe =
|
||||
| { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" }
|
||||
| { op: "entries"; path: string; files: string[]; directories: string[] }
|
||||
| { op: "entries-error"; path: string }
|
||||
| { op: "realpath"; path: string; target: string | null };
|
||||
| { op: "realpath"; path: string; target: string | null }
|
||||
/** The frontend's output depends on the file's raw text or line
|
||||
* positions, not only on its tokens (observeSourceText). */
|
||||
| { op: "text"; path: string };
|
||||
|
||||
export interface FrontendInputSnapshot {
|
||||
version: 1;
|
||||
@@ -105,6 +109,7 @@ export class FrontendInputTracker {
|
||||
|
||||
record(probe: FrontendInputProbe): void {
|
||||
const key = `${probe.op}\0${probe.path}`;
|
||||
if (probe.op === "text" && this.probes.has(key)) return;
|
||||
const previous = this.probes.get(key);
|
||||
if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) {
|
||||
// An input changed while this frontend was running. The build may
|
||||
@@ -158,6 +163,13 @@ function record(probe: FrontendInputProbe): void {
|
||||
(synchronousTracker ?? activeTracker.getStore())?.record(probe);
|
||||
}
|
||||
|
||||
// Source text the frontend reads raw (call-site text in messages, comptime
|
||||
// callbacks, line numbers) changes with edits that keep the tokens.
|
||||
observeSourceText((fileName) => {
|
||||
const tracker = synchronousTracker ?? activeTracker.getStore();
|
||||
if (tracker !== undefined) tracker.record({ op: "text", path: resolve(fileName) });
|
||||
});
|
||||
|
||||
/** Mark the active frontend as unsafe to persist because an exact host query
|
||||
* bypassed the tracked filesystem wrappers. No-op outside a frontend run. */
|
||||
export function markFrontendInputsUnstable(): void {
|
||||
@@ -394,6 +406,9 @@ export function frontendInputsStillMatch(
|
||||
return probe.target === null;
|
||||
}
|
||||
}
|
||||
case "text":
|
||||
// The file probe of the same path checks its content.
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -410,36 +425,44 @@ export function frontendInputsSemanticallyMatch(
|
||||
exclusions: FrontendInputExclusions = {},
|
||||
): FrontendSemanticMatch | null {
|
||||
if (!validFrontendInputSnapshot(snapshot)) return null;
|
||||
const sourceProbes = new Map(
|
||||
snapshot.probes
|
||||
.filter((probe): probe is Extract<FrontendInputProbe, { op: "file" }> => probe.op === "file")
|
||||
.map((probe) => [probe.path, probe]),
|
||||
);
|
||||
const sourceProbes = new Map<string, Extract<FrontendInputProbe, { op: "file" }>>();
|
||||
const rawText = new Set<string>();
|
||||
for (const probe of snapshot.probes) {
|
||||
if (probe.op === "file") sourceProbes.set(probe.path, probe);
|
||||
else if (probe.op === "text") rawText.add(probe.path);
|
||||
}
|
||||
for (const [path, previous] of previousSources) {
|
||||
if (sourceProbes.get(path)?.digest !== frontendSourceDigest(previous)) return null;
|
||||
}
|
||||
const changed: FrontendSemanticChange[] = [];
|
||||
const currentSources = new Map<string, string>();
|
||||
const adjusted: FrontendInputSnapshot = {
|
||||
...snapshot,
|
||||
probes: snapshot.probes.map((probe) => {
|
||||
if (probe.op !== "file") return probe;
|
||||
const previous = previousSources.get(probe.path);
|
||||
if (previous === undefined || frontendSourceDigest(previous) !== probe.digest) return probe;
|
||||
let current: string;
|
||||
try {
|
||||
current = readFileSync(probe.path, "utf8");
|
||||
} catch {
|
||||
return probe;
|
||||
}
|
||||
const currentDigest = frontendSourceDigest(current);
|
||||
currentSources.set(probe.path, current);
|
||||
if (currentDigest === probe.digest) return probe;
|
||||
if (!isSemanticEquivalent(probe.path, previous, current)) return probe;
|
||||
changed.push({ path: probe.path, previous, current });
|
||||
return { ...probe, digest: currentDigest };
|
||||
}),
|
||||
};
|
||||
const probes: FrontendInputProbe[] = [];
|
||||
for (const probe of snapshot.probes) {
|
||||
const previous = probe.op === "file" ? previousSources.get(probe.path) : undefined;
|
||||
if (probe.op !== "file" || previous === undefined) {
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
let current: string;
|
||||
try {
|
||||
current = readFileSync(probe.path, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const currentDigest = frontendSourceDigest(current);
|
||||
currentSources.set(probe.path, current);
|
||||
if (currentDigest === probe.digest) {
|
||||
probes.push(probe);
|
||||
continue;
|
||||
}
|
||||
// One edit that is not equivalent already rules out the match, and so
|
||||
// does any edit of a file whose raw text the frontend read.
|
||||
if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current))
|
||||
return null;
|
||||
changed.push({ path: probe.path, previous, current });
|
||||
probes.push({ ...probe, digest: currentDigest });
|
||||
}
|
||||
const adjusted: FrontendInputSnapshot = { ...snapshot, probes };
|
||||
if (!frontendInputsStillMatch(adjusted, exclusions)) return null;
|
||||
for (const [path, previous] of previousSources) {
|
||||
if (!currentSources.has(path)) {
|
||||
@@ -486,6 +509,8 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front
|
||||
return true;
|
||||
case "realpath":
|
||||
return value.target === null || typeof value.target === "string";
|
||||
case "text":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { lowerCheckedPredicateValue } from "../lower-builtin-values.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
@@ -104,6 +104,13 @@ export function lowerNumberStaticCall(
|
||||
: { kind: "numLit", value: 0, type: F64, loc };
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc };
|
||||
}
|
||||
// JavaScript whose argument does not lower on its own (zod's
|
||||
// `Number.parseInt(val.toFixed(n).replace(".", ""))` over an untyped
|
||||
// parameter): the global parser's own JavaScript forms.
|
||||
if (probed === null && member === "parseInt" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
const argText =
|
||||
probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { lowerOptionalIndex } from "../undefined-index.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { completeFuncValueArgs } from "../call-arguments.js";
|
||||
import { lowerOptionalNumber } from "../lower-exprs.js";
|
||||
import { lowerSafeIndexRead } from "../array-values.js";
|
||||
|
||||
/** Retain native function slots during indexed calls. The array remains
|
||||
@@ -37,7 +37,7 @@ export function lowerArrayCallbackCall(
|
||||
return varRef(local.id, local.type, loc);
|
||||
};
|
||||
const receiver = store(array, "%callbackArray");
|
||||
const index = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.argumentExpression),
|
||||
locOf(access.argumentExpression),
|
||||
|
||||
@@ -464,8 +464,11 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null
|
||||
cur = cur.expression;
|
||||
return cur;
|
||||
};
|
||||
// The text appears only in warnings. A build that reported warnings is
|
||||
// not reused after a source edit, so the read is not recorded as a source
|
||||
// text dependency (ts.getTextOfNode would record it).
|
||||
const textOf = (node: ts.Node): string => {
|
||||
const text = ts.getTextOfNode(node);
|
||||
const text = ts.getSourceFileOfNode(node).text.slice(ts.tokenStartOfNode(node), node.end);
|
||||
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
||||
};
|
||||
const visit = (node: ts.Node): "skip" | "stop" | undefined => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
|
||||
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
|
||||
import {
|
||||
@@ -1160,12 +1165,26 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
}
|
||||
if (recv?.type.kind === "array" || recv?.type.kind === "bytes") {
|
||||
const receiver = lowerer.declareHiddenLocal("%setArray", recv.type);
|
||||
const index = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(expr.left.argumentExpression),
|
||||
locOf(expr.left.argumentExpression),
|
||||
expr.left.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property
|
||||
// (undefined-index.ts), which a typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(expr.left.argumentExpression);
|
||||
const split =
|
||||
recv.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
expr.left.argumentExpression,
|
||||
locOf(expr.left.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
locOf(expr.left.argumentExpression),
|
||||
expr.left.argumentExpression,
|
||||
);
|
||||
if (index.type.kind !== "f64")
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1178,7 +1197,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
const result = varRef(value.id, raw.type, loc);
|
||||
const arr = varRef(receiver.id, recv.type, loc);
|
||||
const idx = varRef(key.id, F64, loc);
|
||||
const write: IrStmt =
|
||||
const store: IrStmt =
|
||||
recv.type.kind === "array"
|
||||
? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc)
|
||||
: {
|
||||
@@ -1188,10 +1207,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
value: lowerer.coerceInto(expr.right, result, F64),
|
||||
loc,
|
||||
};
|
||||
const write: IrStmt = split
|
||||
? {
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, expr.left, loc)],
|
||||
else_: [store],
|
||||
loc,
|
||||
}
|
||||
: store;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
|
||||
...(split?.stmts ?? []),
|
||||
{ kind: "varDecl", localId: key.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: value.id, init: raw, loc },
|
||||
write,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { type Lowerer, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -65,6 +70,7 @@ import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
|
||||
import { isClassCallback } from "../class-callbacks.js";
|
||||
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
|
||||
import { lowerFieldCompoundValue } from "./updates.js";
|
||||
import { readsHeldEvolvingArray } from "../lower-stmts.js";
|
||||
|
||||
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
||||
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
||||
@@ -751,12 +757,16 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
: lowerer.maybeNarrow(arr, expr.expression);
|
||||
}
|
||||
|
||||
// An alias or container of an evolving array whose reads disagree
|
||||
// (`rows[0]` after `const rows = [xs]`) holds elements the checker's type
|
||||
// does not list, so its element read stays checked-dynamic.
|
||||
const heldEvolving = arr.type.kind === "dyn" && readsHeldEvolvingArray(lowerer, expr.expression);
|
||||
// The checker sees an array but the VALUE lowered checked-dynamic (a
|
||||
// jsdoc-typed default export from a .js module — signature 14): the
|
||||
// read rides the dynCheck boundary when the array shape is one the checked-dynamic tree
|
||||
// can validate, and fences by name when it is not — an arrayGet over a
|
||||
// dyn receiver is never emitted (the validator ICE).
|
||||
if (arr.type.kind === "dyn") {
|
||||
if (arr.type.kind === "dyn" && !heldEvolving) {
|
||||
if (
|
||||
canDynCheckTo(
|
||||
receiverIr,
|
||||
@@ -774,14 +784,22 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
}
|
||||
}
|
||||
// Array reads can themselves supply an index. A missing numeric value
|
||||
// is undefined, whose property lookup misses just like NaN; keep that
|
||||
// value instead of rejecting a checker-number index with optional IR.
|
||||
const index = lowerOptionalNumber(
|
||||
// is undefined, which names the array's "undefined" property
|
||||
// (undefined-index.ts).
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(expr.argumentExpression),
|
||||
locOf(expr.argumentExpression),
|
||||
expr.argumentExpression,
|
||||
);
|
||||
if (heldEvolving)
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: arr,
|
||||
key: lowerer.coerceToExpected(index, DYN),
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
};
|
||||
if (
|
||||
arr.type.kind === "array" &&
|
||||
(index.type.kind === "dyn" ||
|
||||
@@ -1518,12 +1536,26 @@ export function lowerElementCompoundParts(
|
||||
"compound assignment through a non-native array or byte view",
|
||||
);
|
||||
}
|
||||
const index = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property (undefined-index.ts),
|
||||
// which a typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(target.argumentExpression);
|
||||
const split =
|
||||
receiver.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
target.argumentExpression,
|
||||
locOf(target.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
locOf(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
if (index.type.kind !== "f64")
|
||||
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type);
|
||||
@@ -1632,15 +1664,25 @@ export function lowerElementCompoundParts(
|
||||
}
|
||||
const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type);
|
||||
const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc);
|
||||
const write: IrStmt =
|
||||
const store: IrStmt =
|
||||
receiver.type.kind === "array"
|
||||
? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc)
|
||||
: { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc };
|
||||
const write: IrStmt = split
|
||||
? {
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
|
||||
else_: [store],
|
||||
loc,
|
||||
}
|
||||
: store;
|
||||
return {
|
||||
expr: {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
|
||||
...(split?.stmts ?? []),
|
||||
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: oldLocal.id, init: oldValue, loc },
|
||||
{ kind: "varDecl", localId: resultLocal.id, init: computed, loc },
|
||||
@@ -1867,13 +1909,23 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
// The checker's flow type is a typed array, but the storage is wider.
|
||||
if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv);
|
||||
// An index the checker types as a number but whose storage may hold
|
||||
// undefined (a hole read) converts with ToNumber, as the reads do.
|
||||
const index = lowerOptionalNumber(
|
||||
// undefined (a hole read) converts with ToNumber, as the reads do; an
|
||||
// undefined index names a property the typed array cannot hold.
|
||||
const rawIndex = lowerer.lowerExpr(target.argumentExpression);
|
||||
const split = splitUndefinedIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
rawIndex,
|
||||
target.argumentExpression,
|
||||
locOf(target.argumentExpression),
|
||||
);
|
||||
const index =
|
||||
split?.index ??
|
||||
lowerOptionalNumber(
|
||||
lowerer,
|
||||
rawIndex,
|
||||
locOf(target.argumentExpression),
|
||||
target.argumentExpression,
|
||||
);
|
||||
if (index.type.kind === "dyn" && recv.type.kind === "bytes") {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
@@ -1889,7 +1941,34 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
||||
}
|
||||
const value = lowerer.lowerExprExpecting(expr.right, F64);
|
||||
return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
|
||||
if (split === null) return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.declareHiddenLocal("%indexedBytes", recv.type);
|
||||
const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
|
||||
...split.stmts,
|
||||
{ kind: "varDecl", localId: valueLocal.id, init: value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
|
||||
else_: [
|
||||
{
|
||||
kind: "bytesSet",
|
||||
arr: varRef(receiver.id, recv.type, loc),
|
||||
index,
|
||||
value: varRef(valueLocal.id, F64, loc),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Tuple element write `t[0] = v`: a positional-field write (recordSet)
|
||||
// — literal indices only, like the read.
|
||||
@@ -2184,7 +2263,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
"assignment through a possibly missing nested array receiver",
|
||||
);
|
||||
}
|
||||
const index = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(target.argumentExpression),
|
||||
locOf(target.argumentExpression),
|
||||
|
||||
@@ -801,8 +801,6 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
"ArrayBuffer constructor values have no native lowering; use new ArrayBuffer(length) for fixed-length shared storage",
|
||||
WeakRef:
|
||||
"deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead",
|
||||
FinalizationRegistry:
|
||||
"finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead",
|
||||
eval: "runtime code evaluation cannot be compiled ahead of time",
|
||||
};
|
||||
lowerer.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined);
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { planUnionRetag } from "../union-retag.js";
|
||||
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
|
||||
import { builtinGlobalValue } from "./property-access.js";
|
||||
import { holdsEvolvingArray } from "../lower-stmts.js";
|
||||
|
||||
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
|
||||
* contextual type when tsc has one (annotated declarations, arguments,
|
||||
@@ -413,7 +414,10 @@ export function lowerDynObjectLiteral(
|
||||
? lowerer.lowerLambda(prop, true)
|
||||
: ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop)
|
||||
? shorthandBuiltinValue(lowerer, prop)!
|
||||
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
|
||||
: !boxValue &&
|
||||
(ts.isObjectLiteralExpression(valueExpr) ||
|
||||
(ts.isArrayLiteralExpression(valueExpr) &&
|
||||
holdsEvolvingArray(lowerer, valueExpr)))
|
||||
? lowerer.lowerExprExpecting(valueExpr, DYN)
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
|
||||
|
||||
@@ -4137,6 +4137,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
|
||||
"Promise",
|
||||
"WeakMap",
|
||||
"WeakSet",
|
||||
"FinalizationRegistry",
|
||||
...ERROR_CONSTRUCTOR_VALUES,
|
||||
]);
|
||||
|
||||
|
||||
@@ -148,6 +148,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "FinalizationRegistry")) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return { kind: "libCall", fn: "finalizationRegistry.is", args: [value], type: BOOL, loc };
|
||||
}
|
||||
const rhsBuiltin = ts.isIdentifier(expr.right)
|
||||
? lowerer.builtinImportOf(expr.right)
|
||||
: ts.isPropertyAccessExpression(expr.right)
|
||||
@@ -1500,6 +1504,32 @@ export function lowerInExpression(
|
||||
);
|
||||
if (helper) recv = { kind: "call", callee: helper, args: [recv], type: siteType, loc };
|
||||
}
|
||||
// A builtin class held as a brand record (TextEncoder, TextDecoder: only
|
||||
// `%`-prefixed internal fields): its members are the class's prototype
|
||||
// members, which the standard library declares, so `"encodeInto" in
|
||||
// encoder` answers from the checker's type, plus Object.prototype's keys.
|
||||
// The receiver evaluates for its effects.
|
||||
if (recv.type.kind === "record") {
|
||||
const shape = lowerer.shapes.get(recv.type.shapeId);
|
||||
if (shape && shape.fields.length > 0 && shape.fields.every((f) => f.name.startsWith("%"))) {
|
||||
const declared =
|
||||
lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.right), key) !== undefined;
|
||||
const answer: IrExpr = {
|
||||
kind: "boolLit",
|
||||
value: declared || OBJECT_PROTOTYPE_KEYS.has(key),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (recv.kind === "varRef") return answer;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: recv, loc }],
|
||||
result: answer,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Error-rooted receivers (builtin or user subclass — the isErrnoException
|
||||
// predicate's `err instanceof Error && "code" in err` shape): `code`
|
||||
// answers from the runtime error's code slot (stamped by fs/system/
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
splitUndefinedIndex,
|
||||
undefinedTypedArrayKeyFence,
|
||||
} from "../undefined-index.js";
|
||||
import { isStrictCode } from "../strict-mode.js";
|
||||
import { isPrivateWriteReceiver } from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
@@ -311,12 +316,27 @@ export function lowerIncDec(
|
||||
(raw.type.kind === "array" || raw.type.kind === "bytes")
|
||||
) {
|
||||
const receiver = save(raw, "%incrementArray");
|
||||
const rawIndex = lowerOptionalNumber(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(access.argumentExpression),
|
||||
locOf(access.argumentExpression),
|
||||
access.argumentExpression,
|
||||
);
|
||||
// An undefined index names the "undefined" property
|
||||
// (undefined-index.ts), which a typed array cannot hold.
|
||||
const loweredIndex = lowerer.lowerExpr(access.argumentExpression);
|
||||
const split =
|
||||
raw.type.kind === "bytes"
|
||||
? splitUndefinedIndex(
|
||||
lowerer,
|
||||
loweredIndex,
|
||||
access.argumentExpression,
|
||||
locOf(access.argumentExpression),
|
||||
)
|
||||
: null;
|
||||
if (split) stmts.push(...split.stmts);
|
||||
const rawIndex =
|
||||
split?.index ??
|
||||
lowerOptionalIndex(
|
||||
lowerer,
|
||||
loweredIndex,
|
||||
locOf(access.argumentExpression),
|
||||
access.argumentExpression,
|
||||
);
|
||||
const index = save(
|
||||
rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc }
|
||||
@@ -340,7 +360,7 @@ export function lowerIncDec(
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
};
|
||||
return finish(read, (value) =>
|
||||
const store = (value: IrExpr): IrStmt =>
|
||||
type.kind === "array"
|
||||
? arrayValueStore(
|
||||
lowerer,
|
||||
@@ -356,8 +376,15 @@ export function lowerIncDec(
|
||||
index,
|
||||
value: lowerer.coerceInto(access, value, F64),
|
||||
loc,
|
||||
},
|
||||
);
|
||||
};
|
||||
if (!split) return finish(read, store);
|
||||
return finish(read, (value) => ({
|
||||
kind: "if",
|
||||
cond: split.isUndefined,
|
||||
then: [undefinedTypedArrayKeyFence(lowerer, access, loc)],
|
||||
else_: [store(value)],
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
// A TS member statically typed number on a checked-dynamic receiver (a
|
||||
// class-backed protocol view): the expanded form `o.v = o.v + 1`, its
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { PoisonError, dynFallbackType } from "./lowerer.js";
|
||||
import { functionCanReturnUndefined } from "../function-completion.js";
|
||||
import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
|
||||
import type { FnCtx } from "./function-context.js";
|
||||
|
||||
/** Keep the conservative completion rule shared by return construction
|
||||
@@ -124,6 +125,32 @@ export function producesConstructor(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a return of `decl` itself (not of a nested function) is a bare
|
||||
* read of a JavaScript variable whose writes the checker does not
|
||||
* constrain. A string that is only appended to stays a string. */
|
||||
function returnsOpenJsBinding(lowerer: Lowerer, decl: ts.SignatureDeclaration): boolean {
|
||||
let open = false;
|
||||
const opens = (value: ts.Expression | undefined): boolean => {
|
||||
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!value || !ts.isIdentifier(value)) return false;
|
||||
const binding = lowerer.resolveValueSymbol(value)?.valueDeclaration;
|
||||
return (
|
||||
binding !== undefined &&
|
||||
ts.isVariableDeclaration(binding) &&
|
||||
jsBindingHasOpenWrites(lowerer, binding) &&
|
||||
!jsBindingAppendsOnly(lowerer, binding)
|
||||
);
|
||||
};
|
||||
const scan = (node: ts.Node): void => {
|
||||
if (open || (node !== decl && ts.isFunctionLike(node))) return;
|
||||
if (ts.isReturnStatement(node) && opens(node.expression)) open = true;
|
||||
ts.forEachChild(node, scan);
|
||||
};
|
||||
if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) open = opens(decl.body);
|
||||
else scan(decl);
|
||||
return open;
|
||||
}
|
||||
|
||||
/** Map a declaration's return contract while retaining JavaScript
|
||||
* inference fallbacks. Diagnostics preserve the caller's blame node. */
|
||||
export function declaredReturnType(
|
||||
@@ -212,6 +239,23 @@ export function declaredReturnType(
|
||||
lowerer.unions.get(mappedReturn.unionId)?.arms.some((arm) => arm.kind === "func")
|
||||
)
|
||||
return DYN;
|
||||
// A JS function returning a local that holds whatever its writes store
|
||||
// (jsBindingHasOpenWrites): the inferred result is the initializer's
|
||||
// type, which the returned value need not have.
|
||||
// An async function's promise fulfills with that value.
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!decl.type &&
|
||||
!decl.asteriskToken &&
|
||||
!hasExplicitJsDocReturn(decl) &&
|
||||
mappedReturn !== null &&
|
||||
mappedReturn.kind !== "dyn" &&
|
||||
!(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") &&
|
||||
returnsOpenJsBinding(lowerer, decl)
|
||||
)
|
||||
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
? { kind: "promise", inner: DYN }
|
||||
: DYN;
|
||||
// JS inference assigns Number to arithmetic over any, although the
|
||||
// runtime operands can both be BigInts. Preserve that native result.
|
||||
if (
|
||||
|
||||
@@ -342,10 +342,15 @@ export function genericCallInstance(
|
||||
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
|
||||
if (local) return local;
|
||||
// A result instantiated at `any` (a type argument inferred from an
|
||||
// untyped value, such as an untyped forwarding call) is checked-dynamic.
|
||||
// untyped value, such as an untyped forwarding call) is checked-dynamic,
|
||||
// and so is a JavaScript result whose JSDoc names a type the file never
|
||||
// declares or imports (`@returns {MiddlewareHandler<E>}` in a published
|
||||
// bundle): the checker's error type is documentation it could not
|
||||
// resolve, not a contract.
|
||||
const mappedReturn =
|
||||
lowerer.mapTypeOf(retTs) ??
|
||||
((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl))
|
||||
((retTs.flags & ts.TypeFlags.Any) !== 0 &&
|
||||
(!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || retTs.isErrorType())
|
||||
? DYN
|
||||
: null);
|
||||
if (!mappedReturn) {
|
||||
|
||||
@@ -3872,7 +3872,14 @@ function lowerDynReceiverMethodCall(
|
||||
}
|
||||
// The argument is a radix for bigint and an encoding for Buffer.
|
||||
// Preserve its runtime value so receiver dispatch can choose correctly.
|
||||
if (access.name.text === "toString" && call.arguments.nodes.length <= 1) {
|
||||
// An authored own `toString` member (zod's `errorUtil.toString`, which
|
||||
// answers string | undefined) is an ordinary method call instead.
|
||||
if (
|
||||
access.name.text === "toString" &&
|
||||
call.arguments.nodes.length <= 1 &&
|
||||
!ownCallable &&
|
||||
!(checkerUntyped && jsAssignsOwnToString(lowerer, access.expression))
|
||||
) {
|
||||
const argument = call.arguments.nodes[0];
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -3982,12 +3989,22 @@ function lowerDynReceiverMethodCall(
|
||||
// The same holds when only the MEMBER is checker-untyped: TypeScript
|
||||
// importing chalk's JavaScript source types the default export as a
|
||||
// plain function, so `chalk.bold` reads as `any` on a typed receiver.
|
||||
const ownToString =
|
||||
access.name.text === "toString" &&
|
||||
checkerUntyped &&
|
||||
jsAssignsOwnToString(lowerer, access.expression);
|
||||
const stringOnlyName =
|
||||
(checkerUntyped ||
|
||||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) &&
|
||||
!ownCallable &&
|
||||
DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null;
|
||||
if (
|
||||
DYN_PROTO_METHOD_NAMES.has(access.name.text) &&
|
||||
!ownCallable &&
|
||||
!ownToString &&
|
||||
!stringOnlyName
|
||||
)
|
||||
return null;
|
||||
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
|
||||
// machinery's short-circuit semantics — not modeled here yet.
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
@@ -4414,6 +4431,9 @@ const DYN_DISPATCH_METHODS = new Set([
|
||||
"additionalHeaders",
|
||||
"altsvc",
|
||||
"origin",
|
||||
// FinalizationRegistry's methods (scr_finalization_registry_new).
|
||||
"register",
|
||||
"unregister",
|
||||
// Worker and MessagePort methods use the owning context's handle adapter.
|
||||
"postMessage",
|
||||
"terminate",
|
||||
@@ -6286,7 +6306,21 @@ export function lowerPromiseMethodCall(
|
||||
`then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
// JavaScript whose declared result type has no mapping (zod's
|
||||
// `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`,
|
||||
// typed by a .d.ts union): the handler's value settles the promise as
|
||||
// a checked-dynamic value, checked where it is used.
|
||||
if (
|
||||
resultT?.kind !== "promise" &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
cb.type.kind === "func" &&
|
||||
cb.type.ret.kind !== "promise" &&
|
||||
cb.type.ret.kind !== "void" &&
|
||||
(cb.type.ret.kind === "dyn" || lowerer.dynConvertible(cb.type.ret))
|
||||
) {
|
||||
resultT = { kind: "promise", inner: DYN };
|
||||
}
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"then with this handler's result type",
|
||||
@@ -8658,11 +8692,15 @@ function lowerObjectStaticCall(
|
||||
probed?.type.kind === "union" &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
lowerer.dynConvertible(probed.type);
|
||||
// An array walks its live view: the present indices, then its numeric
|
||||
// and named properties.
|
||||
const isArray = probed?.type.kind === "array" && lowerer.dynConvertible(probed.type);
|
||||
if (
|
||||
isDyn ||
|
||||
isPrimitive ||
|
||||
isFunction ||
|
||||
isJsUnion ||
|
||||
isArray ||
|
||||
(probed && isDynTypedRefType(probed.type))
|
||||
) {
|
||||
const fn =
|
||||
@@ -8672,7 +8710,14 @@ function lowerObjectStaticCall(
|
||||
? "dyn.objValues"
|
||||
: "dyn.objEntries";
|
||||
let v = lowerer.lowerExpr(argNode);
|
||||
if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
|
||||
if (v.type.kind !== "dyn")
|
||||
v = {
|
||||
kind: "dynFrom",
|
||||
value: v,
|
||||
...(isArray ? { liveRef: true as const } : {}),
|
||||
type: DYN,
|
||||
loc: locOf(call),
|
||||
};
|
||||
// In TypeScript the keys come back as a native string[] and the
|
||||
// entries as a native [string, any][]: the checker keeps every later
|
||||
// write to the arrays well typed. JavaScript may store anything into
|
||||
@@ -11057,6 +11102,93 @@ function fenceClassCopyIntoFreshObject(
|
||||
);
|
||||
}
|
||||
|
||||
/** True when an untyped JavaScript receiver names a binding whose declaring
|
||||
* file assigns it an own `toString` (the TypeScript namespace downlevel
|
||||
* `errorUtil.toString = (message) => ...` in zod): its calls answer what
|
||||
* that function returns, not Object.prototype.toString's string. */
|
||||
function jsAssignsOwnToString(lowerer: Lowerer, receiver: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(receiver)) return false;
|
||||
const decl = bindingDeclaration(lowerer, receiver);
|
||||
if (decl === undefined) return false;
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (!isJsSourceFile(sf)) return false;
|
||||
let targets = lowerer.ownToStringTargets.get(sf);
|
||||
if (targets === undefined) {
|
||||
targets = ownToStringTargets(lowerer, sf);
|
||||
lowerer.ownToStringTargets.set(sf, targets);
|
||||
}
|
||||
return targets.has(decl);
|
||||
}
|
||||
|
||||
const unparenthesized = (e: ts.Expression): ts.Expression => {
|
||||
let out = e;
|
||||
while (ts.isParenthesizedExpression(out)) out = out.expression;
|
||||
return out;
|
||||
};
|
||||
|
||||
/** The declaration of the binding an identifier names, through imports. */
|
||||
function bindingDeclaration(lowerer: Lowerer, id: ts.Identifier): ts.Node | undefined {
|
||||
let sym = lowerer.checker.getSymbolAtLocation(id);
|
||||
if (sym !== undefined && (sym.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
sym = lowerer.checker.getAliasedSymbol(sym);
|
||||
return sym?.valueDeclaration ?? sym?.declarations?.[0];
|
||||
}
|
||||
|
||||
/** The declarations of the bindings a file's assignments `x.toString = ...`
|
||||
* (or `x["toString"] = ...`) give an own `toString`. An assignment through
|
||||
* the parameter of an immediately called function counts for the binding
|
||||
* the call passes (`(function (ns) { ns.toString = f; })(ns || (ns = {}))`). */
|
||||
function ownToStringTargets(lowerer: Lowerer, sf: ts.SourceFile): Set<ts.Node> {
|
||||
const targets = new Set<ts.Node>();
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
const left = unparenthesized(node.left);
|
||||
const named =
|
||||
(ts.isPropertyAccessExpression(left) && left.name.text === "toString") ||
|
||||
(ts.isElementAccessExpression(left) &&
|
||||
ts.isStringLiteralLike(left.argumentExpression) &&
|
||||
left.argumentExpression.text === "toString");
|
||||
const target = named
|
||||
? unparenthesized(
|
||||
(left as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression,
|
||||
)
|
||||
: undefined;
|
||||
if (target !== undefined && ts.isIdentifier(target)) {
|
||||
const decl = bindingDeclaration(lowerer, target);
|
||||
if (decl !== undefined) {
|
||||
targets.add(decl);
|
||||
const passed = iifeArgumentDeclaration(lowerer, decl);
|
||||
if (passed !== undefined) targets.add(passed);
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sf);
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** For a parameter of an immediately called function, the declaration of
|
||||
* the binding its argument names: `x`, `x || (x = {})` or `(x = {})`. */
|
||||
function iifeArgumentDeclaration(lowerer: Lowerer, param: ts.Node): ts.Node | undefined {
|
||||
if (!ts.isParameter(param)) return undefined;
|
||||
const fn = param.parent;
|
||||
if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return undefined;
|
||||
let callee: ts.Node = fn;
|
||||
while (ts.isParenthesizedExpression(callee.parent)) callee = callee.parent;
|
||||
const call = callee.parent;
|
||||
if (!ts.isCallExpression(call) || call.expression !== callee) return undefined;
|
||||
const index = fn.parameters.nodes.indexOf(param);
|
||||
let arg = index < 0 ? undefined : call.arguments.nodes[index];
|
||||
if (arg === undefined) return undefined;
|
||||
arg = unparenthesized(arg);
|
||||
if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
arg = unparenthesized(arg.left);
|
||||
if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
||||
arg = unparenthesized(arg.left);
|
||||
return ts.isIdentifier(arg) ? bindingDeclaration(lowerer, arg) : undefined;
|
||||
}
|
||||
|
||||
/** The runtime calls package shims declare as bodiless functions. */
|
||||
const SHIM_INTRINSICS: ReadonlyMap<string, { fn: IrLibFn; args: IrType[]; ret: IrType }> = new Map<
|
||||
string,
|
||||
|
||||
@@ -239,7 +239,7 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
|
||||
return valid;
|
||||
}
|
||||
|
||||
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
export function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
for (
|
||||
let parent: ts.Node | undefined = decl.parent;
|
||||
parent && !ts.isSourceFile(parent);
|
||||
@@ -6651,7 +6651,7 @@ function staticCallOn(
|
||||
// functions, called directly as `%C.static:m%n` — with the same
|
||||
// through-a-VALUE shadowing fence as plain statics.
|
||||
const gfound = findGenericStaticOn(lowerer, info, access.name.text);
|
||||
if (!gfound) return null;
|
||||
if (!gfound) return classConstructorCall(lowerer, call, access, info);
|
||||
if (throughValue && staticShadowBelow(info, access.name.text)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -6744,6 +6744,89 @@ function staticCallOn(
|
||||
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
|
||||
}
|
||||
|
||||
/** `C.call(thisArg, ...)` / `C.apply(thisArg, args)` on a program class that
|
||||
* declares no such static: Function.prototype.call/apply invoke the class
|
||||
* constructor without `new`, which throws Node's TypeError "Class
|
||||
* constructor C cannot be invoked without 'new'" after the arguments are
|
||||
* evaluated (and, for apply, after the argument list is read). The boxed
|
||||
* class value's call path throws exactly that. Neither expando statics nor
|
||||
* writes through untyped values can give a class an own `call` or `apply`.
|
||||
* A receiver that may hold a subclass at run time (a class value, `this` in
|
||||
* a static method) needs every subclass to lack such a static too. The
|
||||
* receiver must be one the caller does not evaluate again (a name, `this`,
|
||||
* or a namespace member). Null otherwise. */
|
||||
function classConstructorCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
info: ClassInfo,
|
||||
): IrExpr | null {
|
||||
const name = access.name.text;
|
||||
const receiver = access.expression;
|
||||
if (
|
||||
(name !== "call" && name !== "apply") ||
|
||||
!info.decl ||
|
||||
info.def.runtime ||
|
||||
info.builtinError ||
|
||||
info.builtinEmitter ||
|
||||
info.builtinStream ||
|
||||
info.builtinEventTarget ||
|
||||
info.runtimeStatics?.length ||
|
||||
declaresStaticAbove(info, name) ||
|
||||
(exactClassOfReceiver(lowerer, receiver) !== info &&
|
||||
info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))) ||
|
||||
call.arguments.nodes.some(ts.isSpreadElement) ||
|
||||
!(
|
||||
ts.isIdentifier(receiver) ||
|
||||
receiver.kind === ts.SyntaxKind.ThisKeyword ||
|
||||
(ts.isPropertyAccessExpression(receiver) && nsMemberIdentOf(lowerer, receiver) !== null)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(receiver, DYN),
|
||||
method: name,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)),
|
||||
type: DYN,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether `info`'s declaration has a static member that may be named `name`
|
||||
* (a method, field or accessor of that name, or a computed name), or `info`
|
||||
* has no declaration to tell. */
|
||||
function declaresStatic(info: ClassInfo, name: string): boolean {
|
||||
if (!info.decl) return true;
|
||||
return info.decl.members.nodes.some(
|
||||
(member) =>
|
||||
member.name !== undefined &&
|
||||
ts.canHaveModifiers(member) &&
|
||||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) ===
|
||||
true &&
|
||||
(ts.isComputedPropertyName(member.name) ||
|
||||
((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
|
||||
member.name.text === name)),
|
||||
);
|
||||
}
|
||||
|
||||
/** declaresStatic over `info` and its bases, which it inherits statics from. */
|
||||
function declaresStaticAbove(info: ClassInfo, name: string): boolean {
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) if (declaresStatic(c, name)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** declaresStatic over `info` and its subclasses, or runtime statics, which a
|
||||
* function-local JavaScript class defines at run time. */
|
||||
function declaresStaticBelow(info: ClassInfo, name: string): boolean {
|
||||
return (
|
||||
declaresStatic(info, name) ||
|
||||
!!info.runtimeStatics?.length ||
|
||||
info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))
|
||||
);
|
||||
}
|
||||
|
||||
/** `C.m(...)` where `m` is a static method that a function-local
|
||||
* JavaScript class defines at runtime (a factory's `class extends base`),
|
||||
* inherited by `C` through its statically known base chain. When the body
|
||||
@@ -10569,6 +10652,45 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A FinalizationRegistry construction: register and unregister keep
|
||||
// their bookkeeping, and the cleanup callback never runs, which
|
||||
// ECMAScript allows (scr_finalization_registry_new).
|
||||
if (
|
||||
symbol !== null &&
|
||||
ts.symbolName(symbol) === "FinalizationRegistry" &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
) {
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"FinalizationRegistry construction with spread arguments",
|
||||
);
|
||||
}
|
||||
const cleanup = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc);
|
||||
const surplus = args.slice(1).map((arg) => lowerer.lowerExpr(arg));
|
||||
const created: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "finalizationRegistry.new",
|
||||
args: [cleanup],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
if (surplus.length === 0) return created;
|
||||
// Surplus arguments evaluate after the cleanup argument and are ignored.
|
||||
const local = lowerer.declareHiddenLocal("%finalizationCleanup", DYN);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: local.id, init: cleanup, loc },
|
||||
...surplus.map((value): IrStmt => ({ kind: "exprStmt", expr: value, loc })),
|
||||
],
|
||||
result: { ...created, args: [varRef(local.id, DYN, loc)] },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `new Set<T>()`: construct + bulk add from supported values,
|
||||
// including live collection cursors. Duplicates collapse in insertion order.
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
|
||||
@@ -10780,8 +10902,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
Boolean: "boxed wrapper objects have no lowering — use the boolean primitive",
|
||||
WeakRef:
|
||||
"deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead",
|
||||
FinalizationRegistry:
|
||||
"finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead",
|
||||
SharedArrayBuffer:
|
||||
"no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
|
||||
ArrayBuffer:
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import {
|
||||
isNewTarget,
|
||||
lowerNewTarget,
|
||||
lowerNewTargetComparison,
|
||||
lowerNewTargetName,
|
||||
} from "./new-target.js";
|
||||
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
@@ -713,7 +719,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isPrefixUnaryExpression(expr)) return lowerer.lowerPrefixUnary(expr);
|
||||
// `x++` / `x--` in expression position: yields the OLD value.
|
||||
if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false);
|
||||
if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr);
|
||||
if (ts.isBinaryExpression(expr))
|
||||
return lowerNewTargetComparison(lowerer, expr) ?? lowerer.lowerBinary(expr);
|
||||
if (ts.isCallExpression(expr)) {
|
||||
// `h.return?.()` on a helper object: the method exists, so the
|
||||
// optional call is the call.
|
||||
@@ -729,13 +736,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isElementAccessExpression(expr)) return lowerer.lowerElementAccess(expr);
|
||||
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
|
||||
|
||||
if (ts.isPropertyAccessExpression(expr)) return lowerPropertyAccessExpr(lowerer, expr, loc);
|
||||
if (ts.isPropertyAccessExpression(expr))
|
||||
return lowerNewTargetName(lowerer, expr) ?? lowerPropertyAccessExpr(lowerer, expr, loc);
|
||||
|
||||
// Meta-properties, named: `new.target` reflects HOW a function was
|
||||
// invoked (compiled functions are never constructors of themselves —
|
||||
// no runtime invocation record exists), and `import.meta`/
|
||||
// `import.defer` are module-loader surface a native binary does not
|
||||
// carry.
|
||||
if (isNewTarget(expr)) return lowerNewTarget(lowerer, expr);
|
||||
if (ts.isMetaProperty(expr)) {
|
||||
const name =
|
||||
expr.keywordToken === ts.SyntaxKind.NewKeyword ? "new.target" : `import.${expr.name.text}`;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
|
||||
import { strLit } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
@@ -19,10 +18,14 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "global.native",
|
||||
// One immortal name table per program (keyed by its contents): reading
|
||||
// the global object in a hot path allocates nothing, and the runtime
|
||||
// recognizes a table it has already registered.
|
||||
args: [
|
||||
{
|
||||
kind: "arrayLit",
|
||||
elems: names.map((name) => strLit(name, loc)),
|
||||
kind: "templateStrings",
|
||||
key: `%globalThis.names:${names.join(",")}`,
|
||||
cooked: names,
|
||||
type: arrayOf(STRING),
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -2188,13 +2188,21 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
|
||||
// A closed record that kept the undeclared properties of its checked
|
||||
// source renders through its live checked view, which holds them in the
|
||||
// source's own-key order beside the current field values.
|
||||
// source's own-key order beside the current field values. An array with
|
||||
// properties besides its elements does too: the view lists them after
|
||||
// the elements.
|
||||
let prependSourceGate: (() => void) | null = null;
|
||||
if (t.kind === "record" && recordMayKeepSource(lowerer, t)) {
|
||||
const sourceTest: IrExpr | null =
|
||||
t.kind === "record" && recordMayKeepSource(lowerer, t)
|
||||
? recordSourceTest(v(), loc)
|
||||
: t.kind === "array" && lowerer.dynConvertible(t)
|
||||
? { kind: "libCall", fn: "array.hasProperties", args: [v()], type: BOOL, loc }
|
||||
: null;
|
||||
if (sourceTest !== null) {
|
||||
prependSourceGate = (): void => {
|
||||
body.unshift({
|
||||
kind: "if",
|
||||
cond: recordSourceTest(v(), loc),
|
||||
cond: sourceTest,
|
||||
then: [
|
||||
ret({
|
||||
kind: "libCall",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
|
||||
* explicit resource management, and blocked-binding poisoning. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerOptionalIndex } from "./undefined-index.js";
|
||||
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
@@ -204,7 +205,6 @@ import {
|
||||
lowerElementCompoundStatement,
|
||||
lowerIncDec,
|
||||
lowerIncDecTarget,
|
||||
lowerOptionalNumber,
|
||||
lowerGroupsProjection,
|
||||
matchResultNamedGroupsOf,
|
||||
runtimeOptionalGuardIds,
|
||||
@@ -1220,6 +1220,43 @@ function lowerJsVarFromExisting(
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a JavaScript declaration's object value changes shape after its
|
||||
* initializer (jsOpenObjectType, jsObjectBindingExpands) or carries
|
||||
* methods, so only the checked-dynamic representation holds it.
|
||||
* `inferred` is the declaration's mapped checker type. */
|
||||
function jsDeclarationExpandsObject(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
inferred: IrType | null,
|
||||
): boolean {
|
||||
const arms =
|
||||
inferred?.kind === "union"
|
||||
? (lowerer.unions.get(inferred.unionId)?.arms ?? [])
|
||||
: inferred
|
||||
? [inferred]
|
||||
: [];
|
||||
return (
|
||||
(!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
inferred !== null &&
|
||||
jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") ||
|
||||
arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
ts.isObjectLiteralExpression(decl.initializer) &&
|
||||
decl.initializer.properties.nodes.some(
|
||||
(property) =>
|
||||
ts.isMethodDeclaration(property) ||
|
||||
ts.isAccessor(property) ||
|
||||
(ts.isPropertyAssignment(property) &&
|
||||
(ts.isFunctionExpression(property.initializer) ||
|
||||
ts.isArrowFunction(property.initializer))),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/** The `var` symbol's function-scoped binding TYPE — the declared type at
|
||||
* the binding name, with the JS-source fallbacks every mutable binding
|
||||
* takes (an empty-object-literal type is checked-dynamic because tsc
|
||||
@@ -1254,6 +1291,19 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul
|
||||
type = staticImportNamespaceType(lowerer, nameNode.parent.initializer) ?? type;
|
||||
}
|
||||
if (!type || type.kind === "void") return null;
|
||||
// A JavaScript `var` holds whatever its writes store, as a `let` does:
|
||||
// the checker types it by its initializer and checks no later write
|
||||
// (`var fraction = 0;`, later `fraction = match[7].slice(0, 3)`), and an
|
||||
// object it holds may gain members (`mark.snippet = snippet(mark)`).
|
||||
if (
|
||||
type.kind !== "dyn" &&
|
||||
ts.isVariableDeclaration(nameNode.parent) &&
|
||||
nameNode.parent.name === nameNode &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
|
||||
(jsBindingHasOpenWrites(lowerer, nameNode.parent) ||
|
||||
jsDeclarationExpandsObject(lowerer, nameNode.parent, type))
|
||||
)
|
||||
return DYN;
|
||||
// A binding readable before its first store holds undefined until then.
|
||||
return lowerer.runtimeOptionalBindingType(nameNode, type);
|
||||
}
|
||||
@@ -4870,12 +4920,16 @@ export function bindPatternTarget(
|
||||
}
|
||||
// Preserve checked-value pattern bindings when the inferred type
|
||||
// cannot represent them without a refusal or an alias-breaking copy.
|
||||
// A name destructured from a container of an evolving array
|
||||
// (evolvingArrayHold) may hold the array, which the checker types by
|
||||
// the elements written before it.
|
||||
const dynStays =
|
||||
value.type.kind === "dyn" &&
|
||||
(() => {
|
||||
((() => {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(name));
|
||||
return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped);
|
||||
})();
|
||||
})() ||
|
||||
readsHeldEvolvingArray(lowerer, name));
|
||||
let type = dynStays
|
||||
? DYN
|
||||
: (lowerer.runtimeOptionalBindingType(name, lowerer.irTypeOf(name)) ??
|
||||
@@ -5004,13 +5058,16 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
// type once the reads settle it.
|
||||
const written: ts.Expression[] = [];
|
||||
// The file's identifiers are indexed by name once (identifiersNamed), so
|
||||
// each binding resolves only the uses spelled like it.
|
||||
// each binding resolves only the uses spelled like it. A shorthand
|
||||
// property (`{ xs }`) is a use too.
|
||||
for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
|
||||
if (
|
||||
node === decl.name ||
|
||||
node.pos < scope.pos ||
|
||||
node.end > scope.end ||
|
||||
lowerer.checker.getSymbolAtLocation(node) !== symbol
|
||||
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
||||
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
|
||||
: lowerer.checker.getSymbolAtLocation(node)) !== symbol
|
||||
)
|
||||
continue;
|
||||
let target: ts.Node = node;
|
||||
@@ -5068,28 +5125,105 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
return result;
|
||||
}
|
||||
|
||||
/** An unannotated alias of an evolving array binding whose reads disagree
|
||||
* (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the
|
||||
* checker types the alias by the elements written before it, but both names
|
||||
* hold one array, so the alias takes the binding's checked-dynamic type and
|
||||
* sees the later writes. Aliases of such an alias follow it. Null for other
|
||||
* bindings. */
|
||||
function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
let current = decl;
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
let initializer = current.initializer;
|
||||
while (initializer !== undefined && ts.isParenthesizedExpression(initializer))
|
||||
initializer = initializer.expression;
|
||||
if (initializer === undefined || !ts.isIdentifier(initializer)) return null;
|
||||
const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration;
|
||||
/** How a TypeScript expression holds an evolving array binding whose reads
|
||||
* disagree (evolvingArrayType answers checked-dynamic): "evolving" for the
|
||||
* binding itself, "alias" for an unannotated binding initialized with it or
|
||||
* with an alias of it (`const alias = xs`), and "container" for an object or
|
||||
* array literal with it as a property, element or spread source at any
|
||||
* depth (`{ items: xs }`, `[xs]`), an unannotated binding initialized with
|
||||
* or destructured from such a literal, and a property or element read of a
|
||||
* container (`box.items`). The checker types an alias or container by the
|
||||
* elements written before it was taken, but it holds the same array. Null
|
||||
* for other expressions. */
|
||||
function evolvingArrayHold(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
depth = 0,
|
||||
): "evolving" | "alias" | "container" | null {
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (depth >= 8) return null;
|
||||
const binding = (symbol: ts.Symbol | undefined): "evolving" | "alias" | "container" | null => {
|
||||
const source = symbol?.valueDeclaration;
|
||||
// A name destructured from a container (`const { items } = box`) may
|
||||
// be the array or another container.
|
||||
if (source !== undefined && ts.isBindingElement(source) && ts.isIdentifier(source.name)) {
|
||||
const host = hostVariableDeclarationOf(source.name);
|
||||
return host !== null &&
|
||||
!host.type &&
|
||||
host.initializer !== undefined &&
|
||||
evolvingArrayHold(lowerer, host.initializer, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
}
|
||||
if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null;
|
||||
const evolved = evolvingArrayType(lowerer, source);
|
||||
if (evolved !== null) return evolved.kind === "dyn" ? evolved : null;
|
||||
current = source;
|
||||
if (evolved !== null) return evolved.kind === "dyn" ? "evolving" : null;
|
||||
if (source.initializer === undefined) return null;
|
||||
const held = evolvingArrayHold(lowerer, source.initializer, depth + 1);
|
||||
return held === "evolving" ? "alias" : held;
|
||||
};
|
||||
if (ts.isIdentifier(expr)) return binding(lowerer.checker.getSymbolAtLocation(expr));
|
||||
if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr))
|
||||
return evolvingArrayHold(lowerer, expr.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
// A spread of the array itself (`{ ...xs }`, `[...xs]`) copies its
|
||||
// elements, as in Node; a spread of a container copies its references.
|
||||
if (ts.isObjectLiteralExpression(expr)) {
|
||||
for (const property of expr.properties.nodes) {
|
||||
const held = ts.isPropertyAssignment(property)
|
||||
? evolvingArrayHold(lowerer, property.initializer, depth + 1)
|
||||
: ts.isShorthandPropertyAssignment(property)
|
||||
? binding(lowerer.checker.getShorthandAssignmentValueSymbol(property))
|
||||
: ts.isSpreadAssignment(property) &&
|
||||
evolvingArrayHold(lowerer, property.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null;
|
||||
if (held !== null) return "container";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(expr)) {
|
||||
for (const element of expr.elements.nodes) {
|
||||
const held = ts.isSpreadElement(element)
|
||||
? evolvingArrayHold(lowerer, element.expression, depth + 1) === "container"
|
||||
? "container"
|
||||
: null
|
||||
: evolvingArrayHold(lowerer, element, depth + 1);
|
||||
if (held !== null) return "container";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a TypeScript expression is an alias or container of an evolving
|
||||
* array binding whose reads disagree (evolvingArrayHold). Its value is
|
||||
* checked-dynamic, and the checker's type for it lists only the elements
|
||||
* written before it was taken, so element reads through it stay
|
||||
* checked-dynamic instead of checking against that type. */
|
||||
export function readsHeldEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
const held = evolvingArrayHold(lowerer, expr);
|
||||
return held === "alias" || held === "container";
|
||||
}
|
||||
|
||||
/** Whether a TypeScript object or array literal holds an evolving array
|
||||
* binding whose reads disagree (evolvingArrayHold), so it builds as a
|
||||
* checked-dynamic literal that keeps the array. */
|
||||
export function holdsEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
return evolvingArrayHold(lowerer, expr) === "container";
|
||||
}
|
||||
|
||||
/** An unannotated binding initialized with an evolving array binding whose
|
||||
* reads disagree, or with an alias or container of one (evolvingArrayHold):
|
||||
* it takes the array's checked-dynamic type, so it sees the later writes.
|
||||
* Null for other bindings. */
|
||||
function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
if (decl.initializer === undefined) return null;
|
||||
return evolvingArrayHold(lowerer, decl.initializer) === null ? null : DYN;
|
||||
}
|
||||
|
||||
/** A single static type for an initializer-less JavaScript `let` whose
|
||||
* later plain writes all agree. An evolving JS local reads as `undefined`
|
||||
* until control flow proves a write, so a scalar slot is sound only when
|
||||
@@ -5597,26 +5731,7 @@ export function lowerVarDecl(
|
||||
: inferredObjectType
|
||||
? [inferredObjectType]
|
||||
: [];
|
||||
const expandsObject =
|
||||
(!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
inferredObjectType !== null &&
|
||||
jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn") ||
|
||||
inferredObjectArms.some(
|
||||
(arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm),
|
||||
) ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
ts.isObjectLiteralExpression(decl.initializer) &&
|
||||
decl.initializer.properties.nodes.some(
|
||||
(property) =>
|
||||
ts.isMethodDeclaration(property) ||
|
||||
ts.isAccessor(property) ||
|
||||
(ts.isPropertyAssignment(property) &&
|
||||
(ts.isFunctionExpression(property.initializer) ||
|
||||
ts.isArrowFunction(property.initializer))),
|
||||
));
|
||||
const expandsObject = jsDeclarationExpandsObject(lowerer, decl, inferredObjectType);
|
||||
let init: IrExpr;
|
||||
try {
|
||||
init =
|
||||
@@ -5803,7 +5918,7 @@ export function lowerVarDecl(
|
||||
!decl.type &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
) {
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl);
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayHolderType(lowerer, decl);
|
||||
if (evolved !== null) {
|
||||
type = evolved;
|
||||
init = lowerer.lowerExprExpecting(decl.initializer, evolved);
|
||||
@@ -6327,18 +6442,15 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Inferred JS scalar initializers do not constrain later writes through
|
||||
* an any-valued callback. Keep those bindings open, including captured writes. */
|
||||
export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
|
||||
decl.type ||
|
||||
hasJsTypeAnnotation(decl) ||
|
||||
!ts.isIdentifier(decl.name)
|
||||
)
|
||||
return false;
|
||||
const symbol = lowerer.resolveValueSymbol(decl.name);
|
||||
if (!symbol) return false;
|
||||
/** The assignments to a declaration's binding in its function (or file),
|
||||
* `=` and compound alike. Null when the name has no symbol. */
|
||||
function jsBindingWrites(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
name: ts.Identifier,
|
||||
): ts.BinaryExpression[] | null {
|
||||
const symbol = lowerer.resolveValueSymbol(name);
|
||||
if (!symbol) return null;
|
||||
let owner: ts.Node = decl;
|
||||
while (owner.parent && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) owner = owner.parent;
|
||||
let writes = lowerer.jsBindingWritesByOwner.get(owner);
|
||||
@@ -6359,9 +6471,36 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
|
||||
writes = indexed;
|
||||
lowerer.jsBindingWritesByOwner.set(owner, writes);
|
||||
}
|
||||
const candidates = (writes.get(decl.name.text) ?? []).filter(
|
||||
return (writes.get(name.text) ?? []).filter(
|
||||
(node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol,
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a JavaScript binding always holds a string: the checker types it
|
||||
* as one, and every write appends to it (`result += part`), which yields a
|
||||
* string whatever the part is. */
|
||||
export function jsBindingAppendsOnly(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (!ts.isIdentifier(decl.name)) return false;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "string") return false;
|
||||
const writes = jsBindingWrites(lowerer, decl, decl.name);
|
||||
return (
|
||||
writes !== null &&
|
||||
writes.every((node) => node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken)
|
||||
);
|
||||
}
|
||||
|
||||
/** Inferred JS scalar initializers do not constrain later writes through
|
||||
* an any-valued callback. Keep those bindings open, including captured writes. */
|
||||
export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
|
||||
decl.type ||
|
||||
hasJsTypeAnnotation(decl) ||
|
||||
!ts.isIdentifier(decl.name)
|
||||
)
|
||||
return false;
|
||||
const candidates = jsBindingWrites(lowerer, decl, decl.name);
|
||||
if (candidates === null) return false;
|
||||
if (
|
||||
candidates.some(
|
||||
(node) =>
|
||||
@@ -6369,6 +6508,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
|
||||
)
|
||||
)
|
||||
return true;
|
||||
// A `+=` of a string turns a number or boolean binding into a string
|
||||
// (`n += "px"`); the checker keeps the declared type.
|
||||
if (
|
||||
candidates.some(
|
||||
(node) =>
|
||||
node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken &&
|
||||
(lowerer.typeOf(node.right).flags & ts.TypeFlags.StringLike) !== 0,
|
||||
)
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
|
||||
if (declared?.kind === "f64" || declared?.kind === "bool") return true;
|
||||
}
|
||||
// A write that can store null or undefined into a binding the checker
|
||||
// types as a primitive (`offset = match[3] || null`): JavaScript files
|
||||
// are checked without strict null checks, so the declared type does not
|
||||
@@ -9212,7 +9363,7 @@ function lowerAssignTargetInto(
|
||||
if (arr.type.kind === "array") {
|
||||
const recv = lowerer.declareHiddenLocal("%dtRecv", arr.type);
|
||||
out.push({ kind: "varDecl", localId: recv.id, init: arr, loc });
|
||||
const index = lowerOptionalNumber(
|
||||
const index = lowerOptionalIndex(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(t.argumentExpression),
|
||||
locOf(t.argumentExpression),
|
||||
|
||||
@@ -4637,10 +4637,12 @@ function lowerOperatorCallback(
|
||||
"the callback's parameter and result types must cross the checked-dynamic boundary",
|
||||
);
|
||||
}
|
||||
const fn: IrExpr = { kind: "dynFrom", value: v, type: DYN, loc };
|
||||
// flatMap over a compiled generator: the runtime steps it through the
|
||||
// helpers over its type
|
||||
const gen = member === "flatMap" && v.type.ret.kind === "generator" ? v.type.ret : undefined;
|
||||
return { fn: { kind: "dynFrom", value: v, type: DYN, loc }, takesOptions, ...(gen && { gen }) };
|
||||
if (member === "flatMap" && v.type.ret.kind === "generator")
|
||||
return { fn, takesOptions, gen: v.type.ret };
|
||||
return { fn, takesOptions };
|
||||
}
|
||||
|
||||
/** The operator callbacks' `{ signal }` (operators.js): map and the
|
||||
|
||||
@@ -614,6 +614,7 @@ import {
|
||||
lowerForStatement,
|
||||
matchGroupsPatternSource,
|
||||
isGuardedRecordRead,
|
||||
holdsEvolvingArray,
|
||||
} from "./lower-stmts.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
@@ -1922,6 +1923,9 @@ export class Lowerer {
|
||||
/** Identifiers by spelling, per file, for the generic bindings' use
|
||||
* ordering (runsAfterDeclaration). */
|
||||
readonly bindingIdentifiers = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
|
||||
/** Per JavaScript file, the declarations of the bindings it assigns an
|
||||
* own `toString` (jsAssignsOwnToString in lower-calls.ts). */
|
||||
readonly ownToStringTargets = new Map<ts.SourceFile, Set<ts.Node>>();
|
||||
/** Per-symbol result of the never-reassigned file scan
|
||||
* (bindingNeverReassigned — object-literal generic-method receivers). */
|
||||
readonly neverReassignedCache = new Map<ts.Symbol, boolean>();
|
||||
@@ -2184,6 +2188,9 @@ export class Lowerer {
|
||||
* and erased instances). */
|
||||
erasingDepth = 0;
|
||||
localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = [];
|
||||
/** The function-local classes of the program with an `extends` clause, whether lowering collected
|
||||
* them yet or not (new-target.ts); found on first use. */
|
||||
functionLocalSubclasses: ts.ClassLikeDeclaration[] | undefined = undefined;
|
||||
/** Instance types (by checker type id) of the local classes generic
|
||||
* factory calls return, and the per-instance class each names. */
|
||||
readonly localClassInstanceTypes = new Map<number, string>();
|
||||
@@ -10652,7 +10659,8 @@ export class Lowerer {
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts)
|
||||
this.excessJsContextualProperty(x, parts) ||
|
||||
holdsEvolvingArray(this, x)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/* `new.target`. Compiled code has no invocation record, but inside a class
|
||||
* constructor `new.target` is always the class of the object under
|
||||
* construction: a compiled `new` allocates the instance with its concrete
|
||||
* class before any constructor of the chain runs, and every constructor of
|
||||
* the chain receives that instance as `this`. The program's class
|
||||
* hierarchy is closed, so the value is a static dispatch over the
|
||||
* constructor's class and its descendants: the deepest class the instance
|
||||
* is an instance of is its exact class.
|
||||
*
|
||||
* The checker rejects `new.target` in methods, accessors, static blocks and
|
||||
* field initializers. Plain functions (constructible in JavaScript) keep
|
||||
* the refusal. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { classValueRef, functionLocalClass } from "./lower-classes.js";
|
||||
|
||||
export function isNewTarget(node: ts.Node): node is ts.MetaProperty {
|
||||
return (
|
||||
ts.isMetaProperty(node) &&
|
||||
node.keywordToken === ts.SyntaxKind.NewKeyword &&
|
||||
node.name.text === "target"
|
||||
);
|
||||
}
|
||||
|
||||
/** Where `new.target` is bound: a class constructor, or a plain function
|
||||
* (refused). */
|
||||
type Binding =
|
||||
| { kind: "constructor"; declaration: ts.ConstructorDeclaration }
|
||||
| { kind: "function" };
|
||||
|
||||
function bindingOf(node: ts.Node): Binding {
|
||||
for (let p = node.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isArrowFunction(p)) continue;
|
||||
if (ts.isConstructorDeclaration(p)) return { kind: "constructor", declaration: p };
|
||||
if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return { kind: "function" };
|
||||
}
|
||||
return { kind: "function" };
|
||||
}
|
||||
|
||||
interface Dispatch {
|
||||
info: ClassInfo;
|
||||
/** The constructor's descendants, deepest first, then its class. A
|
||||
* generic family stands for its instantiations, also those that later
|
||||
* bodies instantiate: its interval holds them all. */
|
||||
classes: ClassInfo[];
|
||||
self: IrExpr | null;
|
||||
}
|
||||
|
||||
function depth(info: ClassInfo): number {
|
||||
let d = 0;
|
||||
for (let c = info.base; c; c = c.base) d++;
|
||||
return d;
|
||||
}
|
||||
|
||||
/** The function-local classes of the program that extend a class. */
|
||||
function functionLocalSubclasses(lowerer: Lowerer): ts.ClassLikeDeclaration[] {
|
||||
let found = lowerer.functionLocalSubclasses;
|
||||
if (found === undefined) {
|
||||
const classes: ts.ClassLikeDeclaration[] = [];
|
||||
for (const sf of lowerer.moduleOrder)
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
|
||||
baseExpressionOf(node) !== undefined &&
|
||||
functionLocalClass(node)
|
||||
)
|
||||
classes.push(node);
|
||||
});
|
||||
lowerer.functionLocalSubclasses = found = classes;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function baseExpressionOf(node: ts.ClassLikeDeclaration): ts.Expression | undefined {
|
||||
let base = node.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
while (base !== undefined && ts.isParenthesizedExpression(base)) base = base.expression;
|
||||
return base;
|
||||
}
|
||||
|
||||
/** Whether the class `node` extends the class of `info` (for an
|
||||
* instantiation, its family), directly or through other classes, as far as
|
||||
* its `extends` clauses name classes. */
|
||||
function derivesFrom(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ClassLikeDeclaration,
|
||||
info: ClassInfo,
|
||||
seen: Set<ts.Node>,
|
||||
): boolean {
|
||||
if (seen.has(node)) return false;
|
||||
seen.add(node);
|
||||
const base = baseExpressionOf(node);
|
||||
const name =
|
||||
base === undefined
|
||||
? undefined
|
||||
: ts.isIdentifier(base)
|
||||
? base
|
||||
: ts.isPropertyAccessExpression(base)
|
||||
? base.name
|
||||
: undefined;
|
||||
let symbol = name === undefined ? undefined : lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
if (symbol === undefined) return false;
|
||||
const named = lowerer.classBySymbol.get(symbol);
|
||||
if (named !== undefined)
|
||||
return (
|
||||
named === info ||
|
||||
named === info.genericInstance?.family ||
|
||||
lowerer.isSubclassOf(named.def.name, info.def.name)
|
||||
);
|
||||
// A class lowering has not collected: through its declaration.
|
||||
for (const declaration of symbol.declarations) {
|
||||
let cls: ts.Node | undefined = declaration;
|
||||
if (ts.isVariableDeclaration(cls)) {
|
||||
cls = cls.initializer;
|
||||
while (cls !== undefined && ts.isParenthesizedExpression(cls)) cls = cls.expression;
|
||||
}
|
||||
if (
|
||||
cls !== undefined &&
|
||||
(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) &&
|
||||
derivesFrom(lowerer, cls, info, seen)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `node`'s name, quoted, for diagnostics. */
|
||||
function localClassName(node: ts.ClassLikeDeclaration): string {
|
||||
if (node.name) return `'${node.name.text}'`;
|
||||
const parent = node.parent;
|
||||
return parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)
|
||||
? `'${parent.name.text}'`
|
||||
: "a class expression";
|
||||
}
|
||||
|
||||
/** The classes `new.target` can denote in the constructor being lowered,
|
||||
* or null after a diagnostic. */
|
||||
function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null {
|
||||
const binding = bindingOf(expr);
|
||||
if (binding.kind !== "constructor") return null;
|
||||
const info = lowerer.currentClass;
|
||||
const cls = binding.declaration.parent;
|
||||
if (!info || (info.decl !== null && info.decl !== cls)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'new.target' in this constructor (its class has no lowering here)",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// An instantiation's subclasses can also be instantiations of generic
|
||||
// subclasses of its family (`class G<T> extends Box<T>` under Box<number>).
|
||||
const family = info.genericInstance?.family;
|
||||
const descendants: ClassInfo[] = [];
|
||||
for (const other of lowerer.classes.values()) {
|
||||
if (other === info) continue;
|
||||
if (
|
||||
lowerer.isSubclassOf(other.def.name, info.def.name) ||
|
||||
(family !== undefined &&
|
||||
other.generic !== undefined &&
|
||||
other !== family &&
|
||||
lowerer.isSubclassOf(other.def.name, family.def.name))
|
||||
)
|
||||
descendants.push(other);
|
||||
}
|
||||
descendants.sort((a, b) => depth(b) - depth(a));
|
||||
const classes = [...descendants, info];
|
||||
for (const c of classes) {
|
||||
if (c.localClass) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'new.target' where '${c.def.jsName ?? c.def.name}' is a function-local class (each evaluation creates a new class)`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Lowering collects a function-local class with the body declaring it,
|
||||
// which may come after this constructor.
|
||||
for (const local of functionLocalSubclasses(lowerer)) {
|
||||
if (!derivesFrom(lowerer, local, info, new Set())) continue;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'new.target' where ${localClassName(local)} is a function-local class (each evaluation creates a new class)`,
|
||||
);
|
||||
}
|
||||
let self: IrExpr | null = null;
|
||||
if (classes.length > 1) {
|
||||
const thisLocal = lowerer.resolveThis(expr);
|
||||
if (!thisLocal || thisLocal.type.kind !== "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'new.target' where the constructor's 'this' has no class representation",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
self = varRef(thisLocal.id, thisLocal.type, locOf(expr));
|
||||
}
|
||||
return { info, classes, self };
|
||||
}
|
||||
|
||||
/** A conditional chain selecting `arm(D)` for the exact class D of the
|
||||
* instance under construction. */
|
||||
function select(
|
||||
dispatch: Dispatch,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
arm: (info: ClassInfo) => IrExpr,
|
||||
): IrExpr {
|
||||
const { classes, self } = dispatch;
|
||||
let result = arm(classes[classes.length - 1]!);
|
||||
for (let i = classes.length - 2; i >= 0; i--) {
|
||||
const c = classes[i]!;
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "instanceOf", value: self!, className: c.def.name, type: BOOL, loc },
|
||||
then: arm(c),
|
||||
else_: result,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function refuseFunction(lowerer: Lowerer, expr: ts.MetaProperty): never {
|
||||
return lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor)",
|
||||
);
|
||||
}
|
||||
|
||||
/** `new.target` as a value. */
|
||||
export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
if (bindingOf(expr).kind === "function") refuseFunction(lowerer, expr);
|
||||
const dispatch = dispatchOf(lowerer, expr)!;
|
||||
const type: IrType = { kind: "classval", className: dispatch.info.def.name };
|
||||
for (const c of dispatch.classes) {
|
||||
if (c.generic) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'new.target' as a value where '${c.def.jsName ?? c.def.name}' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead)`,
|
||||
);
|
||||
}
|
||||
if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'new.target' as a value where '${c.def.jsName ?? c.def.name}' constructs with a different signature than '${dispatch.info.def.jsName ?? dispatch.info.def.name}' (read 'new.target.name' or compare 'new.target' with a class instead)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return select(dispatch, type, loc, (c) =>
|
||||
lowerer.coerceToExpected(classValueRef(lowerer, c, expr), type),
|
||||
);
|
||||
}
|
||||
|
||||
/** `new.target.name`: the exact class's name, without materializing a
|
||||
* class value (subclasses may construct with other signatures). */
|
||||
export function lowerNewTargetName(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.questionDotToken || access.name.text !== "name") return null;
|
||||
let target: ts.Expression = access.expression;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (!isNewTarget(target) || bindingOf(target).kind !== "constructor") return null;
|
||||
const dispatch = dispatchOf(lowerer, target);
|
||||
if (!dispatch) return null;
|
||||
const loc = locOf(access);
|
||||
return select(dispatch, STRING, loc, (c) => {
|
||||
// An own static `name` field replaces the class's name; a subclass has
|
||||
// its own name. An instantiation's statics are its family's.
|
||||
const owner = c.genericInstance?.family ?? c;
|
||||
const field = owner.staticFields.find((f) => f.name === "name");
|
||||
if (field !== undefined) {
|
||||
if (field.type.kind !== "string")
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
`'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static 'name' that is not a string`,
|
||||
);
|
||||
return { kind: "varRef", localId: field.globalId, type: field.type, loc };
|
||||
}
|
||||
if (owner.staticMethods?.has("name") || owner.genericStatics?.has("name"))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
`'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static method 'name'`,
|
||||
);
|
||||
// A generic family's instantiations share its name.
|
||||
if (c.generic)
|
||||
return { kind: "strLit", value: c.def.jsName ?? c.generic.baseName, type: STRING, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "class.name",
|
||||
args: [classValueRef(lowerer, c, target)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a
|
||||
* program class named by the other operand: an exact-class test. */
|
||||
export function lowerNewTargetComparison(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
): IrExpr | null {
|
||||
const op = expr.operatorToken.kind;
|
||||
const negated =
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken;
|
||||
if (
|
||||
!negated &&
|
||||
op !== ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
||||
op !== ts.SyntaxKind.EqualsEqualsToken
|
||||
)
|
||||
return null;
|
||||
const strip = (e: ts.Expression): ts.Expression => {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
};
|
||||
const left = strip(expr.left);
|
||||
const right = strip(expr.right);
|
||||
const target = isNewTarget(left) ? left : isNewTarget(right) ? right : null;
|
||||
if (!target || bindingOf(target).kind !== "constructor") return null;
|
||||
const other = target === left ? right : left;
|
||||
if (!ts.isIdentifier(other)) return null;
|
||||
// A constructor's new.target is never undefined.
|
||||
if (other.text === "undefined" && (lowerer.typeOf(other).flags & ts.TypeFlags.Undefined) !== 0)
|
||||
return { kind: "boolLit", value: negated, type: BOOL, loc: locOf(expr) };
|
||||
const symbol = lowerer.resolveValueSymbol(other);
|
||||
const named = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
||||
if (!named || named.generic || named.localClass) return null;
|
||||
const dispatch = dispatchOf(lowerer, target);
|
||||
if (!dispatch) return null;
|
||||
const loc = locOf(expr);
|
||||
return select(dispatch, BOOL, loc, (c) => ({
|
||||
kind: "boolLit",
|
||||
value: (c === named) !== negated,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, F64, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerOptionalNumber } from "./lower-exprs.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
|
||||
/** An element index the checker types as a number can be undefined at run
|
||||
* time (an element read from a holey array). JavaScript then names the
|
||||
* property "undefined", not "NaN". */
|
||||
|
||||
/** The array element index `operand` (the lowered index of `node`) as a
|
||||
* number. An undefined index is the runtime's "undefined" key
|
||||
* (`array.undefinedKey`, a NaN the array's property table spells
|
||||
* "undefined"); other values convert with ToNumber, as lowerOptionalNumber
|
||||
* does. */
|
||||
export function lowerOptionalIndex(
|
||||
lowerer: Lowerer,
|
||||
operand: IrExpr,
|
||||
loc: SrcLoc,
|
||||
node: ts.Expression,
|
||||
): IrExpr {
|
||||
const split = splitUndefinedIndex(lowerer, operand, node, loc);
|
||||
if (split === null) return lowerOptionalNumber(lowerer, operand, loc, node);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: split.stmts,
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: split.isUndefined,
|
||||
then: { kind: "libCall", fn: "array.undefinedKey", args: [], type: F64, loc },
|
||||
else_: split.index,
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A typed-array element index that can be undefined: `stmts` evaluate it
|
||||
* once, `isUndefined` tests it, and `index` is the number a defined index
|
||||
* converts to. */
|
||||
export interface SplitIndex {
|
||||
stmts: IrStmt[];
|
||||
isUndefined: IrExpr;
|
||||
index: IrExpr;
|
||||
}
|
||||
|
||||
/** Splits `raw`, the lowered index of `node`, when it is a number that can
|
||||
* be undefined; null otherwise. */
|
||||
export function splitUndefinedIndex(
|
||||
lowerer: Lowerer,
|
||||
raw: IrExpr,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): SplitIndex | null {
|
||||
if (raw.type.kind !== "union") return null;
|
||||
const undefinedTag = lowerer.armTag(raw.type.unionId, UNDEFINED_T);
|
||||
if (undefinedTag < 0) return null;
|
||||
const local = lowerer.declareHiddenLocal("%elementIndex", raw.type);
|
||||
const stable = varRef(local.id, raw.type, loc);
|
||||
const index = lowerOptionalNumber(lowerer, stable, loc, node);
|
||||
if (index.type.kind !== "f64") return null;
|
||||
return {
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: raw, loc }],
|
||||
isUndefined: {
|
||||
kind: "unionIsTag",
|
||||
unionId: raw.type.unionId,
|
||||
tag: undefinedTag,
|
||||
negated: false,
|
||||
value: stable,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
/** A typed array has no property named "undefined" to store into: the
|
||||
* store throws the catchable SC1090 error when it runs. A call, so that it
|
||||
* can sit among an expression's statements. */
|
||||
export function undefinedTypedArrayKeyFence(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrStmt {
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
const message = `typed array properties named 'undefined' (an element index that is undefined) are not supported yet [SC1090 at ${loc.file}:${line}]`;
|
||||
const key = `index.undefinedFence:${message}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = keyedHelperName(lowerer, "%index.undefinedFence", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [{ kind: "runtimeFence", code: "SC1090", message, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [], type: DYN, loc }, loc };
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { tokenEquivalentSource } from "./token-equivalence.js";
|
||||
|
||||
const equivalent = (previous: string, current: string, path = "/src/a.ts") =>
|
||||
tokenEquivalentSource(path, previous, current);
|
||||
|
||||
test("whitespace and plain comments are trivia", () => {
|
||||
expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true);
|
||||
expect(equivalent("f(a,b);\n", "f( a, /* first */ b );\n")).toBe(true);
|
||||
expect(equivalent("const s = `x${a}y`;\n", "const s = `x${ a /* the value */ }y`;\n")).toBe(true);
|
||||
});
|
||||
|
||||
test("tokens, literals and line breaks that change semicolon insertion are not", () => {
|
||||
expect(equivalent("const a = 1;\n", "const a = 2;\n")).toBe(false);
|
||||
expect(equivalent('const s = "a // b";\n', 'const s = "a // c";\n')).toBe(false);
|
||||
expect(equivalent("const s = `a // ${b} /* c */`;\n", "const s = `a // ${b} /* d */`;\n")).toBe(
|
||||
false,
|
||||
);
|
||||
// `return` followed by a line break returns undefined.
|
||||
expect(equivalent("return a;\n", "return\na;\n")).toBe(false);
|
||||
expect(equivalent("return a;\n", "return /*\n*/ a;\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("comment-like text inside regular expressions is code", () => {
|
||||
expect(equivalent("const r = /[//]/; x = 1;\n", "const r = /[//]/; x = 2;\n")).toBe(false);
|
||||
expect(equivalent("if (a) /\\/*/.test(b); c = 1;\n", "if (a) /\\/*/.test(b); c = 2;\n")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(equivalent("const q = a / b; // ratio\n", "const q = a / b; // the ratio\n")).toBe(true);
|
||||
expect(equivalent("return /a/; // one\n", "return /a/; // two\n")).toBe(true);
|
||||
// A `for await` header closes like a `for` header.
|
||||
expect(
|
||||
equivalent(
|
||||
"for await (const x of xs) /[//]/.test(x); c = 1;\n",
|
||||
"for await (const x of xs) /[//]/.test(x); c = 2;\n",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("a / after a line break is never equivalent", () => {
|
||||
const loop = (body: string) => `a: for (;;) {\n${body}\n}\n`;
|
||||
// Automatic semicolon insertion decides whether these start a regular
|
||||
// expression.
|
||||
expect(
|
||||
equivalent(loop("break\n/[//]/.test(x); c = 1;"), loop("break\n/[//]/.test(x); c = 2;")),
|
||||
).toBe(false);
|
||||
expect(
|
||||
equivalent(loop("break a\n/[//]/.test(x); c = 1;"), loop("break a\n/[//]/.test(x); c = 2;")),
|
||||
).toBe(false);
|
||||
expect(
|
||||
equivalent("type A = B\n/[//]/.test(x); c = 1;\n", "type A = B\n/[//]/.test(x); c = 2;\n"),
|
||||
).toBe(false);
|
||||
// A comment spanning lines separates them too; JavaScript keeps its comments as tokens.
|
||||
expect(
|
||||
equivalent(
|
||||
"x\n/* a\n */ /[//]/.test(y); c = 1;\n",
|
||||
"x\n/* a\n */ /[//]/.test(y); c = 2;\n",
|
||||
"/src/a.js",
|
||||
),
|
||||
).toBe(false);
|
||||
// A / on the line of the token before it still divides.
|
||||
expect(equivalent(loop("continue\nx / 2; // one"), loop("continue\nx / 2; // two"))).toBe(true);
|
||||
});
|
||||
|
||||
test("code units outside ASCII are equivalent only inside strings, templates and comments", () => {
|
||||
expect(equivalent('const s = "é"; // one\n', 'const s = "é"; // two\n')).toBe(true);
|
||||
expect(equivalent("const s = `é${a}`; // one\n", "const s = `é${a}`; // two\n")).toBe(true);
|
||||
expect(equivalent("const a = 1; // é\n", "const a = 1; // è\n")).toBe(true);
|
||||
expect(equivalent("const café = 1; // one\n", "const café = 1; // two\n")).toBe(false);
|
||||
expect(
|
||||
equivalent(
|
||||
"ä: for (;;) { break ä\n/[//]/.test(x); c = 1; }\n",
|
||||
"ä: for (;;) { break ä\n/[//]/.test(x); c = 2; }\n",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(equivalent("const r = /é/; // one\n", "const r = /é/; // two\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("whitespace outside ASCII is never equivalent", () => {
|
||||
expect(
|
||||
equivalent("return\u3000/[//]/.test(x); c = 1;\n", "return\u3000/[//]/.test(x); c = 2;\n"),
|
||||
).toBe(false);
|
||||
expect(equivalent("const a = 1;\u00a0// one\n", "const a = 1;\u00a0// two\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("U+2028 and U+2029 end a line comment, so their files are never equivalent", () => {
|
||||
expect(equivalent("// a\u2028c = 1;\n", "// a\u2028c = 2;\n")).toBe(false);
|
||||
expect(equivalent("/* a\u2029*/ c = 1;\n", "/* b\u2029*/ c = 1;\n")).toBe(false);
|
||||
});
|
||||
|
||||
test("comments that can affect compilation are kept", () => {
|
||||
expect(equivalent("/** @type {number} */ let a;\n", "/** @type {string} */ let a;\n")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(equivalent("// @ts-ignore\nf();\n", "f();\n")).toBe(false);
|
||||
expect(equivalent('/// <reference path="a.d.ts" />\n', "\n")).toBe(false);
|
||||
// Every comment is kept in JavaScript, whose types come from JSDoc.
|
||||
expect(equivalent("let a; // one\n", "let a; // two\n", "/src/a.js")).toBe(false);
|
||||
});
|
||||
|
||||
test("text the scanner cannot classify is never equivalent", () => {
|
||||
expect(equivalent("x = {} / 2; // a\n", "x = {} / 2; // b\n")).toBe(false);
|
||||
expect(equivalent("++/a/.lastIndex; // a\n", "++/a/.lastIndex; // b\n")).toBe(false);
|
||||
expect(equivalent("const \\u0061 = 1; // a\n", "const \\u0061 = 1; // b\n")).toBe(false);
|
||||
expect(equivalent("const a = <b/>; // a\n", "const a = <b/>; // b\n", "/src/a.tsx")).toBe(false);
|
||||
expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,391 @@
|
||||
/* Token equivalence of two versions of a source file, for frontend cache
|
||||
* reuse after edits that touch only comments and whitespace.
|
||||
*
|
||||
* Two versions are equivalent when they have the same tokens, in the same
|
||||
* order, with line breaks between the same pairs of tokens (automatic
|
||||
* semicolon insertion depends on them), and differ only in whitespace and
|
||||
* in comments that cannot affect compilation. The scanner answers "not
|
||||
* equivalent" whenever it cannot classify text with certainty, so a wrong
|
||||
* answer can only cost a cache hit:
|
||||
*
|
||||
* - Every comment in a file that is not TypeScript (`.ts`, `.mts`, `.cts`)
|
||||
* is kept as a token: JavaScript types come from JSDoc. TypeScript
|
||||
* comments are kept when they start with `///` (references and
|
||||
* directives) or contain `@` or `#` (checker directives, JSDoc tags, and
|
||||
* annotations lowering reads).
|
||||
* - A `/` starts a regular expression or divides according to the token
|
||||
* before it. After `)` it divides unless the parenthesis closes an `if`,
|
||||
* `while`, `for`, `for await`, or `with` header. After `}`, `++` or
|
||||
* `--` the scanner cannot tell which, so it gives up, as it does for
|
||||
* escaped identifiers, which can spell keywords.
|
||||
* - A `/` with a line break (or a comment that spans lines) since the
|
||||
* previous token gives up: whether automatic semicolon insertion ends the
|
||||
* statement before it decides how it reads.
|
||||
* - Template literals track their `${` substitutions on a brace stack.
|
||||
* - Any code unit outside ASCII gives up, except inside strings, templates
|
||||
* and comments. So do files that contain whitespace or a line terminator
|
||||
* outside ASCII anywhere (U+2028 and U+2029 end a line comment), JSX
|
||||
* files, and unterminated strings, comments, templates and regular
|
||||
* expressions.
|
||||
*
|
||||
* The scanner models only what these rules name; anything else makes the
|
||||
* versions not equivalent, which rebuilds. */
|
||||
|
||||
interface Token {
|
||||
text: string;
|
||||
comment: boolean;
|
||||
lineBreakBefore: boolean;
|
||||
}
|
||||
|
||||
/** Keywords after which `/` starts a regular expression. */
|
||||
const EXPRESSION_KEYWORDS = new Set([
|
||||
"return",
|
||||
"typeof",
|
||||
"instanceof",
|
||||
"in",
|
||||
"of",
|
||||
"new",
|
||||
"delete",
|
||||
"void",
|
||||
"throw",
|
||||
"case",
|
||||
"do",
|
||||
"else",
|
||||
"yield",
|
||||
"await",
|
||||
"default",
|
||||
"extends",
|
||||
]);
|
||||
|
||||
/** Whitespace and line terminators outside ASCII. */
|
||||
const NON_ASCII_SPACE = /[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/;
|
||||
|
||||
/** Keywords whose parenthesized header is followed by a statement. */
|
||||
const HEADER_KEYWORDS = new Set(["if", "while", "for", "with"]);
|
||||
|
||||
const PUNCTUATORS = [
|
||||
">>>=",
|
||||
"...",
|
||||
"===",
|
||||
"!==",
|
||||
"**=",
|
||||
"<<=",
|
||||
">>=",
|
||||
">>>",
|
||||
"&&=",
|
||||
"||=",
|
||||
"??=",
|
||||
"=>",
|
||||
"==",
|
||||
"!=",
|
||||
"<=",
|
||||
">=",
|
||||
"&&",
|
||||
"||",
|
||||
"??",
|
||||
"?.",
|
||||
"++",
|
||||
"--",
|
||||
"+=",
|
||||
"-=",
|
||||
"*=",
|
||||
"%=",
|
||||
"&=",
|
||||
"|=",
|
||||
"^=",
|
||||
"**",
|
||||
"<<",
|
||||
">>",
|
||||
];
|
||||
|
||||
function isTypeScript(path: string): boolean {
|
||||
return path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts");
|
||||
}
|
||||
|
||||
function isIdentifierCode(code: number): boolean {
|
||||
return (
|
||||
(code >= 97 && code <= 122) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 48 && code <= 57) ||
|
||||
code === 36 ||
|
||||
code === 95 ||
|
||||
code === 92
|
||||
);
|
||||
}
|
||||
|
||||
function isLineBreak(code: number): boolean {
|
||||
return code === 10 || code === 13;
|
||||
}
|
||||
|
||||
function isSpace(code: number): boolean {
|
||||
return code === 32 || code === 9 || code === 11 || code === 12 || isLineBreak(code);
|
||||
}
|
||||
|
||||
/** The tokens of `source`, or null when the scanner cannot classify it. */
|
||||
function scan(path: string, source: string): Token[] | null {
|
||||
if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null;
|
||||
if (NON_ASCII_SPACE.test(source)) return null;
|
||||
const typeScript = isTypeScript(path);
|
||||
const tokens: Token[] = [];
|
||||
// Brace kinds: true for a template substitution, false otherwise.
|
||||
const braces: boolean[] = [];
|
||||
// Paren kinds: true for a statement header.
|
||||
const parens: boolean[] = [];
|
||||
let lineBreakBefore = false;
|
||||
// A line break, or a comment spanning lines, since the last token other
|
||||
// than a comment.
|
||||
let breakSinceCode = false;
|
||||
// The previous token and the one before it, and whether a `)` closed a
|
||||
// statement header.
|
||||
const last: { token: string | null; before: string | null } = { token: null, before: null };
|
||||
let closedHeader = false;
|
||||
const push = (text: string, comment = false): void => {
|
||||
tokens.push({ text, comment, lineBreakBefore });
|
||||
lineBreakBefore = false;
|
||||
if (!comment) {
|
||||
breakSinceCode = false;
|
||||
last.before = last.token;
|
||||
last.token = text;
|
||||
}
|
||||
};
|
||||
let index = 0;
|
||||
const length = source.length;
|
||||
// A shebang is one token.
|
||||
if (source.startsWith("#!")) {
|
||||
while (index < length && !isLineBreak(source.charCodeAt(index))) index++;
|
||||
push(source.slice(0, index));
|
||||
}
|
||||
/** Scans template text from `index` (after a backtick or a closing
|
||||
* substitution brace) through the closing backtick or `${`. */
|
||||
const template = (start: number): boolean => {
|
||||
let position = index;
|
||||
while (position < length) {
|
||||
const code = source.charCodeAt(position);
|
||||
if (code === 92) {
|
||||
position += 2;
|
||||
continue;
|
||||
}
|
||||
if (code === 96) {
|
||||
position++;
|
||||
index = position;
|
||||
push(source.slice(start, position));
|
||||
closedHeader = false;
|
||||
return true;
|
||||
}
|
||||
if (code === 36 && source.charCodeAt(position + 1) === 123) {
|
||||
position += 2;
|
||||
index = position;
|
||||
push(source.slice(start, position));
|
||||
braces.push(true);
|
||||
last.before = last.token;
|
||||
last.token = "${";
|
||||
return true;
|
||||
}
|
||||
position++;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (index < length) {
|
||||
const code = source.charCodeAt(index);
|
||||
if (isSpace(code)) {
|
||||
if (isLineBreak(code)) lineBreakBefore = breakSinceCode = true;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
// Outside strings, templates and comments, the scanner models ASCII only.
|
||||
if (code > 127) return null;
|
||||
const next = source.charCodeAt(index + 1);
|
||||
// Comments.
|
||||
if (code === 47 && (next === 47 || next === 42)) {
|
||||
const start = index;
|
||||
if (next === 47) {
|
||||
while (index < length && !isLineBreak(source.charCodeAt(index))) index++;
|
||||
} else {
|
||||
const end = source.indexOf("*/", index + 2);
|
||||
if (end < 0) return null;
|
||||
index = end + 2;
|
||||
}
|
||||
const text = source.slice(start, index);
|
||||
const semantic =
|
||||
!typeScript || text.startsWith("///") || text.includes("@") || text.includes("#");
|
||||
const multiline = /[\r\n]/.test(text);
|
||||
if (multiline) breakSinceCode = true;
|
||||
if (semantic) push(text, true);
|
||||
else if (multiline) lineBreakBefore = true;
|
||||
continue;
|
||||
}
|
||||
// Strings.
|
||||
if (code === 34 || code === 39) {
|
||||
const start = index;
|
||||
index++;
|
||||
while (index < length) {
|
||||
const inner = source.charCodeAt(index);
|
||||
if (inner === 92) {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (isLineBreak(inner)) return null;
|
||||
index++;
|
||||
if (inner === code) break;
|
||||
if (index >= length) return null;
|
||||
}
|
||||
if (source.charCodeAt(index - 1) !== code || index - start < 2) return null;
|
||||
push(source.slice(start, index));
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
// Templates.
|
||||
if (code === 96) {
|
||||
const start = index;
|
||||
index++;
|
||||
if (!template(start)) return null;
|
||||
continue;
|
||||
}
|
||||
// Identifiers, keywords and numbers.
|
||||
if (isIdentifierCode(code) || (code === 46 && next >= 48 && next <= 57)) {
|
||||
const start = index;
|
||||
index++;
|
||||
while (index < length) {
|
||||
const inner = source.charCodeAt(index);
|
||||
if (isIdentifierCode(inner) || inner === 46) {
|
||||
// A dot continues only a number (`1.5`), not member access.
|
||||
if (inner === 46) {
|
||||
const first = source.charCodeAt(start);
|
||||
if (!((first >= 48 && first <= 57) || first === 46)) break;
|
||||
}
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
// Exponent signs (`1e-5`).
|
||||
if (
|
||||
(inner === 43 || inner === 45) &&
|
||||
(source.charCodeAt(index - 1) === 101 || source.charCodeAt(index - 1) === 69)
|
||||
) {
|
||||
const first = source.charCodeAt(start);
|
||||
if ((first >= 48 && first <= 57) || first === 46) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
const word = source.slice(start, index);
|
||||
// An escaped identifier can spell a keyword (`\u0072eturn`).
|
||||
if (word.includes("\\")) return null;
|
||||
push(word);
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
// Division or a regular expression.
|
||||
if (code === 47) {
|
||||
if (breakSinceCode) return null;
|
||||
const before = last.token;
|
||||
let regex: boolean;
|
||||
if (before === null) regex = true;
|
||||
else if (before === "}") return null;
|
||||
else if (before === ")") regex = closedHeader;
|
||||
// `++/re/.lastIndex` is valid, `x++ / 2` too.
|
||||
else if (before === "++" || before === "--") return null;
|
||||
else if (before === "]") regex = false;
|
||||
else if (/^[A-Za-z0-9_$\\]/.test(before)) regex = EXPRESSION_KEYWORDS.has(before);
|
||||
else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`"))
|
||||
regex = false;
|
||||
else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false;
|
||||
else regex = true;
|
||||
if (regex) {
|
||||
const start = index;
|
||||
index++;
|
||||
let inClass = false;
|
||||
while (index < length) {
|
||||
const inner = source.charCodeAt(index);
|
||||
if (isLineBreak(inner) || inner > 127) return null;
|
||||
if (inner === 92) {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (inner === 91) inClass = true;
|
||||
else if (inner === 93) inClass = false;
|
||||
else if (inner === 47 && !inClass) break;
|
||||
index++;
|
||||
}
|
||||
if (index >= length) return null;
|
||||
index++;
|
||||
while (index < length && isIdentifierCode(source.charCodeAt(index))) index++;
|
||||
push(source.slice(start, index));
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
const text = next === 61 ? "/=" : "/";
|
||||
index += text.length;
|
||||
push(text);
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
// Brackets.
|
||||
if (code === 40) {
|
||||
parens.push(
|
||||
last.token !== null &&
|
||||
(HEADER_KEYWORDS.has(last.token) || (last.token === "await" && last.before === "for")),
|
||||
);
|
||||
index++;
|
||||
push("(");
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
if (code === 41) {
|
||||
const header = parens.pop() ?? false;
|
||||
index++;
|
||||
push(")");
|
||||
closedHeader = header;
|
||||
continue;
|
||||
}
|
||||
if (code === 123) {
|
||||
braces.push(false);
|
||||
index++;
|
||||
push("{");
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
if (code === 125) {
|
||||
if (braces.pop() === true) {
|
||||
const start = index;
|
||||
index++;
|
||||
if (!template(start)) return null;
|
||||
continue;
|
||||
}
|
||||
index++;
|
||||
push("}");
|
||||
closedHeader = false;
|
||||
continue;
|
||||
}
|
||||
// Other punctuators.
|
||||
let text = source[index]!;
|
||||
for (const punctuator of PUNCTUATORS)
|
||||
if (source.startsWith(punctuator, index)) {
|
||||
text = punctuator;
|
||||
break;
|
||||
}
|
||||
index += text.length;
|
||||
push(text);
|
||||
closedHeader = false;
|
||||
}
|
||||
if (braces.length !== 0 || parens.length !== 0) return null;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** Whether `current` differs from `previous` only in whitespace and in
|
||||
* comments that cannot affect compilation. */
|
||||
export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
|
||||
if (previous === current) return true;
|
||||
const left = scan(path, previous);
|
||||
if (left === null) return false;
|
||||
const right = scan(path, current);
|
||||
if (right === null || left.length !== right.length) return false;
|
||||
for (let index = 0; index < left.length; index++) {
|
||||
const a = left[index]!;
|
||||
const b = right[index]!;
|
||||
if (a.text !== b.text || a.comment !== b.comment) return false;
|
||||
if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -17,6 +17,16 @@ import {
|
||||
import type { SourceFile } from "./ast-types.js";
|
||||
import { SyntaxKind, NodeFlags } from "./enums.js";
|
||||
|
||||
/** Receives the file name of a source file whose raw text or line
|
||||
* positions a reader takes: the text of a node other than a token
|
||||
* (getText), a node's text with its trivia (getFullText), or line starts. */
|
||||
let sourceTextObserver: ((fileName: string) => void) | undefined;
|
||||
|
||||
/** Installs the observer of raw source text reads (sourceTextObserver). */
|
||||
export function observeSourceText(observer: ((fileName: string) => void) | undefined): void {
|
||||
sourceTextObserver = observer;
|
||||
}
|
||||
|
||||
/** A list of child nodes with the list's own position, the shape of tsc-ts's
|
||||
* NodeList. The file keeps the nodes array, not this object: each read of a
|
||||
* list field returns a new AstNodeList over the same array, so `nodes` keeps
|
||||
@@ -210,6 +220,12 @@ export class AstFile {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Reports a read of this file's raw text or line positions. */
|
||||
noteSourceText(): void {
|
||||
if (sourceTextObserver !== undefined && this.root.kind === SyntaxKind.SourceFile)
|
||||
sourceTextObserver(this.root.fileName);
|
||||
}
|
||||
|
||||
/** The file name of the source file node at `index`; the root's is
|
||||
* read once. */
|
||||
sourceFileName(index: number): string {
|
||||
@@ -220,6 +236,7 @@ export class AstFile {
|
||||
}
|
||||
|
||||
lineStarts(): number[] {
|
||||
this.noteSourceText();
|
||||
if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? "");
|
||||
return this.lines;
|
||||
}
|
||||
@@ -495,10 +512,14 @@ export class AstNode {
|
||||
return this.getStart(sourceFile) - this.pos;
|
||||
}
|
||||
getFullText(sourceFile?: AstNode): string {
|
||||
return ((sourceFile ?? this.file.root).text ?? "").substring(this.pos, this.end);
|
||||
const source = sourceFile ?? this.file.root;
|
||||
source.file.noteSourceText();
|
||||
return (source.text ?? "").substring(this.pos, this.end);
|
||||
}
|
||||
getText(sourceFile?: AstNode): string {
|
||||
const source = sourceFile ?? this.file.root;
|
||||
// A token's text is the same in every token-equivalent version.
|
||||
if (this.kind > SyntaxKind.LastToken) source.file.noteSourceText();
|
||||
return (source.text ?? "").substring(this.getStart(source), this.end);
|
||||
}
|
||||
|
||||
|
||||
@@ -3207,6 +3207,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// Native WeakMap keeps reference identities in checked-dynamic storage.
|
||||
// The runtime rejects reference kinds without a weak lifetime contract.
|
||||
if (isStdlibInterface("WeakMap") || isStdlibInterface("WeakSet")) return DYN;
|
||||
// FinalizationRegistry is a native handle with register and unregister.
|
||||
if (isStdlibInterface("FinalizationRegistry")) return DYN;
|
||||
// Set<T>: Map's sibling — same provenance rule and key domain.
|
||||
// Anything else stays unmapped;
|
||||
// the `new Set` lowering names the offending element type specifically.
|
||||
@@ -7748,6 +7750,22 @@ function wideningAddedProperties(
|
||||
return added;
|
||||
}
|
||||
|
||||
/** Whether `type` is an interface one of whose `extends` clauses names a
|
||||
* type the checker answers `any` (or an error type) for. */
|
||||
function interfaceExtendsAny(type: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Interface) === 0) return false;
|
||||
for (const decl of symbol.declarations) {
|
||||
if (!ts.isInterfaceDeclaration(decl)) continue;
|
||||
for (const clause of decl.heritageClauses?.nodes ?? []) {
|
||||
for (const base of clause.types.nodes) {
|
||||
if ((checker.getTypeAtLocation(base).flags & ts.TypeFlags.Any) !== 0) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mapRecordTypeInner(
|
||||
widened: ts.Type,
|
||||
ctx: TypeMapperCtx,
|
||||
@@ -7763,6 +7781,13 @@ function mapRecordTypeInner(
|
||||
// names the construct.
|
||||
const widenedSym = widened.getSymbol();
|
||||
if (widenedSym !== undefined && (widenedSym.flags & ts.SymbolFlags.Enum) !== 0) return null;
|
||||
// An interface extending a type the checker answers `any` for (in the
|
||||
// compiled world, a package type computed from members only its .d.ts
|
||||
// declares, such as `interface P extends z.infer<typeof S>`) has members
|
||||
// the checker cannot list: its own declared fields are not the whole
|
||||
// object. The value is checked-dynamic, never a record of the declared
|
||||
// fields alone.
|
||||
if (interfaceExtendsAny(widened, checker)) return DYN;
|
||||
// An index signature maps to a hybrid shape: declared fields keep
|
||||
// struct slots, undeclared keys ride the shape's overflow map, valued
|
||||
// uniformly by the signature's value type (`unknown` → dyn — a model-pricing table's
|
||||
|
||||
@@ -94,6 +94,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"stream.unpipeDyn",
|
||||
"weakMap.new",
|
||||
"weakSet.new",
|
||||
// Node's "cleanup must be callable" TypeError.
|
||||
"finalizationRegistry.new",
|
||||
"dyn.fromEntries",
|
||||
"bytes.constructor",
|
||||
"bytes.instanceOf",
|
||||
|
||||
@@ -3617,6 +3617,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"weakSet.is": { argTypes: [DYN], result: BOOL },
|
||||
"weakMap.new": { argTypes: [DYN], result: DYN },
|
||||
"weakSet.new": { argTypes: [DYN], result: DYN },
|
||||
"finalizationRegistry.new": { argTypes: [DYN], result: DYN },
|
||||
"finalizationRegistry.is": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.fromEntries": { argTypes: [DYN], result: DYN },
|
||||
"bytes.constructor": { argTypes: [STRING], result: DYN },
|
||||
"bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL },
|
||||
@@ -3811,6 +3813,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
// Whether the array (args[0]) still has the key (args[1]) from its
|
||||
// namedKeys list. Borrowed; never throws.
|
||||
"array.hasNamedKey": { argTypes: [null, STRING], result: BOOL },
|
||||
// Whether an array (args[0], any element type) has properties besides its
|
||||
// elements: noncanonical numeric keys, match metadata or named
|
||||
// properties. Borrowed; never throws.
|
||||
"array.hasProperties": { argTypes: [null], result: BOOL },
|
||||
// The key of an element access whose index is undefined (the property
|
||||
// named "undefined"; frontend/lowering/undefined-index.ts).
|
||||
"array.undefinedKey": { argTypes: [], result: F64 },
|
||||
// node:events EventEmitter: receivers are emitter-hierarchy objects and
|
||||
// the chaining forms (on/off/removeAll/setMax) return the receiver's
|
||||
// own class — program-dependent object types, checked in the libCall
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { BOOL, F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
type CharAt = IrExpr & { kind: "strIntrinsic" };
|
||||
@@ -140,14 +148,23 @@ function rewriteFunction(fn: IrFunction): IrFunction {
|
||||
return {
|
||||
...fn,
|
||||
body: fn.body.map(rewriteStmt),
|
||||
locals: fn.locals.map((l) => (held.has(l.id) ? { ...l, type: F64 } : l)),
|
||||
locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)),
|
||||
};
|
||||
}
|
||||
|
||||
/** The local, holding the code unit instead of the one-character string. */
|
||||
function unitLocal(local: IrLocal): IrLocal {
|
||||
return { ...local, type: F64 };
|
||||
}
|
||||
|
||||
/** The local a node mentions in a form the rewrite handles: the
|
||||
* declaration from a one-argument charAt, a one-unit test, or a switch
|
||||
* over one-unit cases. */
|
||||
function understoodMention(node: Record<string, unknown>): string | null {
|
||||
// count() visits every object in the body (types, locations, cases), so
|
||||
// only the node kinds handled here are read as IR nodes
|
||||
const kind = node.kind;
|
||||
if (kind !== "varDecl" && kind !== "strEq" && kind !== "strCmp" && kind !== "switch") return null;
|
||||
const n = node as unknown as IrExpr | IrStmt;
|
||||
if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null;
|
||||
if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null;
|
||||
|
||||
@@ -208,6 +208,7 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"contracts.int32Returns[]": "fact",
|
||||
"contracts.int32Locals[]": "other",
|
||||
"contracts.int32Elements[]": "other",
|
||||
"int32ArrayCandidates[]": "other",
|
||||
"contracts.noThrow[]": "fact",
|
||||
"contracts.acyclic[]": "other",
|
||||
"contracts.immortalGlobals[]": "other",
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { reparseExpr } from "./serialize.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
transformExpr,
|
||||
transformStmt,
|
||||
transformStmtList,
|
||||
type IrTransform,
|
||||
} from "./traverse.js";
|
||||
|
||||
/** Local number arrays the backend may store as int32 slots.
|
||||
*
|
||||
* A candidate is a local `number[]` of a synchronous function that no
|
||||
* other binding can hold and only compiled element operations reach:
|
||||
* every write of the local stores a fresh array (an array literal, or
|
||||
* `new Array(n).fill(v)`), and every read of it is the receiver of an
|
||||
* element read, write, state test, `length`, `push` or `pop` (a discarded
|
||||
* `pop` included), or the array of an element-reader helper call. The
|
||||
* array never reaches a runtime function that reads its slots, a library
|
||||
* call, a closure, a return or untyped code.
|
||||
*
|
||||
* Candidates are only proposed here. The int32 slot analysis
|
||||
* (int32-slots.ts) keeps the ones whose every stored element is a proven
|
||||
* int32 (never -0), assuming the candidates' own elements are int32s, at
|
||||
* a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. It keeps
|
||||
* the ones whose stores are exact integers but not all proven int32s as
|
||||
* int32 arrays with checked stores: SCR_ELEM_I32 until a store that is not
|
||||
* an int32 widens them to doubles in place. Their element-reader helper
|
||||
* calls are inlined first (rewriteCandidates), so no shared helper ever
|
||||
* receives an int32 array. */
|
||||
|
||||
/** Array methods a candidate may be the receiver of: the runtime paths
|
||||
* behind them read and write int32 slots through the double-valued slot
|
||||
* accessors. */
|
||||
export const INT32_ARRAY_METHODS: ReadonlySet<string> = new Set([
|
||||
"length",
|
||||
"push",
|
||||
"pop",
|
||||
"getNumber",
|
||||
]);
|
||||
|
||||
/** An element-reader helper (`%arr.idxOr.*`): `(a, i) => a's state at i is
|
||||
* a value ? the element : undefined`, as one returned ternary. */
|
||||
export function isElementReader(fn: IrFunction): boolean {
|
||||
if (fn.params.length !== 2 || fn.body.length !== 1) return false;
|
||||
const [array, index] = fn.params;
|
||||
const ret = fn.body[0]!;
|
||||
if (array!.type.kind !== "array" || array!.type.elem.kind !== "f64") return false;
|
||||
if (index!.type.kind !== "f64" || ret.kind !== "return" || ret.value?.kind !== "ternary")
|
||||
return false;
|
||||
const isParam = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id;
|
||||
const { cond, then, else_ } = ret.value;
|
||||
return (
|
||||
cond.kind === "bin" &&
|
||||
cond.op === "===" &&
|
||||
cond.left.kind === "arrayState" &&
|
||||
isParam(cond.left.arr, array!.localId) &&
|
||||
isParam(cond.left.index, index!.localId) &&
|
||||
cond.right.kind === "numLit" &&
|
||||
cond.right.value === 1 &&
|
||||
then.kind === "unionWrap" &&
|
||||
then.value.kind === "arrayGet" &&
|
||||
isParam(then.value.arr, array!.localId) &&
|
||||
isParam(then.value.index, index!.localId) &&
|
||||
else_.kind === "unionWrap" &&
|
||||
else_.value.kind === "unitLit"
|
||||
);
|
||||
}
|
||||
|
||||
/** `new Array(n).fill(v)` as lowering spells it: `fill` over a sequence
|
||||
* that declares a temporary empty array, sets its length and yields it. */
|
||||
function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null {
|
||||
if (e.kind !== "arrIntrinsic" || e.method !== "fill" || e.receiver.kind !== "seqExpr")
|
||||
return null;
|
||||
const seq = e.receiver;
|
||||
const [decl, length] = seq.stmts;
|
||||
if (
|
||||
seq.stmts.length !== 2 ||
|
||||
decl?.kind !== "varDecl" ||
|
||||
decl.init?.kind !== "arrayLit" ||
|
||||
decl.init.elems.length !== 0 ||
|
||||
length?.kind !== "arraySetLength" ||
|
||||
length.arr.kind !== "varRef" ||
|
||||
length.arr.localId !== decl.localId ||
|
||||
seq.result.kind !== "varRef" ||
|
||||
seq.result.localId !== decl.localId
|
||||
)
|
||||
return null;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Fill-form arrays shorter than this hold doubles even when they are int32
|
||||
* arrays with checked stores: their doubles fit a typical 32 KiB L1 data
|
||||
* cache, so 4-byte slots save no memory traffic. */
|
||||
export const CHECKED_INT32_MIN_LENGTH = 4096;
|
||||
|
||||
/** The length `n` of `new Array(n).fill(v)`, or null for any other
|
||||
* allocation. */
|
||||
export function fillAllocationLength(e: IrExpr): IrExpr | null {
|
||||
const fill = fillAllocation(e);
|
||||
if (!fill || fill.receiver.kind !== "seqExpr") return null;
|
||||
const set = fill.receiver.stmts[1]!;
|
||||
return set.kind === "arraySetLength" ? set.length : null;
|
||||
}
|
||||
|
||||
/** A number expression whose evaluation has no effect: locals, literals and
|
||||
* arithmetic. */
|
||||
function isPureLength(e: IrExpr): boolean {
|
||||
if (e.type.kind !== "f64") return false;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
case "varRef":
|
||||
return true;
|
||||
case "bin":
|
||||
return e.left.type.kind === "f64" && isPureLength(e.left) && isPureLength(e.right);
|
||||
case "unary":
|
||||
return isPureLength(e.operand);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the candidate `id` of `fn` may hold int32 slots with checked
|
||||
* stores: every array it is assigned is the fill form, with a length that
|
||||
* may reach CHECKED_INT32_MIN_LENGTH and evaluates without effects (the
|
||||
* backend evaluates it once more to pick the new array's kind). */
|
||||
export function checkableInt32Allocations(fn: IrFunction, id: string): boolean {
|
||||
let ok = true;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if ((s.kind === "varDecl" || s.kind === "assign") && s.localId === id) {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const length = value ? fillAllocationLength(value) : null;
|
||||
if (
|
||||
length === null ||
|
||||
!isPureLength(length) ||
|
||||
(length.kind === "numLit" && length.value < CHECKED_INT32_MIN_LENGTH)
|
||||
)
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
},
|
||||
expr: () => ok,
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** A fresh number array: a literal without spreads, or the fill form. */
|
||||
export function int32ArrayAllocation(e: IrExpr): boolean {
|
||||
return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null;
|
||||
}
|
||||
|
||||
/** The array literal a candidate's allocation builds (the literal itself,
|
||||
* or the fill form's temporary's), and the fill form's temporary. */
|
||||
export function allocatedLiteral(
|
||||
e: IrExpr,
|
||||
): { literal: IrExpr & { kind: "arrayLit" }; temporary: string | null } | null {
|
||||
if (e.kind === "arrayLit") return { literal: e, temporary: null };
|
||||
const fill = fillAllocation(e);
|
||||
if (!fill || fill.receiver.kind !== "seqExpr") return null;
|
||||
const decl = fill.receiver.stmts[0]!;
|
||||
return decl.kind === "varDecl" && decl.init?.kind === "arrayLit"
|
||||
? { literal: decl.init, temporary: decl.localId }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `xs.pop()` whose value is unused, as lowering spells it: a sequence
|
||||
* that binds the array to a hidden local, then only reads that local's
|
||||
* length and truncates it with pop stores. Returns the hidden local and the
|
||||
* array read it binds. */
|
||||
function discardedPop(e: IrExpr): { local: string; array: IrExpr & { kind: "varRef" } } | null {
|
||||
if (e.kind !== "seqExpr") return null;
|
||||
const [bind, ...rest] = e.stmts;
|
||||
if (bind?.kind !== "varDecl" || bind.init?.kind !== "varRef") return null;
|
||||
const local = bind.localId;
|
||||
const isLocal = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === local;
|
||||
const expr = (x: IrExpr): boolean => {
|
||||
if (x.kind === "arrIntrinsic" && x.method === "length" && isLocal(x.receiver)) return true;
|
||||
return !isLocal(x) && everyExprChild(x, expr, stmt);
|
||||
};
|
||||
const stmt = (x: IrStmt): boolean => {
|
||||
if (x.kind === "arraySetLength" && x.pop === true && isLocal(x.arr)) return expr(x.length);
|
||||
return (
|
||||
!((x.kind === "varDecl" || x.kind === "assign") && x.localId === local) &&
|
||||
everyStmtChild(x, expr, stmt)
|
||||
);
|
||||
};
|
||||
return rest.every(stmt) && expr(e.result) ? { local, array: bind.init } : null;
|
||||
}
|
||||
|
||||
/** Whether evaluating `e` may assign the local `id`. */
|
||||
function writesLocal(e: IrExpr, id: string): boolean {
|
||||
const expr = (x: IrExpr): boolean =>
|
||||
!(x.kind === "assignExpr" && x.localId === id) && everyExprChild(x, expr, stmt);
|
||||
const stmt = (x: IrStmt): boolean =>
|
||||
!((x.kind === "varDecl" || x.kind === "assign") && x.localId === id) &&
|
||||
everyStmtChild(x, expr, stmt);
|
||||
return !expr(e);
|
||||
}
|
||||
|
||||
/** The candidate locals of `fn` (see the module comment). */
|
||||
export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet<string>): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
const params = new Set(fn.params.map((p) => p.localId));
|
||||
for (const local of fn.locals)
|
||||
if (
|
||||
local.type.kind === "array" &&
|
||||
local.type.elem.kind === "f64" &&
|
||||
!local.boxed &&
|
||||
!local.tdz &&
|
||||
!captured.has(local.id) &&
|
||||
!params.has(local.id)
|
||||
)
|
||||
result.add(local.id);
|
||||
if (result.size === 0) return result;
|
||||
const written = new Set<string>();
|
||||
// A receiver position: the candidate is read there without escaping.
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
result.delete(e.localId);
|
||||
return true;
|
||||
case "assignExpr":
|
||||
result.delete(e.localId);
|
||||
break;
|
||||
case "arrayGet":
|
||||
case "arrayState":
|
||||
case "arrayHas":
|
||||
receiver(e.arr);
|
||||
expr(e.index);
|
||||
return true;
|
||||
case "arrIntrinsic":
|
||||
if (!INT32_ARRAY_METHODS.has(e.method)) break;
|
||||
receiver(e.receiver);
|
||||
e.args.forEach(expr);
|
||||
return true;
|
||||
case "seqExpr": {
|
||||
// The pop's hidden local is the array itself; every use of it is a
|
||||
// length read or a pop store.
|
||||
const pop = discardedPop(e);
|
||||
if (!pop) break;
|
||||
e.stmts.slice(1).every(stmt);
|
||||
expr(e.result);
|
||||
return true;
|
||||
}
|
||||
case "call": {
|
||||
if (!readers.has(e.callee) || e.args.length !== 2) break;
|
||||
const [array, index] = e.args;
|
||||
// Inlining evaluates the index before the array read.
|
||||
if (array!.kind === "varRef" && writesLocal(index!, array!.localId))
|
||||
result.delete(array!.localId);
|
||||
receiver(array!);
|
||||
expr(index!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value && int32ArrayAllocation(value)) written.add(s.localId);
|
||||
else if (value) result.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
}
|
||||
case "arraySet":
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
expr(s.value);
|
||||
return true;
|
||||
case "arraySetUndefined":
|
||||
// Stores a present undefined: a state, and a cleared (+0) slot.
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
return true;
|
||||
case "forOf":
|
||||
result.delete(s.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
if (s.catchLocalId) result.delete(s.catchLocalId);
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const id of result) if (!written.has(id)) result.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The element values stored into each candidate: element writes, pushed
|
||||
* values, literal elements and fill values. */
|
||||
export function int32ArrayStores(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
): Map<string, IrExpr[]> {
|
||||
const stores = new Map<string, IrExpr[]>();
|
||||
const add = (id: string, value: IrExpr): void => {
|
||||
const list = stores.get(id);
|
||||
if (list) list.push(value);
|
||||
else stores.set(id, [value]);
|
||||
};
|
||||
const target = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && candidates.has(e.localId) ? e.localId : null;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "arrIntrinsic" && e.method === "push") {
|
||||
const id = target(e.receiver);
|
||||
if (id !== null) for (const arg of e.args) add(id, arg);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if ((s.kind === "varDecl" || s.kind === "assign") && candidates.has(s.localId)) {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (value?.kind === "arrayLit") for (const el of value.elems) add(s.localId, el);
|
||||
else if (value) {
|
||||
const fill = fillAllocation(value);
|
||||
if (fill) add(s.localId, fill.args[0]!);
|
||||
}
|
||||
}
|
||||
if (s.kind === "arraySet") {
|
||||
const id = target(s.arr);
|
||||
if (id !== null) add(id, s.value);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
return stores;
|
||||
}
|
||||
|
||||
/** Rewrite a function's candidates for the backend. A discarded pop uses
|
||||
* the candidate in place of its hidden local. An element-reader helper call
|
||||
* whose array is a candidate is inlined: the helper's returned ternary, with
|
||||
* the candidate for its array and the index for its index, so the read means
|
||||
* the same. An index that is not a plain read or literal is bound to a fresh
|
||||
* local first: in its own declaration just before the statement when the
|
||||
* call is the whole value of a declaration or assignment (the form lowering
|
||||
* gives `a[i]!`, and the one the backend reads without a box), otherwise in a
|
||||
* sequence around the ternary. Selection keeps out indexes that write the
|
||||
* array, so evaluating the index before the array read is unobservable. */
|
||||
function rewriteCandidates(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
readers: ReadonlyMap<string, IrFunction>,
|
||||
): IrFunction {
|
||||
const locals = [...fn.locals];
|
||||
let fresh = 0;
|
||||
const bind = (index: IrExpr): { decl: IrStmt; ref: IrExpr } => {
|
||||
const id = `%int32.read.${fresh++}`;
|
||||
locals.push({ id, name: id, type: F64, mutable: false });
|
||||
return {
|
||||
decl: { kind: "varDecl", localId: id, init: index, loc: index.loc },
|
||||
ref: { kind: "varRef", localId: id, type: F64, loc: index.loc },
|
||||
};
|
||||
};
|
||||
const readerCall = (e: IrExpr | null): (IrExpr & { kind: "call" }) | null =>
|
||||
e?.kind === "call" &&
|
||||
e.args.length === 2 &&
|
||||
readers.has(e.callee) &&
|
||||
e.args[0]!.kind === "varRef" &&
|
||||
candidates.has(e.args[0]!.localId)
|
||||
? e
|
||||
: null;
|
||||
const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit";
|
||||
// The statements of a list, with complex read indexes declared first.
|
||||
const hoist = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
if (s.kind !== "varDecl" && s.kind !== "assign") return [s];
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
const call = readerCall(value);
|
||||
if (!call || simple(call.args[1]!)) return [s];
|
||||
const { decl, ref } = bind(call.args[1]!);
|
||||
const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
|
||||
return [decl, s.kind === "varDecl" ? { ...s, init: read } : { ...s, value: read }];
|
||||
});
|
||||
const lists = (s: IrStmt): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "if":
|
||||
return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) };
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
return { ...s, body: hoist(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: hoist(c.body) })) };
|
||||
case "tryCatch":
|
||||
return {
|
||||
...s,
|
||||
tryBody: hoist(s.tryBody),
|
||||
catchBody: s.catchBody === null ? null : hoist(s.catchBody),
|
||||
finallyBody: s.finallyBody === null ? null : hoist(s.finallyBody),
|
||||
};
|
||||
}
|
||||
return s;
|
||||
};
|
||||
// Hidden pop locals replaced by their candidate array.
|
||||
const popLocals = new Set<string>();
|
||||
const rewrite = (e: IrExpr): IrExpr => {
|
||||
const pop = discardedPop(e);
|
||||
if (e.kind === "seqExpr" && pop && candidates.has(pop.array.localId)) {
|
||||
popLocals.add(pop.local);
|
||||
const array = pop.array;
|
||||
const substitute: IrTransform = {
|
||||
expr: (x) =>
|
||||
x.kind === "varRef" && x.localId === pop.local ? { ...array, loc: x.loc } : x,
|
||||
stmt: (x) => x,
|
||||
};
|
||||
return {
|
||||
...e,
|
||||
stmts: e.stmts.slice(1).map((x) => transformStmt(x, substitute)),
|
||||
result: transformExpr(e.result, substitute),
|
||||
};
|
||||
}
|
||||
const call = readerCall(e);
|
||||
if (!call) return e.kind === "seqExpr" ? { ...e, stmts: hoist(e.stmts) } : e;
|
||||
const helper = readers.get(call.callee)!;
|
||||
const array = call.args[0]!;
|
||||
const [arrayParam, indexParam] = helper.params;
|
||||
const ret = helper.body[0]!;
|
||||
if (ret.kind !== "return" || !ret.value) return e;
|
||||
let index = call.args[1]!;
|
||||
const stmts: IrStmt[] = [];
|
||||
if (!simple(index)) {
|
||||
const { decl, ref } = bind(index);
|
||||
stmts.push(decl);
|
||||
index = ref;
|
||||
}
|
||||
const operand = index;
|
||||
const body = transformExpr(reparseExpr(ret.value), {
|
||||
expr: (x) => {
|
||||
if (x.kind !== "varRef") return x;
|
||||
if (x.localId === arrayParam!.localId) return reparseExpr(array);
|
||||
if (x.localId === indexParam!.localId) return reparseExpr(operand);
|
||||
return x;
|
||||
},
|
||||
stmt: (x) => x,
|
||||
});
|
||||
const typed: IrExpr = { ...body, loc: e.loc };
|
||||
return stmts.length === 0
|
||||
? typed
|
||||
: { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc };
|
||||
};
|
||||
const body = transformStmtList(hoist(fn.body), { expr: rewrite, stmt: lists });
|
||||
if (fresh === 0 && popLocals.size === 0) return { ...fn, body };
|
||||
return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) };
|
||||
}
|
||||
|
||||
/** Propose the module's int32 array candidates (`function\0local` keys in
|
||||
* `int32ArrayCandidates`), only those in `keep` when given, and rewrite
|
||||
* their functions. */
|
||||
export function markInt32ArrayCandidates(
|
||||
mod: IrModule,
|
||||
keep: ReadonlySet<string> | null = null,
|
||||
): IrModule {
|
||||
if (mod.lib !== undefined) return mod;
|
||||
const readers = new Map<string, IrFunction>();
|
||||
for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn);
|
||||
const names = new Set(readers.keys());
|
||||
const keys: string[] = [];
|
||||
const functions = mod.functions.map((fn) => {
|
||||
const candidates = int32ArrayCandidates(fn, names);
|
||||
for (const id of candidates)
|
||||
if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`);
|
||||
else candidates.delete(id);
|
||||
return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers);
|
||||
});
|
||||
return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys };
|
||||
}
|
||||
|
||||
/** The module the backend emits: rewritten for exactly the candidates the
|
||||
* int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a
|
||||
* module without int32 arrays is `mod` itself. A dropped candidate's
|
||||
* rewrite is undone by marking again without it, until every candidate
|
||||
* marked is kept. */
|
||||
export function selectInt32Arrays(
|
||||
mod: IrModule,
|
||||
prove: (marked: IrModule) => ReadonlySet<string>,
|
||||
): IrModule {
|
||||
let keep: ReadonlySet<string> | null = null;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const marked = markInt32ArrayCandidates(mod, keep);
|
||||
if (marked === mod) return mod;
|
||||
const kept = prove(marked);
|
||||
const proposed = marked.int32ArrayCandidates ?? [];
|
||||
if (proposed.every((key) => kept.has(key))) return marked;
|
||||
keep = new Set(proposed.filter((key) => kept.has(key)));
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./integer-ranges.js";
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
|
||||
import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js";
|
||||
|
||||
/** The storage slot an array-valued expression reads, as an
|
||||
@@ -169,7 +170,10 @@ export class Int32Slots {
|
||||
this.int32Locals.has(`${fnName}\0${localId}`) || this.int32Locals.has(`\0${localId}`)
|
||||
? INT32_RANGE
|
||||
: null;
|
||||
if (this.int32Elements.size > 0) result.elements = (arr) => this.hasInt32Elements(fnName, arr);
|
||||
if (this.int32Elements.size > 0) {
|
||||
result.elements = (arr) => this.hasInt32Elements(fnName, arr);
|
||||
if (this.narrowedReads) result.narrowedReads = true;
|
||||
}
|
||||
const params = this.paramFacts.get(fnName);
|
||||
if (params)
|
||||
result.params =
|
||||
@@ -234,6 +238,28 @@ export class Int32Slots {
|
||||
addInt32Elements(key: string): void {
|
||||
this.int32Elements.add(key);
|
||||
}
|
||||
dropInt32Elements(key: string): boolean {
|
||||
return this.int32Elements.delete(key);
|
||||
}
|
||||
hasInt32ElementSlot(key: string): boolean {
|
||||
return this.int32Elements.has(key);
|
||||
}
|
||||
/** Whether the module has int32 array candidates (the narrowedReads
|
||||
* range fact). */
|
||||
narrowedReads = false;
|
||||
/** Local number arrays (`function\0local`) stored as int32 slots: the
|
||||
* module's int32 array candidates whose every stored element the
|
||||
* fixpoint proved an int32. */
|
||||
readonly int32Arrays = new Set<string>();
|
||||
/** Local number arrays that may hold int32 slots with checked stores: the
|
||||
* candidates whose element stores the fixpoint proved exact integers but
|
||||
* not int32s, allocated only by fills that may be long. A fill of at least
|
||||
* CHECKED_INT32_MIN_LENGTH elements creates one with int32 slots and a
|
||||
* shorter one with doubles. Their elements give no int32 facts; compiled
|
||||
* accesses test the array's kind (once per innermost loop where they can,
|
||||
* backend/llvm/int32-kind-loops.ts), and a store that is not an int32
|
||||
* widens it first (scr_arr_widen_i32). */
|
||||
readonly checkedInt32Arrays = new Set<string>();
|
||||
/** Whether every present element of the number array `arr` evaluates to
|
||||
* (read inside `fnName`) is an int32. */
|
||||
hasInt32Elements(fnName: string, arr: IrExpr): boolean {
|
||||
@@ -764,6 +790,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key);
|
||||
for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key);
|
||||
// Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32
|
||||
// elements until a store the fixpoint cannot prove drops one.
|
||||
const arrayCandidates = new Map<string, Set<string>>();
|
||||
slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0;
|
||||
for (const key of mod.int32ArrayCandidates ?? []) {
|
||||
const cut = key.indexOf("\0");
|
||||
const fnName = key.slice(0, cut);
|
||||
const set = arrayCandidates.get(fnName) ?? new Set<string>();
|
||||
set.add(key.slice(cut + 1));
|
||||
arrayCandidates.set(fnName, set);
|
||||
slots.addInt32Elements(`l:${key}`);
|
||||
}
|
||||
|
||||
/* ── candidate fields ── */
|
||||
const children = new Map<string, string[]>();
|
||||
@@ -1043,6 +1081,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
return false;
|
||||
};
|
||||
const hasCandidateWrite = (fn: IrFunction): boolean => {
|
||||
if (arrayCandidates.has(fn.name)) return true;
|
||||
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
|
||||
const returns = slots.hasReturn(fn.name);
|
||||
return !everyStmtList(fn.body, {
|
||||
@@ -1088,6 +1127,33 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
continue;
|
||||
}
|
||||
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
|
||||
// Every element stored into an int32 array candidate must be an int32
|
||||
// under the facts that assume the candidates' own elements are; a
|
||||
// failing store drops its array's element facts, and the function is
|
||||
// analyzed again without them. When every failing store is still an
|
||||
// exact integer under those facts (a counter, `a[i]! + 1`) and the array
|
||||
// is only ever allocated by a fill that may be long
|
||||
// (checkableInt32Allocations), it keeps int32 slots with checked stores
|
||||
// (checkedInt32Arrays), which widen it to doubles in place at the first
|
||||
// store that is not an int32.
|
||||
const arrays = arrayCandidates.get(fn.name);
|
||||
if (arrays) {
|
||||
let dropped = false;
|
||||
for (const [id, values] of int32ArrayStores(fn, arrays)) {
|
||||
const key = `l:${fn.name}\0${id}`;
|
||||
if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
|
||||
slots.dropInt32Elements(key);
|
||||
const checkable = (value: IrExpr): boolean => int32(value) || ranges.get(value) != null;
|
||||
if (values.every(checkable) && checkableInt32Allocations(fn, id))
|
||||
slots.checkedInt32Arrays.add(`${fn.name}\0${id}`);
|
||||
else reason(`array:${fn.name}\0${id}`, "an element store is not int32");
|
||||
dropped = true;
|
||||
}
|
||||
if (dropped && !queued.has(fn.name)) {
|
||||
queued.add(fn.name);
|
||||
queue.push(fn.name);
|
||||
}
|
||||
}
|
||||
// An int32 written into a slot still assumed non-negative: widen the
|
||||
// assumption when the value may be negative, and reanalyze its readers.
|
||||
const settle = (slot: string, e: IrExpr): void => {
|
||||
@@ -1156,6 +1222,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
}
|
||||
for (const proven of provenByFunction.values())
|
||||
for (const value of proven) slots.provenWrites.add(value);
|
||||
for (const [fnName, ids] of arrayCandidates)
|
||||
for (const id of ids)
|
||||
if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
|
||||
slots.int32Arrays.add(`${fnName}\0${id}`);
|
||||
const ranged = rangedParameters(slots, functions, retry, numeric, (callee) =>
|
||||
readers.get(`ret:${callee}`),
|
||||
);
|
||||
|
||||
@@ -220,6 +220,11 @@ export interface IntegerSlotFacts {
|
||||
/** Number arrays whose every present element is an int32 (Int32[]
|
||||
* contracts): an element read that found a value reads an int32. */
|
||||
elements?: (arr: IrExpr) => boolean;
|
||||
/** Element reads narrowed out of a union (`xs[i]!`, also when a union
|
||||
* local holds the read) keep the element's range, and a ternary arm that
|
||||
* always throws yields no value. Only modules with int32 array candidates
|
||||
* (ir/int32-arrays.ts) set it, so no other program's ranges change. */
|
||||
narrowedReads?: boolean;
|
||||
}
|
||||
|
||||
/** A local read, or a numeric local plus or minus an integer literal
|
||||
@@ -240,6 +245,50 @@ function offsetRef(e: IrExpr): { localId: string; offset: number } | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Library calls that always throw. */
|
||||
const THROWING_LIB = /^error\.\w*Throw$/;
|
||||
|
||||
function throws(e: IrExpr): boolean {
|
||||
return e.kind === "libCall" && THROWING_LIB.test(e.fn);
|
||||
}
|
||||
|
||||
/** The values written to each union-typed local of `fn` by declarations
|
||||
* and assignments; null for a local that another write reaches or that a
|
||||
* closure captures. */
|
||||
function unionLocalWrites(
|
||||
fn: IrFunction,
|
||||
captures: ReadonlySet<string>,
|
||||
): Map<string, IrExpr[] | null> {
|
||||
const unionLocals = new Set(
|
||||
fn.locals
|
||||
.filter((l) => l.type.kind === "union" && !l.boxed && !captures.has(l.id))
|
||||
.map((l) => l.id),
|
||||
);
|
||||
const result = new Map<string, IrExpr[] | null>();
|
||||
const add = (id: string, value: IrExpr | null): void => {
|
||||
if (!unionLocals.has(id)) return;
|
||||
const list = result.get(id);
|
||||
if (list === null) return;
|
||||
if (value === null) result.set(id, null);
|
||||
else if (list) list.push(value);
|
||||
else result.set(id, [value]);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
if (s.kind === "varDecl" && s.init) add(s.localId, s.init);
|
||||
else if (s.kind === "assign") add(s.localId, s.value);
|
||||
else if (s.kind === "forOf") add(s.localId, null);
|
||||
else if (s.kind === "tryCatch" && s.catchLocalId) add(s.catchLocalId, null);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr") add(e.localId, null);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Structured local dataflow. Calls cannot write uncaptured local slots,
|
||||
* but argument expressions can; unknown evaluation forms invalidate all
|
||||
* syntactically written slots before their children are inspected. Loops
|
||||
@@ -272,6 +321,10 @@ export function analyzeIntegerRangeFacts(
|
||||
.map((l) => l.id),
|
||||
);
|
||||
const loopLocals = new Map(fn.locals.filter((l) => !captures.has(l.id)).map((l) => [l.id, l]));
|
||||
// With narrowed reads, the values written to union locals (an element
|
||||
// read held before it is narrowed), or null for a local some other write
|
||||
// (assignExpr, for-of, catch, capture) reaches.
|
||||
const unionWrites = slots.narrowedReads ? unionLocalWrites(fn, captures) : null;
|
||||
// A captured binding has no local dataflow: only a proof that covers
|
||||
// every write of the shared box anywhere applies to its reads.
|
||||
const boxedRange = (localId: string): IntegerRange | null => {
|
||||
@@ -490,6 +543,45 @@ export function analyzeIntegerRangeFacts(
|
||||
return range;
|
||||
}
|
||||
|
||||
/** narrowedRange through a union local: every value written to it. */
|
||||
function narrowedValue(
|
||||
value: IrExpr,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): IntegerRange | null | "never" {
|
||||
if (value.kind !== "varRef") return narrowedRange(value, unionId, tag);
|
||||
const written = unionWrites?.get(value.localId);
|
||||
if (!written || written.length === 0) return null;
|
||||
let result: IntegerRange | null | "never" = "never";
|
||||
for (const w of written) {
|
||||
const arm = narrowedRange(w, unionId, tag);
|
||||
if (arm === "never") continue;
|
||||
if (arm === null) return null;
|
||||
result = result === "never" ? arm : union(result, arm);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** The range of the values of union tag `tag` that `value` (already
|
||||
* analyzed) can hold: "never" when no arm holds that tag. */
|
||||
function narrowedRange(
|
||||
value: IrExpr,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): IntegerRange | null | "never" {
|
||||
if (value.kind === "seqExpr") return narrowedRange(value.result, unionId, tag);
|
||||
if (value.kind === "ternary") {
|
||||
const yes = narrowedRange(value.then, unionId, tag),
|
||||
no = narrowedRange(value.else_, unionId, tag);
|
||||
if (yes === "never") return no;
|
||||
if (no === "never") return yes;
|
||||
return yes && no ? union(yes, no) : null;
|
||||
}
|
||||
if (value.kind === "unionWrap" && value.unionId === unionId) {
|
||||
if (value.tag !== tag) return "never";
|
||||
return value.value.kind === "arrayGet" ? (ranges.get(value.value) ?? null) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null {
|
||||
// Shared IR objects have to satisfy the proof at every occurrence.
|
||||
const previous = ranges.get(e);
|
||||
@@ -683,6 +775,10 @@ export function analyzeIntegerRangeFacts(
|
||||
right = expr(e.else_, no);
|
||||
merge(facts, yes, no);
|
||||
if (left && right) range = union(left, right);
|
||||
// With narrowed reads: an arm that always throws yields no value,
|
||||
// so the other arm's range is the result's.
|
||||
else if (slots.narrowedReads && throws(e.then)) range = right;
|
||||
else if (slots.narrowedReads && throws(e.else_)) range = left;
|
||||
break;
|
||||
}
|
||||
case "logical": {
|
||||
@@ -777,27 +873,45 @@ export function analyzeIntegerRangeFacts(
|
||||
expr(e.obj, facts);
|
||||
range = slotRange(e);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// Only modules with narrowed reads take this rule: the narrow
|
||||
// evaluates its operand first, and throws for every arm of another
|
||||
// tag, so an element read `xs[i]!` (a ternary of the wrapped
|
||||
// element and a wrapped undefined) has the element's range.
|
||||
if (!slots.narrowedReads || e.type.kind !== "f64") {
|
||||
range = conservative(e, facts);
|
||||
break;
|
||||
}
|
||||
expr(e.value, facts);
|
||||
{
|
||||
const arm = narrowedValue(e.value, e.unionId, e.tag);
|
||||
range = arm === "never" ? null : arm;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do not assume the traversal order is an evaluation order for an
|
||||
// unknown node. Every child gets the same conservative entry state.
|
||||
havoc(facts, [e], facts);
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => {
|
||||
expr(child, new Map(facts));
|
||||
return true;
|
||||
},
|
||||
(child) => {
|
||||
body([child], new Map(facts));
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// A direct call's result carries its whole-program return proof.
|
||||
range = slotRange(e);
|
||||
range = conservative(e, facts);
|
||||
break;
|
||||
}
|
||||
return remember(e, range);
|
||||
}
|
||||
/** An unknown node: do not assume the traversal order is an evaluation
|
||||
* order. Every child gets the same conservative entry state. */
|
||||
function conservative(e: IrExpr, facts: Facts): IntegerRange | null {
|
||||
havoc(facts, [e], facts);
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => {
|
||||
expr(child, new Map(facts));
|
||||
return true;
|
||||
},
|
||||
(child) => {
|
||||
body([child], new Map(facts));
|
||||
return true;
|
||||
},
|
||||
);
|
||||
// A direct call's result carries its whole-program return proof.
|
||||
return slotRange(e);
|
||||
}
|
||||
function induction(
|
||||
s: IrStmt & { kind: "for" },
|
||||
facts: Facts,
|
||||
|
||||
@@ -1073,6 +1073,12 @@ export interface IrModule {
|
||||
* write in the program (ir/contract-check.ts). Backends may rely on them
|
||||
* unconditionally. Absent when the program writes no contract. */
|
||||
contracts?: IrContracts;
|
||||
/** Local number arrays (`function\0local`) the backend may store as
|
||||
* int32 slots, proposed by ir/int32-arrays.ts after their element-reader
|
||||
* calls were inlined; the int32 slot analysis keeps the ones whose every
|
||||
* stored element it proves an int32, and those whose stores are exact
|
||||
* integers with checked stores. Absent when there is none. */
|
||||
int32ArrayCandidates?: string[];
|
||||
/** The WebAssembly modules translated into this program (wasm/
|
||||
* translate.ts), present exactly when the program uses the WebAssembly
|
||||
* API (possibly empty). The backend appends each module's translation
|
||||
@@ -2094,8 +2100,9 @@ export type IrStmt =
|
||||
* and whose `code` carries the SC diagnostic code; unwinds exactly like
|
||||
* `throw`.
|
||||
* TypeScript sources produce it only as the tsc-unreachable fallthrough
|
||||
* trap (appendImplicitUndefinedReturn's SC9002) — their construct fences
|
||||
* stay compile errors. */
|
||||
* trap (appendImplicitUndefinedReturn's SC9002) and for a typed-array
|
||||
* store through an element index that is undefined at run time
|
||||
* (undefined-index.ts); their construct fences stay compile errors. */
|
||||
| { kind: "runtimeFence"; code: string; message: string; loc: SrcLoc }
|
||||
/** Rethrow of a catch binding (`throw e` where e is the binding):
|
||||
* re-raises the SAVED exception exactly — kind and payload preserved,
|
||||
|
||||
@@ -4267,7 +4267,11 @@ function validateFunction(
|
||||
if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") {
|
||||
if (
|
||||
e.fn === "array.namedKeys" ||
|
||||
e.fn === "array.hasNamedKey" ||
|
||||
e.fn === "array.hasProperties"
|
||||
) {
|
||||
if (e.args[0]?.type.kind !== "array") {
|
||||
err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveBuildCacheRoot } from "../backend/cache-root.js";
|
||||
import { writeTextGeneratedSync } from "./text-file.js";
|
||||
import { joinedParts, writeTextGeneratedSync } from "./text-file.js";
|
||||
|
||||
/** UTF-8 bytes one native compiler string can hold (the runtime's
|
||||
* SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads
|
||||
@@ -28,6 +28,13 @@ export function contentDigest(value: string | Uint8Array): string {
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** contentDigest of the text the parts concatenate to. */
|
||||
export function partsDigest(parts: readonly string[]): string {
|
||||
const hash = createHash("sha256");
|
||||
for (const part of joinedParts(parts)) hash.update(part);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file
|
||||
* (a module's LLVM text) is never held whole. */
|
||||
export function fileDigest(path: string): string {
|
||||
|
||||
@@ -362,7 +362,7 @@ export class NativeCompiler {
|
||||
mkdirSync(inputDirectory);
|
||||
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
writeTextPartsSync(llvmInput, llvm);
|
||||
timing("llvm-write");
|
||||
if (outputKind === "llvm") {
|
||||
renameSync(stagedOutput, output);
|
||||
@@ -496,7 +496,7 @@ export class NativeCompiler {
|
||||
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
|
||||
}
|
||||
mkdirSync(options.outDir, { recursive: true });
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
writeTextPartsSync(llvmPath, llvm);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.ir.json`);
|
||||
@@ -585,7 +585,7 @@ export class NativeCompiler {
|
||||
const llvm = prepared.llvm;
|
||||
const llvmInput = join(stage, "program.ll");
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
writeTextPartsSync(llvmInput, llvm);
|
||||
timing("llvm-write");
|
||||
const features = prepared.features;
|
||||
const pack = selectNativeRuntimePack(
|
||||
@@ -648,7 +648,7 @@ export class NativeCompiler {
|
||||
});
|
||||
}
|
||||
timing("native-archive");
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
writeTextPartsSync(llvmPath, llvm);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.lib.ir.json`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { runNativeTool } from "../backend/native-tools.js";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { contentDigest, NativeCache } from "./cache.js";
|
||||
import { contentDigest, NativeCache, partsDigest } from "./cache.js";
|
||||
|
||||
interface InputIdentity {
|
||||
path: string;
|
||||
@@ -254,7 +254,7 @@ export class NativeExecutableCache {
|
||||
export function openNativeExecutableCache(
|
||||
cache: NativeCache | null,
|
||||
toolchain: NativeToolchain,
|
||||
llvm: string,
|
||||
llvm: readonly string[],
|
||||
options: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
pack: NativeRuntimeSelection,
|
||||
@@ -299,7 +299,7 @@ export function openNativeExecutableCache(
|
||||
const key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: contentDigest(llvm),
|
||||
llvm: partsDigest(llvm),
|
||||
options,
|
||||
toolchain,
|
||||
runtimeIdentity: pack.packageText,
|
||||
|
||||
@@ -11,11 +11,33 @@ import { NativeCache } from "./cache.js";
|
||||
import { prepareNativeExecutable } from "./prepare.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
|
||||
/** A prepared input with its LLVM parts joined: a cache hit reads the text
|
||||
* back as one part. */
|
||||
function text(input: ReturnType<ReturnType<typeof fixture>["prepare"]>) {
|
||||
return input.ok ? { ...input, llvm: input.llvm.join("") } : input;
|
||||
}
|
||||
|
||||
const scratch: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function fixtureToolchain(): NativeToolchain {
|
||||
const target = nativeCodegenTarget()!;
|
||||
return {
|
||||
compilerVersion: "test",
|
||||
target,
|
||||
helper: target.helper,
|
||||
helperExecutable: "unused",
|
||||
helperPackageRoot: "unused",
|
||||
runtimePackRoot: "unused",
|
||||
linker: "unused",
|
||||
linkerArgs: [],
|
||||
dsymutil: "unused",
|
||||
ts7Executable: ts7Executable(),
|
||||
};
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const temporary = mkdtempSync(join(tmpdir(), "scriptc-native-frontend-"));
|
||||
scratch.push(temporary);
|
||||
@@ -32,19 +54,7 @@ function fixture() {
|
||||
}),
|
||||
);
|
||||
writeFileSync(join(root, "package.json"), '{"type":"module"}');
|
||||
const target = nativeCodegenTarget()!;
|
||||
const toolchain: NativeToolchain = {
|
||||
compilerVersion: "test",
|
||||
target,
|
||||
helper: target.helper,
|
||||
helperExecutable: "unused",
|
||||
helperPackageRoot: "unused",
|
||||
runtimePackRoot: "unused",
|
||||
linker: "unused",
|
||||
linkerArgs: [],
|
||||
dsymutil: "unused",
|
||||
ts7Executable: ts7Executable(),
|
||||
};
|
||||
const toolchain = fixtureToolchain();
|
||||
const cache = new NativeCache(join(temporary, "cache"));
|
||||
const options = {
|
||||
outPath: join(root, ".scriptc", "main"),
|
||||
@@ -68,17 +78,77 @@ test("warm frontend reuse survives output creation and invalidates imported sour
|
||||
if (!first.ok) return;
|
||||
mkdirSync(f.options.outDir);
|
||||
writeFileSync(f.options.outPath, "binary placeholder");
|
||||
writeFileSync(join(f.options.outDir, "main.ll"), first.llvm);
|
||||
writeFileSync(join(f.options.outDir, "main.ll"), first.llvm.join(""));
|
||||
const warm = f.prepare();
|
||||
expect(warm).toEqual(first);
|
||||
expect(text(warm)).toEqual(text(first));
|
||||
expect(f.loads()).toBe(1);
|
||||
writeFileSync(f.dependency, 'export const value = "second";\n');
|
||||
const edited = f.prepare();
|
||||
expect(edited.ok).toBe(true);
|
||||
if (edited.ok) expect(edited.llvm).not.toBe(first.llvm);
|
||||
if (edited.ok) expect(edited.llvm.join("")).not.toBe(first.llvm.join(""));
|
||||
expect(f.loads()).toBe(2);
|
||||
});
|
||||
|
||||
test("comment and whitespace edits reuse the frontend output a fresh build reproduces", () => {
|
||||
const f = fixture();
|
||||
const first = f.prepare();
|
||||
expect(first.ok, JSON.stringify(first)).toBe(true);
|
||||
if (!first.ok) return;
|
||||
const commented = '// The exported value.\nexport const value = /* unchanged */ "first";\n';
|
||||
writeFileSync(f.dependency, commented);
|
||||
const reused = f.prepare();
|
||||
expect(f.loads()).toBe(1);
|
||||
expect(reused.ok).toBe(true);
|
||||
if (!reused.ok) return;
|
||||
expect(reused.llvm.join("")).toBe(first.llvm.join(""));
|
||||
expect(new Map(reused.sources).get(f.dependency)).toBe(commented);
|
||||
// The reused output is the one the edited sources lower to.
|
||||
const fresh = prepareNativeExecutable(
|
||||
f.entry,
|
||||
f.options,
|
||||
null,
|
||||
fixtureToolchain(),
|
||||
(path, npmStatic, externalTypes) => runFrontend(path, loadProgram, npmStatic, externalTypes),
|
||||
new NativeCache(join(f.root, "fresh-cache")),
|
||||
);
|
||||
expect(fresh.ok && fresh.llvm.join("")).toBe(first.llvm.join(""));
|
||||
// The refreshed entry still notices a later code edit.
|
||||
writeFileSync(f.dependency, 'export const value = "second";\n');
|
||||
const edited = f.prepare();
|
||||
expect(f.loads()).toBe(2);
|
||||
expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
|
||||
});
|
||||
|
||||
test("a comment edit inside source text the output carries is rebuilt", () => {
|
||||
const f = fixture();
|
||||
// A failing assert(value) reports its call's source text.
|
||||
const source = (call: string) =>
|
||||
`import assert from "node:assert";\nexport const value = "first";\n${call};\n`;
|
||||
writeFileSync(f.dependency, source("assert(value.length > 0)"));
|
||||
const first = f.prepare();
|
||||
expect(first.ok, JSON.stringify(first)).toBe(true);
|
||||
if (!first.ok) return;
|
||||
writeFileSync(f.dependency, source("assert(value.length > 0 /* checked */)"));
|
||||
const edited = f.prepare();
|
||||
expect(f.loads()).toBe(2);
|
||||
expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
|
||||
});
|
||||
|
||||
test("a whitespace edit that moves a reported source line is rebuilt", () => {
|
||||
const f = fixture();
|
||||
// node:test reports the file, line and column of each test.
|
||||
const source = (gap: string) =>
|
||||
`import { test } from "node:test";\nexport const value = "first";\n${gap}test("value", () => {});\n`;
|
||||
writeFileSync(f.dependency, source(""));
|
||||
const first = f.prepare();
|
||||
expect(first.ok, JSON.stringify(first)).toBe(true);
|
||||
if (!first.ok) return;
|
||||
writeFileSync(f.dependency, source("\n"));
|
||||
const edited = f.prepare();
|
||||
expect(f.loads()).toBe(2);
|
||||
expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
|
||||
});
|
||||
|
||||
test("structurally invalid cached features cause a rebuild even with a valid digest", () => {
|
||||
const f = fixture();
|
||||
expect(f.prepare().ok).toBe(true);
|
||||
@@ -101,13 +171,13 @@ test("serialized IR travels as bounded parts through the frontend cache", () =>
|
||||
expect(mod.functions.length).toBeGreaterThan(0);
|
||||
const warm = f.prepare({ emitIr: true });
|
||||
expect(f.loads()).toBe(1);
|
||||
expect(warm).toEqual(first);
|
||||
expect(text(warm)).toEqual(text(first));
|
||||
const family = join(f.cache.root, "frontend");
|
||||
const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!;
|
||||
const payload = JSON.parse(readFileSync(join(family, key), "utf8"));
|
||||
expect(payload.schema).toBe("scriptc.native-frontend.v3");
|
||||
expect(payload.input.llvm).toBe("");
|
||||
expect(payload.input.ir).toBeNull();
|
||||
expect(payload.llvm).toBe(first.llvm);
|
||||
expect(payload.llvm).toBe(first.llvm.join(""));
|
||||
expect(payload.ir).toEqual(first.ir);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ import type {
|
||||
CompileLibraryOptions,
|
||||
CompileRequestOptions,
|
||||
} from "../compile-types.js";
|
||||
import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js";
|
||||
import {
|
||||
emitLlvmModuleParts,
|
||||
type ForkPrimitives,
|
||||
lowRegionTarget,
|
||||
} from "../backend/llvm/emitter.js";
|
||||
import { inlineRcOption } from "../backend/llvm/shapes.js";
|
||||
import { executableLinkFeatures } from "../backend/executable-features.js";
|
||||
import type { NativeLinkFeatures } from "../backend/native-link-info-core.js";
|
||||
@@ -16,6 +20,7 @@ import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsSemanticallyMatch,
|
||||
frontendInputsStillMatch,
|
||||
validFrontendInputSnapshot,
|
||||
type FrontendInputSnapshot,
|
||||
@@ -24,18 +29,22 @@ import {
|
||||
} from "../frontend/input-tracker.js";
|
||||
import { provenanceSources } from "../frontend/provenance-registry.js";
|
||||
import { processFork } from "../frontend/process-fork.js";
|
||||
import { tokenEquivalentSource } from "../frontend/token-equivalence.js";
|
||||
import { llvmBytes, wtf8Bytes } from "../backend/literals.js";
|
||||
import { prepareLibrary } from "../library/prepare.js";
|
||||
import type { LibraryProfile } from "../library/library-profile.js";
|
||||
import type { IrModule } from "../ir/ir.js";
|
||||
import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js";
|
||||
import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js";
|
||||
import { batchTextParts, textSlices } from "./text-file.js";
|
||||
import { batchTextParts, joinedParts, textSlices } from "./text-file.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { nativeFileIdentity } from "./file-identity.js";
|
||||
|
||||
export interface NativeExecutableInput {
|
||||
ok: true;
|
||||
llvm: string;
|
||||
/** The module's LLVM text as parts that concatenate to it
|
||||
* (emitLlvmModuleParts), never joined: a large program's is 150 MB. */
|
||||
llvm: readonly string[];
|
||||
/** Compact serialized IR as bounded parts (`batchTextParts`): compiler-scale
|
||||
* modules exceed one native string. */
|
||||
ir: string[] | null;
|
||||
@@ -71,7 +80,8 @@ function* frontendCacheParts(
|
||||
input: { ...input, llvm: "", ir: null },
|
||||
});
|
||||
yield metadata.slice(0, metadata.length - 1) + ',"llvm":"';
|
||||
for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1);
|
||||
for (const part of joinedParts(input.llvm))
|
||||
for (const slice of textSlices(part)) yield JSON.stringify(slice).slice(1, -1);
|
||||
yield '"';
|
||||
if (input.ir === null) yield ',"ir":null}';
|
||||
else {
|
||||
@@ -111,18 +121,71 @@ function publishFrontendCache(
|
||||
);
|
||||
}
|
||||
|
||||
/** The cached input when every frontend input still matches, else the
|
||||
* entry the inputs no longer match, for reuse after equivalent edits. */
|
||||
function readFrontendCache(
|
||||
cache: NativeCache,
|
||||
key: string,
|
||||
exclusions: FrontendInputExclusions,
|
||||
): NativeExecutableInput | null {
|
||||
): { input: NativeExecutableInput } | { stale: FrontendCacheEntry } | null {
|
||||
// The entry is decoded as one string; larger entries are misses.
|
||||
const bytes = cache.read("frontend", key, NATIVE_STRING_BYTES);
|
||||
if (bytes === null) return null;
|
||||
const cached: unknown = JSON.parse(bytes.toString("utf8"));
|
||||
if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions))
|
||||
if (!validCachedInput(cached)) return null;
|
||||
if (!frontendInputsStillMatch(cached.probes, exclusions)) return { stale: cached };
|
||||
return { input: { ...cached.input, llvm: [cached.llvm], ir: cached.ir } };
|
||||
}
|
||||
|
||||
/** The stale entry's input when every changed source is token-equivalent to
|
||||
* the text the entry was built from (edits to whitespace and to comments
|
||||
* that cannot affect compilation; see token-equivalence.ts). Equivalent
|
||||
* sources lower to the same IR apart from source locations, which only dev
|
||||
* builds' debug information and the serialized IR carry. Diagnostics carry
|
||||
* locations too, so an entry that reported warnings is rebuilt. A changed
|
||||
* source whose raw text or line positions the frontend read (call-site text
|
||||
* in messages, comptime callbacks, test locations) is rebuilt as well:
|
||||
* frontendInputsSemanticallyMatch refuses its `text` probe. */
|
||||
function equivalentFrontendInput(
|
||||
stale: FrontendCacheEntry,
|
||||
options: CompileRequestOptions,
|
||||
exclusions: FrontendInputExclusions,
|
||||
): { input: NativeExecutableInput; probes: FrontendInputSnapshot } | null {
|
||||
if (
|
||||
options.optimization === "dev" ||
|
||||
options.sanitize === true ||
|
||||
options.emitIr === true ||
|
||||
stale.ir !== null ||
|
||||
(stale.input.warnings?.length ?? 0) > 0
|
||||
)
|
||||
return null;
|
||||
return { ...cached.input, llvm: cached.llvm, ir: cached.ir };
|
||||
const match = frontendInputsSemanticallyMatch(
|
||||
stale.probes,
|
||||
new Map(stale.input.sources),
|
||||
tokenEquivalentSource,
|
||||
exclusions,
|
||||
);
|
||||
if (match === null || match.changed.length === 0) return null;
|
||||
// Runtime fences and unsupported-construct messages name their source as
|
||||
// `[<code> at <file>:<line>]`, and an equivalent edit can move the line.
|
||||
if (
|
||||
match.changed.some(({ path }) =>
|
||||
stale.llvm.includes(` at ${llvmBytes(wtf8Bytes(path), false)}:`),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
input: {
|
||||
...stale.input,
|
||||
llvm: [stale.llvm],
|
||||
ir: null,
|
||||
sources: stale.input.sources.map(([path, text]) => [
|
||||
path,
|
||||
match.currentSources.get(path) ?? text,
|
||||
]),
|
||||
},
|
||||
probes: match.snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
function implementationIdentity(toolchain: NativeToolchain): string {
|
||||
@@ -284,10 +347,19 @@ export function prepareNativeExecutable(
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
}),
|
||||
);
|
||||
const input = readFrontendCache(cache, key, exclusions);
|
||||
if (input !== null) {
|
||||
const cached = readFrontendCache(cache, key, exclusions);
|
||||
if (cached !== null && "input" in cached) {
|
||||
timing("frontend-cache-hit");
|
||||
return input;
|
||||
return cached.input;
|
||||
}
|
||||
const equivalent =
|
||||
cached === null || outputKind === "ir"
|
||||
? null
|
||||
: equivalentFrontendInput(cached.stale, options, exclusions);
|
||||
if (equivalent !== null) {
|
||||
publishFrontendCache(cache, key, equivalent.probes, equivalent.input);
|
||||
timing("frontend-cache-equivalent");
|
||||
return equivalent.input;
|
||||
}
|
||||
} catch {
|
||||
key = null;
|
||||
@@ -310,8 +382,8 @@ export function prepareNativeExecutable(
|
||||
timing("ir-serialize");
|
||||
const llvm =
|
||||
outputKind === "ir"
|
||||
? ""
|
||||
: emitLlvmModule(prepared.mod, {
|
||||
? []
|
||||
: emitLlvmModuleParts(prepared.mod, {
|
||||
targetTriple: toolchain.target.llvmTriple,
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
@@ -399,10 +471,10 @@ export function prepareNativeLibrary(
|
||||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
}),
|
||||
);
|
||||
const input = readFrontendCache(cache, key, exclusions);
|
||||
if (input !== null) {
|
||||
const cached = readFrontendCache(cache, key, exclusions);
|
||||
if (cached !== null && "input" in cached) {
|
||||
timing("frontend-cache-hit");
|
||||
return input;
|
||||
return cached.input;
|
||||
}
|
||||
} catch {
|
||||
key = null;
|
||||
@@ -427,7 +499,7 @@ export function prepareNativeLibrary(
|
||||
);
|
||||
});
|
||||
if (!prepared.ok) return prepared;
|
||||
const llvm = emitLlvmModule(prepared.mod, {
|
||||
const llvm = emitLlvmModuleParts(prepared.mod, {
|
||||
targetTriple: toolchain.target.llvmTriple,
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { textSlices, writeTextPartsSync } from "./text-file.js";
|
||||
import { joinedParts, textSlices, writeTextPartsSync } from "./text-file.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
@@ -40,3 +40,13 @@ test("a part larger than one slice is written as its UTF-8 bytes", () => {
|
||||
expect(seen.join("")).toBe("head," + large + "tail");
|
||||
expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024);
|
||||
});
|
||||
|
||||
test("small parts join into batches and a long part passes through whole", () => {
|
||||
const small = Array.from({ length: 3000 }, (_, index) => `line ${index}\n`);
|
||||
const long = "x".repeat(2 * 1024 * 1024);
|
||||
const parts = [...small, long, "tail"];
|
||||
const joined = [...joinedParts(parts)];
|
||||
expect(joined.join("")).toBe(parts.join(""));
|
||||
expect(joined.length).toBeLessThan(5);
|
||||
expect(joined).toContain("tail");
|
||||
});
|
||||
|
||||
@@ -42,6 +42,24 @@ export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator
|
||||
}
|
||||
}
|
||||
|
||||
/** `parts` with consecutive parts joined into strings of about
|
||||
* TEXT_WRITE_LENGTH UTF-16 units (a longer part passes through whole), so
|
||||
* writing or escaping many small parts takes few calls. */
|
||||
export function* joinedParts(parts: readonly string[]): Generator<string> {
|
||||
let batch: string[] = [];
|
||||
let length = 0;
|
||||
for (const part of parts) {
|
||||
batch.push(part);
|
||||
length += part.length;
|
||||
if (length >= TEXT_WRITE_LENGTH) {
|
||||
yield batch.length === 1 ? batch[0]! : batch.join("");
|
||||
batch = [];
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
if (batch.length > 0) yield batch.join("");
|
||||
}
|
||||
|
||||
/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large
|
||||
* part is never encoded whole. `written` sees each slice after it is
|
||||
* written. */
|
||||
@@ -55,8 +73,8 @@ function writeTextPart(fd: number, part: string, written?: (slice: string) => vo
|
||||
}
|
||||
}
|
||||
|
||||
/** Write text parts to `path` in order without joining them first; pass
|
||||
* batched parts (`batchTextParts`) for many small ones. */
|
||||
/** Write text parts to `path` in order without joining them whole: small
|
||||
* parts are written in batches (`joinedParts`). */
|
||||
export function writeTextPartsSync(
|
||||
path: string,
|
||||
parts: readonly string[],
|
||||
@@ -64,7 +82,7 @@ export function writeTextPartsSync(
|
||||
): void {
|
||||
const fd = openSync(path, "w");
|
||||
try {
|
||||
for (const part of parts) writeTextPart(fd, part, written);
|
||||
for (const part of joinedParts(parts)) writeTextPart(fd, part, written);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-index-product-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const source = `
|
||||
function at(a: number[], x: number, y: number, k: number): number | undefined {
|
||||
return a[x * y + k];
|
||||
}
|
||||
function cancel(a: number[], p: number, q: number, r: number, s: number): number | undefined {
|
||||
return a[p * q - r * s];
|
||||
}
|
||||
const a = [1, 2, 3];
|
||||
const v = process.argv.length;
|
||||
console.log(at(a, v | 0, (v + 1) | 0, -v | 0), cancel(a, v | 0, v | 0, v | 0, v | 0));
|
||||
`;
|
||||
|
||||
function body(llvm: string, name: string): string {
|
||||
return new RegExp(`^define [^@]*@sc_\\w+?_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
}
|
||||
|
||||
test("an int32 product plus an int32 indexes as an exact i64 without a round trip", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
// The int32 entry: the index is the i64 product and sum, compared with
|
||||
// the array's counts; the double goes only to the slow path's runtime call.
|
||||
const at = /^define [^@]*@sc_bf_at\.i32\([^]*?^}/m.exec(llvm)?.[0] ?? "";
|
||||
const product = /(%\S+) = mul nsw i64 %\S+, %\S+/.exec(at)?.[1];
|
||||
expect(product).toBeDefined();
|
||||
expect(at).toContain(`= add nsw i64 ${product}, `);
|
||||
expect(at).not.toMatch(/fptosi double %\S+ to i64/);
|
||||
});
|
||||
|
||||
test("a difference of two int32 products keeps the double's round trip", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
// Each product may pass 2^53, where its double rounds before the
|
||||
// difference: the index keeps the exactness test of the double.
|
||||
const cancel = body(llvm, "cancel");
|
||||
expect(cancel).toMatch(/fptosi double %\S+ to i64/);
|
||||
expect(cancel).toMatch(/fcmp oeq double/);
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js";
|
||||
|
||||
async function emitLlvm(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function lowerIr(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const source = `
|
||||
function compose(n: number, rounds: number): number {
|
||||
const p: number[] = [];
|
||||
const q: number[] = [];
|
||||
for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1));
|
||||
for (let i = 0; i < n; i++) q.push(i);
|
||||
let check = 0;
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
for (let i = 0; i < n; i++) q[i] = p[q[i]!]!;
|
||||
check = (check + q[r % n]!) | 0;
|
||||
}
|
||||
return check;
|
||||
}
|
||||
function counts(n: number): number {
|
||||
const c = new Array(n).fill(0) as number[];
|
||||
for (let i = 0; i < n; i++) c[(i * 5) % n] = i;
|
||||
while (c.length > 1) c.pop();
|
||||
return c[0]!;
|
||||
}
|
||||
function fraction(n: number): number {
|
||||
const a = [1, 2, 3];
|
||||
a[1] = n / 3;
|
||||
return a[1]!;
|
||||
}
|
||||
function negativeZero(): number {
|
||||
const a = [1, -0];
|
||||
return 1 / a[1]!;
|
||||
}
|
||||
function counter(n: number): number {
|
||||
const a = new Array(n).fill(0) as number[];
|
||||
for (let i = 0; i < 1000; i++) a[i % n] = a[i % n]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function shortCounter(): number {
|
||||
const a = new Array(16).fill(0) as number[];
|
||||
for (let i = 0; i < 1000; i++) a[i % 16] = a[i % 16]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function literalCounter(n: number): number {
|
||||
const a = [0];
|
||||
for (let i = 0; i < n; i++) a[0] = a[0]! + 1;
|
||||
return a[0]!;
|
||||
}
|
||||
function joined(n: number): string {
|
||||
const a = [n | 0, 2];
|
||||
return a.join(",");
|
||||
}
|
||||
const { n } = JSON.parse('{"n": 8}') as { n: number };
|
||||
console.log(
|
||||
compose(n | 0, 3),
|
||||
counts(n | 0),
|
||||
fraction(n),
|
||||
negativeZero(),
|
||||
counter(n),
|
||||
shortCounter(),
|
||||
literalCounter(n),
|
||||
joined(n),
|
||||
);
|
||||
`;
|
||||
|
||||
/** Every function emitted for the source function \`name\` (its int32 ABI
|
||||
* body and its wrapper included). */
|
||||
function functions(llvm: string, name: string): string {
|
||||
const pattern = new RegExp(`^define [^@]*@sc_b?f_${name}[.\\w]*\\([^]*?^}`, "gm");
|
||||
return [...llvm.matchAll(pattern)].map((m) => m[0]).join("\n");
|
||||
}
|
||||
|
||||
test("arrays storing only int32 elements get 4-byte slots", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const compose = functions(llvm, "compose");
|
||||
expect(compose).not.toBe("");
|
||||
// Both arrays are allocated as SCR_ELEM_I32 (7).
|
||||
expect(compose.match(/@scr_arr_new_small\(i32 7\)/g)?.length).toBe(2);
|
||||
// Reads load an i32 slot; writes store one.
|
||||
expect(compose).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/);
|
||||
expect(compose).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/);
|
||||
// The element read through the helper's ternary stays in a stack box.
|
||||
expect(compose).not.toContain("@scr_union_new_f64");
|
||||
expect(compose).not.toMatch(/call [^\n]*@sc_b?f__x25_arr_idxOr/);
|
||||
// The fill form and a discarded pop.
|
||||
const counts = functions(llvm, "counts");
|
||||
expect(counts).toMatch(/@scr_arr_new\(i32 7, i64 0\)/);
|
||||
expect(counts).not.toContain("popArray");
|
||||
});
|
||||
|
||||
test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
for (const name of ["fraction", "negativeZero", "joined"]) {
|
||||
const fn = functions(llvm, name);
|
||||
expect(fn, name).not.toBe("");
|
||||
expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/);
|
||||
expect(fn, name).not.toContain("arr.kind.");
|
||||
}
|
||||
});
|
||||
|
||||
test("a counter array keeps int32 slots with checked stores", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const counter = functions(llvm, "counter");
|
||||
// `a[k] = a[k]! + 1` is an exact integer but no int32 proof: a fill of at
|
||||
// least 4096 elements creates SCR_ELEM_I32 storage (SCR_ELEM_F64 for a
|
||||
// shorter one), accesses test the kind, and a store widens the array
|
||||
// before a value that is not an int32.
|
||||
expect(counter).toMatch(
|
||||
/fcmp oge double %\w+, [^\n]+\n\s+%\w+ = select i1 %\w+, i32 7, i32 0\n\s+%\w+ = call ptr @scr_arr_new(_small)?\(i32 %\w+/,
|
||||
);
|
||||
expect(counter).toMatch(/load i8, ptr %\w+[^\n]*\n\s+%\w+ = icmp eq i8 %\w+, 7/);
|
||||
expect(counter).toContain("@llvm.fptosi.sat.i32.f64");
|
||||
expect(counter).toContain("call void @scr_arr_widen_i32(");
|
||||
// Arrays with proven stores take no kind test, and a fill of a constant
|
||||
// short length or an array literal keeps doubles.
|
||||
for (const name of ["compose", "counts", "shortCounter", "literalCounter"]) {
|
||||
const fn = functions(llvm, name);
|
||||
expect(fn, name).not.toContain("arr.kind.");
|
||||
expect(fn, name).not.toContain("kind.fast");
|
||||
expect(fn, name).not.toContain("scr_arr_widen_i32");
|
||||
}
|
||||
for (const name of ["shortCounter", "literalCounter"])
|
||||
expect(functions(llvm, name), name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/);
|
||||
});
|
||||
|
||||
test("a loop over a counter array tests its kind once on entry", async () => {
|
||||
const llvm = await emitLlvm(source);
|
||||
const counter = functions(llvm, "counter");
|
||||
// The loop's fast copy runs while the array holds int32 slots, with no
|
||||
// kind test of its own: the sum is an integer, and a sum past the int32
|
||||
// range leaves for the third copy, which re-executes the store with kind
|
||||
// tests and widens the array.
|
||||
expect(counter).toMatch(
|
||||
/icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/,
|
||||
);
|
||||
const rest = counter.slice(counter.search(/^kind\.fast\d+:/m));
|
||||
// The fast copy is the last one, before the loop's exit.
|
||||
const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m));
|
||||
expect(fast).not.toBe("");
|
||||
expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/);
|
||||
expect(fast).toMatch(/add nsw i64 %\w+, 1/);
|
||||
expect(fast).toMatch(
|
||||
/icmp sle i64 %\w+, 2147483647\n[^\n]*\n\s+br i1 %\w+, label %arr\.store\.i32\d+, label %kind\.resume\d+/,
|
||||
);
|
||||
expect(fast).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/);
|
||||
expect(counter).toMatch(/^kind\.resume\d+:/m);
|
||||
});
|
||||
|
||||
test("only the candidates the proof keeps are rewritten", async () => {
|
||||
const mod = await lowerIr(source);
|
||||
const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? [];
|
||||
expect(proposed).toEqual(
|
||||
expect.arrayContaining(["compose\0p.0", "compose\0q.0", "counts\0c.0", "counter\0a.0"]),
|
||||
);
|
||||
// An array that reaches `join` is never proposed.
|
||||
expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false);
|
||||
// Without a kept candidate, the module is the input itself.
|
||||
expect(selectInt32Arrays(mod, () => new Set())).toBe(mod);
|
||||
const kept = new Set(["compose\0p.0", "compose\0q.0"]);
|
||||
const selected = selectInt32Arrays(
|
||||
mod,
|
||||
(marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))),
|
||||
);
|
||||
expect(selected.int32ArrayCandidates).toEqual([...kept]);
|
||||
for (const fn of selected.functions)
|
||||
if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name));
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7009-finalization-registry-wasm-loader/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7037-js-var-open-writes/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7038-js-var-later-writes.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7060-array-numeric-keys.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7061-array-object-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7062-inspect-array-properties.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7325-array-index-products-near-2-53.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7326-int32-element-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7327-int32-array-widening.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7364-new-target-late-instantiations.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7440-fromentries-aliased-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7480-class-call-without-new.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7481-class-call-without-new.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7540-undefined-element-index-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7560-new-target-constructors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7561-new-target-generic-registry.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/class-call-shadowed-static.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/fromentries-aliased-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-late-local-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-local-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-plain-function.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-value-generic-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-value-signatures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/js-own-tostring/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/zod3-infer/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/zod3/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -176,7 +176,22 @@ static size_t scr_arr_prop_index(const ScrArr *a, const char *key,
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
#define SCR_ARR_UNDEFINED_KEY_BITS UINT64_C(0x7ff8000000756e64)
|
||||
|
||||
double scr_arr_undefined_key(void) {
|
||||
uint64_t bits = SCR_ARR_UNDEFINED_KEY_BITS;
|
||||
double key;
|
||||
memcpy(&key, &bits, sizeof key);
|
||||
return key;
|
||||
}
|
||||
|
||||
static size_t scr_arr_prop_key(double i, char *buf) {
|
||||
uint64_t bits;
|
||||
memcpy(&bits, &i, sizeof bits);
|
||||
if (bits == SCR_ARR_UNDEFINED_KEY_BITS) {
|
||||
memcpy(buf, "undefined", sizeof "undefined");
|
||||
return sizeof "undefined" - 1;
|
||||
}
|
||||
return scr_f64_to_str(i, buf);
|
||||
}
|
||||
|
||||
@@ -277,6 +292,7 @@ typedef struct {
|
||||
static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) {
|
||||
if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index];
|
||||
if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]);
|
||||
if (s->elem == SCR_ELEM_I32) return scr_arr_f64_slot((double)((const int32_t *)s->data)[index]);
|
||||
return s->data[index];
|
||||
}
|
||||
|
||||
@@ -578,6 +594,36 @@ static void scr_arr_move_inline(ScrArr *a, size_t cap) {
|
||||
a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS;
|
||||
}
|
||||
|
||||
/* Widen an int32 array (SCR_ELEM_I32) to doubles in place, one way: its
|
||||
* dense values move to 8-byte slots, with their states when it has them, and
|
||||
* its kind becomes SCR_ELEM_F64. Compiled code calls it before storing a
|
||||
* value that is not an int32 into an int32 array with checked stores; `data`
|
||||
* may move. Sparse entries already hold doubles. */
|
||||
void scr_arr_widen_i32(ScrArr *a) {
|
||||
if (a->elem != SCR_ELEM_I32) return;
|
||||
size_t cap = a->cap;
|
||||
if (cap > 0) {
|
||||
/* Int32 arrays never shift, so they have no head room; inline slots
|
||||
* have no ext prefix. */
|
||||
bool inline_slots = scr_arr_inline(a);
|
||||
bool states = scr_arr_has_states(a);
|
||||
size_t unit = sizeof(uint64_t) + (states ? 1 : 0);
|
||||
size_t prefix = inline_slots ? 0 : scr_arr_prefix(a);
|
||||
if (cap > (SIZE_MAX - prefix) / unit) scr_arr_oom();
|
||||
char *start = scr_mem_alloc(prefix + cap * unit);
|
||||
if (!start) scr_arr_oom();
|
||||
if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix);
|
||||
uint64_t *data = (uint64_t *)(start + prefix);
|
||||
const int32_t *old = (const int32_t *)a->data;
|
||||
for (size_t i = 0; i < cap; i++) data[i] = scr_arr_f64_slot((double)old[i]);
|
||||
if (states) memcpy((uint8_t *)data + cap * sizeof(uint64_t), scr_arr_states_end(a) - cap, cap);
|
||||
if (!inline_slots && a->rc != SIZE_MAX) scr_mem_free(scr_arr_alloc_start(a));
|
||||
a->data = data;
|
||||
a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS;
|
||||
}
|
||||
a->elem = SCR_ELEM_F64;
|
||||
}
|
||||
|
||||
/* How a dense allocation is sized. */
|
||||
typedef enum {
|
||||
SCR_ARR_GROW_APPEND, /* geometric headroom for further appends */
|
||||
@@ -621,7 +667,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode);
|
||||
* paths pick the same capacity. */
|
||||
static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) {
|
||||
if (need <= a->cap) return;
|
||||
if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF ||
|
||||
if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || a->elem == SCR_ELEM_I32 ||
|
||||
(a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) {
|
||||
scr_arr_grow_dense_slow(a, need, mode);
|
||||
return;
|
||||
@@ -1904,6 +1950,10 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state,
|
||||
if (state != SCR_ARR_VALUE) scr_arr_cut(a, from);
|
||||
if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from);
|
||||
else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot);
|
||||
else if (a->elem == SCR_ELEM_I32) {
|
||||
int32_t v = scr_arr_i32_of(slot);
|
||||
for (size_t i = from; i < until; i++) ((int32_t *)a->data)[i] = v;
|
||||
}
|
||||
else for (size_t i = from; i < until; i++) a->data[i] = slot;
|
||||
if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from);
|
||||
} else {
|
||||
@@ -2007,7 +2057,7 @@ static uint64_t scr_arr_require_slot(ScrArr *a, double i) {
|
||||
if (scr_arr_prop_get_state(a, i, &prop, &state)) {
|
||||
if (state == SCR_ARR_UNDEFINED) {
|
||||
char buf[32];
|
||||
scr_f64_to_str(i, buf);
|
||||
scr_arr_prop_key(i, buf);
|
||||
scr_trap_fmt("scriptc: RangeError: numeric property %s is undefined\n", buf);
|
||||
}
|
||||
return prop;
|
||||
@@ -5116,7 +5166,15 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) {
|
||||
|
||||
bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
|
||||
char text[64];
|
||||
if (len == sizeof "undefined" - 1 && memcmp(key, "undefined", len) == 0) {
|
||||
*out = scr_arr_undefined_key();
|
||||
return true;
|
||||
}
|
||||
if (len == 0 || len >= sizeof text) return false;
|
||||
/* A number's spelling starts with a digit, '-', "NaN" or "Infinity":
|
||||
* other names answer without parsing. */
|
||||
char first = key[0];
|
||||
if (!(first >= '0' && first <= '9') && first != '-' && first != 'N' && first != 'I') return false;
|
||||
memcpy(text, key, len);
|
||||
text[len] = '\0';
|
||||
char *end;
|
||||
@@ -5130,6 +5188,11 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scr_arr_has_properties(const ScrArr *a) {
|
||||
const ScrArrExt *x = scr_arr_ext_of(a);
|
||||
return x && (x->prop_len || x->metadata);
|
||||
}
|
||||
|
||||
void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
|
||||
const ScrDyn *metadata = SCR_AX(a)->metadata;
|
||||
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE &&
|
||||
@@ -5137,21 +5200,7 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
|
||||
return;
|
||||
metadata = SCR_AX(a)->metadata;
|
||||
if (!metadata) return;
|
||||
for (size_t i = 0; i < metadata->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &metadata->v.obj.entries[i];
|
||||
ScrStr *key = scr_str_new(entry->key, entry->key_len);
|
||||
if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) {
|
||||
scr_dyn_key_set(target, key, entry->value);
|
||||
} else {
|
||||
/* Properties defined through a view keep their attributes. */
|
||||
ScrDyn *name = scr_dyn_new_str(key);
|
||||
ScrDyn *descriptor = scr_dyn_own_descriptor(metadata, key);
|
||||
if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor));
|
||||
scr_dyn_release(descriptor);
|
||||
scr_dyn_release(name);
|
||||
}
|
||||
scr_str_release(key);
|
||||
}
|
||||
for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]);
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_named_keys(ScrArr *a) {
|
||||
@@ -5209,8 +5258,27 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) {
|
||||
}
|
||||
ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL;
|
||||
if (!named || named->v.obj.len == 0) return;
|
||||
/* The view's numeric properties are copies of the native ones (every
|
||||
* write to them goes to the native array), so only the others move. */
|
||||
bool numeric = false;
|
||||
for (size_t i = 0; i < named->v.obj.len && !numeric; i++) {
|
||||
double number;
|
||||
numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number);
|
||||
}
|
||||
if (numeric) {
|
||||
ScrDyn *rest = scr_dyn_new_obj();
|
||||
for (size_t i = 0; i < named->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &named->v.obj.entries[i];
|
||||
double number;
|
||||
if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry);
|
||||
}
|
||||
named = rest;
|
||||
} else {
|
||||
scr_dyn_retain(named);
|
||||
}
|
||||
ScrArrExt *x = scr_arr_ext(a);
|
||||
ScrDyn *old = x->metadata;
|
||||
x->metadata = scr_dyn_retain(named);
|
||||
x->metadata = named->v.obj.len ? named : NULL;
|
||||
if (!x->metadata) scr_dyn_release(named);
|
||||
scr_dyn_release(old);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ typedef enum {
|
||||
* address over 8, 0 for NULL. Compact instances are never counted or
|
||||
* traced, so these arrays retain, release and trace nothing. */
|
||||
SCR_ELEM_CREF,
|
||||
/* Numbers that the compiler proves are always int32 (never -0), in
|
||||
* 4-byte slots: arrays that only compiled element accesses ever reach
|
||||
* (backend/llvm int32 arrays). Every runtime slot is the value's double
|
||||
* bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put
|
||||
* convert, so the paths compiled code calls need no case of their own. */
|
||||
SCR_ELEM_I32,
|
||||
} ScrElemKind;
|
||||
|
||||
typedef enum {
|
||||
@@ -134,11 +140,12 @@ typedef struct ScrArr {
|
||||
* 0 is always valid. */
|
||||
uint32_t filled;
|
||||
/* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or
|
||||
* `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in
|
||||
* the same allocation, when the array has states (SCR_ARR_STATES), by one
|
||||
* state byte per slot in reverse slot order (scr_arr_states_end). Sparse
|
||||
* entries and properties keep 8-byte slots for every kind; a bool slot is
|
||||
* 0 or 1 wherever it lives, and a CREF slot's 8-byte form is the pointer
|
||||
* `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32
|
||||
* (scr_arr_slot_width), followed in the same allocation, when the array
|
||||
* has states (SCR_ARR_STATES), by one state byte per slot in reverse slot
|
||||
* order (scr_arr_states_end). Sparse entries and properties keep 8-byte
|
||||
* slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF
|
||||
* slot's 8-byte form is the pointer and an I32 slot's the double's bits
|
||||
* (scr_arr_dense_get). Rarely
|
||||
* used storage (sparse indices, noncanonical numeric properties,
|
||||
* match-result metadata) lives out of line, and an array that has it
|
||||
@@ -260,7 +267,7 @@ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) {
|
||||
/* Bytes per dense value slot of element kind `elem`. */
|
||||
static inline size_t scr_elem_slot_width(uint8_t elem) {
|
||||
if (elem == SCR_ELEM_BOOL) return 1;
|
||||
if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t);
|
||||
if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t);
|
||||
return sizeof(uint64_t);
|
||||
}
|
||||
|
||||
@@ -325,11 +332,38 @@ static inline void scr_arr_need_states(ScrArr *a) {
|
||||
static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; }
|
||||
static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); }
|
||||
|
||||
/* The bits of `d` as a slot, and back. */
|
||||
static inline uint64_t scr_arr_f64_slot(double d) {
|
||||
uint64_t slot;
|
||||
memcpy(&slot, &d, sizeof slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
static inline double scr_arr_slot_f64(uint64_t slot) {
|
||||
double d;
|
||||
memcpy(&d, &slot, sizeof d);
|
||||
return d;
|
||||
}
|
||||
|
||||
/* An int32 array's slot for a double: its stores are proven int32s that
|
||||
* are not -0 (and a cleared slot is +0), so anything else is a compiler
|
||||
* bug. */
|
||||
static inline int32_t scr_arr_i32_of(uint64_t slot) {
|
||||
double d = scr_arr_slot_f64(slot);
|
||||
if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) ||
|
||||
slot == UINT64_C(0x8000000000000000),
|
||||
0))
|
||||
__builtin_trap();
|
||||
return (int32_t)d;
|
||||
}
|
||||
|
||||
/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */
|
||||
static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) {
|
||||
if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index];
|
||||
if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
||||
return scr_cref_decode(((const uint32_t *)a->data)[index]);
|
||||
if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
||||
return scr_arr_f64_slot((double)((const int32_t *)a->data)[index]);
|
||||
return a->data[index];
|
||||
}
|
||||
|
||||
@@ -337,6 +371,8 @@ static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) {
|
||||
if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot;
|
||||
else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
||||
((uint32_t *)a->data)[index] = scr_cref_encode(slot);
|
||||
else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
||||
((int32_t *)a->data)[index] = scr_arr_i32_of(slot);
|
||||
else a->data[index] = slot;
|
||||
}
|
||||
|
||||
@@ -397,6 +433,9 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap);
|
||||
* array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and
|
||||
* scr_arr_new_ops(ops, 0). Returns +1. */
|
||||
ScrArr *scr_arr_new_small(ScrElemKind elem);
|
||||
/* An int32 array's storage as doubles, in place (SCR_ELEM_I32 to
|
||||
* SCR_ELEM_F64); no-op for any other kind. */
|
||||
void scr_arr_widen_i32(ScrArr *a);
|
||||
ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops);
|
||||
/* scr_arr_new(elem, cap) for a known final length: the `cap` slots share
|
||||
* the array's own block (one allocation). With `states`, the array starts
|
||||
@@ -421,9 +460,15 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
|
||||
* properties of `target`, so neither is lost when the storage is swapped. */
|
||||
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
|
||||
/* True when `key` is the string spelling of a number that is not an array
|
||||
* index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays
|
||||
* keep such properties in their noncanonical property table. */
|
||||
* index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in
|
||||
* *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties
|
||||
* in their noncanonical property table. */
|
||||
bool scr_arr_numeric_key(const char *key, size_t len, double *out);
|
||||
/* The number compiled code passes as the key of an element access whose
|
||||
* index is undefined at run time: a NaN with its own payload, which the
|
||||
* property table spells "undefined" (JavaScript names that property, not
|
||||
* "NaN"). */
|
||||
double scr_arr_undefined_key(void);
|
||||
/* Whether the array still has the named or noncanonical numeric property
|
||||
* `key`: `for...in` skips a key deleted before its turn. Borrows both. */
|
||||
bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
|
||||
@@ -431,6 +476,10 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
|
||||
* noncanonical numeric properties, then the named properties in insertion
|
||||
* order. +1. */
|
||||
ScrArr *scr_arr_named_keys(ScrArr *a);
|
||||
/* Whether the array has properties besides its elements (noncanonical
|
||||
* numeric keys, match metadata, named properties): util.inspect lists
|
||||
* them after the elements. */
|
||||
bool scr_arr_has_properties(const ScrArr *a);
|
||||
/* Installed by scr_regex.c when it records a pending match source; this
|
||||
* unit links without the regex runtime. Writes the pending metadata of
|
||||
* `a` into its dynamic copy or materializes it, answering whether it
|
||||
|
||||
@@ -169,6 +169,7 @@ typedef enum {
|
||||
X(FETCH_RESPONSE, 12) /* native static-fetch Response */ \
|
||||
X(FILE, 42) /* ScrBlob with a name — new File() */ \
|
||||
X(FILE_HANDLE, 17) /* fs/promises.FileHandle */ \
|
||||
X(FINALIZATION_REGISTRY, 72) /* FinalizationRegistry (bookkeeping only) */ \
|
||||
X(GLOBAL, 22) /* native global object identity */ \
|
||||
X(H2_SESSION, 4) /* ScrH2Session — Http2Session (client & server) */ \
|
||||
X(H2_STREAM, 5) /* ScrH2Stream — Http2Stream (client & server) */ \
|
||||
@@ -623,6 +624,9 @@ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDy
|
||||
ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len);
|
||||
/* Own-enumerable check on a SCR_DYN_OBJ. */
|
||||
bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len);
|
||||
/* Stores own property `entry` of `source` on `target` with its attributes:
|
||||
* plain data properties by an ordinary write, others by their descriptor. */
|
||||
void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry);
|
||||
/* Literal discriminator tests used before selecting a typed record layout.
|
||||
* Missing fields and different primitive kinds never match. Borrowed args. */
|
||||
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
|
||||
@@ -1083,6 +1087,8 @@ ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry);
|
||||
bool scr_weak_map_is(const ScrDyn *value);
|
||||
bool scr_weak_set_is(const ScrDyn *value);
|
||||
ScrDyn *scr_weak_map_new(ScrDyn *entries);
|
||||
ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup);
|
||||
bool scr_finalization_registry_is(const ScrDyn *value);
|
||||
ScrDyn *scr_weak_set_new(ScrDyn *values);
|
||||
ScrDyn *scr_dyn_from_entries(ScrDyn *entries);
|
||||
ScrDyn *scr_dyn_mark_snapshot(ScrDyn *value); /* consumes and returns +1 */
|
||||
|
||||
@@ -1856,9 +1856,10 @@ static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set";
|
||||
static const char dyn_s32[] SCR_DYN_COMPAT_STR = "";
|
||||
static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet";
|
||||
static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive";
|
||||
static const char dyn_s35[] SCR_DYN_COMPAT_STR = "FinalizationRegistry";
|
||||
|
||||
static const char *const dyn_global_ctor_names[] = {
|
||||
dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24,
|
||||
dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24, dyn_s35,
|
||||
};
|
||||
#define DYN_GLOBAL_CTORS (sizeof dyn_global_ctor_names / sizeof dyn_global_ctor_names[0])
|
||||
static SCR_TL ScrDyn *dyn_global_ctors[DYN_GLOBAL_CTORS];
|
||||
@@ -1928,8 +1929,8 @@ static ScrDyn *dyn_global_ctor(const char *name) {
|
||||
}
|
||||
if (!dyn_global_ctors[i]) {
|
||||
/* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1,
|
||||
* the collections 0. */
|
||||
uint32_t length = i == 0 || i == 5 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0;
|
||||
* the collections 0, FinalizationRegistry 1. */
|
||||
uint32_t length = i == 0 || i == 5 || i == 8 ? 1 : i == 1 ? 7 : i == 2 ? 2 : 0;
|
||||
dyn_global_ctors[i] = scr_dyn_new_func(scr_closure_new((void *)dyn_global_ctor_names[i], 0),
|
||||
dyn_global_ctor_call, length, dyn_s6,
|
||||
dyn_global_ctor_names[i]);
|
||||
|
||||
@@ -1161,6 +1161,15 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) {
|
||||
|
||||
/* Property-name rendering: bare when the key matches Node's keyStrRegExp
|
||||
* (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */
|
||||
/* util.inspect's text for an accessor property: [Getter], [Setter] or
|
||||
* [Getter/Setter], by which of the two are functions. +1. */
|
||||
static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) {
|
||||
bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC;
|
||||
bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC;
|
||||
const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined";
|
||||
return scr_str_new(text, strlen(text));
|
||||
}
|
||||
|
||||
static void insp_key_into(InspBuf *b, const char *key, size_t len) {
|
||||
if (len == 9 && memcmp(key, "__proto__", 9) == 0) {
|
||||
ib_cstr(b, "['__proto__']");
|
||||
@@ -2018,14 +2027,20 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
return scr_insp_str(d->v.str);
|
||||
case SCR_DYN_ARR: {
|
||||
size_t len = d->v.arr.len;
|
||||
if (len == 0) return scr_str_new("[]", 2);
|
||||
/* Named and noncanonical numeric properties follow the elements as
|
||||
* `key: value` entries (Node's array extras). */
|
||||
const ScrDyn *props = d->v.arr.properties;
|
||||
size_t nprops = 0;
|
||||
for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable;
|
||||
if (len == 0 && nprops == 0) return scr_str_new("[]", 2);
|
||||
if (recurse > depth) return insp_styled_lit("[Array]", 7, INSP_SPECIAL);
|
||||
scr_insp_begin(recurse + 1);
|
||||
/* Node's maxArrayLength (100) counts output entries; a run of holes
|
||||
* is one `<n empty items>` entry (formatSpecialArray). */
|
||||
const unsigned char *presence = d->v.arr.presence;
|
||||
size_t i = 0;
|
||||
for (size_t shown = 0; i < len && shown < 100; shown++) {
|
||||
size_t shown = 0;
|
||||
for (; i < len && shown < 100; shown++) {
|
||||
if (presence && !presence[i]) {
|
||||
size_t next = i + 1;
|
||||
while (next < len && !presence[next]) next++;
|
||||
@@ -2044,15 +2059,38 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
if (scr_exc_pending()) break;
|
||||
}
|
||||
bool more = i < len;
|
||||
size_t entries = shown + (more ? 1 : 0);
|
||||
if (more) {
|
||||
ScrStr *m = scr_insp_more_items((double)(len - i));
|
||||
scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM);
|
||||
scr_str_release(m);
|
||||
}
|
||||
for (size_t k = 0; props && k < props->v.obj.len; k++) {
|
||||
const ScrDynEntry *ent = &props->v.obj.entries[k];
|
||||
if (!ent->enumerable) continue;
|
||||
ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth);
|
||||
InspBuf eb = {0};
|
||||
insp_key_into(&eb, ent->key, ent->key_len);
|
||||
ib_cstr(&eb, ": ");
|
||||
ib_bytes(&eb, val->data, val->len);
|
||||
scr_str_release(val);
|
||||
ScrStr *entry = ib_take(&eb);
|
||||
/* Node's grouping pads by the array element at the entry's output
|
||||
* position (value[i] in groupArrayElements), not by the entry. */
|
||||
bool number = entries < len && (!presence || presence[entries]) &&
|
||||
d->v.arr.items[entries]->kind == SCR_DYN_NUM;
|
||||
scr_insp_entry(entry, number);
|
||||
scr_str_release(entry);
|
||||
entries++;
|
||||
}
|
||||
/* Node's grouping leaves out the last entry when there are more
|
||||
* entries than maxArrayLength (the `... more items` entry when the
|
||||
* array has no properties). */
|
||||
bool last_out = entries > 100;
|
||||
ScrStr *base = scr_str_new("", 0);
|
||||
ScrStr *b0 = scr_str_new("[", 1);
|
||||
ScrStr *b1 = scr_str_new("]", 1);
|
||||
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more);
|
||||
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out);
|
||||
scr_str_release(base);
|
||||
scr_str_release(b0);
|
||||
scr_str_release(b1);
|
||||
|
||||
@@ -3472,6 +3472,43 @@ ScrDyn *scr_dyn_class_view_unavailable(void *ptr) {
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry) {
|
||||
ScrStr *key = scr_str_new(entry->key, entry->key_len);
|
||||
if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) {
|
||||
scr_dyn_key_set(target, key, entry->value);
|
||||
} else {
|
||||
/* Properties defined through a view keep their attributes. */
|
||||
ScrDyn *name = scr_dyn_new_str(key);
|
||||
ScrDyn *descriptor = scr_dyn_own_descriptor(source, key);
|
||||
if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor));
|
||||
scr_dyn_release(descriptor);
|
||||
scr_dyn_release(name);
|
||||
}
|
||||
scr_str_release(key);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key);
|
||||
|
||||
/* A live array's view lists the native numeric properties first, then the
|
||||
* named properties the snapshot copied from the array's metadata. */
|
||||
static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn *view) {
|
||||
const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(capsule);
|
||||
ScrArr *arr = capsule->v.typed_ref.ptr;
|
||||
const ScrArrExt *x = ops ? scr_arr_ext_of(arr) : NULL;
|
||||
if (!x || x->prop_len == 0 || view->kind != SCR_DYN_ARR) return;
|
||||
ScrDyn *named = view->v.arr.properties;
|
||||
view->v.arr.properties = NULL;
|
||||
for (size_t i = 0; i < x->prop_len; i++) {
|
||||
double number;
|
||||
if (!scr_arr_numeric_key(x->props[i].key, x->props[i].key_len, &number)) continue;
|
||||
ScrDyn *value = scr_typed_ref_numeric_property(capsule, number);
|
||||
if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value);
|
||||
}
|
||||
if (!named) return;
|
||||
for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]);
|
||||
scr_dyn_release(named);
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
|
||||
ScrDyn *capsule = (ScrDyn *)d;
|
||||
ScrDyn *fresh = capsule->v.typed_ref.materialize(capsule->v.typed_ref.ptr);
|
||||
@@ -3479,6 +3516,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
|
||||
scr_dyn_release(fresh);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
scr_typed_ref_view_numeric_properties(capsule, fresh);
|
||||
if (!capsule->v.typed_ref.materialized) {
|
||||
capsule->v.typed_ref.materialized = fresh;
|
||||
} else {
|
||||
@@ -3715,6 +3753,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A numeric property of a live array ("-1", "1.5"): +1, or NULL when the
|
||||
* native array has none. */
|
||||
static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key) {
|
||||
ScrArr *arr = d->v.typed_ref.ptr;
|
||||
double state = scr_arr_state(arr, key);
|
||||
if (state == (double)SCR_ARR_VALUE) return scr_dyn_typed_ref_array_ops(d)->get(arr, key);
|
||||
if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* An inherited Date method of an instance of a class declared `extends
|
||||
* Date`, when the instance's own members do not define `key`: +1, or
|
||||
* NULL. */
|
||||
@@ -3739,6 +3787,8 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
|
||||
if (date_method) return date_method;
|
||||
if (!scr_dyn_typed_ref_array_ops(d)) return NULL;
|
||||
if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len);
|
||||
double number;
|
||||
if (scr_arr_numeric_key(key->data, key->len, &number)) return scr_typed_ref_numeric_property(d, number);
|
||||
size_t index;
|
||||
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return NULL;
|
||||
return scr_dyn_typed_ref_element(d, index);
|
||||
@@ -3748,6 +3798,9 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
|
||||
static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!scr_dyn_typed_ref_array_ops(d)) return false;
|
||||
if (scr_typed_ref_is_length(key)) return true;
|
||||
double number;
|
||||
if (scr_arr_numeric_key(key->data, key->len, &number))
|
||||
return scr_arr_state(d->v.typed_ref.ptr, number) != (double)SCR_ARR_HOLE;
|
||||
size_t index;
|
||||
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false;
|
||||
ScrArr *arr = d->v.typed_ref.ptr;
|
||||
@@ -3760,6 +3813,22 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) {
|
||||
static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) {
|
||||
const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d);
|
||||
if (!ops || !ops->set) return false;
|
||||
/* Numeric properties live in the native array's property table, which
|
||||
* holds values of the element type. */
|
||||
double number;
|
||||
if (scr_arr_numeric_key(key->data, key->len, &number)) {
|
||||
if (ops->set(d->v.typed_ref.ptr, number, value)) return true;
|
||||
ScrStr *kind = scr_dyn_typeof(value);
|
||||
ScrJsonBuf message;
|
||||
scr_jb_init(&message);
|
||||
scr_jb_puts(&message, "expected the array's element type at $[");
|
||||
scr_jb_write(&message, key->data, key->len);
|
||||
scr_jb_puts(&message, "], got ");
|
||||
scr_jb_write(&message, kind->data, kind->len);
|
||||
scr_str_release(kind);
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
|
||||
return true;
|
||||
}
|
||||
size_t index;
|
||||
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false;
|
||||
ScrArr *arr = d->v.typed_ref.ptr;
|
||||
@@ -3989,6 +4058,7 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD
|
||||
static const struct { const char *name; int tag; } kinds[] = {
|
||||
{"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET},
|
||||
{"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET},
|
||||
{"FinalizationRegistry", SCR_DYNH_FINALIZATION_REGISTRY},
|
||||
};
|
||||
if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE;
|
||||
for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) {
|
||||
@@ -6707,6 +6777,21 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) {
|
||||
|
||||
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) {
|
||||
if (scr_dyn_error_prototype_fence(target)) return NULL;
|
||||
if (target->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(target) &&
|
||||
(key->kind == SCR_DYN_STR || key->kind == SCR_DYN_NUM)) {
|
||||
/* A native array's numeric properties ("-1", "1.5") live in its
|
||||
* property table, which holds values without attributes. A number key
|
||||
* names one unless it is an array index. */
|
||||
double number = key->kind == SCR_DYN_NUM ? key->v.num : 0;
|
||||
bool numeric = key->kind == SCR_DYN_NUM
|
||||
? !(number >= 0 && number < 4294967295.0 && number == trunc(number))
|
||||
: scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number);
|
||||
if (numeric) {
|
||||
static const char message[] = "Object.defineProperty on a numeric property of a native array is not supported yet";
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (target->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(target);
|
||||
ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor);
|
||||
@@ -7089,6 +7174,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET ||
|
||||
d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER ||
|
||||
d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET ||
|
||||
d->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY ||
|
||||
d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP ||
|
||||
d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB ||
|
||||
d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) {
|
||||
@@ -14572,7 +14658,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) {
|
||||
*kind = key->v.fn.class_obj ? 3 : 2; return 1;
|
||||
case SCR_DYN_HANDLE:
|
||||
if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP &&
|
||||
key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO &&
|
||||
key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_FINALIZATION_REGISTRY &&
|
||||
key->v.handle.tag != SCR_DYNH_STDIO &&
|
||||
key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE &&
|
||||
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET &&
|
||||
key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1;
|
||||
@@ -14751,6 +14838,159 @@ bool scr_weak_set_is(const ScrDyn *value) {
|
||||
ScrDyn *scr_weak_map_new(ScrDyn *seed) { return scr_weak_collection_new(seed, false); }
|
||||
ScrDyn *scr_weak_set_new(ScrDyn *seed) { return scr_weak_collection_new(seed, true); }
|
||||
|
||||
/* FinalizationRegistry. register() and unregister() validate and keep their
|
||||
* bookkeeping as Node does; the cleanup callback never runs, which
|
||||
* ECMAScript allows (an implementation need not collect a target or call
|
||||
* its cleanup), so the registry keeps neither the callback nor the held
|
||||
* values. The unregister tokens are keys of an internal WeakMap counting
|
||||
* each token's registrations, so the registry keeps no token alive. */
|
||||
typedef struct {
|
||||
size_t rc;
|
||||
ScrDyn *tokens; /* WeakMap: token -> registration count */
|
||||
} ScrFinalizationRegistry;
|
||||
|
||||
static void *scr_finalization_registry_retain(void *ptr) {
|
||||
((ScrFinalizationRegistry *)ptr)->rc++;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void scr_finalization_registry_release(void *ptr) {
|
||||
ScrFinalizationRegistry *registry = ptr;
|
||||
if (--registry->rc) return;
|
||||
/* Weak collections keyed by the registry drop their entries. */
|
||||
scr_weak_dispose(registry);
|
||||
scr_dyn_release(registry->tokens);
|
||||
free(registry);
|
||||
}
|
||||
|
||||
static void scr_finalization_type_error(const char *message) {
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
|
||||
}
|
||||
|
||||
/* The weak-key identity of a registration target or token, or false with
|
||||
* the TypeError (or the SC2020 refusal) pending. */
|
||||
static bool scr_finalization_key(const ScrDyn *value, void **ptr, unsigned *kind,
|
||||
const char *invalid) {
|
||||
if (value->kind == SCR_DYN_SYMBOL) {
|
||||
static const char message[] = "FinalizationRegistry with symbol targets or tokens is not supported yet";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
return false;
|
||||
}
|
||||
int supported = scr_weak_key(value, ptr, kind);
|
||||
if (supported < 0) {
|
||||
static const char message[] = "FinalizationRegistry targets of this native reference type have no weak lifetime lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
return false;
|
||||
}
|
||||
if (!supported) {
|
||||
if (invalid) {
|
||||
scr_finalization_type_error(invalid);
|
||||
} else {
|
||||
/* Node's unregister message quotes the token as String() renders it. */
|
||||
ScrStr *text = scr_dyn_string_coerce(value);
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "Invalid unregisterToken ('");
|
||||
if (text) scr_jb_write(&b, text->data, text->len);
|
||||
scr_jb_puts(&b, "')");
|
||||
scr_str_release(text);
|
||||
ScrStr *message = scr_jb_finish(&b);
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len);
|
||||
scr_str_release(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_finalization_registry_invoke(void *ptr, ScrDyn *self, const char *method,
|
||||
ScrDyn *const *args, size_t argc, const char *what) {
|
||||
(void)self;
|
||||
ScrFinalizationRegistry *registry = ptr;
|
||||
ScrWeakMap *tokens = registry->tokens->v.handle.ptr;
|
||||
ScrDyn *first = argc > 0 ? args[0] : scr_dyn_undefined();
|
||||
void *identity = NULL;
|
||||
unsigned kind = 0;
|
||||
if (strcmp(method, "register") == 0) {
|
||||
ScrDyn *held = argc > 1 ? args[1] : scr_dyn_undefined();
|
||||
ScrDyn *token = argc > 2 ? args[2] : scr_dyn_undefined();
|
||||
if (!scr_finalization_key(first, &identity, &kind, "FinalizationRegistry.prototype.register: invalid target"))
|
||||
return NULL;
|
||||
if (scr_dyn_strict_eq(first, held)) {
|
||||
scr_finalization_type_error("FinalizationRegistry.prototype.register: target and holdings must not be same");
|
||||
return NULL;
|
||||
}
|
||||
if (token->kind != SCR_DYN_UNDEF) {
|
||||
if (!scr_finalization_key(token, &identity, &kind, NULL)) return NULL;
|
||||
ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind);
|
||||
double count = entry && entry->value->kind == SCR_DYN_NUM ? entry->value->v.num : 0;
|
||||
ScrDyn *next = scr_dyn_new_num(count + 1);
|
||||
bool ok = scr_weak_set(tokens, token, next);
|
||||
scr_dyn_release(next);
|
||||
if (!ok) return NULL;
|
||||
}
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
if (strcmp(method, "unregister") == 0) {
|
||||
if (!scr_finalization_key(first, &identity, &kind, NULL)) return NULL;
|
||||
ScrWeakEntry *entry = scr_weak_find(tokens, identity, kind);
|
||||
if (entry) scr_weak_remove(entry);
|
||||
return scr_dyn_new_bool(entry != NULL);
|
||||
}
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, what ? what : method);
|
||||
scr_jb_puts(&b, " is not a function");
|
||||
ScrStr *message = scr_jb_finish(&b);
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message->data, message->len);
|
||||
scr_str_release(message);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* A property read answers undefined for names the registry does not have.
|
||||
* Its methods read as values (`const register = registry.register`) are
|
||||
* refused, as a weak collection's are. */
|
||||
static ScrDyn *scr_finalization_registry_get(void *ptr, const char *key, size_t len) {
|
||||
(void)ptr;
|
||||
if ((len == 8 && memcmp(key, "register", 8) == 0) || (len == 10 && memcmp(key, "unregister", 10) == 0)) {
|
||||
static const char message[] = "FinalizationRegistry method values have no native lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_finalization_registry_view(void *ptr) {
|
||||
(void)ptr;
|
||||
return scr_dyn_new_obj();
|
||||
}
|
||||
|
||||
ScrDyn *scr_finalization_registry_new(ScrDyn *cleanup) {
|
||||
if (!scr_dyn_is_callable(cleanup)) {
|
||||
scr_finalization_type_error("FinalizationRegistry: cleanup must be callable");
|
||||
return NULL;
|
||||
}
|
||||
static const ScrDynHandleOps ops = {
|
||||
.cls = "FinalizationRegistry",
|
||||
.retain = &scr_finalization_registry_retain,
|
||||
.release = &scr_finalization_registry_release,
|
||||
.invoke = &scr_finalization_registry_invoke,
|
||||
.get = &scr_finalization_registry_get,
|
||||
.inspect_view = &scr_finalization_registry_view,
|
||||
.inspect_view_empty = true,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_FINALIZATION_REGISTRY, &ops);
|
||||
ScrFinalizationRegistry *registry = calloc(1, sizeof *registry);
|
||||
if (!registry) scr_json_oom();
|
||||
registry->tokens = scr_weak_collection_new(scr_dyn_undefined(), false);
|
||||
if (!registry->tokens) { free(registry); return NULL; }
|
||||
ScrDyn *result = scr_dyn_new_handle(registry, SCR_DYNH_FINALIZATION_REGISTRY);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool scr_finalization_registry_is(const ScrDyn *value) {
|
||||
return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_FINALIZATION_REGISTRY;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_from_entries(ScrDyn *seed) {
|
||||
if (seed->kind == SCR_DYN_UNDEF || seed->kind == SCR_DYN_NULL) {
|
||||
static const char message[] = "undefined is not iterable";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* The Linux backend of the platform readiness contract (scr_platform.h):
|
||||
* epoll for fd readiness, one timerfd per armed one-shot timer. This is
|
||||
* the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it
|
||||
* epoll for fd readiness, one timerfd for every armed one-shot timer. This
|
||||
* is the LIVE Linux poller behind scr_net.c and scr_dgram.c (native-toolchain.ts links it
|
||||
* alongside scr_loop_kqueue.c whenever those units compile; each TU is
|
||||
* empty off its platform). Child-exit wakeups are NOT here: scr_child.c
|
||||
* keeps its narrow seam inline (kqueue EVFILT_PROC on BSD, pidfd_open +
|
||||
@@ -9,18 +9,22 @@
|
||||
*
|
||||
* Design notes:
|
||||
* - Interest is keyed by fd with ONE mask under epoll, so the poller keeps
|
||||
* a linear fd -> (mask, udata) table and merges read/write interest into
|
||||
* EPOLL_CTL_MOD. Linear scan is fine at the units' scale (dozens of fds,
|
||||
* drained in 64-event batches today); swap for a hash if profiles say so.
|
||||
* an fd -> (mask, udata) table (indexed by fd number) and merges
|
||||
* read/write interest into EPOLL_CTL_MOD.
|
||||
* - EPOLLHUP/EPOLLERR/EPOLLRDHUP report as READABLE (plus WRITABLE when
|
||||
* write interest is armed): the units learn EOF/errors from read()==0 or
|
||||
* errno exactly as they do from kqueue's EV_EOF today. Note an fd is in
|
||||
* the epoll set ONLY while some interest is armed, so a consumer-less
|
||||
* socket can never busy-report HUP.
|
||||
* - Timers: EVFILT_TIMER has no epoll analogue, so each armed key owns a
|
||||
* TFD_NONBLOCK timerfd registered EPOLLIN in the same epoll set, tagged
|
||||
* through the same table (mask=TIMER). Fired timers are disarmed and
|
||||
* their timerfd closed at delivery (kqueue's EV_ONESHOT semantics).
|
||||
* - Timers: EVFILT_TIMER has no epoll analogue. Armed keys live in a
|
||||
* deadline min-heap (a pointer hash finds a key's slot), and ONE timerfd
|
||||
* in the epoll set wakes the loop at the earliest deadline. Re-arming,
|
||||
* arming a later deadline and cancelling make no system call: the
|
||||
* timerfd is reprogrammed only when a deadline earlier than its own
|
||||
* arrives, or when it fires early (the heap then holds later deadlines).
|
||||
* A server's per-request idle-timer churn therefore costs nothing in the
|
||||
* kernel, like libuv's timer heap. Expired timers are delivered (and
|
||||
* disarmed: kqueue's EV_ONESHOT semantics) by the next drain.
|
||||
* - Units must scrp_forget a watched fd before close(2) (the contract's
|
||||
* epoll obligation). As a second line of defense against a missed
|
||||
* forget followed by fd-number reuse, watch self-heals: a MOD that
|
||||
@@ -32,47 +36,75 @@
|
||||
#include "scr_platform.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ── fd/timer poller ─────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
int fd; /* watched fd, or the timer's timerfd */
|
||||
unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE, or SCRP_TIMER */
|
||||
void *udata; /* delivery tag; for timers: the arm-time udata */
|
||||
void *timer_key; /* timers only: the caller's key */
|
||||
int fd; /* watched fd */
|
||||
unsigned mask; /* SCRP_READABLE|SCRP_WRITABLE */
|
||||
void *udata; /* delivery tag */
|
||||
} ScrpEntry;
|
||||
|
||||
typedef struct {
|
||||
double deadline; /* CLOCK_MONOTONIC milliseconds */
|
||||
uint64_t seq; /* arm order: equal deadlines fire first-armed first */
|
||||
void *key;
|
||||
void *udata;
|
||||
} ScrpTimer;
|
||||
|
||||
typedef struct {
|
||||
void *key;
|
||||
size_t idx; /* the key's heap index */
|
||||
} ScrpKeySlot;
|
||||
|
||||
struct ScrPoller {
|
||||
int ep;
|
||||
ScrpEntry *entries;
|
||||
size_t n, cap;
|
||||
size_t *slot_of_fd; /* fd -> entry index + 1 (0: unwatched) */
|
||||
size_t fd_cap;
|
||||
int tfd; /* the shared timerfd (-1 until a timer arms) */
|
||||
double tfd_deadline; /* what tfd is programmed for; INFINITY: disarmed */
|
||||
ScrpTimer *heap;
|
||||
size_t nt, heap_cap;
|
||||
ScrpKeySlot *key_slots; /* open addressing over keys (key NULL: empty) */
|
||||
size_t key_cap; /* power of two */
|
||||
uint64_t seq;
|
||||
};
|
||||
|
||||
static ScrpEntry *scrp_find_fd(ScrPoller *p, int fd) {
|
||||
for (size_t i = 0; i < p->n; i++)
|
||||
if (p->entries[i].fd == fd) return &p->entries[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrpEntry *scrp_find_key(ScrPoller *p, void *key) {
|
||||
for (size_t i = 0; i < p->n; i++)
|
||||
if ((p->entries[i].mask & SCRP_TIMER) != 0 && p->entries[i].timer_key == key) return &p->entries[i];
|
||||
return NULL;
|
||||
if (fd < 0 || (size_t)fd >= p->fd_cap || p->slot_of_fd[fd] == 0) return NULL;
|
||||
return &p->entries[p->slot_of_fd[fd] - 1];
|
||||
}
|
||||
|
||||
static void scrp_remove(ScrPoller *p, ScrpEntry *e) {
|
||||
size_t i = (size_t)(e - p->entries);
|
||||
p->entries[i] = p->entries[p->n - 1];
|
||||
p->slot_of_fd[e->fd] = 0;
|
||||
p->n--;
|
||||
if (i != p->n) {
|
||||
p->entries[i] = p->entries[p->n];
|
||||
p->slot_of_fd[p->entries[i].fd] = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool scrp_push(ScrPoller *p, ScrpEntry e) {
|
||||
if ((size_t)e.fd >= p->fd_cap) {
|
||||
size_t cap = p->fd_cap == 0 ? 64 : p->fd_cap;
|
||||
while (cap <= (size_t)e.fd) cap *= 2;
|
||||
size_t *grown = realloc(p->slot_of_fd, cap * sizeof *grown);
|
||||
if (grown == NULL) return false;
|
||||
memset(grown + p->fd_cap, 0, (cap - p->fd_cap) * sizeof *grown);
|
||||
p->slot_of_fd = grown;
|
||||
p->fd_cap = cap;
|
||||
}
|
||||
if (p->n == p->cap) {
|
||||
size_t cap = p->cap == 0 ? 16 : p->cap * 2;
|
||||
ScrpEntry *grown = realloc(p->entries, cap * sizeof *grown);
|
||||
@@ -81,6 +113,117 @@ static bool scrp_push(ScrPoller *p, ScrpEntry e) {
|
||||
p->cap = cap;
|
||||
}
|
||||
p->entries[p->n++] = e;
|
||||
p->slot_of_fd[e.fd] = p->n;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── the timer heap ── */
|
||||
|
||||
static double scrp_now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
static size_t scrp_key_hash(const ScrPoller *p, const void *key) {
|
||||
uintptr_t h = (uintptr_t)key;
|
||||
h ^= h >> 17;
|
||||
h *= (uintptr_t)0x9E3779B97F4A7C15ull;
|
||||
h ^= h >> 29;
|
||||
return (size_t)h & (p->key_cap - 1);
|
||||
}
|
||||
|
||||
/* The hash slot holding `key`, or the empty slot where it would go. */
|
||||
static size_t scrp_key_probe(const ScrPoller *p, const void *key) {
|
||||
size_t i = scrp_key_hash(p, key);
|
||||
while (p->key_slots[i].key != NULL && p->key_slots[i].key != key)
|
||||
i = (i + 1) & (p->key_cap - 1);
|
||||
return i;
|
||||
}
|
||||
|
||||
static ScrpKeySlot *scrp_key_find(const ScrPoller *p, const void *key) {
|
||||
if (p->key_cap == 0) return NULL;
|
||||
size_t i = scrp_key_probe(p, key);
|
||||
return p->key_slots[i].key != NULL ? &p->key_slots[i] : NULL;
|
||||
}
|
||||
|
||||
/* Backward-shift deletion keeps linear probing tombstone-free. */
|
||||
static void scrp_key_erase(ScrPoller *p, size_t i) {
|
||||
size_t mask = p->key_cap - 1;
|
||||
p->key_slots[i].key = NULL;
|
||||
for (size_t j = (i + 1) & mask; p->key_slots[j].key != NULL; j = (j + 1) & mask) {
|
||||
size_t home = scrp_key_hash(p, p->key_slots[j].key);
|
||||
/* move j into the hole when its home is not cyclically within (i, j] */
|
||||
if (((j - home) & mask) >= ((j - i) & mask)) {
|
||||
p->key_slots[i] = p->key_slots[j];
|
||||
p->key_slots[j].key = NULL;
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool scrp_key_grow(ScrPoller *p) {
|
||||
size_t cap = p->key_cap == 0 ? 64 : p->key_cap * 2;
|
||||
ScrpKeySlot *slots = calloc(cap, sizeof *slots);
|
||||
if (slots == NULL) return false;
|
||||
free(p->key_slots);
|
||||
p->key_slots = slots;
|
||||
p->key_cap = cap;
|
||||
for (size_t h = 0; h < p->nt; h++)
|
||||
p->key_slots[scrp_key_probe(p, p->heap[h].key)] = (ScrpKeySlot){p->heap[h].key, h};
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool scrp_timer_less(const ScrpTimer *a, const ScrpTimer *b) {
|
||||
return a->deadline < b->deadline || (a->deadline == b->deadline && a->seq < b->seq);
|
||||
}
|
||||
|
||||
/* Place `t` at heap index `i` and record it in the key table. */
|
||||
static void scrp_heap_put(ScrPoller *p, size_t i, ScrpTimer t) {
|
||||
p->heap[i] = t;
|
||||
scrp_key_find(p, t.key)->idx = i;
|
||||
}
|
||||
|
||||
static void scrp_heap_fix(ScrPoller *p, size_t i) {
|
||||
ScrpTimer t = p->heap[i];
|
||||
while (i > 0) {
|
||||
size_t parent = (i - 1) / 2;
|
||||
if (!scrp_timer_less(&t, &p->heap[parent])) break;
|
||||
scrp_heap_put(p, i, p->heap[parent]);
|
||||
i = parent;
|
||||
}
|
||||
for (;;) {
|
||||
size_t child = 2 * i + 1;
|
||||
if (child >= p->nt) break;
|
||||
if (child + 1 < p->nt && scrp_timer_less(&p->heap[child + 1], &p->heap[child])) child++;
|
||||
if (!scrp_timer_less(&p->heap[child], &t)) break;
|
||||
scrp_heap_put(p, i, p->heap[child]);
|
||||
i = child;
|
||||
}
|
||||
scrp_heap_put(p, i, t);
|
||||
}
|
||||
|
||||
static void scrp_heap_remove(ScrPoller *p, size_t i) {
|
||||
scrp_key_erase(p, scrp_key_probe(p, p->heap[i].key));
|
||||
p->nt--;
|
||||
if (i == p->nt) return;
|
||||
p->heap[i] = p->heap[p->nt];
|
||||
scrp_heap_fix(p, i);
|
||||
}
|
||||
|
||||
/* Program the shared timerfd for `deadline` (INFINITY disarms). */
|
||||
static bool scrp_tfd_program(ScrPoller *p, double deadline) {
|
||||
struct itimerspec its;
|
||||
memset(&its, 0, sizeof its);
|
||||
if (deadline != INFINITY) {
|
||||
if (deadline < 0) deadline = 0;
|
||||
its.it_value.tv_sec = (time_t)(deadline / 1000.0);
|
||||
its.it_value.tv_nsec = (long)((deadline - (double)its.it_value.tv_sec * 1000.0) * 1e6);
|
||||
if (its.it_value.tv_nsec >= 1000000000L) its.it_value.tv_nsec = 999999999L;
|
||||
if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms */
|
||||
}
|
||||
if (timerfd_settime(p->tfd, TFD_TIMER_ABSTIME, &its, NULL) != 0) return false;
|
||||
p->tfd_deadline = deadline;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,18 +235,21 @@ ScrPoller *scrp_poller_new(void) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
p->tfd = -1;
|
||||
p->tfd_deadline = INFINITY;
|
||||
return p;
|
||||
}
|
||||
|
||||
void scrp_poller_free(ScrPoller *p) {
|
||||
if (p == NULL) return;
|
||||
for (size_t i = 0; i < p->n; i++)
|
||||
if ((p->entries[i].mask & SCRP_TIMER) != 0) close(p->entries[i].fd);
|
||||
if (p->tfd >= 0) close(p->tfd);
|
||||
close(p->ep);
|
||||
free(p->entries);
|
||||
free(p->slot_of_fd);
|
||||
free(p->heap);
|
||||
free(p->key_slots);
|
||||
free(p);
|
||||
}
|
||||
|
||||
int scrp_poller_fd(const ScrPoller *p) { return p->ep; }
|
||||
|
||||
static uint32_t scrp_epoll_mask(unsigned mask) {
|
||||
@@ -124,7 +270,7 @@ static bool scrp_watch(ScrPoller *p, int fd, void *udata, bool on, unsigned bit)
|
||||
struct epoll_event ev = {.events = scrp_epoll_mask(next), .data = {.fd = fd}};
|
||||
if (e == NULL) {
|
||||
if (next == 0) return true; /* unwatch of an unwatched fd: no-op */
|
||||
if (!scrp_push(p, (ScrpEntry){fd, next, udata, NULL})) return false;
|
||||
if (!scrp_push(p, (ScrpEntry){fd, next, udata})) return false;
|
||||
int rc = epoll_ctl(p->ep, EPOLL_CTL_ADD, fd, &ev);
|
||||
if (rc != 0 && errno == EEXIST) rc = epoll_ctl(p->ep, EPOLL_CTL_MOD, fd, &ev);
|
||||
if (rc != 0) {
|
||||
@@ -163,41 +309,51 @@ void scrp_forget(ScrPoller *p, int fd) {
|
||||
|
||||
bool scrp_timer_arm(ScrPoller *p, void *key, double ms, void *udata) {
|
||||
if (!(ms >= 0)) ms = 0;
|
||||
ScrpEntry *e = scrp_find_key(p, key);
|
||||
int tfd = e != NULL ? e->fd : timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||
if (tfd < 0) return false;
|
||||
struct itimerspec its;
|
||||
memset(&its, 0, sizeof its);
|
||||
its.it_value.tv_sec = (time_t)(ms / 1000.0);
|
||||
its.it_value.tv_nsec = (long)((ms - (double)its.it_value.tv_sec * 1000.0) * 1e6);
|
||||
if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1; /* 0 disarms timerfd */
|
||||
if (timerfd_settime(tfd, 0, &its, NULL) != 0) {
|
||||
if (e == NULL) close(tfd);
|
||||
return false;
|
||||
if (p->tfd < 0) {
|
||||
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||
if (tfd < 0) return false;
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}};
|
||||
if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) {
|
||||
close(tfd);
|
||||
return false;
|
||||
}
|
||||
p->tfd = tfd;
|
||||
}
|
||||
if (e != NULL) {
|
||||
e->udata = udata;
|
||||
return true; /* re-arm replaces the deadline */
|
||||
ScrpTimer t = {scrp_now_ms() + ms, p->seq++, key, udata};
|
||||
ScrpKeySlot *slot = scrp_key_find(p, key);
|
||||
if (slot != NULL) {
|
||||
/* re-arm replaces the deadline */
|
||||
size_t i = slot->idx;
|
||||
p->heap[i] = t;
|
||||
scrp_heap_fix(p, i);
|
||||
} else {
|
||||
if (p->nt == p->heap_cap) {
|
||||
size_t cap = p->heap_cap == 0 ? 16 : p->heap_cap * 2;
|
||||
ScrpTimer *grown = realloc(p->heap, cap * sizeof *grown);
|
||||
if (grown == NULL) return false;
|
||||
p->heap = grown;
|
||||
p->heap_cap = cap;
|
||||
}
|
||||
if ((p->nt + 1) * 2 > p->key_cap && !scrp_key_grow(p)) return false;
|
||||
p->heap[p->nt] = t;
|
||||
p->key_slots[scrp_key_probe(p, key)] = (ScrpKeySlot){key, p->nt};
|
||||
p->nt++;
|
||||
scrp_heap_fix(p, p->nt - 1);
|
||||
}
|
||||
if (!scrp_push(p, (ScrpEntry){tfd, SCRP_TIMER, udata, key})) {
|
||||
close(tfd);
|
||||
return false;
|
||||
}
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = tfd}};
|
||||
if (epoll_ctl(p->ep, EPOLL_CTL_ADD, tfd, &ev) != 0) {
|
||||
scrp_remove(p, &p->entries[p->n - 1]);
|
||||
close(tfd);
|
||||
/* Only an earlier deadline needs the kernel: a later one is found when
|
||||
* the timerfd fires early. */
|
||||
if (p->heap[0].deadline < p->tfd_deadline && !scrp_tfd_program(p, p->heap[0].deadline)) {
|
||||
scrp_heap_remove(p, scrp_key_find(p, key)->idx);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void scrp_timer_cancel(ScrPoller *p, void *key) {
|
||||
ScrpEntry *e = scrp_find_key(p, key);
|
||||
if (e == NULL) return; /* unarmed or already fired: no-op */
|
||||
(void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL);
|
||||
close(e->fd);
|
||||
scrp_remove(p, e);
|
||||
ScrpKeySlot *slot = scrp_key_find(p, key);
|
||||
if (slot == NULL) return; /* unarmed or already fired: no-op */
|
||||
/* the timerfd keeps its deadline; an early wake finds nothing due */
|
||||
scrp_heap_remove(p, slot->idx);
|
||||
}
|
||||
|
||||
int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) {
|
||||
@@ -205,20 +361,17 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) {
|
||||
struct epoll_event evs[64];
|
||||
int want = max < 64 ? max : 64;
|
||||
int n = epoll_wait(p->ep, evs, want, 0);
|
||||
if (n <= 0) return 0; /* EINTR/none: a spurious pass */
|
||||
int filled = 0;
|
||||
bool timer_woke = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd);
|
||||
if (e == NULL) continue; /* raced with forget */
|
||||
if ((e->mask & SCRP_TIMER) != 0) {
|
||||
out[filled].udata = e->udata;
|
||||
out[filled++].events = SCRP_TIMER;
|
||||
/* one-shot: disarm at delivery, EV_ONESHOT semantics */
|
||||
(void)epoll_ctl(p->ep, EPOLL_CTL_DEL, e->fd, NULL);
|
||||
close(e->fd);
|
||||
scrp_remove(p, e);
|
||||
if (evs[i].data.fd == p->tfd && p->tfd >= 0) {
|
||||
uint64_t ticks;
|
||||
(void)!read(p->tfd, &ticks, sizeof ticks);
|
||||
timer_woke = true;
|
||||
continue;
|
||||
}
|
||||
ScrpEntry *e = scrp_find_fd(p, evs[i].data.fd);
|
||||
if (e == NULL) continue; /* raced with forget */
|
||||
unsigned got = 0;
|
||||
uint32_t ep = evs[i].events;
|
||||
if ((ep & (EPOLLIN | EPOLLHUP | EPOLLERR | EPOLLRDHUP)) != 0) got |= SCRP_READABLE;
|
||||
@@ -229,6 +382,21 @@ int scrp_drain(ScrPoller *p, ScrPollerEvent *out, int max) {
|
||||
out[filled].udata = e->udata;
|
||||
out[filled++].events = got;
|
||||
}
|
||||
/* The timerfd always holds a deadline no later than the heap's earliest
|
||||
* (arming an earlier one reprograms it), so a timer can only be due
|
||||
* after it fires. */
|
||||
if (timer_woke) {
|
||||
p->tfd_deadline = INFINITY; /* one-shot: it fired */
|
||||
/* deliver every expired timer the batch has room for, one-shot */
|
||||
double now = scrp_now_ms();
|
||||
while (p->nt > 0 && filled < max && p->heap[0].deadline <= now) {
|
||||
out[filled].udata = p->heap[0].udata;
|
||||
out[filled++].events = SCRP_TIMER;
|
||||
scrp_heap_remove(p, 0);
|
||||
}
|
||||
/* the next deadline (one already past fires at once) */
|
||||
if (p->nt > 0) (void)scrp_tfd_program(p, p->heap[0].deadline);
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user