mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -15,3 +15,6 @@ JavaScript argument and return conversions preserve native array references; oth
|
||||
|
||||
Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`.
|
||||
Cast before using other operations.
|
||||
|
||||
In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it.
|
||||
In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used.
|
||||
|
||||
@@ -385,7 +385,10 @@ test("while loop counters below a bound version their accesses", () => {
|
||||
]);
|
||||
const declared = (loop: IrStmt): IrFunction =>
|
||||
fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
|
||||
const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => {
|
||||
const versionOf = (
|
||||
loop: IrStmt & { kind: "while" },
|
||||
workers = false,
|
||||
): BytesLoopVersion | null => {
|
||||
const f = declared(loop);
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
return matchBytesLoopVersion(loop, null, {
|
||||
@@ -396,7 +399,7 @@ test("while loop counters below a bound version their accesses", () => {
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
scalarStrings: new Set(),
|
||||
workers: false,
|
||||
workers,
|
||||
owned: new Set(),
|
||||
});
|
||||
};
|
||||
@@ -413,6 +416,11 @@ test("while loop counters below a bound version their accesses", () => {
|
||||
// An access after the counter advances in the same pass stays checked.
|
||||
const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]);
|
||||
expect(versionOf(advanced)).toBeNull();
|
||||
// In a worker program, a while loop versions for its counter's accesses,
|
||||
// and not for thread-owned receivers alone.
|
||||
expect(versionOf(copy, true)!.ownedReceivers).toHaveLength(2);
|
||||
const job = whileLoop([set(ref("out", f64s), ref("k"), get(ref("src", f64s), ref("k"))), step]);
|
||||
expect(versionOf(job, true)).toBeNull();
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
|
||||
@@ -581,6 +581,11 @@ export function matchBytesLoopVersion(
|
||||
pairedKept.length === 0
|
||||
)
|
||||
return null;
|
||||
// A while loop versions only for accesses at its counters. Its other
|
||||
// checks (thread-owned receivers, static indices) save a test or two per
|
||||
// pass, while a second copy of its body doubles the calls it makes, which
|
||||
// can keep a worker's job loop from inlining the function that runs a job.
|
||||
if (loop.kind === "while" && boundedKept.length === 0) return null;
|
||||
const bounds = new Set<Node>();
|
||||
const ownedNodes = new Set<Node>();
|
||||
const used = new Set<number>();
|
||||
|
||||
@@ -993,6 +993,28 @@ export function streamTypedRefMaterializeAdapter(
|
||||
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
|
||||
);
|
||||
}
|
||||
// A JavaScript Error subclass's property bag records, once a dynamic
|
||||
// write has committed the view, where `name` and `message` were created
|
||||
// (zod's error factory assigns `name` before it defines `message`); the
|
||||
// slots hold their values. A `name` the class's construction sets is
|
||||
// already where errorNameAfter places it among the fields.
|
||||
if (
|
||||
meta.def.errorBagOrder &&
|
||||
meta.root.def.name === "%Error" &&
|
||||
meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)
|
||||
) {
|
||||
host.declare(`declare void @scr_error_dyn_follow_bag(ptr, ptr, ptr, i1)`);
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
const slot = B.tmp(),
|
||||
bag = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
B.line(`${bag} = load ptr, ptr ${slot}`);
|
||||
B.line(
|
||||
`call void @scr_error_dyn_follow_bag(ptr ${out}, ptr ${bag}, ptr %p, i1 ${nameAfter === undefined ? "false" : "true"})`,
|
||||
);
|
||||
}
|
||||
// A frozen, sealed or non-extensible instance's view says so.
|
||||
if (withAttributes) {
|
||||
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
|
||||
|
||||
@@ -22,7 +22,10 @@ import { builtinImportOf } from "./module-bindings.js";
|
||||
* lib/internal/fs/glob.js). The pattern and options cross as dyn values so
|
||||
* the runtime validates them in Node's order. The result form (strings or
|
||||
* Dirent rows) comes from the call's mapped result type, so withFileTypes
|
||||
* must be statically known; the runtime checks that the option agrees. */
|
||||
* must be statically known; the runtime checks that the option agrees.
|
||||
* Because of that check, the form follows the checker's resolved overload
|
||||
* even where an untyped options argument leaves the overload undecided
|
||||
* (dynamicOverloadChoice). */
|
||||
|
||||
/** The interned Dirent record array (type-mapper.ts), or null. */
|
||||
function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean {
|
||||
@@ -67,7 +70,7 @@ export function lowerFsGlobSyncCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = globArgs(lowerer, call, "fs.globSync");
|
||||
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const callType = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(call));
|
||||
const types = direntArray(lowerer, callType);
|
||||
if (types) fenceDirentExclude(lowerer, call, "fs.globSync");
|
||||
if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) {
|
||||
@@ -132,7 +135,9 @@ export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc:
|
||||
}
|
||||
const args = globArgs(lowerer, call, "fs/promises glob");
|
||||
// The async iterator's yielded type (its first type argument).
|
||||
const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0];
|
||||
const yielded = lowerer.checker.getTypeArguments(
|
||||
lowerer.checker.getTypeAtLocation(call) as ts.TypeReference,
|
||||
)[0];
|
||||
const element = yielded ? lowerer.mapTypeOf(yielded) : null;
|
||||
const rows: IrType | null = element ? { kind: "array", elem: element } : null;
|
||||
const types = direntArray(lowerer, rows);
|
||||
|
||||
@@ -717,6 +717,105 @@ export function classForInHasKey(
|
||||
return { kind: "call", callee: name, args: [value, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** Box class instances at their exact class. `%dyn.class.normalize` turns a
|
||||
* capsule of a class with subclasses into a capsule of the instance's own
|
||||
* class before any member table reads it, so every such box was followed
|
||||
* by a second capsule. Each conversion of a value whose static class has a
|
||||
* normalization branch now selects the exact class at the conversion, with
|
||||
* the same tests in the same order (%dyn.class.boxExact:C), and the
|
||||
* normalization of the resulting capsule falls through. Only JavaScript
|
||||
* class hierarchies, whose instances live in untyped code, take the exact
|
||||
* box. */
|
||||
export function boxClassInstancesExactly(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const normalize = lowerer.liftedFnNamed("%dyn.class.normalize");
|
||||
if (!normalize) return;
|
||||
// Static class → its constructible descendants in normalization order.
|
||||
const branches = new Map<string, string[]>();
|
||||
for (const stmt of normalize.body) {
|
||||
if (stmt.kind !== "if" || stmt.cond.kind !== "libCall" || stmt.cond.fn !== "dyn.typedRefIs")
|
||||
continue;
|
||||
const key = stmt.cond.args[1];
|
||||
if (key?.kind !== "strLit" || !key.value.startsWith("object:")) continue;
|
||||
const children: string[] = [];
|
||||
for (const inner of stmt.then) {
|
||||
if (inner.kind !== "if" || inner.cond.kind !== "instanceOf") continue;
|
||||
children.push(inner.cond.className);
|
||||
}
|
||||
// TypeScript hierarchies keep their static-class capsules: their boxes
|
||||
// are rarer, and an exact box would emit each subclass's checked view.
|
||||
const className = key.value.slice("object:".length);
|
||||
const decl = lowerer.classes.get(className)?.decl;
|
||||
if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) continue;
|
||||
if (children.length) branches.set(className, children);
|
||||
}
|
||||
if (branches.size === 0) return;
|
||||
const loc = normalize.loc;
|
||||
const helpers = new Map<string, string>();
|
||||
const helperFor = (className: string): string => {
|
||||
const known = helpers.get(className);
|
||||
if (known !== undefined) return known;
|
||||
const name = `%dyn.class.boxExact:${className}`;
|
||||
helpers.set(className, name);
|
||||
const type: IrType = { kind: "object", className };
|
||||
const value = varRef("value", type, loc);
|
||||
functions.push({
|
||||
name,
|
||||
params: [{ localId: "value", name: "value", type }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "value", name: "value", type, mutable: false }],
|
||||
body: [
|
||||
...branches.get(className)!.map((child): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "instanceOf", value, className: child, type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "downcast",
|
||||
value,
|
||||
type: { kind: "object", className: child },
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
})),
|
||||
{ kind: "return", value: { kind: "dynFrom", value, type: DYN, loc }, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
};
|
||||
const rewrite = (fn: IrFunction): void => {
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (statement) => statement,
|
||||
expr: (expression) =>
|
||||
expression.kind === "dynFrom" &&
|
||||
expression.value.type.kind === "object" &&
|
||||
branches.has(expression.value.type.className)
|
||||
? {
|
||||
kind: "call",
|
||||
callee: helperFor(expression.value.type.className),
|
||||
args: [expression.value],
|
||||
type: DYN,
|
||||
loc: expression.loc,
|
||||
}
|
||||
: expression,
|
||||
});
|
||||
};
|
||||
for (const fn of [...functions]) {
|
||||
if (fn === normalize || fn.name.startsWith("%dyn.class.boxExact:")) continue;
|
||||
rewrite(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/** Record field creation after the dispatch worklist closes, including writes
|
||||
* in generated member tables. Native values stay in their slots; the bag
|
||||
* owns only their property presence, attributes, and insertion position. */
|
||||
@@ -1634,6 +1733,15 @@ export class ClassDynamicDispatch {
|
||||
if (this.deletedNames === undefined)
|
||||
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
|
||||
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
|
||||
// An Error family's bag records where `name` and `message` were
|
||||
// created, which the error slots do not.
|
||||
if (root.base?.def.name === "%Error") {
|
||||
const order = (current: ClassInfo): void => {
|
||||
current.def.errorBagOrder = true;
|
||||
current.subclasses.forEach(order);
|
||||
};
|
||||
order(info);
|
||||
}
|
||||
}
|
||||
const loc = this.propertyBag.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
|
||||
@@ -46,6 +46,13 @@ export function collectClassMethodMutations(
|
||||
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
|
||||
const symbolMethods = new Set<string>();
|
||||
let unknownKey: ts.Node | undefined;
|
||||
/** Every write with an unknown key: each may install a replacement of any
|
||||
* method name, and the key-order witness asks what each one installs. */
|
||||
const unknownKeySites: ts.Node[] = [];
|
||||
const markUnknownKey = (site: ts.Node): void => {
|
||||
unknownKey = site;
|
||||
unknownKeySites.push(site);
|
||||
};
|
||||
let numericKey: ts.Node | undefined;
|
||||
let unknownOwnField = false;
|
||||
// `sym:iterator` for `Symbol.iterator`: the name a class method declared
|
||||
@@ -83,7 +90,7 @@ export function collectClassMethodMutations(
|
||||
for (const key of computedMethods)
|
||||
for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name));
|
||||
const markName = (name: string, site: ts.Node, arrow = false): void => {
|
||||
if (name === "prototype" || name === "__proto__") unknownKey = site;
|
||||
if (name === "prototype" || name === "__proto__") markUnknownKey(site);
|
||||
if (methods.has(name)) lowerer.noteMethodAccess(name, site);
|
||||
if (!arrow) receiverFields.add(name);
|
||||
};
|
||||
@@ -449,11 +456,12 @@ export function collectClassMethodMutations(
|
||||
(lowerer.isStdlibGlobal(node, "Object") || lowerer.isStdlibGlobal(node, "Reflect")) &&
|
||||
(!ts.isPropertyAccessExpression(node.parent) || node.parent.expression !== node)
|
||||
)
|
||||
unknownKey = node;
|
||||
markUnknownKey(node);
|
||||
for (const node of reflection) {
|
||||
const objectMethod = lowerer.stdlibGlobalMember(node, "Object");
|
||||
const reflectMethod = lowerer.stdlibGlobalMember(node, "Reflect");
|
||||
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") unknownKey = node;
|
||||
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
|
||||
markUnknownKey(node);
|
||||
const single =
|
||||
objectMethod === "defineProperty" ||
|
||||
reflectMethod === "defineProperty" ||
|
||||
@@ -463,7 +471,7 @@ export function collectClassMethodMutations(
|
||||
if (!single && !multiple) continue;
|
||||
const call = node.parent;
|
||||
if (!ts.isCallExpression(call) || call.expression !== node) {
|
||||
unknownKey = node;
|
||||
markUnknownKey(node);
|
||||
continue;
|
||||
}
|
||||
const receiver = call.arguments.nodes[0];
|
||||
@@ -618,7 +626,7 @@ export function collectClassMethodMutations(
|
||||
// Numeric writes can replace numeric names but not ordinary names.
|
||||
numericKey = site;
|
||||
} else if (!(flags & ts.TypeFlags.ESSymbolLike)) {
|
||||
unknownKey = site;
|
||||
markUnknownKey(site);
|
||||
}
|
||||
}
|
||||
if (numericKey) {
|
||||
@@ -627,7 +635,10 @@ export function collectClassMethodMutations(
|
||||
}
|
||||
}
|
||||
if (unknownKey) {
|
||||
for (const method of methods) lowerer.noteMethodAccess(method, unknownKey);
|
||||
for (const method of methods) {
|
||||
lowerer.prototypeMethodAccesses.set(method, unknownKey);
|
||||
for (const site of unknownKeySites) lowerer.noteMethodSite(method, site);
|
||||
}
|
||||
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
|
||||
}
|
||||
if (!unknownKey && !unknownOwnField)
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PoisonError, dynFallbackType } from "./lowerer.js";
|
||||
import { functionCanReturnUndefined } from "../function-completion.js";
|
||||
import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
|
||||
import type { FnCtx } from "./function-context.js";
|
||||
import { returnsGuessedOverloadValue } from "./overload-choice.js";
|
||||
|
||||
/** Keep the conservative completion rule shared by return construction
|
||||
* and inference. Nested control flow may still make completion unreachable. */
|
||||
@@ -256,6 +257,22 @@ export function declaredReturnType(
|
||||
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
? { kind: "promise", inner: DYN }
|
||||
: DYN;
|
||||
// A JS function returning a value whose type rests on an undecided
|
||||
// overload (guessedOverloadValue): the inferred result is the first
|
||||
// overload's, which the returned value need not have.
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!decl.type &&
|
||||
!decl.asteriskToken &&
|
||||
!hasExplicitJsDocReturn(decl) &&
|
||||
mappedReturn !== null &&
|
||||
mappedReturn.kind !== "dyn" &&
|
||||
!(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") &&
|
||||
returnsGuessedOverloadValue(lowerer, decl)
|
||||
)
|
||||
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
? { kind: "promise", inner: DYN }
|
||||
: DYN;
|
||||
// JS inference assigns Number to arithmetic over any, although the
|
||||
// runtime operands can both be BigInts. Preserve that native result.
|
||||
if (
|
||||
|
||||
@@ -29,7 +29,7 @@ interface Fixture {
|
||||
notes: KeyOrderNotes;
|
||||
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
|
||||
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
|
||||
run(body: IrStmt[]): IrFunction;
|
||||
run(body: IrStmt[], name?: string): IrFunction;
|
||||
order(type: IrType & { kind: "record" }): string[] | undefined;
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ function fixture(): Fixture {
|
||||
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
|
||||
);
|
||||
};
|
||||
const run = (body: IrStmt[]): IrFunction => {
|
||||
const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
|
||||
const run = (body: IrStmt[], name = "main"): IrFunction => {
|
||||
const fn: IrFunction = { name, params: [], returnType: F64, locals: [], body, loc };
|
||||
applyKeyOrderWitnesses(lowerer, [fn]);
|
||||
return fn;
|
||||
};
|
||||
@@ -706,7 +706,10 @@ test("a spread reads its source's order right after the literal evaluates the so
|
||||
|
||||
/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }`
|
||||
* plus `{ ...value, a }` of the same shape, beside `extra`. */
|
||||
function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] {
|
||||
function selfCopy(
|
||||
extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[],
|
||||
fnName = "main",
|
||||
): string[] {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
@@ -716,11 +719,14 @@ function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt
|
||||
["a", "b"],
|
||||
);
|
||||
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false };
|
||||
f.run([
|
||||
decl("v", f.literal(ab, ["b", "a"])),
|
||||
observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
|
||||
...extra(f, ab),
|
||||
]);
|
||||
f.run(
|
||||
[
|
||||
decl("v", f.literal(ab, ["b", "a"])),
|
||||
observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
|
||||
...extra(f, ab),
|
||||
],
|
||||
fnName,
|
||||
);
|
||||
return f.order(ab)!;
|
||||
}
|
||||
|
||||
@@ -762,6 +768,31 @@ test("a replaceable method's checked result builds records when a replacement ma
|
||||
).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
/** A native callback taking the shape, boxed for JavaScript. */
|
||||
const boxedCallback = (ab: IrType): IrExpr => ({
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: "callback",
|
||||
captures: [],
|
||||
type: { kind: "func", params: [ab], ret: { kind: "void" } },
|
||||
loc,
|
||||
},
|
||||
type: { kind: "dyn" },
|
||||
loc,
|
||||
});
|
||||
|
||||
test("a native callback boxed for JavaScript receives records JavaScript builds", () => {
|
||||
expect(selfCopy((_, ab) => [observe(boxedCallback(ab))])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a class's prototype slots box its methods without building their parameters", () => {
|
||||
expect(selfCopy((_, ab) => [observe(boxedCallback(ab))], "%prototype.data.Service")).toEqual([
|
||||
"b",
|
||||
"a",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a replaceable method's result keeps its witness through an IR rewrite", () => {
|
||||
expect(
|
||||
selfCopy((f, ab) => {
|
||||
|
||||
@@ -292,6 +292,17 @@ function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** A compiler helper that boxes a class's own state for JavaScript's reads:
|
||||
* its prototype data and the property and method dispatch over boxed
|
||||
* instances. */
|
||||
function boxesOwnState(fnName: string): boolean {
|
||||
return (
|
||||
fnName.startsWith("%prototype.data.") ||
|
||||
fnName.startsWith("%dyn.class.") ||
|
||||
fnName.startsWith("%class.")
|
||||
);
|
||||
}
|
||||
|
||||
/** A type whose values are primitives (or void): it holds no record. */
|
||||
function primitiveOnly(type: ts.Type): boolean {
|
||||
if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly);
|
||||
@@ -357,8 +368,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
* its checked field setters, each a witness of its own. A checked
|
||||
* function's parameters receive native values: a record parameter is
|
||||
* passed out, never built, though a callback among them receives values
|
||||
* built from JavaScript again. */
|
||||
const shapesIn = (type: IrType, out: Set<string>, seen: Set<string>, produced = false): void => {
|
||||
* built from JavaScript again. `built` false starts the walk at a value
|
||||
* passed out (a native value boxed for JavaScript); `writes` false leaves
|
||||
* out what JavaScript could write into the containers passed out, so the
|
||||
* walk counts only what the functions among them receive. */
|
||||
const shapesIn = (
|
||||
type: IrType,
|
||||
out: Set<string>,
|
||||
seen: Set<string>,
|
||||
produced = false,
|
||||
built = true,
|
||||
writes = true,
|
||||
): void => {
|
||||
const visit = (t: IrType, built: boolean): boolean => {
|
||||
const mark = built ? "" : "-";
|
||||
// A container JavaScript builds converts into fresh native storage, so
|
||||
@@ -367,7 +388,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
// such as callbacks whose parameters it then builds (children passed
|
||||
// out). Those children count in both directions.
|
||||
const child = (type: IrType): boolean => {
|
||||
visit(type, true);
|
||||
if (built || writes) visit(type, true);
|
||||
if (!built) visit(type, false);
|
||||
return true;
|
||||
};
|
||||
@@ -401,7 +422,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
}
|
||||
return everyTypeChild(t, child);
|
||||
};
|
||||
visit(type, true);
|
||||
visit(type, built);
|
||||
};
|
||||
const opaqueShapes = new Set<string>();
|
||||
const opaqueSeen = new Set<string>();
|
||||
@@ -412,6 +433,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
* declaration's order, the order their table holds. */
|
||||
const declaredShapes = new Set<string>();
|
||||
const declaredSeen = new Set<string>();
|
||||
const boxedSeen = new Set<string>();
|
||||
/** The checked result of a replaceable method call while every site that
|
||||
* can replace the method installs functions that return no record. */
|
||||
const unchangedMethodResult = (node: IrExpr): boolean => {
|
||||
@@ -518,6 +540,16 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
case "jsonStringify":
|
||||
case "dynFrom":
|
||||
observedRoots.push(e.value.type);
|
||||
// A native value the program hands to JavaScript: the callbacks it
|
||||
// holds (itself, a field, an element) receive records JavaScript
|
||||
// builds. The helpers that box the program's own state for
|
||||
// JavaScript's reads (a class's prototype slots and property
|
||||
// dispatch) hand out the class's methods, whose arguments the
|
||||
// program's own calls pass as native values, as a replaceable
|
||||
// method's result above. Writes through the boxed value are left
|
||||
// out: reading a boxed value creates no record.
|
||||
if (!boxesOwnState(fnName))
|
||||
shapesIn(e.value.type, runtimeShapes, boxedSeen, true, false, false);
|
||||
return;
|
||||
case "recordKeyGet":
|
||||
observedRoots.push(e.obj.type);
|
||||
|
||||
@@ -1355,7 +1355,7 @@ function lowerAssertMatch(
|
||||
lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr);
|
||||
}
|
||||
const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
|
||||
const re = lowerer.lowerExpr(expr.arguments.nodes[1]!);
|
||||
const re = lowerExpectedValue(lowerer, expr.arguments.nodes[1]!);
|
||||
if (re.type.kind !== "regex") {
|
||||
lowerer.noLowering(
|
||||
`${surface} with a non-regex pattern`,
|
||||
@@ -1420,6 +1420,18 @@ function inErrorHierarchy(lowerer: Lowerer, className: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** An expected string or regex: a value JavaScript stores checked-dynamic
|
||||
* (an object literal's field, an untyped binding) converts to the string
|
||||
* or regex its checker type names, so the comparison takes that arm. */
|
||||
function lowerExpectedValue(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind !== "dyn") return value;
|
||||
const expected = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
return expected?.kind === "string" || expected?.kind === "regex"
|
||||
? lowerer.coerceToExpected(value, expected)
|
||||
: value;
|
||||
}
|
||||
|
||||
/** Classify the expected argument: string → message, regex value → the
|
||||
* regex test over String(error), an error-class identifier → instanceof,
|
||||
* an object LITERAL over name/message/code (string or regex values) →
|
||||
@@ -1436,7 +1448,7 @@ function classifyThrowsExpected(
|
||||
return { form: "message", msg: m.msg, hasMsg: m.hasMsg };
|
||||
}
|
||||
if (expectedT?.kind === "regex") {
|
||||
return { form: "regex", value: lowerer.lowerExpr(node) };
|
||||
return { form: "regex", value: lowerExpectedValue(lowerer, node) };
|
||||
}
|
||||
const sym = ts.isIdentifier(node) ? lowerer.resolveValueSymbol(node) : null;
|
||||
const info =
|
||||
@@ -1509,7 +1521,7 @@ function classifyThrowsExpected(
|
||||
);
|
||||
}
|
||||
const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name;
|
||||
const value = lowerer.lowerExpr(valueNode);
|
||||
const value = lowerExpectedValue(lowerer, valueNode);
|
||||
if (value.type.kind === "string") {
|
||||
byId.set(id, { id, kind: "str", value, enameBake: null });
|
||||
} else if (value.type.kind === "regex") {
|
||||
|
||||
@@ -260,6 +260,7 @@ import {
|
||||
completeArgs,
|
||||
} from "./call-arguments.js";
|
||||
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";
|
||||
@@ -5506,6 +5507,9 @@ function reconcileOverloadReturn(
|
||||
expr: ts.CallExpression | ts.TaggedTemplateExpression,
|
||||
call: IrExpr,
|
||||
): IrExpr {
|
||||
// The checker's overload is a guess for a checked-dynamic argument
|
||||
// another overload accepts: the implementation's value stands.
|
||||
if (ts.isCallExpression(expr) && dynamicOverloadChoice(lowerer, expr)) return call;
|
||||
const rsig = lowerer.checker.getResolvedSignature(expr);
|
||||
const rdecl = rsig ? rsig.declaration : undefined;
|
||||
// Nominal covariant overrides share the base's return ABI, but retain
|
||||
|
||||
@@ -426,6 +426,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
return inner;
|
||||
const use = runtimeOptionalUseOf(expr);
|
||||
if (runtimeOptionalAssertionErases(lowerer, expr, inner, use)) return inner;
|
||||
// A destination that holds null or undefined keeps the value, as `!`
|
||||
// does in JavaScript (`return cache.get(key)!` from a function that
|
||||
// returns `T | null`); the destination's own coercion re-tags it.
|
||||
if (assertionDestinationHoldsNullish(lowerer, expr, inner.type)) return inner;
|
||||
const operand = assertionOperand(lowerer, expr, inner, target ?? null, loc);
|
||||
if (operand) return operand;
|
||||
if (target && target.kind !== "union" && !typeEquals(target, inner.type)) {
|
||||
@@ -935,6 +939,65 @@ export function assertedElementKeepsUndefined(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node!` is a returned value, an arrow function's body or a call
|
||||
* argument whose destination type (the return or parameter type) is `any`
|
||||
* or `unknown` (as for `String(x!)` and `Number(x!)`) or holds null or
|
||||
* undefined that the union `inner` holds and the assertion would remove.
|
||||
* Bindings and assignments are left out: the checker narrows them to the
|
||||
* asserted type for later reads. */
|
||||
function assertionDestinationHoldsNullish(
|
||||
lowerer: Lowerer,
|
||||
node: ts.NonNullExpression,
|
||||
inner: IrType & { kind: "union" },
|
||||
): boolean {
|
||||
const holdsNull = lowerer.armTag(inner.unionId, NULL_T) >= 0;
|
||||
const holdsUndefined = lowerer.armTag(inner.unionId, UNDEFINED_T) >= 0;
|
||||
if (!holdsNull && !holdsUndefined) return false;
|
||||
let outer: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
|
||||
const parent = outer.parent;
|
||||
const destination =
|
||||
(ts.isReturnStatement(parent) && parent.expression === outer) ||
|
||||
(ts.isArrowFunction(parent) && parent.body === outer) ||
|
||||
((ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
|
||||
parent.arguments?.nodes.includes(outer) === true);
|
||||
if (!destination) return false;
|
||||
const contextual = lowerer.checker.getContextualType(outer);
|
||||
if (!contextual) return false;
|
||||
const nullish =
|
||||
ts.TypeFlags.Any |
|
||||
ts.TypeFlags.Unknown |
|
||||
(holdsNull ? ts.TypeFlags.Null : 0) |
|
||||
(holdsUndefined && !optionalStdlibParameter(lowerer, parent, outer)
|
||||
? ts.TypeFlags.Undefined
|
||||
: 0);
|
||||
if ((contextual.flags & ts.TypeFlags.Union) === 0) return (contextual.flags & nullish) !== 0;
|
||||
return constituentTypes(contextual).some((type) => (type.flags & nullish) !== 0);
|
||||
}
|
||||
|
||||
/** Whether `argument` of `call` binds an optional parameter of a standard
|
||||
* library declaration (`decoder.decode(input?)`). The undefined such a
|
||||
* parameter's type holds comes from its optionality, and the builtin
|
||||
* lowerings read the argument as the declared type, so `x!` keeps the
|
||||
* checked narrowing there. */
|
||||
function optionalStdlibParameter(
|
||||
lowerer: Lowerer,
|
||||
call: ts.Node,
|
||||
argument: ts.Expression,
|
||||
): boolean {
|
||||
if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) return false;
|
||||
const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
|
||||
if (declaration === undefined || !lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)))
|
||||
return false;
|
||||
const index = call.arguments?.nodes.indexOf(argument) ?? -1;
|
||||
const parameter = declaration.parameters?.nodes[index];
|
||||
return (
|
||||
parameter !== undefined &&
|
||||
parameter.dotDotDotToken === undefined &&
|
||||
(parameter.questionToken !== undefined || parameter.initializer !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/** `x!` as the operand of an operator that converts its operands: an
|
||||
* arithmetic, bitwise, relational or equality operator, `+`, a compound
|
||||
* assignment, a unary `-`, `+`, `~` or `!`, a template span, or an argument
|
||||
|
||||
@@ -2380,7 +2380,6 @@ export function collectGlobals(
|
||||
// References from separately-declared functions cascade to
|
||||
// their own per-site runtime fences.
|
||||
const factoryType =
|
||||
!isLet &&
|
||||
isJsSourceFile(sf) &&
|
||||
!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
|
||||
@@ -150,7 +150,11 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import type { FsPathPlan } from "./builtins/fs-paths.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
|
||||
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
|
||||
import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
|
||||
import {
|
||||
ClassDynamicDispatch,
|
||||
boxClassInstancesExactly,
|
||||
trackClassFieldCreation,
|
||||
} from "./class-dynamic-dispatch.js";
|
||||
import { guardClassAttributeWrites } from "./class-attributes.js";
|
||||
import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js";
|
||||
import { applyHandleProperties } from "./handle-properties.js";
|
||||
@@ -430,6 +434,7 @@ import {
|
||||
collectSignatureInner,
|
||||
lambdaSignature,
|
||||
} from "./call-signatures.js";
|
||||
import { dynamicOverloadChoice, guessedOverloadIdentifier } from "./overload-choice.js";
|
||||
import {
|
||||
implicitAnyParamSymbolsOf,
|
||||
implicitLocalFnNodeOf,
|
||||
@@ -2841,6 +2846,8 @@ export class Lowerer {
|
||||
/** Parameters read as `any` in the error-path copy of an error-first
|
||||
* callback. */
|
||||
readonly anySymbols = new Set<ts.Symbol>();
|
||||
/** overload-choice's answers per call, binding and function. */
|
||||
readonly dynamicOverloadAnswers = new Map<ts.Node, boolean>();
|
||||
/** Error parameters of an error-first callback with a known value in
|
||||
* the copy being lowered: true while it holds an Error, false while it
|
||||
* holds null (staticCondition folds conditions on them). */
|
||||
@@ -7767,6 +7774,7 @@ export class Lowerer {
|
||||
finalizeClassMethodValues(this, functions);
|
||||
finalizePrivateBrands(this, functions);
|
||||
trackClassFieldCreation(this, functions);
|
||||
boxClassInstancesExactly(this, functions);
|
||||
guardClassAttributeWrites(this, functions);
|
||||
guardFrozenWrites(this, functions);
|
||||
applyHandleProperties(this, functions);
|
||||
@@ -9032,6 +9040,20 @@ export class Lowerer {
|
||||
|
||||
typeOf(node: ts.Node): ts.Type {
|
||||
if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType();
|
||||
// A checked-dynamic argument that another overload could take at run
|
||||
// time leaves the resolved overload's return a guess, and JS bindings
|
||||
// initialized with that result hold whatever it was.
|
||||
if (
|
||||
(ts.isCallExpression(node) || ts.isNewExpression(node)) &&
|
||||
dynamicOverloadChoice(this, node)
|
||||
)
|
||||
return this.checker.getAnyType();
|
||||
if (
|
||||
ts.isIdentifier(node) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node)) &&
|
||||
guessedOverloadIdentifier(this, node)
|
||||
)
|
||||
return this.checker.getAnyType();
|
||||
if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
|
||||
const receiver =
|
||||
node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0
|
||||
|
||||
@@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import { implicitDefaultInstance } from "./generic-functions.js";
|
||||
import { type FnSig } from "./call-signatures.js";
|
||||
|
||||
/** An ordinary constructor's explicit object return replaces its receiver.
|
||||
* Without observing this or new.target, it can use the function's call ABI. */
|
||||
function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
|
||||
if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false;
|
||||
/** The JavaScript function declaration (or never-reassigned function
|
||||
* expression binding) `new` calls, when it is an ordinary constructor: not
|
||||
* a generator or async function. */
|
||||
function jsConstructorFunctionOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
): ts.FunctionDeclaration | ts.FunctionExpression | null {
|
||||
if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null;
|
||||
// This probe also sees ordinary classes during global collection. Leave
|
||||
// their deferred diagnostics for the class declaration's lowering.
|
||||
const wasCollecting = lowerer.collecting;
|
||||
@@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
|
||||
} finally {
|
||||
lowerer.collecting = wasCollecting;
|
||||
}
|
||||
if (!symbol) return false;
|
||||
if (!symbol) return null;
|
||||
const owner = symbol.declarations.find(
|
||||
(node) =>
|
||||
(ts.isFunctionDeclaration(node) && node.body !== undefined) ||
|
||||
@@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
|
||||
declaration.asteriskToken ||
|
||||
declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
)
|
||||
return false;
|
||||
return null;
|
||||
return declaration;
|
||||
}
|
||||
|
||||
/** An ordinary constructor's explicit object return replaces its receiver.
|
||||
* Without observing this or new.target, it can use the function's call ABI. */
|
||||
function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
|
||||
const declaration = jsConstructorFunctionOf(lowerer, expr);
|
||||
if (!declaration?.body) return false;
|
||||
const objectReturn = (expression: ts.Expression): boolean =>
|
||||
ts.isObjectLiteralExpression(expression) ||
|
||||
ts.isArrayLiteralExpression(expression) ||
|
||||
@@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
|
||||
|
||||
/** Collection must register the shared result before reaching factory
|
||||
* bodies: those bodies can reference later globals in the same module. */
|
||||
/** The storage of a JavaScript module binding initialized with `new` of an
|
||||
* ordinary function: a factory's result and a constructor function's
|
||||
* receiver (`const head = new Segment()`) are both checked-dynamic
|
||||
* values, which functions declared elsewhere in the module read through
|
||||
* the global. */
|
||||
export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
|
||||
return isObjectFactoryNew(lowerer, expr) ? DYN : null;
|
||||
return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null;
|
||||
}
|
||||
|
||||
export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null {
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { typeEquals, type IrType } from "../../ir/ir.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import { isThisParameter } from "./call-signatures.js";
|
||||
import { hasExplicitJsDocReturn } from "./function-returns.js";
|
||||
import { hasJsTypeAnnotation } from "./lower-stmts.js";
|
||||
|
||||
/** Whether a checked-dynamic argument leaves a JS call's overload
|
||||
* undecided.
|
||||
*
|
||||
* The checker resolves an overloaded call whose argument is `any` (an
|
||||
* untyped parameter, a value read from one) to the FIRST overload that
|
||||
* argument satisfies, but the value can match any overload at run time:
|
||||
* `generateKeyPairSync("ec", options)` answers KeyObjects or encoded
|
||||
* strings by the options' contents, `digest(encoding)` a Buffer when the
|
||||
* encoding is undefined. When another overload the call could take at
|
||||
* run time returns a different type, the resolved overload's return is a
|
||||
* guess, and the result is checked-dynamic instead.
|
||||
*
|
||||
* An overload is reachable when the arguments fit its arity (trailing
|
||||
* checked-dynamic or `undefined` arguments may be absent) and every
|
||||
* statically typed argument is assignable to its parameter. Generic
|
||||
* overloads whose return cannot be mapped without instantiation do not
|
||||
* count, so the resolved instantiation stands for them. */
|
||||
export function dynamicOverloadChoice(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.NewExpression,
|
||||
): boolean {
|
||||
return cachedAnswer(lowerer, expr, () => computeDynamicOverloadChoice(lowerer, expr));
|
||||
}
|
||||
|
||||
/** Whether a JavaScript value's checker type rests on an undecided
|
||||
* overload (dynamicOverloadChoice): the call itself, an awaited one, a
|
||||
* call to an unannotated JS function returning such a value, or a read of
|
||||
* an unannotated JS binding initialized with one (destructured or not).
|
||||
* Such a value is checked-dynamic wherever the checker's inference would
|
||||
* otherwise carry the guess into a binding or a return. */
|
||||
export function guessedOverloadValue(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression;
|
||||
if (ts.isCallExpression(expr) || ts.isNewExpression(expr)) {
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
if (dynamicOverloadChoice(lowerer, expr)) return true;
|
||||
const decl = lowerer.checker.getResolvedSignature(expr)?.declaration;
|
||||
return (
|
||||
decl !== undefined &&
|
||||
(ts.isFunctionDeclaration(decl) ||
|
||||
ts.isFunctionExpression(decl) ||
|
||||
ts.isArrowFunction(decl)) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
returnsGuessedOverloadValue(lowerer, decl)
|
||||
);
|
||||
}
|
||||
return ts.isIdentifier(expr) && guessedOverloadIdentifier(lowerer, expr);
|
||||
}
|
||||
|
||||
/** Whether a JS identifier, as a binding's declared name or as a read,
|
||||
* names a guessedOverloadBinding. */
|
||||
export function guessedOverloadIdentifier(lowerer: Lowerer, node: ts.Identifier): boolean {
|
||||
return cachedAnswer(lowerer, node, () => {
|
||||
const parent = node.parent;
|
||||
if (
|
||||
parent !== undefined &&
|
||||
(ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) &&
|
||||
parent.name === node
|
||||
)
|
||||
return guessedOverloadBinding(lowerer, parent);
|
||||
const decl = lowerer.checker.getSymbolAtLocation(node)?.valueDeclaration;
|
||||
return decl !== undefined && guessedOverloadBinding(lowerer, decl);
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether `decl` is an unannotated JS variable or destructured binding
|
||||
* whose declaration's initializer is a guessedOverloadValue. */
|
||||
function guessedOverloadBinding(lowerer: Lowerer, decl: ts.Node): boolean {
|
||||
let root: ts.Node | undefined = decl;
|
||||
while (root && ts.isBindingElement(root) && root.parent && ts.isBindingPattern(root.parent)) {
|
||||
root = root.parent.parent;
|
||||
}
|
||||
if (!root) return false;
|
||||
if (!ts.isVariableDeclaration(root) || !root.initializer || root.type) return false;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(root)) || hasJsTypeAnnotation(root)) return false;
|
||||
const init = root.initializer;
|
||||
return cachedAnswer(lowerer, root, () => guessedOverloadValue(lowerer, init));
|
||||
}
|
||||
|
||||
/** Whether a return of the unannotated JS function `decl` itself is a
|
||||
* guessedOverloadValue. */
|
||||
export function returnsGuessedOverloadValue(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.SignatureDeclaration,
|
||||
): boolean {
|
||||
if (decl.type || decl.asteriskToken || hasExplicitJsDocReturn(decl)) return false;
|
||||
return cachedAnswer(lowerer, decl, () => {
|
||||
if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) {
|
||||
return guessedOverloadValue(lowerer, decl.body);
|
||||
}
|
||||
let found = false;
|
||||
const scan = (node: ts.Node): void => {
|
||||
if (found || (node !== decl && ts.isFunctionLike(node))) return;
|
||||
if (ts.isReturnStatement(node) && node.expression) {
|
||||
found = guessedOverloadValue(lowerer, node.expression);
|
||||
}
|
||||
ts.forEachChild(node, scan);
|
||||
};
|
||||
scan(decl);
|
||||
return found;
|
||||
});
|
||||
}
|
||||
|
||||
/** One answer per node; a node asked again while its answer is being
|
||||
* computed (recursive functions, self-referencing bindings) answers
|
||||
* false. */
|
||||
function cachedAnswer(lowerer: Lowerer, node: ts.Node, compute: () => boolean): boolean {
|
||||
const cached = lowerer.dynamicOverloadAnswers.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
lowerer.dynamicOverloadAnswers.set(node, false);
|
||||
const answer = compute();
|
||||
lowerer.dynamicOverloadAnswers.set(node, answer);
|
||||
return answer;
|
||||
}
|
||||
|
||||
function computeDynamicOverloadChoice(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.NewExpression,
|
||||
): boolean {
|
||||
// TypeScript sources keep the resolved overload: their reads validate
|
||||
// the value against it where it is used.
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.length === 0 || args.some(ts.isSpreadElement)) return false;
|
||||
const dynamic = args.map((arg) => checkedDynamicArgument(lowerer, arg));
|
||||
if (!dynamic.includes(true)) return false;
|
||||
const checker = lowerer.checker;
|
||||
const calleeType = checker.getTypeAtLocation(expr.expression);
|
||||
const signatures = ts.isNewExpression(expr)
|
||||
? checker.getConstructSignatures(calleeType)
|
||||
: checker.getCallSignatures(calleeType);
|
||||
if (signatures.length < 2) return false;
|
||||
const resolved = checker.getResolvedSignature(expr);
|
||||
if (!resolved) return false;
|
||||
const resolvedReturn = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(resolved));
|
||||
if (resolvedReturn === null) return false;
|
||||
// Arguments that may be absent at run time: the trailing run of
|
||||
// checked-dynamic and `undefined` arguments.
|
||||
let present = args.length;
|
||||
while (present > 0 && (dynamic[present - 1] || isUndefinedArgument(args[present - 1]!))) {
|
||||
present--;
|
||||
}
|
||||
for (const signature of signatures) {
|
||||
const decl = signature.declaration;
|
||||
if (!decl || decl === resolved.declaration || !ts.isFunctionLike(decl)) continue;
|
||||
const generic = signature.getTypeParameters().length > 0;
|
||||
const params = decl.parameters.nodes.filter((param) => !isThisParameter(param));
|
||||
const required = params.filter(
|
||||
(param) => !param.questionToken && !param.initializer && !param.dotDotDotToken,
|
||||
).length;
|
||||
const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length;
|
||||
if (required > args.length || present > maximum) continue;
|
||||
const symbols = signature.getParameters();
|
||||
let fits = true;
|
||||
for (let i = 0; i < args.length && i < params.length && fits; i++) {
|
||||
if (dynamic[i] || generic || params[i]!.dotDotDotToken) continue;
|
||||
const symbol = symbols[i];
|
||||
if (!symbol) continue;
|
||||
fits = checker.isTypeAssignableTo(
|
||||
checker.getTypeAtLocation(args[i]!),
|
||||
checker.getTypeOfSymbol(symbol),
|
||||
);
|
||||
}
|
||||
if (!fits) continue;
|
||||
const candidate = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(signature));
|
||||
if (candidate !== null && !holdsResult(resolvedReturn, candidate)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a value of the resolved overload's mapped return also holds
|
||||
* every value of `candidate`: the same type, or a checked-dynamic one (a
|
||||
* promise of one fulfills with anything). */
|
||||
function holdsResult(resolved: IrType, candidate: IrType): boolean {
|
||||
if (resolved.kind === "dyn" || typeEquals(resolved, candidate)) return true;
|
||||
return (
|
||||
resolved.kind === "promise" &&
|
||||
candidate.kind === "promise" &&
|
||||
holdsResult(resolved.inner, candidate.inner)
|
||||
);
|
||||
}
|
||||
|
||||
function checkedDynamicArgument(lowerer: Lowerer, arg: ts.Expression): boolean {
|
||||
if (lowerer.checker.getTypeAtLocation(arg).flags & ts.TypeFlags.Any) return true;
|
||||
return (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
function isUndefinedArgument(arg: ts.Expression): boolean {
|
||||
return (ts.isIdentifier(arg) && arg.text === "undefined") || ts.isVoidExpression(arg);
|
||||
}
|
||||
@@ -1499,6 +1499,10 @@ export interface IrClassDef {
|
||||
* class of the chain sets `name` during construction, and then an own
|
||||
* `name` follows every other own property. */
|
||||
errorNameAfter?: string | null;
|
||||
/** JavaScript Error subclasses with a property bag: a view committed by a
|
||||
* dynamic write keeps `name` and `message` where the bag recorded them
|
||||
* (scr_error_dyn_follow_bag). */
|
||||
errorBagOrder?: boolean;
|
||||
/** Stable module symbol identities for public instance layout fields. */
|
||||
symbolFields?: { field: string; globalId: string }[];
|
||||
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
|
||||
|
||||
@@ -221,3 +221,21 @@ test("a JavaScript file TypeScript checks uses its JSDoc types for class layouts
|
||||
expect(layout(unchecked, "Account").fields).toBe("owner:dyn balance:dyn");
|
||||
expect(params(unchecked, "%Account.constructor")).toBe("(object, dyn, dyn): void");
|
||||
});
|
||||
|
||||
test("instances of a class with subclasses box at their exact class", async () => {
|
||||
const module = await lower(`${SHAPES}
|
||||
/** @param {any} value */
|
||||
function keep(value) {
|
||||
return value;
|
||||
}
|
||||
/** @param {Shape} shape */
|
||||
function cross(shape) {
|
||||
return keep(shape);
|
||||
}
|
||||
console.log(cross(new Square()).size, cross(new Shape()).label);
|
||||
`);
|
||||
const helper = module.functions.find((fn) => fn.name === "%dyn.class.boxExact:Shape");
|
||||
expect(helper).toBeDefined();
|
||||
expect(calls(module, "cross")).toContain("%dyn.class.boxExact:Shape");
|
||||
expect(JSON.stringify(helper!.body)).toContain('"className":"Square"');
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7189-non-null-assertion-nullish-destination.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7453-untyped-overload-results.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7624-key-order-boxed-callback-records/lib.js",
|
||||
"<repo>/tests/corpus/7624-key-order-boxed-callback-records/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7680-typed-array-search.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7681-typed-array-search-dynamic.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7682-constructor-function-globals.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7796-boxed-subclass-identity.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8112-error-factory-own-key-order.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8113-error-class-field-before-name.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8160-assert-expected-from-fields.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-masked-index-stores.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 1398,
|
||||
"end": 1407
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 1413,
|
||||
"end": 1422
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'WebSocket'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 3716,
|
||||
"end": 3725
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
|
||||
"start": 4798,
|
||||
"end": 4806
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
|
||||
"start": 549,
|
||||
"end": 557
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Cannot find name 'FormData'.",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
|
||||
"start": 5572,
|
||||
"end": 5580
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3341,6 +3341,9 @@ static bool scr_bytes_search_start(size_t len, double from, size_t *start) {
|
||||
}
|
||||
|
||||
double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last) {
|
||||
/* A Buffer viewed as a Uint8Array keeps Buffer's own search: the needle
|
||||
* is taken as a byte, and a NaN byteOffset searches the whole buffer. */
|
||||
if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, !last);
|
||||
SCR_BYTES_SNAPSHOT(b);
|
||||
size_t len = b->len;
|
||||
size_t size = scr_bytes_elem_size(b->elem);
|
||||
@@ -3361,6 +3364,7 @@ double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool la
|
||||
}
|
||||
|
||||
bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from) {
|
||||
if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, true) >= 0;
|
||||
SCR_BYTES_SNAPSHOT(b);
|
||||
size_t len = b->len;
|
||||
size_t size = scr_bytes_elem_size(b->elem);
|
||||
|
||||
@@ -541,10 +541,12 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd)
|
||||
/* TypedArray indexOf (last false) and lastIndexOf (last true) over any
|
||||
* element kind, with strict equality: NaN is never found and -0 matches 0.
|
||||
* `from` is the fromIndex argument (ToIntegerOrInfinity applies); the
|
||||
* caller passes 0, or +Infinity for lastIndexOf, when it is omitted. */
|
||||
* caller passes 0, or +Infinity for lastIndexOf, when it is omitted. A
|
||||
* Buffer (a Uint8Array receiver can be one) searches as Buffer's
|
||||
* indexOf and lastIndexOf do. */
|
||||
double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last);
|
||||
/* TypedArray includes: indexOf's search from `from` with SameValueZero,
|
||||
* so NaN is found. */
|
||||
* so NaN is found. A Buffer searches as Buffer's includes does. */
|
||||
bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from);
|
||||
ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end);
|
||||
ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end);
|
||||
|
||||
@@ -1334,6 +1334,11 @@ ScrDyn *scr_dyn_from_error(const ScrError *e);
|
||||
void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *));
|
||||
ScrDyn *scr_error_dyn_fields(const ScrError *e);
|
||||
void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len);
|
||||
/* An Error subclass view of an instance whose class keeps a property bag:
|
||||
* `name` and `message` take their committed positions, and data values
|
||||
* from the slots (scr_json.c). `name_placed`: the class's construction sets
|
||||
* `name`, and the view already has it in place. */
|
||||
void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed);
|
||||
/* An accessor property of a fresh object: enumerable and configurable,
|
||||
* with the given getter and setter (either may be NULL). Moves both. */
|
||||
void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter);
|
||||
|
||||
@@ -388,8 +388,80 @@ static SCR_TL bool scr_stdin_destroyed = false;
|
||||
static SCR_TL bool scr_stdin_paused = false;
|
||||
static SCR_TL bool scr_stdin_resumed = false;
|
||||
static SCR_TL bool scr_stdin_read_eof = false;
|
||||
static SCR_TL ScrBytes *scr_stdin_buffer;
|
||||
/* Input held for read() and later consumers, as Node's BufferList: the
|
||||
* chunks in arrival order and their total length, so an arrival appends
|
||||
* without copying what is already held. */
|
||||
static SCR_TL ScrBytes **scr_stdin_held = NULL;
|
||||
static SCR_TL size_t scr_stdin_nheld = 0, scr_stdin_held_cap = 0;
|
||||
static SCR_TL size_t scr_stdin_buffered = 0;
|
||||
static SCR_TL size_t scr_stdin_read_demand;
|
||||
/* 'readable' listeners (paused mode, as Node's once one is added): input
|
||||
* stays buffered for read(), and the event fires on the next tick when
|
||||
* input arrives while a read wanted more (`need`), and once at EOF. */
|
||||
static SCR_TL ScrStdinEndL *scr_stdin_readable = NULL;
|
||||
static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0;
|
||||
static SCR_TL bool scr_stdin_readable_need = false;
|
||||
static SCR_TL bool scr_stdin_readable_queued = false;
|
||||
|
||||
/* Appends an arrived chunk (moves) to the held input. */
|
||||
static void scr_stdin_hold(ScrBytes *chunk) {
|
||||
if (chunk->len == 0) {
|
||||
scr_bytes_release(chunk);
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_nheld == scr_stdin_held_cap) {
|
||||
scr_stdin_held_cap = scr_stdin_held_cap ? scr_stdin_held_cap * 2 : 4;
|
||||
scr_stdin_held = realloc(scr_stdin_held, scr_stdin_held_cap * sizeof *scr_stdin_held);
|
||||
if (!scr_stdin_held) scr_events_oom();
|
||||
}
|
||||
scr_stdin_held[scr_stdin_nheld++] = chunk;
|
||||
scr_stdin_buffered += chunk->len;
|
||||
}
|
||||
|
||||
/* The first n held bytes as one chunk (+1), 0 < n <= scr_stdin_buffered.
|
||||
* A whole first chunk moves out without a copy; otherwise the bytes are
|
||||
* joined once, and a partly taken chunk keeps its rest. */
|
||||
static ScrBytes *scr_stdin_take(size_t n) {
|
||||
ScrBytes *first = scr_stdin_held[0];
|
||||
size_t used = 0;
|
||||
ScrBytes *out;
|
||||
if (n == first->len) {
|
||||
out = first;
|
||||
used = 1;
|
||||
} else if (n < first->len) {
|
||||
out = scr_bytes_slice(first, 0, (double)n);
|
||||
scr_stdin_held[0] = scr_bytes_slice(first, (double)n, (double)first->len);
|
||||
scr_bytes_release(first);
|
||||
} else {
|
||||
out = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
||||
size_t at = 0;
|
||||
while (at < n) {
|
||||
ScrBytes *chunk = scr_stdin_held[used];
|
||||
size_t part = chunk->len <= n - at ? chunk->len : n - at;
|
||||
memcpy(out->data + at, chunk->data, part);
|
||||
at += part;
|
||||
if (part == chunk->len) {
|
||||
scr_bytes_release(chunk);
|
||||
used++;
|
||||
} else {
|
||||
scr_stdin_held[used] = scr_bytes_slice(chunk, (double)part, (double)chunk->len);
|
||||
scr_bytes_release(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
memmove(scr_stdin_held, scr_stdin_held + used, (scr_stdin_nheld - used) * sizeof *scr_stdin_held);
|
||||
scr_stdin_nheld -= used;
|
||||
scr_stdin_buffered -= n;
|
||||
return out;
|
||||
}
|
||||
|
||||
static void scr_stdin_drop_held(void) {
|
||||
for (size_t i = 0; i < scr_stdin_nheld; i++) scr_bytes_release(scr_stdin_held[i]);
|
||||
free(scr_stdin_held);
|
||||
scr_stdin_held = NULL;
|
||||
scr_stdin_nheld = scr_stdin_held_cap = 0;
|
||||
scr_stdin_buffered = 0;
|
||||
}
|
||||
|
||||
#ifdef SCR_STDIO_EXTENDED
|
||||
/* process.stdin.setEncoding(enc): the canonical encoding name (NULL until
|
||||
@@ -475,6 +547,78 @@ void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) {
|
||||
scr_stdin_nend++;
|
||||
}
|
||||
|
||||
static void scr_stdin_finish(bool fire_end);
|
||||
|
||||
static void scr_stdin_emit_readable(void) {
|
||||
scr_stdin_readable_queued = false;
|
||||
size_t length = scr_stdin_buffered;
|
||||
if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return;
|
||||
size_t n = scr_stdin_nreadable;
|
||||
ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap);
|
||||
if (!snap) scr_events_oom();
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
snap[i] = scr_stdin_readable[i];
|
||||
scr_closure_retain(snap[i].cb);
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (snap[i].once) {
|
||||
for (size_t j = 0; j < scr_stdin_nreadable; j++) {
|
||||
if (scr_stdin_readable[j].cb == snap[i].cb) {
|
||||
scr_closure_release(scr_stdin_readable[j].cb);
|
||||
memmove(scr_stdin_readable + j, scr_stdin_readable + j + 1,
|
||||
(scr_stdin_nreadable - j - 1) * sizeof *scr_stdin_readable);
|
||||
scr_stdin_nreadable--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!scr_exc_pending() && !scr_stdin_eof && !scr_stdin_destroyed)
|
||||
((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
|
||||
scr_closure_release(snap[i].cb);
|
||||
}
|
||||
free(snap);
|
||||
length = scr_stdin_buffered;
|
||||
/* Another event is due once the buffer reaches the 64 KiB mark, or the
|
||||
* larger size a listener's read(size) asked for. */
|
||||
scr_stdin_readable_need =
|
||||
!scr_stdin_read_eof && (length <= 65536 || length < scr_stdin_read_demand);
|
||||
}
|
||||
|
||||
static void scr_stdin_queue_readable(void) {
|
||||
if (scr_stdin_readable_queued || !scr_stdin_nreadable) return;
|
||||
scr_stdin_readable_queued = true;
|
||||
scr_next_tick_raw(scr_stdin_emit_readable);
|
||||
}
|
||||
|
||||
/* Adding a 'readable' listener starts reading ahead to the 64 KiB mark; a
|
||||
* buffer that already holds input fires the event at once (next tick). */
|
||||
static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) {
|
||||
if (scr_stdin_eof || scr_stdin_destroyed) {
|
||||
scr_closure_release(cb);
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_nreadable == scr_stdin_readable_cap) {
|
||||
scr_stdin_readable_cap = scr_stdin_readable_cap ? scr_stdin_readable_cap * 2 : 2;
|
||||
scr_stdin_readable = realloc(scr_stdin_readable, scr_stdin_readable_cap * sizeof *scr_stdin_readable);
|
||||
if (!scr_stdin_readable) scr_events_oom();
|
||||
}
|
||||
scr_stdin_readable[scr_stdin_nreadable].cb = cb;
|
||||
scr_stdin_readable[scr_stdin_nreadable].once = once;
|
||||
scr_stdin_nreadable++;
|
||||
scr_stdin_readable_need = true;
|
||||
if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536;
|
||||
if (scr_stdin_buffered) scr_stdin_queue_readable();
|
||||
}
|
||||
|
||||
/* The input ended: with 'readable' listeners the event fires once more and
|
||||
* 'end' waits for the read that finds nothing left; otherwise 'end' fires
|
||||
* once the buffer is drained. */
|
||||
static void scr_stdin_reached_eof(void) {
|
||||
scr_stdin_read_eof = true;
|
||||
if (scr_stdin_nreadable) scr_stdin_queue_readable();
|
||||
else if (!scr_stdin_buffered) scr_stdin_finish(true);
|
||||
}
|
||||
|
||||
void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
|
||||
if (scr_stdin_eof || scr_stdin_destroyed) {
|
||||
scr_closure_release(cb);
|
||||
@@ -516,10 +660,15 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; }
|
||||
* alive (a paused Node stdin with only those never flows — the process
|
||||
* exits with them registered). */
|
||||
static bool scr_stdin_watching(void) {
|
||||
return !scr_stdin_eof && !scr_stdin_destroyed &&
|
||||
((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) ||
|
||||
scr_stdin_waiter != NULL ||
|
||||
(!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0)));
|
||||
if (scr_stdin_eof || scr_stdin_destroyed) return false;
|
||||
/* 'readable' listeners keep the stream paused, as in Node, where
|
||||
* resume() and 'data' listeners do not make it flow: it reads ahead only
|
||||
* while a read wants more than is held (the 64 KiB mark, or a larger
|
||||
* read(size)), and stops until something drains it. */
|
||||
if (scr_stdin_nreadable) return !scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered;
|
||||
return (!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) ||
|
||||
scr_stdin_waiter != NULL ||
|
||||
(!scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered);
|
||||
}
|
||||
|
||||
bool scr_stdin_pending(void) { return scr_stdin_watching(); }
|
||||
@@ -528,6 +677,10 @@ static void scr_stdin_drop_listeners(void) {
|
||||
for (size_t i = 0; i < scr_stdin_ndata; i++) scr_closure_release(scr_stdin_data[i].cb);
|
||||
for (size_t i = 0; i < scr_stdin_nend; i++) scr_closure_release(scr_stdin_end[i].cb);
|
||||
for (size_t i = 0; i < scr_stdin_nerr; i++) scr_closure_release(scr_stdin_err[i].cb);
|
||||
for (size_t i = 0; i < scr_stdin_nreadable; i++) scr_closure_release(scr_stdin_readable[i].cb);
|
||||
free(scr_stdin_readable);
|
||||
scr_stdin_readable = NULL;
|
||||
scr_stdin_nreadable = scr_stdin_readable_cap = 0;
|
||||
free(scr_stdin_data);
|
||||
free(scr_stdin_end);
|
||||
free(scr_stdin_err);
|
||||
@@ -593,8 +746,7 @@ static void scr_stdin_finish(bool fire_end) {
|
||||
void scr_stdin_destroy(void) {
|
||||
if (scr_stdin_destroyed) return;
|
||||
scr_stdin_destroyed = true;
|
||||
scr_bytes_release(scr_stdin_buffer);
|
||||
scr_stdin_buffer = NULL;
|
||||
scr_stdin_drop_held();
|
||||
scr_stdin_read_demand = 0;
|
||||
scr_stdin_settle_done();
|
||||
scr_stdin_drop_listeners();
|
||||
@@ -627,16 +779,9 @@ static void scr_stdin_deliver(ScrBytes *chunk);
|
||||
/* A chunk that arrived (moves): delivered, or buffered while paused or
|
||||
* without a consumer. */
|
||||
static void scr_stdin_arrived(ScrBytes *chunk) {
|
||||
if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) {
|
||||
if (scr_stdin_buffer) {
|
||||
size_t before = scr_stdin_buffer->len;
|
||||
ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len));
|
||||
memcpy(joined->data, scr_stdin_buffer->data, before);
|
||||
memcpy(joined->data + before, chunk->data, chunk->len);
|
||||
scr_bytes_release(scr_stdin_buffer);
|
||||
scr_bytes_release(chunk);
|
||||
scr_stdin_buffer = joined;
|
||||
} else scr_stdin_buffer = chunk;
|
||||
if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) {
|
||||
if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable();
|
||||
scr_stdin_hold(chunk);
|
||||
return;
|
||||
}
|
||||
scr_stdin_deliver(chunk);
|
||||
@@ -645,15 +790,18 @@ static void scr_stdin_arrived(ScrBytes *chunk) {
|
||||
|
||||
static void scr_stdin_service(void) {
|
||||
if (!scr_stdin_watching()) return;
|
||||
if (!scr_stdin_paused && scr_stdin_buffer) {
|
||||
ScrBytes *chunk = scr_stdin_buffer;
|
||||
scr_stdin_buffer = NULL;
|
||||
/* Buffered input goes to a consumer: 'data' listeners, a resumed
|
||||
* stream, or a parked for-await read. After the last 'readable' listener
|
||||
* it stays buffered until one arrives, as in Node. */
|
||||
if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffered &&
|
||||
(scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) {
|
||||
ScrBytes *chunk = scr_stdin_take(scr_stdin_buffered);
|
||||
scr_stdin_deliver(chunk);
|
||||
scr_bytes_release(chunk);
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_read_eof) {
|
||||
if (!scr_stdin_buffer) scr_stdin_finish(true);
|
||||
if (!scr_stdin_buffered && !scr_stdin_nreadable) scr_stdin_finish(true);
|
||||
return;
|
||||
}
|
||||
if (scr_stdin_fed) {
|
||||
@@ -662,10 +810,7 @@ static void scr_stdin_service(void) {
|
||||
bool ended = false;
|
||||
ScrBytes *chunk = scr_stdin_fed_take(&ended);
|
||||
if (chunk) scr_stdin_arrived(chunk);
|
||||
else if (ended) {
|
||||
scr_stdin_read_eof = true;
|
||||
if (!scr_stdin_buffer) scr_stdin_finish(true);
|
||||
}
|
||||
else if (ended) scr_stdin_reached_eof();
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
@@ -727,8 +872,7 @@ static void scr_stdin_service(void) {
|
||||
return;
|
||||
}
|
||||
if (n == 0) {
|
||||
scr_stdin_read_eof = true;
|
||||
if (!scr_stdin_buffer) scr_stdin_finish(true);
|
||||
scr_stdin_reached_eof();
|
||||
return;
|
||||
}
|
||||
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
||||
@@ -802,6 +946,8 @@ static void scr_stdin_deliver(ScrBytes *chunk) {
|
||||
#endif
|
||||
ScrPromise *p = scr_stdin_waiter;
|
||||
scr_stdin_waiter = NULL;
|
||||
/* Node's stdin yields Buffers. */
|
||||
chunk->is_buffer = true;
|
||||
scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v,
|
||||
scr_bytes_release_v, NULL);
|
||||
scr_promise_release(p);
|
||||
@@ -878,6 +1024,7 @@ static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) {
|
||||
}
|
||||
#endif
|
||||
static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); }
|
||||
static void scr_stdio_readable(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); }
|
||||
static void scr_stdio_error(ScrClosure *cb, ScrStr *message) {
|
||||
ScrError *error = scr_error_new(SCR_ERR_ERROR, message);
|
||||
ScrDyn *arg = scr_dyn_from_error(error);
|
||||
@@ -971,7 +1118,7 @@ static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int
|
||||
}
|
||||
|
||||
static void scr_stdio_finish_read(void) {
|
||||
if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed)
|
||||
if (scr_stdin_read_eof && !scr_stdin_buffered && !scr_stdin_eof && !scr_stdin_destroyed)
|
||||
scr_stdin_finish(true);
|
||||
}
|
||||
|
||||
@@ -980,21 +1127,21 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) {
|
||||
if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) ||
|
||||
size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num))
|
||||
return scr_stdio_refusal("read size");
|
||||
size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0;
|
||||
size_t available = scr_stdin_buffered;
|
||||
size_t wanted = all ? available : (size_t)size->v.num;
|
||||
if (scr_stdin_read_eof && wanted > available) wanted = available;
|
||||
if (wanted == 0 || wanted > available) {
|
||||
if (!scr_stdin_eof && !scr_stdin_destroyed) {
|
||||
scr_stdin_readable_need = true;
|
||||
scr_stdin_read_demand = wanted > 65536 ? wanted : 65536;
|
||||
if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read);
|
||||
}
|
||||
return scr_dyn_new_null();
|
||||
}
|
||||
ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted);
|
||||
ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL;
|
||||
scr_bytes_release(scr_stdin_buffer);
|
||||
scr_stdin_buffer = rest;
|
||||
ScrBytes *chunk = scr_stdin_take(wanted);
|
||||
bool rest = scr_stdin_buffered > 0;
|
||||
scr_stdin_read_demand = 65536;
|
||||
if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true;
|
||||
scr_stdin_deliver(chunk);
|
||||
ScrDyn *result = scr_dyn_new_chunk(chunk);
|
||||
scr_bytes_release(chunk);
|
||||
@@ -1150,12 +1297,14 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0;
|
||||
bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0;
|
||||
bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0;
|
||||
bool readable = stream->fd == 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 8 &&
|
||||
memcmp(event, "readable", 8) == 0;
|
||||
#ifdef SCR_STDIO_EXTENDED
|
||||
bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 &&
|
||||
memcmp(event, "resize", 6) == 0;
|
||||
if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) {
|
||||
if ((!data && !end && !error && !readable && !resize) || (stream->fd != 0 && !error && !resize)) {
|
||||
#else
|
||||
if ((!data && !end && !error) || (stream->fd != 0 && !error)) {
|
||||
if ((!data && !end && !error && !readable) || (stream->fd != 0 && !error)) {
|
||||
#endif
|
||||
result = scr_stdio_refusal("event");
|
||||
goto done;
|
||||
@@ -1175,6 +1324,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once);
|
||||
#endif
|
||||
if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once);
|
||||
if (readable) scr_stdin_on_readable(scr_stdio_callback(listener, (void *)scr_stdio_readable), once);
|
||||
if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once);
|
||||
} else {
|
||||
#define REMOVE_STDIO_LISTENER(list, count, fire) \
|
||||
@@ -1187,6 +1337,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
|
||||
}
|
||||
if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); }
|
||||
if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); }
|
||||
if (readable) { REMOVE_STDIO_LISTENER(scr_stdin_readable, scr_stdin_nreadable, scr_stdio_readable); }
|
||||
if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); }
|
||||
#undef REMOVE_STDIO_LISTENER
|
||||
}
|
||||
@@ -1246,7 +1397,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) {
|
||||
return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension);
|
||||
}
|
||||
if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0)
|
||||
return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 0);
|
||||
return scr_dyn_new_num((double)scr_stdin_buffered);
|
||||
if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0)
|
||||
return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined());
|
||||
if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed);
|
||||
@@ -1531,8 +1682,7 @@ static void scr_events_cleanup_atexit(void) {
|
||||
if (scr_context_is_main()) scr_sig_cleanup();
|
||||
scr_stdin_settle_done();
|
||||
scr_stdin_drop_listeners();
|
||||
scr_bytes_release(scr_stdin_buffer);
|
||||
scr_stdin_buffer = NULL;
|
||||
scr_stdin_drop_held();
|
||||
if (scr_stdio_initialized) scr_stdio_cleanup();
|
||||
}
|
||||
|
||||
|
||||
@@ -8973,6 +8973,102 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len)
|
||||
view->v.obj.index = NULL;
|
||||
}
|
||||
|
||||
static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t key_len) {
|
||||
for (size_t i = 0; i < object->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &object->v.obj.entries[i];
|
||||
if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i;
|
||||
}
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
/* Whether bag entry b is one of the entries Error's constructor creates
|
||||
* before the instance's class fields: `stack`, a hidden entry (a key
|
||||
* starting with '%'), and a `message` that directly follows them. */
|
||||
static bool scr_error_bag_constructed(const ScrDyn *bag, size_t b) {
|
||||
for (size_t i = 0; i <= b; i++) {
|
||||
const ScrDynEntry *entry = &bag->v.obj.entries[i];
|
||||
bool constructed = (entry->key_len == 5 && memcmp(entry->key, "stack", 5) == 0) ||
|
||||
(entry->key_len > 0 && entry->key[0] == '%') ||
|
||||
(entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0);
|
||||
if (!constructed) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The view index at which bag entry b, now at `from`, belongs: after the
|
||||
* nearest earlier bag property the view has. An entry created after
|
||||
* construction (any `name` placed here, since no constructor of the class
|
||||
* sets it, and a `message` that is not one of Error's constructor entries)
|
||||
* also follows the class fields, which live in slots and not in the bag: it
|
||||
* passes over the constructor's entries and over a `name` the construction
|
||||
* set, and without another earlier property goes before the nearest later
|
||||
* one the view has, or stays where the view put it. A `message` Error's
|
||||
* constructor created follows the nearest earlier entry, or goes first. */
|
||||
static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t b, bool name,
|
||||
bool name_placed, size_t from) {
|
||||
bool late = name || !scr_error_bag_constructed(bag, b);
|
||||
for (size_t p = b; p-- > 0;) {
|
||||
const ScrDynEntry *anchor = &bag->v.obj.entries[p];
|
||||
if (late && scr_error_bag_constructed(bag, p)) break;
|
||||
if (late && name_placed && anchor->key_len == 4 && memcmp(anchor->key, "name", 4) == 0) continue;
|
||||
size_t after = scr_dyn_entry_index(view, anchor->key, anchor->key_len);
|
||||
if (after != SIZE_MAX) return after < from ? after + 1 : after;
|
||||
}
|
||||
if (!late) return 0;
|
||||
for (size_t p = b + 1; p < bag->v.obj.len; p++) {
|
||||
size_t before = scr_dyn_entry_index(view, bag->v.obj.entries[p].key, bag->v.obj.entries[p].key_len);
|
||||
if (before != SIZE_MAX) return before > from ? before - 1 : before;
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
/* The bag holds the view as it was last committed, in the order its
|
||||
* properties were created: zod's error factory assigns `name` and then
|
||||
* defines `message`, and Node lists them in that order. The slots rebuild
|
||||
* both at fixed positions (scr_error_dyn_fields, scr_error_dyn_place_name),
|
||||
* so each one present in the bag moves to follow the property it follows
|
||||
* there. A typed write to `name` or `message` reaches only the slot, so a
|
||||
* data property takes the slot's value. */
|
||||
void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed) {
|
||||
if (!bag || bag->kind != SCR_DYN_OBJ || view->kind != SCR_DYN_OBJ) return;
|
||||
static const struct { const char *key; size_t len; } slots[] = {{"name", 4}, {"message", 7}};
|
||||
for (size_t s = 0; s < 2; s++) {
|
||||
bool name = s == 0;
|
||||
if (!(name ? e->name_present : e->message_present)) continue;
|
||||
size_t at = scr_dyn_entry_index(view, slots[s].key, slots[s].len);
|
||||
if (at == SIZE_MAX || view->v.obj.entries[at].accessor) continue;
|
||||
ScrDyn *value = scr_dyn_new_str(name ? e->name : e->message);
|
||||
scr_dyn_region_store(view, value);
|
||||
ScrDyn *old = view->v.obj.entries[at].value;
|
||||
view->v.obj.entries[at].value = value;
|
||||
scr_dyn_release(old);
|
||||
}
|
||||
bool moved = false;
|
||||
for (size_t b = 0; b < bag->v.obj.len; b++) {
|
||||
const ScrDynEntry *bag_entry = &bag->v.obj.entries[b];
|
||||
bool slot = (bag_entry->key_len == 4 && memcmp(bag_entry->key, "name", 4) == 0) ||
|
||||
(bag_entry->key_len == 7 && memcmp(bag_entry->key, "message", 7) == 0);
|
||||
bool name = bag_entry->key_len == 4;
|
||||
/* A `name` the class's construction sets is in place already. */
|
||||
if (!slot || (name && name_placed)) continue;
|
||||
size_t from = scr_dyn_entry_index(view, bag_entry->key, bag_entry->key_len);
|
||||
if (from == SIZE_MAX) continue;
|
||||
size_t to = scr_error_bag_place(view, bag, b, name, name_placed, from);
|
||||
if (to == from) continue;
|
||||
ScrDynEntry *entries = view->v.obj.entries;
|
||||
ScrDynEntry entry = entries[from];
|
||||
if (to < from) memmove(&entries[to + 1], &entries[to], (from - to) * sizeof *entries);
|
||||
else memmove(&entries[from], &entries[from + 1], (to - from) * sizeof *entries);
|
||||
entries[to] = entry;
|
||||
moved = true;
|
||||
}
|
||||
if (moved) {
|
||||
SCR_JS_SHAPE_RESET(view);
|
||||
free(view->v.obj.index);
|
||||
view->v.obj.index = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_from_error(const ScrError *e) {
|
||||
size_t hit = scr_errdyn_find(e, true);
|
||||
if (hit != SIZE_MAX) return scr_dyn_retain(scr_errdyn_cache[hit].dyn);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// `!` has no runtime effect: a value asserted non-null that still holds
|
||||
// null or undefined reaches a return value, an arrow function's result or
|
||||
// an argument whose declared type holds it (or is any or unknown), unchanged.
|
||||
interface Shape {
|
||||
size: number;
|
||||
}
|
||||
|
||||
const shapes = new Map<string, Shape | null>();
|
||||
function shapeOf(key: string): Shape | null {
|
||||
if (shapes.has(key)) return shapes.get(key)!;
|
||||
const result = key.length > 3 ? { size: key.length } : null;
|
||||
shapes.set(key, result);
|
||||
return result;
|
||||
}
|
||||
console.log(shapeOf("ab"), shapeOf("abcd"));
|
||||
console.log(shapeOf("ab"), shapeOf("abcd"), shapes.size);
|
||||
|
||||
const counts = new Map<string, number>([["a", 1]]);
|
||||
function countOf(key: string): number | undefined {
|
||||
return counts.get(key)!;
|
||||
}
|
||||
console.log(countOf("a"), countOf("b"));
|
||||
|
||||
function describe(shape: Shape | null): string {
|
||||
return shape === null ? "none" : `size ${shape.size}`;
|
||||
}
|
||||
console.log(describe(shapes.get("ab")!), describe(shapes.get("abcd")!));
|
||||
|
||||
class Slot {
|
||||
readonly label: string;
|
||||
constructor(shape: Shape | null) {
|
||||
this.label = shape === null ? "empty" : `slot ${shape.size}`;
|
||||
}
|
||||
}
|
||||
console.log(new Slot(shapes.get("ab")!).label, new Slot(shapes.get("abcd")!).label);
|
||||
|
||||
const lookup = (key: string): Shape | null => shapes.get(key)!;
|
||||
console.log(lookup("ab"), lookup("abcd")?.size);
|
||||
|
||||
const names = new Map<string, string | null>([["a", null], ["b", "bee"]]);
|
||||
const amounts = new Map<string, number | undefined>([["a", undefined], ["b", 2]]);
|
||||
function kindOf(value: unknown): string {
|
||||
return value === null ? "null" : typeof value;
|
||||
}
|
||||
console.log(String(names.get("a")!), String(names.get("b")!));
|
||||
console.log(Number(amounts.get("a")!), Number(amounts.get("b")!));
|
||||
console.log(kindOf(names.get("a")!), kindOf(amounts.get("a")!), kindOf(names.get("b")!));
|
||||
|
||||
// An optional parameter of a standard library method keeps the checked
|
||||
// narrowing: `decode(input?)` reads the asserted Uint8Array.
|
||||
const chunks = new Map<string, Uint8Array | undefined>([["a", new TextEncoder().encode("chunk")]]);
|
||||
interface Read {
|
||||
done: boolean;
|
||||
value?: Uint8Array;
|
||||
}
|
||||
const read: Read = { done: false, value: chunks.get("a") };
|
||||
console.log(new TextDecoder().decode(chunks.get("a")!), new TextDecoder().decode(read.value!).slice(0, 3));
|
||||
@@ -0,0 +1,85 @@
|
||||
// An untyped argument can match any overload of a call at run time. The
|
||||
// result follows the overload the value selects, through bindings,
|
||||
// destructuring and function returns.
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const ecPair = (options) => crypto.generateKeyPairSync("ec", options);
|
||||
const objects = ecPair({ namedCurve: "P-256" });
|
||||
console.log(typeof objects.privateKey, objects.privateKey.type, objects.publicKey.asymmetricKeyType);
|
||||
const pem = ecPair({
|
||||
namedCurve: "P-256",
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
||||
});
|
||||
console.log(typeof pem.privateKey, pem.publicKey.split("\n")[0]);
|
||||
|
||||
function privateKeyKind(options) {
|
||||
const pair = crypto.generateKeyPairSync("ec", options);
|
||||
return typeof pair.privateKey;
|
||||
}
|
||||
console.log(privateKeyKind({ namedCurve: "P-384" }));
|
||||
|
||||
function spread(options) {
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
|
||||
namedCurve: "P-256",
|
||||
...options,
|
||||
});
|
||||
return [typeof privateKey, Buffer.isBuffer(publicKey)];
|
||||
}
|
||||
console.log(spread({}), spread({ publicKeyEncoding: { type: "spki", format: "der" } }));
|
||||
|
||||
const digest = (encoding) => crypto.createHash("sha256").update("x").digest(encoding);
|
||||
console.log(typeof digest("hex"), Buffer.isBuffer(digest(undefined)), digest(undefined).length);
|
||||
const hmac = (encoding) => crypto.createHmac("sha256", "k").update("v").digest(encoding);
|
||||
console.log(hmac("base64"), Buffer.isBuffer(hmac()));
|
||||
function digestLength(encoding) {
|
||||
let value = crypto.createHash("sha1").update("y").digest(encoding);
|
||||
return Buffer.isBuffer(value) ? `buffer ${value.length}` : `string ${value.length}`;
|
||||
}
|
||||
console.log(digestLength(undefined), digestLength("hex"));
|
||||
async function digestLater(encoding) {
|
||||
return crypto.createHash("md5").update("z").digest(encoding);
|
||||
}
|
||||
|
||||
const key = crypto.generateKeyPairSync("ed25519").publicKey;
|
||||
const exported = (options) => key.export(options);
|
||||
console.log(
|
||||
Buffer.isBuffer(exported({ format: "der", type: "spki" })),
|
||||
typeof exported({ format: "pem", type: "spki" }),
|
||||
exported({ format: "jwk" }).kty,
|
||||
);
|
||||
|
||||
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "overloads-")), "text.txt");
|
||||
fs.writeFileSync(file, "hello");
|
||||
const read = (options) => fs.readFileSync(file, options);
|
||||
const text = read("utf8");
|
||||
const bytes = read(undefined);
|
||||
const latin1 = read({ encoding: "latin1" });
|
||||
console.log(text, Buffer.isBuffer(bytes), bytes.length, latin1);
|
||||
console.log(read("utf8"), read({ encoding: "hex" }));
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {number} value
|
||||
* @returns {number}
|
||||
*/
|
||||
/** @param {string | number} value */
|
||||
function same(value) {
|
||||
return value;
|
||||
}
|
||||
const viaSame = (value) => same(value);
|
||||
console.log(typeof viaSame("s"), typeof viaSame(3), viaSame(3) + 1);
|
||||
|
||||
(async () => {
|
||||
const later = await digestLater(undefined);
|
||||
console.log(Buffer.isBuffer(later), typeof (await digestLater("hex")));
|
||||
fs.rmSync(path.dirname(file), { recursive: true });
|
||||
})();
|
||||
@@ -1,11 +1,18 @@
|
||||
// Records built in JavaScript in another key order than the TypeScript
|
||||
// literals of the same type: a replacement method installed through a
|
||||
// computed key, and a function stored on the global object, read through a
|
||||
// typed view, that calls a callback inside its options record.
|
||||
// computed key (beside another computed write that installs a function
|
||||
// returning a primitive), and a function stored on the global object, read
|
||||
// through a typed view, that calls a callback inside its options record.
|
||||
export function install(target, name) {
|
||||
target[name] = () => ({ a: 1, b: 2 });
|
||||
}
|
||||
|
||||
// A later computed write of a function that returns a primitive does not
|
||||
// hide the record-returning one above.
|
||||
export function installLabel(target, name) {
|
||||
target[name] = () => "label";
|
||||
}
|
||||
|
||||
globalThis.__eachRow = function (options) {
|
||||
options.visit({ x: 5, y: 6 });
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copies and enumerations of records JavaScript builds follow the order
|
||||
// JavaScript wrote, also when the TypeScript literals of the type write
|
||||
// another order and spread their own type. Each case uses its own type.
|
||||
import { install } from "./lib.js";
|
||||
import { install, installLabel } from "./lib.js";
|
||||
|
||||
interface Pair {
|
||||
a: number;
|
||||
@@ -39,6 +39,9 @@ showPair("method", store.get());
|
||||
showPair("self copy", { ...store.get(), b: 3 });
|
||||
install(store, "get");
|
||||
showPair("replaced", store.get());
|
||||
const labels: { label(): string } = { label: () => "plain" };
|
||||
installLabel(labels, "label");
|
||||
console.log(labels.label());
|
||||
|
||||
// A JavaScript function, checked into a typed slot, calls a native callback
|
||||
// it receives inside its options record with a record it builds.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// A JavaScript function that calls a callback inside its options object
|
||||
// with records it builds, in another key order than the TypeScript literals
|
||||
// of the callback's parameter type.
|
||||
export function each(options) {
|
||||
options.visit({ a: 1, b: 2 });
|
||||
options.visit({ a: 3, b: 4 });
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// A native callback handed to JavaScript inside an options record receives
|
||||
// records JavaScript builds: copies and enumerations of them follow the
|
||||
// order JavaScript wrote, also when the TypeScript literals of the type
|
||||
// write another order and spread their own type.
|
||||
import { each } from "./lib.js";
|
||||
|
||||
interface Row {
|
||||
a: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
function show(label: string, row: Row): void {
|
||||
const copy = { ...row };
|
||||
console.log(label, Object.keys(row).join(), JSON.stringify(copy), Object.keys(copy).join());
|
||||
}
|
||||
|
||||
const literal: Row = { b: 20, a: 10 };
|
||||
show("literal", literal);
|
||||
show("self copy", { ...literal, b: 21 });
|
||||
const seen: Row[] = [];
|
||||
each({
|
||||
visit: (row: Row) => {
|
||||
seen.push(row);
|
||||
show("callback", row);
|
||||
},
|
||||
});
|
||||
console.log(JSON.stringify(seen), JSON.stringify(seen.map((row) => ({ ...row, a: row.a * 2 }))));
|
||||
@@ -0,0 +1,70 @@
|
||||
// TypedArray.prototype.indexOf, lastIndexOf and includes on every numeric
|
||||
// element kind: strict equality (NaN never found) for indexOf/lastIndexOf,
|
||||
// SameValueZero (NaN found, -0 equals 0) for includes, and fromIndex as
|
||||
// ToIntegerOrInfinity, counted from the end when negative.
|
||||
const bytes = new Uint8Array([1, 2, 10, 3, 10, 255]);
|
||||
console.log("u8", bytes.indexOf(10), bytes.indexOf(10, 3), bytes.indexOf(10, 5), bytes.lastIndexOf(10));
|
||||
console.log("u8 from", bytes.indexOf(10, -2), bytes.indexOf(10, -100), bytes.indexOf(1, Infinity), bytes.indexOf(10, 2.9));
|
||||
console.log("u8 last", bytes.lastIndexOf(10, 3), bytes.lastIndexOf(10, -3), bytes.lastIndexOf(1, -100), bytes.lastIndexOf(255, 99));
|
||||
console.log("u8 values", bytes.indexOf(256 + 10), bytes.indexOf(-246), bytes.indexOf(10.5), bytes.includes(255), bytes.includes(0));
|
||||
console.log("u8 includes from", bytes.includes(1, 1), bytes.includes(255, -1), bytes.includes(2, -5));
|
||||
|
||||
const clamped = new Uint8ClampedArray([0, 300, -5, 127.5]);
|
||||
console.log("u8c", clamped.indexOf(255), clamped.indexOf(0, 1), clamped.lastIndexOf(0), clamped.indexOf(128));
|
||||
|
||||
const signed = new Int8Array([-1, 127, -128, 0]);
|
||||
console.log("i8", signed.indexOf(-1), signed.indexOf(255), signed.indexOf(-128), signed.lastIndexOf(0));
|
||||
|
||||
const shorts = new Int16Array([5, -300, 5, 7]);
|
||||
const ushorts = new Uint16Array([65535, 1, 65535]);
|
||||
console.log("16", shorts.indexOf(-300), shorts.lastIndexOf(5), ushorts.indexOf(-1), ushorts.lastIndexOf(65535, 1));
|
||||
|
||||
const ints = new Int32Array([-2147483648, 0, 2147483647, 0]);
|
||||
const uints = new Uint32Array([4294967295, 3]);
|
||||
console.log("32", ints.indexOf(2147483647), ints.lastIndexOf(0), uints.indexOf(4294967295), uints.includes(-1));
|
||||
|
||||
const floats = new Float64Array([1.5, NaN, -0, 0, Infinity]);
|
||||
console.log("f64", floats.indexOf(NaN), floats.lastIndexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.indexOf(-0));
|
||||
console.log("f64 more", floats.lastIndexOf(0), floats.indexOf(Infinity), floats.includes(-Infinity), floats.includes(NaN, 2));
|
||||
|
||||
const singles = new Float32Array([1.1, 0.5, NaN]);
|
||||
console.log("f32", singles.indexOf(1.1), singles.indexOf(Math.fround(1.1)), singles.indexOf(0.5), singles.includes(NaN), singles.indexOf(NaN));
|
||||
|
||||
const empty = new Float64Array(0);
|
||||
console.log("empty", empty.indexOf(0), empty.lastIndexOf(0), empty.includes(0), empty.includes(0, -1));
|
||||
|
||||
// Views search their own window of the buffer.
|
||||
const buffer = new ArrayBuffer(16);
|
||||
const whole = new Uint8Array(buffer);
|
||||
whole.set([9, 9, 1, 2, 9, 3, 9, 9]);
|
||||
const view = new Uint8Array(buffer, 2, 4);
|
||||
const sub = whole.subarray(3, 6);
|
||||
console.log("views", view.indexOf(9), view.lastIndexOf(9), view.indexOf(1), sub.indexOf(9), sub.includes(1), sub.lastIndexOf(3));
|
||||
const words = new Uint16Array(buffer, 2, 3);
|
||||
console.log("word view", words.indexOf(0x0201), words.length);
|
||||
|
||||
// Shared memory searches the same way.
|
||||
const shared = new Int32Array(new SharedArrayBuffer(16));
|
||||
shared[1] = 7;
|
||||
shared[3] = 7;
|
||||
console.log("shared", shared.indexOf(7), shared.lastIndexOf(7), shared.includes(0), shared.indexOf(7, 2));
|
||||
|
||||
// A loop that finds every occurrence, as a line splitter does.
|
||||
const text = new TextEncoder().encode("one\ntwo\n\nthree\n");
|
||||
const lines: number[] = [];
|
||||
for (let at = text.indexOf(10); at !== -1; at = text.indexOf(10, at + 1)) lines.push(at);
|
||||
console.log("lines", lines.join(","));
|
||||
|
||||
// A Buffer passed as a Uint8Array keeps Buffer's search: a number needle
|
||||
// is taken as a byte, and a NaN byteOffset searches the whole buffer.
|
||||
function searchView(view: Uint8Array): string {
|
||||
return [view.indexOf(97 + 256), view.lastIndexOf(98, NaN), view.includes(353), view.indexOf(98, -1)].join(",");
|
||||
}
|
||||
console.log(searchView(Buffer.from("abcab")), searchView(new Uint8Array([97, 98, 99, 97, 98])));
|
||||
|
||||
// An explicit undefined fromIndex converts to NaN, which searches from 0:
|
||||
// lastIndexOf then looks at index 0 only.
|
||||
const values = new Int16Array([3, 1, 3, 2, 3]);
|
||||
const absent: number | undefined = process.argv.length > 99 ? 1 : undefined;
|
||||
console.log("undefined from", values.indexOf(3, undefined), values.lastIndexOf(3, undefined), values.lastIndexOf(1, undefined), values.includes(2, undefined));
|
||||
console.log("maybe undefined from", values.indexOf(3, absent), values.lastIndexOf(3, absent), values.includes(1, absent));
|
||||
@@ -0,0 +1,28 @@
|
||||
// TypedArray search on untyped values (parameters of untyped functions and
|
||||
// object members): needles that are not numbers are never found (strict
|
||||
// equality), fromIndex may be undefined, a string or a fraction, and
|
||||
// Buffers keep their own indexOf, which wraps number needles and takes
|
||||
// strings.
|
||||
function views(bytes, floats, shorts) {
|
||||
return { bytes, floats, shorts };
|
||||
}
|
||||
const { bytes, floats, shorts } = views(
|
||||
new Uint8Array([1, 2, 10, 3, 10]),
|
||||
new Float64Array([0.5, NaN, -0]),
|
||||
new Int16Array([-1, 300, -1]),
|
||||
);
|
||||
console.log(bytes.indexOf(10), bytes.indexOf(10, 3), bytes.lastIndexOf(10), bytes.includes(3), bytes.includes(4));
|
||||
console.log(bytes.lastIndexOf(1, undefined), bytes.lastIndexOf(10), bytes.indexOf(10, undefined), bytes.includes(1, undefined));
|
||||
console.log(bytes.indexOf(10, '3'), bytes.indexOf(10, 2.9), bytes.indexOf(10, -1), bytes.lastIndexOf(10, -2), bytes.indexOf(10, null));
|
||||
console.log(bytes.indexOf('10'), bytes.includes('1'), bytes.indexOf(10n), bytes.indexOf(), bytes.includes(), bytes.indexOf(266));
|
||||
console.log(floats.indexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.lastIndexOf(-0), floats.includes(0.5, 1));
|
||||
console.log(shorts.indexOf(-1), shorts.lastIndexOf(-1), shorts.indexOf(300, -2), shorts.includes(65535));
|
||||
const holder = { view: bytes.subarray(2) };
|
||||
console.log(holder.view.indexOf(10), holder.view.lastIndexOf(10), holder.view.includes(1));
|
||||
const buffer = Buffer.from([1, 2, 10, 3, 10]);
|
||||
const boxed = [buffer];
|
||||
console.log(boxed[0].indexOf(10), boxed[0].indexOf(266), boxed[0].indexOf('\n'), boxed[0].includes(Buffer.from([3, 10])));
|
||||
console.log(boxed[0].subarray(3).indexOf(10), Buffer.isBuffer(boxed[0].subarray(3)), boxed[0].lastIndexOf(10, -2));
|
||||
let count = 0;
|
||||
for (let at = bytes.indexOf(10); at !== -1; at = bytes.indexOf(10, at + 1)) count++;
|
||||
console.log('count', count);
|
||||
@@ -0,0 +1,55 @@
|
||||
// Module bindings that hold `new F()` of an ordinary constructor function
|
||||
// (const, let and var), read and reassigned by functions declared elsewhere
|
||||
// in the module, such as the head and tail of a linked list.
|
||||
'use strict';
|
||||
function Segment(prev) {
|
||||
this.prev = prev;
|
||||
this.next = null;
|
||||
this.data = new Array(4);
|
||||
this.pos = 0;
|
||||
}
|
||||
const head = new Segment();
|
||||
let current = head;
|
||||
var spare = new Segment(head);
|
||||
|
||||
function append(value) {
|
||||
if (current.pos === current.data.length) {
|
||||
current = current.next || (current.next = new Segment(current));
|
||||
}
|
||||
current.data[current.pos++] = value;
|
||||
}
|
||||
|
||||
function collect() {
|
||||
const out = [];
|
||||
for (let node = head; node; node = node.next) out.push(node.data.slice(0, node.pos).join(''));
|
||||
return out.join('|');
|
||||
}
|
||||
|
||||
function reset() {
|
||||
current = head;
|
||||
for (let node = head; node; node = node.next) node.pos = 0;
|
||||
}
|
||||
|
||||
for (const ch of 'abcdefghij') append(ch);
|
||||
console.log(collect(), current === head, current.prev === head.next ? 'two' : 'more');
|
||||
reset();
|
||||
append('z');
|
||||
console.log(collect(), current === head, head.next !== null);
|
||||
console.log(spare.prev === head, spare instanceof Segment, typeof spare.data);
|
||||
spare = new Segment(spare);
|
||||
console.log(spare.prev.prev === head);
|
||||
|
||||
function Counter(start) {
|
||||
this.value = start;
|
||||
}
|
||||
Counter.prototype.bump = function () {
|
||||
return ++this.value;
|
||||
};
|
||||
let counter = new Counter(10);
|
||||
function bumpTwice() {
|
||||
counter.bump();
|
||||
return counter.bump();
|
||||
}
|
||||
console.log(bumpTwice(), counter.value);
|
||||
counter = new Counter(0);
|
||||
console.log(bumpTwice());
|
||||
@@ -0,0 +1,106 @@
|
||||
"use strict";
|
||||
// Class instances of a hierarchy cross into untyped code through their
|
||||
// base type many times. Each crossing names the same object: identity,
|
||||
// Map, Set and WeakMap keys, properties added through untyped code, and
|
||||
// field writes from either side stay one object, and every subclass keeps
|
||||
// its own members, as in Node.
|
||||
|
||||
class Shape {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.hits = 0;
|
||||
}
|
||||
area() {
|
||||
return 0;
|
||||
}
|
||||
describe() {
|
||||
return `${this.name}:${this.area()}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Square extends Shape {
|
||||
constructor(side) {
|
||||
super("square");
|
||||
this.side = side;
|
||||
}
|
||||
area() {
|
||||
return this.side * this.side;
|
||||
}
|
||||
}
|
||||
|
||||
class Cube extends Square {
|
||||
constructor(side) {
|
||||
super(side);
|
||||
this.name = "cube";
|
||||
}
|
||||
area() {
|
||||
return 6 * super.area();
|
||||
}
|
||||
get volume() {
|
||||
return this.side ** 3;
|
||||
}
|
||||
}
|
||||
|
||||
class Circle extends Shape {
|
||||
constructor(r) {
|
||||
super("circle");
|
||||
this.r = r;
|
||||
}
|
||||
area() {
|
||||
return Math.round(Math.PI * this.r * this.r);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
function untyped(value) {
|
||||
return value;
|
||||
}
|
||||
/** @param {any} value @param {string} key @param {unknown} extra */
|
||||
function tag(value, key, extra) {
|
||||
value[key] = extra;
|
||||
value.hits++;
|
||||
return value;
|
||||
}
|
||||
|
||||
/** @param {Shape} shape */
|
||||
function crossAsBase(shape) {
|
||||
return untyped(shape);
|
||||
}
|
||||
|
||||
const shapes = [new Shape("plain"), new Square(2), new Cube(3), new Circle(1)];
|
||||
const seen = new Map();
|
||||
const weak = new WeakMap();
|
||||
const set = new Set();
|
||||
for (let round = 0; round < 50; round++) {
|
||||
for (const shape of shapes) {
|
||||
const boxed = crossAsBase(shape);
|
||||
if (round === 0) {
|
||||
seen.set(boxed, shape.name);
|
||||
weak.set(boxed, round);
|
||||
}
|
||||
set.add(boxed);
|
||||
if (boxed !== shape || untyped(boxed) !== crossAsBase(shape)) console.log("identity lost", shape.name);
|
||||
}
|
||||
}
|
||||
console.log(seen.size, set.size, shapes.map((s) => seen.get(crossAsBase(s))).join(","), shapes.every((s) => weak.has(untyped(s))));
|
||||
|
||||
for (const shape of shapes) {
|
||||
const boxed = crossAsBase(shape);
|
||||
tag(boxed, "color", shape.name.length);
|
||||
shape.hits += 10;
|
||||
console.log(boxed.describe(), boxed.area(), boxed.hits, shape.hits, boxed.color, Object.keys(boxed).join(","));
|
||||
console.log(boxed instanceof Shape, boxed instanceof Square, boxed instanceof Cube, boxed instanceof Circle, boxed.constructor.name);
|
||||
}
|
||||
|
||||
/** @type {Shape} */
|
||||
const asBase = shapes[2];
|
||||
const cube = crossAsBase(asBase);
|
||||
console.log(cube.volume, JSON.stringify(cube), "side" in cube, "volume" in cube, Object.prototype.hasOwnProperty.call(cube, "volume"));
|
||||
/** @type {any} */ (shapes[1]).side = 5;
|
||||
console.log(crossAsBase(shapes[1]).area(), shapes[1].area(), untyped(shapes[1]).side);
|
||||
|
||||
const many = [];
|
||||
for (let i = 0; i < 1000; i++) many.push(crossAsBase(i % 2 ? new Square(i) : new Circle(i)));
|
||||
let squares = 0;
|
||||
for (const item of many) if (item instanceof Square) squares += item.side;
|
||||
console.log(many.length, squares, many[999].describe(), many[998].describe());
|
||||
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
// Errors built by a constructor factory, the way zod 4 builds ZodError: a
|
||||
// class extending the parent passed in, then an initializer that assigns
|
||||
// `name`, defines non-enumerable fields and an enumerable `message`
|
||||
// accessor, and a second initializer that reassigns `name`. JSON and
|
||||
// Object.keys list `name` before `message`, as they were created, and
|
||||
// `name` holds the last assignment.
|
||||
|
||||
const messageDescriptor = {
|
||||
get() {
|
||||
return this.state.message ?? JSON.stringify(this.state.issues);
|
||||
},
|
||||
set(value) {
|
||||
this.state.message = value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
|
||||
function factory(name, initializer, parent) {
|
||||
const Parent = parent ?? Object;
|
||||
class Definition extends Parent {}
|
||||
Object.defineProperty(Definition, "name", { value: name });
|
||||
function construct(def) {
|
||||
const instance = parent ? new Definition() : this;
|
||||
Object.defineProperty(instance, "state", { value: { issues: def }, enumerable: false });
|
||||
initializer(instance, def);
|
||||
return instance;
|
||||
}
|
||||
Object.defineProperty(construct, "init", { value: initializer });
|
||||
Object.defineProperty(construct, "name", { value: name });
|
||||
return construct;
|
||||
}
|
||||
|
||||
const baseInit = (instance, def) => {
|
||||
instance.name = "$BaseError";
|
||||
Object.defineProperty(instance, "issues", { value: def, enumerable: false });
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
};
|
||||
const BaseError = factory("$BaseError", baseInit, Error);
|
||||
const ReportError = factory(
|
||||
"ReportError",
|
||||
(instance, def) => {
|
||||
BaseError.init(instance, def);
|
||||
instance.name = "ReportError";
|
||||
},
|
||||
Error,
|
||||
);
|
||||
|
||||
const report = new ReportError([{ code: "too_small", path: ["title"] }]);
|
||||
console.log(report.name, report instanceof Error, Object.keys(report).join(","));
|
||||
console.log(JSON.stringify(report));
|
||||
console.log(JSON.stringify({ success: false, error: report }));
|
||||
report.message = "replaced";
|
||||
console.log(JSON.stringify(report), report.message);
|
||||
report.name = "Renamed";
|
||||
console.log(JSON.stringify(report), report.name);
|
||||
|
||||
const base = new BaseError([1, 2]);
|
||||
console.log(base.name, Object.keys(base).join(","), JSON.stringify(base));
|
||||
|
||||
// The same steps on a class extending Error directly.
|
||||
function build(flag) {
|
||||
class Direct extends Error {}
|
||||
const instance = flag ? new Direct() : {};
|
||||
instance.name = "First";
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
Object.defineProperty(instance, "state", { value: { issues: ["i"] }, enumerable: false });
|
||||
instance.name = "Second";
|
||||
return instance;
|
||||
}
|
||||
const direct = build(true);
|
||||
console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name);
|
||||
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
// Error subclasses with a property bag that a later dynamic definition or
|
||||
// write commits. Object.keys and JSON keep the order in which an instance
|
||||
// created its properties: a class field before an own `name` that is
|
||||
// assigned after construction, a `name` a constructor assigns before a
|
||||
// later property, and a `message` that Error's constructor created before
|
||||
// all of them.
|
||||
|
||||
const messageDescriptor = {
|
||||
get() {
|
||||
return "described";
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
|
||||
// A factory-built class: the field, then an initializer that assigns `name`
|
||||
// and defines `message`.
|
||||
function factory(initializer) {
|
||||
class Definition extends Error {
|
||||
code = "E_FIELD";
|
||||
}
|
||||
return function construct() {
|
||||
const instance = new Definition();
|
||||
initializer(instance);
|
||||
return instance;
|
||||
};
|
||||
}
|
||||
const Fielded = factory((instance) => {
|
||||
instance.name = "Fielded";
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
});
|
||||
const fielded = new Fielded();
|
||||
console.log(Object.keys(fielded).join(","), JSON.stringify(fielded));
|
||||
fielded.extra = 1;
|
||||
console.log(Object.keys(fielded).join(","), JSON.stringify(fielded));
|
||||
fielded.name = "Renamed";
|
||||
console.log(Object.keys(fielded).join(","), JSON.stringify(fielded), fielded.name);
|
||||
|
||||
// A class whose constructor passes a message to Error and then assigns
|
||||
// `name`; the message is redefined after construction.
|
||||
class Direct extends Error {
|
||||
code = "E_DIRECT";
|
||||
constructor() {
|
||||
super("constructed");
|
||||
this.name = "Direct";
|
||||
}
|
||||
}
|
||||
function redefine(instance) {
|
||||
Object.defineProperty(instance, "message", messageDescriptor);
|
||||
instance.extra = 2;
|
||||
return instance;
|
||||
}
|
||||
const direct = redefine(new Direct());
|
||||
console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name);
|
||||
|
||||
// Constructors that assign `name` and then another property, which their
|
||||
// instances create in that order, with and without a message passed to
|
||||
// Error, before the message is redefined.
|
||||
class Named extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "Named";
|
||||
this.detail = "named";
|
||||
}
|
||||
}
|
||||
const named = redefine(new Named());
|
||||
console.log(Object.keys(named).join(","), JSON.stringify(named), named.name);
|
||||
|
||||
class NamedMessage extends Error {
|
||||
constructor() {
|
||||
super("constructed");
|
||||
this.name = "NamedMessage";
|
||||
this.detail = "named";
|
||||
}
|
||||
}
|
||||
const namedMessage = redefine(new NamedMessage());
|
||||
console.log(Object.keys(namedMessage).join(","), JSON.stringify(namedMessage), namedMessage.name);
|
||||
@@ -0,0 +1,58 @@
|
||||
// Expected errors stored in an object literal's fields: a JavaScript
|
||||
// object keeps its fields checked-dynamic, and assert.throws, assert.match
|
||||
// and the expected-object form convert them to the regex or string the
|
||||
// fields hold.
|
||||
"use strict";
|
||||
const assert = require("assert");
|
||||
|
||||
const errMessages = {
|
||||
auth: / auth/,
|
||||
state: / state/,
|
||||
length: /Invalid initialization vector/,
|
||||
code: "ERR_CRYPTO_INVALID_IV",
|
||||
};
|
||||
|
||||
const error = (message, code) => {
|
||||
const e = new Error(message);
|
||||
if (code) e.code = code;
|
||||
return e;
|
||||
};
|
||||
|
||||
assert.throws(() => {
|
||||
throw error("Unsupported state or unable to authenticate data");
|
||||
}, errMessages.auth);
|
||||
assert.throws(() => {
|
||||
throw error("Invalid state for operation getAuthTag");
|
||||
}, errMessages.state);
|
||||
assert.throws(
|
||||
() => {
|
||||
throw error("Invalid initialization vector", "ERR_CRYPTO_INVALID_IV");
|
||||
},
|
||||
{ code: errMessages.code, message: errMessages.length },
|
||||
);
|
||||
assert.match("unable to auth", errMessages.auth);
|
||||
assert.doesNotMatch("no match here", errMessages.state);
|
||||
|
||||
const report = (label, run) => {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "passed");
|
||||
} catch (error) {
|
||||
console.log(label, error.name, error.code, error.message.split("\n")[0]);
|
||||
}
|
||||
};
|
||||
report("wrong message", () =>
|
||||
assert.throws(() => {
|
||||
throw error("something else");
|
||||
}, errMessages.length),
|
||||
);
|
||||
report("wrong code", () =>
|
||||
assert.throws(
|
||||
() => {
|
||||
throw error("Invalid initialization vector", "ERR_OTHER");
|
||||
},
|
||||
{ code: errMessages.code },
|
||||
),
|
||||
);
|
||||
report("match", () => assert.match("plain text", errMessages.auth));
|
||||
console.log("done");
|
||||
@@ -0,0 +1,40 @@
|
||||
// Several typed array accesses in one counted loop share an index that is
|
||||
// not an affine function of the counter (a masked or remaindered counter).
|
||||
// The loop versioning analysis records that such an index has no affine
|
||||
// shape and reads the recorded result again at each later access.
|
||||
const length = 64;
|
||||
const bytes = new Uint8Array(length);
|
||||
const words = new Int32Array(16);
|
||||
const totals = new Float64Array(16);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const slot = i & 15;
|
||||
words[slot] = words[slot]! + i;
|
||||
totals[slot] = totals[slot]! + i * 0.5;
|
||||
bytes[i] = slot;
|
||||
}
|
||||
console.log(Array.from(words).join(","));
|
||||
console.log(Array.from(totals).join(","));
|
||||
console.log(bytes.reduce((sum, value) => sum + value, 0));
|
||||
|
||||
const ring = new Uint16Array(8);
|
||||
const seen = new Uint8Array(8);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const at = i % 8;
|
||||
ring[at] = ring[at]! * 3 + i;
|
||||
seen[at] = seen[at]! + 1;
|
||||
}
|
||||
console.log(Array.from(ring).join(","), Array.from(seen).join(","));
|
||||
|
||||
function scatter(source: Uint8Array, mask: number): Uint32Array {
|
||||
const out = new Uint32Array(mask + 1);
|
||||
const counts = new Uint32Array(mask + 1);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const bucket = (source[i]! + i) & mask;
|
||||
out[bucket] = out[bucket]! + source[i]!;
|
||||
counts[bucket] = counts[bucket]! + 1;
|
||||
}
|
||||
for (let i = 0; i < out.length; i++) out[i] = out[i]! * 10 + counts[i]!;
|
||||
return out;
|
||||
}
|
||||
console.log(Array.from(scatter(bytes, 7)).join(","));
|
||||
@@ -0,0 +1,10 @@
|
||||
// for await over process.stdin yields Buffers: Buffer.isBuffer holds and
|
||||
// indexOf takes a byte and a start offset (only chunking-independent facts
|
||||
// are printed).
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
||||
console.log('buffers', chunks.every((chunk) => Buffer.isBuffer(chunk)));
|
||||
const all = Buffer.concat(chunks);
|
||||
const newlines = [];
|
||||
for (let at = all.indexOf(10); at !== -1; at = all.indexOf(10, at + 1)) newlines.push(at);
|
||||
console.log('newlines', newlines.join(','), all.subarray(newlines[0] + 1).indexOf('#'));
|
||||
@@ -0,0 +1,27 @@
|
||||
// 'readable', 'data' and resume() together, with no read() for a while:
|
||||
// 'readable' keeps the stream paused, so it stops reading at its read-ahead
|
||||
// mark until a read drains it, and 'data' sees what read() returns.
|
||||
const input = process.stdin;
|
||||
let draining = false;
|
||||
let peak = 0;
|
||||
let dataBytes = 0;
|
||||
let readBytes = 0;
|
||||
const drain = () => {
|
||||
let chunk;
|
||||
while ((chunk = input.read()) !== null) readBytes += chunk.length;
|
||||
};
|
||||
input.on('data', (chunk) => {
|
||||
dataBytes += chunk.length;
|
||||
});
|
||||
input.on('readable', () => {
|
||||
peak = Math.max(peak, input.readableLength);
|
||||
if (draining) drain();
|
||||
});
|
||||
input.resume();
|
||||
setTimeout(() => {
|
||||
peak = Math.max(peak, input.readableLength);
|
||||
console.log('held below 128 KiB while not draining', peak <= 131072);
|
||||
draining = true;
|
||||
drain();
|
||||
}, 400);
|
||||
input.on('end', () => console.log('read', readBytes, 'data', dataBytes));
|
||||
@@ -0,0 +1,9 @@
|
||||
// read(size) above the 64 KiB read-ahead mark: 'readable' keeps firing as
|
||||
// input arrives until the requested size is buffered.
|
||||
const input = process.stdin;
|
||||
const sizes = [];
|
||||
input.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = input.read(150000)) !== null) sizes.push(chunk.length);
|
||||
});
|
||||
input.on('end', () => console.log(sizes.join(',')));
|
||||
@@ -0,0 +1,12 @@
|
||||
// A once('readable') listener that reads part of the input: the rest stays
|
||||
// buffered for a 'data' listener added afterwards.
|
||||
const input = process.stdin;
|
||||
input.once('readable', () => {
|
||||
const head = input.read(3);
|
||||
console.log('head', head === null ? null : head.toString('ascii'));
|
||||
setTimeout(() => {
|
||||
const rest = [];
|
||||
input.on('data', (chunk) => rest.push(chunk.toString('ascii')));
|
||||
input.on('end', () => console.log('rest', rest.join('')));
|
||||
}, 50);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// Fixed-size reads of stdin: read(n) answers null until n bytes are
|
||||
// buffered, and the rest once the input ended.
|
||||
const input = process.stdin;
|
||||
const chunks = [];
|
||||
const counts = [];
|
||||
input.on('readable', () => {
|
||||
let reads = 0;
|
||||
let chunk;
|
||||
while ((chunk = input.read(4)) !== null) {
|
||||
chunks.push(chunk.toString('ascii'));
|
||||
reads++;
|
||||
}
|
||||
counts.push(reads);
|
||||
});
|
||||
input.on('end', () => {
|
||||
console.log(chunks.join('|'));
|
||||
console.log(counts.join(','));
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// process.stdin in paused mode through 'readable': each event drains the
|
||||
// buffer with read(), one more event follows EOF, and 'end' comes after the
|
||||
// read that finds the buffer empty.
|
||||
const input = process.stdin;
|
||||
const events = [];
|
||||
let text = '';
|
||||
input.on('readable', () => {
|
||||
let reads = 0;
|
||||
let chunk;
|
||||
while ((chunk = input.read()) !== null) {
|
||||
text += String(chunk);
|
||||
reads++;
|
||||
}
|
||||
events.push(`readable ${reads}`);
|
||||
});
|
||||
input.on('end', () => {
|
||||
console.log(events.join(', '));
|
||||
console.log(JSON.stringify(text), input.readableLength);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
// @hono/zod-validator without a hook: a failed check answers 400 with the
|
||||
// whole safeParse result serialized, so the ZodError's own enumerable
|
||||
// properties (name, then message) and their order reach the client.
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Hono } from "hono";
|
||||
import { z } from "zod";
|
||||
|
||||
const Item = z.object({ title: z.string().min(2), qty: z.number().int().positive() });
|
||||
|
||||
const app = new Hono();
|
||||
app.post("/items", zValidator("json", Item), (c) => {
|
||||
const item = c.req.valid("json");
|
||||
return c.json({ created: item.title, qty: item.qty }, 201);
|
||||
});
|
||||
app.get("/items", zValidator("query", z.object({ limit: z.coerce.number().int().max(5) })), (c) => {
|
||||
return c.json({ limit: c.req.valid("query").limit });
|
||||
});
|
||||
|
||||
const post = (body: unknown) =>
|
||||
app.request("/items", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
||||
|
||||
const show = async (label: string, res: Response | Promise<Response>) => {
|
||||
const r = await res;
|
||||
console.log(label, r.status, await r.text());
|
||||
};
|
||||
|
||||
await show("ok", post({ title: "pen", qty: 2 }));
|
||||
await show("bad-title", post({ title: "p", qty: 2 }));
|
||||
await show("bad-both", post({ title: 3, qty: -1 }));
|
||||
await show("query-ok", app.request("/items?limit=4"));
|
||||
await show("query-bad", app.request("/items?limit=9"));
|
||||
|
||||
const result = Item.safeParse({ title: "x", qty: 0 });
|
||||
if (!result.success) {
|
||||
const e = result.error;
|
||||
console.log(e.name, e instanceof Error, Object.keys(e).join(","));
|
||||
console.log(JSON.stringify(e));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "hono-zod-default-hook-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2653, "san": 3208 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2991, "san": 4866 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6931, "san": 8003 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2204, "san": 3060 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1649, "san": 958 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1623, "san": 962 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4600, "san": 5500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2200, "san": 10300 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1900, "san": 9700 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2656, "san": 5234 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2791, "san": 3186 }
|
||||
@@ -34,6 +34,66 @@ export const eventLoopCases: EventLoopCase[] = [
|
||||
fixture: "stdio-paused-exit.mjs",
|
||||
script: { writes: [], end: false },
|
||||
},
|
||||
{
|
||||
title: "stdio values: 'readable' drains with read(), then fires once more at EOF",
|
||||
fixture: "stdio-readable.mjs",
|
||||
script: {
|
||||
writes: [
|
||||
{ delayMs: 0, data: "alpha " },
|
||||
{ delayMs: 60, data: "beta\n" },
|
||||
],
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "stdio values: 'readable' with read(size) holds a partial tail until EOF",
|
||||
fixture: "stdio-readable-size.mjs",
|
||||
script: { writes: [{ delayMs: 0, data: "abcdefghij" }], end: true },
|
||||
},
|
||||
{
|
||||
title: "stdio values: input a once('readable') listener leaves stays buffered for 'data'",
|
||||
fixture: "stdio-readable-once-rest.mjs",
|
||||
script: { writes: [{ delayMs: 0, data: "abcdefgh" }], end: true },
|
||||
},
|
||||
{
|
||||
title: "stdio values: 'readable' with a read(size) above the read-ahead mark",
|
||||
fixture: "stdio-readable-large-size.mjs",
|
||||
script: {
|
||||
writes: [
|
||||
{ delayMs: 0, data: "a".repeat(70000) },
|
||||
{ delayMs: 30, data: "b".repeat(70000) },
|
||||
{ delayMs: 30, data: "c".repeat(60000) },
|
||||
],
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "stdio values: 'readable' with 'data' and resume() holds input at the read-ahead mark",
|
||||
fixture: "stdio-readable-backpressure.mjs",
|
||||
script: {
|
||||
writes: Array.from({ length: 16 }, (_, i) => ({
|
||||
delayMs: i === 0 ? 0 : 5,
|
||||
data: String.fromCharCode(97 + i).repeat(65536),
|
||||
})),
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "stdio values: 'readable' on an immediately ended pipe",
|
||||
fixture: "stdio-readable.mjs",
|
||||
script: { writes: [], end: true },
|
||||
},
|
||||
{
|
||||
title: "for-await: process.stdin yields Buffers",
|
||||
fixture: "stdin-for-await-buffers.mjs",
|
||||
script: {
|
||||
writes: [
|
||||
{ delayMs: 0, data: "#one\nabcd\n" },
|
||||
{ delayMs: 40, data: "#two\nefgh\n" },
|
||||
],
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "read-all: one instant write, then EOF",
|
||||
fixture: "stdin-read-all.ts",
|
||||
|
||||
@@ -115,10 +115,11 @@ const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);
|
||||
* built-in undici implementation's module graph, the zod 3 cases (zod3,
|
||||
* zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's 3,700-line
|
||||
* types module, and the zod 4 cases (zod4-boundary, zod4-record,
|
||||
* hono-zod-validator)
|
||||
* hono-zod-validator, hono-zod-default-hook)
|
||||
* compile zod 4's core, classic API and locales. A sanitized build of any
|
||||
* of them takes minutes on a CI runner. */
|
||||
const TIMEOUTS: Record<string, number> = {
|
||||
"hono-zod-default-hook": 900_000,
|
||||
"hono-zod-validator": 900_000,
|
||||
undici5: 600_000,
|
||||
undici7: 600_000,
|
||||
|
||||
@@ -92,6 +92,7 @@ const programs = [
|
||||
"local-union-storage.ts",
|
||||
"class-callback-storage.ts",
|
||||
"byte-record-windows.ts",
|
||||
"typed-array-masked-index-stores.ts",
|
||||
"constant-callback-dispatch.ts",
|
||||
"constant-number-remainders.ts",
|
||||
"numeric-call-specialization.ts",
|
||||
@@ -99,6 +100,10 @@ const programs = [
|
||||
"native-worker-roots/main.ts",
|
||||
"native-worker-shared-views.ts",
|
||||
];
|
||||
// Debug info turns the emitter's loop optimizations off. These programs are
|
||||
// emitted without it, so the compiled emitter runs its loop analyses on them
|
||||
// (typed-array loop versioning, which caches null index shapes).
|
||||
const withoutDebugInfo = new Set(["typed-array-masked-index-stores.ts"]);
|
||||
|
||||
for (const backend of ["llvm"] as const) {
|
||||
test(
|
||||
@@ -227,10 +232,12 @@ for (const backend of ["llvm"] as const) {
|
||||
if (!lowered.ok)
|
||||
throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`);
|
||||
const mod = deserializeModule(readFileSync(irPath, "utf8"));
|
||||
const request: LlvmEmitterRequest = llvmEmitterRequest({
|
||||
debug: true,
|
||||
sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
|
||||
});
|
||||
const request: LlvmEmitterRequest = withoutDebugInfo.has(source)
|
||||
? llvmEmitterRequest()
|
||||
: llvmEmitterRequest({
|
||||
debug: true,
|
||||
sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
|
||||
});
|
||||
const text = await emit(mod, source, request);
|
||||
const llvmPath = join(dir, "program.ll");
|
||||
const outPath = executable("program");
|
||||
|
||||
Reference in New Issue
Block a user