mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # packages/compiler/test/ts7/baselines/order-parity.json
This commit is contained in:
@@ -9,6 +9,45 @@ import type {
|
||||
} from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Lines that decide whether the double `index` is an integer in
|
||||
* `[0, length)`, where `length` is an unsigned `sizeType` count, and give
|
||||
* that integer as an i64. `%{ok}` is the answer; `%{wide}` is meaningful
|
||||
* only when it holds.
|
||||
*
|
||||
* A signed round trip replaces the unsigned one (`fptoui`/`uitofp` expand
|
||||
* to long sequences on x86-64), and an integer compare replaces two float
|
||||
* compares against the converted length. `freeze` makes an out-of-range or
|
||||
* NaN conversion an arbitrary integer instead of poison; such an integer
|
||||
* never converts back to the index, or else is not below the length. Below
|
||||
* the length (far under 2^53) the round trip is exact, so equality means the
|
||||
* index is that integer. -0 converts to 0, as ToPropertyKey(-0) is "0". */
|
||||
export function exactIndexLines(
|
||||
index: string,
|
||||
length: string,
|
||||
sizeType: string,
|
||||
tmp: () => string,
|
||||
): { lines: string[]; ok: string; wide: string } {
|
||||
const raw = tmp(),
|
||||
wide = tmp(),
|
||||
back = tmp(),
|
||||
integral = tmp(),
|
||||
below = tmp(),
|
||||
ok = tmp();
|
||||
const lines = [
|
||||
`${raw} = fptosi double ${index} to i64`,
|
||||
`${wide} = freeze i64 ${raw}`,
|
||||
`${back} = sitofp i64 ${wide} to double`,
|
||||
`${integral} = fcmp oeq double ${back}, ${index}`,
|
||||
];
|
||||
let len64 = length;
|
||||
if (sizeType !== "i64") {
|
||||
len64 = tmp();
|
||||
lines.push(`${len64} = zext ${sizeType} ${length} to i64`);
|
||||
}
|
||||
lines.push(`${below} = icmp ult i64 ${wide}, ${len64}`, `${ok} = and i1 ${integral}, ${below}`);
|
||||
return { lines, ok, wide };
|
||||
}
|
||||
|
||||
/** User-controlled text embedded after an LLVM `;` comment marker. Preserve
|
||||
* ordinary output byte-for-byte, but encode control and line-separator code
|
||||
* units so a property name can never inject a line or invalid source byte. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
import { BYTES_ELEM_NUM, f64Lit } from "./common.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
} from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { WalkerHost } from "./walkers.js";
|
||||
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
/** ScrDynKind values (scr_runtime.h). */
|
||||
/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */
|
||||
@@ -83,7 +85,9 @@ export interface DynHost extends WalkerHost {
|
||||
virtualEntry?(implFn: string): string;
|
||||
/** The immortal ABSENT field-slot state of an undefined-armed union. */
|
||||
absentInstanceRef(unionId: string): string;
|
||||
liveDynRefAdapter(t: IrType): { snapshot: string; commit: string };
|
||||
/** The shared adapter record: live array capsules read its `arrayOps`, so a
|
||||
* narrower return type would hand the native compiler a copy without it. */
|
||||
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
|
||||
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
@@ -212,6 +216,7 @@ export class LlDyn {
|
||||
B.line(
|
||||
`%result = call ptr ${typedRefConstructor(this.host, type)}(ptr %value, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.host.cstr(typeId)}, ${this.S} ${Buffer.byteLength(typeId, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(this.host, adapter, "%result")) B.line(line);
|
||||
} else {
|
||||
B.line(`%result = call ptr @${this.toDynHelper(type)}(${ty} %value)`);
|
||||
}
|
||||
@@ -3179,6 +3184,19 @@ export class LlDyn {
|
||||
const lNext = B.newLabel("kg.n");
|
||||
B.condBr(isTyped, lTyped, lNext);
|
||||
B.startBlock(lTyped);
|
||||
// Live arrays answer `length` and own elements from the native array;
|
||||
// rebuilding the snapshot per read would make index loops quadratic.
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_key_get(ptr, ptr)`);
|
||||
const fast = B.tmp();
|
||||
const hit = B.tmp();
|
||||
B.line(`${fast} = call ptr @scr_dyn_typed_ref_key_get(ptr %d, ptr %k)`);
|
||||
B.line(`${hit} = icmp ne ptr ${fast}, null`);
|
||||
const lFast = B.newLabel("kg.tr.fast");
|
||||
const lView = B.newLabel("kg.tr.view");
|
||||
B.condBr(hit, lFast, lView);
|
||||
B.startBlock(lFast);
|
||||
B.terminate(`ret ptr ${fast}`);
|
||||
B.startBlock(lView);
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const materialized = B.tmp();
|
||||
|
||||
@@ -99,6 +99,8 @@ import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
import { findIntegerViews } from "./integer-views.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
@@ -174,6 +176,7 @@ import { StackCaptures } from "./stack-captures.js";
|
||||
import {
|
||||
ABSENT_FIELD_PAYLOAD,
|
||||
emitFieldAbsentTest,
|
||||
exactIndexLines,
|
||||
f64Lit,
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
@@ -403,7 +406,7 @@ export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}):
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
return new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
|
||||
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
|
||||
: mod,
|
||||
options,
|
||||
).emit();
|
||||
@@ -416,7 +419,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
const parts = new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? scalarizeNumericRecords(specializeNumericCalls(mod, mangleFunction))
|
||||
? scalarizeNumericRecords(specializeNumericCalls(foldConstantGlobals(mod), mangleFunction))
|
||||
: mod,
|
||||
options,
|
||||
).emitParts();
|
||||
@@ -733,6 +736,11 @@ export class LlEmitter {
|
||||
{ slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } }
|
||||
>();
|
||||
private countedLoopsEnabled = false;
|
||||
/** Whole-program final reference fields (ir/final-fields.ts). */
|
||||
private finalFields!: FinalFields;
|
||||
/** The current function may borrow final fields of borrowable objects:
|
||||
* not a constructor (where they change), not async or a generator. */
|
||||
private finalFieldBorrows = false;
|
||||
integerRanges: IntegerRanges = new Map();
|
||||
private byteWindowsEnabled = true;
|
||||
private byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
|
||||
@@ -807,6 +815,7 @@ export class LlEmitter {
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings = findInitializerBindings(mod);
|
||||
this.int32Slots = analyzeInt32Slots(mod);
|
||||
this.finalFields = analyzeFinalFields(mod);
|
||||
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
|
||||
this.ffiExtendNarrowIntegers =
|
||||
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
|
||||
@@ -1918,25 +1927,18 @@ export class LlEmitter {
|
||||
const flushedDecls = new Set(this.decls);
|
||||
for (const table of this.constantNumericTables.values()) {
|
||||
const n = table.values.length;
|
||||
let tableTmp = 0;
|
||||
const tableIndex = exactIndexLines("%i", String(n), "i64", () => `%ix${tableTmp++}`);
|
||||
out.push(
|
||||
`@${table.symbol} = private constant [${n} x double] [${table.values.map((v) => `double ${f64Lit(v)}`).join(", ")}]`,
|
||||
`define internal double @${table.symbol}_get(ptr %a, double %i) alwaysinline #0 {`,
|
||||
`entry:`,
|
||||
` %initialized = icmp ne ptr %a, null`,
|
||||
` %nonnegative = fcmp oge double %i, ${f64Lit(0)}`,
|
||||
` %below = fcmp olt double %i, ${f64Lit(n)}`,
|
||||
` %range = and i1 %nonnegative, %below`,
|
||||
` %safe = and i1 %initialized, %range`,
|
||||
` br i1 %safe, label %convert, label %fallback`,
|
||||
`convert:`,
|
||||
// The branch must dominate fptoui: NaN/out-of-range conversion
|
||||
// would produce poison. Fractional indices also need a fallback.
|
||||
` %index = fptoui double %i to ${this.sizeType}`,
|
||||
` %roundtrip = sitofp ${this.sizeType} %index to double`,
|
||||
` %integer = fcmp oeq double %roundtrip, %i`,
|
||||
` br i1 %integer, label %read, label %fallback`,
|
||||
...tableIndex.lines.map((line) => ` ${line}`),
|
||||
` %safe = and i1 %initialized, ${tableIndex.ok}`,
|
||||
` br i1 %safe, label %read, label %fallback`,
|
||||
`read:`,
|
||||
` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, ${this.sizeType} 0, ${this.sizeType} %index`,
|
||||
` %slot = getelementptr inbounds [${n} x double], ptr @${table.symbol}, i64 0, i64 ${tableIndex.wide}`,
|
||||
` %value = load double, ptr %slot`,
|
||||
` ret double %value`,
|
||||
`fallback:`,
|
||||
@@ -2761,22 +2763,20 @@ export class LlEmitter {
|
||||
// keeps the complete SameValueZero semantics.
|
||||
if (this.decls.has(NUMBER_MAP_ENTRY_DECL)) {
|
||||
const sz = this.sizeType;
|
||||
let mapTmp = 0;
|
||||
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
|
||||
defs.push(
|
||||
`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 19`,
|
||||
` %nd = load ${sz}, ptr %ndp`,
|
||||
` %ndf = sitofp ${sz} %nd to double`,
|
||||
` %lo = fcmp oge double %k, 0.0`,
|
||||
` %hi = fcmp olt double %k, %ndf`,
|
||||
` %in = and i1 %lo, %hi`,
|
||||
` br i1 %in, label %direct, label %outside`,
|
||||
`direct:`,
|
||||
` %i = fptoui double %k to ${sz}`,
|
||||
` %back = sitofp ${sz} %i to double`,
|
||||
` %int = fcmp oeq double %back, %k`,
|
||||
` br i1 %int, label %slot, label %miss`,
|
||||
// A non-integral key inside the table is as absent as one outside it.
|
||||
...mapIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${mapIndex.ok}, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64"
|
||||
? ` %i = add i64 ${mapIndex.wide}, 0`
|
||||
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 18`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
@@ -4957,6 +4957,8 @@ export class LlEmitter {
|
||||
);
|
||||
this.integerLoopBindings.clear();
|
||||
this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator;
|
||||
this.finalFieldBorrows =
|
||||
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
|
||||
this.byteWindowsEnabled = this.countedLoopsEnabled;
|
||||
this.byteWindowEntries =
|
||||
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
@@ -5326,6 +5328,17 @@ export class LlEmitter {
|
||||
// stable owner. The throwing arm's typed dummy owns nothing.
|
||||
if (value.kind === "unionNarrow") return this.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "libCall") return value.fn === "error.nodeThrow";
|
||||
// A final field never changes outside constructors: while its object is
|
||||
// borrowable, so is the field's value. Nullable-pointer fields have no
|
||||
// box to lend and keep their owning path.
|
||||
if (
|
||||
value.kind === "fieldGet" &&
|
||||
this.finalFieldBorrows &&
|
||||
isRefCounted(value.type) &&
|
||||
this.finalFields.isFinal(value.className, value.field) &&
|
||||
!this.nullableFieldGet(value)
|
||||
)
|
||||
return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj);
|
||||
if (value.kind === "ternary")
|
||||
return (
|
||||
this.canBorrowReceiver(value.then) &&
|
||||
@@ -5333,6 +5346,9 @@ export class LlEmitter {
|
||||
this.canBorrowCallArgument(value.then) &&
|
||||
this.canBorrowCallArgument(value.else_)
|
||||
);
|
||||
// An optional chain's bound receiver stays unchanged until its body
|
||||
// finishes (see emitReadReceiver).
|
||||
if (value.kind === "chainRecv") return this.chainSlots.has(value.id);
|
||||
if (value.kind !== "varRef") return false;
|
||||
const binding = this.binding(value.localId);
|
||||
return (
|
||||
@@ -6045,14 +6061,49 @@ export class LlEmitter {
|
||||
}
|
||||
let countedJoin: string | null = null;
|
||||
if (countedLoop?.guarded) {
|
||||
const bound = this.emitExpr(countedLoop.limit);
|
||||
const safe = B.tmp();
|
||||
let safe = "true";
|
||||
if (countedLoop.limitGuarded) {
|
||||
const bound = this.emitExpr(countedLoop.limit);
|
||||
safe = B.tmp();
|
||||
B.line(
|
||||
`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`,
|
||||
);
|
||||
}
|
||||
if (countedLoop.startGuarded) {
|
||||
// An exact integer of at most 2^53 - 1 in magnitude, and not -0
|
||||
// (the bit patterns of the value and its round trip agree).
|
||||
// NaN and infinities fail the ordered range tests.
|
||||
const start = this.emitExpr(countedLoop.start);
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
wide = B.tmp(),
|
||||
back = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
backBits = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${start.name} to i64`);
|
||||
B.line(`${wide} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${wide} to double`);
|
||||
B.line(`${bits} = bitcast double ${start.name} to i64`);
|
||||
B.line(`${backBits} = bitcast double ${back} to i64`);
|
||||
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
if (safe === "true") safe = ok;
|
||||
else {
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${safe}, ${ok}`);
|
||||
safe = both;
|
||||
}
|
||||
}
|
||||
const fast = B.newLabel("counted.fast");
|
||||
const slow = B.newLabel("counted.slow");
|
||||
countedJoin = B.newLabel("counted.done");
|
||||
B.line(
|
||||
`${safe} = fcmp ${countedLoop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(countedLoop.guardLimit)}`,
|
||||
);
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
this.countedLoopsEnabled = false;
|
||||
@@ -7254,6 +7305,16 @@ export class LlEmitter {
|
||||
this.B.line(`${value} = load ptr, ptr ${binding.slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "chainRecv") {
|
||||
// The chain's bind slot holds its receiver unchanged until the body
|
||||
// finishes, owned by the slot itself or by the chain's receiver.
|
||||
const bound = this.chainSlots.get(e.id);
|
||||
if (!bound)
|
||||
throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
|
||||
const value = this.B.tmp();
|
||||
this.B.line(`${value} = load ${this.llType(bound.type)}, ptr ${bound.name}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "arrayGet" && this.canBorrowReceiver(e)) {
|
||||
// The array keeps the element alive until the consumer; a missing
|
||||
// element still traps exactly like the owned read.
|
||||
@@ -7506,6 +7567,8 @@ export class LlEmitter {
|
||||
|
||||
canBorrowReceiver(e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "chainRecv":
|
||||
return this.chainSlots.has(e.id);
|
||||
case "varRef": {
|
||||
const binding = this.binding(e.localId);
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { F64_INF, f64Lit } from "./common.js";
|
||||
import { F64_INF, exactIndexLines, f64Lit } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
@@ -118,28 +118,12 @@ export function emitBytesIndex(
|
||||
B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`);
|
||||
return narrow;
|
||||
}
|
||||
const lenF64 = B.tmp();
|
||||
const nonnegative = B.tmp();
|
||||
const belowLen = B.tmp();
|
||||
const inRange = B.tmp();
|
||||
B.line(`${lenF64} = sitofp ${host.sizeType} ${len} to double`);
|
||||
B.line(`${nonnegative} = fcmp oge double ${index}, ${f64Lit(0)}`);
|
||||
B.line(`${belowLen} = fcmp olt double ${index}, ${lenF64}`);
|
||||
B.line(`${inRange} = and i1 ${nonnegative}, ${belowLen}`);
|
||||
|
||||
const rangeOk = B.newLabel("bytes.index.range");
|
||||
const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp());
|
||||
for (const line of exact.lines) B.line(line);
|
||||
const invalid = B.newLabel("bytes.index.invalid");
|
||||
const valid = B.newLabel("bytes.index.valid");
|
||||
B.condBr(inRange, rangeOk, invalid);
|
||||
|
||||
B.startBlock(rangeOk);
|
||||
const idx = B.tmp();
|
||||
const roundTrip = B.tmp();
|
||||
const integral = B.tmp();
|
||||
B.line(`${idx} = fptoui double ${index} to ${host.sizeType}`);
|
||||
B.line(`${roundTrip} = sitofp ${host.sizeType} ${idx} to double`);
|
||||
B.line(`${integral} = fcmp oeq double ${roundTrip}, ${index}`);
|
||||
B.condBr(integral, valid, invalid);
|
||||
B.condBr(exact.ok, valid, invalid);
|
||||
let idx = exact.wide;
|
||||
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
@@ -151,6 +135,10 @@ export function emitBytesIndex(
|
||||
}
|
||||
|
||||
B.startBlock(valid);
|
||||
if (host.sizeType !== "i64") {
|
||||
idx = B.tmp();
|
||||
B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`);
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
@@ -197,6 +185,20 @@ export function emitBytesGet(
|
||||
const local = emitSharedBytesSplit(host, receiver);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @${fn}(ptr ${receiver}, double ${index.name})`);
|
||||
// Integer elements read as integers in int32/uint32 range, or NaN for a
|
||||
// missing one: ToUint32 is the integer, or 0. Keeping it lets the join
|
||||
// carry the local path's integer hint.
|
||||
let sharedU32: string | undefined;
|
||||
if (elem !== "f32" && elem !== "f64") {
|
||||
const missing = B.tmp(),
|
||||
present = B.tmp(),
|
||||
wide = B.tmp();
|
||||
sharedU32 = B.tmp();
|
||||
B.line(`${missing} = fcmp uno double ${result}, ${result}`);
|
||||
B.line(`${present} = select i1 ${missing}, double 0.0, double ${result}`);
|
||||
B.line(`${wide} = fptosi double ${present} to i64`);
|
||||
B.line(`${sharedU32} = trunc i64 ${wide} to i32`);
|
||||
}
|
||||
const shared = B.newLabel("bytes.shared.done");
|
||||
B.br(shared);
|
||||
B.startBlock(local);
|
||||
@@ -211,7 +213,10 @@ export function emitBytesGet(
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi double [ ${own.name}, %${owned} ], [ ${result}, %${shared} ]`);
|
||||
return { name: out, type: F64 };
|
||||
if (own.uint32 === undefined || sharedU32 === undefined) return { name: out, type: F64 };
|
||||
const u32 = B.tmp();
|
||||
B.line(`${u32} = phi i32 [ ${own.uint32}, %${owned} ], [ ${sharedU32}, %${shared} ]`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
}
|
||||
return emitBytesGetLocal(host, elem, receiver, index, expr, inBounds, invalidNaN);
|
||||
}
|
||||
@@ -265,9 +270,10 @@ function emitBytesGetLocal(
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi double [ ${loaded.name}, %${ok} ], [ ${f64Lit(NaN)}, %${invalid} ]`);
|
||||
// NaN has no integer value: the integer hint survives only when the read
|
||||
// is proven valid (the invalid block is then unreachable).
|
||||
if (loaded.uint32 === undefined || !inBounds) return { name: out, type: F64 };
|
||||
// The hint is ToUint32 of the value, and ToUint32(NaN) is 0, so it holds
|
||||
// for an invalid index too. Integer ranges (which would claim more) are
|
||||
// never derived for NaN-coded reads.
|
||||
if (loaded.uint32 === undefined) return { name: out, type: F64 };
|
||||
const u32 = B.tmp();
|
||||
B.line(`${u32} = phi i32 [ ${loaded.uint32}, %${ok} ], [ 0, %${invalid} ]`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
|
||||
@@ -28,4 +28,6 @@ export type LibCallPrefix = LibCallPrefixOf<IrLibFn>;
|
||||
export interface LlStreamTypedRefAdapter {
|
||||
snapshot: string;
|
||||
commit: string;
|
||||
/** `@sym` of the ScrDynTypedArrayOps table for live array capsules. */
|
||||
arrayOps?: string;
|
||||
}
|
||||
|
||||
@@ -282,21 +282,51 @@ export function emitControlExpr(
|
||||
throw new InternalCompilerError("llvm emitter bug: optChain union arms");
|
||||
const multiple = def.arms.length - unitTags.length > 1;
|
||||
const narrowed = multiple ? e.receiver.type : def.arms[narrowIdx]!;
|
||||
// `owner.field?.member`: a receiver projected from a borrowable owner
|
||||
// needs no reference of its own while the body cannot remove an edge
|
||||
// (so nothing can overwrite the projection before the body finishes).
|
||||
const borrowReceiver =
|
||||
!multiple &&
|
||||
isRefCounted(narrowed) &&
|
||||
host.canBorrowReceiver(e.receiver) &&
|
||||
host.referenceEffects.preserves(e.body);
|
||||
// A single present arm is only tested and extracted (+1) before the
|
||||
// body runs, so a fresh receiver box can stay on the stack.
|
||||
const r =
|
||||
!multiple && host.canStackReceiver(e.receiver)
|
||||
? host.emitReadReceiver(e.receiver)
|
||||
: host.emitExpr(e.receiver);
|
||||
const stacked = !borrowReceiver && !multiple && host.canStackReceiver(e.receiver);
|
||||
let r: LlValue;
|
||||
const nullableField = borrowReceiver ? host.nullableFieldGet(e.receiver) : null;
|
||||
if (nullableField !== null && e.receiver.kind === "fieldGet") {
|
||||
// A nullable-pointer field stores no union: test and peek the
|
||||
// pointer itself, through a private stack box when the union's own
|
||||
// representation is boxed (emitUnionProjection's form).
|
||||
const owner = host.emitReadReceiver(e.receiver.obj);
|
||||
const { ptr } = host.classFieldPtr(owner.name, e.receiver.className, e.receiver.field);
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
r = {
|
||||
name: host.nullableUnions.get(def.id) ? p : host.stackNullableBox(p, nullableField),
|
||||
type: e.receiver.type,
|
||||
};
|
||||
} else if (borrowReceiver || stacked) r = host.emitReadReceiver(e.receiver);
|
||||
else r = host.emitExpr(e.receiver);
|
||||
// The bind slot borrows a ref payload whenever something else keeps it
|
||||
// alive until the body finishes: the borrowed projection above, or the
|
||||
// receiver value the statement frame owns (emitExpr). Stack receivers
|
||||
// may borrow their payload from a container, so they keep their +1.
|
||||
const borrowBind = !multiple && !stacked && isRefCounted(narrowed);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${host.llType(narrowed)}`);
|
||||
B.line(
|
||||
`store ${host.llType(narrowed)} ${host.llType(narrowed) === "ptr" ? "null" : host.llType(narrowed) === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
|
||||
);
|
||||
host.ownSlot(bind, narrowed);
|
||||
if (!borrowBind) host.ownSlot(bind, narrowed);
|
||||
const isUnit = host.tagInSet(r.name, def.id, unitTags);
|
||||
const extract = (): string =>
|
||||
multiple ? host.retainValue(r.name, narrowed) : host.unionExtract(r.name, def.id, narrowed);
|
||||
multiple
|
||||
? host.retainValue(r.name, narrowed)
|
||||
: borrowBind
|
||||
? host.unionPeek(r.name, def.id)
|
||||
: host.unionExtract(r.name, def.id, narrowed);
|
||||
if (e.type.kind === "void") {
|
||||
// Statement form (cb?.()): no result value at all.
|
||||
const lb = B.newLabel("oc.b");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { classMembershipIntervals } from "./classes.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { preservesDynTest } from "./checked-value-lifetimes.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitNullableIsTag, emitNullablePresent } from "./union-repr.js";
|
||||
@@ -96,6 +97,7 @@ export function emitDynamicExpr(
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, v.type)}(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(host.shapeHost, adapter, boxed)) B.line(line);
|
||||
return host.own({ name: boxed, type: e.type });
|
||||
}
|
||||
if (v.type.kind === "func") {
|
||||
|
||||
@@ -17,7 +17,15 @@ import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { classFieldIndex, classStructSym } from "./classes.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
elemAccess,
|
||||
llFieldType,
|
||||
releaseSym,
|
||||
traceArg,
|
||||
vAdapters,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
import type { NullableRefField } from "./nullable-fields.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
@@ -567,6 +575,7 @@ export function liveDynUnionRefAdapter(
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, arm)}(ptr ${payload}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(armKey)}, ${host.sizeType} ${Buffer.byteLength(armKey, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(host.shapeHost, adapter, boxed)) B.line(line);
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
});
|
||||
valueArms.forEach(({ arm }, index) => {
|
||||
@@ -648,6 +657,7 @@ export function streamTypedRefBoxValue(
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, t)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${nested.snapshot}, ptr ${nested.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(host.shapeHost, nested, boxed)) B.line(line);
|
||||
return boxed;
|
||||
}
|
||||
|
||||
@@ -972,6 +982,9 @@ export function streamTypedRefMaterializeAdapter(
|
||||
}
|
||||
} else if (t.kind === "array") {
|
||||
const elem = t.elem;
|
||||
// Published before the element converters: a recursive element type
|
||||
// can box this same array type while the bodies below are emitted.
|
||||
adapter.arrayOps = `@${snapshot}_ops`;
|
||||
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
||||
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
||||
host.declare(`declare double @scr_arr_len(ptr)`);
|
||||
@@ -1033,6 +1046,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
host.declare(`declare void @scr_arr_copy_metadata(ptr, ptr)`);
|
||||
B.line(`call void @scr_arr_copy_metadata(ptr %p, ptr ${out})`);
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
liveArrayOps(host, t, snapshot);
|
||||
} else {
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call ptr @${host.dyn.toDynHelper(t)}(ptr %p)`);
|
||||
@@ -1048,6 +1062,91 @@ export function streamTypedRefMaterializeAdapter(
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/** Per-element hooks for a live array capsule (ScrDynTypedArrayOps). The
|
||||
* runtime answers presence and length from the native array and calls these
|
||||
* to convert exactly one element, so `xs[i]`, `xs[i] = v`, `i in xs`, and
|
||||
* iteration on a live array do not rebuild the whole snapshot per access.
|
||||
* `get` boxes an element exactly like one step of the snapshot loop; `set`
|
||||
* stores only values the element type accepts and otherwise reports false
|
||||
* without side effects, leaving the snapshot path to raise its diagnostic. */
|
||||
function liveArrayOps(
|
||||
host: LlvmEmitterContext,
|
||||
t: IrType & { kind: "array" },
|
||||
snapshot: string,
|
||||
): void {
|
||||
const elem = t.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const valueTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const get = `${snapshot}_get`;
|
||||
const set = `${snapshot}_set`;
|
||||
const ops = `${snapshot}_ops`;
|
||||
|
||||
const G = new BlockBuilder();
|
||||
const value = G.tmp();
|
||||
host.declare(
|
||||
`declare ${acc === "bool" ? "zeroext i1" : valueTy} @scr_arr_get_${acc}(ptr, double)`,
|
||||
);
|
||||
G.line(
|
||||
`${value} = call ${valueTy} @scr_arr_get_${acc}(ptr %p, double %i)${acc === "ref" ? " ; +1" : ""}`,
|
||||
);
|
||||
const boxed = host.streamTypedRefBoxValue(G, elem, value);
|
||||
if (isRefCounted(elem)) G.line(`call void ${releaseSym(host.shapeHost, elem)}(ptr ${value})`);
|
||||
G.terminate(`ret ptr ${boxed}`);
|
||||
|
||||
const S = new BlockBuilder();
|
||||
const fits = S.tmp();
|
||||
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
|
||||
const store = S.newLabel("live.array.store");
|
||||
const refuse = S.newLabel("live.array.refuse");
|
||||
S.condBr(fits, store, refuse);
|
||||
S.startBlock(refuse);
|
||||
S.terminate(`ret i1 false`);
|
||||
S.startBlock(store);
|
||||
const next = S.tmp();
|
||||
S.line(
|
||||
`${next} = call ${acc === "bool" ? "zeroext " : ""}${valueTy} @${host.dyn.dynCheckHelper(elem)}(ptr %v, ptr null)`,
|
||||
);
|
||||
// A conversion that throws (an accessor on a plain object, say) has
|
||||
// already raised the write's error; report it as handled.
|
||||
const pending = S.tmp();
|
||||
for (const line of host.pendingTestLines(pending)) S.line(line.trimStart());
|
||||
const write = S.newLabel("live.array.write");
|
||||
const failed = S.newLabel("live.array.failed");
|
||||
S.condBr(pending, failed, write);
|
||||
S.startBlock(failed);
|
||||
S.terminate(`ret i1 true`);
|
||||
S.startBlock(write);
|
||||
host.declare(
|
||||
`declare void @scr_arr_set_${acc}(ptr, double, ${acc === "bool" ? "i1 zeroext" : valueTy})`,
|
||||
);
|
||||
S.line(`call void @scr_arr_set_${acc}(ptr %p, double %i, ${valueTy} ${next}) ; takes ownership`);
|
||||
S.terminate(`ret i1 true`);
|
||||
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${get}(ptr %p, double %i) ${FN_ATTRS} { ; live element of ${typeKey(t)}`,
|
||||
G.render(),
|
||||
`}`,
|
||||
``,
|
||||
`define internal zeroext i1 @${set}(ptr %p, double %i, ptr %v) ${FN_ATTRS} { ; live element store into ${typeKey(t)}`,
|
||||
S.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${ops} = internal constant { ptr, ptr } { ptr @${get}, ptr @${set} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** Attach the element hooks right after a live array capsule is built. */
|
||||
export function bindLiveArrayOps(
|
||||
host: ShapeHost,
|
||||
adapter: LlStreamTypedRefAdapter,
|
||||
capsule: string,
|
||||
): string[] {
|
||||
if (adapter.arrayOps === undefined) return [];
|
||||
host.declare(`declare void @scr_dyn_typed_ref_bind_array(ptr, ptr)`);
|
||||
return [`call void @scr_dyn_typed_ref_bind_array(ptr ${capsule}, ptr ${adapter.arrayOps})`];
|
||||
}
|
||||
|
||||
export function streamFromArrayAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
t: IrType & { kind: "array" },
|
||||
@@ -1139,10 +1238,11 @@ export function streamFromArrayAdapter(
|
||||
const rc = vAdapters(host.shapeHost, elem);
|
||||
const keyPtr = host.cstr(key);
|
||||
let commit: string;
|
||||
let live: LlStreamTypedRefAdapter | undefined;
|
||||
if (streamTypedRefEligible(elem)) {
|
||||
const adapter = host.liveDynRefAdapter(elem);
|
||||
snapshot = adapter.snapshot;
|
||||
commit = adapter.commit;
|
||||
live = host.liveDynRefAdapter(elem);
|
||||
snapshot = live.snapshot;
|
||||
commit = live.commit;
|
||||
} else {
|
||||
commit = host.streamTypedRefCommitAdapter(elem, snapshot);
|
||||
host.resolveThunkDefs.push(
|
||||
@@ -1158,6 +1258,7 @@ export function streamFromArrayAdapter(
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host.shapeHost, elem)}(ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${keyPtr}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${snapshot}, ptr ${commit})`,
|
||||
);
|
||||
if (live) for (const line of bindLiveArrayOps(host.shapeHost, live, boxed)) B.line(line);
|
||||
} else {
|
||||
boxed = B.tmp();
|
||||
B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(elem)}(${valueTy} ${value})`);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { exactIndexLines } from "./common.js";
|
||||
|
||||
export interface LocalArrayRead {
|
||||
type: IrType;
|
||||
@@ -338,33 +339,29 @@ function emitDenseReferenceArrayRead(
|
||||
// The dense path uses the existing ScrArr ABI. Sparse indices and
|
||||
// noncanonical numeric properties retain the runtime lookup semantics.
|
||||
const capPtr = B.tmp(),
|
||||
cap = B.tmp(),
|
||||
capNumber = B.tmp(),
|
||||
nonnegative = B.tmp(),
|
||||
belowCap = B.tmp(),
|
||||
inRange = B.tmp();
|
||||
cap = B.tmp();
|
||||
B.line(`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 2`);
|
||||
host.markMemoryPointer(capPtr, "array:header");
|
||||
B.line(`${cap} = load ${host.sizeType}, ptr ${capPtr}${host.fieldAliasAttachment(capPtr)}`);
|
||||
if (integerIndex) B.line(`${inRange} = icmp ult ${host.sizeType} ${integerIndex}, ${cap}`);
|
||||
else {
|
||||
B.line(`${capNumber} = sitofp ${host.sizeType} ${cap} to double`);
|
||||
B.line(`${nonnegative} = fcmp oge double ${index.name}, 0.0`);
|
||||
B.line(`${belowCap} = fcmp olt double ${index.name}, ${capNumber}`);
|
||||
B.line(`${inRange} = and i1 ${nonnegative}, ${belowCap}`);
|
||||
let offset: string, inRange: string;
|
||||
if (integerIndex) {
|
||||
offset = integerIndex;
|
||||
inRange = B.tmp();
|
||||
B.line(`${inRange} = icmp ult ${host.sizeType} ${integerIndex}, ${cap}`);
|
||||
} else {
|
||||
// Capacity bounds the dense slots; the length check below still decides presence.
|
||||
const exact = exactIndexLines(index.name, cap, host.sizeType, () => B.tmp());
|
||||
for (const line of exact.lines) B.line(line);
|
||||
inRange = exact.ok;
|
||||
offset = exact.wide;
|
||||
if (host.sizeType !== "i64") {
|
||||
offset = B.tmp();
|
||||
B.line(`${offset} = trunc i64 ${exact.wide} to ${host.sizeType}`);
|
||||
}
|
||||
}
|
||||
B.condBr(inRange, range, slow);
|
||||
B.startBlock(range);
|
||||
const offset = integerIndex ?? B.tmp(),
|
||||
roundTrip = B.tmp(),
|
||||
integral = B.tmp();
|
||||
if (integerIndex) B.br(dense);
|
||||
else {
|
||||
B.line(`${offset} = fptoui double ${index.name} to ${host.sizeType}`);
|
||||
B.line(`${roundTrip} = sitofp ${host.sizeType} ${offset} to double`);
|
||||
B.line(`${integral} = fcmp oeq double ${index.name}, ${roundTrip}`);
|
||||
B.condBr(integral, dense, slow);
|
||||
}
|
||||
B.br(dense);
|
||||
B.startBlock(dense);
|
||||
const lenPtr = B.tmp(),
|
||||
len = B.tmp(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { preservesDynTest } from "./checked-value-lifetimes.js";
|
||||
import { isStableReceiverOperand } from "../../ir/analysis.js";
|
||||
import {
|
||||
isRefCounted,
|
||||
type IrArrIntrinsicMethod,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -38,6 +39,25 @@ export function preservesRegexInputs(method: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** Array operations that never release an element or run user code: reads,
|
||||
* copies into fresh arrays, and appends (an append retains the new element
|
||||
* and may move the slot storage, but every element stays referenced).
|
||||
* Removals (pop, shift, splice), truncation and overwrites are absent. */
|
||||
const EDGE_PRESERVING_ARRAY_METHODS: ReadonlySet<IrArrIntrinsicMethod> = new Set([
|
||||
"length",
|
||||
"getNumber",
|
||||
"indexEq",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"nextPresent",
|
||||
"join",
|
||||
"slice",
|
||||
"concatSpread",
|
||||
"toReversed",
|
||||
"push",
|
||||
"pushSpread",
|
||||
]);
|
||||
|
||||
function expressionPreservesEdges(
|
||||
e: IrExpr,
|
||||
call: (value: Call) => boolean,
|
||||
@@ -80,6 +100,10 @@ function expressionPreservesEdges(
|
||||
case "caughtCheck":
|
||||
// An immortal class object, or a fresh one retaining captured boxes.
|
||||
case "classRef":
|
||||
// A chain evaluates its receiver and body (checked as children); its
|
||||
// bound receiver is a read.
|
||||
case "optChain":
|
||||
case "chainRecv":
|
||||
return true;
|
||||
// Formatting a primitive allocates a string and runs no user code.
|
||||
case "toString":
|
||||
@@ -90,6 +114,10 @@ function expressionPreservesEdges(
|
||||
);
|
||||
case "virtualCall":
|
||||
return virtualCall?.(e) ?? false;
|
||||
// A module binding's TDZ check reads its initialization flag and may
|
||||
// throw; it never touches a reference.
|
||||
case "intrinsic":
|
||||
return e.name === "module.tdzCheck";
|
||||
case "dynTest":
|
||||
return preservesDynTest(e.test);
|
||||
case "assignExpr":
|
||||
@@ -109,12 +137,19 @@ function expressionPreservesEdges(
|
||||
e.fn === "error.new" ||
|
||||
e.fn === "error.nodeThrow" ||
|
||||
e.fn.startsWith("math.") ||
|
||||
// Atomic read-modify-write on an integer typed array (a shared id
|
||||
// counter): numbers only, no callback.
|
||||
e.fn === "atomics.op" ||
|
||||
isStableReceiverOperand(e, "")
|
||||
);
|
||||
case "call":
|
||||
return call(e);
|
||||
case "arrIntrinsic":
|
||||
return e.method === "length";
|
||||
return EDGE_PRESERVING_ARRAY_METHODS.has(e.method);
|
||||
// A fresh array retains its elements. Spreads keep the conservative
|
||||
// answer (an iterable spread may run an iterator).
|
||||
case "arrayLit":
|
||||
return e.spreads === undefined || e.spreads.length === 0;
|
||||
case "bytesIntrinsic":
|
||||
return (
|
||||
e.method === "get" ||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* capacity word's low 32 bits on both i64 and i32 layouts.
|
||||
*/
|
||||
|
||||
import { exactIndexLines } from "./common.js";
|
||||
|
||||
export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)";
|
||||
export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)";
|
||||
|
||||
@@ -55,30 +57,32 @@ export const STRING_READ_HELPERS: readonly StringReadHelper[] = [
|
||||
},
|
||||
{
|
||||
decl: STRING_CHAR_CODE_AT_DECL,
|
||||
define: (sz, attrs) => [
|
||||
`define internal double @sc_str_char_code_at(ptr %s, double %i) ${attrs} {`,
|
||||
`entry:`,
|
||||
...asciiTest(),
|
||||
` br i1 %ascii, label %bounds, label %slow`,
|
||||
`bounds:`,
|
||||
...loadLength(sz),
|
||||
` %lenf = sitofp ${sz} %len to double`,
|
||||
// NaN fails both ordered comparisons; -0 passes and truncates to 0.
|
||||
` %lo = fcmp oge double %i, 0.0`,
|
||||
` %hi = fcmp olt double %i, %lenf`,
|
||||
` %in = and i1 %lo, %hi`,
|
||||
` br i1 %in, label %read, label %slow`,
|
||||
`read:`,
|
||||
` %k = fptoui double %i to ${sz}`,
|
||||
` %data = getelementptr inbounds %ScrStr, ptr %s, i32 1`,
|
||||
` %p = getelementptr inbounds i8, ptr %data, ${sz} %k`,
|
||||
` %byte = load i8, ptr %p`,
|
||||
` %unit = uitofp i8 %byte to double`,
|
||||
` ret double %unit`,
|
||||
`slow:`,
|
||||
` %r = call double @scr_str_char_code_at(ptr %s, double %i)`,
|
||||
` ret double %r`,
|
||||
`}`,
|
||||
],
|
||||
define: (sz, attrs) => {
|
||||
let n = 0;
|
||||
const exactIndex = exactIndexLines("%i", "%len", sz, () => `%ix${n++}`);
|
||||
return [
|
||||
`define internal double @sc_str_char_code_at(ptr %s, double %i) ${attrs} {`,
|
||||
`entry:`,
|
||||
...asciiTest(),
|
||||
` br i1 %ascii, label %bounds, label %slow`,
|
||||
`bounds:`,
|
||||
...loadLength(sz),
|
||||
...exactIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${exactIndex.ok}, label %read, label %slow`,
|
||||
`read:`,
|
||||
sz === "i64"
|
||||
? ` %k = add i64 ${exactIndex.wide}, 0`
|
||||
: ` %k = trunc i64 ${exactIndex.wide} to ${sz}`,
|
||||
` %data = getelementptr inbounds %ScrStr, ptr %s, i32 1`,
|
||||
` %p = getelementptr inbounds i8, ptr %data, ${sz} %k`,
|
||||
` %byte = load i8, ptr %p`,
|
||||
` %unit = uitofp i8 %byte to double`,
|
||||
` ret double %unit`,
|
||||
`slow:`,
|
||||
` %r = call double @scr_str_char_code_at(ptr %s, double %i)`,
|
||||
` ret double %r`,
|
||||
`}`,
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { foldConstantGlobals } from "./constant-globals.js";
|
||||
import { F64, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
|
||||
const loc = { file: "constant-globals.ts", start: 0, end: 0 };
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const ref = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: F64 });
|
||||
const assign = (localId: string, value: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId,
|
||||
value,
|
||||
});
|
||||
const ret = (value: IrExpr): IrStmt => ({ loc, kind: "return", value });
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: name === "%init" ? VOID : F64,
|
||||
body,
|
||||
});
|
||||
const global = (id: string, extra: object = {}) => ({
|
||||
id,
|
||||
name: id,
|
||||
type: F64,
|
||||
mutable: false,
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("immutable globals stored once with a literal read as that literal", () => {
|
||||
const mod: IrModule = {
|
||||
irVersion: 15,
|
||||
sourceFile: "constant-globals.ts",
|
||||
entry: "%init",
|
||||
functions: [
|
||||
fn("%init", [
|
||||
assign("%g.DOT", num(46)),
|
||||
assign("%g.LATE", num(7)),
|
||||
assign("%g.TWICE", num(1)),
|
||||
assign("%g.TWICE", num(2)),
|
||||
assign("%g.MUT", num(3)),
|
||||
]),
|
||||
fn("read", [
|
||||
ret({
|
||||
loc,
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: ref("%g.DOT"),
|
||||
right: {
|
||||
loc,
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: ref("%g.LATE"),
|
||||
right: {
|
||||
loc,
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: ref("%g.TWICE"),
|
||||
right: ref("%g.MUT"),
|
||||
type: F64,
|
||||
},
|
||||
type: F64,
|
||||
},
|
||||
type: F64,
|
||||
}),
|
||||
]),
|
||||
],
|
||||
classes: [],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [
|
||||
global("%g.DOT"),
|
||||
global("%g.LATE", { initFlag: "%g.LATE.initialized" }),
|
||||
global("%g.TWICE"),
|
||||
global("%g.MUT", { mutable: true }),
|
||||
],
|
||||
};
|
||||
const folded = foldConstantGlobals(mod);
|
||||
const reads: string[] = [];
|
||||
const walk = (e: IrExpr): void => {
|
||||
if (e.kind === "varRef") reads.push(e.localId);
|
||||
else if (e.kind === "numLit") reads.push(String(e.value));
|
||||
else if (e.kind === "bin") {
|
||||
walk(e.left);
|
||||
walk(e.right);
|
||||
}
|
||||
};
|
||||
const body = folded.functions.find((f) => f.name === "read")!.body[0]!;
|
||||
if (body.kind === "return" && body.value) walk(body.value);
|
||||
expect(reads).toEqual(["46", "%g.LATE", "%g.TWICE", "%g.MUT"]);
|
||||
// The initializer keeps its store; the input module is not modified.
|
||||
expect(folded.functions[0]).toBe(mod.functions[0]);
|
||||
expect(mod.functions[1]!.body[0]).not.toBe(body);
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
|
||||
import { everyStmtList, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
type Literal = (IrExpr & { kind: "numLit" }) | (IrExpr & { kind: "boolLit" });
|
||||
|
||||
/** Module constants such as `const DOT = 0x2e` read as their literal.
|
||||
*
|
||||
* A module-level binding is a global that the module initializer stores
|
||||
* once; every other function loads it (a thread-local load in worker
|
||||
* programs), which also hides the value from integer ranges, switch
|
||||
* dispatch and LLVM's constant folding. When the global is immutable, has
|
||||
* exactly one write in the program and that write stores a number or
|
||||
* boolean literal, every read sees that literal: reads that could run before
|
||||
* the store carry the frontend's initialization flag (`initFlag`) or a TDZ
|
||||
* sentinel, and those globals are left alone. The store stays, so module
|
||||
* namespace views and debuggers still find the value in the global. Library
|
||||
* builds keep their globals: a host may observe them across the boundary. */
|
||||
export function foldConstantGlobals(mod: IrModule): IrModule {
|
||||
if (mod.lib || !mod.globals?.length) return mod;
|
||||
const candidates = new Set<string>();
|
||||
for (const g of mod.globals)
|
||||
if (
|
||||
!g.mutable &&
|
||||
g.tdz !== true &&
|
||||
g.initFlag === undefined &&
|
||||
(g.type.kind === "f64" || g.type.kind === "bool")
|
||||
)
|
||||
candidates.add(g.id);
|
||||
if (candidates.size === 0) return mod;
|
||||
|
||||
const writes = new Map<string, number>();
|
||||
const values = new Map<string, Literal>();
|
||||
const write = (id: string, value: IrExpr | null): void => {
|
||||
if (!candidates.has(id)) return;
|
||||
writes.set(id, (writes.get(id) ?? 0) + 1);
|
||||
if (value && (value.kind === "numLit" || value.kind === "boolLit")) values.set(id, value);
|
||||
else values.delete(id);
|
||||
};
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
// Every node other than a read that names the binding is a write
|
||||
// (assignments, loop and catch bindings, compound updates); only a
|
||||
// plain assignment or declaration contributes a value.
|
||||
stmt: (s) => {
|
||||
if (s.kind === "assign") write(s.localId, s.value);
|
||||
else if (s.kind === "varDecl") write(s.localId, s.init);
|
||||
else if ("localId" in s && typeof s.localId === "string") write(s.localId, null);
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
if (e.kind !== "varRef" && "localId" in e && typeof e.localId === "string")
|
||||
write(e.localId, null);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const constants = new Map<string, Literal>();
|
||||
for (const [id, value] of values) if (writes.get(id) === 1) constants.set(id, value);
|
||||
if (constants.size === 0) return mod;
|
||||
|
||||
const reads = (e: IrExpr): boolean => !(e.kind === "varRef" && constants.has(e.localId));
|
||||
const rewriteExpr = (e: IrExpr): IrExpr => {
|
||||
if (e.kind === "varRef") {
|
||||
const value = constants.get(e.localId);
|
||||
return value ? { ...value, type: e.type, loc: e.loc } : e;
|
||||
}
|
||||
return mapExprChildren(e, rewriteExpr, rewriteStmt);
|
||||
};
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt);
|
||||
let changed = false;
|
||||
const functions = mod.functions.map((fn): IrFunction => {
|
||||
if (everyStmtList(fn.body, { stmt: () => true, expr: reads })) return fn;
|
||||
changed = true;
|
||||
return { ...fn, body: fn.body.map(rewriteStmt) };
|
||||
});
|
||||
return changed ? { ...mod, functions } : mod;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { analyzeFinalFields, constructorClass } from "./final-fields.js";
|
||||
import {
|
||||
BOOL,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "./ir.js";
|
||||
|
||||
const loc = { file: "final-fields.ts", start: 0, end: 0 };
|
||||
const obj = (className: string): IrType => ({ kind: "object", className });
|
||||
const ref = (localId: string, type: IrType): IrExpr => ({ loc, kind: "varRef", localId, type });
|
||||
const str = (value: string): IrExpr => ({ loc, kind: "strLit", value, type: STRING });
|
||||
const set = (target: IrExpr, className: string, field: string, value: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "fieldSet",
|
||||
obj: target,
|
||||
className,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
const fn = (name: string, self: string | null, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: self ? [{ localId: "this", name: "this", type: obj(self) }] : [],
|
||||
locals: self ? [{ id: "this", name: "this", type: obj(self), mutable: false }] : [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const cls = (name: string, fields: string[], base?: string): IrClassDef => ({
|
||||
loc,
|
||||
name,
|
||||
...(base ? { base } : {}),
|
||||
fields: fields.map((field) => ({ name: field, type: STRING })),
|
||||
});
|
||||
const mod = (classes: IrClassDef[], functions: IrFunction[]): IrModule => ({
|
||||
irVersion: 15,
|
||||
sourceFile: "final-fields.ts",
|
||||
entry: "%main",
|
||||
classes,
|
||||
functions: [fn("%main", null, []), ...functions],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
});
|
||||
const self = (className: string): IrExpr => ref("this", obj(className));
|
||||
|
||||
test("constructor names map to their class", () => {
|
||||
expect(constructorClass("%%m1.Parser.constructor")).toBe("%m1.Parser");
|
||||
expect(constructorClass("%%m1.Parser.parse")).toBeNull();
|
||||
});
|
||||
|
||||
test("fields written only through their constructors' this are final", () => {
|
||||
const finals = analyzeFinalFields(
|
||||
mod(
|
||||
[cls("A", ["kept", "moved"]), cls("B", ["kept", "moved", "own"], "A")],
|
||||
[
|
||||
fn("%A.constructor", "A", [
|
||||
set(self("A"), "A", "kept", str("a")),
|
||||
set(self("A"), "A", "kept", str("twice")),
|
||||
set(self("A"), "A", "moved", str("a")),
|
||||
]),
|
||||
// A subclass constructor initializes its base's field through a guard.
|
||||
fn("%B.constructor", "B", [
|
||||
set(
|
||||
{
|
||||
loc,
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: {
|
||||
loc,
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: ref("superCalled", BOOL),
|
||||
type: BOOL,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [str("ReferenceError")],
|
||||
type: VOID,
|
||||
},
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
},
|
||||
],
|
||||
result: self("B"),
|
||||
type: obj("B"),
|
||||
},
|
||||
"B",
|
||||
"own",
|
||||
str("b"),
|
||||
),
|
||||
]),
|
||||
// Any write elsewhere disqualifies the field family.
|
||||
fn("%A.reset", "A", [set(self("A"), "A", "moved", str("later"))]),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(finals.isFinal("A", "kept")).toBe(true);
|
||||
expect(finals.isFinal("B", "kept")).toBe(true);
|
||||
expect(finals.isFinal("B", "own")).toBe(true);
|
||||
expect(finals.isFinal("A", "moved")).toBe(false);
|
||||
expect(finals.isFinal("B", "moved")).toBe(false);
|
||||
expect(finals.reasons.get("A\0moved")).toBe("written in %A.reset");
|
||||
});
|
||||
|
||||
test("constructors writing another object, and library builds, are not final", () => {
|
||||
const other = fn("%A.constructor", "A", [set(ref("peer", obj("A")), "A", "kept", str("a"))]);
|
||||
other.locals.push({ id: "peer", name: "peer", type: obj("A"), mutable: false });
|
||||
expect(analyzeFinalFields(mod([cls("A", ["kept"])], [other])).isFinal("A", "kept")).toBe(false);
|
||||
const library = mod([cls("A", ["kept"])], []);
|
||||
library.lib = { profile: "p" } as unknown as NonNullable<IrModule["lib"]>;
|
||||
expect(analyzeFinalFields(library).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { isRefCounted, type IrClassDef, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { dynamicFieldStores } from "./int32-slots.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Whole-program final reference fields.
|
||||
*
|
||||
* A reference-counted class field is final when every write the program can
|
||||
* make to it is a constructor of its family storing through that
|
||||
* constructor's own `this`. Outside the constructors such a field never
|
||||
* changes, so while its object is alive the field keeps its value alive: a
|
||||
* read of `o.f` may be borrowed for as long as `o` itself is (`this.scanner`
|
||||
* inside a method, across any call), without a retain and a release.
|
||||
* Constructors are excluded from that use, because they are where the field
|
||||
* changes; so are async functions and generators, whose suspensions let
|
||||
* other code (a constructor that is still running) continue.
|
||||
*
|
||||
* Writes the IR does not show disqualify fields up front: library builds
|
||||
* (instances cross a native boundary), live dynamic views whose stores can
|
||||
* name the field (see dynamicFieldStores), and classes with runtime-provided
|
||||
* or generic ancestors. Generated stores only initialize fresh objects or
|
||||
* tear down dead ones. */
|
||||
export class FinalFields {
|
||||
private readonly finals = new Set<string>();
|
||||
private readonly familyOf = new Map<string, string | null>();
|
||||
/** Disqualified families and the first reason, for tests. */
|
||||
readonly reasons = new Map<string, string>();
|
||||
|
||||
constructor(private readonly classes: ReadonlyMap<string, IrClassDef> = new Map()) {}
|
||||
|
||||
/** The topmost class in the base chain declaring the field, and the field. */
|
||||
family(className: string, field: string): string | null {
|
||||
const key = `${className}\0${field}`;
|
||||
const cached = this.familyOf.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
let owner: IrClassDef | undefined = this.classes.get(className);
|
||||
let result: string | null = null;
|
||||
if (owner?.fields.some((f) => f.name === field)) {
|
||||
for (;;) {
|
||||
const base: IrClassDef | undefined = owner.base ? this.classes.get(owner.base) : undefined;
|
||||
if (!base?.fields.some((f) => f.name === field)) break;
|
||||
owner = base;
|
||||
}
|
||||
result = `${owner.name}\0${field}`;
|
||||
}
|
||||
this.familyOf.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
isFinal(className: string, field: string): boolean {
|
||||
if (this.finals.size === 0) return false;
|
||||
const family = this.family(className, field);
|
||||
return family !== null && this.finals.has(family);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.finals.size;
|
||||
}
|
||||
|
||||
/* ── construction (analyzeFinalFields only) ── */
|
||||
add(family: string): void {
|
||||
this.finals.add(family);
|
||||
}
|
||||
drop(family: string, why: string): void {
|
||||
if (!this.reasons.has(family)) this.reasons.set(family, why);
|
||||
this.finals.delete(family);
|
||||
}
|
||||
}
|
||||
|
||||
const CONSTRUCTOR_SUFFIX = ".constructor";
|
||||
|
||||
/** `%C.constructor`'s class name, or null for any other function. */
|
||||
export function constructorClass(fnName: string): string | null {
|
||||
return fnName.startsWith("%") && fnName.endsWith(CONSTRUCTOR_SUFFIX)
|
||||
? fnName.slice(1, -CONSTRUCTOR_SUFFIX.length)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function analyzeFinalFields(mod: IrModule): FinalFields {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const finals = new FinalFields(classes);
|
||||
if (mod.lib) return finals;
|
||||
const dynamic = dynamicFieldStores(mod);
|
||||
if (dynamic.all) return finals;
|
||||
|
||||
const genericFamilies = new Set<string>();
|
||||
for (const cls of classes.values())
|
||||
if (cls.genericOf !== undefined) {
|
||||
genericFamilies.add(cls.name);
|
||||
genericFamilies.add(cls.genericOf);
|
||||
}
|
||||
const plainClass = (cls: IrClassDef): boolean => {
|
||||
for (let c: IrClassDef | undefined = cls; c; c = c.base ? classes.get(c.base) : undefined) {
|
||||
if (c.runtime || genericFamilies.has(c.name)) return false;
|
||||
if (c.base && !classes.has(c.base)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const inFamily = (className: string, family: string): boolean => {
|
||||
const owner = family.slice(0, family.indexOf("\0"));
|
||||
for (let c = classes.get(className); c; c = c.base ? classes.get(c.base) : undefined)
|
||||
if (c.name === owner) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const cls of classes.values())
|
||||
for (const field of cls.fields) {
|
||||
if (!isRefCounted(field.type) || field.name.startsWith("%")) continue;
|
||||
const family = finals.family(cls.name, field.name)!;
|
||||
if (finals.reasons.has(family)) continue;
|
||||
if (!plainClass(cls)) finals.drop(family, "runtime-provided or generic class");
|
||||
else if (dynamic.names.has(field.name))
|
||||
finals.drop(family, "dynamic stores can name the field");
|
||||
else finals.add(family);
|
||||
}
|
||||
if (finals.size === 0) return finals;
|
||||
|
||||
for (const fn of mod.functions) {
|
||||
const ctor = constructorClass(fn.name);
|
||||
const self = ctor !== null ? fn.params[0]?.localId : undefined;
|
||||
const selfLocal = self !== undefined ? fn.locals.find((l) => l.id === self) : undefined;
|
||||
// `this` must name the object under construction for the whole body.
|
||||
const constructing = selfLocal !== undefined && !selfLocal.boxed && !reassigns(fn.body, self!);
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "fieldSet") {
|
||||
const family = finals.family(s.className, s.field);
|
||||
if (family !== null && finals.isFinal(s.className, s.field)) {
|
||||
const own =
|
||||
constructing && ctor !== null && inFamily(ctor, family) && isSelf(s.obj, self!);
|
||||
if (!own) finals.drop(family, `written in ${fn.name}`);
|
||||
}
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt);
|
||||
for (const s of fn.body) stmt(s);
|
||||
}
|
||||
return finals;
|
||||
}
|
||||
|
||||
/** `this` itself, or behind the derived constructor's guards (`if (!super
|
||||
* called) throw`), which read no reference and write nothing. */
|
||||
function isSelf(e: IrExpr, self: string): boolean {
|
||||
if (e.kind === "varRef") return e.localId === self;
|
||||
if (e.kind !== "seqExpr") return false;
|
||||
for (const s of e.stmts) {
|
||||
if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false;
|
||||
const only = s.then[0]!;
|
||||
if (
|
||||
only.kind !== "exprStmt" ||
|
||||
only.expr.kind !== "libCall" ||
|
||||
only.expr.fn !== "error.nodeThrow" ||
|
||||
!only.expr.args.every((arg) => arg.kind === "numLit" || arg.kind === "strLit") ||
|
||||
!throwFreeCondition(s.cond)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return isSelf(e.result, self);
|
||||
}
|
||||
|
||||
function throwFreeCondition(e: IrExpr): boolean {
|
||||
if (e.kind === "varRef" || e.kind === "boolLit") return e.type.kind === "bool";
|
||||
return e.kind === "unary" && throwFreeCondition(e.operand);
|
||||
}
|
||||
|
||||
function reassigns(body: IrStmt[], localId: string): boolean {
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === localId) found = true;
|
||||
return !found && everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "assign" && s.localId === localId && s.initializes !== true) found = true;
|
||||
return !found && everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
for (const s of body) if (!stmt(s)) break;
|
||||
return found;
|
||||
}
|
||||
@@ -271,15 +271,16 @@ function walkFunction(
|
||||
for (const s of fn.body) stmt(s);
|
||||
}
|
||||
|
||||
/** The class and record field names checked-dynamic code can store into a
|
||||
* live typed-ref capsule (a capsule commit rewrites every other field with
|
||||
* its unchanged snapshot value). `all`: some runtime helper that can store
|
||||
* an arbitrary property receives a dynamic value, or the program has a
|
||||
* dynamic island; otherwise only literal-keyed `dyn.keySet`/`dyn.keyDelete`
|
||||
* name fields, collected in `names`. */
|
||||
export function dynamicFieldStores(mod: IrModule): { all: boolean; names: Set<string> } {
|
||||
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
|
||||
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
|
||||
/** The class field names that runtime stores through live dynamic views
|
||||
* (typed-ref capsules) can write: every name when `all` (islands, jsval
|
||||
* values, or a dynamic store whose key is not a literal), otherwise the
|
||||
* literal keys of `dyn.keySet`/`dyn.keyDelete`. A view commits a store back
|
||||
* into the instance's native field slots, which the IR does not show. */
|
||||
export function dynamicFieldStores(
|
||||
mod: IrModule,
|
||||
records = new Map((mod.records ?? []).map((r) => [r.id, r])),
|
||||
unions = new Map((mod.unions ?? []).map((u) => [u.id, u])),
|
||||
): { all: boolean; names: Set<string> } {
|
||||
const dynTypes = new Map<IrType, boolean>();
|
||||
const dynNamed = new Map<string, boolean>();
|
||||
const dynish = (type: IrType): boolean => {
|
||||
@@ -436,7 +437,7 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
// writes every field from the view, which holds the exact snapshot value
|
||||
// of any property no store named. So reaching dynamic code disqualifies
|
||||
// exactly the field names some dynamic store in the program can name.
|
||||
const { all: dynWritesAll, names: dynWritten } = dynamicFieldStores(mod);
|
||||
const { all: dynWritesAll, names: dynWritten } = dynamicFieldStores(mod, records, unions);
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
|
||||
@@ -164,3 +164,21 @@ test("versioning is bounded and requires an integer consumer", () => {
|
||||
expect(matchIntegerCountedForLoop(nested, withLimit)).toBeNull();
|
||||
expect(matchIntegerCountedForLoop(counted(number(4)), locals)?.guarded).toBe(false);
|
||||
});
|
||||
|
||||
test("an unproven start versions the loop on an exact integer start", () => {
|
||||
const withStart = new Map(locals);
|
||||
withStart.set("start", { id: "start", name: "start", type: F64, mutable: false });
|
||||
const s = counted(number(8));
|
||||
s.init = { kind: "varDecl", localId: "i", init: ref("start"), loc };
|
||||
expect(matchIntegerCountedForLoop(s, withStart)).toMatchObject({
|
||||
guarded: true,
|
||||
startGuarded: true,
|
||||
limitGuarded: false,
|
||||
range: { min: -Number.MAX_SAFE_INTEGER, max: 7 },
|
||||
});
|
||||
// The versioned body is duplicated, so the usual size bound applies.
|
||||
const large = counted(number(8));
|
||||
large.init = { kind: "varDecl", localId: "i", init: ref("start"), loc };
|
||||
large.body = Array.from({ length: 81 }, () => large.body[0]!);
|
||||
expect(matchIntegerCountedForLoop(large, withStart)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -20,6 +20,11 @@ export interface IntegerCountedForLoop {
|
||||
step: number;
|
||||
inclusive: boolean;
|
||||
guarded: boolean;
|
||||
/** The limit needs the run-time `guardLimit` test (part of `guarded`). */
|
||||
limitGuarded: boolean;
|
||||
/** The start has no proven integer range: the versioned loop also tests
|
||||
* that it is an exact integer (not -0) of at most 2^53 in magnitude. */
|
||||
startGuarded: boolean;
|
||||
guardLimit: number;
|
||||
range: IntegerRange;
|
||||
}
|
||||
@@ -104,10 +109,13 @@ export function matchIntegerCountedForLoop(
|
||||
const local = locals.get(init.localId);
|
||||
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
|
||||
const start = init.init;
|
||||
const startRange =
|
||||
let startRange =
|
||||
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
|
||||
? { min: start.value, max: start.value }
|
||||
: ranges.get(start);
|
||||
// An unproven start is tested at run time, on a versioned loop.
|
||||
const startGuarded = !startRange && start.kind !== "numLit" && start.type.kind === "f64";
|
||||
if (startGuarded) startRange = { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
|
||||
if (
|
||||
!startRange ||
|
||||
startRange.min < -Number.MAX_SAFE_INTEGER ||
|
||||
@@ -132,9 +140,10 @@ export function matchIntegerCountedForLoop(
|
||||
const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0);
|
||||
const boundRange =
|
||||
limit.kind === "numLit" ? { min: limit.value, max: limit.value } : ranges.get(limit);
|
||||
const guarded =
|
||||
const limitGuarded =
|
||||
!boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit));
|
||||
if (guarded && limit.kind === "numLit") return null;
|
||||
if (limitGuarded && limit.kind === "numLit") return null;
|
||||
const guarded = limitGuarded || startGuarded;
|
||||
let useful = false;
|
||||
let cost = 0;
|
||||
const counter = (e: IrExpr): boolean =>
|
||||
@@ -204,7 +213,18 @@ export function matchIntegerCountedForLoop(
|
||||
),
|
||||
max: startRange.max,
|
||||
};
|
||||
return { localId: local.id, start, limit, step, inclusive, guarded, guardLimit, range };
|
||||
return {
|
||||
localId: local.id,
|
||||
start,
|
||||
limit,
|
||||
step,
|
||||
inclusive,
|
||||
guarded,
|
||||
limitGuarded,
|
||||
startGuarded,
|
||||
guardLimit,
|
||||
range,
|
||||
};
|
||||
}
|
||||
|
||||
/** True when a lowered subtree writes `localId`. Local ids are unique per
|
||||
|
||||
@@ -269,15 +269,22 @@ console.log(work());
|
||||
expect(facts.borrowed.get("relay")).toEqual(new Set([0, 1]));
|
||||
expect(facts.parameters.has("keep")).toBe(false);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
// An append only adds an edge, so `keep` preserves every heap edge, borrows
|
||||
// every reference parameter and returns one of them: it is a borrowed-return
|
||||
// function. Its body retains only the element stored into the array; the
|
||||
// returned parameter goes back +0 (the caller still owns it) and the owned
|
||||
// adapter retains it before releasing its parameters.
|
||||
const keep = body(llvm, "sc_bf_keep");
|
||||
expect(keep.match(/@sc_retain_Item/g)).toHaveLength(2);
|
||||
expect(keep.match(/@sc_retain_Item/g)).toHaveLength(1);
|
||||
expect(keep).not.toContain("@scr_arr_retain_v");
|
||||
expect(keep).not.toContain("@scr_arr_release");
|
||||
const relay = body(llvm, "sc_bf_relay");
|
||||
expect(relay).toContain("@sc_bf_keep");
|
||||
expect(relay).toMatch(/call ptr @sc_bf_keep\([^)]*\) ; borrowed result/);
|
||||
expect(relay).not.toContain("@sc_retain_Item");
|
||||
expect(relay).not.toContain("@scr_arr_retain_v");
|
||||
const owned = body(llvm, "sc_f_keep");
|
||||
expect(owned.match(/@sc_retain_Item/g)).toHaveLength(1);
|
||||
expect(owned.indexOf("@sc_retain_Item")).toBeLessThan(owned.indexOf("@sc_release_Item"));
|
||||
expect(owned).toContain("@sc_release_Item");
|
||||
expect(owned).toContain("@scr_arr_release");
|
||||
});
|
||||
|
||||
@@ -15802,6 +15802,38 @@
|
||||
"<repo>/tests/corpus/4660-typed-rest-spread-forwarding.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4598-live-array-ref-element-access.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4598-live-array-ref-element-access.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4620-optional-chain-borrowed-receivers.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4620-optional-chain-borrowed-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4621-edge-preserving-callees/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4621-edge-preserving-callees/compare.ts",
|
||||
"<repo>/tests/corpus/4621-edge-preserving-callees/items.ts",
|
||||
"<repo>/tests/corpus/4621-edge-preserving-callees/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/exact-index-round-trip.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/exact-index-round-trip.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/final-field-borrows.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/final-field-borrows.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,9 +347,8 @@ static SCR_TL size_t scr_cyc_live_after[SCR_CYC_NGENS];
|
||||
#endif
|
||||
|
||||
/* Per level, doublings of the growth fraction (see scr_cyc_adapt_growth),
|
||||
* and the run of growth-triggered passes at that level whose yield asked for
|
||||
* a wider fraction than the current one. Only the mature and old entries are
|
||||
* used.
|
||||
* and whether the last growth-triggered pass at that level asked for a wider
|
||||
* fraction than the current one. Only the mature and old entries are used.
|
||||
*
|
||||
* The fraction is capped at 16 (shift 6): after passes at a level stop
|
||||
* finding garbage, the heap may grow to 17 times its size after the last
|
||||
@@ -493,10 +492,23 @@ static size_t scr_cyc_backlog_threshold(unsigned gen) {
|
||||
*
|
||||
* Narrowing takes effect at once: a pass that finds garbage again moves the
|
||||
* fraction straight to what its yield asks for. Widening waits for two
|
||||
* passes in a row that asked for it and then moves one doubling per pass. A
|
||||
* program that discards older structures at a steady rate sees its passes
|
||||
* alternate between finding the garbage just aged into the level and finding
|
||||
* it still younger, and a single miss says little. */
|
||||
* passes in a row that asked for it. A program that discards older
|
||||
* structures at a steady rate sees its passes alternate between finding the
|
||||
* garbage just aged into the level and finding it still younger, and a
|
||||
* single miss says little.
|
||||
*
|
||||
* How far a qualifying pass widens depends on what the level's passes walk.
|
||||
* A full pass walks the whole heap, and a heap that grows for a whole run
|
||||
* gets only a handful of them, so widening a doubling per pass spent the
|
||||
* largest walks on the way to the cap (a run with seven full passes never
|
||||
* reached it). The old level therefore widens two doublings per qualifying
|
||||
* pass, never past what the pass asked for: passes that keep finding
|
||||
* nothing reach the cap of 16 on the fourth pass instead of the sixth, while
|
||||
* the heap is still small. A mature pass walks only what was promoted since
|
||||
* the last one, so its total walking hardly depends on how passes are
|
||||
* spaced, and the mature level keeps widening a doubling per pass, which
|
||||
* lets less young garbage float. */
|
||||
#define SCR_CYC_OLD_WIDEN_STEP 2
|
||||
static void scr_cyc_adapt_growth(unsigned gen, size_t growth,
|
||||
size_t reclaimed) {
|
||||
unsigned want = 0, max = scr_cyc_growth_max_shift();
|
||||
@@ -505,9 +517,11 @@ static void scr_cyc_adapt_growth(unsigned gen, size_t growth,
|
||||
if (want <= scr_cyc_growth_shift[gen]) {
|
||||
scr_cyc_growth_shift[gen] = (unsigned char)want;
|
||||
scr_cyc_growth_streak[gen] = 0;
|
||||
} else if (++scr_cyc_growth_streak[gen] >= 2) {
|
||||
scr_cyc_growth_streak[gen] = 2;
|
||||
scr_cyc_growth_shift[gen]++;
|
||||
} else if (scr_cyc_growth_streak[gen]) {
|
||||
unsigned shift = scr_cyc_growth_shift[gen] + (gen == SCR_CYC_OLD ? SCR_CYC_OLD_WIDEN_STEP : 1);
|
||||
scr_cyc_growth_shift[gen] = (unsigned char)(shift < want ? shift : want);
|
||||
} else {
|
||||
scr_cyc_growth_streak[gen] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -720,6 +720,49 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc,
|
||||
const char *what, ScrDyn *callback_recv);
|
||||
|
||||
/* Live native arrays: the constant-time methods that loops call once per
|
||||
* element. Each mirrors the checked-array dispatch below (which consults
|
||||
* neither own properties nor the prototype for these builtins) but touches
|
||||
* one native element instead of materializing and recommitting the whole
|
||||
* array. Anything that could observe more — a hole, argument coercion, a
|
||||
* multi-value push, a value the element type rejects — sets *handled false
|
||||
* and takes the snapshot path. */
|
||||
static ScrDyn *dyn_live_array_method(ScrDyn *recv, const char *method, ScrDyn *const *args,
|
||||
size_t argc, const char *what, bool *handled) {
|
||||
const ScrDynTypedArrayOps *ops = recv->v.typed_ref.array;
|
||||
if (!ops) return NULL;
|
||||
ScrArr *arr = recv->v.typed_ref.ptr;
|
||||
size_t len = arr->len;
|
||||
if (dyn_name_is(method, "push") && argc == 1 && ops->set) {
|
||||
if (!ops->set(arr, (double)len, args[0])) return NULL;
|
||||
*handled = true;
|
||||
return scr_exc_pending() ? NULL : scr_dyn_new_num((double)arr->len);
|
||||
}
|
||||
if (dyn_name_is(method, "pop")) {
|
||||
if (len == 0) {
|
||||
*handled = true;
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
ScrDyn *last = scr_dyn_typed_ref_element(recv, len - 1);
|
||||
if (!last) return NULL;
|
||||
scr_arr_set_len(arr, (double)(len - 1));
|
||||
*handled = true;
|
||||
return last;
|
||||
}
|
||||
if (dyn_name_is(method, "at") && (argc == 0 || args[0]->kind == SCR_DYN_NUM || args[0]->kind == SCR_DYN_UNDEF)) {
|
||||
double index = dyn_index_arg(args, argc, 0, 0, what);
|
||||
if (index < 0) index += (double)len;
|
||||
if (index < 0 || index >= (double)len) {
|
||||
*handled = true;
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
ScrDyn *value = scr_dyn_typed_ref_element(recv, (size_t)index);
|
||||
if (value) *handled = true;
|
||||
return value;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method) {
|
||||
if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL)
|
||||
return scr_dyn_invoke(recv, method, NULL, 0, method);
|
||||
@@ -800,6 +843,9 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
* Mutators such as reverse/sort return their receiver, so translate that
|
||||
* snapshot identity back to the externally visible capsule. */
|
||||
if (recv->kind == SCR_DYN_TYPED_REF) {
|
||||
bool handled = false;
|
||||
ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled);
|
||||
if (handled) return fast;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
|
||||
if (scr_exc_pending()) {
|
||||
scr_dyn_release(materialized);
|
||||
|
||||
@@ -2071,14 +2071,16 @@ ScrDyn *scr_dyn_new_typed_ref(
|
||||
value->v.typed_ref.type_key_len == type_key_len && !memcmp(value->v.typed_ref.type_key, type_key, type_key_len))
|
||||
return scr_dyn_retain(value);
|
||||
}
|
||||
if (type_key_len >= (size_t)1 << 30) scr_trap("scriptc: typed reference key too long\n");
|
||||
ScrDyn *d = scr_dyn_alloc(SCR_DYN_TYPED_REF);
|
||||
d->v.typed_ref.ptr = retain(ptr);
|
||||
d->v.typed_ref.retain = retain;
|
||||
d->v.typed_ref.release = release;
|
||||
d->v.typed_ref.type_key = type_key;
|
||||
d->v.typed_ref.type_key_len = type_key_len;
|
||||
d->v.typed_ref.type_key_len = (uint32_t)type_key_len;
|
||||
d->v.typed_ref.materialize = materialize;
|
||||
d->v.typed_ref.commit = commit;
|
||||
d->v.typed_ref.array = NULL;
|
||||
d->v.typed_ref.materialized = NULL;
|
||||
d->v.typed_ref.casts = NULL;
|
||||
d->v.typed_ref.traced = false;
|
||||
@@ -2238,6 +2240,74 @@ void scr_dyn_typed_ref_commit(ScrDyn *d) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── live native arrays: per-element access ──────────────────────────────
|
||||
* Materializing rebuilds the whole array, so doing it for every `xs[i]`,
|
||||
* `xs.length`, or iteration step makes a loop over a live array quadratic.
|
||||
* The native array is the source of truth (every refresh rebuilds the view
|
||||
* from it and keeps only the prototype and symbol properties), so element
|
||||
* reads, presence, and in-range writes can consult it directly. Anything
|
||||
* whose answer may come from the prototype chain — holes and out-of-range
|
||||
* indexes — or needs the snapshot's diagnostics still takes the snapshot. */
|
||||
void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops) {
|
||||
if (d && d->kind == SCR_DYN_TYPED_REF) d->v.typed_ref.array = ops;
|
||||
}
|
||||
|
||||
static bool scr_typed_ref_canonical_index(const char *data, size_t len, size_t *out) {
|
||||
if (len == 0 || len > 10 || (len > 1 && data[0] == '0')) return false;
|
||||
uint64_t index = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] < '0' || data[i] > '9') return false;
|
||||
index = index * 10 + (uint64_t)(data[i] - '0');
|
||||
}
|
||||
if (index >= 4294967295ULL) return false;
|
||||
*out = (size_t)index;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool scr_typed_ref_is_length(const ScrStr *key) {
|
||||
return key->len == 6 && memcmp(key->data, "length", 6) == 0;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_typed_ref_element(const ScrDyn *d, size_t index) {
|
||||
ScrArr *arr = d->v.typed_ref.ptr;
|
||||
if (index >= arr->len) return NULL;
|
||||
double state = scr_arr_state(arr, (double)index);
|
||||
if (state == (double)SCR_ARR_VALUE) return d->v.typed_ref.array->get(arr, (double)index);
|
||||
if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!d->v.typed_ref.array) return NULL;
|
||||
if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len);
|
||||
size_t index;
|
||||
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return NULL;
|
||||
return scr_dyn_typed_ref_element(d, index);
|
||||
}
|
||||
|
||||
/* An own `length` or element answers `in`/hasOwn without the snapshot. */
|
||||
static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!d->v.typed_ref.array) return false;
|
||||
if (scr_typed_ref_is_length(key)) 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;
|
||||
return index < arr->len && scr_arr_state(arr, (double)index) != (double)SCR_ARR_HOLE;
|
||||
}
|
||||
|
||||
/* Writes an element in place or appends at `length`. False (nothing done)
|
||||
* when the value does not fit the element type or the write could grow the
|
||||
* array by more than one element: the snapshot path owns those outcomes. */
|
||||
static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) {
|
||||
const ScrDynTypedArrayOps *ops = d->v.typed_ref.array;
|
||||
if (!ops || !ops->set) return false;
|
||||
size_t index;
|
||||
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false;
|
||||
ScrArr *arr = d->v.typed_ref.ptr;
|
||||
if (index > arr->len) return false;
|
||||
return ops->set(arr, (double)index, value);
|
||||
}
|
||||
|
||||
void *scr_dyn_typed_ref_cached_cast(
|
||||
const ScrDyn *d, const char *type_key, size_t type_key_len) {
|
||||
for (ScrDynTypedCast *cast = d->v.typed_ref.casts; cast;
|
||||
@@ -3433,6 +3503,27 @@ static ScrDyn *scr_native_iterator_value(ScrNativeIterator *iterator, bool *done
|
||||
/* An inherited indexed getter can advance this same iterator recursively.
|
||||
* Keep the source alive even if that nested call exhausts the iterator. */
|
||||
if (source) scr_dyn_retain(source);
|
||||
/* A live native array answers an in-range own element directly. Holes
|
||||
* (prototype lookups) and completion fall through to the view below. */
|
||||
if (source && source->kind == SCR_DYN_TYPED_REF && source->v.typed_ref.array &&
|
||||
iterator->kind == 0 && !iterator->array_like &&
|
||||
iterator->index < (double)((ScrArr *)source->v.typed_ref.ptr)->len) {
|
||||
double index = iterator->index;
|
||||
ScrDyn *value = iterator->selection == 1 ? scr_dyn_new_num(index)
|
||||
: scr_dyn_typed_ref_element(source, (size_t)index);
|
||||
if (value) {
|
||||
if (iterator->selection == 2) {
|
||||
ScrDyn *entry = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(entry, scr_dyn_new_num(index));
|
||||
scr_dyn_arr_push(entry, value);
|
||||
value = entry;
|
||||
}
|
||||
iterator->index++;
|
||||
scr_dyn_release(source);
|
||||
*done_out = false;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
/* Native array capsules retain the live array, so refresh its view at each
|
||||
* step rather than iterating a snapshot taken when the iterator opened. */
|
||||
ScrDyn *view = source && source->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(source) : NULL;
|
||||
@@ -5504,6 +5595,7 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
|
||||
if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key);
|
||||
if (v->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(v, "'in'");
|
||||
if (v->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_typed_ref_array_has_own(v, key)) return true;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(v);
|
||||
bool out = scr_dyn_has_key(materialized, key);
|
||||
scr_dyn_release(materialized);
|
||||
@@ -5620,6 +5712,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(recv)) return;
|
||||
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
|
||||
if (recv->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_typed_ref_array_set(recv, key, value)) return;
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
|
||||
scr_dyn_key_set(materialized, key, value);
|
||||
if (!scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
|
||||
@@ -8645,6 +8738,11 @@ static bool scr_dyn_canonical_own_index(const ScrStr *key, size_t length) {
|
||||
|
||||
bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
|
||||
if (v->kind == SCR_DYN_TYPED_REF) {
|
||||
/* Own canonical indexes never consult the prototype: a hole is absent. */
|
||||
size_t index;
|
||||
if (v->v.typed_ref.array &&
|
||||
(scr_typed_ref_is_length(key) || scr_typed_ref_canonical_index(key->data, key->len, &index)))
|
||||
return scr_typed_ref_array_has_own(v, key);
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(v);
|
||||
if (!view) return false;
|
||||
bool result = scr_dyn_has_own(view, key);
|
||||
|
||||
@@ -4368,6 +4368,20 @@ _Static_assert(sizeof(ScrDynEntry) == (sizeof(void *) == 8 ? 48 : 24),
|
||||
_Static_assert(offsetof(ScrDynEntry, value) == 2 * sizeof(void *),
|
||||
"LLVM checked-object readers use the third pointer-sized field");
|
||||
|
||||
/* Element access for a live typed reference to a native ScrArr, emitted per
|
||||
* array type by the compiler. The runtime decides presence and length from
|
||||
* the native array itself; these hooks only convert one element.
|
||||
* get: +1 dyn view of the element at a dense index whose state is
|
||||
* SCR_ARR_VALUE, built exactly as the whole-array snapshot builds it.
|
||||
* set: stores `value` at index (index <= length) when it fits the element
|
||||
* type and returns true; returns false with no side effects when it
|
||||
* does not, so the caller can take the snapshot path and report the
|
||||
* mismatch with its usual diagnostic. */
|
||||
typedef struct ScrDynTypedArrayOps {
|
||||
ScrDyn *(*get)(void *arr, double index);
|
||||
bool (*set)(void *arr, double index, const ScrDyn *value);
|
||||
} ScrDynTypedArrayOps;
|
||||
|
||||
struct ScrDyn {
|
||||
size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */
|
||||
ScrDynKind kind;
|
||||
@@ -4429,17 +4443,21 @@ struct ScrDyn {
|
||||
void *(*retain)(void *);
|
||||
void (*release)(void *);
|
||||
const char *type_key;
|
||||
size_t type_key_len;
|
||||
ScrDyn *(*materialize)(void *);
|
||||
void (*commit)(void *, const ScrDyn *);
|
||||
/* Live native arrays: per-element access that bypasses the whole-array
|
||||
* snapshot (scr_dyn_typed_ref_bind_array). NULL for other referents. */
|
||||
const ScrDynTypedArrayOps *array;
|
||||
/* Both caches belong to this capsule. ReadableStream canonicalizes
|
||||
* capsules for repeated source references, so the refreshed dyn view
|
||||
* and every structurally converted static view keep JS reference
|
||||
* identity while the source reference remains observable. */
|
||||
ScrDyn *materialized;
|
||||
ScrDynTypedCast *casts;
|
||||
bool traced; /* native referent has a cycle header */
|
||||
bool observed; /* native release reports this identity's disposal */
|
||||
/* One word keeps this, the largest payload, at its previous size. */
|
||||
uint32_t type_key_len : 30;
|
||||
uint32_t traced : 1; /* native referent has a cycle header */
|
||||
uint32_t observed : 1; /* native release reports this identity's disposal */
|
||||
} typed_ref;
|
||||
/* SCR_DYN_PROMISE: the retained promise. The boundary contract: it
|
||||
* settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a
|
||||
@@ -4706,6 +4724,17 @@ void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */
|
||||
ScrDyn *scr_dyn_class_view_unavailable(void *ptr);
|
||||
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */
|
||||
void scr_dyn_typed_ref_commit(ScrDyn *d);
|
||||
/* Attach per-element access to a live array capsule (idempotent). Indexed
|
||||
* reads, `length`, in-range writes, `in`, own-property probes, and array
|
||||
* iteration then touch one element instead of rebuilding the snapshot. */
|
||||
void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops);
|
||||
/* Fast keyed read on a live array capsule: +1 value for `length` and for
|
||||
* present or explicitly-undefined elements; NULL when the answer needs the
|
||||
* snapshot (holes, out-of-range, non-index names, or no bound ops). */
|
||||
ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key);
|
||||
/* +1 view of an own element of a bound live array capsule; NULL for a hole
|
||||
* or an index at or past its length. */
|
||||
ScrDyn *scr_dyn_typed_ref_element(const ScrDyn *d, size_t index);
|
||||
void *scr_dyn_typed_ref_cached_cast(
|
||||
const ScrDyn *d, const char *type_key, size_t type_key_len); /* +1/NULL */
|
||||
void scr_dyn_typed_ref_cache_cast(
|
||||
|
||||
@@ -11,6 +11,33 @@
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <fcntl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Every worker's mailbox holds descriptors. Once a process passes 64 of
|
||||
* them, the kernel grows its descriptor table, and while other threads share
|
||||
* the table it first waits a full RCU grace period (synchronize_rcu in
|
||||
* expand_fdtable): measured 11-25 ms on a 16-vCPU host, on the thread that
|
||||
* starts the next worker. Growing the table at load, while the process still
|
||||
* has one thread, never waits; the table never shrinks. 256 entries leave
|
||||
* room for the most workers the allocator gives arenas to. */
|
||||
__attribute__((constructor)) static void scr_worker_reserve_fd_table(void) {
|
||||
const int top = 255;
|
||||
struct rlimit limit;
|
||||
if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur <= (rlim_t)top)
|
||||
return;
|
||||
for (int fd = 0; fd <= 2; fd++) {
|
||||
int copy = fcntl(fd, F_DUPFD_CLOEXEC, top);
|
||||
if (copy >= 0) {
|
||||
(void)close(copy);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct ScrWorkerThread {
|
||||
atomic_size_t references;
|
||||
atomic_bool cancel;
|
||||
|
||||
@@ -54,7 +54,32 @@ static int entry(uint32_t root_id) {
|
||||
return (int)root_id;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
/* Worker builds grow the descriptor table at load, while the process has one
|
||||
* thread: growing it later, with threads sharing it, waits a full RCU grace
|
||||
* period on the thread that opens the next worker's mailbox. */
|
||||
static void check_fd_table_reserved(void) {
|
||||
struct rlimit limit;
|
||||
if (getrlimit(RLIMIT_NOFILE, &limit) != 0 || (limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur <= 255)) return;
|
||||
FILE *status = fopen("/proc/self/status", "r");
|
||||
assert(status);
|
||||
char line[256];
|
||||
long size = -1;
|
||||
while (fgets(line, sizeof line, status))
|
||||
if (strncmp(line, "FDSize:", 7) == 0) size = strtol(line + 7, NULL, 10);
|
||||
fclose(status);
|
||||
assert(size >= 256);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
#if defined(__linux__)
|
||||
check_fd_table_reserved();
|
||||
#endif
|
||||
scr_init();
|
||||
scr_lib_init(0, NULL);
|
||||
ScrMailbox *mailbox = scr_mailbox_new();
|
||||
|
||||
@@ -39,6 +39,7 @@ test("cycle traversal preserves counts, deep graphs, configured thresholds and g
|
||||
[undefined, "1", "threshold=256 growth_cap=1"],
|
||||
["1", "1", "threshold=1 growth_cap=1"],
|
||||
[undefined, "3", "threshold=256 growth_cap=2"],
|
||||
[undefined, "8", "threshold=256 growth_cap=8"],
|
||||
[undefined, "64", "threshold=256 growth_cap=16"],
|
||||
] as const) {
|
||||
const env = { ...baseEnv };
|
||||
|
||||
@@ -454,25 +454,38 @@ static size_t grow_dead_cycle_until_full_pass(size_t old_garbage) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Passes that keep finding nothing widen the old level's fraction one
|
||||
* doubling at a time up to the configured cap and no further: once there, a
|
||||
* dead cycle goes exactly when the heap has grown the cap past the floor,
|
||||
* not one doubling earlier (a narrower cap) and not later (a wider or
|
||||
* missing one). A pass that then finds old garbage narrows the fraction at
|
||||
* once. Runs last: it leaves the fraction narrowed but not at the base. */
|
||||
/* Passes that keep finding nothing widen the old level's fraction up to the
|
||||
* configured cap and no further, two doublings per pass once two passes in a
|
||||
* row have asked: at the cap a dead cycle goes exactly when the heap has
|
||||
* grown the cap past the floor, not one doubling earlier (a narrower cap)
|
||||
* and not later (a wider or missing one). A pass that finds old garbage
|
||||
* narrows the fraction at once, and a single pass that finds nothing after
|
||||
* it does not widen it again. From where the ring below narrows it (two
|
||||
* doublings under any cap of 4 or more, the base fraction under 1 and 2),
|
||||
* the second pass in a row that finds nothing reaches the cap; widening a
|
||||
* doubling per pass would not. Runs last: it leaves the fraction narrowed
|
||||
* but not at the base. */
|
||||
static void check_growth_cap_bounds_floating_garbage(void) {
|
||||
enum { FLOOR = 4096 };
|
||||
size_t cap = configured_growth_cap();
|
||||
/* Passes wait for the release trigger, and dead rings float until the
|
||||
* next restricted pass, so allow two nursery's worth. */
|
||||
size_t slack = 2 * configured_nursery_threshold() + 2;
|
||||
size_t rounds = 2; /* two passes asking to widen before the first doubling */
|
||||
for (size_t c = cap; c >= 1; c /= 2) rounds++; /* one per doubling up to cap */
|
||||
for (size_t i = 0; i < rounds; i++) grow_dead_cycle_until_full_pass(0);
|
||||
size_t narrowed = FLOOR + FLOOR * cap / 2 + slack;
|
||||
size_t due = FLOOR + FLOOR * cap;
|
||||
/* The previous test left the fraction at the cap. Old garbage narrows it
|
||||
* (see below); the pass after that is the first to find nothing and must
|
||||
* not widen it; the second in a row widens it to the cap. */
|
||||
grow_dead_cycle_until_full_pass(FLOOR / 2 - 64);
|
||||
size_t live = grow_dead_cycle_until_full_pass(0);
|
||||
check(live > due, "the growth fraction stopped widening below the cap");
|
||||
check(live <= narrowed, "a pass that found old garbage did not narrow the growth fraction");
|
||||
live = grow_dead_cycle_until_full_pass(0);
|
||||
check(live <= narrowed, "a single pass that found nothing widened the growth fraction");
|
||||
live = grow_dead_cycle_until_full_pass(0);
|
||||
check(live > due, "two passes in a row that found nothing did not widen to the cap");
|
||||
check(live <= due + slack, "the growth fraction widened past the cap");
|
||||
live = grow_dead_cycle_until_full_pass(0);
|
||||
check(live > due && live <= due + slack, "the growth fraction left the cap");
|
||||
|
||||
/* At any cap, old garbage of an eighth of the floor or more is a yield
|
||||
* that asks for at least one doubling less (this ring asks for two below
|
||||
@@ -480,8 +493,7 @@ static void check_growth_cap_bounds_floating_garbage(void) {
|
||||
live = grow_dead_cycle_until_full_pass(FLOOR / 2 - 64);
|
||||
check(live > due && live <= due + slack, "a capped pass came early or late");
|
||||
live = grow_dead_cycle_until_full_pass(0);
|
||||
check(live <= FLOOR + FLOOR * cap / 2 + slack,
|
||||
"a pass that found old garbage did not narrow the growth fraction");
|
||||
check(live <= narrowed, "a pass that found old garbage did not narrow the growth fraction");
|
||||
}
|
||||
|
||||
static void check_deep_ring(void) {
|
||||
|
||||
@@ -71,6 +71,84 @@ static void typed_view_failure_tests(void) {
|
||||
scr_str_release(source);
|
||||
}
|
||||
|
||||
/* Live array capsules with bound element hooks answer per-element queries
|
||||
* from the native array; only holes, out-of-range reads, and rejected
|
||||
* values may fall back to materializing the whole array. */
|
||||
static size_t live_array_materializations;
|
||||
static ScrDyn *live_array_materialize(void *value) {
|
||||
live_array_materializations++;
|
||||
ScrArr *arr = value;
|
||||
ScrDyn *view = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < arr->len; i++) {
|
||||
if (scr_arr_state(arr, (double)i) == (double)SCR_ARR_VALUE)
|
||||
scr_dyn_arr_push(view, scr_dyn_new_num(scr_arr_get_f64(arr, (double)i)));
|
||||
else scr_dyn_arr_push_hole(view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
static ScrDyn *live_array_get(void *arr, double index) {
|
||||
return scr_dyn_new_num(scr_arr_get_f64(arr, index));
|
||||
}
|
||||
static bool live_array_set(void *arr, double index, const ScrDyn *value) {
|
||||
if (value->kind != SCR_DYN_NUM) return false;
|
||||
scr_arr_set_f64(arr, index, value->v.num);
|
||||
return true;
|
||||
}
|
||||
static const ScrDynTypedArrayOps live_array_ops = { live_array_get, live_array_set };
|
||||
|
||||
static void live_array_ref_tests(void) {
|
||||
ScrArr *arr = scr_arr_new(SCR_ELEM_F64, 4);
|
||||
for (int i = 0; i < 4; i++) scr_arr_push_f64(arr, i * 10);
|
||||
scr_arr_delete(arr, 2);
|
||||
ScrDyn *capsule = scr_dyn_new_typed_ref(arr, scr_arr_retain_v, scr_arr_release_v,
|
||||
"array<f64>", 10, live_array_materialize, NULL);
|
||||
scr_dyn_typed_ref_bind_array(capsule, &live_array_ops);
|
||||
live_array_materializations = 0;
|
||||
|
||||
ScrStr *length = S("length"), *one = S("1"), *two = S("2"), *four = S("4"), *five = S("5"), *lead = S("01");
|
||||
ScrDyn *got = scr_dyn_typed_ref_key_get(capsule, length);
|
||||
check(got && got->kind == SCR_DYN_NUM && got->v.num == 4, "live array length");
|
||||
scr_dyn_release(got);
|
||||
got = scr_dyn_typed_ref_key_get(capsule, one);
|
||||
check(got && got->kind == SCR_DYN_NUM && got->v.num == 10, "live array element");
|
||||
scr_dyn_release(got);
|
||||
check(!scr_dyn_typed_ref_key_get(capsule, two), "live array hole needs the view");
|
||||
check(!scr_dyn_typed_ref_key_get(capsule, four), "live array past length needs the view");
|
||||
check(!scr_dyn_typed_ref_key_get(capsule, lead), "noncanonical index needs the view");
|
||||
check(scr_dyn_has_key(capsule, one) && scr_dyn_has_own(capsule, one) && scr_dyn_has_own(capsule, length),
|
||||
"live array own presence");
|
||||
check(!scr_dyn_has_own(capsule, two) && !scr_dyn_has_own(capsule, four), "live array own absence");
|
||||
check(live_array_materializations == 0, "live array reads never materialize");
|
||||
|
||||
ScrDyn *value = scr_dyn_new_num(7);
|
||||
scr_dyn_key_set(capsule, one, value);
|
||||
scr_dyn_key_set(capsule, two, value);
|
||||
scr_dyn_key_set(capsule, four, value);
|
||||
scr_dyn_release(value);
|
||||
check(arr->len == 5 && scr_arr_get_f64(arr, 1) == 7 && scr_arr_get_f64(arr, 2) == 7 &&
|
||||
scr_arr_get_f64(arr, 4) == 7, "live array writes and appends land natively");
|
||||
check(live_array_materializations == 0, "live array writes never materialize");
|
||||
|
||||
ScrDyn *text = scr_dyn_new_str(one);
|
||||
scr_dyn_key_set(capsule, one, text);
|
||||
scr_dyn_release(text);
|
||||
check(live_array_materializations == 1 && scr_arr_get_f64(arr, 1) == 7,
|
||||
"rejected value takes the snapshot path without a native write");
|
||||
ScrStr *seven = S("7");
|
||||
value = scr_dyn_new_num(1);
|
||||
scr_dyn_key_set(capsule, seven, value);
|
||||
scr_dyn_release(value);
|
||||
check(live_array_materializations == 2 && arr->len == 5, "growth past length takes the snapshot path");
|
||||
check(!scr_dyn_has_key(capsule, five) && live_array_materializations == 3,
|
||||
"absent index consults the view");
|
||||
check(!scr_exc_pending(), "live array fallbacks raise nothing without a commit");
|
||||
|
||||
scr_str_release(length); scr_str_release(one); scr_str_release(two);
|
||||
scr_str_release(four); scr_str_release(five); scr_str_release(seven); scr_str_release(lead);
|
||||
scr_dyn_release(capsule);
|
||||
scr_arr_release(arr);
|
||||
}
|
||||
|
||||
/* Parse `text`, expect success, return the checked-dynamic tree (+1). */
|
||||
static ScrDyn *parse_ok(const char *text, const char *name) {
|
||||
ScrStr *t = S(text);
|
||||
@@ -560,6 +638,7 @@ int main(void) {
|
||||
|
||||
json_callback_tests();
|
||||
typed_view_failure_tests();
|
||||
live_array_ref_tests();
|
||||
|
||||
printf("%d/%d checks passed\n", checks - failures, checks);
|
||||
return failures == 0 ? 0 : 1;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// A static array passed where a dynamically typed array is expected travels
|
||||
// as a live reference: element reads, length, writes, presence, iteration,
|
||||
// and push/pop/at act on the original array, and changes made through
|
||||
// either view are visible through the other. The 100k-element loops pin
|
||||
// that each access costs one element rather than a copy of the array.
|
||||
|
||||
type Slot = object | undefined;
|
||||
type Loose = any[];
|
||||
|
||||
const N = 100_000;
|
||||
|
||||
function sumByIndex(xs: Slot[]): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < xs.length; i++) total += xs[i] as unknown as number;
|
||||
return total;
|
||||
}
|
||||
function scale(xs: Loose, factor: number): void {
|
||||
for (let i = 0; i < xs.length; i++) xs[i] = (xs[i] as number) * factor;
|
||||
}
|
||||
function countPresent(xs: Slot[]): number {
|
||||
let present = 0;
|
||||
for (let i = 0; i < xs.length; i++) if (i in xs && Object.hasOwn(xs, i)) present++;
|
||||
return present;
|
||||
}
|
||||
function sumByIteration(xs: Slot[]): number {
|
||||
let total = 0;
|
||||
for (const value of xs) total += value as unknown as number;
|
||||
return total;
|
||||
}
|
||||
function churn(xs: Loose, count: number): number {
|
||||
for (let i = 0; i < count; i++) xs.push(i);
|
||||
let total = 0;
|
||||
for (let i = 0; i < count; i++) total += (xs.at(-1 - i) as number) - (xs.at(i) as number);
|
||||
for (let i = 0; i < count; i++) total += xs.pop() as number;
|
||||
return total;
|
||||
}
|
||||
function writeThrough(xs: Slot[], index: number, value: unknown): void {
|
||||
(xs as unknown[])[index] = value;
|
||||
}
|
||||
|
||||
// Large live arrays: every loop would take minutes if an access rebuilt
|
||||
// the whole array.
|
||||
const big: number[] = [];
|
||||
for (let i = 0; i < N; i++) big.push(i);
|
||||
const bigView = big as unknown as Slot[];
|
||||
console.log("sum", sumByIndex(bigView), sumByIteration(bigView));
|
||||
scale(big, 2);
|
||||
console.log("scaled", big[0], big[1], big[N - 1], sumByIndex(bigView));
|
||||
console.log("present", countPresent(bigView));
|
||||
console.log("churn", churn(big, N), big.length, big[N - 1]);
|
||||
big.length = 10;
|
||||
console.log("truncated", bigView.length, bigView[9], bigView[10]);
|
||||
|
||||
const floats = new Float64Array(N);
|
||||
for (let i = 0; i < N; i++) floats[i] = i / 2;
|
||||
console.log("typed array", sumByIndex(floats as unknown as Slot[]), sumByIteration(floats as unknown as Slot[]));
|
||||
|
||||
interface Point { x: number; y: number }
|
||||
const many: Point[] = [];
|
||||
for (let i = 0; i < N; i++) many.push({ x: i, y: -i });
|
||||
const manyView = many as unknown as Slot[];
|
||||
let nested = 0;
|
||||
for (let i = 0; i < manyView.length; i++) {
|
||||
const point = manyView[i] as Point;
|
||||
point.y = point.x * 2;
|
||||
nested += point.x;
|
||||
}
|
||||
console.log("nested", nested, many[N - 1]!.y, (manyView[N - 1] as Point).y);
|
||||
|
||||
// Reads, length, holes, and out-of-range indexes through the view.
|
||||
const nums: number[] = [1, 2, 3, 4];
|
||||
const view = nums as unknown as Slot[];
|
||||
console.log("len", view.length, "first", view[0], "last", view[view.length - 1], "oob", view[view.length], view[-1 as number]);
|
||||
console.log("in", 0 in view, view.length in view, "length" in view, Object.hasOwn(view, 3), Object.hasOwn(view, 4));
|
||||
|
||||
// Writes are visible both ways.
|
||||
writeThrough(view, 1, 20);
|
||||
console.log("native sees", nums[1], nums.length);
|
||||
nums[2] = 30;
|
||||
console.log("view sees", view[2], view.length);
|
||||
nums.push(5);
|
||||
console.log("view after native push", view.length, view[4]);
|
||||
writeThrough(view, 5, 6);
|
||||
console.log("append", nums.length, nums[5]);
|
||||
writeThrough(view, 8, 9);
|
||||
console.log("gap", nums.length, JSON.stringify(nums), 6 in nums, 7 in view, Object.hasOwn(view, 7));
|
||||
console.log("hole reads", view[6], view[7]);
|
||||
nums.length = 3;
|
||||
console.log("native truncate", view.length, view[3], JSON.stringify(view));
|
||||
|
||||
// Holes in the original array.
|
||||
const holey: number[] = [1, , 3] as number[];
|
||||
const holeyView = holey as unknown as Slot[];
|
||||
console.log("holey", holeyView.length, holeyView[1], 1 in holeyView, holeyView[2]);
|
||||
writeThrough(holeyView, 1, 2);
|
||||
console.log("filled", JSON.stringify(holey), 1 in holey);
|
||||
let holeySum = 0;
|
||||
const sparse: number[] = [1, , 3] as number[];
|
||||
for (const value of sparse as unknown as Slot[]) holeySum += value === undefined ? 100 : (value as unknown as number);
|
||||
console.log("holey iteration", holeySum);
|
||||
|
||||
// Strings and booleans.
|
||||
const words: string[] = ["a", "b"];
|
||||
const wordsView = words as unknown as Slot[];
|
||||
writeThrough(wordsView, 0, "z");
|
||||
(wordsView as unknown as Loose).push("c");
|
||||
console.log("words", words.join(""), wordsView[1], wordsView.length, (wordsView as unknown as Loose).at(-1));
|
||||
const flags: boolean[] = [true, false];
|
||||
const flagsView = flags as unknown as Slot[];
|
||||
writeThrough(flagsView, 1, true);
|
||||
console.log("flags", flags[1], flagsView[0], (flagsView as unknown as Loose).pop(), flags.length);
|
||||
|
||||
// Nested objects keep identity and liveness both ways.
|
||||
const points: Point[] = [{ x: 1, y: 2 }, { x: 3, y: 4 }];
|
||||
const pointsView = points as unknown as Slot[];
|
||||
const p0 = pointsView[0] as Point;
|
||||
console.log("nested", p0.x, pointsView[0] === pointsView[0], p0 === pointsView[0], (pointsView[1] as Point).y);
|
||||
p0.x = 10;
|
||||
console.log("native nested", points[0]!.x);
|
||||
points[1]!.y = 40;
|
||||
console.log("view nested", (pointsView[1] as Point).y);
|
||||
writeThrough(pointsView, 0, { x: 7, y: 8 });
|
||||
console.log("replaced", points[0]!.x, (pointsView[0] as Point).y, p0.x);
|
||||
points[1] = { x: 5, y: 6 };
|
||||
console.log("native replaced", (pointsView[1] as Point).x);
|
||||
const maybe: (Point | undefined)[] = [{ x: 1, y: 1 }, undefined];
|
||||
const maybeView = maybe as unknown as Slot[];
|
||||
writeThrough(maybeView, 1, { x: 2, y: 2 });
|
||||
writeThrough(maybeView, 0, undefined);
|
||||
console.log("optional", maybe[0] === undefined, maybe[1]!.x, maybeView[0], (maybeView[1] as Point).y);
|
||||
|
||||
// Nested arrays.
|
||||
const grid: number[][] = [[1, 2], [3, 4]];
|
||||
const gridView = grid as unknown as Slot[];
|
||||
const row = gridView[1] as number[];
|
||||
row[0] = 30;
|
||||
console.log("grid", grid[1]![0], (gridView[0] as number[]).length, gridView.length);
|
||||
grid[0]!.push(99);
|
||||
console.log("grid push", (gridView[0] as number[])[2], (gridView[0] as number[]).length);
|
||||
|
||||
// Iteration sees elements appended during the loop.
|
||||
const growing: number[] = [1, 2, 3];
|
||||
let total = 0;
|
||||
for (const value of growing as unknown as Slot[]) {
|
||||
total += value as unknown as number;
|
||||
if (total === 1) growing.push(4);
|
||||
}
|
||||
console.log("for-of", total);
|
||||
const keys: string[] = [];
|
||||
for (const key in growing as unknown as Slot[]) keys.push(key);
|
||||
console.log("for-in", keys.join(","), JSON.stringify(Object.keys(growing as unknown as Slot[])));
|
||||
console.log("isArray", Array.isArray(growing as unknown as Slot[]), typeof (growing as unknown as Slot[]));
|
||||
@@ -0,0 +1,125 @@
|
||||
// An optional chain binds its receiver once. A receiver projected from a
|
||||
// stable owner (`owner.field?.member`) is borrowed while the chain's body
|
||||
// cannot remove a reference edge, and the bound value is borrowed by the body
|
||||
// whenever the chain's receiver keeps it alive. Bodies that replace or clear
|
||||
// the receiver's field, or drop the last other reference, must still see the
|
||||
// value the chain bound first, exactly as Node does.
|
||||
|
||||
class Sym {
|
||||
flags: number;
|
||||
name: string;
|
||||
constructor(name: string, flags: number) {
|
||||
this.name = name;
|
||||
this.flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
class Table {
|
||||
members: Map<string, Sym> | undefined = undefined;
|
||||
first: Sym | undefined = undefined;
|
||||
items: (Sym | undefined)[] = [];
|
||||
next: Table | undefined = undefined;
|
||||
onRead: ((s: string) => number) | undefined = undefined;
|
||||
label(): string {
|
||||
return `table(${this.members?.size ?? 0})`;
|
||||
}
|
||||
}
|
||||
|
||||
class Owner {
|
||||
table: Table | undefined;
|
||||
constructor(table: Table | undefined) {
|
||||
this.table = table;
|
||||
}
|
||||
}
|
||||
|
||||
function lookup(t: Table, name: string): Sym | undefined {
|
||||
const s = t.members?.get(name);
|
||||
if (s !== undefined && s.flags !== 0) return s;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function deep(o: Owner, name: string): number {
|
||||
return o.table?.next?.members?.get(name)?.flags ?? -1;
|
||||
}
|
||||
|
||||
let cleared = 0;
|
||||
function clearAndCount(o: Owner): number {
|
||||
o.table = undefined;
|
||||
cleared++;
|
||||
return cleared;
|
||||
}
|
||||
|
||||
function replaceMembers(t: Table): Map<string, Sym> {
|
||||
const fresh = new Map<string, Sym>();
|
||||
fresh.set("fresh", new Sym("fresh", 9));
|
||||
t.members = fresh;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
function makeTable(n: number): Table {
|
||||
const t = new Table();
|
||||
const m = new Map<string, Sym>();
|
||||
for (let i = 0; i < n; i++) m.set(`k${i}`, new Sym(`k${i}`, i));
|
||||
t.members = m;
|
||||
t.first = new Sym("first", 1);
|
||||
t.items = [new Sym("a", 1), undefined, new Sym("c", 3)];
|
||||
return t;
|
||||
}
|
||||
|
||||
const t = makeTable(4);
|
||||
console.log(lookup(t, "k2")?.name, lookup(t, "k0")?.name, lookup(t, "zz")?.name);
|
||||
|
||||
// The field is cleared by the body's argument: the chain still reads the map
|
||||
// it bound first, and the map stays alive for the whole call.
|
||||
const o = new Owner(t);
|
||||
console.log(o.table?.members?.get(`k${clearAndCount(o)}`)?.name, o.table === undefined);
|
||||
|
||||
// The body replaces the field the receiver was read from.
|
||||
const t2 = makeTable(3);
|
||||
console.log(t2.members?.get(replaceMembers(t2).has("fresh") ? "k1" : "k2")?.name, t2.members?.size);
|
||||
|
||||
// The only other reference disappears inside the body.
|
||||
const holder = new Owner(makeTable(2));
|
||||
const flags = holder.table?.first?.flags;
|
||||
const read = holder.table?.items.map((s) => {
|
||||
holder.table = undefined;
|
||||
return s?.name ?? "-";
|
||||
});
|
||||
console.log(flags, read?.join(","), holder.table === undefined);
|
||||
|
||||
// Nested chains over stable owners, including missing links.
|
||||
const a = makeTable(2);
|
||||
const b = makeTable(5);
|
||||
a.next = b;
|
||||
console.log(deep(new Owner(a), "k4"), deep(new Owner(b), "k4"), deep(new Owner(undefined), "k1"));
|
||||
|
||||
// Chains over call results and element reads.
|
||||
function current(): Table | undefined {
|
||||
return a;
|
||||
}
|
||||
console.log(current()?.members?.get("k1")?.name, a.items[2]?.name, a.items[1]?.name ?? "hole");
|
||||
|
||||
// Optional calls through a field that the callback itself clears.
|
||||
function callRead(t: Table, s: string): number | undefined {
|
||||
return t.onRead?.(s);
|
||||
}
|
||||
const c = makeTable(1);
|
||||
c.onRead = (s: string): number => {
|
||||
c.onRead = undefined;
|
||||
return s.length;
|
||||
};
|
||||
console.log(callRead(c, "abc"), callRead(c, "abcd"), c.label());
|
||||
|
||||
// Values read through a chain outlive later stores to its receiver.
|
||||
const d = makeTable(3);
|
||||
const kept = d.members?.get("k2");
|
||||
d.members = undefined;
|
||||
console.log(kept?.name, kept?.flags, lookup(d, "k2"));
|
||||
|
||||
// A loop that exercises the borrowed lookup heavily.
|
||||
let total = 0;
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const s = b.members?.get(`k${i % 7}`);
|
||||
total += s?.flags ?? 100;
|
||||
}
|
||||
console.log(total);
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Item, emptyItems } from "./items.ts";
|
||||
|
||||
export function compareLists(a: readonly Item[] | undefined, b: readonly Item[] | undefined): number {
|
||||
const x = a ?? emptyItems;
|
||||
const y = b ?? emptyItems;
|
||||
if (x.length !== y.length) return x.length - y.length;
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
const r = x[i]!.name < y[i]!.name ? -1 : x[i]!.name > y[i]!.name ? 1 : 0;
|
||||
if (r !== 0) return r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Appends `count` fillers to `list`, which also owns `keep`, then reads `keep`. */
|
||||
export function appendMany(list: Item[], keep: Item, count: number, filler: Item): number {
|
||||
for (let i = 0; i < count; i++) list.push(filler);
|
||||
return keep.name.length;
|
||||
}
|
||||
|
||||
const ids = new Int32Array(new SharedArrayBuffer(4));
|
||||
|
||||
export function labelOf(item: Item): string {
|
||||
if (item.id === 0) item.id = Atomics.add(ids, 0, 1) + 1;
|
||||
return `${item.name}#${item.id}`;
|
||||
}
|
||||
|
||||
export function snapshot(list: readonly Item[], extra: Item): Item[] {
|
||||
const head = list.slice(0, 2);
|
||||
const out = [extra, head[0]!, head[1]!];
|
||||
out.push(extra);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Removes the element that also arrives as `keep`, then reads `keep`. */
|
||||
export function shiftThenRead(list: Item[], keep: Item): string {
|
||||
const removed = list.shift()?.name.length ?? -1;
|
||||
return `${keep.name}/${removed}`;
|
||||
}
|
||||
|
||||
/** Truncates the array that owns `keep`, then reads `keep`. */
|
||||
export function truncateThenRead(list: Item[], keep: Item): string {
|
||||
list.length = 0;
|
||||
return keep.name;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { compareLists } from "./compare.ts";
|
||||
|
||||
export class Item {
|
||||
name: string;
|
||||
id = 0;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
export class Holder {
|
||||
list: Item[] = [];
|
||||
short: Item[] | undefined = undefined;
|
||||
first: Item;
|
||||
constructor(first: Item) {
|
||||
this.first = first;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeHolder(n: number): Holder {
|
||||
const h = new Holder(new Item("first"));
|
||||
for (let i = 0; i < n; i++) h.list.push(new Item(`item${i}`));
|
||||
h.short = [new Item("s0"), new Item("s1")];
|
||||
return h;
|
||||
}
|
||||
|
||||
// Read while the cycle is still evaluating: the comparator sees its empty
|
||||
// list binding before its declaration ran.
|
||||
let early = "";
|
||||
try {
|
||||
early = String(compareLists(undefined, undefined));
|
||||
} catch (error) {
|
||||
early = error instanceof ReferenceError ? "tdz" : "other";
|
||||
}
|
||||
console.log("early", early);
|
||||
|
||||
export const emptyItems: readonly Item[] = [];
|
||||
console.log("late", compareLists(undefined, []));
|
||||
@@ -0,0 +1,51 @@
|
||||
// Callees that append to arrays, build fresh arrays, bump an atomic id
|
||||
// counter or read a module binding behind its import-cycle TDZ check remove
|
||||
// no reference edge, so callers pass field projections borrowed. The values
|
||||
// must stay intact even when the callee appends to the very array that owns
|
||||
// a borrowed argument and moves its storage.
|
||||
import { Holder, Item, makeHolder } from "./items.ts";
|
||||
import { appendMany, compareLists, labelOf, shiftThenRead, snapshot, truncateThenRead } from "./compare.ts";
|
||||
|
||||
const h = makeHolder(3);
|
||||
const filler = new Item("filler");
|
||||
console.log(appendMany(h.list, h.list[0]!, 200, filler), h.list.length, h.list[0]!.name);
|
||||
console.log(labelOf(h.first), labelOf(h.first), labelOf(h.list[5]!));
|
||||
console.log(snapshot(h.list, h.first).map((i: Item) => i.name).join(","));
|
||||
|
||||
const other: Holder = makeHolder(3);
|
||||
console.log(compareLists(h.short, other.short), compareLists(undefined, other.short), compareLists(h.short, undefined));
|
||||
other.short = undefined;
|
||||
console.log(compareLists(h.short, other.short), compareLists(other.short, other.short));
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 300; i++) sum += appendMany(h.list, h.list[i % 7]!, 1, filler);
|
||||
console.log(sum, h.list.length);
|
||||
|
||||
function removals(d: Holder): string {
|
||||
d.list[0] = new Item("only-in-list");
|
||||
const shifted = shiftThenRead(d.list, d.list[0]!);
|
||||
d.list[0] = new Item("truncated");
|
||||
const truncated = truncateThenRead(d.list, d.list[0]!);
|
||||
return `${shifted} ${truncated} ${d.list.length}`;
|
||||
}
|
||||
console.log(removals(makeHolder(2)));
|
||||
|
||||
// Loop bindings borrow their element while the loop body preserves edges:
|
||||
// appending (and moving the storage) keeps them; removing them must not.
|
||||
function grow(list: Item[], filler: Item): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const x = list[i]!;
|
||||
total += appendMany(list, x, 64, filler) + x.name.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
function drain(list: Item[]): string {
|
||||
let out = "";
|
||||
for (const x of list) out += shiftThenRead(list, x) + ";";
|
||||
return out;
|
||||
}
|
||||
const g = makeHolder(6);
|
||||
console.log(grow(g.list, filler), g.list.length);
|
||||
const e = makeHolder(6);
|
||||
console.log(drain(e.list), e.list.length);
|
||||
@@ -0,0 +1,10 @@
|
||||
export const SLASH = 0x2f;
|
||||
export const DOT = 0x2e;
|
||||
export const NEGATIVE = -1;
|
||||
export const HALF = 0.5;
|
||||
export const ENABLED = true;
|
||||
export const MINUS_ZERO = -0;
|
||||
export let counter = 0;
|
||||
export function bump(): number {
|
||||
return ++counter;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Module constants read as their literal values; bindings that change or
|
||||
// can be read before their declaration keep their global.
|
||||
import * as ns from "./consts.ts";
|
||||
import { DOT, ENABLED, HALF, MINUS_ZERO, NEGATIVE, SLASH, bump, counter } from "./consts.ts";
|
||||
|
||||
const COLON = 0x3a;
|
||||
const LIMIT = 3;
|
||||
|
||||
function classify(code: number): string {
|
||||
switch (code) {
|
||||
case SLASH:
|
||||
return "slash";
|
||||
case DOT:
|
||||
return "dot";
|
||||
case COLON:
|
||||
return "colon";
|
||||
case NEGATIVE:
|
||||
return "negative";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
|
||||
const path = "a/b.c:d";
|
||||
const kinds: string[] = [];
|
||||
for (let i = 0; i < path.length; i++) kinds.push(classify(path.charCodeAt(i)));
|
||||
console.log(kinds.join(","), classify(-1), classify(NaN));
|
||||
console.log(HALF * 3, ENABLED && LIMIT > 2, Object.is(MINUS_ZERO, -0), 1 / MINUS_ZERO);
|
||||
console.log(ns.DOT, ns.SLASH, Object.keys(ns).sort().join(" "));
|
||||
|
||||
// A mutable export keeps observing writes.
|
||||
bump();
|
||||
bump();
|
||||
console.log(counter, ns.counter);
|
||||
|
||||
// A function hoisted above the declaration it reads still throws in the TDZ.
|
||||
try {
|
||||
console.log(early());
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
const LATE = 7;
|
||||
function early(): number {
|
||||
return LATE * 2;
|
||||
}
|
||||
console.log(early());
|
||||
|
||||
// A loop binding at module scope is written once per iteration.
|
||||
const totals: number[] = [];
|
||||
for (const step of [1, 2, 3]) totals.push(step * LIMIT);
|
||||
console.log(totals.join(" "));
|
||||
@@ -0,0 +1,112 @@
|
||||
// Indexes that are not proven integers: typed-array reads, inline ASCII
|
||||
// charCodeAt, local array reads and numeric-keyed lookups decide validity
|
||||
// with a signed round trip. Every index shape must keep Node's answer.
|
||||
|
||||
const odd: number[] = [
|
||||
0, -0, 1, 2.5, -1, -0.5, 0.5, 3, 4, 7, 8, 1e-300, 2 ** 31, 2 ** 32, 2 ** 52, 2 ** 53, 2 ** 53 + 2,
|
||||
2 ** 63, 2 ** 64, -(2 ** 63), 1e300, -1e300, Infinity, -Infinity, NaN,
|
||||
];
|
||||
|
||||
// Values the compiler cannot see through.
|
||||
function opaque(values: number[], i: number): number {
|
||||
return values[i % values.length]!;
|
||||
}
|
||||
|
||||
class Cursor {
|
||||
pos = 0;
|
||||
readonly units: Uint16Array;
|
||||
readonly bytes: Uint8Array;
|
||||
readonly words: Int32Array;
|
||||
readonly text: string;
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
this.units = new Uint16Array(text.length);
|
||||
for (let i = 0; i < text.length; i++) this.units[i] = text.charCodeAt(i);
|
||||
this.bytes = new Uint8Array([1, 2, 3, 250, 255]);
|
||||
this.words = new Int32Array([-1, 2147483647, -2147483648, 7]);
|
||||
}
|
||||
unit(): number {
|
||||
return this.units[this.pos];
|
||||
}
|
||||
byte(): number {
|
||||
return this.bytes[this.pos];
|
||||
}
|
||||
word(): number {
|
||||
return this.words[this.pos];
|
||||
}
|
||||
code(): number {
|
||||
return this.text.charCodeAt(this.pos);
|
||||
}
|
||||
// ToInt32 / ToUint32 of a read that may be missing (NaN -> 0).
|
||||
mixed(): number {
|
||||
return (this.units[this.pos] ^ 0x5a5a) + (this.bytes[this.pos] | 0) + (this.words[this.pos] >>> 0);
|
||||
}
|
||||
}
|
||||
|
||||
const cursor = new Cursor("scriptc!");
|
||||
const rows: string[] = [];
|
||||
for (let k = 0; k < odd.length; k++) {
|
||||
cursor.pos = opaque(odd, k);
|
||||
rows.push(
|
||||
[
|
||||
String(cursor.pos),
|
||||
String(cursor.unit()),
|
||||
String(cursor.byte()),
|
||||
String(cursor.word()),
|
||||
String(cursor.code()),
|
||||
String(cursor.mixed()),
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
console.log(rows.join("\n"));
|
||||
|
||||
// Local arrays read by an unproven index: holes, out-of-range and fractions.
|
||||
function arrayReads(index: number): string {
|
||||
const values: (string | undefined)[] = ["a", "b", undefined, "d"];
|
||||
const numbers = [10, 20, 30];
|
||||
return `${values[index]}|${numbers[index]}|${numbers[index] ?? "none"}`;
|
||||
}
|
||||
for (let k = 0; k < odd.length; k++) console.log(arrayReads(opaque(odd, k)));
|
||||
|
||||
// A non-ASCII string keeps the runtime path for every index.
|
||||
const wide = "héllo😀";
|
||||
for (let k = 0; k < odd.length; k++) {
|
||||
const i = opaque(odd, k);
|
||||
console.log(i, wide.charCodeAt(i), "abc".charCodeAt(i));
|
||||
}
|
||||
|
||||
// Numeric-keyed maps with a dense integer index.
|
||||
const byIndex = new Map<number, string>();
|
||||
for (let i = 0; i < 6; i++) byIndex.set(i, `v${i}`);
|
||||
for (let k = 0; k < odd.length; k++) {
|
||||
const key = opaque(odd, k);
|
||||
console.log(key, byIndex.get(key) ?? "absent", byIndex.has(key));
|
||||
}
|
||||
|
||||
// A hash over a range whose bounds come from fields.
|
||||
function rangeHash(chars: Uint16Array, start: number, end: number): number {
|
||||
let hash = 0x811c9dc5 | 0;
|
||||
for (let i = start; i < end; i++) hash = Math.imul(hash ^ chars[i], 0x01000193);
|
||||
return hash ^ (hash >>> 15);
|
||||
}
|
||||
console.log(
|
||||
rangeHash(cursor.units, 0, 8),
|
||||
rangeHash(cursor.units, 2, 12),
|
||||
rangeHash(cursor.units, -3, 2),
|
||||
rangeHash(cursor.units, 0.5, 4),
|
||||
rangeHash(cursor.units, opaque(odd, 1), opaque(odd, 3)),
|
||||
);
|
||||
|
||||
// Counted loops from an unproven start run as integer loops only when the
|
||||
// start is an exact integer; -0, fractions, NaN and huge starts keep the
|
||||
// double loop, and the counter's own value stays observable.
|
||||
function observe(chars: Uint16Array, start: number, end: number): string {
|
||||
const seen: string[] = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
seen.push(`${Object.is(i, -0) ? "-0" : i}:${chars[i] ^ 1}:${(i * 2) | 0}`);
|
||||
if (seen.length > 6) break;
|
||||
}
|
||||
return seen.join(" ");
|
||||
}
|
||||
for (const start of [0, -0, 1, 2.5, -2, NaN, 2 ** 53 - 2, -(2 ** 53), 1e300, -Infinity])
|
||||
console.log(String(start), observe(cursor.units, opaque([start], 0), opaque([start + 4, 9], start === -Infinity ? 1 : 0)));
|
||||
@@ -0,0 +1,127 @@
|
||||
// Reference fields written only by their class's constructors may be read
|
||||
// without a retain wherever their object is held. Every other field keeps
|
||||
// its owning reads: a callee may replace it while the caller still uses the
|
||||
// old value.
|
||||
|
||||
class Buffer2 {
|
||||
readonly items: string[] = [];
|
||||
label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
push(item: string): number {
|
||||
this.items.push(item);
|
||||
return this.items.length;
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.label}:${this.items.join(",")}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Owner {
|
||||
// Final: written only here, twice, with a method call between the writes.
|
||||
readonly primary: Buffer2;
|
||||
// Not final: replaced by `rotate`.
|
||||
current: Buffer2;
|
||||
log: string[] = [];
|
||||
constructor() {
|
||||
this.primary = new Buffer2("first");
|
||||
this.current = new Buffer2("c0");
|
||||
this.log.push(this.peek());
|
||||
this.primary = new Buffer2("primary");
|
||||
this.log.push(this.peek());
|
||||
}
|
||||
peek(): string {
|
||||
const primary = this.primary;
|
||||
primary.push("peeked");
|
||||
return primary.describe();
|
||||
}
|
||||
// The callee replaces `current` while the caller's receiver is the old one.
|
||||
rotate(next: string): Buffer2 {
|
||||
const old = this.current;
|
||||
this.current = new Buffer2(next);
|
||||
return old;
|
||||
}
|
||||
useCurrent(): string {
|
||||
return this.current.push(this.rotate("c1").describe()) + " " + this.current.describe();
|
||||
}
|
||||
usePrimary(): string {
|
||||
const before = this.primary.push("a");
|
||||
const again = this.primary.push(this.rotate("c2").label);
|
||||
return `${before} ${again} ${this.primary.describe()}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Derived extends Owner {
|
||||
readonly extra: Buffer2;
|
||||
constructor() {
|
||||
super();
|
||||
this.extra = new Buffer2("extra");
|
||||
this.extra.push(this.primary.describe());
|
||||
}
|
||||
report(): string {
|
||||
return `${this.extra.describe()} / ${this.primary.describe()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// A field a closure created in the constructor writes later is not final.
|
||||
class Callbacks {
|
||||
target: Buffer2;
|
||||
readonly reset: () => void;
|
||||
constructor() {
|
||||
this.target = new Buffer2("t0");
|
||||
this.reset = () => {
|
||||
this.target = new Buffer2("t1");
|
||||
};
|
||||
}
|
||||
run(): string {
|
||||
return this.target.push(this.fire()) + " " + this.target.describe();
|
||||
}
|
||||
fire(): string {
|
||||
this.reset();
|
||||
return "fired";
|
||||
}
|
||||
}
|
||||
|
||||
// A field written through another object, outside any constructor.
|
||||
class Node2 {
|
||||
next: Node2 | undefined;
|
||||
readonly name: string;
|
||||
readonly payload: Buffer2;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.payload = new Buffer2(name);
|
||||
}
|
||||
}
|
||||
function relink(a: Node2): string {
|
||||
const first = a.next!;
|
||||
a.next = new Node2("b2");
|
||||
return first.payload.describe() + " -> " + a.next.payload.describe();
|
||||
}
|
||||
|
||||
const owner = new Owner();
|
||||
console.log(owner.log.join(" | "));
|
||||
console.log(owner.useCurrent());
|
||||
console.log(owner.usePrimary());
|
||||
console.log(owner.current.describe());
|
||||
const derived = new Derived();
|
||||
console.log(derived.report());
|
||||
const callbacks = new Callbacks();
|
||||
console.log(callbacks.run());
|
||||
const a = new Node2("a");
|
||||
a.next = new Node2("b");
|
||||
a.next.payload.push("x");
|
||||
console.log(relink(a));
|
||||
|
||||
// Async methods keep owning reads across their suspensions.
|
||||
class Pending {
|
||||
readonly box: Buffer2 = new Buffer2("async");
|
||||
async later(): Promise<string> {
|
||||
const box = this.box;
|
||||
box.push("before");
|
||||
await Promise.resolve();
|
||||
box.push("after");
|
||||
return box.describe();
|
||||
}
|
||||
}
|
||||
new Pending().later().then((text) => console.log(text));
|
||||
Reference in New Issue
Block a user