mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Merge origin/main into fix/pi-wp6b-stdlib-tail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,11 @@ declare module "node:fs" {
|
||||
/* The bare-encoding spelling — the options record's encoding key alone. */
|
||||
export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void;
|
||||
export function writeFileSync(path: string | URL, data: string | Uint8Array): void;
|
||||
/* The file-descriptor form: Node writes the whole data to an already
|
||||
* open descriptor and leaves it open (closeSync owns its lifetime, and
|
||||
* the write lands at the descriptor's current offset). */
|
||||
export function writeFileSync(fd: number, data: string): void;
|
||||
export function writeFileSync(fd: number, data: string, encoding: "utf8" | "utf-8"): void;
|
||||
export function appendFileSync(path: string | URL, data: string | Uint8Array): void;
|
||||
export function existsSync(path: string | URL): boolean;
|
||||
export function mkdirSync(path: string | URL): void;
|
||||
|
||||
@@ -67,6 +67,38 @@ console.log(total);
|
||||
expect(body).not.toContain("ScrArena.closed");
|
||||
expect(body).not.toContain("@scr_arena_disposal");
|
||||
}
|
||||
// A record literal stores every field, and no dispose entry reads them:
|
||||
// the record's constructor does not clear the object. A class constructor
|
||||
// may leave fields unset, so the class's does.
|
||||
const record = constructors.find((body) => body.includes("@sc_rnew_"))!;
|
||||
const cls = constructors.find((body) => !body.includes("@sc_rnew_"))!;
|
||||
expect(record).not.toContain("@llvm.memset");
|
||||
expect(cls).toContain("@llvm.memset");
|
||||
});
|
||||
|
||||
test("record literals in an arena that is not closed keep the clear their dispose entries need", async () => {
|
||||
// The items hold outside strings, so their groups are not closed.
|
||||
const llvm = await llvmFor(`
|
||||
interface Item {
|
||||
label: string;
|
||||
next: Item | null;
|
||||
}
|
||||
function chain(labels: string[], n: number): Item {
|
||||
return n === 0 ? { label: labels[0]!, next: null } : { label: labels[n % labels.length]!, next: chain(labels, n - 1) };
|
||||
}
|
||||
function length(item: Item): number {
|
||||
return item.next === null ? item.label.length : item.label.length + length(item.next);
|
||||
}
|
||||
const labels = ["a", "bb", "ccc"];
|
||||
let total = 0;
|
||||
for (let round = 0; round < 4; round++) total += length(chain(labels, 10 + round));
|
||||
console.log(total);
|
||||
`);
|
||||
expect(llvm).toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 0\)/);
|
||||
const items = arenaConstructors(llvm).filter((body) => body.includes("@sc_rnew_"));
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0]).toContain(".dispose");
|
||||
expect(items[0]).toContain("@llvm.memset");
|
||||
});
|
||||
|
||||
test("an arena that is not closed keeps the dispose list in every arena-aware constructor", async () => {
|
||||
|
||||
@@ -275,6 +275,12 @@ export function arenaCurrentAddress(host: ArenaHost, dest: string): string[] | n
|
||||
* the arena is full. The bump, the clear, the header and the dispose entry
|
||||
* are inline (scr_arena.c's ScrArena ABI); scr_arena_obj takes a new chunk
|
||||
* and scr_arena_disposal grows the list.
|
||||
*
|
||||
* `clear` is what the bump path zeroes: the whole object, or only the
|
||||
* collector header, for callers that store every word of the object before
|
||||
* anything reads it (a record literal of a shape with no dispose entries:
|
||||
* `init` stores the count and the shape number, and the literal every
|
||||
* field). scr_arena_obj and the heap constructor return zeroed objects.
|
||||
*/
|
||||
export function arenaNewLines(
|
||||
host: ArenaHost,
|
||||
@@ -284,6 +290,7 @@ export function arenaNewLines(
|
||||
dispose: string | null,
|
||||
init: string[],
|
||||
result: string,
|
||||
clear: "object" | "header" = "object",
|
||||
): string[] {
|
||||
const S = host.sizeType;
|
||||
// ScrArena's fields and dispose entries are pointer-sized (scr_arena.c).
|
||||
@@ -328,8 +335,13 @@ export function arenaNewLines(
|
||||
`carve:`,
|
||||
` %end = getelementptr inbounds i8, ptr %bp, ${S} %n`,
|
||||
` store ptr %end, ptr %a`,
|
||||
` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`,
|
||||
);
|
||||
if (clear === "object")
|
||||
lines.push(` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`);
|
||||
else if (cyc !== null)
|
||||
lines.push(
|
||||
` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} ${CYC_HDR}, i1 false)`,
|
||||
);
|
||||
if (cyc !== null)
|
||||
lines.push(
|
||||
` %id = trunc i32 %tid to i16`,
|
||||
|
||||
@@ -557,6 +557,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fs.readFileSyncBuf": "scr_fs_read_file_bytes",
|
||||
"fs.readFileSyncBytes": "scr_fs_read_file_bytes",
|
||||
"fs.writeFileSyncBytes": "scr_fs_write_file_bytes",
|
||||
"fs.writeFileSyncFd": "scr_fs_write_file_fd",
|
||||
"fs.appendFileSyncBytes": "scr_fs_append_file_bytes",
|
||||
"fs.readFdSyncBytes": "scr_fs_read_fd_bytes",
|
||||
// Stats snapshots (scr_lib.c): statSync/lstatSync throw like the other
|
||||
|
||||
@@ -2387,8 +2387,20 @@ export function emitRecordShapes(
|
||||
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
|
||||
);
|
||||
// A record literal covers every field of its shape (ir.ts recordLit),
|
||||
// so without dispose entries, which read the fields at the drop, no
|
||||
// field is read before the literal stores it.
|
||||
defs.push(
|
||||
...arenaNewLines(host, newSym, sizeOf, traced ? cycType : null, dispose, init, "%o"),
|
||||
...arenaNewLines(
|
||||
host,
|
||||
newSym,
|
||||
sizeOf,
|
||||
traced ? cycType : null,
|
||||
dispose,
|
||||
init,
|
||||
"%o",
|
||||
dispose === null ? "header" : "object",
|
||||
),
|
||||
);
|
||||
if (dispose !== null)
|
||||
defs.push(
|
||||
|
||||
@@ -647,26 +647,27 @@ export function lowerWithArraySpecies(
|
||||
// instance is created (with that length for map, 0 otherwise), then
|
||||
// the method runs to that length and its elements move into the
|
||||
// instance.
|
||||
const bounded =
|
||||
result.kind === "call" &&
|
||||
result.args[0]?.kind === "varRef" &&
|
||||
result.args[0].localId === local.id
|
||||
? boundedArrayHofHelper(lowerer, result.callee)
|
||||
: null;
|
||||
if (bounded === null || result.kind !== "call")
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
let bound: IrLocal | undefined;
|
||||
const rebound = rebuildSpeciesCall(result, local.id, (inner) => {
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of inner.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const length = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
bound = length;
|
||||
args.push({ kind: "varRef", localId: length.id, type: F64, loc });
|
||||
const helper = boundedArrayHofHelper(lowerer, inner.callee);
|
||||
return helper === null ? null : { ...inner, callee: helper, args };
|
||||
});
|
||||
if (rebound === null || bound === undefined)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'${access.name.text}' in this form when the program declares a class that extends Array`,
|
||||
);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of result.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const bound = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: bound.id,
|
||||
@@ -681,7 +682,6 @@ export function lowerWithArraySpecies(
|
||||
loc,
|
||||
});
|
||||
const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc };
|
||||
args.push(boundRef);
|
||||
const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type);
|
||||
const length: IrExpr = mode.resultLength
|
||||
? boundRef
|
||||
@@ -706,7 +706,7 @@ export function lowerWithArraySpecies(
|
||||
fn: "array.speciesFinish",
|
||||
args: [
|
||||
{ kind: "varRef", localId: instance.id, type: receiver.type, loc },
|
||||
{ ...result, callee: bounded, args },
|
||||
rebound,
|
||||
{ kind: "boolLit", value: mode.setLength, type: BOOL, loc },
|
||||
],
|
||||
type: result.type,
|
||||
@@ -736,6 +736,31 @@ export function lowerWithArraySpecies(
|
||||
};
|
||||
}
|
||||
|
||||
/** `e` with the `%arr.<method>` helper call over `receiverLocal` rebuilt by
|
||||
* `rebuild`, or null when `e` holds no such call. The helper call is the
|
||||
* one taking the receiver as its first argument; a filter whose callback
|
||||
* narrows converts the filtered array after the helper ran, so the call
|
||||
* can sit behind a wrapper (the checked-narrowing call), which this
|
||||
* rebuild leaves in place. */
|
||||
function rebuildSpeciesCall(
|
||||
e: IrExpr,
|
||||
receiverLocal: string,
|
||||
rebuild: (inner: IrExpr & { kind: "call" }) => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
if (e.kind !== "call") return null;
|
||||
const first = e.args[0];
|
||||
if (first !== undefined && first.kind === "varRef" && first.localId === receiverLocal)
|
||||
return rebuild(e);
|
||||
for (let i = 0; i < e.args.length; i++) {
|
||||
const replaced = rebuildSpeciesCall(e.args[i]!, receiverLocal, rebuild);
|
||||
if (replaced === null) continue;
|
||||
const args = e.args.slice();
|
||||
args[i] = replaced;
|
||||
return { ...e, args };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `C.of(...items)` and `C.from(iterable)` on a class declared `extends
|
||||
* Array` that declares neither: Array.of / Array.from's elements, in an
|
||||
* instance the class constructs (`new C(items.length)` for of, `new C()`
|
||||
|
||||
@@ -997,6 +997,42 @@ export function lowerFsWriteBytesCall(
|
||||
return null;
|
||||
}
|
||||
|
||||
// The writeFileSync(fd, data[, "utf8"]) form — Node accepts a file
|
||||
// descriptor where it accepts a path (write to a descriptor openSync
|
||||
// returned, the descriptor staying open: closeSync owns its lifetime).
|
||||
// Routed by the ARGUMENT's static type, like readFileSync(fd).
|
||||
export function lowerFsWriteDescriptorCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const supported = 'only string data is supported: writeFileSync(fd, data, "utf8")';
|
||||
const dataNode = expr.arguments.nodes[1]!;
|
||||
const dataIr = lowerer.mapTypeOf(lowerer.typeOf(dataNode));
|
||||
if (dataIr?.kind !== "string") {
|
||||
lowerer.noLowering(
|
||||
`writeFileSync(fd) of '${dataIr ? lowerer.fmt(dataIr) : lowerer.checker.typeToString(lowerer.typeOf(dataNode))}' data`,
|
||||
dataNode,
|
||||
supported,
|
||||
);
|
||||
}
|
||||
if (expr.arguments.nodes.length > 3) {
|
||||
lowerer.noLowering(
|
||||
`writeFileSync(fd) with ${expr.arguments.nodes.length} arguments`,
|
||||
expr,
|
||||
supported,
|
||||
);
|
||||
}
|
||||
const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "fs.writeFileSyncFd",
|
||||
args: [fd, lowerer.lowerExprExpecting(dataNode, STRING)],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerFsWriteOptionsCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
lowerFsReadSyncCall,
|
||||
lowerFsWriteSyncCall,
|
||||
lowerFsReadDescriptorCall,
|
||||
lowerFsWriteDescriptorCall,
|
||||
lowerFsEncodedRead,
|
||||
lowerFsPromisesEncodedRead,
|
||||
lowerFsMkdirSyncCall,
|
||||
@@ -331,6 +332,17 @@ export function lowerBuiltinModuleCall(
|
||||
const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc);
|
||||
if (lowered) return lowered;
|
||||
}
|
||||
// writeFileSync(fd, data): Node accepts an open descriptor where it
|
||||
// accepts a path. Routed by the FIRST ARGUMENT's static type, like
|
||||
// readFileSync(fd) (before the path forms complete the data type).
|
||||
if (
|
||||
bi.module === "fs" &&
|
||||
bi.member === "writeFileSync" &&
|
||||
expr.arguments.nodes.length >= 2 &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64"
|
||||
) {
|
||||
return lowerFsWriteDescriptorCall(lowerer, expr, loc);
|
||||
}
|
||||
if (
|
||||
bi.module === "fs" &&
|
||||
(bi.member === "writeFileSync" || bi.member === "appendFileSync") &&
|
||||
|
||||
@@ -5,7 +5,11 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { countedFor, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import {
|
||||
maybePromiseResolve,
|
||||
maybePromiseUnionOf,
|
||||
promiseOrUnitUnionOf,
|
||||
} from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -670,11 +674,17 @@ export function lowerPromiseStaticCall(
|
||||
if (v.type.kind !== "promise") lowerer.badType(argNode, argT);
|
||||
return v;
|
||||
}
|
||||
const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null;
|
||||
// A promise-or-unit union (`Promise<void> | void`,
|
||||
// `Promise<T> | undefined`) resolves exactly like a maybe-promise
|
||||
// one: Node adopts a promise operand and fulfills every other value
|
||||
// as itself (a nullish arm fulfills undefined).
|
||||
const maybePromise = argIr
|
||||
? (maybePromiseUnionOf(lowerer, argIr) ?? promiseOrUnitUnionOf(lowerer, argIr))
|
||||
: null;
|
||||
if (maybePromise) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const v = lowerer.lowerExpr(argNode);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type);
|
||||
const mp = maybePromiseUnionOf(lowerer, v.type) ?? promiseOrUnitUnionOf(lowerer, v.type);
|
||||
const resolved =
|
||||
resultT?.kind === "promise" && mp
|
||||
? maybePromiseResolve(lowerer, v, mp, resultT, loc)
|
||||
|
||||
@@ -62,6 +62,22 @@ export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunc
|
||||
)
|
||||
return;
|
||||
const seen = new Map<string, boolean>();
|
||||
// A type is recursive when it reaches itself without naming a shape or a
|
||||
// union: `type Rec = string | Rec[]` passes through the array, set, map
|
||||
// or promise case below. Those kinds have no id to memoize by, so a type
|
||||
// met again while it is still being visited answers false: what a cycle
|
||||
// adds to `holds` (a certificate exists somewhere in the type) is
|
||||
// reachability, and nothing outside the cycle makes it reachable.
|
||||
const visiting = new Set<IrType>();
|
||||
const descend = (t: IrType, visit: () => boolean): boolean => {
|
||||
if (visiting.has(t)) return false;
|
||||
visiting.add(t);
|
||||
try {
|
||||
return visit();
|
||||
} finally {
|
||||
visiting.delete(t);
|
||||
}
|
||||
};
|
||||
const holds = (t: IrType): boolean => {
|
||||
switch (t.kind) {
|
||||
case "record": {
|
||||
@@ -79,13 +95,24 @@ export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunc
|
||||
}
|
||||
case "array":
|
||||
case "set":
|
||||
return holds(t.elem);
|
||||
return descend(t, () => holds(t.elem));
|
||||
case "map":
|
||||
return holds(t.key) || holds(t.value);
|
||||
return descend(t, () => holds(t.key) || holds(t.value));
|
||||
case "promise":
|
||||
return holds(t.inner);
|
||||
case "union":
|
||||
return lowerer.unions.get(t.unionId)?.arms.some(holds) === true;
|
||||
return descend(t, () => holds(t.inner));
|
||||
case "union": {
|
||||
// Unions have an id, so a cycle that names one is answered from the
|
||||
// memo above; the identity guard covers a cycle that does not.
|
||||
const key = `union:${t.unionId}`;
|
||||
const known = seen.get(key);
|
||||
if (known !== undefined) return known;
|
||||
return descend(t, () => {
|
||||
seen.set(key, false);
|
||||
const result = lowerer.unions.get(t.unionId)?.arms.some(holds) === true;
|
||||
seen.set(key, result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3382,10 +3382,13 @@ export function lowerTuplePredicateCall(
|
||||
* of the receiver's length when it starts. ArraySpeciesCreate reads the
|
||||
* receiver's length before the species constructor runs, so a constructor
|
||||
* that appends to the receiver does not extend the loop
|
||||
* (lowerWithArraySpecies). Null when `name` is no such helper. */
|
||||
* (lowerWithArraySpecies). Every helper that reads a length is one, the
|
||||
* flatMap whose callback returns a union included, whether or not a
|
||||
* pipeline can fuse it (arrHofInfo holds the fusable ones); forEach makes
|
||||
* no array, so it stays out. Null when `name` is no such helper. */
|
||||
export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null {
|
||||
const info = lowerer.arrHofInfo.get(name);
|
||||
if (!info || info.method === "forEach") return null;
|
||||
if (!name.startsWith("%arr.")) return null;
|
||||
if (lowerer.arrHofInfo.get(name)?.method === "forEach") return null;
|
||||
const bounded = `${name}.bounded`;
|
||||
if (lowerer.liftedFnNamed(bounded)) return bounded;
|
||||
const fn = lowerer.liftedFnNamed(name);
|
||||
|
||||
@@ -1158,6 +1158,37 @@ export function lowerArrayFromCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `Array.from(typedArray, mapper)`: the element read is live (a
|
||||
// mapper that writes the source changes later reads, as in Node),
|
||||
// so this is a loop over the typed array, not a toArray snapshot.
|
||||
if (src.type.kind === "bytes" && args.length === 2) {
|
||||
const callback = lowerArrayCallback(lowerer, args[1]!, [F64], arrayOf(F64));
|
||||
if (callback.arity > 2)
|
||||
lowerer.noLowering("Array.from mapper with an array parameter", args[1]!);
|
||||
const fnRet = callback.fnArg.type.ret;
|
||||
if (fnRet.kind === "void" || fnRet.kind === "func")
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
requireProducedArrayElement(lowerer, call, "'Array.from(typedArray, mapper)'", fnRet);
|
||||
const outElem = callbackArrayElement(lowerer, call, fnRet);
|
||||
// The element kind rides in the key because the lifted helper's
|
||||
// source parameter carries it (the reads themselves are always f64).
|
||||
const key = `fromBytes:${src.type.elem}:${typeKey(outElem)}:${typeKey(fnRet)}:${callback.arity}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%arr.fromBytes", key);
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
lowerer.liftedFns.push(
|
||||
buildArrayFromBytesFn(lowerer, helper, src.type, outElem, fnRet, callback.arity, loc),
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [src, callback.fnArg],
|
||||
type: arrayOf(outElem),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A union of typed arrays and number arrays (indexed-unions.ts).
|
||||
const indexedArms = indexedUnionArms(lowerer, src.type);
|
||||
if (indexedArms && args.length === 1)
|
||||
@@ -1320,6 +1351,96 @@ function buildArrayFromArrayFn(
|
||||
};
|
||||
}
|
||||
|
||||
/** The `Array.from(typedArray, mapper)` generation loop: every index is
|
||||
* read through the typed array itself (live, like Node's iteration), so
|
||||
* a mapper that writes the source is visible to the indexes it has not
|
||||
* visited yet. */
|
||||
function buildArrayFromBytesFn(
|
||||
lowerer: Lowerer,
|
||||
name: string,
|
||||
bytesT: IrType & { kind: "bytes" },
|
||||
outElem: IrType,
|
||||
fnRet: IrType,
|
||||
arity: number,
|
||||
loc: SrcLoc,
|
||||
): IrFunction {
|
||||
const outT = arrayOf(outElem);
|
||||
const fnT = funcOf([F64, F64].slice(0, arity), fnRet);
|
||||
const a = varRef("a.0", bytesT, loc);
|
||||
const i = varRef("i.0", F64, loc);
|
||||
const out = varRef("out.0", outT, loc);
|
||||
// The index parameter is the second one, never read for an index the
|
||||
// element covers, so an element-only mapper drops it.
|
||||
const reads: IrExpr[] = [
|
||||
{ kind: "bytesIntrinsic", method: "get", receiver: a, args: [i], type: F64, loc },
|
||||
i,
|
||||
];
|
||||
const mapped: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fnT, loc),
|
||||
args: reads.slice(0, arity),
|
||||
type: fnRet,
|
||||
loc,
|
||||
};
|
||||
const held = !typeEquals(fnRet, outElem);
|
||||
const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped;
|
||||
return {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "a.0", name: "a", type: bytesT },
|
||||
{ localId: "f.0", name: "f", type: fnT },
|
||||
],
|
||||
returnType: outT,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: bytesT, mutable: true },
|
||||
{ id: "f.0", name: "f", type: fnT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: i,
|
||||
right: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: a,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []),
|
||||
arrayValueStore(lowerer, out, i, value, outElem, loc),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "i.0",
|
||||
value: { kind: "bin", op: "+", left: i, right: numLit(1, loc), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Array.from(s)` / `[...s]` on a STRING: the code-point split into a
|
||||
* fresh string[] (scr_str_code_points). */
|
||||
export function strCharsCall(src: IrExpr, loc: SrcLoc): IrExpr {
|
||||
|
||||
@@ -3592,11 +3592,12 @@ export function lowerUnionProperty(
|
||||
* message. The caller (the property/element dispatch) maybeNarrows. */
|
||||
export function lowerUnionKeyedRead(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
expr: ts.Node,
|
||||
unionId: string,
|
||||
value: IrExpr,
|
||||
key: IrExpr,
|
||||
literalField: string | null,
|
||||
missingFieldAsUndefined = false,
|
||||
): IrExpr | null {
|
||||
const def = lowerer.unions.get(unionId);
|
||||
if (!def) return null;
|
||||
@@ -3646,6 +3647,14 @@ export function lowerUnionKeyedRead(
|
||||
}
|
||||
if (!shape.indexValue) {
|
||||
if (literalField === null && shape.fields.length > 0) continue;
|
||||
// A record arm that does not declare the key at all answers
|
||||
// UNDEFINED in JS. Destructuring asks for that answer (its own
|
||||
// desugar reads a missing property as undefined, Node-exact); a
|
||||
// dotted read keeps refusing until narrowing proves the arm.
|
||||
if (literalField !== null && missingFieldAsUndefined) {
|
||||
push(UNDEFINED_T);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!pushAnswer(shape.indexValue)) return null;
|
||||
@@ -3690,6 +3699,12 @@ export function lowerUnionKeyedRead(
|
||||
if (!surfaces(declared)) return null;
|
||||
continue;
|
||||
}
|
||||
if (literalField !== null && !shape.indexValue && missingFieldAsUndefined) {
|
||||
// The missing-field answer above must surface as the join's
|
||||
// undefined arm (the backend's keyed-read miss path stores it).
|
||||
if (!(type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0)) return null;
|
||||
continue;
|
||||
}
|
||||
const ovfShape: IrRecordShape =
|
||||
literalField !== null && shape.indexValue ? { ...shape, fields: [] } : shape;
|
||||
if (!recordKeyResultOk(lowerer, ovfShape, type)) return null;
|
||||
|
||||
@@ -279,7 +279,11 @@ import { isThisParameter, type ParamShape } from "./call-signatures.js";
|
||||
import { dynamicOverloadChoice } from "./overload-choice.js";
|
||||
import { lowerStringIteratorValue } from "./iterator-adapters.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
promiseArmedUnionOf,
|
||||
promiseOrUnitUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import {
|
||||
requireObjLitGenericReceiver,
|
||||
objLitGenericFnNodeOf,
|
||||
@@ -4888,9 +4892,10 @@ const DYN_STRING_ONLY_METHODS = new Set([
|
||||
/** `Array.isArray(v)` — a real runtime test on `unknown` values (the checked-dynamic tree's
|
||||
* array kind: dyn arrays answer true, bytes/objects/scalars false — exactly
|
||||
* JS, Uint8Array included), a compile-time constant on statically-typed
|
||||
* ones (an `T[]` value IS an array, every other static kind is not; folded
|
||||
* only over side-effect-free reads, the `in`-operator discipline; unions
|
||||
* fence with the narrow-first hint). Null when the callee isn't THE
|
||||
* ones (an `T[]` value IS an array, every other static kind is not; a
|
||||
* computed argument is evaluated and its value discarded, the
|
||||
* `in`-operator discipline; unions fence with the narrow-first hint). Null
|
||||
* when the callee isn't THE
|
||||
* stdlib Array.isArray, so the chain keeps trying. */
|
||||
function lowerArrayIsArrayCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -4960,14 +4965,23 @@ function lowerArrayIsArrayCall(
|
||||
);
|
||||
}
|
||||
if (arg.type.kind === "caught") return null;
|
||||
if (arg.kind === "varRef" || arg.kind === "recordGet" || arg.kind === "fieldGet") {
|
||||
return { kind: "boolLit", value: lowerer.isArrayValueType(arg.type), type: BOOL, loc };
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
"statically-decided Array.isArray on computed arguments (bind the value to a variable first)",
|
||||
);
|
||||
const folded: IrExpr = {
|
||||
kind: "boolLit",
|
||||
value: lowerer.isArrayValueType(arg.type),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (arg.kind === "varRef" || arg.kind === "recordGet" || arg.kind === "fieldGet") return folded;
|
||||
// A computed argument (`Array.isArray(make())`, `Array.isArray([1, 2])`)
|
||||
// still answers from its static type, so evaluate the argument for its
|
||||
// effects and discard the value.
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: arg, loc }],
|
||||
result: folded,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Predicate declarations currently being inlined — re-entrancy guard
|
||||
@@ -5998,9 +6012,19 @@ export function lowerPromiseMethodCall(
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (lowerer.chainBlocked(access, call)) return null;
|
||||
const member = access.name.text;
|
||||
if (member !== "then" && member !== "catch" && member !== "finally") return null;
|
||||
// A call that ENDS an optional chain (`refreshing?.catch(h)`,
|
||||
// `client?.close().catch(h)`) checker-types as the chain's
|
||||
// undefined-armed union while the body this lowering builds is the
|
||||
// promise arm — `finishOptionalChain` wraps it back into the union.
|
||||
// Take the arm so the handler typing sees the promise it handles.
|
||||
const callResultType = (): IrType | undefined => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!t || t.kind === "promise") return t ?? undefined;
|
||||
return promiseOrUnitUnionOf(lowerer, t)?.promise ?? t;
|
||||
};
|
||||
let recvT =
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access.expression)) ??
|
||||
dynFallbackType(lowerer, access.expression, lowerer.typeOf(access.expression));
|
||||
@@ -6117,7 +6141,7 @@ export function lowerPromiseMethodCall(
|
||||
const rejectedNode = call.arguments.nodes[1]!;
|
||||
const fulfilledAbsent = isAbsentHandler(fulfilledNode);
|
||||
const rejectedAbsent = isAbsentHandler(rejectedNode);
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
// An untyped call in JavaScript whose receiver arrived as a typed
|
||||
// promise (a function instantiated with one): Promise.prototype.then
|
||||
// over the promise as a checked-dynamic value, which shares its
|
||||
@@ -6516,7 +6540,7 @@ export function lowerPromiseMethodCall(
|
||||
`then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
let resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let resultT = callResultType();
|
||||
// JavaScript whose declared result type has no mapping (zod's
|
||||
// `Promise.all(items).then((r) => ParseStatus.mergeArray(status, r))`,
|
||||
// typed by a .d.ts union): the handler's value settles the promise as
|
||||
@@ -6864,7 +6888,7 @@ export function lowerPromiseMethodCall(
|
||||
"value — take `(e)` or `(e: unknown)` and narrow with instanceof)",
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const resultT = callResultType();
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"catch with this handler's result type",
|
||||
@@ -6908,7 +6932,10 @@ export function lowerPromiseMethodCall(
|
||||
if (ts.isBlock(hb)) {
|
||||
catchBody = lowerer.lowerStmts(hb.statements.nodes);
|
||||
} else if (R.kind === "void") {
|
||||
catchBody = [{ kind: "exprStmt", expr: lowerer.lowerExpr(hb), loc: locOf(hb) }];
|
||||
// The handler's value is discarded, so the body lowers as a
|
||||
// statement: `() => undefined` is a pure no-op (a bare unit
|
||||
// literal has no IR value), not an expression to evaluate.
|
||||
catchBody = [lowerer.lowerExprStatement(hb)];
|
||||
} else {
|
||||
// Bare-expression handler: `(e) => v` (promise results flatten
|
||||
// through the async-return path, like any `return v`).
|
||||
|
||||
@@ -27,7 +27,11 @@ import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands
|
||||
import { staticCondition } from "./static-typeof.js";
|
||||
import { constituentTypes } from "../ts7/checker.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
|
||||
import {
|
||||
lowerMaybePromiseAwait,
|
||||
maybePromiseUnionOf,
|
||||
promiseArmedUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import { checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import {
|
||||
BIGINT_T,
|
||||
@@ -676,9 +680,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (inner.kind === "union" || inner.kind === "dyn") {
|
||||
// A union inner would need an arm-wise re-tag into the result
|
||||
|
||||
if (inner.kind === "union") {
|
||||
// A UNION inner (`Promise<A | B> | undefined`): the awaited
|
||||
// result is the checker's own `Awaited` of the operand, which
|
||||
// the maybe-promise lowering already produces — it awaits the
|
||||
// narrowed promise arm and coerces its settled union, and the
|
||||
// unit arms keep JS's one-microtask hop. Only the promise-or-
|
||||
// unit shape reaches here, so the maybe-promise union is the
|
||||
// same union with its only data arms being units.
|
||||
const armed = promiseArmedUnionOf(lowerer, value.type);
|
||||
if (armed) return lowerMaybePromiseAwait(lowerer, expr, value, armed, loc);
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`awaiting '${lowerer.fmt(value.type)}' (the promise's inner type has no combined result union — await the promise arm after narrowing instead)`,
|
||||
);
|
||||
}
|
||||
if (inner.kind === "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
|
||||
@@ -10,8 +10,12 @@ import {
|
||||
lowerIndexedUnionNumberRead,
|
||||
lowerIndexedUnionRead,
|
||||
} from "./indexed-unions.js";
|
||||
import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import {
|
||||
declaredMemberOf,
|
||||
isPromisePrototypeRead,
|
||||
lowerUnionKeyedRead,
|
||||
} from "./expressions/property-access.js";
|
||||
import { hostedStorageView, hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import { requestGeneralStreamSurface } from "./general-streams.js";
|
||||
import { lowerForAwaitDynStream, nodeStreamInterfaceSide } from "./dyn-streams.js";
|
||||
import { lowerForAwaitValueStream } from "./builtins/hash-stream.js";
|
||||
@@ -3538,6 +3542,34 @@ export function lowerBindingPattern(
|
||||
.get(srcType.unionId)
|
||||
?.arms.every((arm) => arm.kind === "record" || arm.kind === "object")
|
||||
) {
|
||||
const arms = lowerer.unions.get(srcType.unionId)!.arms;
|
||||
if (!unionObjectPatternFits(lowerer, pattern, arms)) {
|
||||
// NARROW FIRST: the checker may have refined the union at this
|
||||
// very site (`if (u.kind === "a") { const { x } = u }`, an `in`
|
||||
// guard) while the IR value keeps the stored, wider union. Bind
|
||||
// the narrowed view once and destructure that.
|
||||
const view = narrowedUnionPatternSource(
|
||||
lowerer,
|
||||
pattern,
|
||||
present,
|
||||
srcType.unionId,
|
||||
locOf(pattern),
|
||||
);
|
||||
if (view !== null) {
|
||||
const tmp = lowerer.declareHiddenLocal("%destrN", view.type);
|
||||
out.push({ kind: "varDecl", localId: tmp.id, init: view, loc: locOf(pattern) });
|
||||
lowerer.lowerBindingPattern(
|
||||
pattern,
|
||||
() => ({ kind: "varRef", localId: tmp.id, type: view.type, loc: locOf(pattern) }),
|
||||
view.type,
|
||||
isLet,
|
||||
out,
|
||||
dynSpell,
|
||||
allowDynObject,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
|
||||
return;
|
||||
}
|
||||
@@ -3950,11 +3982,235 @@ function lowerUrlObjectPattern(
|
||||
}
|
||||
}
|
||||
|
||||
/** The fresh record for `{ a, ...rest }` over a UNION source: per arm,
|
||||
* the unconsumed fields copied out at destructure time into a fresh
|
||||
* record of the checker's arm-wise rest type, selected by the runtime
|
||||
* tag. Null-safe answer: an arm whose remaining fields do not spell an
|
||||
* arm of the checker's rest type keeps the named fence. */
|
||||
function unionObjectRestValue(
|
||||
lowerer: Lowerer,
|
||||
el: ts.BindingElement,
|
||||
srcRef: () => IrExpr,
|
||||
srcType: IrType & { kind: "union" },
|
||||
arms: readonly IrType[],
|
||||
): IrExpr {
|
||||
const loc = locOf(el);
|
||||
const restT = patternBindingType(lowerer, el.name!);
|
||||
const fence: (why: string) => never = (why) => lowerer.unsupported("SC1031", el, why);
|
||||
if (restT === null) {
|
||||
fence(`rest bindings over union sources (${NARROW_FIRST})`);
|
||||
}
|
||||
// The consumed set is the pattern's literal keys: a computed sibling
|
||||
// would make it a runtime fact.
|
||||
const consumed = new Set<string>();
|
||||
const pattern = el.parent;
|
||||
if (ts.isObjectBindingPattern(pattern)) {
|
||||
for (const sib of pattern.elements.nodes) {
|
||||
if (sib === el || sib.dotDotDotToken) continue;
|
||||
const key = sib.propertyName ?? sib.name;
|
||||
if (key && ts.isComputedPropertyName(key)) {
|
||||
fence("rest bindings beside computed keys (the consumed set is a runtime fact)");
|
||||
}
|
||||
if (key && (ts.isIdentifier(key) || ts.isStringLiteral(key) || ts.isNumericLiteral(key))) {
|
||||
consumed.add(key.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
const pieces: { tag: number; value: IrExpr }[] = [];
|
||||
for (let tag = 0; tag < arms.length; tag++) {
|
||||
const arm = arms[tag]!;
|
||||
if (arm.kind !== "record") {
|
||||
fence(`rest bindings over union sources (${NARROW_FIRST})`);
|
||||
}
|
||||
const shape = lowerer.shapes.get(arm.shapeId);
|
||||
if (!shape || shape.indexValue) {
|
||||
fence(
|
||||
"rest bindings over index-signature shapes (the undeclared entries would need overflow packing)",
|
||||
);
|
||||
}
|
||||
const rem = shape.fields.filter((f) => !consumed.has(f.name));
|
||||
if (rem.some((f) => f.name.startsWith("%"))) {
|
||||
fence("rest bindings over accessor shapes (the entries are closures, not data)");
|
||||
}
|
||||
const armRef: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: srcType.unionId,
|
||||
tag,
|
||||
value: srcRef(),
|
||||
type: arm,
|
||||
loc,
|
||||
};
|
||||
const packed: IrExpr = lowerer.keyOrder.copy(
|
||||
{
|
||||
kind: "recordLit",
|
||||
fields: rem.map((f) => ({
|
||||
name: f.name,
|
||||
value: lowerer.presenceKeepingCopy(
|
||||
armRef,
|
||||
arm.shapeId,
|
||||
f.name,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: armRef,
|
||||
shapeId: arm.shapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
),
|
||||
})),
|
||||
type: {
|
||||
kind: "record",
|
||||
shapeId: lowerer.shapes.intern(rem.map((f) => ({ name: f.name, type: f.type }))),
|
||||
},
|
||||
loc,
|
||||
},
|
||||
arm.shapeId,
|
||||
);
|
||||
const coerced = lowerer.coerceInto(el, packed, restT);
|
||||
if (!typeEquals(coerced.type, restT)) {
|
||||
fence(
|
||||
`rest bindings over union sources (the remaining fields of an arm do not spell an arm of '${lowerer.fmt(restT)}')`,
|
||||
);
|
||||
}
|
||||
pieces.push({ tag, value: coerced });
|
||||
}
|
||||
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: {
|
||||
kind: "unionIsTag",
|
||||
unionId: srcType.unionId,
|
||||
tag: piece.tag,
|
||||
negated: false,
|
||||
value: srcRef(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: piece.value,
|
||||
else_: result,
|
||||
type: restT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (result === null) {
|
||||
fence(`rest bindings over union sources (${NARROW_FIRST})`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The one type every arm declares for `propName`, or null when an arm
|
||||
* does not declare it (or types it differently, or not at all). */
|
||||
function unionPatternCommonType(
|
||||
lowerer: Lowerer,
|
||||
arms: readonly IrType[],
|
||||
propName: string,
|
||||
): IrType | null {
|
||||
let common: IrType | null = null;
|
||||
for (const arm of arms) {
|
||||
let ft: IrType | undefined;
|
||||
if (arm.kind === "record") {
|
||||
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
|
||||
} else if (arm.kind === "object") {
|
||||
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
|
||||
}
|
||||
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
|
||||
return null;
|
||||
}
|
||||
common = ft;
|
||||
}
|
||||
return common;
|
||||
}
|
||||
|
||||
/** Whether the unionDisc fast path lowers this pattern over these arms
|
||||
* unchanged: no rest element, and every element's key folds to one name
|
||||
* every arm declares with one type. */
|
||||
function unionObjectPatternFits(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
arms: readonly IrType[],
|
||||
): boolean {
|
||||
for (const el of pattern.elements.nodes) {
|
||||
if (el.name === undefined) continue;
|
||||
if (el.dotDotDotToken) return false;
|
||||
const prop = el.propertyName ?? el.name;
|
||||
const propName =
|
||||
ts.isIdentifier(prop) ||
|
||||
ts.isComputedPropertyName(prop) ||
|
||||
ts.isStringLiteralLike(prop) ||
|
||||
ts.isNumericLiteral(prop)
|
||||
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
|
||||
: null;
|
||||
if (propName === null) return false;
|
||||
if (unionPatternCommonType(lowerer, arms, propName) === null) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The CHECKER-NARROWED view of a destructuring source: `if (u.kind ===
|
||||
* "a") { const { x } = u }` or an `in` guard refines the type at the
|
||||
* pattern's own site, while the IR value keeps the stored (wider) union
|
||||
* and the un-narrowed lowering. Bind the value to the narrowed sub-union
|
||||
* — a trusted arm extraction for one arm, a re-tag with the excluded
|
||||
* arms as traps for a sub-union — so the pattern reads the arms the
|
||||
* checker proved. Null when the checker did not narrow, when the
|
||||
* refinement is an intersection of stored records (hostedStorageView
|
||||
* keeps that layout), or when nothing was excluded. */
|
||||
function narrowedUnionPatternSource(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
value: IrExpr,
|
||||
fromId: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const from = lowerer.unions.get(fromId);
|
||||
if (!from) return null;
|
||||
const narrowed = lowerer.irTypeOf(pattern);
|
||||
if (narrowed.kind === "void" || isUnitType(narrowed)) return null;
|
||||
const siteArms =
|
||||
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
|
||||
if (!siteArms || siteArms.length === 0) return null;
|
||||
const allowed = new Set<number>();
|
||||
for (const arm of siteArms) {
|
||||
const tag = lowerer.armTag(fromId, arm);
|
||||
// Not a stored arm: a checker intersection that refines the stored
|
||||
// records, which keeps the value's layout.
|
||||
if (tag < 0) return hostedStorageView(lowerer, value, narrowed, loc);
|
||||
allowed.add(tag);
|
||||
}
|
||||
if (allowed.size === 0 || allowed.size === from.arms.length) return null;
|
||||
if (allowed.size === 1) {
|
||||
const tag = [...allowed][0]!;
|
||||
// The checker proved the tag; the extraction traps on a mismatch
|
||||
// instead of reading another arm.
|
||||
return { kind: "unionNarrow", unionId: fromId, tag, value, type: from.arms[tag]!, loc };
|
||||
}
|
||||
const arms = from.arms.filter((_, tag) => allowed.has(tag));
|
||||
const subId = lowerer.unions.transform(from, arms);
|
||||
const helper = lowerer.narrowedRetagHelper(pattern, fromId, subId, loc);
|
||||
if (!helper) return null;
|
||||
return {
|
||||
kind: "call",
|
||||
callee: helper,
|
||||
args: [value],
|
||||
type: { kind: "union", unionId: subId },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** An object pattern over a union of records and class instances
|
||||
* (`function f({ kind, path }: Add | Remove)`): each element reads a data
|
||||
* field every arm declares with one type, through the runtime tag, as
|
||||
* `value.field` does. Rest elements and fields the arms type differently
|
||||
* stay fenced. */
|
||||
* field through the runtime tag, as `value.field` does — the shared
|
||||
* `unionDisc` when every arm declares it with one type, else the
|
||||
* `unionKeyGet` join (an arm that does not declare the key answers
|
||||
* undefined, as JS reads it). A rest element packs each arm's unconsumed
|
||||
* fields into a fresh record. Class arms still need same-typed fields. */
|
||||
function lowerUnionObjectPattern(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
@@ -3967,7 +4223,17 @@ function lowerUnionObjectPattern(
|
||||
for (const el of pattern.elements.nodes) {
|
||||
if (el.name === undefined) continue;
|
||||
if (el.dotDotDotToken) {
|
||||
lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`);
|
||||
// `{ a, ...rest }` over a union: each arm packs its own unconsumed
|
||||
// fields into a FRESH record (JS's CopyDataProperties makes a fresh
|
||||
// object too), and the checker's rest type is the union of those
|
||||
// per-arm packs. The tag decides which pack runs.
|
||||
lowerer.bindPatternTarget(
|
||||
el.name,
|
||||
unionObjectRestValue(lowerer, el, srcRef, srcType, arms),
|
||||
isLet,
|
||||
out,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const prop = el.propertyName ?? el.name;
|
||||
const propName =
|
||||
@@ -3984,35 +4250,43 @@ function lowerUnionObjectPattern(
|
||||
"destructuring with computed keys that do not fold to one property name",
|
||||
);
|
||||
}
|
||||
let common: IrType | null = null;
|
||||
for (const arm of arms) {
|
||||
let ft: IrType | undefined;
|
||||
if (arm.kind === "record") {
|
||||
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
|
||||
} else if (arm.kind === "object") {
|
||||
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
|
||||
}
|
||||
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
|
||||
common = null;
|
||||
break;
|
||||
}
|
||||
common = ft;
|
||||
}
|
||||
if (common === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
const common = unionPatternCommonType(lowerer, arms, propName);
|
||||
let value: IrExpr;
|
||||
if (common !== null) {
|
||||
value = {
|
||||
kind: "unionDisc",
|
||||
unionId: srcType.unionId,
|
||||
field: propName,
|
||||
value: srcRef(),
|
||||
type: common,
|
||||
loc: locOf(el),
|
||||
};
|
||||
} else {
|
||||
// The unionDisc generalization (`unionKeyGet`): each arm answers
|
||||
// its own declared type and an arm that does not declare the
|
||||
// key answers undefined — exactly JS's desugar, which reads a
|
||||
// missing property as undefined.
|
||||
const loc = locOf(el);
|
||||
const joined = lowerUnionKeyedRead(
|
||||
lowerer,
|
||||
el,
|
||||
`destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`,
|
||||
srcType.unionId,
|
||||
srcRef(),
|
||||
{ kind: "strLit", value: propName, type: STRING, loc },
|
||||
propName,
|
||||
true,
|
||||
);
|
||||
if (joined === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
`destructuring '${propName}' from a union whose arms answer it with types that have no combined representation (every arm's '${
|
||||
propName
|
||||
}' must join into one union — ${NARROW_FIRST})`,
|
||||
);
|
||||
}
|
||||
value = joined;
|
||||
}
|
||||
let value: IrExpr = {
|
||||
kind: "unionDisc",
|
||||
unionId: srcType.unionId,
|
||||
field: propName,
|
||||
value: srcRef(),
|
||||
type: common,
|
||||
loc: locOf(el),
|
||||
};
|
||||
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
|
||||
lowerer.bindPatternTarget(el.name, value, isLet, out);
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js";
|
||||
import { dynamicSlotOperationCounts, dynamicTierWarning } from "./dynamic-tier.js";
|
||||
import { promoteJsvals } from "../../ir/jsval.js";
|
||||
import { returnDispatchReceivers } from "./receiver-returns.js";
|
||||
import {
|
||||
analyzeTiers,
|
||||
dynamicStatementCount,
|
||||
@@ -9464,6 +9465,7 @@ export class Lowerer {
|
||||
applyHandleProperties(this, functions);
|
||||
installBufferValue(functions);
|
||||
installFetchBodyWebStreams(this, functions);
|
||||
returnDispatchReceivers(this, functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
// collection fenced — Symbol-keyed fields, an unsupported base): the
|
||||
|
||||
@@ -23,6 +23,18 @@ export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUn
|
||||
return armed;
|
||||
}
|
||||
|
||||
/** The promise arm of a promise-or-unit union (`Promise<void> | void`,
|
||||
* `Promise<T> | undefined`): exactly one promise arm and only nullish
|
||||
* arms beside it, so no other arm carries a value the promise path would
|
||||
* have to reconcile. A guard that proves the value present (`?.`, an
|
||||
* explicit narrow) leaves this arm alone. */
|
||||
export function promiseOrUnitUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null {
|
||||
const armed = promiseArmedUnionOf(lowerer, t);
|
||||
if (!armed) return null;
|
||||
if (!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. */
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* Methods that return their receiver, called through an untyped value.
|
||||
*
|
||||
* A JavaScript method that ends every path with `return this` (a fluent
|
||||
* setter such as `step(dx, dy) { this.x += dx; return this; }`) boxes its
|
||||
* receiver on every call: a lookup of the instance's live capsule, or a new
|
||||
* capsule when it has none. A class dispatch helper
|
||||
* (`%dyn.class.call.N`) that calls such a method already holds a capsule
|
||||
* of the same instance at its exact class: its receiver after
|
||||
* normalization. The helper now calls a copy of the method that returns
|
||||
* nothing and answers its own receiver, so the call boxes nothing. Other
|
||||
* callers (vtables, typed code, method values) keep the method itself. */
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { VOID } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
const DISPATCH_PREFIX = "%dyn.class.call.";
|
||||
const BOX_EXACT_PREFIX = "%dyn.class.boxExact:";
|
||||
|
||||
/** The name of a method's copy that returns nothing. */
|
||||
function receiverCopyName(method: string): string {
|
||||
return `%dyn.class.receiver:${method.slice(1)}`;
|
||||
}
|
||||
|
||||
export function returnDispatchReceivers(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
const copies = new Map<string, IrFunction | null>();
|
||||
const copyOf = (name: string): IrFunction | null => {
|
||||
const known = copies.get(name);
|
||||
if (known !== undefined) return known;
|
||||
const method = byName.get(name);
|
||||
const copy = method && returnsReceiver(lowerer, method) ? receiverCopy(method) : null;
|
||||
copies.set(name, copy);
|
||||
if (copy) functions.push(copy);
|
||||
return copy;
|
||||
};
|
||||
for (const helper of functions) {
|
||||
if (!helper.name.startsWith(DISPATCH_PREFIX)) continue;
|
||||
const receiver = helper.params[0]?.localId;
|
||||
if (receiver === undefined || !normalizedOnly(helper, receiver)) continue;
|
||||
for (const stmt of helper.body) {
|
||||
if (stmt.kind !== "if" || stmt.then.length !== 1) continue;
|
||||
const only = stmt.then[0]!;
|
||||
if (only.kind !== "return" || only.value === null || only.value.kind !== "call") continue;
|
||||
const call = only.value;
|
||||
if (!receiverArgument(call.args[0], receiver)) continue;
|
||||
const copy = copyOf(call.callee);
|
||||
if (!copy) continue;
|
||||
stmt.then = [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { ...call, callee: copy.name, type: VOID },
|
||||
loc: only.loc,
|
||||
},
|
||||
{ kind: "return", value: varRef(receiver, only.value.type, only.loc), loc: only.loc },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The helper's receiver is assigned only by its normalization, so it is
|
||||
* the capsule of the instance's own class wherever a branch reads it. */
|
||||
function normalizedOnly(helper: IrFunction, receiver: string): boolean {
|
||||
return everyStmtList(helper.body, {
|
||||
stmt: (s) =>
|
||||
!(
|
||||
(s.kind === "assign" || s.kind === "forOf") &&
|
||||
s.localId === receiver &&
|
||||
!(
|
||||
s.kind === "assign" &&
|
||||
s.value.kind === "call" &&
|
||||
s.value.callee === "%dyn.class.normalize"
|
||||
)
|
||||
),
|
||||
expr: (e) => !((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === receiver),
|
||||
});
|
||||
}
|
||||
|
||||
/** `dynCheck(receiver)`, possibly upcast to the method's class. */
|
||||
function receiverArgument(arg: IrExpr | undefined, receiver: string): boolean {
|
||||
if (arg?.kind === "upcast") arg = arg.value;
|
||||
return arg?.kind === "dynCheck" && arg.value.kind === "varRef" && arg.value.localId === receiver;
|
||||
}
|
||||
|
||||
/** Whether every path of a synchronous method returns `this` boxed at its
|
||||
* exact class. */
|
||||
function returnsReceiver(lowerer: Lowerer, fn: IrFunction): boolean {
|
||||
if (fn.async !== undefined || fn.generator !== undefined) return false;
|
||||
if (fn.captures?.length || fn.classCaptures?.length) return false;
|
||||
if (fn.returnType.kind !== "dyn") return false;
|
||||
const self = fn.params[0];
|
||||
if (self?.type.kind !== "object" || self.name !== "this") return false;
|
||||
const last = fn.body[fn.body.length - 1];
|
||||
if (last?.kind !== "return") return false;
|
||||
const isSelf = (value: IrExpr | null): boolean => {
|
||||
if (value === null) return false;
|
||||
if (value.kind === "call" && value.callee.startsWith(BOX_EXACT_PREFIX))
|
||||
return thisRef(value.args[0], self.localId);
|
||||
// Without exact boxing the capsule names the static class, which is
|
||||
// the instance's own class only when nothing extends it.
|
||||
if (value.kind === "dynFrom" && value.liveRef === undefined && value.fnName === undefined) {
|
||||
const className = self.type.kind === "object" ? self.type.className : "";
|
||||
return (
|
||||
thisRef(value.value, self.localId) &&
|
||||
(lowerer.classes.get(className)?.subclasses.length ?? 1) === 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return everyStmtList(fn.body, {
|
||||
stmt: (s) => s.kind !== "return" || isSelf(s.value),
|
||||
expr: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
function thisRef(value: IrExpr | undefined, thisId: string): boolean {
|
||||
return value?.kind === "varRef" && value.localId === thisId;
|
||||
}
|
||||
|
||||
/** A copy of `method` whose returns return nothing. */
|
||||
function receiverCopy(method: IrFunction): IrFunction {
|
||||
const copy = structuredClone(method);
|
||||
copy.name = receiverCopyName(method.name);
|
||||
copy.returnType = VOID;
|
||||
copy.body = transformStmtList(copy.body, {
|
||||
stmt: (s): IrStmt => (s.kind === "return" ? { ...s, value: null } : s),
|
||||
expr: (e) => e,
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
@@ -2197,7 +2197,9 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
rename: ["fs.checkPath"],
|
||||
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
|
||||
// The bytes-data form and the { mode } options form.
|
||||
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
|
||||
// The bytes-data form, the { mode } options form, and the
|
||||
// file-descriptor data form.
|
||||
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync", "fs.writeFileSyncFd"],
|
||||
appendFileSync: ["fs.appendFileSyncBytes"],
|
||||
// The fs stream members (bytesRead/bytesWritten, pending, path, close).
|
||||
createReadStream: [
|
||||
|
||||
@@ -920,6 +920,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"emitter.setDefaultMaxChk",
|
||||
"fs.readFileSyncBytes",
|
||||
"fs.writeFileSyncBytes",
|
||||
"fs.writeFileSyncFd",
|
||||
"fs.appendFileSyncBytes",
|
||||
"zlib.inflateSync",
|
||||
"zlib.inflateRawSync",
|
||||
|
||||
@@ -5289,6 +5289,11 @@ function buildLibFnSignatures() {
|
||||
"fs.mkdtempSync": { argTypes: [STRING], result: STRING },
|
||||
"fs.accessSync": { argTypes: [STRING, F64], result: VOID },
|
||||
"fs.readFdSync": { argTypes: [F64, STRING], result: STRING },
|
||||
/** The writeFileSync(fd, data) form: a write(2) loop over the whole
|
||||
* string at the descriptor's current offset, leaving the descriptor
|
||||
* open (closeSync owns it). Errors carry the fd operation's errno
|
||||
* shape ("EBADF: bad file descriptor, write"). */
|
||||
"fs.writeFileSyncFd": { argTypes: [F64, STRING], result: VOID },
|
||||
"fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 },
|
||||
/** Validated isatty(3) probe. Stream property lowering maps false to
|
||||
* undefined; tty.isatty exposes the boolean result directly. */
|
||||
|
||||
@@ -344,3 +344,66 @@ test("a parameter read by dyn consumers or passed cells keeps the dyn ABI", () =
|
||||
expect(twice.returnType).toEqual(DYN);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
});
|
||||
|
||||
/** `for (...) %g.sum = %g.sum + o.step` in a dynamic function. */
|
||||
function globalLoop(name: string, global: string): IrFunction {
|
||||
return fn(name, [
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: global,
|
||||
value: op("dyn.add", ref(global), keyGet(ref("o"), "step")),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: ref("o"), loc },
|
||||
]);
|
||||
}
|
||||
|
||||
test("a dyn module global written by operators in a loop becomes a jsval global", () => {
|
||||
const loop = globalLoop("loop", "%g.e.sum");
|
||||
// %g.sum.x, once: a dyn consumer.
|
||||
const report = fn("report", [
|
||||
{ kind: "exprStmt", expr: keyGet(ref("%g.e.sum"), "x"), loc },
|
||||
{ kind: "return", value: ref("o"), loc },
|
||||
]);
|
||||
const mod = moduleOf(loop, report);
|
||||
mod.globals = [{ id: "%g.e.sum", name: "sum", type: DYN, mutable: true }];
|
||||
promoteJsvals(mod);
|
||||
expect(mod.globals[0]!.type).toEqual(JSVAL);
|
||||
// The loop's own result (o, a jsval result) is the second js.fromDyn.
|
||||
expect(jsCalls(loop.body)).toEqual(["js.add", "js.fromDyn", "js.fromDyn"]);
|
||||
// One box for the read; the second call is the jsval result o.
|
||||
expect(jsCalls(report.body)).toEqual(["js.toDyn", "js.fromDyn"]);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
});
|
||||
|
||||
test("a module global a static function reads stays dyn", () => {
|
||||
const loop = globalLoop("loop", "%g.e.sum");
|
||||
const reader: IrFunction = {
|
||||
name: "reader",
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: [],
|
||||
body: [{ kind: "return", value: ref("%g.e.sum"), loc }],
|
||||
loc,
|
||||
};
|
||||
const mod = moduleOf(loop, reader);
|
||||
expect(reader.tier).toBeUndefined();
|
||||
mod.globals = [{ id: "%g.e.sum", name: "sum", type: DYN, mutable: true }];
|
||||
promoteJsvals(mod);
|
||||
expect(mod.globals[0]!.type).toEqual(DYN);
|
||||
expect(jsCalls(loop.body)).toEqual([
|
||||
"js.toDyn",
|
||||
"js.add",
|
||||
"js.fromDyn",
|
||||
"js.fromDyn",
|
||||
"js.fromDyn",
|
||||
]);
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
* of the parameter by dyn consumers box more often than its callers do;
|
||||
* - a dyn local becomes a jsval local when the boxing it saves on writes
|
||||
* of operator results outweighs the boxing its reads by dyn consumers add
|
||||
* (loop depth weights both).
|
||||
* (loop depth weights both);
|
||||
* - a dyn module global becomes a jsval global by the same measure, summed
|
||||
* over every function, when only dynamic-tier functions reference it.
|
||||
*
|
||||
* The rewrite changes representation only: every js builtin implements its
|
||||
* dyn twin's JS semantics, including ToPrimitive order and error messages
|
||||
@@ -116,10 +118,12 @@ export function promoteJsvals(mod: IrModule): number {
|
||||
const storesJs = (e: IrExpr): boolean =>
|
||||
e.kind === "call" && sites.site(e.callee)?.kind === "set";
|
||||
const params = jsvalParams(mod, direct, results, storesJs);
|
||||
const globals = promotableGlobals(mod, results, params, storesJs);
|
||||
let count = 0;
|
||||
for (const fn of mod.functions)
|
||||
if (jsvalEligible(fn) && promoteFunction(fn, results, params, storesJs)) count++;
|
||||
if (jsvalEligible(fn) && promoteFunction(fn, results, params, storesJs, globals)) count++;
|
||||
for (const fn of mod.functions) if (results.has(fn.name)) fn.returnType = JSVAL;
|
||||
for (const global of mod.globals ?? []) if (globals.has(global.id)) global.type = JSVAL;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -273,6 +277,22 @@ function promotableLocals(
|
||||
credits: ReadonlyMap<string, number> = new Map(),
|
||||
jsParams: ReadonlyMap<string, readonly boolean[]> = new Map(),
|
||||
): Set<string> {
|
||||
const candidates = localCandidates(fn, credits);
|
||||
if (candidates.size === 0) return new Set();
|
||||
const blocked = new Set<string>();
|
||||
scanBindingCosts(fn, candidates, blocked, producer, jsResult, storesJs, jsParams);
|
||||
const promoted = new Set<string>();
|
||||
for (const [id, cost] of candidates)
|
||||
if (!blocked.has(id) && cost.saved > cost.added) promoted.add(id);
|
||||
return promoted;
|
||||
}
|
||||
|
||||
/** The dyn locals of a function that may become jsval locals, parameters
|
||||
* with their credit. */
|
||||
function localCandidates(
|
||||
fn: IrFunction,
|
||||
credits: ReadonlyMap<string, number>,
|
||||
): Map<string, LocalCost> {
|
||||
const candidates = new Map<string, LocalCost>();
|
||||
for (const local of fn.locals) {
|
||||
if (local.type.kind === "dyn" && local.boxed === undefined && local.tdz === undefined)
|
||||
@@ -286,8 +306,69 @@ function promotableLocals(
|
||||
}
|
||||
for (const c of fn.captures ?? []) candidates.delete(c.localId);
|
||||
for (const c of fn.classCaptures ?? []) candidates.delete(c.localId);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** The dyn module globals that become jsval globals: those that only
|
||||
* dynamic-tier functions reference, by reads and plain writes, and whose
|
||||
* saved boxing outweighs the added boxing summed over every function, the
|
||||
* same measure as for locals. Libraries keep every global (the host reads
|
||||
* exported bindings), and so do bindings with a temporal dead zone. */
|
||||
function promotableGlobals(
|
||||
mod: IrModule,
|
||||
results: ReadonlySet<string>,
|
||||
jsParams: ReadonlyMap<string, readonly boolean[]>,
|
||||
storesJs: (e: IrExpr) => boolean,
|
||||
): Set<string> {
|
||||
const candidates = new Map<string, LocalCost>();
|
||||
if (mod.lib !== undefined) return new Set();
|
||||
for (const global of mod.globals ?? [])
|
||||
if (global.type.kind === "dyn" && global.tdz === undefined && global.initFlag === undefined)
|
||||
candidates.set(global.id, { saved: 0, added: 0 });
|
||||
if (candidates.size === 0) return new Set();
|
||||
const blocked = new Set<string>();
|
||||
const producer = (e: IrExpr): boolean =>
|
||||
isProducerCall(e) || (e.kind === "call" && results.has(e.callee));
|
||||
for (const fn of mod.functions) {
|
||||
if (!jsvalEligible(fn)) {
|
||||
// A static function reads and writes the binding as a cell (I1).
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (
|
||||
(e.kind === "varRef" || e.kind === "assignExpr" || e.kind === "incDec") &&
|
||||
candidates.has(e.localId)
|
||||
)
|
||||
blocked.add(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (st) => {
|
||||
if ((st.kind === "assign" || st.kind === "forOf") && candidates.has(st.localId))
|
||||
blocked.add(st.localId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
scanBindingCosts(fn, candidates, blocked, producer, results.has(fn.name), storesJs, jsParams);
|
||||
}
|
||||
const promoted = new Set<string>();
|
||||
for (const [id, cost] of candidates)
|
||||
if (!blocked.has(id) && cost.saved > cost.added) promoted.add(id);
|
||||
return promoted;
|
||||
}
|
||||
|
||||
/** Adds to `candidates` the boxing a jsval binding saves and adds in `fn`,
|
||||
* and to `blocked` the candidates `fn` references other than by reads and
|
||||
* plain writes. */
|
||||
function scanBindingCosts(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlyMap<string, LocalCost>,
|
||||
blocked: Set<string>,
|
||||
producer: (e: IrExpr) => boolean,
|
||||
jsResult: boolean,
|
||||
storesJs: (e: IrExpr) => boolean,
|
||||
jsParams: ReadonlyMap<string, readonly boolean[]>,
|
||||
): void {
|
||||
const write = (id: string, value: IrExpr | null, weight: number): void => {
|
||||
const cost = candidates.get(id);
|
||||
if (cost === undefined) return;
|
||||
@@ -431,10 +512,6 @@ function promotableLocals(
|
||||
}
|
||||
};
|
||||
for (const s of fn.body) stmt(s, 1);
|
||||
const promoted = new Set<string>();
|
||||
for (const [id, cost] of candidates)
|
||||
if (!blocked.has(id) && cost.saved > cost.added) promoted.add(id);
|
||||
return promoted;
|
||||
}
|
||||
|
||||
/** A statement-level sequence whose result nothing reads. */
|
||||
@@ -449,6 +526,7 @@ function promoteFunction(
|
||||
results: ReadonlySet<string>,
|
||||
params: ReadonlyMap<string, readonly boolean[]>,
|
||||
storesJs: (e: IrExpr) => boolean,
|
||||
globals: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const resultCall = (e: IrExpr): boolean => e.kind === "call" && results.has(e.callee);
|
||||
const operatorCall = (e: IrExpr): boolean => isProducerCall(e);
|
||||
@@ -460,6 +538,7 @@ function promoteFunction(
|
||||
new Map(),
|
||||
params,
|
||||
);
|
||||
for (const id of globals) promoted.add(id);
|
||||
const ownParams = params.get(fn.name);
|
||||
fn.params.forEach((p, i) => {
|
||||
if (ownParams?.[i] === true) promoted.add(p.localId);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8420-x509-recursive-dyn-type.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8421-array-subclass-species-forms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8422-array-isarray-computed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/array-from-typed-array-mapper.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-method-returns-receiver.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-module-let-imports/counter.mjs",
|
||||
"<repo>/tests/corpus/dynamic-module-let-imports/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-module-let-numbers.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fs-write-file-sync-fd.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-catch-optional-chain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-resolve-promise-or-unit.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-void-catch-undefined.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-await-promise-arm.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-destructuring-join-narrow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -4242,6 +4242,25 @@ double scr_fs_write_str_sync(double fd, ScrStr *data, double position,
|
||||
return wrote;
|
||||
}
|
||||
|
||||
/* writeFileSync(fd, data): Node writes the WHOLE data to the descriptor
|
||||
* and leaves it open (closeSync owns its lifetime), at the descriptor's
|
||||
* current offset — libuv's uv_fs_write loop, so a pipe's short writes
|
||||
* resume until the data is gone. Errors keep the fd operation's errno
|
||||
* shape ("EBADF: bad file descriptor, write"). */
|
||||
void scr_fs_write_file_fd(double fd, ScrStr *data) {
|
||||
char *owned;
|
||||
const char *bytes = scr_str_utf8(data, &owned);
|
||||
size_t at = 0;
|
||||
while (at < data->len) {
|
||||
double wrote = scr_fs_write_bytes(fd, bytes + at, data->len - at, -1);
|
||||
/* A throw leaves 0 (or the short count a partial write reported);
|
||||
* a zero-length write on a nonempty slice would spin, so stop. */
|
||||
if (wrote <= 0) break;
|
||||
at += (size_t)wrote;
|
||||
}
|
||||
scr_str_utf8_end(owned);
|
||||
}
|
||||
|
||||
/* fs.readSync(fd, buffer, offset, length[, position]) — the buffer form.
|
||||
* Node validates offset/length against the buffer before reading and
|
||||
* throws ERR_OUT_OF_RANGE; here the checks clamp to the same contract and
|
||||
|
||||
@@ -289,6 +289,10 @@ ScrStr *scr_fs_read_file(ScrStr *path);
|
||||
ScrStr *scr_fs_realpath(ScrStr *path);
|
||||
ScrStr *scr_fs_realpath_promise(ScrStr *path);
|
||||
void scr_fs_write_file(ScrStr *path, ScrStr *data);
|
||||
/* writeFileSync(fd, data): a write(2) loop over the whole string at the
|
||||
* descriptor's current offset; the descriptor stays open (closeSync owns
|
||||
* it). Errors carry the fd operation's errno shape. */
|
||||
void scr_fs_write_file_fd(double fd, ScrStr *data);
|
||||
void scr_fs_append_file(ScrStr *path, ScrStr *data);
|
||||
bool scr_fs_exists(ScrStr *path);
|
||||
void scr_fs_mkdir(ScrStr *path);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// A recursive type that cycles through an array and a set, next to a
|
||||
// certificate: the fence that refuses an X509Certificate in an `unknown`
|
||||
// slot (x509.ts) walks the converted value's type, and a cycle through
|
||||
// anything but a record used to recurse until the stack ran out — a crash,
|
||||
// not a diagnostic. The types are named here the way a JSON-ish union is,
|
||||
// and the certificate is the one 7090 parses.
|
||||
import { X509Certificate } from "node:crypto";
|
||||
|
||||
// A self-signed certificate without alternative names (as in 7090).
|
||||
const PLAIN = `-----BEGIN CERTIFICATE-----
|
||||
MIIC8jCCAdoCAQcwDQYJKoZIhvcNAQELBQAwPjEaMBgGA1UEAwwRcGxhaW4uZXhh
|
||||
bXBsZS5vcmcxIDAeBgkqhkiG9w0BCQEWEXBsYWluQGV4YW1wbGUub3JnMCAXDTI2
|
||||
MTAyMjA5NDY0MVoYDzIxMjYwOTI4MDk0NjQxWjA+MRowGAYDVQQDDBFwbGFpbi5l
|
||||
eGFtcGxlLm9yZzEgMB4GCSqGSIb3DQEJARYRcGxhaW5AZXhhbXBsZS5vcmcwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq01NcNAy9Ng04mVuAFvmZUvsm
|
||||
rY7aOaMgtODmptulgZ2qw25E09RgGYCn1S+4IYDu9hH9X4s+28Ag7Khmj4nmeoXN
|
||||
of5095wche8DR2AAUElqyr/Nmgm2kTLdIJX4wl0eWXHIlmSPdOUNovA6JjGKH99b
|
||||
8W7a2TqY/TsSadpPSvvN2esr7Jr4KgW7MXPUOnwNbcBOVcQP9rtFrduAWz01q0HM
|
||||
02LboclnUnk/e58BXAj8vgdVojljDcc0bhbdz/Hh1/5gaaNILjJNOd4AgwZaCT02
|
||||
KojGz4Nb6MSgFmON2iMgAZjY1fXXkJ7wsfVPytjW73e7uV/RFuqSHQZenxupAgMB
|
||||
AAEwDQYJKoZIhvcNAQELBQADggEBAJX1DGuiSLoCKBDI3oCgAu7X86G01ovEgZJr
|
||||
8JrmGHW5q7ujjo53t46h8XPm3LJ8UV9/MZ+Yry43xB2txXYv3b3g/i+7JdTWLgUD
|
||||
INA5VWvVkEZfm0lBEpU1Agpy4lV5uUCKZTgP/3MY5hnJ0YhQIs4BOjbwueMRFshL
|
||||
yyHVb73MMbpWaZ7+T368seafa+ekeuY6gQly/HacuszjoYd6KKV3n8eDewenClCF
|
||||
HoOF1Y4I+CvmR+a6B3iBczJ+a4tceFIIWsZUIeqzvGHpM7G9YwlmbvGprfLgR8Nv
|
||||
igUib/D9tU3hdL6VWN5nMjdXV/8Le4nDnBjVBjYDkVbsnmT91AQ=
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
type Rec = string | Rec[];
|
||||
type RecSet = number | Set<RecSet>;
|
||||
|
||||
const cert = new X509Certificate(PLAIN);
|
||||
const rec: Rec = ["a", ["b"]];
|
||||
const set: RecSet = new Set([1, 2]);
|
||||
|
||||
function describe(value: unknown): string {
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
console.log(describe(rec), rec.length, describe(set), set.size);
|
||||
console.log(cert.subject);
|
||||
@@ -0,0 +1,32 @@
|
||||
// One class that extends Array sends every array method call in the program
|
||||
// through the species lowering, plain-array receivers included. Two callback
|
||||
// forms the species path used to refuse: a filter whose callback narrows the
|
||||
// element union (a type predicate, or a discriminant test tsc infers one
|
||||
// from), which wraps the filtered array in a checked-narrowing conversion,
|
||||
// and a flatMap whose callback returns a union holding an array arm.
|
||||
class Stack extends Array<number> {}
|
||||
|
||||
type Item = { kind: "a"; n: number } | { kind: "b"; s: string };
|
||||
const items: Item[] = [
|
||||
{ kind: "a", n: 1 },
|
||||
{ kind: "b", s: "x" },
|
||||
{ kind: "a", n: 2 },
|
||||
];
|
||||
const isA = (item: Item): item is { kind: "a"; n: number } => item.kind === "a";
|
||||
|
||||
const tagged = items.filter(isA);
|
||||
const inferred = items.filter((item) => item.kind === "b");
|
||||
const blocked = items.filter((item) => {
|
||||
return item.kind === "b";
|
||||
});
|
||||
const flattened = items.flatMap((item) => (item.kind === "a" ? [item.n] : []));
|
||||
const mixed = items.flatMap((item) => (item.kind === "a" ? [item.n] : undefined));
|
||||
console.log(tagged.length, tagged.map((item) => item.n).join(","));
|
||||
console.log(inferred.length, inferred.map((item) => item.s).join(","), blocked.length);
|
||||
console.log(flattened.join(","), mixed.length, mixed.join(","));
|
||||
|
||||
const stack = Stack.from([1, 2, 3]);
|
||||
const even = stack.filter((n) => n % 2 === 0);
|
||||
const doubled = stack.flatMap((n) => (n > 1 ? [n, n * 10] : []));
|
||||
console.log(even instanceof Stack, even.join(","), stack.length);
|
||||
console.log(doubled instanceof Stack, doubled.join(","), stack.length);
|
||||
@@ -0,0 +1,28 @@
|
||||
// `Array.isArray` over a computed argument: a call, an array literal, an
|
||||
// object literal. The answer comes from the argument's static type, so the
|
||||
// argument is evaluated for its effects and its value discarded. A union
|
||||
// argument keeps answering by its runtime tag, and side effects of the
|
||||
// argument survive the fold.
|
||||
function rows(): number[] {
|
||||
return [1, 2];
|
||||
}
|
||||
function label(): string {
|
||||
return "rows";
|
||||
}
|
||||
type Pair = [number, string];
|
||||
function pair(): Pair {
|
||||
return [3, "three"];
|
||||
}
|
||||
let evals = 0;
|
||||
function counted(): number[] {
|
||||
evals++;
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(Array.isArray(rows()), Array.isArray([1, 2]), Array.isArray({ a: 1 }));
|
||||
console.log(Array.isArray(label()), Array.isArray(pair()));
|
||||
console.log(Array.isArray(counted()), evals);
|
||||
const maybe: string | string[] = ["a"];
|
||||
const narrow: string | string[] = "a";
|
||||
console.log(Array.isArray(maybe), Array.isArray(narrow));
|
||||
console.log(Array.isArray(rows().map((n) => n * 2)));
|
||||
@@ -0,0 +1,25 @@
|
||||
// `Array.from(typedArray, mapper)`: the mapper receives each element as a
|
||||
// number and its index, and the element reads are LIVE — iteration reads
|
||||
// index i after visiting i-1, so a mapper that writes the source changes
|
||||
// the values the loop has not visited yet. The result is a plain array of
|
||||
// the mapper's results.
|
||||
|
||||
const bytes = new Uint8Array([104, 105, 33]);
|
||||
console.log(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""));
|
||||
console.log(Array.from(bytes, (byte, index) => `${index}:${byte}`).join(","));
|
||||
|
||||
const live = new Uint8Array([1, 2, 3]);
|
||||
const doubled = Array.from(live, (value, index) => {
|
||||
if (index === 0) live[2] = 30;
|
||||
return value * 2;
|
||||
});
|
||||
console.log(doubled.join(","));
|
||||
|
||||
// A mapper that ignores its parameters, and an empty source.
|
||||
console.log(Array.from(new Uint8Array(0), () => 7).length);
|
||||
console.log(Array.from(bytes, () => "x").join(""));
|
||||
|
||||
// Element identity survives the map: objects are not copied.
|
||||
const words = new Uint8Array([1, 2]);
|
||||
const boxed = Array.from(words, (word) => ({ word }));
|
||||
console.log(boxed.length, boxed[1]!.word);
|
||||
@@ -0,0 +1,72 @@
|
||||
// Untyped methods that return their receiver, called through untyped
|
||||
// values: chains, identity, subclasses, overrides and partial returns.
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
move(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
scale(k) {
|
||||
if (k === 0) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
return this;
|
||||
}
|
||||
this.x *= k;
|
||||
this.y *= k;
|
||||
return this;
|
||||
}
|
||||
maybe(flag) {
|
||||
if (flag) return this;
|
||||
return { x: -1 };
|
||||
}
|
||||
check(v) {
|
||||
if (v < 0) throw new RangeError("negative " + v);
|
||||
this.x += v;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Tagged extends Point {
|
||||
constructor(x, y, tag) {
|
||||
super(x, y);
|
||||
this.tag = tag;
|
||||
}
|
||||
move(dx, dy) {
|
||||
super.move(dx, dy);
|
||||
return this.tag;
|
||||
}
|
||||
}
|
||||
class Plain extends Point {}
|
||||
const points = [new Point(1, 2), new Tagged(3, 4, "t"), new Plain(5, 6), new Point(7, 8)];
|
||||
let sum = 0;
|
||||
for (let r = 0; r < 200; r++) {
|
||||
const p = points[r % points.length];
|
||||
const moved = p.move(1, -1);
|
||||
if (typeof moved === "object") {
|
||||
sum += moved.x + moved.y;
|
||||
if (moved !== p) throw new Error("identity lost");
|
||||
} else sum += moved.length;
|
||||
const scaled = p.scale(r % 3);
|
||||
sum += scaled === p ? 1 : 0;
|
||||
sum += p.maybe(r % 2 === 0).x;
|
||||
}
|
||||
console.log(sum);
|
||||
const chain = points[0].move(1, 1).scale(2).move(-1, 0);
|
||||
console.log(chain === points[0], chain.x, chain.y, chain instanceof Point);
|
||||
const plain = points[2];
|
||||
const r1 = plain.move(2, 2);
|
||||
r1.extra = "seen";
|
||||
console.log(plain.extra, r1 === plain, Object.keys(r1).join(","), r1.constructor.name);
|
||||
try {
|
||||
points[3].check(1).check(-2);
|
||||
} catch (e) {
|
||||
console.log(e instanceof RangeError, e.message, points[3].x);
|
||||
}
|
||||
const results = [];
|
||||
for (const p of points) results.push(p.scale(1));
|
||||
console.log(results.map((p, i) => p === points[i]).join(","));
|
||||
console.log(JSON.stringify(points));
|
||||
@@ -0,0 +1,13 @@
|
||||
// Module-level bindings of an untyped module that a typed module imports.
|
||||
export let total = 0;
|
||||
let hidden = 0;
|
||||
export function run(rounds) {
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
total += i * 3;
|
||||
hidden += i;
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
export function reset(value) {
|
||||
total = value;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// A typed module reads a live binding that untyped code updates in a loop.
|
||||
import { reset, run, total } from "./counter.mjs";
|
||||
|
||||
console.log(run(100), total);
|
||||
reset(2.5);
|
||||
console.log(run(10), total);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Module-level `let` bindings that untyped code updates in loops: numbers,
|
||||
// then values of other kinds, read by functions declared before and after.
|
||||
function readTotal() {
|
||||
return total;
|
||||
}
|
||||
let total = 0;
|
||||
let label = "n";
|
||||
let mixed = 1;
|
||||
let big = 10n;
|
||||
let counter = 0;
|
||||
var hoisted;
|
||||
function bump(n) {
|
||||
return n * 2 + 1;
|
||||
}
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
total += bump(i);
|
||||
label += i % 10 === 0 ? i : "";
|
||||
mixed = mixed * 1.5 + i;
|
||||
big += BigInt(i);
|
||||
counter++;
|
||||
}
|
||||
console.log(total, readTotal(), label.length, mixed, big, counter);
|
||||
total = total / 0;
|
||||
console.log(total, readTotal());
|
||||
total = -0;
|
||||
console.log(Object.is(total, -0), 1 / total);
|
||||
total = NaN;
|
||||
console.log(total === total, String(total));
|
||||
mixed = "text";
|
||||
mixed += 5;
|
||||
console.log(mixed, typeof mixed);
|
||||
mixed = { value: 3 };
|
||||
mixed.value += 4;
|
||||
console.log(mixed.value, JSON.stringify(mixed));
|
||||
mixed = undefined;
|
||||
console.log(mixed, typeof mixed, mixed ?? "fallback");
|
||||
mixed = null;
|
||||
console.log(mixed === null, `${mixed}`);
|
||||
hoisted = 0;
|
||||
for (let i = 0; i < 10; i++) hoisted += i * 0.1;
|
||||
console.log(hoisted);
|
||||
const add = (n) => {
|
||||
total = (Number.isNaN(total) ? 0 : total) + n;
|
||||
return total;
|
||||
};
|
||||
for (let i = 0; i < 5; i++) add(i);
|
||||
console.log(total, readTotal());
|
||||
let product = 1;
|
||||
for (let i = 1; i <= 25; i++) product *= i;
|
||||
console.log(product, product > Number.MAX_SAFE_INTEGER);
|
||||
let text = 0;
|
||||
for (let i = 0; i < 3; i++) text = text + "" + i;
|
||||
console.log(text, typeof text);
|
||||
@@ -0,0 +1,14 @@
|
||||
// writeFileSync(fd, data): Node accepts an open file descriptor where it
|
||||
// accepts a path, writes the whole data from the descriptor's current
|
||||
// offset, and leaves the descriptor open.
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const scratch = path.join(os.tmpdir(), `scriptc-write-fd-${process.pid}.txt`);
|
||||
const fd = fs.openSync(scratch, "w");
|
||||
fs.writeFileSync(fd, "hello ");
|
||||
fs.writeFileSync(fd, "world");
|
||||
fs.closeSync(fd);
|
||||
console.log(fs.readFileSync(scratch, "utf8"));
|
||||
fs.rmSync(scratch);
|
||||
@@ -0,0 +1,64 @@
|
||||
// `?.` reaches a promise method through a receiver that may be nullish:
|
||||
// `p?.catch(handler)` guards the method read (JS calls nothing when `p` is
|
||||
// nullish), and the chain tail `f()?.then(..).catch(..)` guards the whole
|
||||
// tail. The guarded call itself is an ordinary promise method call, so the
|
||||
// handler typing sees the promise arm, and the chain result is
|
||||
// `Promise<..> | undefined`.
|
||||
|
||||
interface Client {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class Box {
|
||||
opening: Promise<Client> | undefined;
|
||||
pending: Promise<void> | undefined;
|
||||
|
||||
constructor(opening: Promise<Client> | undefined, pending: Promise<void> | undefined) {
|
||||
this.opening = opening;
|
||||
this.pending = pending;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.opening?.catch(() => undefined);
|
||||
console.log("1 opening guarded");
|
||||
await this.pending?.catch(() => undefined);
|
||||
console.log("2 pending guarded");
|
||||
}
|
||||
}
|
||||
|
||||
function open(name: string): Promise<Client> {
|
||||
return Promise.resolve({ name });
|
||||
}
|
||||
|
||||
function pick(flag: boolean): Promise<string> | undefined {
|
||||
return flag ? Promise.resolve("value") : undefined;
|
||||
}
|
||||
|
||||
function tail(flag: boolean): Promise<number | undefined> {
|
||||
return pick(flag)!
|
||||
.then((value) => {
|
||||
console.log("tail then", value);
|
||||
return value.length;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
function guardedTail(flag: boolean): void {
|
||||
pick(flag)?.then((value) => {
|
||||
console.log("guarded then", value);
|
||||
return value.length;
|
||||
});
|
||||
console.log("3 tail attached", flag);
|
||||
}
|
||||
|
||||
const present = new Box(open("digest"), Promise.resolve());
|
||||
const absent = new Box(undefined, undefined);
|
||||
present
|
||||
.run()
|
||||
.then(() => absent.run())
|
||||
.then(() => tail(true))
|
||||
.then(() => {
|
||||
guardedTail(true);
|
||||
guardedTail(false);
|
||||
})
|
||||
.then(() => console.log("done"));
|
||||
@@ -0,0 +1,34 @@
|
||||
// `Promise.resolve(u)` over a promise-or-unit union (`Promise<void> |
|
||||
// void`): Node adopts a promise operand — the SAME promise comes
|
||||
// back — and fulfills a nullish one as undefined. The settled turn of the
|
||||
// adopted promise is unchanged, so a handler chain observes the same
|
||||
// microtask order either way.
|
||||
|
||||
let runs = 0;
|
||||
|
||||
async function step(label: string): Promise<void> {
|
||||
runs++;
|
||||
await Promise.resolve();
|
||||
console.log("step", label, runs);
|
||||
}
|
||||
|
||||
// A callback whose declared result is `Promise<void> | void`: the
|
||||
// async path answers a promise, the sync one answers undefined.
|
||||
function handler(flag: boolean): Promise<void> | void {
|
||||
if (flag) return step("async");
|
||||
runs++;
|
||||
console.log("sync handler", runs);
|
||||
}
|
||||
|
||||
function call(flag: boolean): Promise<void> {
|
||||
return Promise.resolve(handler(flag)).then(() => console.log("call done", flag));
|
||||
}
|
||||
|
||||
const pending: Promise<void> | void = Promise.resolve();
|
||||
console.log("identity", Promise.resolve(pending) === pending);
|
||||
|
||||
console.log("start");
|
||||
call(true)
|
||||
.then(() => call(false))
|
||||
.then(() => call(true))
|
||||
.then(() => console.log("runs", runs));
|
||||
@@ -0,0 +1,31 @@
|
||||
// `Promise<void>.catch(() => undefined)` — the handler's value is
|
||||
// discarded and the recovered promise settles as void. The handler runs
|
||||
// exactly once, and the chain continues on the next turn whether the
|
||||
// receiver fulfilled or rejected.
|
||||
|
||||
const ok: Promise<void> = Promise.resolve();
|
||||
const bad: Promise<void> = Promise.reject(new Error("boom"));
|
||||
|
||||
ok.catch(() => undefined).then(() => console.log("fulfilled recovered"));
|
||||
|
||||
bad
|
||||
.catch(() => {
|
||||
console.log("rejected recovered");
|
||||
})
|
||||
.then(() => console.log("rejected continued"));
|
||||
|
||||
let runs = 0;
|
||||
|
||||
function recover(): Promise<void> {
|
||||
runs++;
|
||||
return runs % 2 === 1 ? Promise.reject(new Error("odd")) : Promise.resolve();
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
await recover().catch(() => undefined);
|
||||
console.log("awaited recover", runs);
|
||||
await recover().catch(() => undefined);
|
||||
console.log("awaited recover", runs);
|
||||
}
|
||||
|
||||
void run().then(() => console.log("done"));
|
||||
@@ -0,0 +1,24 @@
|
||||
// Awaiting a union whose promise arm settles a union
|
||||
// (`Promise<A | B> | undefined`): the promise arm is awaited, and a
|
||||
// non-promise arm answers its own value after one microtask turn.
|
||||
function maybe(flag: boolean): Promise<string | null> | undefined {
|
||||
if (!flag) return undefined;
|
||||
return Promise.resolve(flag ? "value" : null);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const absent = await maybe(false);
|
||||
console.log(String(absent));
|
||||
const settled = await maybe(true);
|
||||
console.log(String(settled));
|
||||
const nil = await maybe(true);
|
||||
console.log(nil === null);
|
||||
|
||||
// Awaiting a non-promise arm takes exactly one microtask turn.
|
||||
const order: string[] = [];
|
||||
Promise.resolve().then(() => order.push("microtask"));
|
||||
const hopped = await maybe(false);
|
||||
order.push(`after:${String(hopped)}`);
|
||||
console.log(order.join(","));
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,73 @@
|
||||
// Object patterns over a union whose arms type a field differently (or do
|
||||
// not declare it), narrowed sources, and rest elements over a union.
|
||||
interface Wide {
|
||||
kind: "wide";
|
||||
value: number;
|
||||
extra?: string;
|
||||
}
|
||||
interface Narrow {
|
||||
kind: "narrow";
|
||||
value: string;
|
||||
extra?: undefined;
|
||||
}
|
||||
type Item = Wide | Narrow;
|
||||
|
||||
// The joined read: `number | string`, `string | undefined`.
|
||||
function read(item: Item): void {
|
||||
const { value, extra } = item;
|
||||
console.log(item.kind, String(value), String(extra));
|
||||
}
|
||||
read({ kind: "wide", value: 7, extra: "e" });
|
||||
read({ kind: "narrow", value: "v" });
|
||||
|
||||
interface Delta {
|
||||
type: "delta";
|
||||
partial: { text: string };
|
||||
other: number;
|
||||
}
|
||||
interface Final {
|
||||
type: "final";
|
||||
partial: { text: number };
|
||||
tail: boolean;
|
||||
}
|
||||
interface Bare {
|
||||
type: "bare";
|
||||
other: number;
|
||||
}
|
||||
type Wire = Delta | Final | Bare;
|
||||
|
||||
// A rest element over a union packs each arm's own remaining fields.
|
||||
function rest(event: Delta | Final): string {
|
||||
const { partial: _partial, ...kept } = event;
|
||||
return JSON.stringify(kept);
|
||||
}
|
||||
console.log(rest({ type: "delta", partial: { text: "t" }, other: 2 }));
|
||||
console.log(rest({ type: "final", partial: { text: 3 }, tail: true }));
|
||||
|
||||
// A discriminant test and an `in` guard narrow the source; the pattern
|
||||
// reads the arms the checker proved.
|
||||
function narrowed(event: Wire): string {
|
||||
if (event.type === "delta") {
|
||||
const { partial, other } = event;
|
||||
return `tag:${partial.text}:${other}`;
|
||||
}
|
||||
if ("partial" in event) {
|
||||
const { partial } = event;
|
||||
return `in:${String(partial.text)}`;
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
console.log(narrowed({ type: "delta", partial: { text: "t" }, other: 2 }));
|
||||
console.log(narrowed({ type: "final", partial: { text: 3 }, tail: true }));
|
||||
console.log(narrowed({ type: "bare", other: 4 }));
|
||||
|
||||
// An `in` guard alone narrows to a SUB-union: both remaining arms keep
|
||||
// their own field types.
|
||||
function inOnly(w: Wire): string {
|
||||
if (!("partial" in w)) return "none";
|
||||
const { partial } = w;
|
||||
return String(partial.text);
|
||||
}
|
||||
console.log(inOnly({ type: "delta", partial: { text: "t" }, other: 2 }));
|
||||
console.log(inOnly({ type: "final", partial: { text: 3 }, tail: true }));
|
||||
console.log(inOnly({ type: "bare", other: 4 }));
|
||||
@@ -42,7 +42,7 @@ interface E2 {
|
||||
label: string;
|
||||
}
|
||||
function fromUnion(u: E1 | E2): string {
|
||||
const { b } = u; // union source whose arms type the field differently: narrow first
|
||||
const { b } = u; // arms type the field differently: the joined read compiles
|
||||
return String(b);
|
||||
}
|
||||
console.log(fromUnion({ kind: "one", b: "s", count: 1 }));
|
||||
|
||||
@@ -12,7 +12,7 @@ const g = globalThis;
|
||||
const nan = NaN; // lowers now — a non-finite numeric literal like Infinity
|
||||
const s5 = String(5);
|
||||
const n5 = Number.NaN; // lowers now, with the other Number constants
|
||||
const isArr = Array.isArray([1]);
|
||||
const isArr = Array.isArray([1]); // lowers now: a computed argument folds after it is evaluated
|
||||
const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
const aliasedAssign = Object.assign(merged, { b: 2 }); // aliased targets stay fenced (real mutation of a live object)
|
||||
const refl = Reflect.has({ a: 1 }, "a");
|
||||
|
||||
@@ -19,22 +19,6 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a
|
||||
| ^~~~~~
|
||||
32 | console.log(onlyIn);
|
||||
|
||||
destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet
|
||||
|
||||
44 | function fromUnion(u: E1 | E2): string {
|
||||
45 | const { b } = u; // union source whose arms type the field differently: narrow first
|
||||
| ^
|
||||
46 | return String(b);
|
||||
|
||||
destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration
|
||||
|
||||
45 | const { b } = u; // union source whose arms type the field differently: narrow first
|
||||
46 | return String(b);
|
||||
| ^
|
||||
47 | }
|
||||
|
||||
hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
destructuring-fences.ts:51:7 - error SC1031: object destructuring of non-record values (the source is array-typed) are not supported yet
|
||||
|
||||
50 | const xs = [1, 2, 3];
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
stdlib-fence.ts:15:15 - error SC1090: statically-decided Array.isArray on computed arguments (bind the value to a variable first) are not supported yet
|
||||
|
||||
14 | const n5 = Number.NaN; // lowers now, with the other Number constants
|
||||
15 | const isArr = Array.isArray([1]);
|
||||
| ^~~~~~~~~~~~~~~~~~
|
||||
16 | const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
|
||||
stdlib-fence.ts:17:23 - error SC2020: 'Object.assign' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
16 | const merged = Object.assign({}, { a: 1 }); // the empty-target literal-source shape lowers now (it IS the source literal)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2100, "san": 1600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1850, "san": 1250 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 1375 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1400, "san": 1200 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3750, "san": 5900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1000, "san": 900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 900, "san": 900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1600, "san": 900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3150, "san": 4400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2900, "san": 2900 }
|
||||
Reference in New Issue
Block a user