mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Merge main
This commit is contained in:
@@ -1 +1 @@
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. They raise an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile.
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. A function value with fixed parameters cannot be stored where such a variadic function type is expected.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union.
|
||||
- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union. A union may hold one promise beside other members, such as `T | Promise<T>` or `Promise<R | void> | R | void`: `await`, an async function's `return`, `Promise.resolve`, and `instanceof Promise` act on the member it holds, with Node's microtask timing. A promise member beside a member that declares `then`, two promise members in one union, and `console.log` of a union holding a promise are unsupported.
|
||||
|
||||
@@ -418,7 +418,12 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
|
||||
// An internal function referenced by exactly one other function, and by
|
||||
// no global, is a whole-program inlining candidate; keep it with that
|
||||
// function so its partition can inline it without importing.
|
||||
// function so its partition can inline it without importing. A function
|
||||
// body larger than this is past LLVM's inline cost limit even for the only
|
||||
// call to a local function, so it is placed by its own name. Otherwise a
|
||||
// program's module initializers, which only the entry point calls, would
|
||||
// all share the entry point's partition.
|
||||
constexpr size_t MaxInlinedBodyBytes = 256 * 1024;
|
||||
constexpr unsigned Shared = ~1u;
|
||||
std::vector<unsigned> Referrer(Symbols.size(), NoPartition);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
@@ -441,7 +446,8 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
for (unsigned Steps = 0; Steps < 1024; ++Steps) {
|
||||
const Symbol &S = Symbols[Root];
|
||||
unsigned Parent = Referrer[Root];
|
||||
if (!S.Local || Parent == NoPartition || Parent == Shared ||
|
||||
if (!S.Local || S.Body.size() > MaxInlinedBodyBytes ||
|
||||
Parent == NoPartition || Parent == Shared ||
|
||||
Symbols[Parent].Kind != SymbolKind::Function || Parent == I)
|
||||
break;
|
||||
Root = Parent;
|
||||
|
||||
@@ -22,8 +22,8 @@ struct ProgramPartitions {
|
||||
// Divides the LLVM assembly scriptc emits into Count modules without parsing
|
||||
// it, so the partitions can be parsed concurrently. Functions are placed by a
|
||||
// stable hash of their name; an internal function referenced by exactly one
|
||||
// other function stays with that function, and a global stays with the first
|
||||
// function that references it. Returns nullopt for any construct outside the
|
||||
// other function stays with that function unless its body is too large to
|
||||
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
|
||||
// emitter's narrow top-level grammar; callers then split a parsed module.
|
||||
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
|
||||
unsigned Count);
|
||||
|
||||
@@ -152,6 +152,97 @@ function declareGlobal(host: AllocHost, decl: string): void {
|
||||
if (host.threadLocalAlloc !== true) host.declare(decl);
|
||||
}
|
||||
|
||||
/** `scr_cyc_buffers` (scr_rc.h ScrCycBuffers): the three candidate buffers
|
||||
* `{ v, n, cap, used }`, the inline buffering window and the allocation
|
||||
* trigger. */
|
||||
const CYC_BUFFERS_TYPE = "{ [3 x { ptr, i64, i64, i64 }], i64, i64 }";
|
||||
/** Byte offsets in it (asserted in scr_rc.h). */
|
||||
const CYC_NURSERY_COUNT = 8;
|
||||
const CYC_WINDOW = 96;
|
||||
const CYC_ALLOC_TRIGGER = 104;
|
||||
|
||||
/** Whether emitted release paths can append nursery candidates to the
|
||||
* collector's buffer themselves (through {@link cycleAppendHelper}): where
|
||||
* the runtime keeps that state in ordinary globals (executables without
|
||||
* workers and classic libraries, on the 64-bit targets with inline
|
||||
* allocation). */
|
||||
export function inlineBufferingSupported(host: AllocHost): boolean {
|
||||
return host.inlineAlloc === true && host.threadLocalAlloc !== true && host.sizeType === "i64";
|
||||
}
|
||||
|
||||
/** The emitted symbol of {@link cycleAppendHelper}. */
|
||||
export const CYC_APPEND = "sc_cyc_append";
|
||||
|
||||
/** The runtime symbols {@link cycleAppendHelper} uses. The helper is
|
||||
* defined after the module's extern block, so its caller declares them. */
|
||||
export function cycleAppendDecls(): string[] {
|
||||
return [
|
||||
`@scr_cyc_buffers = external global ${CYC_BUFFERS_TYPE}`,
|
||||
`@scr_cyc_live = external global i64`,
|
||||
`declare void @scr_cyc_on_release(ptr)`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The candidate-buffering step of every emitted release that leaves a
|
||||
* headered object alive and unbuffered: scr_rc.h's scr_cyc_append, then
|
||||
* scr_cyc_on_release when it declines. A nursery object goes onto the
|
||||
* nursery buffer while the buffer holds fewer entries than the runtime's
|
||||
* window and the live count is below the allocation trigger.
|
||||
*
|
||||
* One out-of-line definition per module keeps the release fast paths, which
|
||||
* LLVM inlines into their callers, as small as the plain call they had. It
|
||||
* uses preserve_mostcc, so the call clobbers almost no caller registers:
|
||||
* the callers' code around a release stays what it was, and the append
|
||||
* itself saves only the few registers it uses. Its slow path enters the
|
||||
* runtime through a second preserve_mostcc function, which saves the rest. */
|
||||
export function cycleAppendHelper(): string[] {
|
||||
return [
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}(ptr %o) noinline #0 { ; scr_cyc_append, else scr_cyc_on_release`,
|
||||
`entry:`,
|
||||
` %genp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_GEN}`,
|
||||
` %gen = load i8, ptr %genp ; ScrCycHdr.gen`,
|
||||
` %young = icmp eq i8 %gen, 0`,
|
||||
` br i1 %young, label %window, label %call`,
|
||||
`window:`,
|
||||
` %np = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_NURSERY_COUNT}`,
|
||||
` %n = load i64, ptr %np ; nursery buffer entries`,
|
||||
` %wp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_WINDOW}`,
|
||||
` %w = load i64, ptr %wp ; window`,
|
||||
` %room = icmp ult i64 %n, %w`,
|
||||
` br i1 %room, label %live, label %call`,
|
||||
`live:`,
|
||||
` %lv = load i64, ptr @scr_cyc_live`,
|
||||
` %tp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_ALLOC_TRIGGER}`,
|
||||
` %t = load i64, ptr %tp ; allocation trigger`,
|
||||
` %due = icmp uge i64 %lv, %t`,
|
||||
` br i1 %due, label %call, label %append`,
|
||||
`append:`,
|
||||
` %v = load ptr, ptr @scr_cyc_buffers`,
|
||||
` %slot = getelementptr inbounds ptr, ptr %v, i64 %n`,
|
||||
` store ptr %o, ptr %slot`,
|
||||
` %n1 = add i64 %n, 1`,
|
||||
` store i64 %n1, ptr %np`,
|
||||
` %i = trunc i64 %n to i32`,
|
||||
` %b = or i32 %i, -2147483648 ; SCR_CYC_BUFFERED_BIT | position`,
|
||||
` %bufp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_BUF}`,
|
||||
` store i32 %b, ptr %bufp`,
|
||||
` ret void`,
|
||||
`call:`,
|
||||
` tail call preserve_mostcc void @${CYC_APPEND}_call(ptr %o)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
// The runtime call clobbers every caller-saved register, which this
|
||||
// preserve_mostcc function must restore: saving them here keeps those
|
||||
// saves out of the append path above.
|
||||
`define internal preserve_mostcc void @${CYC_APPEND}_call(ptr %o) noinline #0 {`,
|
||||
`entry:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
/** A collector header to install: the object type's constant ScrCycType
|
||||
* descriptor (its trace and teardown; see cycTypeDescriptor). */
|
||||
export interface CycHeader {
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { unshareNodes } from "../../ir/unshare.js";
|
||||
import { copyStableCaptures } from "../../ir/capture-values.js";
|
||||
import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -515,7 +516,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
@@ -543,7 +546,9 @@ export function emitLlvmModuleSource(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
|
||||
@@ -407,6 +407,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.fromDyn": "scr_buffer_from_dyn",
|
||||
"buffer.concat": "scr_buffer_concat",
|
||||
"buffer.concatLen": "scr_buffer_concat_len",
|
||||
"buffer.concatDyn": "scr_buffer_concat_dyn",
|
||||
"buffer.concatLenDyn": "scr_buffer_concat_len_dyn",
|
||||
"buffer.alloc": "scr_buffer_alloc",
|
||||
"buffer.allocUnsafe": "scr_buffer_alloc_unsafe",
|
||||
"buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow",
|
||||
|
||||
@@ -205,6 +205,43 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(fn("sc_rc_retain_promise")).not.toContain("store i32");
|
||||
});
|
||||
|
||||
test("optimized builds buffer candidates through the module's append helper", () => {
|
||||
const emit = (inlineRc: true | "release", extra: Partial<ShapeHost>) => {
|
||||
const { host } = declarationHost(64, inlineRc);
|
||||
const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra };
|
||||
releaseSym(configured, { kind: "dyn" });
|
||||
return {
|
||||
ir: emitInlineRcHelpers(configured).join("\n"),
|
||||
decls: inlineRcDecls(configured),
|
||||
};
|
||||
};
|
||||
for (const mode of [true, "release"] as const) {
|
||||
const { ir, decls } = emit(mode, {});
|
||||
expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)");
|
||||
expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline");
|
||||
// The young test reads the generation byte at obj-5; the append fills
|
||||
// scr_cyc_buffers' nursery buffer and the header's position at obj-4.
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i64 -5");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96");
|
||||
expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104");
|
||||
expect(ir).toContain("load i64, ptr @scr_cyc_live");
|
||||
expect(ir).toContain("or i32 %i, -2147483648");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls).toContain(
|
||||
"@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }",
|
||||
);
|
||||
expect(decls).toContain("@scr_cyc_live = external global i64");
|
||||
}
|
||||
// Worker executables and targets without inline allocation call
|
||||
// scr_cyc_on_release, which tries the same append first.
|
||||
for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) {
|
||||
const { ir, decls } = emit(true, extra);
|
||||
expect(ir).not.toContain("sc_cyc_append");
|
||||
expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
expect(decls.join("\n")).not.toContain("scr_cyc_buffers");
|
||||
}
|
||||
});
|
||||
|
||||
test("uses the wasm32 header offsets", () => {
|
||||
const { host } = declarationHost(32, true);
|
||||
releaseSym(host, { kind: "union", unionId: "u0" } as never);
|
||||
|
||||
@@ -28,7 +28,16 @@ import {
|
||||
mangleRecordStruct,
|
||||
mangleRecordTrace,
|
||||
} from "../mangle.js";
|
||||
import { CYC_HDR, cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import {
|
||||
CYC_HDR,
|
||||
cycTypeDescriptor,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
CYC_APPEND,
|
||||
cycleAppendDecls,
|
||||
cycleAppendHelper,
|
||||
inlineBufferingSupported,
|
||||
} from "./alloc.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
@@ -479,6 +488,10 @@ export const INLINE_RC_FAMILIES: Readonly<Record<string, InlineRcFamily>> = {
|
||||
|
||||
type RcOp = "retain" | "release";
|
||||
|
||||
/** The helper key (with the "family:op" keys) of the module's candidate
|
||||
* append helper (alloc.ts cycleAppendHelper). */
|
||||
const CYC_APPEND_KEY = "cyc:append";
|
||||
|
||||
/** The mirrored family for an RC operation on a runtime stem (`scr_str`),
|
||||
* or null when it stays a call. */
|
||||
export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineRcFamily | null {
|
||||
@@ -540,6 +553,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string {
|
||||
}
|
||||
host.rcHelpers.add(`${family}:${op}`);
|
||||
for (const decl of rcHelperDecls(family, op)) host.declare(decl);
|
||||
// Its body is written last (emitInlineRcHelpers), after the requests are
|
||||
// collected: a release that can buffer a candidate requests the append
|
||||
// helper now.
|
||||
if (op === "release" && INLINE_RC_FAMILIES[family]!.cycle === "always") requestCycleAppend(host);
|
||||
return `@${inlineRcName(family, op)}`;
|
||||
}
|
||||
|
||||
@@ -637,6 +654,10 @@ function rcHelperKeys(host: ShapeHost): string[] {
|
||||
export function inlineRcDecls(host: ShapeHost): string[] {
|
||||
const decls = new Set<string>();
|
||||
for (const key of rcHelperKeys(host)) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
for (const decl of cycleAppendDecls()) decls.add(decl);
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
for (const decl of rcHelperDecls(family, op)) decls.add(decl);
|
||||
}
|
||||
@@ -782,6 +803,10 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st
|
||||
export function emitInlineRcHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of rcHelperKeys(host).sort()) {
|
||||
if (key === CYC_APPEND_KEY) {
|
||||
out.push(...cycleAppendHelper());
|
||||
continue;
|
||||
}
|
||||
const [family, op] = key.split(":") as [string, RcOp];
|
||||
const row = INLINE_RC_FAMILIES[family]!;
|
||||
out.push(
|
||||
@@ -1434,12 +1459,6 @@ export function releaseFastPath(
|
||||
];
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object) and call the runtime only to enqueue a new candidate.
|
||||
* Every emitted release of a headered object shares this sequence
|
||||
* (scr_runtime.h asserts the offset); it ends by branching to `done`. */
|
||||
/** One traced field of an emitted trace function: load the slot and visit
|
||||
* it unless it is null. Every collector visitor ignores null children, and
|
||||
* most traced slots of a typical heap are empty (optional links, unset
|
||||
@@ -1463,16 +1482,39 @@ export function traceFieldVisitLines(
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether candidate buffering goes through the module's append helper
|
||||
* (alloc.ts cycleAppendHelper), requesting its definition if so: in
|
||||
* optimized builds (those with inline RC helpers) that support it. */
|
||||
function requestCycleAppend(host: ShapeHost): boolean {
|
||||
if (host.rcHelpers === null || !inlineBufferingSupported(host)) return false;
|
||||
if (!host.rcHelpers.has(CYC_APPEND_KEY)) {
|
||||
host.rcHelpers.add(CYC_APPEND_KEY);
|
||||
for (const decl of cycleAppendDecls()) host.declare(decl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Test the sign
|
||||
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
|
||||
* object), and buffer a new candidate: through the module's
|
||||
* cycleAppendHelper in optimized builds that support it, through
|
||||
* scr_cyc_on_release otherwise. Every emitted release of a headered object
|
||||
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
|
||||
* branching to `done`. */
|
||||
export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
const append = requestCycleAppend(host);
|
||||
return [
|
||||
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %buf = load i32, ptr %bufp`,
|
||||
` %queued = icmp slt i32 %buf, 0`,
|
||||
` br i1 %queued, label %${done}, label %enqueue`,
|
||||
`enqueue:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
append
|
||||
? ` call preserve_mostcc void @${CYC_APPEND}(ptr %o) ; buffer the candidate; may collect`
|
||||
: ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
` br label %${done}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
isJsonStringifyDynamicType,
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrType,
|
||||
classDynViewSupported,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDynTypedRefType,
|
||||
type IrExpr,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
@@ -157,6 +161,16 @@ export function lowerJsonMethodCall(
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
const unreadable = classFieldWithoutJsonView(lowerer, value.type, new Set());
|
||||
if (unreadable !== null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
argNode,
|
||||
`JSON.stringify of '${lowerer.fmt(value.type)}' values (field '${unreadable.field}' of class ` +
|
||||
`'${unreadable.className}' has type '${lowerer.fmt(unreadable.type)}', which the runtime JSON ` +
|
||||
`traversal cannot ${unreadable.reason})`,
|
||||
);
|
||||
}
|
||||
// Keep the original containers live while the runtime visits them:
|
||||
// a toJSON callback can mutate a later field, and shared/cyclic
|
||||
// references must retain their identities throughout traversal.
|
||||
@@ -276,6 +290,100 @@ function lowerOptionalStringifyRoot(
|
||||
return { kind: "call", callee: helper, args: [value], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** True when a class field type holds a RegExp or a Promise outside a
|
||||
* nested class instance (whose own fields are checked separately): Node
|
||||
* writes both as {}, and the runtime JSON traversal has no writer for them. */
|
||||
function holdsUnwritable(lowerer: Lowerer, t: IrType, visiting: Set<string>): boolean {
|
||||
switch (t.kind) {
|
||||
case "regex":
|
||||
case "promise":
|
||||
return true;
|
||||
case "array":
|
||||
return holdsUnwritable(lowerer, t.elem, visiting);
|
||||
case "record": {
|
||||
if (visiting.has(t.shapeId)) return false;
|
||||
visiting.add(t.shapeId);
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
return (
|
||||
(shape?.fields ?? []).some((field) => holdsUnwritable(lowerer, field.type, visiting)) ||
|
||||
(shape?.indexValue !== undefined && holdsUnwritable(lowerer, shape.indexValue, visiting))
|
||||
);
|
||||
}
|
||||
case "union": {
|
||||
if (visiting.has(t.unionId)) return false;
|
||||
visiting.add(t.unionId);
|
||||
return (lowerer.unions.get(t.unionId)?.arms ?? []).some((arm) =>
|
||||
holdsUnwritable(lowerer, arm, visiting),
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The first class field reachable from a stringify input that the class's
|
||||
* checked-dynamic property view cannot represent, or that holds a RegExp or
|
||||
* a Promise, which the runtime JSON traversal cannot write. The traversal
|
||||
* reads class instances through that view, so such a class is refused at
|
||||
* compile time instead of throwing when the view is built or written. */
|
||||
function classFieldWithoutJsonView(
|
||||
lowerer: Lowerer,
|
||||
t: IrType,
|
||||
visiting: Set<string>,
|
||||
): { className: string; field: string; type: IrType; reason: "read" | "write" } | null {
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return classFieldWithoutJsonView(lowerer, t.elem, visiting);
|
||||
case "record": {
|
||||
if (visiting.has(t.shapeId)) return null;
|
||||
visiting.add(t.shapeId);
|
||||
const shape = getRecord(t.shapeId);
|
||||
for (const field of shape?.fields ?? []) {
|
||||
const found = classFieldWithoutJsonView(lowerer, field.type, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return shape?.indexValue
|
||||
? classFieldWithoutJsonView(lowerer, shape.indexValue, visiting)
|
||||
: null;
|
||||
}
|
||||
case "union": {
|
||||
if (visiting.has(t.unionId)) return null;
|
||||
visiting.add(t.unionId);
|
||||
for (const arm of getUnion(t.unionId)?.arms ?? []) {
|
||||
const found = classFieldWithoutJsonView(lowerer, arm, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case "object": {
|
||||
if (!isDynTypedRefType(t) || visiting.has(t.className)) return null;
|
||||
visiting.add(t.className);
|
||||
const info = lowerer.classes.get(t.className);
|
||||
for (const field of info?.def.fields ?? []) {
|
||||
if (!isClassOwnEnumerableFieldName(field.name)) continue;
|
||||
const unreadable = !classDynViewSupported([field], getRecord, getUnion);
|
||||
if (unreadable || holdsUnwritable(lowerer, field.type, new Set())) {
|
||||
const className =
|
||||
info?.def.jsName ?? info?.decl?.name?.text ?? t.className.replace(/^%/, "");
|
||||
return {
|
||||
className,
|
||||
field: field.name,
|
||||
type: field.type,
|
||||
reason: unreadable ? "read" : "write",
|
||||
};
|
||||
}
|
||||
const found = classFieldWithoutJsonView(lowerer, field.type, visiting);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Only explicit null/undefined select the no-callback path. */
|
||||
function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (ts.isParenthesizedExpression(node)) return jsonNullishArgument(lowerer, node.expression);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { checkedPromiseAll } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -402,6 +403,22 @@ export function lowerPromiseStaticCall(
|
||||
if (v.type.kind !== "promise") lowerer.badType(argNode, argT);
|
||||
return v;
|
||||
}
|
||||
const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null;
|
||||
if (maybePromise) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const v = lowerer.lowerExpr(argNode);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type);
|
||||
const resolved =
|
||||
resultT?.kind === "promise" && mp
|
||||
? maybePromiseResolve(lowerer, v, mp, resultT, loc)
|
||||
: null;
|
||||
if (resolved) return resolved;
|
||||
lowerer.noLowering(
|
||||
`Promise.resolve over '${lowerer.fmt(v.type)}' at this result type`,
|
||||
argNode,
|
||||
"the promise arm passes through identically, so it must already be the result's promise type",
|
||||
);
|
||||
}
|
||||
if (
|
||||
argIr?.kind === "union" &&
|
||||
(lowerer.unions.get(argIr.unionId)?.arms ?? []).some((a) => a.kind === "promise")
|
||||
@@ -423,7 +440,17 @@ export function lowerPromiseStaticCall(
|
||||
}
|
||||
}
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
// A maybe-promise context (`return Promise.resolve(v)` from a function
|
||||
// returning `Promise<R | void> | R | void`) names its promise arm: the
|
||||
// fresh promise takes that payload type directly.
|
||||
if (argNode !== undefined && resultT?.kind === "promise") {
|
||||
const ctxT = lowerer.checker.getContextualType(call);
|
||||
const contextual = ctxT?.isUnionType() ? lowerer.mapTypeOf(ctxT) : null;
|
||||
const mp = contextual ? maybePromiseUnionOf(lowerer, contextual) : null;
|
||||
if (mp && !typeEquals(mp.promise, resultT) && mp.promise.inner.kind !== "void")
|
||||
resultT = mp.promise;
|
||||
}
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"Promise.resolve at this type",
|
||||
|
||||
@@ -546,7 +546,13 @@ export function lowerSpreadArgsCall(
|
||||
callee: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (!isJsSourceFile(expr.getSourceFile())) return null;
|
||||
// TypeScript reaches this lane only for checked-dynamic variadic callees
|
||||
// (`(...args: unknown[]) => R`), whose ABI is the JS one.
|
||||
if (
|
||||
!isJsSourceFile(expr.getSourceFile()) &&
|
||||
!(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined)
|
||||
)
|
||||
return null;
|
||||
if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null;
|
||||
if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null;
|
||||
const calleeName =
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
|
||||
import { genResultRecord, jsOpenObjectType } from "../type-mapper.js";
|
||||
import { genResultRecord, isAnyArrayType, jsOpenObjectType } from "../type-mapper.js";
|
||||
import { dynFallbackType } from "./lowerer.js";
|
||||
import { builtinModuleFnOf } from "./surfaces.js";
|
||||
import { mixinFnShapeOf } from "./lower-mixins.js";
|
||||
@@ -286,10 +286,17 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
return { type: DYN, mode: "dynRest" };
|
||||
}
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]` in TypeScript: the binding is
|
||||
// the checked-dynamic array (how `unknown[]` maps everywhere), so the
|
||||
// ABI is the JS variadic dyn form above — the function type marks it
|
||||
// with `rest` and no typed slot (the type mapper agrees).
|
||||
const restTs = lowerer.typeOf(param.name);
|
||||
if (isAnyArrayType(restTs, lowerer.checker)) return { type: DYN, mode: "dynRest" };
|
||||
const type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name));
|
||||
if (type.kind === "dyn") return { type: DYN, mode: "dynRest" };
|
||||
// Tuple-typed rest params don't map to an array; generic rest is the
|
||||
// generic path's business. Anything non-array here is unmappable.
|
||||
if (type.kind !== "array") lowerer.badType(param.name, lowerer.typeOf(param.name));
|
||||
if (type.kind !== "array") lowerer.badType(param.name, restTs);
|
||||
return { type, mode: "rest" };
|
||||
}
|
||||
if (param.initializer) {
|
||||
@@ -861,13 +868,34 @@ export function lambdaSignature(
|
||||
ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
|
||||
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
|
||||
: null;
|
||||
const ctxRet = isAsyncLike && ctxRetRaw?.kind === "promise" ? ctxRetRaw.inner : ctxRetRaw;
|
||||
// An async lambda always produces a promise, so a slot returning a
|
||||
// promise-armed union (`Promise<R | void> | R | void`, `Promise<void>
|
||||
// | void`) is met through that promise arm: its payload is the ABI.
|
||||
const ctxPromiseArm =
|
||||
isAsyncLike && ctxRetRaw?.kind === "union"
|
||||
? lowerer.unions.get(ctxRetRaw.unionId)?.arms.filter((arm) => arm.kind === "promise")
|
||||
: undefined;
|
||||
const ctxRet =
|
||||
isAsyncLike && ctxRetRaw?.kind === "promise"
|
||||
? ctxRetRaw.inner
|
||||
: ctxPromiseArm?.length === 1 && ctxPromiseArm[0]!.kind === "promise"
|
||||
? ctxPromiseArm[0]!.inner
|
||||
: ctxRetRaw;
|
||||
// A TypeScript variadic lambda (`(...args) => handler(...args)`) in a
|
||||
// checked-dynamic variadic slot returning `unknown` adopts that result
|
||||
// too: rest closures have no function-value adapter, so the closure
|
||||
// must match the slot's ABI exactly.
|
||||
const dynVariadicSlot =
|
||||
ctxRet?.kind === "dyn" &&
|
||||
!isJsSourceFile(node.getSourceFile()) &&
|
||||
shapes.some((shape) => shape.mode === "dynRest");
|
||||
if (
|
||||
(ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") &&
|
||||
(innerRet.kind !== "void" ||
|
||||
(ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0))
|
||||
dynVariadicSlot ||
|
||||
((ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") &&
|
||||
(innerRet.kind !== "void" ||
|
||||
(ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0)))
|
||||
) {
|
||||
ret = isAsyncLike ? { kind: "promise", inner: ctxRet } : ctxRet;
|
||||
ret = isAsyncLike ? { kind: "promise", inner: ctxRet! } : ctxRet!;
|
||||
}
|
||||
// A VOID slot discards the callback's result (TS's void-returning
|
||||
// assignability rule; JS ignores the value), so an UNANNOTATED
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
|
||||
/** Wrap a zero-argument function whose array result needs an element
|
||||
* conversion. Each invocation creates a fresh converted array; the factory
|
||||
@@ -96,6 +97,11 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool
|
||||
if (dst.kind === "union") {
|
||||
if (src.kind === "union") return lowerer.unionRetagMappable(src.unionId, dst.unionId);
|
||||
if (src.kind === "void") return lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0;
|
||||
if (src.kind === "promise") {
|
||||
// A maybe-promise union's promise arm accepts payload widening.
|
||||
const mp = maybePromiseUnionOf(lowerer, dst);
|
||||
if (mp) return lowerer.promisePayloadWidens(src, mp.promise);
|
||||
}
|
||||
return !isUnitType(src) && lowerer.armTag(dst.unionId, src) >= 0;
|
||||
}
|
||||
if (src.kind === "union") {
|
||||
|
||||
@@ -1511,11 +1511,23 @@ function lowerBufferStaticValue(
|
||||
} else {
|
||||
list = lowerer.lowerExpr(argNode);
|
||||
if (!typeEquals(list.type, arrayOf(BYTES_U8))) {
|
||||
lowerer.noLowering(
|
||||
`Buffer.concat of '${lowerer.fmt(list.type)}' values`,
|
||||
argNode,
|
||||
"one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape",
|
||||
);
|
||||
// Any other list (JavaScript's untyped `const chunks = []`, or a
|
||||
// union of arrays): the runtime checks it as Node does (an Array of
|
||||
// Uint8Arrays, the ERR_INVALID_ARG_TYPE errors otherwise) before
|
||||
// concatenating.
|
||||
const value: IrExpr =
|
||||
list.type.kind === "dyn" ? list : { kind: "dynFrom", value: list, type: DYN, loc };
|
||||
if (args.length === 2) {
|
||||
const total = lowerer.lowerExprExpecting(args[1]!, F64);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.concatLenDyn",
|
||||
args: [value, total],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "libCall", fn: "buffer.concatDyn", args: [value], type: BYTES_U8, loc };
|
||||
}
|
||||
}
|
||||
if (args.length === 2) {
|
||||
|
||||
@@ -222,6 +222,7 @@ import {
|
||||
} from "./call-arguments.js";
|
||||
import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -5322,6 +5323,8 @@ export function lowerPromiseMethodCall(
|
||||
const resultOf = (value: IrExpr, returnsPromise = false): IrStmt[] => {
|
||||
if (value.type.kind === "promise")
|
||||
return [lowerer.adoptingReturn(value, R, (e) => lowerer.coerceInto(call, e, R))];
|
||||
const flattened = maybePromiseHandlerResult(lowerer, call, value, R);
|
||||
if (flattened) return [flattened];
|
||||
if (returnsPromise) {
|
||||
// A boxed handler's promise result adopts too: the then job's
|
||||
// turn after the call, then the await of the returned value.
|
||||
@@ -5672,7 +5675,10 @@ export function lowerPromiseMethodCall(
|
||||
// The handler's result: promise returns flatten exactly like
|
||||
// `return p` in any async body (the spec's thenable adoption);
|
||||
// everything else coerces into R.
|
||||
if (handlerCall.type.kind === "promise" && R.kind !== "promise") {
|
||||
const flattened = maybePromiseHandlerResult(lowerer, call, handlerCall, R);
|
||||
if (flattened) {
|
||||
body.push(flattened);
|
||||
} else if (handlerCall.type.kind === "promise" && R.kind !== "promise") {
|
||||
body.push(lowerer.adoptingReturn(handlerCall, R, (e) => lowerer.coerceInto(call, e, R)));
|
||||
} else if (R.kind === "void") {
|
||||
body.push({ kind: "exprStmt", expr: handlerCall, loc });
|
||||
@@ -9778,3 +9784,26 @@ export function lowerStaticNumberGlobal(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A then handler's promise-armed result (`.then(() => cached ?? load())`
|
||||
* typed `T | Promise<T>`, or a `Promise<void> | void` callback's result):
|
||||
* the lifted async body adopts the promise arm and fulfills with the
|
||||
* others, like `return u` in an async function. Null for other results. */
|
||||
function maybePromiseHandlerResult(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
value: IrExpr,
|
||||
R: IrType,
|
||||
): IrStmt | null {
|
||||
const mp = promiseArmedUnionOf(lowerer, value.type);
|
||||
if (!mp || promiseArmedUnionOf(lowerer, R)) return null;
|
||||
const stmt = maybePromiseReturnStmt(lowerer, value, mp, R, (e) => lowerer.coerceInto(call, e, R));
|
||||
if (stmt === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`then handlers returning '${lowerer.fmt(value.type)}' into '${lowerer.fmt(R)}' (an arm does not convert to the result)`,
|
||||
);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
@@ -1573,7 +1573,14 @@ function inferredEmptyCollectionFieldType(
|
||||
const mappedArgument = (argument: ts.Expression): IrType | null => {
|
||||
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(argument));
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (!mapped || mapped.kind === "dyn" || mapped.kind === "void" || isUnitType(mapped))
|
||||
// Inferred JS collection fields keep functions in checked slots.
|
||||
if (
|
||||
!mapped ||
|
||||
mapped.kind === "dyn" ||
|
||||
mapped.kind === "void" ||
|
||||
mapped.kind === "func" ||
|
||||
isUnitType(mapped)
|
||||
)
|
||||
return null;
|
||||
return mapped;
|
||||
};
|
||||
@@ -9246,7 +9253,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
"SC1090",
|
||||
expr,
|
||||
`Map values of type '${lowerer.checker.typeToString(targs[1])}' ` +
|
||||
`(Map values must be number, string, boolean, records, class instances, ` +
|
||||
`(Map values must be number, string, boolean, records, class instances, functions, ` +
|
||||
`typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ import {
|
||||
isCjsExportsIdentifier,
|
||||
lowerCjsExportsObjectValue,
|
||||
} from "./cjs-exports-object.js";
|
||||
import {
|
||||
lowerMaybePromiseAwait,
|
||||
maybePromiseInstanceOf,
|
||||
maybePromiseUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
|
||||
@@ -1788,6 +1793,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
}
|
||||
return { kind: "awaitUnionExpr", value, promiseTag, type, loc };
|
||||
}
|
||||
const maybePromise = maybePromiseUnionOf(lowerer, value.type);
|
||||
if (maybePromise) return lowerMaybePromiseAwait(lowerer, expr, value, maybePromise, loc);
|
||||
}
|
||||
if (value.type.kind !== "promise") {
|
||||
// A CHECKED-DYNAMIC operand (`await v` where v rode an untyped
|
||||
@@ -4737,12 +4744,61 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
|
||||
/** `typeof (u as any).then` / `if ((u as Promise<T>).then)` over a
|
||||
* maybe-promise union `u` (the cast is erasure in JS): a promise arm has
|
||||
* the prototype's `then` function, and no data arm is thenable (the type
|
||||
* mapper's rule), so the union tag answers. Null for every other shape. */
|
||||
function maybePromiseThenPresence(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
result: IrExpr,
|
||||
): IrExpr | null {
|
||||
let receiver: ts.Expression = access.expression;
|
||||
let cast = false;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression;
|
||||
else if (ts.isAsExpression(receiver) || ts.isTypeAssertion(receiver)) {
|
||||
receiver = receiver.expression;
|
||||
cast = true;
|
||||
} else break;
|
||||
}
|
||||
if (!cast) return null;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(receiver));
|
||||
if (!mapped || !maybePromiseUnionOf(lowerer, mapped)) return null;
|
||||
const value = lowerer.lowerExpr(receiver);
|
||||
const mp = maybePromiseUnionOf(lowerer, value.type);
|
||||
if (!mp) return null;
|
||||
const loc = locOf(access);
|
||||
const absent: IrExpr =
|
||||
result.type.kind === "bool"
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc }
|
||||
: { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: result,
|
||||
else_: absent,
|
||||
type: result.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function lowerPromiseThenPresence(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
result: IrExpr,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "then") return null;
|
||||
const maybePromise = maybePromiseThenPresence(lowerer, access, result);
|
||||
if (maybePromise !== null) return maybePromise;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const promiseType =
|
||||
mapped?.kind === "promise"
|
||||
@@ -5926,6 +5982,11 @@ export function lowerArrayLiteral(
|
||||
const elem = mapped.elem;
|
||||
const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem];
|
||||
if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN;
|
||||
// Inferred JavaScript literals mixing promises and plain values
|
||||
// (`[Promise.resolve(1), 2]`) keep the checked-dynamic array: their
|
||||
// consumers (Readable.from, Promise.all over iterators) inspect each
|
||||
// element at run time.
|
||||
if (maybePromiseUnionOf(lowerer, elem)) mapped = DYN;
|
||||
}
|
||||
if (
|
||||
!expected &&
|
||||
@@ -12694,6 +12755,38 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
};
|
||||
}
|
||||
}
|
||||
// `x instanceof Promise` over a promise-armed union (the maybe-promise
|
||||
// `T | Promise<T>` dispatch): a union tag test — the data arms are
|
||||
// never promises (scriptc promises are all native, and no data arm
|
||||
// can hold one). A promise-typed operand answers true after its
|
||||
// evaluation; a checked-dynamic one asks the runtime.
|
||||
if (
|
||||
ts.isIdentifier(expr.right) &&
|
||||
lowerer.isStdlibGlobal(expr.right, "Promise") &&
|
||||
!lowerer.caughtLocalOf(expr.left)
|
||||
) {
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
const tagTest = maybePromiseInstanceOf(lowerer, left, loc);
|
||||
if (tagTest) return tagTest;
|
||||
if (left.type.kind === "dyn")
|
||||
return { kind: "dynTest", test: "promise", value: left, type: BOOL, loc };
|
||||
if (left.type.kind === "promise") {
|
||||
const yes: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
if (isSafeToDiscard(left)) return yes;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: left, loc }],
|
||||
result: yes,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'instanceof Promise' on '${lowerer.fmt(left.type)}' values (only promises, promise-armed unions, and checked-dynamic values have a lowering)`,
|
||||
);
|
||||
}
|
||||
// `x instanceof RegExp` over a union with a regex arm (the
|
||||
// skip-utility `string | RegExp` dispatch): a union tag test — the
|
||||
// socket-narrowing shape; the checker types the branches and
|
||||
@@ -15932,7 +16025,11 @@ export function lowerPromiseRejectCall(
|
||||
);
|
||||
}
|
||||
const ctxT = lowerer.checker.getContextualType(call);
|
||||
const type = ctxT ? lowerer.mapTypeOf(ctxT) : null;
|
||||
const contextual = ctxT ? lowerer.mapTypeOf(ctxT) : null;
|
||||
// A maybe-promise context (`return Promise.reject(e)` from a function
|
||||
// returning `T | Promise<T>`) names its one promise arm.
|
||||
const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null;
|
||||
const type = maybePromise ? maybePromise.promise : contextual;
|
||||
const reasonNode = call.arguments[0]!;
|
||||
// A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode
|
||||
// an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree
|
||||
|
||||
@@ -345,9 +345,12 @@ export function lowerLambda(
|
||||
} else {
|
||||
const value = lowerer.lowerExpr(bodyExpr);
|
||||
// `async () => p` with a void result still resolves the
|
||||
// function's promise with p (its settlement, rejections included).
|
||||
body =
|
||||
value.kind === "unitLit"
|
||||
// function's promise with p (its settlement, rejections included);
|
||||
// a maybe-promise value adopts only its promise arm.
|
||||
const flattened = isAsync ? lowerer.maybePromiseReturn(bodyExpr, value, VOID) : null;
|
||||
body = flattened
|
||||
? [flattened]
|
||||
: value.kind === "unitLit"
|
||||
? []
|
||||
: isAsync && value.type.kind === "promise"
|
||||
? [lowerer.adoptingReturn(value, VOID, (e) => e)]
|
||||
@@ -367,8 +370,13 @@ export function lowerLambda(
|
||||
if (isAsync && value.type.kind === "promise" && bodyReturn.kind !== "promise") {
|
||||
value = lowerer.returnedPromiseAwait(value);
|
||||
}
|
||||
body =
|
||||
value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!))
|
||||
const flattened =
|
||||
isAsync && !fnCtx.inferReturn
|
||||
? lowerer.maybePromiseReturn(bodyExpr, value, bodyReturn)
|
||||
: null;
|
||||
body = flattened
|
||||
? [flattened]
|
||||
: value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!))
|
||||
? [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }]
|
||||
: [
|
||||
{
|
||||
|
||||
@@ -609,6 +609,11 @@ import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js";
|
||||
import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { lowerTernary } from "./lower-exprs.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
maybePromiseUnionOf,
|
||||
promiseArmedUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
|
||||
/** Entry function name. '%' cannot appear in a TS identifier, so a user
|
||||
* function can never collide with it (mangling is injective per prefix). */
|
||||
@@ -1317,17 +1322,20 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
|
||||
if (args.length === 2) {
|
||||
const key = lowerer.mapTypeOf(args[0]!);
|
||||
const value = lowerer.mapTypeOf(args[1]!);
|
||||
// Inferred JS function types keep the checked-dynamic slot (their
|
||||
// closures carry JS arity, not a typed closure ABI).
|
||||
return {
|
||||
kind: "map",
|
||||
key:
|
||||
key &&
|
||||
key.kind !== "func" &&
|
||||
isSupportedMapKey(
|
||||
key,
|
||||
key.kind === "union" ? lowerer.unions.get(key.unionId)?.arms : undefined,
|
||||
)
|
||||
? key
|
||||
: DYN,
|
||||
value: value && isSupportedMapValue(value) ? value : DYN,
|
||||
value: value && value.kind !== "func" && isSupportedMapValue(value) ? value : DYN,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8317,6 +8325,18 @@ export class Lowerer {
|
||||
// undefined/null literals store the dyn unit values. Types outside the
|
||||
// dyn's domain (bytes, classes, Maps, ...) fall through to
|
||||
// requireExactShape's SC1101 fence.
|
||||
// A promise entering a maybe-promise union whose promise arm has a
|
||||
// wider payload (`Promise<R>` into `Promise<R | void> | R | void`)
|
||||
// takes the payload widening below first, then wraps into that arm.
|
||||
if (expected.kind === "union" && expr.type.kind === "promise") {
|
||||
const mp = maybePromiseUnionOf(this, expected);
|
||||
if (
|
||||
mp &&
|
||||
!typeEquals(mp.promise, expr.type) &&
|
||||
this.promisePayloadWidens(expr.type, mp.promise)
|
||||
)
|
||||
expr = this.coerceToExpected(expr, mp.promise);
|
||||
}
|
||||
// Promise payloads may widen at a checked return boundary, including
|
||||
// arrays inferred as tuple results by Promise.all. Keep both payload
|
||||
// ABIs explicit: box settlement, then validate the destination payload.
|
||||
@@ -9515,6 +9535,25 @@ export class Lowerer {
|
||||
return type;
|
||||
}
|
||||
|
||||
/** Whether a promise of `from` converts to a promise of `to` through the
|
||||
* checked payload widening (coerceToExpected's boxed-settlement rule,
|
||||
* which keeps the promise's identity and settlement turn). */
|
||||
promisePayloadWidens(
|
||||
from: IrType & { kind: "promise" },
|
||||
to: IrType & { kind: "promise" },
|
||||
): boolean {
|
||||
if (typeEquals(from, to)) return true;
|
||||
if (to.inner.kind === "void") return from.inner.kind !== "void";
|
||||
return (
|
||||
this.dynConvertible(from) &&
|
||||
canDynCheckTo(
|
||||
to,
|
||||
(id) => this.shapes.get(id),
|
||||
(id) => this.unions.get(id),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a static type converts to a dyn value (the dynFrom
|
||||
* walker's domain): JSON-safe, bytes<u8> (Uint8Array/Buffer — the checked-dynamic tree's
|
||||
* bytes kind, payload copied; stdin chunks into unknown-typed helpers),
|
||||
@@ -9554,16 +9593,26 @@ export class Lowerer {
|
||||
// storage directly, just like arguments and variable initializers.
|
||||
// Contextual types (such as Proxy descriptors' symbol-valued fields)
|
||||
// must not force an intermediate record with incompatible field slots.
|
||||
if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected);
|
||||
return this.asyncReturnValueOf(node, this.lowerExpr(node), expected);
|
||||
}
|
||||
|
||||
/** A fresh JS object literal returned into a checked-native (dyn) result,
|
||||
* which lowers straight into that storage. */
|
||||
private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean {
|
||||
let literal = node;
|
||||
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
|
||||
if (
|
||||
return (
|
||||
expected.kind === "dyn" &&
|
||||
isJsSourceFile(node.getSourceFile()) &&
|
||||
ts.isObjectLiteralExpression(literal)
|
||||
) {
|
||||
return this.lowerExprExpecting(node, expected);
|
||||
}
|
||||
const e = this.lowerExpr(node);
|
||||
);
|
||||
}
|
||||
|
||||
/** The value of `return <e>` (already lowered untyped) in an async or
|
||||
* void context: a promise flattens through the returned-promise await,
|
||||
* everything else converts into the declared result. */
|
||||
private asyncReturnValueOf(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr | null {
|
||||
if (expected.kind === "void" && e.kind === "unitLit") return null;
|
||||
if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") {
|
||||
const awaited = this.returnedPromiseAwait(e);
|
||||
@@ -9629,10 +9678,32 @@ export class Lowerer {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** `return u` of a promise-armed union (maybe-promise, or promise-or-
|
||||
* unit) in an async function: adopt the promise arm, fulfill with the
|
||||
* others (maybe-promise.ts). Null when the return is not that shape. */
|
||||
maybePromiseReturn(node: ts.Expression, e: IrExpr, expected: IrType): IrStmt | null {
|
||||
if (!this.adoptsReturnedPromise()) return null;
|
||||
const mp = promiseArmedUnionOf(this, e.type);
|
||||
if (!mp || promiseArmedUnionOf(this, expected)) return null;
|
||||
const stmt = maybePromiseReturnStmt(this, e, mp, expected, (x) =>
|
||||
this.coerceInto(node, x, expected),
|
||||
);
|
||||
if (stmt === null) {
|
||||
this.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`returning '${this.fmt(e.type)}' from an async function whose result is '${this.fmt(expected)}' (an arm does not convert to the result)`,
|
||||
);
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
private lowerReturnOf(node: ts.Expression, loc: SrcLoc): IrStmt {
|
||||
const expected = this.ctx.returnType;
|
||||
if (expected.kind === "void") {
|
||||
let e = this.lowerExpr(node);
|
||||
const flattened = this.maybePromiseReturn(node, e, expected);
|
||||
if (flattened) return flattened;
|
||||
if (this.ctx.isAsync && e.type.kind === "promise") {
|
||||
if (this.adoptsReturnedPromise()) return this.adoptingReturn(e, VOID, (x) => x);
|
||||
e = this.returnedPromiseAwait(e);
|
||||
@@ -9648,6 +9719,15 @@ export class Lowerer {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) {
|
||||
// The async return lowers its operand untyped first (a promise
|
||||
// adopts before its payload converts); a maybe-promise union needs
|
||||
// the statement form, which branches between adoption and fulfillment.
|
||||
const e = this.lowerExpr(node);
|
||||
const flattened = this.maybePromiseReturn(node, e, expected);
|
||||
if (flattened) return flattened;
|
||||
return { kind: "return", value: this.asyncReturnValueOf(node, e, expected), loc };
|
||||
}
|
||||
return { kind: "return", value: this.lowerReturnValue(node), loc };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { BOOL, isUnitType, typeEquals, VOID } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** A MAYBE-PROMISE union (`T | Promise<T>`, `Promise<R | void> | R |
|
||||
* void`): one promise arm beside at least one data arm. The type mapper
|
||||
* mints these only when no data arm is thenable, so the union tag decides
|
||||
* exactly what JS's await decides: the promise arm settles, every other
|
||||
* arm is a non-thenable that answers itself. Promise-or-unit unions keep
|
||||
* their own lowerings (awaitUnionExpr). */
|
||||
export interface MaybePromiseUnion {
|
||||
def: IrUnionDef;
|
||||
promiseTag: number;
|
||||
promise: IrType & { kind: "promise" };
|
||||
}
|
||||
|
||||
export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
const armed = promiseArmedUnionOf(lowerer, t);
|
||||
if (!armed || armed.def.arms.every((a, i) => i === armed.promiseTag || isUnitType(a)))
|
||||
return null;
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** Any union with exactly one promise arm: a maybe-promise union or a
|
||||
* promise-or-unit union (`Promise<void> | void`). An async `return`
|
||||
* resolves both the same way: adopt the promise arm, fulfill otherwise. */
|
||||
export function promiseArmedUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
if (t.kind !== "union") return null;
|
||||
const def = lowerer.unions.get(t.unionId);
|
||||
if (!def) return null;
|
||||
const tags = def.arms.flatMap((a, i) => (a.kind === "promise" ? [i] : []));
|
||||
if (tags.length !== 1) return null;
|
||||
const promiseTag = tags[0]!;
|
||||
const promise = def.arms[promiseTag]!;
|
||||
if (promise.kind !== "promise") return null;
|
||||
return { def, promiseTag, promise };
|
||||
}
|
||||
|
||||
function unionTagTest(mp: MaybePromiseUnion, value: IrExpr, tag: number, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "unionIsTag",
|
||||
unionId: mp.def.id,
|
||||
tag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The value of `ref` (a repeatable read of the maybe-promise union) as
|
||||
* `target`, given that it does not hold the promise arm: each remaining
|
||||
* arm extracts and converts. Null when some arm cannot convert. */
|
||||
function nonPromiseArmsAs(
|
||||
lowerer: Lowerer,
|
||||
mp: MaybePromiseUnion,
|
||||
ref: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const pieces: { tag: number; value: IrExpr }[] = [];
|
||||
for (let tag = 0; tag < mp.def.arms.length; tag++) {
|
||||
if (tag === mp.promiseTag) continue;
|
||||
const arm = mp.def.arms[tag]!;
|
||||
const source: IrExpr =
|
||||
arm.kind === "undefinedT" || arm.kind === "nullT"
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: arm.kind === "undefinedT" ? "undefined" : "null",
|
||||
type: arm,
|
||||
loc,
|
||||
}
|
||||
: { kind: "unionNarrow", unionId: mp.def.id, tag, value: ref, type: arm, loc };
|
||||
const value = lowerer.coerceToExpected(source, target);
|
||||
if (!typeEquals(value.type, target)) return null;
|
||||
pieces.push({ tag, value });
|
||||
}
|
||||
let result: IrExpr | null = null;
|
||||
for (let i = pieces.length - 1; i >= 0; i--) {
|
||||
const piece = pieces[i]!;
|
||||
result =
|
||||
result === null
|
||||
? piece.value
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, piece.tag, loc),
|
||||
then: piece.value,
|
||||
else_: result,
|
||||
type: target,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function hopStmt(loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `await u` over a maybe-promise union. JS's await resolves the operand:
|
||||
* a native promise is awaited directly (resumption one turn after it
|
||||
* settles; a rejection throws here), and any other non-thenable value
|
||||
* resumes after exactly one microtask turn with the value itself. The
|
||||
* result type is the checker's type of the await expression (`Awaited`
|
||||
* of the union), which holds the promise's settled type and every data
|
||||
* arm. */
|
||||
export function lowerMaybePromiseAwait(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AwaitExpression,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (!resultT || resultT.kind === "void" || resultT.kind === "dyn" || isUnitType(resultT)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`awaiting '${lowerer.fmt(value.type)}' (the awaited result type has no compiled representation)`,
|
||||
);
|
||||
}
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const inner = mp.promise.inner;
|
||||
const awaited: IrExpr = {
|
||||
kind: "awaitExpr",
|
||||
value: {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
},
|
||||
type: inner,
|
||||
loc,
|
||||
};
|
||||
let settled: IrExpr | null;
|
||||
if (inner.kind === "void") {
|
||||
const undef = lowerer.wrappedUndefined(resultT, loc);
|
||||
settled =
|
||||
undef === null
|
||||
? null
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: awaited, loc }],
|
||||
result: undef,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
settled = lowerer.coerceToExpected(awaited, resultT);
|
||||
if (!typeEquals(settled.type, resultT)) settled = null;
|
||||
}
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT, loc);
|
||||
if (settled === null || rest === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`awaiting '${lowerer.fmt(value.type)}' (its arms do not all convert to the awaited result '${lowerer.fmt(resultT)}')`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: settled,
|
||||
else_: { kind: "seqExpr", stmts: [hopStmt(loc)], result: rest, type: resultT, loc },
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `return u` of a promise-armed union from an async function (or a then
|
||||
* handler's lifted body) whose settled type is `R`: the promise arm is
|
||||
* adopted exactly like `return p` (the spec's thenable resolution, with
|
||||
* its then-job turn), and the other arms fulfill directly. Null when an
|
||||
* arm cannot convert to R. */
|
||||
export function maybePromiseReturnStmt(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
R: IrType,
|
||||
coerce: (awaited: IrExpr) => IrExpr,
|
||||
): IrStmt | null {
|
||||
const loc = value.loc;
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const promise: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
};
|
||||
let otherwise: IrStmt[];
|
||||
if (R.kind === "void") {
|
||||
otherwise = [{ kind: "return", value: null, loc }];
|
||||
} else {
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, R, loc);
|
||||
if (rest === null) return null;
|
||||
otherwise = [{ kind: "return", value: rest, loc }];
|
||||
}
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: local.id, init: value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: [lowerer.adoptingReturn(promise, R, coerce)],
|
||||
else_: otherwise,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Promise.resolve(u)` over a maybe-promise union: a promise argument is
|
||||
* returned as is (every scriptc promise is native, so the spec's
|
||||
* constructor check answers identity), any other value fulfills a fresh
|
||||
* promise. Null when the promise arm is not already the result type (no
|
||||
* identity-preserving payload adapter exists) or a data arm cannot convert. */
|
||||
export function maybePromiseResolve(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
mp: MaybePromiseUnion,
|
||||
resultT: IrType & { kind: "promise" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const local = lowerer.declareHiddenLocal("%maybePromise", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
let promise: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: mp.def.id,
|
||||
tag: mp.promiseTag,
|
||||
value: ref,
|
||||
type: mp.promise,
|
||||
loc,
|
||||
};
|
||||
if (!typeEquals(mp.promise, resultT)) {
|
||||
if (resultT.inner.kind !== "void") return null;
|
||||
promise = { kind: "promiseVoidWiden", value: promise, type: resultT, loc };
|
||||
}
|
||||
let fresh: IrExpr;
|
||||
if (resultT.inner.kind === "void") {
|
||||
fresh = { kind: "intrinsic", name: "promise.resolve", args: [], type: resultT, loc };
|
||||
} else {
|
||||
const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT.inner, loc);
|
||||
if (rest === null) return null;
|
||||
fresh = { kind: "intrinsic", name: "promise.resolve", args: [rest], type: resultT, loc };
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: unionTagTest(mp, ref, mp.promiseTag, loc),
|
||||
then: promise,
|
||||
else_: fresh,
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `u instanceof Promise` over a maybe-promise (or promise-or-unit) union:
|
||||
* the union tag test. */
|
||||
export function maybePromiseInstanceOf(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const def = lowerer.unions.get(value.type.unionId);
|
||||
const tag = def ? def.arms.findIndex((a) => a.kind === "promise") : -1;
|
||||
if (!def || tag < 0) return null;
|
||||
return { kind: "unionIsTag", unionId: def.id, tag, negated: false, value, type: BOOL, loc };
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
SYMBOL_T,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
maybePromiseUnionArmsOk,
|
||||
unionContainerArmsOk,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
@@ -4195,7 +4196,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
a.kind === "dyn" ||
|
||||
a.kind === "generator",
|
||||
) ||
|
||||
!unionContainerArmsOk(arms)
|
||||
!maybePromiseUnionOk(parts, arms, ctx)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -4253,6 +4254,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
let dynRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
@@ -4315,6 +4317,21 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]` declared in program source:
|
||||
// the checked-dynamic variadic ABI (no typed slot; the callee's hidden
|
||||
// trailing dyn array receives every surplus argument, as for JS rest
|
||||
// params). Declaration files keep their existing paths: library
|
||||
// callback slots such as String#replace's `(substring, ...args: any[])`
|
||||
// have intrinsic lowerings that inspect the unmapped type.
|
||||
if (
|
||||
rest &&
|
||||
decl !== undefined &&
|
||||
!decl.getSourceFile().isDeclarationFile &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
continue;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
@@ -4375,7 +4392,9 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
: dynRest
|
||||
? { kind: "func", params, ret, rest: true }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Retain a literal discriminator before widening erases its values. A
|
||||
@@ -6279,9 +6298,49 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** `any[]` (or `readonly any[]`): an array type whose element is `any`. */
|
||||
export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
if (!checker.isArrayType(t)) return false;
|
||||
const elem = checker.getTypeArguments(t as ts.TypeReference)[0];
|
||||
return elem !== undefined && (elem.flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
/** The maybe-promise union rule (mapTypeInner's union rule; IR
|
||||
* maybePromiseUnionArmsOk): at most one promise arm, and when it has data
|
||||
* siblings none of them may be thenable — `await` and async `return`
|
||||
* dispatch on the union tag, so a data arm whose type declares `then`
|
||||
* would skip the thenable adoption JS performs. */
|
||||
function maybePromiseUnionOk(
|
||||
parts: readonly ts.Type[],
|
||||
arms: IrType[],
|
||||
ctx: TypeMapperCtx,
|
||||
): boolean {
|
||||
if (unionContainerArmsOk(arms)) return true;
|
||||
if (!maybePromiseUnionArmsOk(arms)) return false;
|
||||
// A function beside a promise (`Promise<T> | (() => Promise<T>)`, the
|
||||
// assert.rejects argument) keeps its existing representation: callers
|
||||
// dispatch on callability, not on a then-able promise.
|
||||
if (arms.some((arm) => arm.kind === "func")) return false;
|
||||
return maybePromiseThenableArm(parts, ctx) === null;
|
||||
}
|
||||
|
||||
/** The first non-promise constituent of a promise-armed union whose type
|
||||
* has a `then` member (a thenable data arm), or null. */
|
||||
function maybePromiseThenableArm(parts: readonly ts.Type[], ctx: TypeMapperCtx): ts.Type | null {
|
||||
const { checker } = ctx;
|
||||
for (const part of parts) {
|
||||
if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) continue;
|
||||
if (mapType(part, ctx)?.kind === "promise") continue;
|
||||
// Primitive arms answer through their wrapper prototypes, none of
|
||||
// which has `then`; object arms answer through their own members.
|
||||
if (checker.getPropertyOfType(part, "then")) return part;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Arm kinds with no home in a compiled union (mapTypeInner's union rule):
|
||||
* no runtime narrowing test exists against sibling data arms. */
|
||||
function armHasUnionHome(arm: IrType, siblingCount: number): boolean {
|
||||
function armHasUnionHome(arm: IrType): boolean {
|
||||
switch (arm.kind) {
|
||||
case "void":
|
||||
case "union":
|
||||
@@ -6290,9 +6349,6 @@ function armHasUnionHome(arm: IrType, siblingCount: number): boolean {
|
||||
case "generator":
|
||||
case "dyn":
|
||||
return false;
|
||||
// Promise data siblings still lack a supported narrowing contract.
|
||||
case "promise":
|
||||
return siblingCount === 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -6417,10 +6473,24 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx):
|
||||
if (!mapped) {
|
||||
return `the union shape is supported, but its arm '${text(part)}' does not compile`;
|
||||
}
|
||||
if (!armHasUnionHome(mapped, dataArms.length - 1)) {
|
||||
if (!armHasUnionHome(mapped)) {
|
||||
return `the union shape is supported, but '${text(part)}' arms have no home in a compiled union yet (no runtime narrowing test exists against sibling arms)`;
|
||||
}
|
||||
}
|
||||
const promiseArms = mappedArms.filter(({ mapped }) => mapped?.kind === "promise");
|
||||
if (promiseArms.length > 1) {
|
||||
return `the union shape is supported, but it has several promise arms ('${text(promiseArms[0]!.p)}' and '${text(promiseArms[1]!.p)}'), and a compiled union holds at most one (the union tag is the 'instanceof Promise' test)`;
|
||||
}
|
||||
if (promiseArms.length === 1 && dataArms.length > 1) {
|
||||
const callable = mappedArms.find(({ mapped }) => mapped?.kind === "func");
|
||||
if (callable) {
|
||||
return `the union shape is supported, but its function arm '${text(callable.p)}' sits beside a promise arm, and a maybe-promise union holds only data arms beside its promise`;
|
||||
}
|
||||
const thenable = maybePromiseThenableArm(parts, ctx);
|
||||
if (thenable) {
|
||||
return `the union shape is supported, but its arm '${text(thenable)}' declares 'then' beside a promise arm, so 'await' would have to adopt it as a thenable (a maybe-promise union's data arms must not be thenable)`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -694,6 +694,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"crypto.scryptCb",
|
||||
"buffer.concat",
|
||||
"buffer.concatLen",
|
||||
"buffer.concatDyn",
|
||||
"buffer.concatLenDyn",
|
||||
"buffer.fromDyn",
|
||||
"buffer.fromStrChecked",
|
||||
"buffer.alloc",
|
||||
|
||||
@@ -2644,6 +2644,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* opaque reference inputs retain explicit runtime refusals. */
|
||||
"buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 },
|
||||
"buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 },
|
||||
"buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 },
|
||||
/** The pooled Buffer factories (Node's allocate/fromStringFast/
|
||||
* fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays,
|
||||
* number[] and checked values, and concat return views into the
|
||||
@@ -2670,6 +2671,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* zero-padded to the total. THROWS Node's 'length' RangeError on a
|
||||
* negative/non-integer total (may-throw seed). */
|
||||
"buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 },
|
||||
"buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 },
|
||||
// The checked-dynamic compare/equals validators (Node's argument
|
||||
// ladders over dyn-boxed invalid-input probes).
|
||||
/** The checked-dynamic compare/equals validators (scr_bytes_io.c) —
|
||||
|
||||
@@ -159,6 +159,17 @@ describe("checked records with opaque payloads", () => {
|
||||
isJsonStringifyDynamicType(arrayOf({ kind: "union", unionId: "optional" }), record, union),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("class instances stringify through the runtime JSON traversal only", () => {
|
||||
const point: IrType = { kind: "object", className: "Point" };
|
||||
const failure: IrType = { kind: "object", className: "%Error" };
|
||||
expect(isJsonStringifyDynamicType(point, record, union)).toBe(true);
|
||||
expect(isJsonStringifyDynamicType(arrayOf(point), record, union)).toBe(true);
|
||||
expect(isJsonStringifyDynamicType(failure, record, union)).toBe(false);
|
||||
expect(isJsonStringifySafeType(point, record, union)).toBe(false);
|
||||
expect(isJsonSafeType(point, record, union)).toBe(false);
|
||||
expect(canDynCheckTo(arrayOf(point), record, union)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("native typed-array boundaries", () => {
|
||||
|
||||
@@ -667,7 +667,12 @@ export function isIdentityCollectionKey(t: IrType): boolean {
|
||||
t.kind === "object" ||
|
||||
t.kind === "array" ||
|
||||
t.kind === "netServer" ||
|
||||
t.kind === "symbol"
|
||||
t.kind === "symbol" ||
|
||||
// A function value is one closure for its whole life (top-level
|
||||
// functions intern one immortal closure, inner closures allocate once
|
||||
// per evaluation), so pointer identity is JS function identity.
|
||||
// Closures are cycle-headered and traced like records.
|
||||
t.kind === "func"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -713,8 +718,8 @@ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean {
|
||||
return isSupportedMapKey(t, unionArms);
|
||||
}
|
||||
|
||||
/** The Map VALUE fence: scalars, supported native references and checked
|
||||
* values. Functions require checked-value boxing.
|
||||
/** The Map VALUE fence: scalars, supported native references, closures
|
||||
* and checked values.
|
||||
* Record/object/union values can point back at the map holding them, which
|
||||
* is exactly why ref-valued maps are cycle-capable (see the backend's
|
||||
* cycle analysis and docs/memory.md). Shared frontend/validator. */
|
||||
@@ -768,6 +773,12 @@ export function isSupportedMapValue(t: IrType): boolean {
|
||||
// a regex holds no references), map form.
|
||||
case "regex":
|
||||
return true;
|
||||
// A closure (Map<string, (x: T) => U> — the factory/handler registry):
|
||||
// the index-signature overflow story (scr_closure adapters; closures
|
||||
// are cycle-headered and traced, so a handler capturing its own
|
||||
// registry collects).
|
||||
case "func":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -783,7 +794,7 @@ export function isSupportedMapValue(t: IrType): boolean {
|
||||
* Nested index-signature RECORDS ride the record kind like any other.
|
||||
* Shared frontend (mapType) / validator. */
|
||||
export function isSupportedIndexValue(t: IrType): boolean {
|
||||
return t.kind === "dyn" || t.kind === "func" || isSupportedMapValue(t);
|
||||
return t.kind === "dyn" || isSupportedMapValue(t);
|
||||
}
|
||||
|
||||
export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
@@ -791,14 +802,25 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
}
|
||||
|
||||
/** Maps and sets use tagged union storage and retain their native identity.
|
||||
* Promises still require nullable-only unions until their data-arm narrowing
|
||||
* and await boundary have a matching contract. */
|
||||
* Joined unions (inferred joins of member reads and array elements) keep
|
||||
* promises nullable-only; declared maybe-promise unions are admitted by
|
||||
* maybePromiseUnionArmsOk. */
|
||||
export function unionContainerArmsOk(arms: IrType[]): boolean {
|
||||
return arms.every(
|
||||
(a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)),
|
||||
);
|
||||
}
|
||||
|
||||
/** The MAYBE-PROMISE union (`T | Promise<T>`, `Promise<R | void> | R |
|
||||
* void`): at most one promise arm, beside unit or data arms. The union tag
|
||||
* is the `instanceof Promise` test, and `await` / async `return` branch on
|
||||
* it (the promise arm awaits or adopts, the other arms take the
|
||||
* non-thenable path). Data arms must not be thenable; the type mapper
|
||||
* checks that against the checker types before minting such a union. */
|
||||
export function maybePromiseUnionArmsOk(arms: IrType[]): boolean {
|
||||
return arms.filter((a) => a.kind === "promise").length <= 1;
|
||||
}
|
||||
|
||||
/** Canonical, injective text form of an IrType — the building block of
|
||||
* shape/union identity keys, generic-function instantiation keys, and the
|
||||
* backend's per-type helper interning (jsonStringify/dynCheck walkers).
|
||||
@@ -3730,9 +3752,10 @@ export function isJsonStringifySafeType(
|
||||
return isJsonSafeAt(t, getRecord, getUnion, true, false, new Set());
|
||||
}
|
||||
|
||||
/** A JSON-shaped container with unknown payloads needs the runtime JSON
|
||||
* traversal. Its declared structure can be boxed, but payloads may carry
|
||||
* omitted values, toJSON methods, or cycles. This predicate does not widen
|
||||
/** A JSON-shaped container with unknown payloads, or a class instance at
|
||||
* any position, needs the runtime JSON traversal. Its declared structure can
|
||||
* be boxed, but payloads may carry omitted values, toJSON methods, added
|
||||
* properties, or cycles. This predicate does not widen
|
||||
* the type-directed serializer, checked-value boundary, or root-undefined
|
||||
* contract; callers must select the checked runtime traversal explicitly. */
|
||||
export function isJsonStringifyDynamicType(
|
||||
@@ -3741,7 +3764,7 @@ export function isJsonStringifyDynamicType(
|
||||
getUnion: (unionId: string) => IrUnionDef | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
(t.kind === "record" || t.kind === "array" || t.kind === "union") &&
|
||||
(t.kind === "record" || t.kind === "array" || t.kind === "union" || isDynTypedRefType(t)) &&
|
||||
isJsonSafeAt(t, getRecord, getUnion, true, false, new Set(), true) &&
|
||||
!isJsonStringifySafeType(t, getRecord, getUnion)
|
||||
);
|
||||
@@ -3851,8 +3874,12 @@ function isJsonSafeAt(
|
||||
return nativeFields && !stringify;
|
||||
case "bytes":
|
||||
return nativeFields;
|
||||
case "func":
|
||||
// Class instances stringify through the runtime JSON traversal, which
|
||||
// reads their own enumerable properties and calls toJSON.
|
||||
case "object":
|
||||
if (nativeFields && stringify && isDynTypedRefType(t)) return true;
|
||||
return false;
|
||||
case "func":
|
||||
// Class values stringify as "{}" husks in Node (own enumerable statics
|
||||
// aside — not representable type-directedly); rejected like Maps.
|
||||
case "classval":
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { resolveSelfCaptures } from "./self-captures.js";
|
||||
|
||||
const loc = { file: "self.ts", start: 0, end: 0 };
|
||||
const FN = funcOf([], F64);
|
||||
const ref = (localId: string, type = FN): IrExpr => ({ kind: "varRef", localId, type, loc });
|
||||
const call = (localId: string): IrExpr => ({
|
||||
kind: "callValue",
|
||||
callee: ref(localId),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const self: IrExpr = { kind: "selfRef", type: FN, loc };
|
||||
const closure = (fnName: string, captures: string[], extra: object = {}): IrExpr => ({
|
||||
kind: "closure",
|
||||
fnName,
|
||||
captures,
|
||||
type: FN,
|
||||
loc,
|
||||
...extra,
|
||||
});
|
||||
const decl = (localId: string, init: IrExpr | null): IrStmt => ({
|
||||
kind: "varDecl",
|
||||
localId,
|
||||
init,
|
||||
loc,
|
||||
});
|
||||
const assign = (localId: string, value: IrExpr, initializes = false): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId,
|
||||
value,
|
||||
...(initializes ? { initializes: true as const } : {}),
|
||||
loc,
|
||||
});
|
||||
const ret = (e: IrExpr): IrStmt => ({ kind: "return", value: e, loc });
|
||||
const use = (e: IrExpr): IrStmt => ({ kind: "exprStmt", expr: e, loc });
|
||||
const local = (id: string, extra: Partial<IrLocal> = {}): IrLocal => ({
|
||||
id,
|
||||
name: id.split(".")[0]!,
|
||||
type: FN,
|
||||
mutable: false,
|
||||
boxed: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
function outer(body: IrStmt[], locals: IrLocal[]): IrFunction {
|
||||
return { name: "outer", params: [], returnType: F64, locals, body, loc };
|
||||
}
|
||||
function lifted(name: string, body: IrStmt[], captured: IrLocal[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [],
|
||||
returnType: F64,
|
||||
captures: captured.map((l) => ({ localId: l.id, name: l.name, type: l.type })),
|
||||
locals: captured,
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
function run(...functions: IrFunction[]): Map<string, IrFunction> {
|
||||
const mod = { functions } as unknown as IrModule;
|
||||
return new Map(resolveSelfCaptures(mod).functions.map((fn) => [fn.name, fn]));
|
||||
}
|
||||
|
||||
test("a closure stored in the dead-zone binding it captures reads itself", () => {
|
||||
const fns = run(
|
||||
outer(
|
||||
[decl("f.0", null), assign("f.0", closure("walk", ["f.0"]), true), ret(call("f.0"))],
|
||||
[local("f.0", { mutable: true, tdz: true })],
|
||||
),
|
||||
lifted("walk", [ret(call("f.0"))], [local("f.0", { tdz: true })]),
|
||||
);
|
||||
const walk = fns.get("walk")!;
|
||||
expect(walk.captures).toEqual([]);
|
||||
expect(walk.locals).toEqual([]);
|
||||
expect(walk.body).toEqual([ret({ kind: "callValue", callee: self, args: [], type: F64, loc })]);
|
||||
const origin = fns.get("outer")!;
|
||||
expect(origin.body[1]).toEqual(assign("f.0", closure("walk", []), true));
|
||||
// The dead-zone box still represents the binding's uninitialized state.
|
||||
expect(origin.locals[0]).toMatchObject({ boxed: true, tdz: true });
|
||||
});
|
||||
|
||||
test("a binding that only held the closure loses its box", () => {
|
||||
const n = local("n.0", { type: F64 });
|
||||
const fns = run(
|
||||
outer(
|
||||
[
|
||||
decl("n.0", { kind: "numLit", value: 1, type: F64, loc }),
|
||||
decl("f.0", closure("walk", ["n.0", "f.0"])),
|
||||
ret(call("f.0")),
|
||||
],
|
||||
[n, local("f.0")],
|
||||
),
|
||||
lifted("walk", [use(ref("n.0", F64)), ret(call("f.0"))], [n, local("f.0")]),
|
||||
);
|
||||
expect(fns.get("walk")!.captures!.map((c) => c.localId)).toEqual(["n.0"]);
|
||||
const origin = fns.get("outer")!;
|
||||
expect(origin.body[1]).toEqual(decl("f.0", closure("walk", ["n.0"])));
|
||||
expect(origin.locals.find((l) => l.id === "f.0")!.boxed).toBeUndefined();
|
||||
expect(origin.locals.find((l) => l.id === "n.0")!.boxed).toBe(true);
|
||||
});
|
||||
|
||||
test("hoisted declarations qualify; later writes and second sites do not", () => {
|
||||
const body = (extra: IrStmt[]): IrStmt[] => [
|
||||
decl("f.0", null),
|
||||
assign("f.0", closure("walk", ["f.0"])),
|
||||
...extra,
|
||||
ret(call("f.0")),
|
||||
];
|
||||
const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]);
|
||||
const hoisted = run(outer(body([]), [local("f.0", { mutable: true })]), walk);
|
||||
expect(hoisted.get("walk")!.captures).toEqual([]);
|
||||
expect(hoisted.get("outer")!.locals[0]!.boxed).toBeUndefined();
|
||||
|
||||
const rewritten = run(
|
||||
outer(body([assign("f.0", closure("other", []))]), [local("f.0", { mutable: true })]),
|
||||
walk,
|
||||
lifted("other", [ret({ kind: "numLit", value: 0, type: F64, loc })], []),
|
||||
);
|
||||
expect(rewritten.get("walk")).toBe(walk);
|
||||
|
||||
// The same body stored somewhere else: its environment must keep the box.
|
||||
const elsewhere = run(
|
||||
outer(
|
||||
[...body([]).slice(0, 2), use(closure("walk", ["f.0"])), ret(call("f.0"))],
|
||||
[local("f.0", { mutable: true })],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(elsewhere.get("walk")).toBe(walk);
|
||||
});
|
||||
|
||||
test("an assignment in another statement list, an adapter or a nested capture keeps the box", () => {
|
||||
const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]);
|
||||
const branch = run(
|
||||
outer(
|
||||
[
|
||||
decl("f.0", null),
|
||||
{ kind: "block", body: [assign("f.0", closure("walk", ["f.0"]))], loc },
|
||||
ret(call("f.0")),
|
||||
],
|
||||
[local("f.0", { mutable: true })],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(branch.get("walk")).toBe(walk);
|
||||
|
||||
const adapter = run(
|
||||
outer(
|
||||
[decl("f.0", closure("walk", ["f.0"], { adapts: true })), ret(call("f.0"))],
|
||||
[local("f.0")],
|
||||
),
|
||||
walk,
|
||||
);
|
||||
expect(adapter.get("walk")).toBe(walk);
|
||||
|
||||
const nested = lifted("walk", [use(closure("inner", ["f.0"])), ret(call("f.0"))], [local("f.0")]);
|
||||
const recaptured = run(
|
||||
outer([decl("f.0", closure("walk", ["f.0"])), ret(call("f.0"))], [local("f.0")]),
|
||||
nested,
|
||||
lifted("inner", [ret(call("f.1"))], [local("f.1")]),
|
||||
);
|
||||
expect(recaptured.get("walk")).toBe(nested);
|
||||
});
|
||||
|
||||
test("mutually recursive closures only drop their own capture", () => {
|
||||
const fns = run(
|
||||
outer(
|
||||
[
|
||||
decl("a.0", null),
|
||||
decl("b.0", null),
|
||||
assign("a.0", closure("fa", ["a.0", "b.0"]), true),
|
||||
assign("b.0", closure("fb", ["a.0", "b.0"]), true),
|
||||
ret(call("a.0")),
|
||||
],
|
||||
[local("a.0", { mutable: true, tdz: true }), local("b.0", { mutable: true, tdz: true })],
|
||||
),
|
||||
lifted(
|
||||
"fa",
|
||||
[use(call("b.0")), ret(call("a.0"))],
|
||||
[local("a.0", { tdz: true }), local("b.0", { tdz: true })],
|
||||
),
|
||||
lifted(
|
||||
"fb",
|
||||
[use(call("a.0")), ret(call("b.0"))],
|
||||
[local("a.0", { tdz: true }), local("b.0", { tdz: true })],
|
||||
),
|
||||
);
|
||||
expect(fns.get("fa")!.captures!.map((c) => c.localId)).toEqual(["b.0"]);
|
||||
expect(fns.get("fb")!.captures!.map((c) => c.localId)).toEqual(["a.0"]);
|
||||
expect(fns.get("outer")!.body.slice(2)).toEqual([
|
||||
assign("a.0", closure("fa", ["b.0"]), true),
|
||||
assign("b.0", closure("fb", ["a.0"]), true),
|
||||
ret(call("a.0")),
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import { CaptureFlow } from "./capture-values.js";
|
||||
import {
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js";
|
||||
|
||||
/** Closures that capture the binding they are stored in read themselves
|
||||
* through `selfRef` instead of through that binding's box.
|
||||
*
|
||||
* `const visit = (node) => { ... visit(child) ... }` lowers to a closure
|
||||
* over the box of `visit`, and the box then holds the closure: a two-object
|
||||
* reference cycle that reference counting cannot free. Each closure made
|
||||
* this way is buffered as a cycle candidate and reclaimed only when a
|
||||
* collector pass walks it. Nested function declarations already read their
|
||||
* own name through `selfRef`; this pass gives arrows and function
|
||||
* expressions bound to a binding the same form once the IR proves that the
|
||||
* binding always holds the closure itself while the closure can run:
|
||||
*
|
||||
* - every creation site of the closure body stores the new closure, as is,
|
||||
* into a boxed local of the creating function: either the initializer of
|
||||
* that local's only declaration, or the only assignment of a local whose
|
||||
* only declaration is empty and precedes the assignment in the same
|
||||
* statement list (so each box is filled once, and a closure can only run
|
||||
* after the statement that stores it);
|
||||
* - nothing else writes the binding, in the creating function or in any
|
||||
* body that shares its box (CaptureFlow.unchanged), and no local class
|
||||
* captures it;
|
||||
* - the closure body is synchronous, is not a generator, and does not pass
|
||||
* the binding on to a nested closure (its environment would need a box of
|
||||
* its own, allocated on every call).
|
||||
*
|
||||
* The capture then disappears from the body and from every creation site,
|
||||
* and each read of it becomes `selfRef` (the running closure, retained),
|
||||
* which is the same pointer the box would have produced. The creating
|
||||
* function keeps its binding; it loses its box when no other closure
|
||||
* captures it, except for temporal-dead-zone bindings, whose box still
|
||||
* carries the empty state its own reads test. */
|
||||
export function resolveSelfCaptures(mod: IrModule): IrModule {
|
||||
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const sites = new Map<string, { owner: string; closure: IrExpr & { kind: "closure" } }[]>();
|
||||
/** Closure nodes stored into a local by a qualifying definition, with that
|
||||
* local and the number of writes the definition itself makes. */
|
||||
const definitions = new Map<IrExpr, { local: string; writes: number }>();
|
||||
const declarations = new Map<string, Map<string, number>>();
|
||||
for (const fn of mod.functions) {
|
||||
const counts = new Map<string, number>();
|
||||
declarations.set(fn.name, counts);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure") {
|
||||
let list = sites.get(e.fnName);
|
||||
if (list === undefined) sites.set(e.fnName, (list = []));
|
||||
list.push({ owner: fn.name, closure: e });
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl") counts.set(s.localId, (counts.get(s.localId) ?? 0) + 1);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
for (const list of statementLists(fn.body)) {
|
||||
list.forEach((s, i) => {
|
||||
if (s.kind !== "varDecl") return;
|
||||
if (s.init !== null) {
|
||||
if (s.init.kind === "closure") definitions.set(s.init, { local: s.localId, writes: 0 });
|
||||
return;
|
||||
}
|
||||
for (let j = i + 1; j < list.length; j++) {
|
||||
const next = list[j]!;
|
||||
if (next.kind === "assign" && next.localId === s.localId) {
|
||||
if (next.value.kind === "closure")
|
||||
definitions.set(next.value, { local: s.localId, writes: 1 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let flow: CaptureFlow | null = null;
|
||||
/** Per closure body, the capture index that names its own closure. */
|
||||
const resolved = new Map<string, number>();
|
||||
for (const fn of mod.functions) {
|
||||
const captures = fn.captures;
|
||||
if (captures === undefined || captures.length === 0) continue;
|
||||
if (fn.async === true || fn.generator !== undefined || fn.classCaptures !== undefined) continue;
|
||||
const list = sites.get(fn.name);
|
||||
if (list === undefined || list.length === 0) continue;
|
||||
const recaptured = capturedIds(fn.body);
|
||||
for (let index = 0; index < captures.length; index++) {
|
||||
const capture = captures[index]!;
|
||||
if (capture.type.kind !== "func" || recaptured.has(capture.localId)) continue;
|
||||
const qualifies = list.every(({ owner, closure }) => {
|
||||
if (closure.adapts === true || !typeEquals(closure.type, capture.type)) return false;
|
||||
const definition = definitions.get(closure);
|
||||
const source = closure.captures[index];
|
||||
if (definition === undefined || source !== definition.local) return false;
|
||||
const creator = functions.get(owner)!;
|
||||
const local = creator.locals.find((l) => l.id === source);
|
||||
if (local?.boxed !== true || !typeEquals(local.type, closure.type)) return false;
|
||||
if (creator.params.some((p) => p.localId === source)) return false;
|
||||
if (creator.captures?.some((c) => c.localId === source)) return false;
|
||||
if (creator.classCaptures?.some((c) => c.localId === source)) return false;
|
||||
if (declarations.get(owner)!.get(source) !== 1) return false;
|
||||
flow ??= new CaptureFlow(mod.functions);
|
||||
return flow.unchanged(owner, source, definition.writes);
|
||||
});
|
||||
if (qualifies) {
|
||||
resolved.set(fn.name, index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resolved.size === 0) return mod;
|
||||
|
||||
const rewritten = mod.functions.map((fn) => {
|
||||
const own = resolved.get(fn.name);
|
||||
const self = own === undefined ? null : fn.captures![own]!;
|
||||
let changed = false;
|
||||
const expr = (e: IrExpr): IrExpr => {
|
||||
if (self !== null && e.kind === "varRef" && e.localId === self.localId) {
|
||||
changed = true;
|
||||
return { kind: "selfRef", type: self.type, loc: e.loc };
|
||||
}
|
||||
if (e.kind === "closure") {
|
||||
const drop = resolved.get(e.fnName);
|
||||
if (drop !== undefined) {
|
||||
changed = true;
|
||||
return { ...e, captures: e.captures.filter((_, i) => i !== drop) };
|
||||
}
|
||||
}
|
||||
return mapExprChildren(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): IrStmt => mapStmtChildren(s, expr, stmt);
|
||||
const body = fn.body.map(stmt);
|
||||
if (!changed && self === null) return fn;
|
||||
// A binding no environment captures any more needs no box. A dead-zone
|
||||
// binding keeps the box that represents its uninitialized state.
|
||||
const stillCaptured = capturedIds(body);
|
||||
// The callback returns IrLocal explicitly: inferred from both returns,
|
||||
// its type would be the unboxed copy's, and the self-hosted compiler
|
||||
// would narrow every kept local to that shape, dropping `boxed`.
|
||||
const locals = fn.locals
|
||||
.filter((local) => self === null || local.id !== self.localId)
|
||||
.map((local): IrLocal => {
|
||||
if (local.boxed !== true || local.tdz === true || stillCaptured.has(local.id)) return local;
|
||||
if (fn.captures?.some((c) => c.localId === local.id)) return local;
|
||||
if (fn.classCaptures?.some((c) => c.localId === local.id)) return local;
|
||||
if (!wasSource(fn, local.id, resolved)) return local;
|
||||
const { boxed: _boxed, ...plain } = local;
|
||||
return plain;
|
||||
});
|
||||
const next: IrFunction = { ...fn, locals, body };
|
||||
if (self !== null) next.captures = fn.captures!.filter((_, i) => i !== own);
|
||||
return next;
|
||||
});
|
||||
return { ...mod, functions: rewritten };
|
||||
}
|
||||
|
||||
/** Whether `fn` stored a resolved closure into `id`, the only case in which
|
||||
* this pass may unbox a binding (others stay exactly as lowered). */
|
||||
function wasSource(fn: IrFunction, id: string, resolved: ReadonlyMap<string, number>): boolean {
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure") {
|
||||
const index = resolved.get(e.fnName);
|
||||
if (index !== undefined && e.captures[index] === id) found = true;
|
||||
}
|
||||
return !found && everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
fn.body.every(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Locals some closure or class created in `body` captures. */
|
||||
function capturedIds(body: readonly IrStmt[]): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "closure" || e.kind === "classRef")
|
||||
for (const id of e.captures ?? []) ids.add(id);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
body.every(stmt);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** `body` and every statement list nested in its statements (not the ones
|
||||
* inside expressions). Statements of one list run in order, each at most
|
||||
* once per execution of the list. */
|
||||
function statementLists(body: readonly IrStmt[]): (readonly IrStmt[])[] {
|
||||
const lists: (readonly IrStmt[])[] = [];
|
||||
const visit = (list: readonly IrStmt[]): void => {
|
||||
lists.push(list);
|
||||
for (const s of list) {
|
||||
switch (s.kind) {
|
||||
case "block":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "forOf":
|
||||
visit(s.body);
|
||||
break;
|
||||
case "if":
|
||||
visit(s.then);
|
||||
if (s.else_ !== null) visit(s.else_);
|
||||
break;
|
||||
case "switch":
|
||||
for (const c of s.cases) visit(c.body);
|
||||
break;
|
||||
case "tryCatch":
|
||||
visit(s.tryBody);
|
||||
if (s.catchBody !== null) visit(s.catchBody);
|
||||
if (s.finallyBody !== null) visit(s.finallyBody);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(body);
|
||||
return lists;
|
||||
}
|
||||
@@ -286,17 +286,25 @@ test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as
|
||||
},
|
||||
);
|
||||
|
||||
test.each([{ kind: "promise", inner: F64 } as IrType])(
|
||||
"%j payloads still refuse unrelated data siblings",
|
||||
(type) => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "mixed", arms: [type, STRING, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod).map((error) => error.message)).toContain(
|
||||
`union mixed: ${type.kind} arm 0 beside non-unit arms`,
|
||||
);
|
||||
},
|
||||
);
|
||||
test("a maybe-promise union holds one promise arm beside data arms", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{ id: "maybe", arms: [{ kind: "promise", inner: F64 }, STRING, UNDEFINED_T] },
|
||||
]);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
|
||||
});
|
||||
|
||||
test("a union refuses a second promise arm", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
{
|
||||
id: "mixed",
|
||||
arms: [BOOL, { kind: "promise", inner: F64 }, { kind: "promise", inner: STRING }],
|
||||
},
|
||||
]);
|
||||
expect(validateModule(mod).map((error) => error.message)).toContain(
|
||||
"union mixed: promise arm 1 beside another promise arm",
|
||||
);
|
||||
});
|
||||
|
||||
test("differently typed collection payloads preserve separate union tags", () => {
|
||||
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
STRING,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
unionContainerArmsOk,
|
||||
maybePromiseUnionArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES } from "./ir.js";
|
||||
@@ -821,7 +821,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
u.arms.forEach((arm, i) => {
|
||||
// The unit kinds (undefinedT/nullT) are valid arms — union membership
|
||||
// is the ONLY place they may appear. Container payloads preserve
|
||||
// identity; promise data siblings retain their separate fence.
|
||||
// identity; a union carries at most one promise arm (maybe-promise).
|
||||
if (
|
||||
arm.kind === "void" ||
|
||||
arm.kind === "union" ||
|
||||
@@ -831,9 +831,9 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
) {
|
||||
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
|
||||
}
|
||||
if (arm.kind === "promise" && !unionContainerArmsOk(u.arms)) {
|
||||
if (arm.kind === "promise" && !maybePromiseUnionArmsOk(u.arms)) {
|
||||
errors.push({
|
||||
message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`,
|
||||
message: `union ${u.id}: ${arm.kind} arm ${i} beside another promise arm`,
|
||||
loc: noLoc,
|
||||
});
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5973-buffer-concat-untyped.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5985-self-capturing-closures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6000-json-stringify-class-instances.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6001-json-stringify-js-class-instances.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6070-iterator-helpers-untyped.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6240-maybe-promise-await-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6241-maybe-promise-handlers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6242-maybe-promise-rejections.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6243-maybe-promise-narrowing.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6244-maybe-promise-async-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6245-rest-unknown-handlers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6246-map-set-function-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6247-next-proxy-maybe-promise.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6248-tagged-template-any-rest.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6249-tagged-template-missing-substitutions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/json-class-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/maybe-promise-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -389,6 +389,10 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */
|
||||
ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding);
|
||||
ScrBytes *scr_buffer_concat(const ScrArr *list);
|
||||
ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total);
|
||||
/* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list
|
||||
* and each element, then the typed concat. NULL with the exception pending. */
|
||||
ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list);
|
||||
ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total);
|
||||
/* Drops the pool's slab reference and restores Buffer.poolSize (process
|
||||
* exit audits and library session resets). */
|
||||
void scr_buffer_pool_reset(void);
|
||||
|
||||
@@ -1249,6 +1249,56 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length)
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Buffer.concat over an untyped list: Node's checks in Node's order (an
|
||||
* Array, then the total of the two-argument form unless the list is empty,
|
||||
* then every element a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the
|
||||
* typed concat over the elements. NULL with the exception pending. */
|
||||
static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bool has_total) {
|
||||
ScrDyn *materialized = NULL;
|
||||
if (list->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(list) != NULL) {
|
||||
materialized = scr_dyn_typed_ref_materialize(list);
|
||||
list = materialized;
|
||||
}
|
||||
if (list->kind != SCR_DYN_ARR) {
|
||||
scr_dyn_arg_type_fail("list", "an instance of Array", list);
|
||||
scr_dyn_release(materialized);
|
||||
return NULL;
|
||||
}
|
||||
size_t n = list->v.arr.len;
|
||||
if (has_total && n > 0 && !scr_bytes_validate_off("length", total, 9007199254740991.0)) {
|
||||
scr_dyn_release(materialized);
|
||||
return NULL;
|
||||
}
|
||||
ScrArr *parts = scr_arr_new(SCR_ELEM_BYTES, (double)n);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
ScrDyn *item = scr_dyn_index_get(list, (double)i);
|
||||
const ScrDyn *shown = item != NULL ? item : scr_dyn_undefined();
|
||||
if (!scr_dyn_bytes_is(shown, SCR_BYTES_U8) || shown->v.bytes->is_data_view) {
|
||||
char name[40];
|
||||
snprintf(name, sizeof name, "list[%zu]", i);
|
||||
scr_dyn_arg_type_fail(name, "an instance of Buffer or Uint8Array", shown);
|
||||
scr_dyn_release(item);
|
||||
scr_arr_release(parts);
|
||||
scr_dyn_release(materialized);
|
||||
return NULL;
|
||||
}
|
||||
scr_arr_push_ref(parts, scr_bytes_retain(shown->v.bytes));
|
||||
scr_dyn_release(item);
|
||||
}
|
||||
ScrBytes *out = has_total ? scr_buffer_concat_len(parts, total) : scr_buffer_concat(parts);
|
||||
scr_arr_release(parts);
|
||||
scr_dyn_release(materialized);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrBytes *scr_buffer_concat_dyn(const ScrDyn *list) {
|
||||
return scr_buffer_concat_dyn_impl(list, 0, false);
|
||||
}
|
||||
|
||||
ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) {
|
||||
return scr_buffer_concat_dyn_impl(list, total, true);
|
||||
}
|
||||
|
||||
ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) {
|
||||
if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding);
|
||||
if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes);
|
||||
|
||||
@@ -175,14 +175,11 @@ static void scr_cyc_oom(void) {
|
||||
* struct on purpose: buffering a candidate is the hot path, and splitting
|
||||
* them across three arrays indexed by generation would touch three cache
|
||||
* lines per release where the original single buffer touched one. */
|
||||
typedef struct {
|
||||
void **v;
|
||||
size_t n, cap;
|
||||
/* The most entries a pass held, for the work vectors a pass fills
|
||||
* (scr_cyc_push_work); scr_cyc_pass_done frees a large vector that the
|
||||
* pass used little of. */
|
||||
size_t used;
|
||||
} ScrVec;
|
||||
/* Its layout is scr_rc.h's ScrCycBuffer (the candidate buffers are part of
|
||||
* the inline buffering ABI). `used`: the most entries a pass held, for the
|
||||
* work vectors a pass fills (scr_cyc_push_work); scr_cyc_pass_done frees a
|
||||
* large vector that the pass used little of. */
|
||||
typedef ScrCycBuffer ScrVec;
|
||||
|
||||
typedef struct {
|
||||
void *object;
|
||||
@@ -226,7 +223,6 @@ typedef struct {
|
||||
unsigned char death_shift;
|
||||
bool falling;
|
||||
bool keep_base;
|
||||
size_t nbuffered;
|
||||
size_t trigger;
|
||||
size_t alloc_trigger;
|
||||
size_t gray_referenced;
|
||||
@@ -260,7 +256,6 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD};
|
||||
#define scr_cyc_growth_shift (scr_cyc_thread.growth_shift)
|
||||
#define scr_cyc_growth_streak (scr_cyc_thread.growth_streak)
|
||||
#define scr_cyc_backlog_shift (scr_cyc_thread.backlog_shift)
|
||||
#define scr_cyc_nbuffered (scr_cyc_thread.nbuffered)
|
||||
#define scr_cyc_trigger (scr_cyc_thread.trigger)
|
||||
#define scr_cyc_alloc_trigger (scr_cyc_thread.alloc_trigger)
|
||||
#define scr_gray_referenced (scr_cyc_thread.gray_referenced)
|
||||
@@ -392,7 +387,16 @@ static inline void scr_cyc_note_used(ScrVec *vec) {
|
||||
/* ── the candidate-root buffers (one per generation) ──────────────────── */
|
||||
|
||||
#ifndef SCR_WORKERS
|
||||
static SCR_TL ScrVec scr_roots[SCR_CYC_NGENS];
|
||||
/* Emitted and inline releases append nursery candidates to these buffers
|
||||
* directly where the build allows it (scr_rc.h). Explicitly initialized so
|
||||
* library localization can demote it (see scr_cyc_types). */
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
ScrCycBuffers scr_cyc_buffers = {0};
|
||||
#else
|
||||
static SCR_TL ScrCycBuffers scr_cyc_buffers;
|
||||
#endif
|
||||
#define scr_roots (scr_cyc_buffers.roots)
|
||||
#define scr_cyc_alloc_trigger (scr_cyc_buffers.alloc_trigger)
|
||||
static SCR_TL bool scr_collecting = false;
|
||||
#endif
|
||||
|
||||
@@ -456,6 +460,7 @@ static SCR_TL bool scr_yin_busy;
|
||||
static void scr_yin_leave(ScrCycHdr *h, void *obj);
|
||||
static size_t scr_yin_collect(void);
|
||||
#endif
|
||||
static void scr_cyc_route_bypassed(void);
|
||||
/* A limited pass (scr_cyc_collect_set) takes only the candidates in this
|
||||
* set from the buffers up to `scr_cyc_take_gen`; NULL takes them all. */
|
||||
static SCR_TL const ScrCycSet *scr_cyc_take = NULL;
|
||||
@@ -568,18 +573,18 @@ static size_t scr_cyc_nursery_threshold(void) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
/* Buffered candidates across every generation, and the count at which the
|
||||
* release path next collects. Keeping a running total (rather than summing
|
||||
* the buffers) is what holds the hot path to one load and one compare, as
|
||||
* it was when there was a single buffer. The trigger is re-armed at the end
|
||||
* of every pass to "whatever survived, plus a nursery's worth": without that
|
||||
/* The number of buffered candidates (across every generation) at which the
|
||||
* release path next collects, and the live count at which it collects
|
||||
* however few candidates there are. The trigger is re-armed at the end of
|
||||
* every pass to "whatever survived, plus a nursery's worth": without that
|
||||
* hysteresis a large older buffer — which a nursery pass cannot drain —
|
||||
* would re-arm on every single release. Both start at zero, so the first
|
||||
* release runs one trivial pass that arms them properly. */
|
||||
* release runs one trivial pass that arms them properly. The inline
|
||||
* appends (scr_rc.h) compare against the window scr_cyc_window_update
|
||||
* derives from these, so the slow path sums the buffers when it needs the
|
||||
* total. */
|
||||
#ifndef SCR_WORKERS
|
||||
static SCR_TL size_t scr_cyc_nbuffered = 0;
|
||||
static SCR_TL size_t scr_cyc_trigger = 0;
|
||||
static SCR_TL size_t scr_cyc_alloc_trigger = 0;
|
||||
#endif
|
||||
/* Gray objects whose count is above zero after the trial decrements so far
|
||||
* (see scr_mg_visit). markGray records the gray objects in scr_white while
|
||||
@@ -612,6 +617,34 @@ static size_t scr_cyc_buffered(void) {
|
||||
+ scr_roots[SCR_CYC_OLD].n;
|
||||
}
|
||||
|
||||
/* The inline buffering window (scr_rc.h): how many entries the nursery
|
||||
* buffer may hold before an append would bring the buffered total to the
|
||||
* trigger or would need the buffer to grow. While a pass runs, every
|
||||
* release takes the call. Recomputed wherever the buffers, the nursery
|
||||
* buffer's capacity or the trigger change in bulk; the release and death
|
||||
* paths adjust it by one (scr_cyc_window_shrink). */
|
||||
static inline void scr_cyc_window_update(void) {
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
size_t window = 0;
|
||||
if (!scr_collecting) {
|
||||
size_t older = scr_roots[SCR_CYC_MATURE].n + scr_roots[SCR_CYC_OLD].n;
|
||||
if (scr_cyc_trigger > older + 1) window = scr_cyc_trigger - older - 1;
|
||||
if (window > scr_roots[SCR_CYC_NURSERY].cap) window = scr_roots[SCR_CYC_NURSERY].cap;
|
||||
}
|
||||
scr_cyc_buffers.window = window;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* One step of the window's formula going down by one: an older candidate
|
||||
* was buffered, or a nursery death lowered the trigger. A window below the
|
||||
* formula only sends more releases through the call, so the clamp to the
|
||||
* capacity can stay where it is. */
|
||||
static inline void scr_cyc_window_shrink(void) {
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_buffers.window) scr_cyc_buffers.window--;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool scr_cyc_older_backlog(void) {
|
||||
return scr_roots[SCR_CYC_MATURE].n != 0 || scr_roots[SCR_CYC_OLD].n != 0;
|
||||
}
|
||||
@@ -772,6 +805,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow
|
||||
* per pass, so it is kept out of line and built for size, which leaves the
|
||||
* hot buffering path in scr_cyc_on_release small. */
|
||||
static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) {
|
||||
scr_cyc_route_bypassed();
|
||||
scr_cyc_note_fall();
|
||||
unsigned gen = SCR_CYC_OLD;
|
||||
bool by_growth = scr_cyc_grown(SCR_CYC_OLD);
|
||||
@@ -797,6 +831,7 @@ void scr_cyc_collect_scheduled(void) {
|
||||
* allocation trigger in a program that has gone idle. */
|
||||
if (scr_yin.n != 0) scr_yin_collect();
|
||||
#endif
|
||||
scr_cyc_route_bypassed();
|
||||
scr_cyc_note_fall();
|
||||
if (scr_cyc_buffered() == 0) {
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
@@ -877,6 +912,10 @@ typedef struct ScrCycKind {
|
||||
static SCR_TL ScrCycKind *scr_cyc_kinds;
|
||||
static SCR_TL size_t scr_cyc_kinds_cap;
|
||||
#endif
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
/* Types that bypass the nursery right now (scr_cyc_route_bypassed). */
|
||||
static size_t scr_cyc_nursery_bypassing = 0;
|
||||
#endif
|
||||
|
||||
/* The statistics of `kind` (nonzero), growing the table to cover it. */
|
||||
static SCR_CYC_COMPACT ScrCycKind *scr_cyc_kind_grow(uint32_t kind) {
|
||||
@@ -901,7 +940,12 @@ static inline bool scr_cyc_bypassed(const ScrCycType *type, unsigned gen) {
|
||||
if (kind >= scr_cyc_kinds_cap) return false;
|
||||
ScrCycLevelStat *k = &scr_cyc_kinds[kind].level[gen];
|
||||
if (k->bypass == 0) return false;
|
||||
if (--k->bypass == 0) k->walked = k->freed = 0;
|
||||
if (--k->bypass == 0) {
|
||||
k->walked = k->freed = 0;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (gen == SCR_CYC_NURSERY) scr_cyc_nursery_bypassing--;
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -919,9 +963,16 @@ static inline void scr_cyc_walked(const ScrCycType *type, unsigned gen) {
|
||||
if (!type->kind) return;
|
||||
ScrCycLevelStat *k = &scr_cyc_kind(type->kind)->level[gen];
|
||||
if (++k->walked == SCR_CYC_BYPASS_WINDOW) {
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
bool before = k->bypass != 0;
|
||||
#endif
|
||||
k->bypass = k->freed * SCR_CYC_BYPASS_RATIO < SCR_CYC_BYPASS_WINDOW
|
||||
? SCR_CYC_BYPASS_WINDOW * SCR_CYC_BYPASS_PROBE
|
||||
: 0;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (gen == SCR_CYC_NURSERY && before != (k->bypass != 0))
|
||||
scr_cyc_nursery_bypassing += before ? (size_t)-1 : 1;
|
||||
#endif
|
||||
k->walked = k->freed = 0;
|
||||
}
|
||||
}
|
||||
@@ -930,6 +981,40 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) {
|
||||
if (type->kind) scr_cyc_kind(type->kind)->level[gen].freed++;
|
||||
}
|
||||
|
||||
/* An inline append (scr_rc.h) leaves every nursery candidate in the nursery
|
||||
* buffer, including one whose type bypasses the nursery, which
|
||||
* scr_cyc_on_release would have buffered past it. Where a candidate waits
|
||||
* matters only once something reads the buffers, so every scheduling
|
||||
* decision and every pass first moves such candidates to the generation
|
||||
* scr_cyc_on_release would have chosen, consuming the bypass counts as it
|
||||
* would have, in buffer order. Candidates that scr_cyc_on_release buffered
|
||||
* already passed the same test with the counts unchanged since (they only
|
||||
* change during passes), so they stay. */
|
||||
static SCR_CYC_COMPACT void scr_cyc_route_bypassed(void) {
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_nursery_bypassing == 0) return;
|
||||
ScrVec *nursery = &scr_roots[SCR_CYC_NURSERY];
|
||||
for (size_t i = 0; i < nursery->n;) {
|
||||
void *obj = nursery->v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
unsigned gen = scr_cyc_buffer_gen(scr_cyc_type(h), SCR_CYC_NURSERY);
|
||||
if (gen == SCR_CYC_NURSERY) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
void *last = nursery->v[--nursery->n];
|
||||
nursery->v[i] = last;
|
||||
if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i);
|
||||
scr_cyc_set_gen(h, gen);
|
||||
ScrVec *b = &scr_roots[gen];
|
||||
if (b->n == b->cap) scr_cyc_grow(b);
|
||||
scr_cyc_set_buffered(h, (uint32_t)b->n);
|
||||
b->v[b->n++] = obj;
|
||||
}
|
||||
scr_cyc_window_update();
|
||||
#endif
|
||||
}
|
||||
|
||||
void scr_cyc_on_dead(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (!scr_cyc_is_buffered(h)) return;
|
||||
@@ -949,11 +1034,12 @@ void scr_cyc_on_dead(void *obj) {
|
||||
b->v[i] = last;
|
||||
if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i);
|
||||
scr_cyc_clear_buffered(h);
|
||||
scr_cyc_nbuffered--;
|
||||
/* A death must not consume room reserved for a future candidate. */
|
||||
scr_cyc_trigger--;
|
||||
if (scr_cyc_gen(h) != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
/* An older death lowers the older count with the trigger: the window
|
||||
* stays. A nursery death lowers only the trigger. */
|
||||
if (scr_cyc_gen(h) == SCR_CYC_NURSERY) scr_cyc_window_shrink();
|
||||
else if (!scr_cyc_older_backlog()) scr_cyc_scheduled_mature_age = 0;
|
||||
}
|
||||
|
||||
/* Publication (scr_publish.c) takes buffered objects out of their candidate
|
||||
@@ -1014,31 +1100,39 @@ void scr_cyc_pub_fill(void) {
|
||||
}
|
||||
holes->n = 0;
|
||||
size_t closed = before - b->n;
|
||||
scr_cyc_nbuffered -= closed;
|
||||
/* A death must not consume room reserved for a future candidate. */
|
||||
scr_cyc_trigger -= closed;
|
||||
if (closed && gen != SCR_CYC_NURSERY) older = true;
|
||||
}
|
||||
if (older && !scr_cyc_older_backlog()) scr_cyc_scheduled_mature_age = 0;
|
||||
scr_cyc_window_update();
|
||||
}
|
||||
|
||||
/* Every release that leaves an unbuffered object alive lands here (the
|
||||
* emitted and inline release paths test `buffered` first), so it stays what
|
||||
* it has always been: buffer inline, then one compare. */
|
||||
* emitted and inline release paths test `buffered` first) unless the window
|
||||
* let it append a nursery candidate on its own fast path (scr_rc.h), so it
|
||||
* stays what it has always been: the same append, then buffer and compare. */
|
||||
void scr_cyc_on_release(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
/* Releasing an existing candidate cannot advance the trigger. A pass
|
||||
* always re-arms after its teardowns, including any candidates they add. */
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_buffer_gen(scr_cyc_type(h), scr_cyc_gen(h)));
|
||||
ScrVec *b = &scr_roots[scr_cyc_gen(h)];
|
||||
if (b->n == b->cap) scr_cyc_grow(b);
|
||||
unsigned gen = scr_cyc_gen(h);
|
||||
ScrVec *b = &scr_roots[gen];
|
||||
if (b->n == b->cap) {
|
||||
scr_cyc_grow(b);
|
||||
scr_cyc_window_update();
|
||||
}
|
||||
scr_cyc_set_buffered(h, (uint32_t)b->n);
|
||||
b->v[b->n++] = obj;
|
||||
scr_cyc_nbuffered++;
|
||||
if (gen != SCR_CYC_NURSERY) scr_cyc_window_shrink();
|
||||
/* Never re-entered: a teardown's releases of untraced children can buffer
|
||||
* new candidates mid-collection, but they only wait for the next pass. */
|
||||
if (!scr_collecting && (scr_cyc_nbuffered >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger))
|
||||
if (!scr_collecting && (scr_cyc_buffered() >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger))
|
||||
scr_cyc_collect_due();
|
||||
}
|
||||
|
||||
@@ -1523,8 +1617,10 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) {
|
||||
scr_cyc_note_fall();
|
||||
scr_yin_busy = true;
|
||||
/* Releases made by teardowns below buffer candidates but must not start
|
||||
* a trial-deletion pass while the dead set is half torn down. */
|
||||
* a trial-deletion pass while the dead set is half torn down, and take
|
||||
* the call, as during a pass (scr_cyc_window_update). */
|
||||
scr_collecting = true;
|
||||
scr_cyc_window_update();
|
||||
if (n > scr_yin_counts_cap) {
|
||||
size_t cap = scr_yin_counts_cap ? scr_yin_counts_cap * 2 : 1024;
|
||||
while (cap < n) cap *= 2;
|
||||
@@ -1605,11 +1701,12 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) {
|
||||
scr_cyc_trim(&scr_yin_xrel);
|
||||
scr_cyc_live_seen = scr_cyc_live;
|
||||
scr_collecting = false;
|
||||
scr_cyc_window_update();
|
||||
scr_yin_busy = false;
|
||||
/* The candidates buffered above count toward trial deletion's trigger,
|
||||
* which only a release checks; a program whose releases are all of
|
||||
* young objects would otherwise never reach it. */
|
||||
if (scr_cyc_nbuffered >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger)
|
||||
if (scr_cyc_buffered() >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger)
|
||||
scr_cyc_collect_due();
|
||||
return freed;
|
||||
}
|
||||
@@ -1765,10 +1862,10 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
|
||||
}
|
||||
/* markRoots drained buffers in bulk and teardowns moved the count around;
|
||||
* resync from the buffers themselves and re-arm. */
|
||||
scr_cyc_nbuffered = scr_cyc_buffered();
|
||||
scr_cyc_trigger = scr_cyc_nbuffered + scr_cyc_nursery_threshold();
|
||||
scr_cyc_trigger = scr_cyc_buffered() + scr_cyc_nursery_threshold();
|
||||
scr_cyc_alloc_trigger = scr_cyc_live + SCR_CYC_NURSERY_ALLOCATIONS;
|
||||
scr_collecting = false;
|
||||
scr_cyc_window_update();
|
||||
return freed;
|
||||
}
|
||||
|
||||
@@ -1808,7 +1905,9 @@ static size_t scr_cyc_pass_done(size_t freed) {
|
||||
|
||||
static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
if (scr_collecting) return 0;
|
||||
scr_cyc_route_bypassed();
|
||||
scr_collecting = true;
|
||||
scr_cyc_window_update();
|
||||
scr_gen_limit = gen_limit;
|
||||
|
||||
/* markRoots: every buffered entry is a candidate (retains do not mark
|
||||
@@ -1986,6 +2085,9 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
#ifdef SCR_CYC_TESTING
|
||||
/* Tests only: one pass at exactly `gen`, outside the schedule. */
|
||||
size_t scr_cyc_pass_at(unsigned gen) { return scr_cyc_pass(gen); }
|
||||
/* Tests only: route appended candidates of bypassing types, as the next
|
||||
* scheduling decision or pass would. */
|
||||
void scr_cyc_route_bypassed_at(void) { scr_cyc_route_bypassed(); }
|
||||
#endif
|
||||
|
||||
size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) {
|
||||
@@ -2054,4 +2156,8 @@ void scr_cyc_context_cleanup(void) {
|
||||
scr_yin_counts = NULL;
|
||||
scr_yin_counts_cap = 0;
|
||||
#endif
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
scr_cyc_nursery_bypassing = 0;
|
||||
#endif
|
||||
scr_cyc_window_update();
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ typedef enum {
|
||||
SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */
|
||||
SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */
|
||||
SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */
|
||||
SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */
|
||||
SCR_DYNH_COUNT,
|
||||
} ScrDynHandleTag;
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ static bool scr_builtin_iterable(const ScrDyn *value) {
|
||||
return value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES ||
|
||||
(scr_dyn_generator(value) && value->v.typed_ref.type_key[0] != 'a') ||
|
||||
(value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value)) ||
|
||||
(value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET ||
|
||||
(value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_ITERATOR_HELPER || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET ||
|
||||
scr_dyn_handle_ops_of(value)->iter_pack || scr_dyn_handle_ops_of(value)->iter_step));
|
||||
}
|
||||
static bool scr_iterator_object(const ScrDyn *value) {
|
||||
@@ -4657,8 +4657,16 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) {
|
||||
return ((ScrNativeIterator *)value->v.handle.ptr)->step_done;
|
||||
}
|
||||
|
||||
static int scr_ih_method_index(const char *name, size_t len);
|
||||
static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc);
|
||||
static ScrDyn *scr_ih_method_function(int index);
|
||||
|
||||
static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) {
|
||||
if (len != 4 || memcmp(key, "next", 4)) return NULL;
|
||||
if (len != 4 || memcmp(key, "next", 4)) {
|
||||
/* The Iterator.prototype helper methods. */
|
||||
int index = scr_ih_method_index(key, len);
|
||||
return index >= 0 ? scr_ih_method_function(index) : NULL;
|
||||
}
|
||||
unsigned kind = ((ScrNativeIterator *)ptr)->kind;
|
||||
if (kind == 4) return scr_iterator_function(10, &scr_native_handle_iterator_next_call, "next");
|
||||
return kind == 3 ? scr_iterator_function(8, &scr_native_set_iterator_next_call, "next")
|
||||
@@ -4669,8 +4677,9 @@ static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) {
|
||||
|
||||
static ScrDyn *scr_native_iterator_invoke(void *ptr, ScrDyn *self, const char *method,
|
||||
ScrDyn *const *args, size_t argc, const char *what) {
|
||||
(void)self;
|
||||
if (!strcmp(method, "next")) return scr_native_iterator_next(ptr);
|
||||
int index = scr_ih_method_index(method, strlen(method));
|
||||
if (index >= 0) return scr_iterator_helper_method(self, index, args, argc);
|
||||
ScrDyn *missing = scr_dyn_undefined();
|
||||
return scr_dyn_call(missing, args, argc, what);
|
||||
}
|
||||
@@ -4700,6 +4709,682 @@ static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── iterator helpers on checked iterators (ES2025) ─────────────────────────
|
||||
* The Iterator.prototype methods over a checked iterator object: a native
|
||||
* array, string, Map or Set iterator, or a helper object made here. Typed
|
||||
* code lowers its helpers statically; these answer untyped JavaScript
|
||||
* receivers. The stages (map, filter, take, drop, flatMap) make helper
|
||||
* objects that pull lazily; the consuming methods (toArray, forEach, reduce,
|
||||
* some, every, find) drive the iterator to completion or to their answer.
|
||||
* Closing follows IteratorClose: a helper closes the iterator it was made
|
||||
* from when a callback throws, when its budget is spent, and on return();
|
||||
* an error from a close that runs because of another error is dropped. */
|
||||
|
||||
static ScrDyn *scr_iterator_call(const ScrDyn *method, const ScrDyn *receiver, const char *name);
|
||||
static ScrDyn *scr_iterator_method(const ScrDyn *source);
|
||||
|
||||
enum { SCR_IH_MAP, SCR_IH_FILTER, SCR_IH_TAKE, SCR_IH_DROP, SCR_IH_FLATMAP };
|
||||
enum { SCR_IH_START, SCR_IH_YIELDED, SCR_IH_RUNNING, SCR_IH_DONE };
|
||||
|
||||
typedef struct {
|
||||
size_t rc;
|
||||
unsigned kind;
|
||||
unsigned state;
|
||||
ScrDyn *iterated; /* the iterator the helper was made from */
|
||||
ScrDyn *next; /* its next method, read when the helper was made */
|
||||
ScrDyn *fn; /* the map, filter or flatMap callback */
|
||||
double remaining; /* the take or drop budget */
|
||||
double counter;
|
||||
ScrDyn *inner; /* flatMap's current inner iterator */
|
||||
ScrDyn *inner_next;
|
||||
} ScrIterHelper;
|
||||
|
||||
static const char *const scr_ih_method_names[] = {
|
||||
"map", "filter", "take", "drop", "flatMap", "reduce", "toArray", "forEach", "some", "every", "find",
|
||||
};
|
||||
#define SCR_IH_METHOD_COUNT (sizeof scr_ih_method_names / sizeof scr_ih_method_names[0])
|
||||
|
||||
/* The index of an Iterator.prototype helper method name, or -1. */
|
||||
static int scr_ih_method_index(const char *name, size_t len) {
|
||||
for (size_t i = 0; i < SCR_IH_METHOD_COUNT; i++) {
|
||||
if (strlen(scr_ih_method_names[i]) == len && !memcmp(scr_ih_method_names[i], name, len)) return (int)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* V8's description of a value in "<value> is not a function": the type,
|
||||
* with the value for primitives that print one. */
|
||||
static void scr_ih_describe_type(ScrJsonBuf *b, const ScrDyn *v) {
|
||||
switch (v->kind) {
|
||||
case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); return;
|
||||
case SCR_DYN_NULL: scr_jb_puts(b, "object null"); return;
|
||||
case SCR_DYN_BOOL: scr_jb_puts(b, v->v.b ? "boolean true" : "boolean false"); return;
|
||||
case SCR_DYN_NUM: {
|
||||
scr_jb_puts(b, "number ");
|
||||
ScrStr *n = scr_f64_to_scrstr(v->v.num);
|
||||
scr_jb_write(b, n->data, n->len);
|
||||
scr_str_release(n);
|
||||
return;
|
||||
}
|
||||
case SCR_DYN_STR:
|
||||
scr_jb_puts(b, "string \"");
|
||||
scr_jb_write(b, v->v.str->data, v->v.str->len);
|
||||
scr_jb_putc(b, '"');
|
||||
return;
|
||||
case SCR_DYN_SYMBOL: scr_jb_puts(b, "symbol"); return;
|
||||
case SCR_DYN_BIGINT: scr_jb_puts(b, "bigint"); return;
|
||||
default: scr_jb_puts(b, "object"); return;
|
||||
}
|
||||
}
|
||||
|
||||
static bool scr_ih_require_callable(const ScrDyn *fn) {
|
||||
if (scr_dyn_is_callable(fn)) return true;
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_ih_describe_type(&b, fn);
|
||||
scr_jb_puts(&b, " is not a function");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* V8's text for the value in "<value> must be positive": numbers, strings
|
||||
* raw, undefined by name,
|
||||
* arrays and objects by their default string or constructor name. */
|
||||
static void scr_ih_describe_value(ScrJsonBuf *b, const ScrDyn *v) {
|
||||
if (v->kind == SCR_DYN_ARR) {
|
||||
scr_jb_puts(b, "[object Array]");
|
||||
return;
|
||||
}
|
||||
if (v->kind == SCR_DYN_OBJ) {
|
||||
scr_jb_puts(b, "#<Object>");
|
||||
return;
|
||||
}
|
||||
if (v->kind == SCR_DYN_HANDLE) {
|
||||
scr_jb_puts(b, "#<");
|
||||
scr_jb_puts(b, scr_dyn_handle_ops_of(v)->cls);
|
||||
scr_jb_putc(b, '>');
|
||||
return;
|
||||
}
|
||||
if (v->kind == SCR_DYN_STR) {
|
||||
scr_jb_write(b, v->v.str->data, v->v.str->len);
|
||||
return;
|
||||
}
|
||||
if (v->kind == SCR_DYN_NUM) {
|
||||
ScrStr *n = scr_f64_to_scrstr(v->v.num);
|
||||
scr_jb_write(b, n->data, n->len);
|
||||
scr_str_release(n);
|
||||
return;
|
||||
}
|
||||
scr_jb_puts(b, v->kind == SCR_DYN_UNDEF ? "undefined" : "NaN");
|
||||
}
|
||||
|
||||
/* ToIntegerOrInfinity of a take/drop budget with Node's RangeError for NaN
|
||||
* and negative budgets. Answers false with the exception pending. */
|
||||
static bool scr_ih_budget(const ScrDyn *limit, double *out) {
|
||||
double n = scr_dyn_number_coerce(limit);
|
||||
if (scr_exc_pending()) return false;
|
||||
if (n != n) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_ih_describe_value(&b, limit);
|
||||
scr_jb_puts(&b, " must be positive");
|
||||
scr_throw_error(SCR_ERR_RANGE, scr_jb_finish(&b));
|
||||
return false;
|
||||
}
|
||||
double t = n < 9007199254740992.0 && n > -9007199254740992.0 ? n - fmod(n, 1.0) : n;
|
||||
if (t < 0) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_ih_describe_value(&b, limit);
|
||||
scr_jb_puts(&b, " must be positive");
|
||||
scr_throw_error(SCR_ERR_RANGE, scr_jb_finish(&b));
|
||||
return false;
|
||||
}
|
||||
*out = t == 0 ? 0 : t;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* IteratorClose(iterator, completion). `abrupt` is a throw completion: the
|
||||
* pending error stays and anything the return() call raises is dropped.
|
||||
* Otherwise errors from return() propagate, and a non-object result is
|
||||
* Node's TypeError. Answers false when an exception is pending after. */
|
||||
static bool scr_ih_close(ScrDyn *iterator, bool abrupt) {
|
||||
ScrCaught *first = abrupt ? scr_exc_take() : NULL;
|
||||
ScrDyn *method = scr_iterator_read(iterator, "return", 6);
|
||||
bool ok = method != NULL;
|
||||
if (ok && method->kind != SCR_DYN_UNDEF && method->kind != SCR_DYN_NULL) {
|
||||
ScrDyn *result = scr_iterator_call(method, iterator, "return");
|
||||
if (!result) ok = false;
|
||||
else {
|
||||
if (!abrupt && !scr_iterator_object(result)) {
|
||||
static const char message[] = "Iterator result is not an object";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
ok = false;
|
||||
}
|
||||
scr_dyn_release(result);
|
||||
}
|
||||
}
|
||||
scr_dyn_release(method);
|
||||
if (first) {
|
||||
if (scr_exc_pending()) {
|
||||
ScrCaught *dropped = scr_exc_take();
|
||||
if (dropped) scr_caught_release(dropped);
|
||||
}
|
||||
scr_rethrow(first);
|
||||
scr_caught_release(first);
|
||||
return false;
|
||||
}
|
||||
return ok && !scr_exc_pending();
|
||||
}
|
||||
|
||||
/* IteratorStepValue over (iterator, next): +1 value with *done false, or
|
||||
* *done true (the answer is then undefined); NULL with the exception
|
||||
* pending when next() throws or answers a non-object. */
|
||||
static ScrDyn *scr_ih_step(ScrDyn *iterator, ScrDyn *next, bool *done) {
|
||||
*done = false;
|
||||
if (scr_dyn_iterator_can_step(iterator, next)) {
|
||||
ScrDyn *value = scr_dyn_iterator_step(iterator);
|
||||
if (!value) return NULL;
|
||||
*done = scr_dyn_iterator_step_done(iterator);
|
||||
return value;
|
||||
}
|
||||
ScrDyn *raw = scr_iterator_call(next, iterator, "next");
|
||||
if (!raw) return NULL;
|
||||
ScrDyn *result = scr_dyn_iterator_result(raw);
|
||||
scr_dyn_release(raw);
|
||||
if (!result) return NULL;
|
||||
ScrDyn *finished = scr_iterator_read(result, "done", 4);
|
||||
if (!finished) {
|
||||
scr_dyn_release(result);
|
||||
return NULL;
|
||||
}
|
||||
*done = scr_dyn_truthy(finished);
|
||||
scr_dyn_release(finished);
|
||||
ScrDyn *value = *done ? scr_dyn_retain(scr_dyn_undefined()) : scr_iterator_read(result, "value", 5);
|
||||
scr_dyn_release(result);
|
||||
return value;
|
||||
}
|
||||
|
||||
/* GetIteratorDirect: the receiver must be an object; its next method is
|
||||
* read once. +1 next, or NULL with the TypeError pending. */
|
||||
static ScrDyn *scr_ih_direct(const ScrDyn *self, const char *method) {
|
||||
if (!scr_iterator_object(self)) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "Iterator.prototype.");
|
||||
scr_jb_puts(&b, method);
|
||||
scr_jb_puts(&b, " called on non-object");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
|
||||
return NULL;
|
||||
}
|
||||
return scr_iterator_read(self, "next", 4);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_result(ScrDyn *value, bool done) {
|
||||
ScrDyn *result = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(result, "value", 5, value);
|
||||
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done));
|
||||
return result;
|
||||
}
|
||||
|
||||
static void *scr_ih_retain(void *ptr) {
|
||||
((ScrIterHelper *)ptr)->rc++;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void scr_ih_release(void *ptr) {
|
||||
ScrIterHelper *h = ptr;
|
||||
if (--h->rc) return;
|
||||
scr_dyn_release(h->iterated);
|
||||
scr_dyn_release(h->next);
|
||||
if (h->fn) scr_dyn_release(h->fn);
|
||||
if (h->inner) scr_dyn_release(h->inner);
|
||||
if (h->inner_next) scr_dyn_release(h->inner_next);
|
||||
free(h);
|
||||
}
|
||||
|
||||
static void scr_ih_clear_inner(ScrIterHelper *h) {
|
||||
if (h->inner) scr_dyn_release(h->inner);
|
||||
if (h->inner_next) scr_dyn_release(h->inner_next);
|
||||
h->inner = NULL;
|
||||
h->inner_next = NULL;
|
||||
}
|
||||
|
||||
/* GetIteratorFlattenable(value, reject-primitives) for flatMap's callback
|
||||
* result: +1 iterator with *next_out +1, or NULL with the TypeError pending. */
|
||||
static ScrDyn *scr_ih_flattenable(ScrDyn *value, ScrDyn **next_out) {
|
||||
if (!scr_iterator_object(value)) {
|
||||
static const char message[] = "Iterator.prototype.flatMap called on non-object";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
ScrDyn *method = scr_iterator_method(value);
|
||||
if (!method) return NULL;
|
||||
ScrDyn *iterator;
|
||||
if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) {
|
||||
iterator = scr_dyn_retain(value);
|
||||
} else {
|
||||
iterator = scr_iterator_call(method, value, "value[Symbol.iterator]");
|
||||
if (iterator && !scr_iterator_object(iterator)) {
|
||||
scr_dyn_release(iterator);
|
||||
static const char message[] = "Iterator result is not an object";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
iterator = NULL;
|
||||
}
|
||||
}
|
||||
scr_dyn_release(method);
|
||||
if (!iterator) return NULL;
|
||||
ScrDyn *next = scr_iterator_read(iterator, "next", 4);
|
||||
if (!next) {
|
||||
scr_dyn_release(iterator);
|
||||
return NULL;
|
||||
}
|
||||
*next_out = next;
|
||||
return iterator;
|
||||
}
|
||||
|
||||
/* Calls a stage callback with (value, counter); +1 result or NULL. */
|
||||
static ScrDyn *scr_ih_callback(ScrDyn *fn, ScrDyn *value, double counter) {
|
||||
ScrDyn *index = scr_dyn_new_num(counter);
|
||||
ScrDyn *args[2] = { value, index };
|
||||
ScrDyn *undefined = scr_dyn_retain(scr_dyn_undefined());
|
||||
scr_dyn_this_push_dyn(undefined);
|
||||
ScrDyn *result = scr_dyn_call(fn, args, 2, "callback");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(undefined);
|
||||
scr_dyn_release(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* The helper's next value: +1 value with *done false, *done true, or NULL
|
||||
* with the exception pending (the helper is then done). */
|
||||
static ScrDyn *scr_ih_advance(ScrIterHelper *h, bool *done) {
|
||||
*done = false;
|
||||
for (;;) {
|
||||
if (h->kind == SCR_IH_FLATMAP && h->inner) {
|
||||
bool inner_done;
|
||||
ScrDyn *value = scr_ih_step(h->inner, h->inner_next, &inner_done);
|
||||
if (!value) {
|
||||
scr_ih_close(h->iterated, true);
|
||||
return NULL;
|
||||
}
|
||||
if (!inner_done) return value;
|
||||
scr_dyn_release(value);
|
||||
scr_ih_clear_inner(h);
|
||||
continue;
|
||||
}
|
||||
if (h->kind == SCR_IH_TAKE) {
|
||||
if (h->remaining == 0) {
|
||||
*done = true;
|
||||
if (!scr_ih_close(h->iterated, false)) return NULL;
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
if (h->remaining != INFINITY) h->remaining--;
|
||||
}
|
||||
if (h->kind == SCR_IH_DROP) {
|
||||
while (h->remaining > 0) {
|
||||
if (h->remaining != INFINITY) h->remaining--;
|
||||
bool skipped_done;
|
||||
ScrDyn *skipped = scr_ih_step(h->iterated, h->next, &skipped_done);
|
||||
if (!skipped) return NULL;
|
||||
scr_dyn_release(skipped);
|
||||
if (skipped_done) {
|
||||
*done = true;
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
}
|
||||
}
|
||||
bool source_done;
|
||||
ScrDyn *value = scr_ih_step(h->iterated, h->next, &source_done);
|
||||
if (!value) return NULL;
|
||||
if (source_done) {
|
||||
*done = true;
|
||||
return value;
|
||||
}
|
||||
if (h->kind == SCR_IH_TAKE || h->kind == SCR_IH_DROP) return value;
|
||||
ScrDyn *mapped = scr_ih_callback(h->fn, value, h->counter);
|
||||
if (!mapped) {
|
||||
scr_dyn_release(value);
|
||||
scr_ih_close(h->iterated, true);
|
||||
return NULL;
|
||||
}
|
||||
h->counter++;
|
||||
if (h->kind == SCR_IH_MAP) {
|
||||
scr_dyn_release(value);
|
||||
return mapped;
|
||||
}
|
||||
if (h->kind == SCR_IH_FILTER) {
|
||||
bool keep = scr_dyn_truthy(mapped);
|
||||
scr_dyn_release(mapped);
|
||||
if (keep) return value;
|
||||
scr_dyn_release(value);
|
||||
continue;
|
||||
}
|
||||
/* flatMap */
|
||||
scr_dyn_release(value);
|
||||
ScrDyn *inner_next = NULL;
|
||||
ScrDyn *inner = scr_ih_flattenable(mapped, &inner_next);
|
||||
scr_dyn_release(mapped);
|
||||
if (!inner) {
|
||||
scr_ih_close(h->iterated, true);
|
||||
return NULL;
|
||||
}
|
||||
h->inner = inner;
|
||||
h->inner_next = inner_next;
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_ih_running_error(void) {
|
||||
static const char message[] = "Generator is already running";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_next(ScrIterHelper *h) {
|
||||
if (h->state == SCR_IH_RUNNING) {
|
||||
scr_ih_running_error();
|
||||
return NULL;
|
||||
}
|
||||
if (h->state == SCR_IH_DONE) return scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true);
|
||||
h->state = SCR_IH_RUNNING;
|
||||
scr_ih_retain(h);
|
||||
bool done;
|
||||
ScrDyn *value = scr_ih_advance(h, &done);
|
||||
ScrDyn *result = NULL;
|
||||
if (!value || done) {
|
||||
h->state = SCR_IH_DONE;
|
||||
scr_ih_clear_inner(h);
|
||||
if (value) {
|
||||
scr_dyn_release(value);
|
||||
result = scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true);
|
||||
}
|
||||
} else {
|
||||
h->state = SCR_IH_YIELDED;
|
||||
result = scr_ih_result(value, false);
|
||||
}
|
||||
scr_ih_release(h);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* %IteratorHelperPrototype%.return: an unstarted or suspended helper
|
||||
* completes and closes the iterator it was made from (flatMap closes its
|
||||
* inner iterator first). */
|
||||
static ScrDyn *scr_ih_return(ScrIterHelper *h) {
|
||||
if (h->state == SCR_IH_RUNNING) {
|
||||
scr_ih_running_error();
|
||||
return NULL;
|
||||
}
|
||||
if (h->state != SCR_IH_DONE) {
|
||||
h->state = SCR_IH_DONE;
|
||||
scr_ih_retain(h);
|
||||
bool ok = true;
|
||||
if (h->inner) {
|
||||
ScrDyn *inner = scr_dyn_retain(h->inner);
|
||||
scr_ih_clear_inner(h);
|
||||
if (!scr_ih_close(inner, false)) {
|
||||
scr_ih_close(h->iterated, true);
|
||||
ok = false;
|
||||
}
|
||||
scr_dyn_release(inner);
|
||||
}
|
||||
if (ok) ok = scr_ih_close(h->iterated, false);
|
||||
scr_ih_release(h);
|
||||
if (!ok) return NULL;
|
||||
}
|
||||
return scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc);
|
||||
|
||||
static ScrDyn *scr_ih_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args,
|
||||
size_t argc, const char *what) {
|
||||
if (!strcmp(method, "next")) return scr_ih_next(ptr);
|
||||
if (!strcmp(method, "return")) return scr_ih_return(ptr);
|
||||
int index = scr_ih_method_index(method, strlen(method));
|
||||
if (index >= 0) return scr_iterator_helper_method(self, index, args, argc);
|
||||
return scr_dyn_call(scr_dyn_undefined(), args, argc, what);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_method_function(int index);
|
||||
|
||||
static ScrDyn *scr_ih_next_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
(void)clo; (void)args; (void)argc;
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrDyn *result = NULL;
|
||||
if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ITERATOR_HELPER) {
|
||||
result = scr_ih_next(self->v.handle.ptr);
|
||||
} else {
|
||||
static const char message[] = "Method Iterator Helper.prototype.next called on incompatible receiver";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
scr_dyn_release(self);
|
||||
return result;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_return_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
(void)clo; (void)args; (void)argc;
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrDyn *result = NULL;
|
||||
if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ITERATOR_HELPER) {
|
||||
result = scr_ih_return(self->v.handle.ptr);
|
||||
} else {
|
||||
static const char message[] = "Method Iterator Helper.prototype.return called on incompatible receiver";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
scr_dyn_release(self);
|
||||
return result;
|
||||
}
|
||||
|
||||
static SCR_TL ScrDyn *scr_ih_functions[SCR_IH_METHOD_COUNT + 2];
|
||||
static SCR_TL bool scr_ih_functions_registered;
|
||||
|
||||
static void scr_ih_functions_cleanup(void) {
|
||||
for (size_t i = 0; i < SCR_IH_METHOD_COUNT + 2; i++) {
|
||||
scr_dyn_release(scr_ih_functions[i]);
|
||||
scr_ih_functions[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_function(size_t slot, ScrDynThunk thunk, const char *name) {
|
||||
if (!scr_ih_functions_registered) {
|
||||
scr_ih_functions_registered = true;
|
||||
scr_atexit(scr_ih_functions_cleanup);
|
||||
}
|
||||
if (!scr_ih_functions[slot])
|
||||
scr_ih_functions[slot] = scr_dyn_new_func(scr_closure_new(NULL, 0), thunk, 0, "native:iterator", name);
|
||||
return scr_dyn_retain(scr_ih_functions[slot]);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_get(void *ptr, const char *key, size_t len) {
|
||||
(void)ptr;
|
||||
if (len == 4 && !memcmp(key, "next", 4)) return scr_ih_function(SCR_IH_METHOD_COUNT, &scr_ih_next_call, "next");
|
||||
if (len == 6 && !memcmp(key, "return", 6))
|
||||
return scr_ih_function(SCR_IH_METHOD_COUNT + 1, &scr_ih_return_call, "return");
|
||||
int index = scr_ih_method_index(key, len);
|
||||
return index >= 0 ? scr_ih_method_function(index) : NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_inspect_view(void *ptr) {
|
||||
(void)ptr;
|
||||
return scr_dyn_new_obj();
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_new(unsigned kind, ScrDyn *iterated, ScrDyn *next, ScrDyn *fn, double remaining) {
|
||||
static const ScrDynHandleOps ops = {
|
||||
.cls = "Object",
|
||||
.retain = &scr_ih_retain,
|
||||
.release = &scr_ih_release,
|
||||
.invoke = &scr_ih_invoke,
|
||||
.get = &scr_ih_get,
|
||||
.inspect_view = &scr_ih_inspect_view,
|
||||
.to_string_tag = "Iterator Helper",
|
||||
.inspect_view_empty = true,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_ITERATOR_HELPER, &ops);
|
||||
ScrIterHelper *h = malloc(sizeof *h);
|
||||
if (!h) scr_json_oom();
|
||||
*h = (ScrIterHelper){ .rc = 1, .kind = kind, .state = SCR_IH_START, .iterated = scr_dyn_retain(iterated),
|
||||
.next = next, .fn = fn ? scr_dyn_retain(fn) : NULL, .remaining = remaining };
|
||||
ScrDyn *result = scr_dyn_new_handle(h, SCR_DYNH_ITERATOR_HELPER);
|
||||
scr_ih_release(h);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* The consuming methods: drive (self, next) with `fn`. */
|
||||
static ScrDyn *scr_ih_consume(ScrDyn *self, ScrDyn *next, int index, ScrDyn *fn, ScrDyn *const *args,
|
||||
size_t argc) {
|
||||
const char *name = scr_ih_method_names[index];
|
||||
bool reduce = !strcmp(name, "reduce");
|
||||
ScrDyn *out = !strcmp(name, "toArray") ? scr_dyn_new_arr() : NULL;
|
||||
ScrDyn *acc = NULL;
|
||||
double counter = 0;
|
||||
if (reduce && argc >= 2) acc = scr_dyn_retain(args[1]);
|
||||
else if (reduce) {
|
||||
bool done;
|
||||
ScrDyn *first = scr_ih_step(self, next, &done);
|
||||
if (!first) return NULL;
|
||||
if (done) {
|
||||
scr_dyn_release(first);
|
||||
static const char message[] = "Reduce of a done iterator with no initial value";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
acc = first;
|
||||
counter = 1;
|
||||
}
|
||||
for (;;) {
|
||||
bool done;
|
||||
ScrDyn *value = scr_ih_step(self, next, &done);
|
||||
if (!value) {
|
||||
if (out) scr_dyn_release(out);
|
||||
if (acc) scr_dyn_release(acc);
|
||||
return NULL;
|
||||
}
|
||||
if (done) {
|
||||
scr_dyn_release(value);
|
||||
break;
|
||||
}
|
||||
if (out) {
|
||||
scr_dyn_arr_push(out, value);
|
||||
continue;
|
||||
}
|
||||
ScrDyn *answer;
|
||||
if (reduce) {
|
||||
ScrDyn *index_value = scr_dyn_new_num(counter);
|
||||
ScrDyn *call_args[3] = { acc, value, index_value };
|
||||
ScrDyn *undefined = scr_dyn_retain(scr_dyn_undefined());
|
||||
scr_dyn_this_push_dyn(undefined);
|
||||
answer = scr_dyn_call(fn, call_args, 3, "callback");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(undefined);
|
||||
scr_dyn_release(index_value);
|
||||
} else {
|
||||
answer = scr_ih_callback(fn, value, counter);
|
||||
}
|
||||
counter++;
|
||||
if (!answer) {
|
||||
scr_dyn_release(value);
|
||||
if (acc) scr_dyn_release(acc);
|
||||
scr_ih_close(self, true);
|
||||
return NULL;
|
||||
}
|
||||
if (reduce) {
|
||||
scr_dyn_release(acc);
|
||||
acc = answer;
|
||||
scr_dyn_release(value);
|
||||
continue;
|
||||
}
|
||||
bool truthy = scr_dyn_truthy(answer);
|
||||
scr_dyn_release(answer);
|
||||
if (!strcmp(name, "forEach")) {
|
||||
scr_dyn_release(value);
|
||||
continue;
|
||||
}
|
||||
bool stop = !strcmp(name, "every") ? !truthy : truthy;
|
||||
if (!stop) {
|
||||
scr_dyn_release(value);
|
||||
continue;
|
||||
}
|
||||
if (!scr_ih_close(self, false)) {
|
||||
scr_dyn_release(value);
|
||||
return NULL;
|
||||
}
|
||||
if (!strcmp(name, "find")) return value;
|
||||
scr_dyn_release(value);
|
||||
return scr_dyn_new_bool(!strcmp(name, "some"));
|
||||
}
|
||||
if (out) return out;
|
||||
if (reduce) return acc;
|
||||
if (!strcmp(name, "some") || !strcmp(name, "every")) return scr_dyn_new_bool(!strcmp(name, "every"));
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* An Iterator.prototype helper method called on `self` (borrowed). */
|
||||
static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc) {
|
||||
const char *name = scr_ih_method_names[index];
|
||||
ScrDyn *next = scr_ih_direct(self, name);
|
||||
if (!next) return NULL;
|
||||
ScrDyn *arg = argc > 0 ? args[0] : scr_dyn_undefined();
|
||||
if (!strcmp(name, "take") || !strcmp(name, "drop")) {
|
||||
double budget;
|
||||
if (!scr_ih_budget(arg, &budget)) {
|
||||
scr_dyn_release(next);
|
||||
scr_ih_close(self, true);
|
||||
return NULL;
|
||||
}
|
||||
return scr_ih_new(!strcmp(name, "take") ? SCR_IH_TAKE : SCR_IH_DROP, self, next, NULL, budget);
|
||||
}
|
||||
if (!strcmp(name, "toArray")) {
|
||||
ScrDyn *result = scr_ih_consume(self, next, index, NULL, args, argc);
|
||||
scr_dyn_release(next);
|
||||
return result;
|
||||
}
|
||||
if (!scr_ih_require_callable(arg)) {
|
||||
scr_dyn_release(next);
|
||||
scr_ih_close(self, true);
|
||||
return NULL;
|
||||
}
|
||||
if (index < 5) {
|
||||
unsigned kind = !strcmp(name, "map") ? SCR_IH_MAP : !strcmp(name, "filter") ? SCR_IH_FILTER : SCR_IH_FLATMAP;
|
||||
return scr_ih_new(kind, self, next, arg, 0);
|
||||
}
|
||||
ScrDyn *result = scr_ih_consume(self, next, index, arg, args, argc);
|
||||
scr_dyn_release(next);
|
||||
return result;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_ih_method_call(int index, ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrDyn *result = scr_iterator_helper_method(self, index, args, argc);
|
||||
scr_dyn_release(self);
|
||||
return result;
|
||||
}
|
||||
|
||||
#define SCR_IH_THUNK(i) \
|
||||
static ScrDyn *scr_ih_method_call_##i(ScrClosure *clo, ScrDyn *const *args, size_t argc) { \
|
||||
(void)clo; \
|
||||
return scr_ih_method_call(i, args, argc); \
|
||||
}
|
||||
SCR_IH_THUNK(0)
|
||||
SCR_IH_THUNK(1)
|
||||
SCR_IH_THUNK(2)
|
||||
SCR_IH_THUNK(3)
|
||||
SCR_IH_THUNK(4)
|
||||
SCR_IH_THUNK(5)
|
||||
SCR_IH_THUNK(6)
|
||||
SCR_IH_THUNK(7)
|
||||
SCR_IH_THUNK(8)
|
||||
SCR_IH_THUNK(9)
|
||||
SCR_IH_THUNK(10)
|
||||
|
||||
static ScrDyn *scr_ih_method_function(int index) {
|
||||
static const ScrDynThunk thunks[SCR_IH_METHOD_COUNT] = {
|
||||
&scr_ih_method_call_0, &scr_ih_method_call_1, &scr_ih_method_call_2, &scr_ih_method_call_3,
|
||||
&scr_ih_method_call_4, &scr_ih_method_call_5, &scr_ih_method_call_6, &scr_ih_method_call_7,
|
||||
&scr_ih_method_call_8, &scr_ih_method_call_9, &scr_ih_method_call_10,
|
||||
};
|
||||
return scr_ih_function((size_t)index, thunks[index], scr_ih_method_names[index]);
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection) {
|
||||
return scr_native_collection_iterator(source, selection);
|
||||
}
|
||||
@@ -12818,8 +13503,15 @@ static ScrDyn *scr_native_set_pack(void *ptr) {
|
||||
|
||||
ScrDyn *scr_dyn_native_set(ScrMap *value) {
|
||||
static const ScrDynHandleOps ops = {
|
||||
"Set", &scr_map_retain_v, &scr_map_release_v, &scr_native_set_invoke,
|
||||
&scr_native_set_get, &scr_weak_map_write, NULL, &scr_native_set_pack,
|
||||
.cls = "Set",
|
||||
.retain = &scr_map_retain_v,
|
||||
.release = &scr_map_release_v,
|
||||
.invoke = &scr_native_set_invoke,
|
||||
.get = &scr_native_set_get,
|
||||
.set = &scr_weak_map_write,
|
||||
.iter_pack = &scr_native_set_pack,
|
||||
/* Entries live in internal slots: JSON.stringify writes {}, as Node. */
|
||||
.json_empty_object = true,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_SET, &ops);
|
||||
return scr_dyn_new_handle(value, SCR_DYNH_SET);
|
||||
@@ -12928,8 +13620,15 @@ static ScrDyn *scr_native_map_pack(void *ptr) {
|
||||
|
||||
ScrDyn *scr_dyn_native_map(ScrMap *value) {
|
||||
static const ScrDynHandleOps ops = {
|
||||
"Map", &scr_map_retain_v, &scr_map_release_v, &scr_native_map_invoke,
|
||||
&scr_native_map_get, &scr_weak_map_write, NULL, &scr_native_map_pack,
|
||||
.cls = "Map",
|
||||
.retain = &scr_map_retain_v,
|
||||
.release = &scr_map_release_v,
|
||||
.invoke = &scr_native_map_invoke,
|
||||
.get = &scr_native_map_get,
|
||||
.set = &scr_weak_map_write,
|
||||
.iter_pack = &scr_native_map_pack,
|
||||
/* Entries live in internal slots: JSON.stringify writes {}, as Node. */
|
||||
.json_empty_object = true,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_MAP, &ops);
|
||||
return scr_dyn_new_handle(value, SCR_DYNH_MAP);
|
||||
|
||||
@@ -482,12 +482,76 @@ uint32_t scr_cyc_pub_take(void *obj);
|
||||
void scr_cyc_pub_restore(void *obj, uint32_t index);
|
||||
void scr_cyc_pub_fill(void);
|
||||
|
||||
/* The candidate buffers, one per generation (scr_cycle.c), and the window
|
||||
* through which a release buffers a nursery candidate on its fast path. A
|
||||
* release that leaves an unbuffered nursery object alive appends it to the
|
||||
* nursery buffer while that buffer holds fewer than `window` entries and the
|
||||
* live count is below `alloc_trigger` (scr_cyc_append); anything else takes
|
||||
* the full scr_cyc_on_release. scr_cycle.c keeps `window` at most at the
|
||||
* number of nursery entries the buffers can hold before a collection falls
|
||||
* due or the buffer must grow, and at 0 while a pass runs, so the append is
|
||||
* exactly what scr_cyc_on_release would do with a nursery candidate. A
|
||||
* candidate whose type bypasses the nursery is moved past it before the
|
||||
* buffers are next read (scr_cycle.c, "level bypass").
|
||||
*
|
||||
* The runtime's own release paths append inline (scr_cyc_released), and
|
||||
* scr_cyc_on_release tries the append first. The layout is an ABI:
|
||||
* optimized emitted code appends through a per-module helper
|
||||
* (llvm/alloc.ts cycleAppendHelper) that addresses roots[0].v, roots[0].n,
|
||||
* window and alloc_trigger at fixed offsets. Builds whose collector state
|
||||
* is thread-local (worker executables, thread-instanced libraries) never
|
||||
* append on a fast path. */
|
||||
typedef struct ScrCycBuffer {
|
||||
void **v;
|
||||
size_t n, cap;
|
||||
/* The most entries a pass held, for the work vectors a pass fills
|
||||
* (scr_cycle.c ScrVec); unused by the candidate buffers. */
|
||||
size_t used;
|
||||
} ScrCycBuffer;
|
||||
typedef struct ScrCycBuffers {
|
||||
ScrCycBuffer roots[SCR_CYC_NGENS];
|
||||
size_t window;
|
||||
size_t alloc_trigger;
|
||||
} ScrCycBuffers;
|
||||
#if !defined(SCR_WORKERS) && !(defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
|
||||
#define SCR_CYC_INLINE_BUFFERING 1
|
||||
extern ScrCycBuffers scr_cyc_buffers;
|
||||
#if UINTPTR_MAX == UINT64_MAX
|
||||
_Static_assert(offsetof(ScrCycBuffers, roots) == 0 && offsetof(ScrCycBuffer, v) == 0 &&
|
||||
offsetof(ScrCycBuffer, n) == 8 && offsetof(ScrCycBuffers, window) == 96 &&
|
||||
offsetof(ScrCycBuffers, alloc_trigger) == 104,
|
||||
"llvm/alloc.ts appends nursery candidates against this layout");
|
||||
#endif
|
||||
/* Append the unbuffered object `obj` (header `h`) to the nursery buffer if
|
||||
* it is a nursery object and the window allows; false leaves it to the rest
|
||||
* of scr_cyc_on_release. */
|
||||
static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) {
|
||||
ScrCycBuffer *nursery = &scr_cyc_buffers.roots[SCR_CYC_NURSERY];
|
||||
size_t n = nursery->n;
|
||||
if (scr_cyc_gen(h) != SCR_CYC_NURSERY || n >= scr_cyc_buffers.window ||
|
||||
scr_cyc_live >= scr_cyc_buffers.alloc_trigger)
|
||||
return false;
|
||||
nursery->v[n] = obj;
|
||||
nursery->n = n + 1;
|
||||
scr_cyc_set_buffered(h, (uint32_t)n);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
#define SCR_CYC_INLINE_BUFFERING 0
|
||||
#endif
|
||||
|
||||
/* Inline halves of the hooks for runtime release paths, mirroring the
|
||||
* emitted ones (llvm/shapes.ts): a release of an already-buffered
|
||||
* object, and the death of an object that was never buffered, need no call.
|
||||
* object, and the death of an object that was never buffered, need no call,
|
||||
* and a nursery candidate is appended inline while the window allows.
|
||||
* Most releases of long-lived containers take the first early return. */
|
||||
static inline void scr_cyc_released(void *obj) {
|
||||
if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj);
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
if (scr_cyc_append(h, obj)) return;
|
||||
#endif
|
||||
scr_cyc_on_release(obj);
|
||||
}
|
||||
static inline void scr_cyc_dead(void *obj) {
|
||||
if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj);
|
||||
|
||||
@@ -712,8 +712,11 @@ static void check_deferred_white_restoration(void) {
|
||||
* once the type's objects die in nursery cycles again, the next probe window
|
||||
* finds that and it is walked again. Window, ratio and probe interval mirror
|
||||
* SCR_CYC_BYPASS_*. */
|
||||
/* Tests only (SCR_CYC_TESTING): one pass at exactly `gen`. */
|
||||
/* Tests only (SCR_CYC_TESTING): one pass at exactly `gen`, and the routing
|
||||
* of appended candidates whose type bypasses a level, which the next
|
||||
* scheduling decision or pass would do. */
|
||||
size_t scr_cyc_pass_at(unsigned gen);
|
||||
void scr_cyc_route_bypassed_at(void);
|
||||
static size_t bypass_traced; /* trace calls on bypass-kind nodes */
|
||||
static size_t bypass_freed; /* bypass-kind nodes freed */
|
||||
static void bypass_trace(void *obj, ScrTraceVisit visit, void *ctx) {
|
||||
@@ -785,6 +788,22 @@ static void check_nursery_bypass(void) {
|
||||
scr_collect_cycles();
|
||||
check(bypass_freed - before == 2 * RINGS, "an older pass missed dead rings of a bypassed type");
|
||||
|
||||
/* The same through the inline release path, which leaves the candidates
|
||||
* in the nursery buffer: the next pass routes them past the nursery. */
|
||||
before = bypass_freed;
|
||||
for (size_t r = 0; r < RINGS; r++) {
|
||||
Node *a = make_bypass_node(), *b = make_bypass_node();
|
||||
a->next = b;
|
||||
b->next = a;
|
||||
a->rc++;
|
||||
a->rc--;
|
||||
scr_cyc_released(a);
|
||||
}
|
||||
run_release_passes();
|
||||
check(bypass_freed - before < 2 * RINGS, "inline-buffered bypassed rings were walked by the nursery");
|
||||
scr_collect_cycles();
|
||||
check(bypass_freed - before == 2 * RINGS, "an older pass missed inline-buffered rings");
|
||||
|
||||
/* Now the type's objects die in nursery cycles. Once INTERVAL windows'
|
||||
* worth of candidates went past the nursery, a probe window walks them
|
||||
* there again, and its yield turns the bypass off. A nursery pass after
|
||||
@@ -832,13 +851,16 @@ static Node *make_mature_kind_node(void) {
|
||||
}
|
||||
|
||||
/* How many of PROBE fresh, externally owned candidates a release buffers
|
||||
* straight into the old generation. */
|
||||
* straight into the old generation, by the time anything reads the buffers
|
||||
* (a release may append the candidate to the nursery buffer, which the next
|
||||
* decision routes past the levels its type bypasses). */
|
||||
static size_t probe_old_on_release(Node **owned, size_t probe) {
|
||||
scr_collect_cycles(); /* reset the growth baselines: no pass runs below */
|
||||
size_t old = 0;
|
||||
for (size_t i = 0; i < probe; i++) {
|
||||
owned[i] = make_mature_kind_node();
|
||||
read_live(owned[i]);
|
||||
scr_cyc_route_bypassed_at();
|
||||
old += scr_cyc_gen(scr_cyc_hdr(owned[i])) == SCR_CYC_OLD;
|
||||
}
|
||||
return old;
|
||||
@@ -1187,6 +1209,53 @@ static void check_large_old_buffer_with_nursery_candidates(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Releases through scr_cyc_released append nursery candidates inline while
|
||||
* the window allows, and fall back to scr_cyc_on_release exactly when a
|
||||
* collection is due: the candidate that brings the buffered total to the
|
||||
* trigger runs the pass, as it would through the call. */
|
||||
static void check_inline_buffering(void) {
|
||||
enum { MAX = 4096 };
|
||||
static Node *nodes[MAX];
|
||||
size_t threshold = configured_nursery_threshold();
|
||||
if (threshold > MAX) return;
|
||||
scr_collect_cycles();
|
||||
size_t before = traced;
|
||||
for (size_t i = 0; i < threshold; i++) {
|
||||
nodes[i] = make_ring(1);
|
||||
nodes[i]->rc--; /* drop the self edge: a live leaf held by its owner */
|
||||
nodes[i]->next = NULL;
|
||||
nodes[i]->rc++;
|
||||
nodes[i]->rc--;
|
||||
scr_cyc_released(nodes[i]);
|
||||
ScrCycHdr *h = scr_cyc_hdr(nodes[i]);
|
||||
if (i + 1 < threshold) {
|
||||
check(scr_cyc_is_buffered(h) && scr_cyc_gen(h) == SCR_CYC_NURSERY,
|
||||
"a nursery release did not buffer its candidate");
|
||||
#if SCR_CYC_INLINE_BUFFERING
|
||||
check(scr_cyc_buffers.roots[SCR_CYC_NURSERY].v[scr_cyc_buf_index(h)] == nodes[i],
|
||||
"an inline append is not where the header says");
|
||||
#endif
|
||||
check(traced == before, "a pass ran before the trigger");
|
||||
}
|
||||
}
|
||||
/* The last release reached the trigger: one nursery pass walked them all
|
||||
* and promoted the survivors, which wait in the mature buffer. */
|
||||
check(traced > before, "the release that reached the trigger did not collect");
|
||||
for (size_t i = 0; i < threshold; i++) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(nodes[i]);
|
||||
check(scr_cyc_gen(h) == SCR_CYC_MATURE, "a nursery survivor was not promoted");
|
||||
check(nodes[i]->rc == 1, "a survivor lost its count");
|
||||
}
|
||||
size_t freed_before = freed;
|
||||
for (size_t i = 0; i < threshold; i++) {
|
||||
nodes[i]->rc = 0;
|
||||
scr_cyc_on_dead(nodes[i]);
|
||||
node_free(nodes[i]);
|
||||
}
|
||||
check(freed - freed_before == threshold, "the leaves were not freed");
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
/* The death rule: once objects have died outside passes (the live count
|
||||
* fell by 1/32 of the heap the last full pass left, here the floor's), a
|
||||
* full pass is due as soon as the heap has grown the base quarter past it,
|
||||
@@ -1266,6 +1335,7 @@ static void check_death_brings_full_pass(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
check_inline_buffering();
|
||||
check_old_garbage_reclaimed_while_growing();
|
||||
check_older_rings_into_old();
|
||||
check_cycle_spanning_mature_and_old();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Buffer.concat over an untyped list (JavaScript's `const chunks = []`):
|
||||
// Uint8Array elements concatenate, the length form pads or truncates, and
|
||||
// a non-array list or a non-Uint8Array element throws Node's TypeError. The
|
||||
// length form checks the length before the elements, and an empty list
|
||||
// returns before either.
|
||||
const chunks = [];
|
||||
chunks.push(Buffer.from("ab"));
|
||||
chunks.push(new Uint8Array([99, 100]));
|
||||
console.log(Buffer.concat(chunks).toString());
|
||||
console.log(Buffer.concat(chunks, 6));
|
||||
console.log(Buffer.concat(chunks, 3).toString());
|
||||
console.log(Buffer.concat([]).length);
|
||||
|
||||
const lists = [[1], ["a"], [Buffer.from("x"), null], "x", 5, [new Uint16Array(1)]];
|
||||
for (const list of lists) {
|
||||
try {
|
||||
Buffer.concat(list);
|
||||
console.log("no error");
|
||||
} catch (e) {
|
||||
console.log(e.name, e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const withLength = [
|
||||
[[1], -1],
|
||||
[[Buffer.from("a"), "b"], NaN],
|
||||
[[1], 2],
|
||||
[[], -1],
|
||||
[[Buffer.from("a")], 1.5],
|
||||
[[Buffer.from("a"), Buffer.from("b")], 2],
|
||||
];
|
||||
for (const [list, length] of withLength) {
|
||||
try {
|
||||
console.log("length", Buffer.concat(list, length).length);
|
||||
} catch (e) {
|
||||
console.log(e.name, e.code, e.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Closures that call themselves through the binding they are stored in:
|
||||
// const arrows, function expressions, closures made in loops, closures that
|
||||
// also hand themselves to nested callbacks, and a binding that is later
|
||||
// rebound (its closure must see the new value). Each read of the binding
|
||||
// inside the body is the same function object the binding holds. The
|
||||
// sanitized lane's RC audit fails if any of these closures leaks or is
|
||||
// freed while reachable.
|
||||
|
||||
const fact = (n: number): number => (n <= 1 ? 1 : n * fact(n - 1));
|
||||
console.log(fact(10));
|
||||
|
||||
const pick = (n: number): (() => number) | null => (n === 0 ? null : () => n);
|
||||
const identity = (depth: number): unknown => (depth === 0 ? identity : identity(depth - 1));
|
||||
console.log(identity(3) === identity, pick(0) === null, pick(2)!());
|
||||
|
||||
const fib = function (n: number): number {
|
||||
return n < 2 ? n : fib(n - 1) + fib(n - 2);
|
||||
};
|
||||
console.log(fib(15));
|
||||
|
||||
const outerName = function innerName(n: number): number {
|
||||
return n <= 0 ? 0 : 1 + outerName(n - 1) + innerName(0);
|
||||
};
|
||||
console.log(outerName(5));
|
||||
|
||||
// One closure per iteration, each recursing into itself and reading its
|
||||
// own iteration's value.
|
||||
const perIteration: ((n: number) => number)[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const step = (n: number): number => (n === 0 ? i : step(n - 1) + 10);
|
||||
perIteration.push(step);
|
||||
}
|
||||
console.log(perIteration.map((f) => f(2)).join(" "));
|
||||
console.log(perIteration[0] === perIteration[1], perIteration[2] === perIteration[2]);
|
||||
|
||||
// The closure also passes itself to nested callbacks.
|
||||
interface Tree {
|
||||
value: number;
|
||||
kids: Tree[];
|
||||
}
|
||||
const leaf = (value: number): Tree => ({ value, kids: [] });
|
||||
const tree: Tree = {
|
||||
value: 1,
|
||||
kids: [{ value: 2, kids: [leaf(3), leaf(4)] }, leaf(5)],
|
||||
};
|
||||
const sum = (node: Tree): number => node.value + node.kids.map((kid) => sum(kid)).reduce((a, b) => a + b, 0);
|
||||
console.log(sum(tree));
|
||||
const visited: number[] = [];
|
||||
const walk = (node: Tree): void => {
|
||||
visited.push(node.value);
|
||||
node.kids.forEach(walk);
|
||||
};
|
||||
walk(tree);
|
||||
console.log(visited.join(","));
|
||||
|
||||
// Captured state next to the self reference.
|
||||
function counterTo(limit: number): () => number {
|
||||
let calls = 0;
|
||||
const tick = (): number => {
|
||||
calls++;
|
||||
return calls < limit ? tick() : calls;
|
||||
};
|
||||
return tick;
|
||||
}
|
||||
const tick = counterTo(5);
|
||||
console.log(tick(), tick());
|
||||
|
||||
// A rebound binding: the first closure calls whatever the binding holds now.
|
||||
let rebound = (n: number): number => (n <= 0 ? 0 : 1 + rebound(n - 1));
|
||||
const first = rebound;
|
||||
console.log(first(3));
|
||||
rebound = (n: number): number => n * 100;
|
||||
console.log(first(3), rebound(3));
|
||||
|
||||
// Mutual recursion keeps both bindings live.
|
||||
const isEven = (n: number): boolean => (n === 0 ? true : isOdd(n - 1));
|
||||
const isOdd = (n: number): boolean => (n === 0 ? false : isEven(n - 1));
|
||||
console.log(isEven(10), isOdd(7), isEven(3));
|
||||
|
||||
// Identity as a map key from inside the body.
|
||||
const seen = new Map<unknown, number>();
|
||||
const keyed = (n: number): number => {
|
||||
seen.set(keyed, (seen.get(keyed) ?? 0) + 1);
|
||||
return n === 0 ? seen.size : keyed(n - 1);
|
||||
};
|
||||
console.log(keyed(4), seen.get(keyed));
|
||||
|
||||
// An object that holds a closure capturing the object (a real cycle).
|
||||
class Widget {
|
||||
label: string;
|
||||
onChange: (() => string) | null = null;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
function widgets(count: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const widget = new Widget(`w${i}`);
|
||||
const describe = (depth: number): string => (depth === 0 ? widget.label : describe(depth - 1));
|
||||
widget.onChange = () => describe(2);
|
||||
total += widget.onChange().length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
console.log(widgets(1000));
|
||||
|
||||
// Many short-lived recursive closures.
|
||||
let checksum = 0;
|
||||
for (let round = 0; round < 20000; round++) {
|
||||
const down = (n: number): number => (n === 0 ? round % 7 : down(n - 1));
|
||||
checksum += down(3);
|
||||
}
|
||||
console.log(checksum);
|
||||
|
||||
// Generators and async functions that name themselves.
|
||||
const countdown = function* (n: number): Generator<number> {
|
||||
if (n < 0) return;
|
||||
yield n;
|
||||
yield* countdown(n - 1);
|
||||
};
|
||||
console.log([...countdown(3)].join(""));
|
||||
const later = async (n: number): Promise<number> => (n === 0 ? 0 : 1 + (await later(n - 1)));
|
||||
later(3).then((value) => console.log("async", value));
|
||||
@@ -0,0 +1,143 @@
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
norm(): number {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
class Account {
|
||||
static count = 0;
|
||||
#secret = "hidden";
|
||||
private balance = 10;
|
||||
protected owner = "ada";
|
||||
label: string | undefined = undefined;
|
||||
tags: string[] = ["a", "b"];
|
||||
get secret(): string {
|
||||
return this.#secret;
|
||||
}
|
||||
}
|
||||
|
||||
class Stamp {
|
||||
at = 5;
|
||||
toJSON(key: string): string {
|
||||
return `stamp(${key})`;
|
||||
}
|
||||
}
|
||||
|
||||
class Base {
|
||||
kind = "base";
|
||||
toJSON(): { kind: string; via: string } {
|
||||
return { kind: this.kind, via: "Base.toJSON" };
|
||||
}
|
||||
}
|
||||
class Child extends Base {
|
||||
extra = 1;
|
||||
}
|
||||
|
||||
class Labeled extends Point {
|
||||
name = "p";
|
||||
}
|
||||
|
||||
class Box<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class Registry {
|
||||
entries = new Map<string, number>([["a", 1]]);
|
||||
count = 2;
|
||||
}
|
||||
|
||||
class Cycle {
|
||||
name = "loop";
|
||||
next: Cycle | null = null;
|
||||
}
|
||||
|
||||
class Big {
|
||||
n = 10n;
|
||||
}
|
||||
|
||||
const tag = Symbol("tag");
|
||||
|
||||
class Handlers {
|
||||
name = "h";
|
||||
onEvent = (n: number): number => n + 1;
|
||||
payload: unknown = { a: [1, "two", null] };
|
||||
[tag] = "symbol-keyed";
|
||||
}
|
||||
|
||||
class Failure extends Error {
|
||||
code = "E_FAIL";
|
||||
}
|
||||
|
||||
class Empty {}
|
||||
|
||||
class Tagged {
|
||||
s = new Set<number>([1, 2]);
|
||||
count = 2;
|
||||
}
|
||||
|
||||
const p = new Point(3, 4);
|
||||
console.log(JSON.stringify(p));
|
||||
console.log(JSON.stringify(new Account()));
|
||||
console.log(JSON.stringify(new Stamp()));
|
||||
console.log(JSON.stringify({ first: new Stamp(), list: [new Stamp()] }));
|
||||
console.log(JSON.stringify(new Child()));
|
||||
console.log(JSON.stringify(new Labeled(1, 2)));
|
||||
console.log(JSON.stringify(new Box("text")), JSON.stringify(new Box(new Point(0, 1))));
|
||||
console.log(JSON.stringify(new Registry()));
|
||||
console.log(JSON.stringify([p, null, new Point(5, 6)]));
|
||||
console.log(JSON.stringify({ point: p, points: [p, p] }, null, 2));
|
||||
console.log(JSON.stringify(p, null, "--"));
|
||||
console.log(JSON.stringify(p, ["x"]));
|
||||
console.log(JSON.stringify(p, (key: string, value: unknown) => (key === "y" ? undefined : value)));
|
||||
|
||||
const maybe: Point | null = Math.random() < 2 ? p : null;
|
||||
console.log(JSON.stringify(maybe));
|
||||
|
||||
const loop = new Cycle();
|
||||
loop.next = loop;
|
||||
try {
|
||||
JSON.stringify(loop);
|
||||
console.log("no error");
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError);
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.stringify(new Big());
|
||||
console.log("no error");
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError);
|
||||
}
|
||||
|
||||
p.x = 30;
|
||||
console.log(JSON.stringify(p));
|
||||
|
||||
const h = new Handlers();
|
||||
console.log(JSON.stringify(h));
|
||||
console.log(JSON.stringify(new Empty()));
|
||||
console.log(JSON.stringify(new Failure("boom")));
|
||||
h.payload = 7;
|
||||
console.log(JSON.stringify({ handlers: [h] }));
|
||||
const u: unknown = new Map([["a", 1]]);
|
||||
console.log(JSON.stringify(new Tagged()), JSON.stringify(u));
|
||||
|
||||
// Date fields write their toJSON text, also in nested instances.
|
||||
class Schedule {
|
||||
name = "weekly";
|
||||
next = new Date(Date.UTC(2024, 0, 1));
|
||||
}
|
||||
class Calendar {
|
||||
title = "team";
|
||||
schedules: Schedule[] = [new Schedule()];
|
||||
}
|
||||
console.log(JSON.stringify(new Schedule()));
|
||||
console.log(JSON.stringify({ calendar: new Calendar() }));
|
||||
@@ -0,0 +1,49 @@
|
||||
class Box {
|
||||
constructor(v) {
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
class Plain {}
|
||||
|
||||
class Late {
|
||||
constructor() {
|
||||
this.a = 1;
|
||||
}
|
||||
setB() {
|
||||
this.b = 2;
|
||||
}
|
||||
toString() {
|
||||
return "late";
|
||||
}
|
||||
}
|
||||
|
||||
class Shaped {
|
||||
constructor(n) {
|
||||
this.n = n;
|
||||
}
|
||||
toJSON() {
|
||||
return { shaped: this.n };
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Box(1);
|
||||
console.log(JSON.stringify(b));
|
||||
b.extra = "e";
|
||||
console.log(JSON.stringify(b));
|
||||
|
||||
const q = new Plain();
|
||||
console.log(JSON.stringify(q));
|
||||
q.a = 1;
|
||||
q.inner = new Box(2);
|
||||
q.list = [new Box(3), null];
|
||||
console.log(JSON.stringify(q));
|
||||
console.log(JSON.stringify([q, b], null, 1));
|
||||
|
||||
const late = new Late();
|
||||
console.log(JSON.stringify(late));
|
||||
late.c = 3;
|
||||
late.setB();
|
||||
console.log(JSON.stringify(late));
|
||||
|
||||
console.log(JSON.stringify({ s: new Shaped(4), arr: [new Shaped(5)] }));
|
||||
@@ -0,0 +1,89 @@
|
||||
// Iterator helpers on untyped iterators in JavaScript: the receivers come
|
||||
// from parameters without inferred types, so the helpers run on the
|
||||
// runtime's iterator objects.
|
||||
const log = [];
|
||||
function flush(what) {
|
||||
console.log(what, "|", log.join(" "));
|
||||
log.length = 0;
|
||||
}
|
||||
function values(xs) {
|
||||
return xs.values();
|
||||
}
|
||||
function keysOf(m) {
|
||||
return m.keys();
|
||||
}
|
||||
function charsOf(s) {
|
||||
return s[Symbol.iterator]();
|
||||
}
|
||||
|
||||
// Stages and consuming methods.
|
||||
const evens = values([1, 2, 3, 4, 6]).filter((x) => x % 2 === 0);
|
||||
console.log(evens.next(), evens.take(1).toArray(), [...evens]);
|
||||
console.log(values([1, 2, 3]).map((x, i) => x * 10 + i).toArray(), values([]).map((x) => x).toArray());
|
||||
console.log(values([1, 2, 3]).reduce((a, b) => a + b), values([1, 2]).reduce((a, b, i) => a + b * i, 100));
|
||||
console.log(values([5, 6, 7]).drop(1).toArray(), values([5, 6, 7]).take(Infinity).toArray());
|
||||
console.log(values(new Set([1, 3, 5])).find((x) => x > 2), values(new Set()).find(() => true));
|
||||
console.log(values([1, 2, 3]).some((x) => x === 2), values([1, 2, 3]).every((x) => x < 3));
|
||||
console.log(keysOf(new Map([["a", 1], ["b", 2]])).map((k) => k.toUpperCase()).toArray());
|
||||
console.log(charsOf("a-b-c").filter((c) => c !== "-").toArray().join(""));
|
||||
const seen = [];
|
||||
values(["p", "q"]).forEach((v, i) => seen.push(v + i));
|
||||
console.log(seen.join(","));
|
||||
|
||||
// Helper objects.
|
||||
const doubled = values([1, 2, 3]).map((x) => x * 2);
|
||||
console.log(typeof doubled.next, typeof doubled.return, typeof doubled.map, typeof doubled.throw);
|
||||
console.log(doubled.next(), doubled.return(), doubled.next());
|
||||
for (const x of values([1, 2, 3]).drop(1)) log.push(`of${x}`);
|
||||
flush("for...of");
|
||||
console.log([...values([4, 5]).flatMap((x) => [x, x])]);
|
||||
console.log(Object.prototype.toString.call(doubled), String(doubled));
|
||||
console.log(doubled);
|
||||
let self = null;
|
||||
self = values([1, 2]).map((x) => {
|
||||
if (x === 2) {
|
||||
try {
|
||||
self.next();
|
||||
} catch (e) {
|
||||
log.push(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
return x;
|
||||
});
|
||||
flush(self.toArray());
|
||||
|
||||
// flatMap closes an inner iterator it leaves early.
|
||||
function inner(label) {
|
||||
let n = 0;
|
||||
return {
|
||||
next() {
|
||||
n++;
|
||||
return { value: `${label}${n}`, done: n > 3 };
|
||||
},
|
||||
return() {
|
||||
log.push(`${label}:closed`);
|
||||
return { done: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
flush(values([1, 2]).flatMap((x) => inner(`i${x}`)).take(2).toArray());
|
||||
const stepping = values([1]).flatMap(() => inner("j"));
|
||||
stepping.next();
|
||||
flush([stepping.return(), stepping.next()]);
|
||||
|
||||
// Node's errors for invalid arguments.
|
||||
function attempt(f) {
|
||||
try {
|
||||
console.log(f());
|
||||
} catch (e) {
|
||||
console.log(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
attempt(() => values([1]).take("x"));
|
||||
attempt(() => values([1]).drop(-1));
|
||||
attempt(() => values([1]).map(5));
|
||||
attempt(() => values([1]).filter(null));
|
||||
attempt(() => values([1]).reduce(undefined));
|
||||
attempt(() => values([1]).forEach("f"));
|
||||
attempt(() => values([]).reduce((a, b) => a));
|
||||
attempt(() => values([1, 2]).flatMap((x) => x).toArray());
|
||||
@@ -0,0 +1,60 @@
|
||||
// `await` on a maybe-promise union (`T | Promise<T>`): the promise arm
|
||||
// resumes one turn after it settles, every other arm is a non-thenable that
|
||||
// resumes after exactly one microtask turn with the value itself. A ticker
|
||||
// chain interleaves with each await so the turn counts are observable.
|
||||
|
||||
type MaybeString = string | Promise<string>;
|
||||
|
||||
const log: string[] = [];
|
||||
let tick = 0;
|
||||
function ticker(turns: number): void {
|
||||
if (turns === 0) return;
|
||||
Promise.resolve().then(() => {
|
||||
tick++;
|
||||
log.push("T" + tick);
|
||||
ticker(turns - 1);
|
||||
});
|
||||
}
|
||||
|
||||
const settled: Promise<string> = Promise.resolve("settled");
|
||||
function choose(kind: number): MaybeString {
|
||||
if (kind === 0) return "plain";
|
||||
if (kind === 1) return settled;
|
||||
// Settles two turns from now.
|
||||
return new Promise<string>((resolve) => {
|
||||
Promise.resolve().then(() => resolve("late"));
|
||||
});
|
||||
}
|
||||
|
||||
async function awaitOnce(kind: number): Promise<void> {
|
||||
log.push("start" + kind);
|
||||
const value = await choose(kind);
|
||||
log.push("got" + kind + ":" + value);
|
||||
}
|
||||
|
||||
async function awaitTwice(): Promise<void> {
|
||||
const first = await choose(0);
|
||||
const second = await choose(1);
|
||||
log.push("twice:" + first + "+" + second);
|
||||
}
|
||||
|
||||
// A union with unit arms beside the data arm: `undefined` is a
|
||||
// non-thenable too.
|
||||
type MaybeCount = number | undefined | Promise<number | undefined>;
|
||||
function count(kind: number): MaybeCount {
|
||||
return kind === 0 ? 7 : kind === 1 ? undefined : Promise.resolve(kind === 2 ? 9 : undefined);
|
||||
}
|
||||
async function counts(): Promise<void> {
|
||||
for (let kind = 0; kind < 4; kind++) {
|
||||
const value = await count(kind);
|
||||
log.push("count" + kind + ":" + String(value));
|
||||
}
|
||||
}
|
||||
|
||||
ticker(12);
|
||||
awaitOnce(0);
|
||||
awaitOnce(1);
|
||||
awaitOnce(2);
|
||||
awaitTwice();
|
||||
counts();
|
||||
setTimeout(() => console.log(log.join(" ")), 5);
|
||||
@@ -0,0 +1,77 @@
|
||||
// Handlers typed `Promise<R | void> | R | void` (an extension event
|
||||
// handler) and members typed `(p: string) => boolean | Promise<boolean>`
|
||||
// (a tool's filesystem operations): sync and async implementations share
|
||||
// one slot, and the caller awaits whatever comes back.
|
||||
|
||||
interface Ops {
|
||||
exists: (absolutePath: string) => boolean | Promise<boolean>;
|
||||
}
|
||||
const syncOps: Ops = { exists: (p) => p.length > 3 };
|
||||
const asyncOps: Ops = { exists: async (p) => p.startsWith("/") };
|
||||
|
||||
interface Result {
|
||||
block: boolean;
|
||||
reason: string;
|
||||
}
|
||||
type Handler = (n: number) => Promise<Result | void> | Result | void;
|
||||
|
||||
const handlers: Handler[] = [];
|
||||
handlers.push((n) => {
|
||||
if (n > 5) return { block: true, reason: "sync big" };
|
||||
});
|
||||
handlers.push(async (n) => {
|
||||
if (n === 3) return { block: true, reason: "async three" };
|
||||
});
|
||||
handlers.push((n) => (n === 4 ? Promise.resolve({ block: false, reason: "promise four" }) : undefined));
|
||||
|
||||
// Named functions flow into the slot through their declared types.
|
||||
async function asyncNamed(n: number): Promise<Result> {
|
||||
return { block: n === 2, reason: "async named" };
|
||||
}
|
||||
function syncNamed(n: number): Result {
|
||||
return { block: n === 1, reason: "sync named" };
|
||||
}
|
||||
function voidNamed(n: number): void {
|
||||
console.log("void handler saw", n);
|
||||
}
|
||||
async function asyncVoid(n: number): Promise<void> {
|
||||
console.log("async void handler saw", n);
|
||||
}
|
||||
const named: Handler[] = [asyncNamed, syncNamed, voidNamed, asyncVoid];
|
||||
|
||||
async function run(list: Handler[], n: number): Promise<string> {
|
||||
for (const h of list) {
|
||||
const r = await h(n);
|
||||
if (r && r.block) return r.reason;
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
// A class-instance arm beside null and the promise arm.
|
||||
class Response {
|
||||
status: number;
|
||||
constructor(status: number) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
type Route = (path: string) => Response | null | Promise<Response | null>;
|
||||
const routes: Route[] = [
|
||||
(path) => (path === "/sync" ? new Response(200) : null),
|
||||
async (path) => (path === "/async" ? new Response(202) : null),
|
||||
];
|
||||
async function route(path: string): Promise<string> {
|
||||
for (const r of routes) {
|
||||
const res = await r(path);
|
||||
if (res instanceof Response) return path + " " + res.status;
|
||||
}
|
||||
return path + " 404";
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(await syncOps.exists("/tmp/x"), await syncOps.exists("/a"));
|
||||
console.log(await asyncOps.exists("/tmp/x"), await asyncOps.exists("tmp"));
|
||||
for (const n of [1, 3, 4, 7]) console.log(n, await run(handlers, n));
|
||||
for (const n of [1, 2, 9]) console.log(n, await run(named, n));
|
||||
for (const path of ["/sync", "/async", "/missing"]) console.log(await route(path));
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,77 @@
|
||||
// Rejections through maybe-promise values: a rejected promise arm throws at
|
||||
// the await, a synchronous throw never reaches the await, an async return
|
||||
// adopts the promise arm (so its rejection settles the caller's promise
|
||||
// after the function's own handlers), and a `Promise<void> | void`
|
||||
// callback's promise is adopted by an async function returning it.
|
||||
|
||||
type MaybeBool = boolean | Promise<boolean>;
|
||||
function check(kind: number): MaybeBool {
|
||||
if (kind === 0) return true;
|
||||
if (kind === 1) return Promise.reject(new Error("rejected " + kind));
|
||||
if (kind === 2) throw new Error("thrown " + kind);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
async function awaitEach(): Promise<void> {
|
||||
for (let kind = 0; kind < 4; kind++) {
|
||||
try {
|
||||
console.log(kind, await check(kind));
|
||||
} catch (e) {
|
||||
console.log(kind, "caught", (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `return check(1)` inside try/catch/finally: JS completes the return first
|
||||
// (the finally runs), and only then does the async function's promise adopt
|
||||
// the rejected arm, so this function's own catch never sees it.
|
||||
async function returnInTry(kind: number): Promise<boolean> {
|
||||
try {
|
||||
return check(kind);
|
||||
} catch (e) {
|
||||
console.log("inner catch", (e as Error).message);
|
||||
return false;
|
||||
} finally {
|
||||
console.log("finally", kind);
|
||||
}
|
||||
}
|
||||
|
||||
type Callback = () => Promise<void> | void;
|
||||
const callbacks: Callback[] = [
|
||||
() => {
|
||||
console.log("sync callback");
|
||||
},
|
||||
async () => {
|
||||
throw new Error("async callback failed");
|
||||
},
|
||||
];
|
||||
async function runCallback(cb: Callback): Promise<void> {
|
||||
return cb();
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await awaitEach();
|
||||
for (const kind of [0, 1, 2]) {
|
||||
try {
|
||||
console.log("returned", await returnInTry(kind));
|
||||
} catch (e) {
|
||||
console.log("outer catch", (e as Error).message);
|
||||
}
|
||||
}
|
||||
for (const cb of callbacks) {
|
||||
try {
|
||||
await runCallback(cb);
|
||||
console.log("callback ok");
|
||||
} catch (e) {
|
||||
console.log("callback caught", (e as Error).message);
|
||||
}
|
||||
}
|
||||
// A then handler returning a rejected promise arm rejects the chain.
|
||||
await Promise.resolve(1)
|
||||
.then(() => check(1))
|
||||
.then(
|
||||
(v) => console.log("then value", v),
|
||||
(e: Error) => console.log("then rejected", e.message),
|
||||
);
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,61 @@
|
||||
// Telling the arms of a maybe-promise union apart: `instanceof Promise`,
|
||||
// the `then` probe through a cast, typeof, truthiness, equality, and
|
||||
// Promise.resolve (which hands a promise back as is and wraps a value).
|
||||
|
||||
type MaybeString = string | Promise<string>;
|
||||
function make(i: number): MaybeString {
|
||||
return i % 2 === 1 ? "s" + i : Promise.resolve("p" + i);
|
||||
}
|
||||
|
||||
interface Box {
|
||||
v: number;
|
||||
}
|
||||
type MaybeBox = Box | Promise<Box>;
|
||||
function box(i: number): MaybeBox {
|
||||
return i % 2 === 0 ? { v: i } : Promise.resolve({ v: i * 10 });
|
||||
}
|
||||
function isThenable(x: MaybeBox): boolean {
|
||||
return typeof (x as any).then === "function";
|
||||
}
|
||||
|
||||
type Slot = { name: string; value: string | Promise<string> | null };
|
||||
const slots: Slot[] = [
|
||||
{ name: "a", value: "x" },
|
||||
{ name: "b", value: Promise.resolve("y") },
|
||||
{ name: "c", value: null },
|
||||
];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const v = make(i);
|
||||
if (v instanceof Promise) console.log(i, "promise", await v);
|
||||
else console.log(i, "value", v.toUpperCase());
|
||||
console.log(typeof v);
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const b = box(i);
|
||||
console.log(i, isThenable(b), typeof (b as Promise<Box>).then);
|
||||
if ((b as Promise<Box>).then) console.log("has then");
|
||||
console.log((await b).v);
|
||||
}
|
||||
for (const s of slots) {
|
||||
const v = await s.value;
|
||||
console.log(s.name, v === null ? "null" : v, s.value === null, s.value instanceof Promise, typeof s.value);
|
||||
if (s.value) console.log("truthy", s.name);
|
||||
}
|
||||
console.log(slots[1]!.value === slots[1]!.value);
|
||||
|
||||
// Promise.resolve keeps a promise's identity and wraps a value.
|
||||
const p = Promise.resolve(7);
|
||||
const get = (b: boolean): number | Promise<number> => (b ? p : 3);
|
||||
const fromPromise = Promise.resolve(get(true));
|
||||
const fromValue = Promise.resolve(get(false));
|
||||
console.log(fromPromise === p, fromValue === p);
|
||||
const order: string[] = [];
|
||||
fromPromise.then((v) => order.push("promise" + v));
|
||||
fromValue.then((v) => order.push("value" + v));
|
||||
Promise.resolve().then(() => order.push("other"));
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
||||
console.log(order.join(","));
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,61 @@
|
||||
// Returning a maybe-promise value from async code flattens it: the
|
||||
// promise arm is adopted (the extra then-job turns of a returned promise),
|
||||
// any other arm fulfills directly. Covers block bodies, concise arrow
|
||||
// bodies, `return await`, and then handlers.
|
||||
|
||||
type MaybeString = string | Promise<string>;
|
||||
const settled: Promise<string> = Promise.resolve("settled");
|
||||
function choose(kind: number): MaybeString {
|
||||
if (kind === 0) return "plain";
|
||||
if (kind === 1) return settled;
|
||||
return new Promise<string>((resolve) => setTimeout(() => resolve("timer"), 1));
|
||||
}
|
||||
|
||||
async function viaReturn(kind: number): Promise<string> {
|
||||
return choose(kind);
|
||||
}
|
||||
async function viaReturnAwait(kind: number): Promise<string> {
|
||||
return await choose(kind);
|
||||
}
|
||||
const viaArrow = async (kind: number): Promise<string> => choose(kind);
|
||||
|
||||
const log: string[] = [];
|
||||
let tick = 0;
|
||||
function ticker(turns: number): void {
|
||||
if (turns === 0) return;
|
||||
Promise.resolve().then(() => {
|
||||
tick++;
|
||||
log.push("T" + tick);
|
||||
ticker(turns - 1);
|
||||
});
|
||||
}
|
||||
|
||||
// A then handler returning a maybe-promise value: a cache that answers
|
||||
// synchronously once loaded.
|
||||
const cache = new Map<string, string>();
|
||||
function load(key: string): string | Promise<string> {
|
||||
const hit = cache.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
return new Promise<string>((resolve) =>
|
||||
setTimeout(() => {
|
||||
cache.set(key, key.toUpperCase());
|
||||
resolve(key.toUpperCase());
|
||||
}, 1),
|
||||
);
|
||||
}
|
||||
|
||||
ticker(10);
|
||||
for (let kind = 0; kind < 3; kind++) {
|
||||
viaReturn(kind).then((v) => log.push("ret" + kind + ":" + v));
|
||||
viaReturnAwait(kind).then((v) => log.push("await" + kind + ":" + v));
|
||||
viaArrow(kind).then((v) => log.push("arrow" + kind + ":" + v));
|
||||
}
|
||||
const start = Promise.resolve("k");
|
||||
start
|
||||
.then((key) => load(key))
|
||||
.then((v) => {
|
||||
log.push("first:" + v);
|
||||
return start.then((key) => load(key));
|
||||
})
|
||||
.then((v) => log.push("second:" + v));
|
||||
setTimeout(() => console.log(log.join(" ")), 20);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Rest parameters typed `unknown[]` or `any[]` in function types: the
|
||||
// handler registry of an extension host, with forwarding wrappers and
|
||||
// spread calls.
|
||||
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
const handlers = new Map<string, HandlerFn[]>();
|
||||
function on(event: string, handler: HandlerFn): () => void {
|
||||
const registered: HandlerFn = (...args) => handler(...args);
|
||||
const list = handlers.get(event) ?? [];
|
||||
list.push(registered);
|
||||
handlers.set(event, list);
|
||||
return () => {
|
||||
const current = handlers.get(event);
|
||||
if (!current) return;
|
||||
const index = current.indexOf(registered);
|
||||
if (index !== -1) current.splice(index, 1);
|
||||
};
|
||||
}
|
||||
async function emit(event: string, ...args: unknown[]): Promise<unknown[]> {
|
||||
const out: unknown[] = [];
|
||||
for (const h of handlers.get(event) ?? []) out.push(await h(...args));
|
||||
return out;
|
||||
}
|
||||
|
||||
on("x", async (...args) => args.length);
|
||||
const off = on("x", async (...args: unknown[]) => typeof args[0] + ":" + String(args[1]));
|
||||
on("y", async (...args) => "no args " + args.length);
|
||||
|
||||
function sum(...nums: unknown[]): number {
|
||||
let total = 0;
|
||||
for (const n of nums) if (typeof n === "number") total += n;
|
||||
return total;
|
||||
}
|
||||
const stored: (...values: unknown[]) => number = sum;
|
||||
const logAll: (...values: any[]) => void = (...values: any[]) => console.log(values.length, values[0]);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(JSON.stringify(await emit("x", 1, "two", true)));
|
||||
off();
|
||||
console.log(JSON.stringify(await emit("x", 5)));
|
||||
console.log(JSON.stringify(await emit("y")));
|
||||
console.log(sum(1, 2, "x", 4), stored(5, 6), stored());
|
||||
logAll("p", "q");
|
||||
logAll();
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,52 @@
|
||||
// Functions as Map values, Map keys and Set elements: factory and listener
|
||||
// registries keep each closure's identity, and a handler that captures its
|
||||
// own registry is collected with it.
|
||||
|
||||
interface Extension {
|
||||
name: string;
|
||||
run(x: number): number;
|
||||
}
|
||||
type ExtensionFactory = (api: { log: (s: string) => void }) => Extension;
|
||||
const factories = new Map<string, ExtensionFactory>();
|
||||
factories.set("double", (api) => ({
|
||||
name: "double",
|
||||
run: (x) => {
|
||||
api.log("double " + x);
|
||||
return x * 2;
|
||||
},
|
||||
}));
|
||||
factories.set("inc", () => ({ name: "inc", run: (x) => x + 1 }));
|
||||
const lines: string[] = [];
|
||||
const api = { log: (s: string) => lines.push(s) };
|
||||
for (const [key, make] of factories) {
|
||||
const ext = make(api);
|
||||
console.log(key, ext.name, ext.run(10));
|
||||
}
|
||||
console.log(factories.get("missing") === undefined, typeof factories.get("inc"), factories.has("inc"));
|
||||
const inc = factories.get("inc");
|
||||
if (inc) console.log(inc(api).run(41));
|
||||
factories.delete("inc");
|
||||
console.log([...factories.keys()].join(","), factories.size, lines.join(";"));
|
||||
|
||||
type Listener = (v: string) => void;
|
||||
const listeners = new Set<Listener>();
|
||||
const a: Listener = (v) => console.log("a", v);
|
||||
const b: Listener = (v) => console.log("b", v);
|
||||
listeners.add(a);
|
||||
listeners.add(b);
|
||||
listeners.add(a);
|
||||
console.log(listeners.size, listeners.has(a), listeners.has((v) => console.log(v)));
|
||||
for (const l of listeners) l("x");
|
||||
listeners.delete(a);
|
||||
listeners.forEach((l) => l("y"));
|
||||
|
||||
const counts = new Map<Listener, number>();
|
||||
counts.set(a, 1);
|
||||
counts.set(b, 2);
|
||||
counts.set(a, 3);
|
||||
console.log(counts.size, counts.get(a), counts.get(b));
|
||||
|
||||
const registry = new Map<string, () => number>();
|
||||
registry.set("self", () => registry.size);
|
||||
registry.set("double", () => registry.size * 2);
|
||||
console.log([...registry.values()].map((f) => f()).join(","));
|
||||
@@ -0,0 +1,35 @@
|
||||
// The Next.js proxy signature: `(request, event) => NextMiddlewareResult |
|
||||
// Promise<NextMiddlewareResult>` where the result is a response class, null,
|
||||
// undefined or void. Sync and async proxies run through one host.
|
||||
|
||||
class NextResponse {
|
||||
status: number;
|
||||
constructor(status: number) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
type NextMiddlewareResult = NextResponse | null | undefined | void;
|
||||
type NextProxy = (request: string, event: number) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
|
||||
|
||||
const proxy: NextProxy = (request) => {
|
||||
if (request === "/health") return new NextResponse(200);
|
||||
if (request === "/async") return Promise.resolve(new NextResponse(202));
|
||||
if (request === "/null") return null;
|
||||
};
|
||||
const asyncProxy: NextProxy = async (request) => (request === "/a" ? new NextResponse(201) : undefined);
|
||||
|
||||
async function handle(p: NextProxy, request: string): Promise<string> {
|
||||
const result = await p(request, 0);
|
||||
return result ? String(result.status) : String(result);
|
||||
}
|
||||
|
||||
// The repro shape: Promise.resolve over the handler's result.
|
||||
type Handler = (n: number) => NextResponse | null | Promise<NextResponse | null>;
|
||||
const h: Handler = () => new NextResponse(204);
|
||||
Promise.resolve(h(1)).then((r) => console.log(r ? r.status : "none"));
|
||||
|
||||
async function main(): Promise<void> {
|
||||
for (const request of ["/health", "/async", "/null", "/other"]) console.log(request, await handle(proxy, request));
|
||||
for (const request of ["/a", "/b"]) console.log(request, await handle(asyncProxy, request));
|
||||
}
|
||||
main();
|
||||
@@ -5,9 +5,9 @@
|
||||
const byFlag = new Map<boolean, string>();
|
||||
console.log(byFlag);
|
||||
|
||||
// Map values have no slot for functions.
|
||||
const listeners = new Map<string, () => void>();
|
||||
console.log(listeners);
|
||||
// Map values have no slot for Dates.
|
||||
const stamps = new Map<string, Date>();
|
||||
console.log(stamps);
|
||||
|
||||
// Boolean Set elements remain outside the supported element domain.
|
||||
const flags = new Set<boolean>();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// JSON.stringify reads class instances through their checked-dynamic
|
||||
// property view. A class whose declared field holds a RegExp or a Promise,
|
||||
// which the runtime JSON traversal cannot write, is refused at compile
|
||||
// time, naming the field.
|
||||
class Pattern {
|
||||
name = "digits";
|
||||
source = /\d+/g;
|
||||
}
|
||||
|
||||
class Job {
|
||||
title = "fetch";
|
||||
pending = Promise.resolve(1);
|
||||
}
|
||||
|
||||
class Catalog {
|
||||
title = "team";
|
||||
patterns: Pattern[] = [new Pattern()];
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(new Pattern()));
|
||||
console.log(JSON.stringify(new Job()));
|
||||
console.log(JSON.stringify({ catalog: new Catalog() }));
|
||||
@@ -4,7 +4,7 @@
|
||||
// Boolean keys compile alongside the remaining value and method fences.
|
||||
const byFlag = new Map<boolean, string>();
|
||||
|
||||
// Values exclude functions (no closure story in the uniform value slot yet).
|
||||
// Function values compile (closures keep their identity in the slot).
|
||||
const handlers = new Map<string, () => void>();
|
||||
|
||||
// Typed nested Maps retain their identity and are supported.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Maybe-promise unions (`T | Promise<T>`) compile when the union tag can
|
||||
// answer what JS's await decides. These forms stay refused.
|
||||
|
||||
// A data arm that declares `then` is a thenable: await would adopt it.
|
||||
interface Thenish {
|
||||
then(cb: (v: number) => void): void;
|
||||
}
|
||||
function thenable(): Thenish | Promise<number> {
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
console.log(typeof thenable);
|
||||
|
||||
// Two promise arms: the union tag cannot tell them apart by instanceof.
|
||||
function twoPromises(): Promise<number> | Promise<string> | boolean {
|
||||
return true;
|
||||
}
|
||||
console.log(typeof twoPromises);
|
||||
|
||||
// Promises have no inspect lowering, so neither does a union holding one.
|
||||
function maybe(b: boolean): string | Promise<string> {
|
||||
return b ? "s" : Promise.resolve("p");
|
||||
}
|
||||
console.log(maybe(true));
|
||||
|
||||
// Promise.resolve hands the promise arm back as is, so the result type
|
||||
// must be that arm's own promise type.
|
||||
const widened = Promise.resolve<string | number>(maybe(false));
|
||||
console.log(typeof widened);
|
||||
|
||||
// `instanceof Promise` on a value that is never a promise.
|
||||
const record = { v: 1 };
|
||||
console.log(record instanceof Promise);
|
||||
|
||||
// A variadic `unknown[]` slot takes closures with the same variadic ABI.
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
const fixed: HandlerFn = async (first: unknown) => first;
|
||||
fixed(1, 2).then((v) => console.log(v));
|
||||
@@ -1,27 +1,27 @@
|
||||
component-fences.ts:9:7 - error SC2009: values of type 'Map<string, () => void>' cannot be compiled: the Map shape is supported, but '() => void' values have no Map slot yet
|
||||
component-fences.ts:9:7 - error SC2009: values of type 'Map<string, Date>' cannot be compiled: the Map shape is supported, but 'Date' values have no Map slot yet
|
||||
|
||||
8 | // Map values have no slot for functions.
|
||||
9 | const listeners = new Map<string, () => void>();
|
||||
| ^~~~~~~~~
|
||||
10 | console.log(listeners);
|
||||
8 | // Map values have no slot for Dates.
|
||||
9 | const stamps = new Map<string, Date>();
|
||||
| ^~~~~~
|
||||
10 | console.log(stamps);
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
component-fences.ts:9:19 - error SC1090: Map values of type '() => void' (Map values must be number, string, boolean, records, class instances, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet
|
||||
component-fences.ts:9:16 - error SC1090: Map values of type 'Date' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet
|
||||
|
||||
8 | // Map values have no slot for functions.
|
||||
9 | const listeners = new Map<string, () => void>();
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
10 | console.log(listeners);
|
||||
8 | // Map values have no slot for Dates.
|
||||
9 | const stamps = new Map<string, Date>();
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~
|
||||
10 | console.log(stamps);
|
||||
|
||||
component-fences.ts:10:13 - error SC2004: uses of 'listeners' inherit the blocker on its declaration
|
||||
component-fences.ts:10:13 - error SC2004: uses of 'stamps' inherit the blocker on its declaration
|
||||
|
||||
9 | const listeners = new Map<string, () => void>();
|
||||
10 | console.log(listeners);
|
||||
| ^~~~~~~~~
|
||||
9 | const stamps = new Map<string, Date>();
|
||||
10 | console.log(stamps);
|
||||
| ^~~~~~
|
||||
11 |
|
||||
|
||||
hint: the declaration of 'listeners' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
hint: the declaration of 'stamps' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
component-fences.ts:35:7 - error SC2009: values of type '() => WeakRef<object>' cannot be compiled: the function shape is supported, but its return type 'WeakRef<object>' does not compile
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
json-class-fields.ts:20:28 - error SC1090: JSON.stringify of 'Pattern' values (field 'source' of class 'Pattern' has type 'RegExp', which the runtime JSON traversal cannot write) are not supported yet
|
||||
|
||||
19 |
|
||||
20 | console.log(JSON.stringify(new Pattern()));
|
||||
| ^~~~~~~~~~~~~
|
||||
21 | console.log(JSON.stringify(new Job()));
|
||||
|
||||
json-class-fields.ts:21:28 - error SC1090: JSON.stringify of 'Job' values (field 'pending' of class 'Job' has type 'Promise<number>', which the runtime JSON traversal cannot write) are not supported yet
|
||||
|
||||
20 | console.log(JSON.stringify(new Pattern()));
|
||||
21 | console.log(JSON.stringify(new Job()));
|
||||
| ^~~~~~~~~
|
||||
22 | console.log(JSON.stringify({ catalog: new Catalog() }));
|
||||
|
||||
json-class-fields.ts:22:28 - error SC1090: JSON.stringify of '{ catalog: Catalog }' values (field 'source' of class 'Pattern' has type 'RegExp', which the runtime JSON traversal cannot write) are not supported yet
|
||||
|
||||
21 | console.log(JSON.stringify(new Job()));
|
||||
22 | console.log(JSON.stringify({ catalog: new Catalog() }));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
23 |
|
||||
@@ -1,19 +1,3 @@
|
||||
maps.ts:8:7 - error SC2009: values of type 'Map<string, () => void>' cannot be compiled: the Map shape is supported, but '() => void' values have no Map slot yet
|
||||
|
||||
7 | // Values exclude functions (no closure story in the uniform value slot yet).
|
||||
8 | const handlers = new Map<string, () => void>();
|
||||
| ^~~~~~~~
|
||||
9 |
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
maps.ts:8:18 - error SC1090: Map values of type '() => void' (Map values must be number, string, boolean, records, class instances, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet
|
||||
|
||||
7 | // Values exclude functions (no closure story in the uniform value slot yet).
|
||||
8 | const handlers = new Map<string, () => void>();
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
9 |
|
||||
|
||||
maps.ts:29:28 - error SC1090: JSON.stringify of 'Map<string, number>' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet
|
||||
|
||||
28 | const m = new Map<string, number>();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
maybe-promise-fences.ts:8:10 - error SC2009: values of type 'Promise<number> | Thenish' cannot be compiled: the union shape is supported, but its arm 'Thenish' declares 'then' beside a promise arm, so 'await' would have to adopt it as a thenable (a maybe-promise union's data arms must not be thenable)
|
||||
|
||||
7 | }
|
||||
8 | function thenable(): Thenish | Promise<number> {
|
||||
| ^~~~~~~~
|
||||
9 | return Promise.resolve(1);
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
maybe-promise-fences.ts:11:20 - error SC2004: uses of 'thenable' inherit the blocker on its declaration
|
||||
|
||||
10 | }
|
||||
11 | console.log(typeof thenable);
|
||||
| ^~~~~~~~
|
||||
12 |
|
||||
|
||||
hint: the declaration of 'thenable' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
maybe-promise-fences.ts:14:10 - error SC2009: values of type 'boolean | Promise<string> | Promise<number>' cannot be compiled: the union shape is supported, but it has several promise arms ('Promise<string>' and 'Promise<number>'), and a compiled union holds at most one (the union tag is the 'instanceof Promise' test)
|
||||
|
||||
13 | // Two promise arms: the union tag cannot tell them apart by instanceof.
|
||||
14 | function twoPromises(): Promise<number> | Promise<string> | boolean {
|
||||
| ^~~~~~~~~~~
|
||||
15 | return true;
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
maybe-promise-fences.ts:17:20 - error SC2004: uses of 'twoPromises' inherit the blocker on its declaration
|
||||
|
||||
16 | }
|
||||
17 | console.log(typeof twoPromises);
|
||||
| ^~~~~~~~~~~
|
||||
18 |
|
||||
|
||||
hint: the declaration of 'twoPromises' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
maybe-promise-fences.ts:23:13 - error SC1090: console.log of 'Promise<string> | string' values ('Promise<string>' values have no inspect lowering) are not supported yet
|
||||
|
||||
22 | }
|
||||
23 | console.log(maybe(true));
|
||||
| ^~~~~~~~~~~
|
||||
24 |
|
||||
|
||||
maybe-promise-fences.ts:27:50 - error SC2020: 'Promise.resolve over 'Promise<string> | string' at this result type' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
26 | // must be that arm's own promise type.
|
||||
27 | const widened = Promise.resolve<string | number>(maybe(false));
|
||||
| ^~~~~~~~~~~~
|
||||
28 | console.log(typeof widened);
|
||||
|
||||
hint: the promise arm passes through identically, so it must already be the result's promise type
|
||||
|
||||
maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: number }' values (only promises, promise-armed unions, and checked-dynamic values have a lowering) are not supported yet
|
||||
|
||||
31 | const record = { v: 1 };
|
||||
32 | console.log(record instanceof Promise);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
33 |
|
||||
|
||||
maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise<unknown>' values where '() => Promise<unknown>' is expected is not supported yet
|
||||
|
||||
35 | type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
36 | const fixed: HandlerFn = async (first: unknown) => first;
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
37 | fixed(1, 2).then((v) => console.log(v));
|
||||
@@ -1,44 +0,0 @@
|
||||
retired-engine-2251-tagged-templates-dyn.ts:6:15 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile
|
||||
|
||||
5 | // non-function tag throws Node's catchable TypeError).
|
||||
6 | function f(...args: any[]): string {
|
||||
| ^~~~
|
||||
7 | return args.length > 1
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
retired-engine-2251-tagged-templates-dyn.ts:11:13 - error SC2004: uses of 'f' inherit the blocker on its declaration
|
||||
|
||||
10 | }
|
||||
11 | console.log(f`a${1}b${2}c`);
|
||||
| ^
|
||||
12 | console.log(f`plain`);
|
||||
|
||||
hint: the declaration of 'f' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
retired-engine-2251-tagged-templates-dyn.ts:12:13 - error SC2004: uses of 'f' inherit the blocker on its declaration
|
||||
|
||||
11 | console.log(f`a${1}b${2}c`);
|
||||
12 | console.log(f`plain`);
|
||||
| ^
|
||||
13 |
|
||||
|
||||
hint: the declaration of 'f' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
retired-engine-2251-tagged-templates-dyn.ts:14:29 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile
|
||||
|
||||
13 |
|
||||
14 | function declare(x: any, ...ys: any[]): string {
|
||||
| ^~
|
||||
15 | return `${String(x)}|${ys.length}`;
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
retired-engine-2251-tagged-templates-dyn.ts:17:13 - error SC2004: uses of 'declare' inherit the blocker on its declaration
|
||||
|
||||
16 | }
|
||||
17 | console.log(declare`Hello ${0} world!`);
|
||||
| ^~~~~~~
|
||||
18 |
|
||||
|
||||
hint: the declaration of 'declare' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
retired-engine-2790-array-missing-tagged-substitutions.ts:11:25 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile
|
||||
|
||||
10 |
|
||||
11 | function dynamicRest(...args: any[]): string {
|
||||
| ^~~~
|
||||
12 | return `${args.length}:${String(args[1])}`;
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
retired-engine-2790-array-missing-tagged-substitutions.ts:14:13 - error SC2004: uses of 'dynamicRest' inherit the blocker on its declaration
|
||||
|
||||
13 | }
|
||||
14 | console.log(dynamicRest`a${values[9]}b`);
|
||||
| ^~~~~~~~~~~
|
||||
15 |
|
||||
|
||||
hint: the declaration of 'dynamicRest' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -2193,6 +2193,7 @@
|
||||
"5970-net-connect-host-name.ts": 2400,
|
||||
"5971-buffer-chunks-to-string.ts": 2700,
|
||||
"5972-net-set-encoding-listeners.ts": 2550,
|
||||
"5973-buffer-concat-untyped.js": 1900,
|
||||
"5973-large-closure-environment.ts": 4499,
|
||||
"5974-typeof-module-exports.cjs": 2000,
|
||||
"5975-zlib-class-values.ts": 2800,
|
||||
@@ -2208,6 +2209,8 @@
|
||||
"5995-array-dense-loop-boundaries.ts": 2596,
|
||||
"5996-rejections-between-macrotasks.cjs": 3034,
|
||||
"600-closures-basic.ts": 2349,
|
||||
"6000-json-stringify-class-instances.ts": 7567,
|
||||
"6001-json-stringify-js-class-instances.js": 4668,
|
||||
"601-closures-loops.ts": 1788,
|
||||
"602-closures-identity-recursion.ts": 2372,
|
||||
"6020-class-extends-function-constructor.js": 4300,
|
||||
@@ -2219,6 +2222,7 @@
|
||||
"6043-string-index-narrowed-union.ts": 2983,
|
||||
"605-closures-forward-capture-tdz.ts": 3277,
|
||||
"6051-generic-class-factory-results.ts": 4363,
|
||||
"6070-iterator-helpers-untyped.js": 4050,
|
||||
"6100-record-get-or-create.ts": 3771,
|
||||
"6101-record-get-or-create-unchecked/main.ts": 2107,
|
||||
"6103-typeof-builtin-bindings.cjs": 2202,
|
||||
@@ -5248,6 +5252,7 @@
|
||||
"5970-net-connect-host-name.ts": 2800,
|
||||
"5971-buffer-chunks-to-string.ts": 3100,
|
||||
"5972-net-set-encoding-listeners.ts": 3000,
|
||||
"5973-buffer-concat-untyped.js": 2900,
|
||||
"5973-large-closure-environment.ts": 5145,
|
||||
"5974-typeof-module-exports.cjs": 2500,
|
||||
"5975-zlib-class-values.ts": 4200,
|
||||
@@ -5263,6 +5268,8 @@
|
||||
"5995-array-dense-loop-boundaries.ts": 4171,
|
||||
"5996-rejections-between-macrotasks.cjs": 22172,
|
||||
"600-closures-basic.ts": 2534,
|
||||
"6000-json-stringify-class-instances.ts": 12107,
|
||||
"6001-json-stringify-js-class-instances.js": 7469,
|
||||
"601-closures-loops.ts": 2297,
|
||||
"602-closures-identity-recursion.ts": 2427,
|
||||
"6020-class-extends-function-constructor.js": 7500,
|
||||
@@ -5274,6 +5281,7 @@
|
||||
"6043-string-index-narrowed-union.ts": 3627,
|
||||
"605-closures-forward-capture-tdz.ts": 4638,
|
||||
"6051-generic-class-factory-results.ts": 46581,
|
||||
"6070-iterator-helpers-untyped.js": 6900,
|
||||
"6100-record-get-or-create.ts": 3173,
|
||||
"6101-record-get-or-create-unchecked/main.ts": 3228,
|
||||
"6103-typeof-builtin-bindings.cjs": 1796,
|
||||
|
||||
@@ -109,7 +109,6 @@ const KNOWN: Record<string, Known> = {
|
||||
"web-streams": { build: "SC2013" },
|
||||
"workspace-copied": { build: "SC2013" },
|
||||
"workspace-unclean": { build: "SC2013" },
|
||||
"zlib-shims": { run: "SC2020" },
|
||||
};
|
||||
|
||||
const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);
|
||||
|
||||
@@ -160,12 +160,14 @@ nativeToolchainTest(
|
||||
// Symbol absence is the primary reachability contract. Keep a deliberately
|
||||
// roomy, platform-specific hello-world ceiling too: it catches losing
|
||||
// section GC without pinning an exact linker/SDK byte count. The canonical
|
||||
// Linux build is about 64-68KB and current Mach-O builds are about 70KB.
|
||||
// ELF segments are page aligned, so the Linux size moves in 4 KiB steps
|
||||
// whenever .text crosses a page boundary. These limits leave several
|
||||
// native pages of linker-version slack while remaining far below the
|
||||
// former roughly-400KB always-linked runtime.
|
||||
const helloSizeLimit = process.platform === "linux" ? 80 * 1024 : 96 * 1024;
|
||||
// Linux build is about 84KB and current Mach-O builds are about 70KB. Most
|
||||
// of Linux hello's runtime code is the cycle collector, which its exit
|
||||
// path reaches (scr_lib_cleanup releases an array). ELF segments are page
|
||||
// aligned, so the Linux size moves in 4 KiB steps whenever .text crosses
|
||||
// a page boundary. These limits leave several native pages of
|
||||
// linker-version slack while remaining far below the former roughly-400KB
|
||||
// always-linked runtime.
|
||||
const helloSizeLimit = 96 * 1024;
|
||||
expect(statSync(hello.binaryPath).size).toBeLessThan(helloSizeLimit);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user