Merge main

This commit is contained in:
Chris Tate
2026-10-23 21:48:59 -05:00
72 changed files with 1954 additions and 84 deletions
@@ -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);
}
+4
View File
@@ -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"');
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7189-non-null-assertion-nullish-destination.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7453-untyped-overload-results.cjs"
],
"diags": []
}
@@ -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": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7680-typed-array-search.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7681-typed-array-search-dynamic.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7682-constructor-function-globals.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7796-boxed-subclass-identity.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8112-error-factory-own-key-order.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8113-error-class-field-before-name.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8160-assert-expected-from-fields.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/typed-array-masked-index-stores.ts"
],
"diags": []
}
@@ -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
}
}
]
}
+4
View File
@@ -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);
+4 -2
View File
@@ -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);
+5
View File
@@ -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);
+188 -38
View File
@@ -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();
}
+96
View File
@@ -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 }))));
+70
View File
@@ -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(","));
+10
View File
@@ -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(',')));
+12
View File
@@ -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);
});
+18
View File
@@ -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
View File
@@ -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);
});
+38
View File
@@ -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 }
+60
View File
@@ -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",
+2 -1
View File
@@ -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");