From 97e85bf238e4583ce3daff31577e0d20544c0541 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:35:02 -0500 Subject: [PATCH 1/9] Compile js-yaml and smol-toml statically and match Node --- package.json | 3 +- .../compiler/src/backend/llvm/lib-shared.ts | 9 +- .../lowering/builtins/date-classes.ts | 555 ++++++++++++++++++ .../src/frontend/lowering/builtins/dates.ts | 95 ++- .../src/frontend/lowering/builtins/errors.ts | 111 ++++ .../lowering/class-dynamic-dispatch.ts | 114 +++- .../lowering/class-method-mutations.ts | 3 + .../src/frontend/lowering/class-prototypes.ts | 27 + .../frontend/lowering/class-to-primitive.ts | 51 +- .../frontend/lowering/expressions/binary.ts | 53 +- .../lowering/expressions/element-access.ts | 18 +- .../lowering/expressions/object-literals.ts | 3 + .../lowering/expressions/property-access.ts | 70 ++- .../lowering/expressions/relational.ts | 64 +- .../frontend/lowering/expressions/updates.ts | 60 +- .../src/frontend/lowering/lower-calls.ts | 150 ++++- .../src/frontend/lowering/lower-classes.ts | 79 ++- .../src/frontend/lowering/lower-inspect.ts | 11 +- .../src/frontend/lowering/lower-modules.ts | 2 + .../src/frontend/lowering/lower-stmts.ts | 42 +- .../compiler/src/frontend/lowering/lowerer.ts | 28 +- .../src/frontend/lowering/private-brands.ts | 90 ++- .../src/frontend/lowering/surfaces.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/compiler/src/ir/int32-slots.ts | 1 + packages/compiler/src/ir/tiers.ts | 1 + .../7040-es5-error-constructor.mjs.json | 6 + .../7041-error-called-without-new.mjs.json | 6 + ...42-string-method-names-on-objects.mjs.json | 6 + .../tests/corpus/7043-date-set-time.mjs.json | 6 + .../7044-nested-empty-object-maps.mjs.json | 6 + ...045-instanceof-constructor-values.mjs.json | 6 + .../7046-parse-float-extra-arguments.mjs.json | 6 + .../corpus/7047-dynamic-string-index.mjs.json | 6 + .../7048-unary-number-coercions.mjs.json | 6 + .../7049-member-assignment-values.mjs.json | 6 + .../tests/corpus/7050-date-subclass.mjs.json | 6 + .../corpus/7051-date-subclass-typed.ts.json | 6 + .../7052-date-subclass-dynamic.mjs.json | 6 + ...-private-fields-untyped-receivers.mjs.json | 6 + ...054-let-bindings-with-null-writes.mjs.json | 6 + packages/runtime/src/scr_dyn.h | 38 ++ packages/runtime/src/scr_dyn_invoke.c | 10 + packages/runtime/src/scr_inspect.c | 35 +- packages/runtime/src/scr_json.c | 317 +++++++++- packages/runtime/src/scr_lib.c | 178 +++++- pnpm-lock.yaml | 19 +- tests/corpus/7040-es5-error-constructor.mjs | 79 +++ .../corpus/7041-error-called-without-new.mjs | 40 ++ .../7042-string-method-names-on-objects.mjs | 70 +++ tests/corpus/7043-date-set-time.mjs | 28 + .../corpus/7044-nested-empty-object-maps.mjs | 46 ++ .../7045-instanceof-constructor-values.mjs | 43 ++ .../7046-parse-float-extra-arguments.mjs | 9 + tests/corpus/7047-dynamic-string-index.mjs | 21 + tests/corpus/7048-unary-number-coercions.mjs | 34 ++ .../corpus/7049-member-assignment-values.mjs | 20 + tests/corpus/7050-date-subclass.mjs | 69 +++ tests/corpus/7051-date-subclass-typed.ts | 47 ++ tests/corpus/7052-date-subclass-dynamic.mjs | 43 ++ .../7053-private-fields-untyped-receivers.mjs | 72 +++ .../7054-let-bindings-with-null-writes.mjs | 37 ++ tests/fixtures/npm-static/js-yaml-cli.mjs | 63 ++ tests/fixtures/npm-static/smol-toml-cli.mjs | 68 +++ .../7040-es5-error-constructor.mjs.json | 1 + .../7041-error-called-without-new.mjs.json | 1 + ...42-string-method-names-on-objects.mjs.json | 1 + .../corpus-costs/7043-date-set-time.mjs.json | 1 + .../7044-nested-empty-object-maps.mjs.json | 1 + ...045-instanceof-constructor-values.mjs.json | 1 + .../7046-parse-float-extra-arguments.mjs.json | 1 + .../7047-dynamic-string-index.mjs.json | 1 + .../7048-unary-number-coercions.mjs.json | 1 + .../7049-member-assignment-values.mjs.json | 1 + .../corpus-costs/7050-date-subclass.mjs.json | 1 + .../7051-date-subclass-typed.ts.json | 1 + .../7052-date-subclass-dynamic.mjs.json | 1 + ...-private-fields-untyped-receivers.mjs.json | 1 + ...054-let-bindings-with-null-writes.mjs.json | 1 + tests/harness/npm-static-costs.json | 4 + tests/harness/npm-static.test.ts | 32 + 82 files changed, 3065 insertions(+), 114 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/date-classes.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json create mode 100644 tests/corpus/7040-es5-error-constructor.mjs create mode 100644 tests/corpus/7041-error-called-without-new.mjs create mode 100644 tests/corpus/7042-string-method-names-on-objects.mjs create mode 100644 tests/corpus/7043-date-set-time.mjs create mode 100644 tests/corpus/7044-nested-empty-object-maps.mjs create mode 100644 tests/corpus/7045-instanceof-constructor-values.mjs create mode 100644 tests/corpus/7046-parse-float-extra-arguments.mjs create mode 100644 tests/corpus/7047-dynamic-string-index.mjs create mode 100644 tests/corpus/7048-unary-number-coercions.mjs create mode 100644 tests/corpus/7049-member-assignment-values.mjs create mode 100644 tests/corpus/7050-date-subclass.mjs create mode 100644 tests/corpus/7051-date-subclass-typed.ts create mode 100644 tests/corpus/7052-date-subclass-dynamic.mjs create mode 100644 tests/corpus/7053-private-fields-untyped-receivers.mjs create mode 100644 tests/corpus/7054-let-bindings-with-null-writes.mjs create mode 100644 tests/fixtures/npm-static/js-yaml-cli.mjs create mode 100644 tests/fixtures/npm-static/smol-toml-cli.mjs create mode 100644 tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json create mode 100644 tests/harness/corpus-costs/7041-error-called-without-new.mjs.json create mode 100644 tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json create mode 100644 tests/harness/corpus-costs/7043-date-set-time.mjs.json create mode 100644 tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json create mode 100644 tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json create mode 100644 tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json create mode 100644 tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json create mode 100644 tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json create mode 100644 tests/harness/corpus-costs/7049-member-assignment-values.mjs.json create mode 100644 tests/harness/corpus-costs/7050-date-subclass.mjs.json create mode 100644 tests/harness/corpus-costs/7051-date-subclass-typed.ts.json create mode 100644 tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json create mode 100644 tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json create mode 100644 tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json diff --git a/package.json b/package.json index 5cc30d9f8..69177e0e6 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,11 @@ "@scriptc/types": "workspace:*", "@types/node": "^24.0.0", "effect": "3.22.2", - "js-yaml": "4.3.0", + "js-yaml": "4.3.2", "oxfmt": "0.71.0", "oxlint": "1.86.0", "rollup": "4.62.2", + "smol-toml": "1.7.1", "three": "0.186.1", "tsx": "^4.19.0", "typescript": "5.9.3", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..5669704d0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -997,12 +997,15 @@ export const LIB_FN_SYMS: Record = { "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", "dyn.nativeUrlIs": "scr_dyn_native_url_is", - "dyn.nativeDateIs": "scr_dyn_native_date_is", + "dyn.nativeDateIs": "scr_dyn_date_instance_is", + "dyn.nativeDateHandleIs": "scr_dyn_native_date_is", "dyn.globalConstructor": "scr_dyn_global_constructor", "dyn.builtinFunction": "scr_dyn_builtin_function", "date.nativeNew": "scr_dyn_native_date_new", "date.nativeNewFields": "scr_dyn_native_date_new_fields", "date.nativeNewValue": "scr_dyn_native_date_new_value", + "date.argumentsTime": "scr_dyn_date_arguments_time", + "date.classOperation": "scr_date_class_operation_install", "date.checkedValue": "scr_dyn_native_date_value", "dyn.nativeRegexIs": "scr_dyn_native_regex_is", "dyn.toString": "scr_dyn_to_string_argument", @@ -1478,6 +1481,10 @@ export const LIB_FN_SYMS: Record = { "dyn.arrayPrototype": "scr_dyn_array_prototype", "dyn.arrayConstructor": "scr_dyn_array_constructor", "dyn.objectPrototype": "scr_dyn_object_prototype", + "dyn.errorPrototype": "scr_dyn_error_prototype", + "dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype", + "dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace", + "dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function", "dyn.functionApply": "scr_dyn_function_apply", "dyn.builtinMethod": "scr_dyn_builtin_method", "dyn.classPrototype": "scr_dyn_class_prototype", diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts new file mode 100644 index 000000000..ac6fe33a9 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -0,0 +1,555 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { type ClassInfo } from "../lower-classes.js"; +import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + NULL_T, + STRING, + VOID, + typeKey, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; + +/** The hidden field holding the time value of a class declared `extends + * Date`, the first field of its layout. */ +export const DATE_TIME_FIELD = "%time"; + +/** The class declared `extends Date` at the root of `info`'s base chain, or + * null. */ +export function dateRootOf(info: ClassInfo | null | undefined): ClassInfo | null { + for (let c = info ?? null; c; c = c.base) if (c.extendsDate) return c; + return null; +} + +/** Whether the program declares a class that extends Date. */ +export function programDeclaresDateClass(lowerer: Lowerer): boolean { + for (const info of lowerer.classes.values()) if (info.extendsDate) return true; + return false; +} + +/** The Date.prototype methods a class declared `extends Date` inherits. + * Those without a lowering refuse at the call. */ +const DATE_PROTOTYPE_METHODS: ReadonlySet = new Set([ + "getTime", + "valueOf", + "toISOString", + "toUTCString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toGMTString", + "toLocaleString", + "toLocaleDateString", + "toLocaleTimeString", + "getFullYear", + "getUTCFullYear", + "getYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", + "setTime", + "setFullYear", + "setUTCFullYear", + "setYear", + "setMonth", + "setUTCMonth", + "setDate", + "setUTCDate", + "setHours", + "setUTCHours", + "setMinutes", + "setUTCMinutes", + "setSeconds", + "setUTCSeconds", + "setMilliseconds", + "setUTCMilliseconds", +]); + +export const DATE_PROTOTYPE_METHOD_NAMES: readonly string[] = [...DATE_PROTOTYPE_METHODS]; + +export function isDatePrototypeMethod(name: string): boolean { + return DATE_PROTOTYPE_METHODS.has(name); +} + +/** Whether `name` is a Date method an instance of `info` inherits from + * Date.prototype: its class chain is rooted at a class declared `extends + * Date` and declares no member of that name. The runtime's view of the + * instance answers it (scr_dyn_date_class_method). */ +export function dateInheritedMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + return ( + dateRootOf(info) !== null && + DATE_PROTOTYPE_METHODS.has(name) && + !lowerer.findMethodOn(info, name) && + !info.fields.has(name) + ); +} + +/** super(...args) in a class declared `extends Date`: the time value the + * Date constructor computes from the argument vector `args` (one dyn + * array), stored in the hidden field. */ +export function dateSuperCall(thisRef: IrExpr, info: ClassInfo, args: IrExpr, loc: SrcLoc): IrStmt { + return { + kind: "fieldSet", + obj: thisRef, + className: info.def.name, + field: DATE_TIME_FIELD, + value: { kind: "libCall", fn: "date.argumentsTime", args: [args], type: DATE_T, loc }, + loc, + }; +} + +/** The time value of `receiver`, an instance of a class rooted at `root`. */ +function timeOf(lowerer: Lowerer, receiver: IrExpr, root: ClassInfo, loc: SrcLoc): IrExpr { + return { + kind: "fieldGet", + obj: lowerer.upcastTo(receiver, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; +} + +/** `receiver.name()` with no arguments where the static class `info` does + * not declare `name`: the subclasses of `info` that declare it run theirs, + * any other instance the Date method. `receiver` is a variable read. */ +function instanceMethodCall( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + receiver: IrExpr, + name: string, + inherited: IrExpr, + loc: SrcLoc, +): IrExpr { + const found = lowerer.findMethodOn(info, name); + if (found) { + if (found.sig.params.length > 0) + lowerer.unsupported( + "SC1090", + blame, + `a '${name}' declaration with parameters in a class that extends Date, called by an inherited Date method`, + ); + const args: IrExpr[] = []; + if (lowerer.overrideBelow(info, name)) { + lowerer.noteVirtualEdge(info, name); + return { + kind: "virtualCall", + className: info.def.name, + method: name, + args: [lowerer.upcastTo(receiver, info.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + lowerer.noteEdge(`%${found.declarer.def.name}.${name}`); + return { + kind: "call", + callee: `%${found.declarer.def.name}.${name}`, + args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + let result = inherited; + // Deeper declarations are tested first. + const declaring: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const sub of c.subclasses) { + if (sub.methods.has(name)) declaring.push(sub); + else visit(sub); + } + }; + visit(info); + for (const sub of declaring) { + const call = instanceMethodCall( + lowerer, + blame, + sub, + { kind: "downcast", value: receiver, type: { kind: "object", className: sub.def.name }, loc }, + name, + inherited, + loc, + ); + const type = result.type; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: receiver, className: sub.def.name, type: BOOL, loc }, + then: lowerer.coerceToExpected(call, type), + else_: result, + type, + loc, + }; + } + return result; +} + +/** ToNumber of a call argument. */ +function numberArgument(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr { + if (!node) return { kind: "numLit", value: Number.NaN, type: F64, loc }; + const value = lowerer.lowerExpr(node); + if (value.type.kind === "f64") return value; + if (value.type.kind === "dyn") + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc }; + if (lowerer.dynConvertible(value.type)) + return { + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value, type: DYN, loc }], + type: F64, + loc, + }; + return lowerer.coerceInto(node, value, F64); +} + +/** A Date.prototype method a class rooted at `root` does not declare, + * called on `receiver` (an instance of `info`, evaluated here once). The + * method reads or sets the hidden time value; toJSON calls the instance's + * toISOString, as Date.prototype.toJSON does. */ +export function lowerInheritedDateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + info: ClassInfo, + receiver: IrExpr, +): IrExpr { + const root = dateRootOf(info)!; + const loc = locOf(call); + const local = lowerer.declareHiddenLocal("%dateReceiver", receiver.type); + const value = varRef(local.id, receiver.type, loc); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }]; + const args = call.arguments.nodes; + if (args.some(ts.isSpreadElement)) + lowerer.unsupported("SC1090", call, `spread arguments to 'Date.prototype.${name}'`); + const done = (result: IrExpr): IrExpr => ({ + kind: "seqExpr", + stmts, + result, + type: result.type, + loc, + }); + if (name === "setTime") { + const time = lowerer.declareHiddenLocal("%dateTime", DATE_T); + stmts.push( + { + kind: "varDecl", + localId: time.id, + init: { + kind: "libCall", + fn: "date.newMs", + args: [numberArgument(lowerer, args[0], loc)], + type: DATE_T, + loc, + }, + loc, + }, + ...args.slice(1).map((arg): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc: locOf(arg), + })), + { + kind: "fieldSet", + obj: lowerer.upcastTo(value, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: varRef(time.id, DATE_T, loc), + loc, + }, + ); + return done({ + kind: "libCall", + fn: "date.getTime", + args: [varRef(time.id, DATE_T, loc)], + type: F64, + loc, + }); + } + for (const arg of args) { + if (name !== "toJSON") + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }); + } + if (name === "toJSON") return done(dateClassToJson(lowerer, call, info, value, loc)); + const result = dateValueMethod(name, timeOf(lowerer, value, root, loc), loc); + if (!result) lowerer.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT); + return done(result); +} + +/** Date.prototype.toJSON on `value` (a variable read of an instance of + * `info`): null for an invalid time, else the instance's toISOString(). */ +function dateClassToJson( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + const root = dateRootOf(info)!; + const time: IrExpr = { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, value, root, loc)], + type: F64, + loc, + }; + const type = dateJsonType(lowerer); + const iso = instanceMethodCall( + lowerer, + blame, + info, + value, + "toISOString", + { + kind: "libCall", + fn: "date.toISOStringValue", + args: [timeOf(lowerer, value, root, loc)], + type: STRING, + loc, + }, + loc, + ); + return { + kind: "ternary", + // An invalid date's time value is NaN, the one value unequal to itself. + cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc }, + then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type), + else_: lowerer.coerceToExpected(iso, type), + type, + loc, + }; +} + +/** `super.name(...)` in a class whose base chain declares no `name`, when + * the chain is rooted at a class declared `extends Date`. */ +export function lowerDateSuperMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + cls: ClassInfo, + thisRef: IrExpr, +): IrExpr | null { + if (!dateRootOf(cls) || !isDatePrototypeMethod(name)) return null; + // Date.prototype.toJSON reads this.toISOString, which this class or a + // subclass may declare. + return lowerInheritedDateMethodCall(lowerer, call, name, cls, thisRef); +} + +const DATE_CLASS_TIME = "%date.classTime"; +const DATE_CLASS_METHOD = "%date.classMethod"; + +/** The classes of a program rooted at a class declared `extends Date`. */ +function dateClasses(lowerer: Lowerer): ClassInfo[] { + return [...lowerer.classes.values()].filter( + (info) => !info.def.runtime && !info.generic && dateRootOf(info) !== null, + ); +} + +const PARAMS = ["op", "value", "argument"]; + +/** A lifted `(op, value, argument)` function over checked-dynamic values. */ +function liftOperation(lowerer: Lowerer, name: string, body: IrStmt[], loc: SrcLoc): void { + lowerer.liftedFns.push({ + name, + params: PARAMS.map((p) => ({ localId: p, name: p, type: DYN })), + returnType: DYN, + locals: PARAMS.map((p) => ({ id: p, name: p, type: DYN, mutable: false })), + body, + loc, + }); +} + +function opIs(code: number, loc: SrcLoc): IrExpr { + return { + kind: "bin", + op: "===", + left: { kind: "dynCheck", value: varRef("op", DYN, loc), type: F64, loc }, + right: { kind: "numLit", value: code, type: F64, loc }, + type: BOOL, + loc, + }; +} + +/** Lifts `%date.classMethod(op, value, argument)` when the program declares + * a class that extends Date: ops 2 to 5 call `value`'s toISOString, + * toJSON(argument), toString and valueOf as checked-dynamic method calls, + * which the class dispatch resolves to each class's declarations and the + * runtime to the inherited Date methods. Called before the class dispatch + * runs over the lowered functions. */ +export function noteDateClassMethods(lowerer: Lowerer): void { + if (lowerer.liftedFnNamed(DATE_CLASS_METHOD)) return; + const classes = dateClasses(lowerer); + if (classes.length === 0) return; + const loc = classes[0]!.def.loc; + const value = varRef("value", DYN, loc); + const body: IrStmt[] = ["toISOString", "toJSON", "toString", "valueOf"].map( + (method, index): IrStmt => ({ + kind: "if", + cond: opIs(index + 2, loc), + then: [ + { + kind: "return", + value: { + kind: "dynInvoke", + recv: value, + method, + calleeName: method, + args: method === "toJSON" ? [varRef("argument", DYN, loc)] : [], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }), + ); + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_METHOD, body, loc); +} + +/** %main's installation of the functions the runtime calls for a + * checked-dynamic value that holds an instance of a class declared `extends + * Date` (scr_date_class_operation_install). `%date.classTime(op, value, + * argument)` answers the time value for op 0 (undefined for any other + * value), sets it from the number `argument` for op 1 and answers the + * class's name for op 6; `%date.classMethod` (noteDateClassMethods) calls + * its methods. */ +export function dateClassOperationPrelude(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const classes = dateClasses(lowerer); + if (classes.length === 0) return []; + noteDateClassMethods(lowerer); + const value = varRef("value", DYN, loc); + const argument = varRef("argument", DYN, loc); + const body: IrStmt[] = []; + for (const info of classes) { + const root = dateRootOf(info)!; + const type: IrType = { kind: "object", className: info.def.name }; + const instance: IrExpr = { kind: "dynCheck", value, type, loc }; + body.push({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [ + { + kind: "if", + cond: opIs(6, loc), + then: [ + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "strLit", + value: info.def.jsName ?? info.def.name, + type: STRING, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "if", + cond: opIs(1, loc), + then: [ + { + kind: "fieldSet", + obj: lowerer.upcastTo(instance, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: { + kind: "libCall", + fn: "date.newMs", + args: [{ kind: "dynCheck", value: argument, type: F64, loc }], + type: DATE_T, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, instance, root, loc)], + type: F64, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_TIME, body, loc); + const closure = (fnName: string): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName, + captures: [], + type: { kind: "func", params: [DYN, DYN, DYN], ret: DYN }, + loc, + }, + type: DYN, + loc, + }); + return [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "date.classOperation", + args: [closure(DATE_CLASS_TIME), closure(DATE_CLASS_METHOD)], + type: VOID, + loc, + }, + loc, + }, + ]; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 31be995e7..a598f4ee6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { programDeclaresDateClass } from "./date-classes.js"; import { BOOL, F64, @@ -9,6 +11,7 @@ import { type IrStmt, type IrType, NULL_T, + type SrcLoc, STRING, typeKey, } from "../../../ir/ir.js"; @@ -39,7 +42,7 @@ const DATE_TEXT_FNS: ReadonlyMap = new Map([ ["toTimeString", "date.toTimeString"], ]); -const DATE_METHOD_HINT = +export const DATE_METHOD_HINT = "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one @@ -52,7 +55,52 @@ export function lowerDateCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); - if (lowerer.stdlibGlobalMember(access, "Date") === "now") { + const member = lowerer.stdlibGlobalMember(access, "Date"); + if (member === "now" || member === "parse" || member === "UTC") + return lowerDateStaticCall(lowerer, call, member); + if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; + if (!lowerer.isStdlibMember(access)) return null; + const name = access.name.text; + // A Date a JavaScript file constructs is a mutable object; setTime on + // it takes the dynamic method call. So does every Date method on a + // checked-dynamic receiver when the program declares a class that extends + // Date: the value can be an instance, whose class may declare the method. + if ( + (name === "setTime" || programDeclaresDateClass(lowerer)) && + tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn" + ) + return null; + if (name === "toJSON") return lowerDateToJson(lowerer, call, access); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + } + const raw = lowerer.lowerExpr(access.expression); + const receiver: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } + : raw; + if (receiver.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const result = dateValueMethod(name, receiver, loc); + if (result) return result; + lowerer.noLowering( + `Date.prototype.${name}`, + call, + DATE_METHOD_HINT, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** Date.now(), Date.parse(text) and Date.UTC(...), also called through a + * class declared `extends Date`, which inherits them. Null for other + * names. */ +export function lowerDateStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, +): IrExpr | null { + const loc = locOf(call); + if (name === "now") { if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call); } @@ -60,7 +108,7 @@ export function lowerDateCall( } // Date.parse uses the same date-string parser as new Date(dateString) // (V8's grammar in the runtime), so their NaN and TimeClip behavior agree. - if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { + if (name === "parse") { if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); } @@ -72,7 +120,7 @@ export function lowerDateCall( // TimeClip. Omitted trailing arguments complete with the spec's // defaults (month 0, date 1, time parts 0); tsc pins every present // argument to number, so the seven-f64 ABI is exact. - if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") { + if (name === "UTC") { if ( call.arguments.nodes.length < 1 || call.arguments.nodes.length > 7 || @@ -92,49 +140,38 @@ export function lowerDateCall( } return { kind: "libCall", fn: "date.utc", args, type: F64, loc }; } - if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; - if (!lowerer.isStdlibMember(access)) return null; - const name = access.name.text; - if (name === "toJSON") return lowerDateToJson(lowerer, call, access); - if (call.arguments.nodes.length !== 0) { - lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); - } - const raw = lowerer.lowerExpr(access.expression); - const receiver: IrExpr = - raw.type.kind === "dyn" - ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } - : raw; - if (receiver.type.kind !== "date") - lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + return null; +} + +/** The Date methods without arguments that have a lowering, applied to the + * time value `value` (a `date`): getTime, valueOf, toISOString, + * toUTCString, the calendar getters and the text forms. Null for other + * names. */ +export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExpr | null { if (name === "getTime" || name === "valueOf") { return { kind: "libCall", fn: name === "getTime" ? "date.getTime" : "date.valueOf", - args: [receiver], + args: [value], type: F64, loc, }; } if (name === "toISOString") { - return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc }; } if (name === "toUTCString") { - return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toUTCString", args: [value], type: STRING, loc }; } const fn = own(DATE_GETTER_FNS, name); if (fn !== undefined) { - return { kind: "libCall", fn, args: [receiver], type: F64, loc }; + return { kind: "libCall", fn, args: [value], type: F64, loc }; } const textFn = DATE_TEXT_FNS.get(name); if (textFn !== undefined) { - return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc }; } - lowerer.noLowering( - `Date.prototype.${name}`, - call, - DATE_METHOD_HINT, - lowerer.checker.getSymbolAtLocation(access.name), - ); + return null; } /** `d.toJSON(key?)`: Node answers null for an invalid date and the diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index d95442829..1371769d6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { dynUndefinedExpr } from "../../../ir/build.js"; import { BOOL, CAUGHT, @@ -8,6 +10,7 @@ import { DYN_CLASS_PROPERTIES, F64, type IrExpr, + type IrStmt, type IrType, STRING, } from "../../../ir/ir.js"; @@ -267,6 +270,114 @@ function isErrorClassType(lowerer: Lowerer, type: IrType): boolean { ); } +/** Error classes that construct when called without `new`. */ +const CALLABLE_ERROR_CLASSES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + +/** `Error(message?, options?)` and `Error.call(thisArg, message?, options?)` + * (an ES5 constructor function's `Error.call(this)`), and the same for the + * other standard error classes: called as a function, the constructor + * returns a new error and ignores the receiver. `thisArg` is evaluated + * first. Null for other callees. */ +export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken) return null; + let callee = call.expression; + let args = call.arguments.nodes; + let thisArg: ts.Expression | undefined; + const viaCall = + ts.isPropertyAccessExpression(callee) && + !callee.questionDotToken && + callee.name.text === "call" && + ts.isIdentifier(callee.expression); + if (viaCall) { + callee = (callee as ts.PropertyAccessExpression).expression; + thisArg = args[0]; + args = args.slice(1); + } + if (!ts.isIdentifier(callee) || args.some(ts.isSpreadElement)) return null; + if (thisArg && ts.isSpreadElement(thisArg)) return null; + const info = lowerer.builtinErrorInfoOf(lowerer.checker.getSymbolAtLocation(callee)); + if (!info || !CALLABLE_ERROR_CLASSES.has(info.def.name)) return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + if (thisArg) { + const receiver = lowerer.lowerExpr(thisArg); + if (receiver.kind !== "varRef" && receiver.kind !== "unitLit") + stmts.push({ kind: "exprStmt", expr: receiver, loc: receiver.loc }); + } + const error: IrExpr = { + kind: "libCall", + fn: "error.newOptions", + args: lowerer.errorConstructorArgs(args, loc, call), + type: { kind: "object", className: info.def.name }, + loc, + }; + return stmts.length > 0 + ? { kind: "seqExpr", stmts, result: error, type: error.type, loc } + : error; +} + +/** `Error.captureStackTrace(target[, constructorOpt])` (also called as + * `Error.captureStackTrace?.(...)`, which always calls it). On a + * checked-dynamic target (`this` in a JavaScript constructor function) the + * runtime defines target's `stack`. An instance of a class that extends + * Error derives its stack from its name and message when the stack is + * read, so the call only evaluates its arguments. Both arguments are + * evaluated; the second only selects frames in Node. Null for other + * targets and callees. */ +export function lowerErrorCaptureStackTraceCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken) return null; + if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null; + const args = call.arguments.nodes; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + const probed = tryLowerExpression(lowerer, args[0]!); + const loc = locOf(call); + if ( + probed?.type.kind === "object" && + (probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error")) + ) { + const stmts: IrStmt[] = []; + for (const arg of args) { + const value = lowerer.lowerExpr(arg); + if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc }); + } + const result = dynUndefinedExpr(loc); + return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; + } + if (probed?.type.kind !== "dyn") return null; + const target = lowerer.lowerExpr(args[0]!); + const result: IrExpr = { + kind: "libCall", + fn: "dyn.errorCaptureStackTrace", + args: [target], + type: DYN, + loc, + }; + if (args.length === 1) return result; + const local = lowerer.declareHiddenLocal("%captureTarget", DYN); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: local.id, init: target, loc }, + { kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc }, + ], + result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] }, + type: DYN, + loc, + }; +} + /** `Error.isError(value)`. A catch binding tests its payload against the * Error class, an `unknown` value tests the checked-dynamic tree's error * encoding, and a union tests its runtime tag against the arms whose class diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index c90e1135b..fefe85d0c 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2,8 +2,17 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; +import { + DATE_PROTOTYPE_METHOD_NAMES, + DATE_TIME_FIELD, + dateInheritedMember, + dateRootOf, + programDeclaresDateClass, +} from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; import { BOOL, + DATE_T, DYN, F64, DYN_CLASS_PROPERTIES as PROPERTY_BAG, @@ -296,6 +305,32 @@ export function classInstanceOf( ); const body: IrStmt[] = prototype ? [ + // In a program that declares a class extending Date, a Date can + // meet a class test (`date instanceof Moment`): its prototype + // chain holds no class prototype. + ...(programDeclaresDateClass(lowerer) + ? [ + { + kind: "if" as const, + cond: { + kind: "libCall" as const, + fn: "dyn.nativeDateHandleIs" as const, + args: [varRef("value", DYN, loc)], + type: BOOL, + loc, + }, + then: [ + { + kind: "return" as const, + value: { kind: "boolLit" as const, value: false, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ] + : []), { kind: "varDecl", localId: "candidate", init: varRef("value", DYN, loc), loc }, { kind: "while", @@ -2871,6 +2906,7 @@ export class ClassDynamicDispatch { !isClassOwnEnumerableFieldName(dispatch.name) || (!(info.builtinStream !== undefined && streamProperty(dispatch.name)) && !eventTargetInheritedMember(info, dispatch.name) && + !dateInheritedMember(lowerer, info, dispatch.name) && !findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && @@ -2987,7 +3023,8 @@ export class ClassDynamicDispatch { ]; if ( ((info.builtinStream !== undefined && streamProperty(name)) || - eventTargetInheritedMember(info, name)) && + eventTargetInheritedMember(info, name) || + dateInheritedMember(lowerer, info, name)) && dispatch.probe ) return [ @@ -3200,6 +3237,9 @@ export class ClassDynamicDispatch { for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name); const inherited = eventTargetApiMembers(info); if (inherited) for (const name of inherited) names.add(name); + if (dateRootOf(info)) + for (const name of DATE_PROTOTYPE_METHOD_NAMES) + if (dateInheritedMember(lowerer, info, name)) names.add(name); for (let owner: ClassInfo | null = info; owner; owner = owner.base) { const methodNames = [...owner.methods.keys()]; @@ -3863,6 +3903,70 @@ export class ClassDynamicDispatch { ]; return { kind: "if", cond: condition, then: body, else_: null, loc }; } + // Date.prototype[Symbol.toPrimitive] takes "default" as "string", and a + // class declared `extends Date` inherits Date's toString and valueOf, + // which answer primitives from the time value. + const dateRoot = dateRootOf(info); + if (dateRoot) { + const dateOrder: ("valueOf" | "toString")[] = + hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"]; + const { receiver } = this.propertyReceiver(plan, loc); + const body: IrStmt[] = []; + for (const method of dateOrder) { + if (!conversionMethod(lowerer, info, method)) { + const time: IrExpr = { + kind: "fieldGet", + obj: upcastTo(lowerer, receiver, dateRoot.def.name), + className: dateRoot.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + body.push({ + kind: "return", + value: lowerer.coerceToExpected(dateValueMethod(method, time, loc)!, DYN), + loc, + }); + break; + } + const result = `%toPrimitive.${method}.${helper.locals.length}`; + helper.locals.push({ id: result, name: method, type: DYN, mutable: false }); + body.push( + { + kind: "varDecl", + localId: result, + init: { + kind: "call", + callee: this.toPrimitiveCall(lowerer, plan, method, 0, loc), + args: [varRef("p.0", DYN, loc)], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: isPrimitive(varRef(result, DYN, loc), loc), + then: [{ kind: "return", value: varRef(result, DYN, loc), loc }], + else_: null, + loc, + }, + ); + } + if (body.at(-1)?.kind !== "return") + body.push({ + kind: "return", + value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc), + loc, + }); + return { + kind: "if", + cond: this.propertyReceiver(plan, loc).condition, + then: body, + else_: null, + loc, + }; + } const order: ("valueOf" | "toString")[] = hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"]; if (!order.some((method) => conversionMethod(lowerer, info, method))) return null; @@ -4731,9 +4835,11 @@ export class ClassDynamicDispatch { ): IrStmt[] { const { name, write } = dispatch; const loc = dispatch.fn.loc; - // The inherited EventTarget/Event members of a subclass instance: the - // runtime's capsule view answers the accessors and method values. - if (eventTargetInheritedMember(info, name)) { + // The inherited EventTarget/Event members of a subclass instance, and + // the inherited Date methods of an instance of a class declared + // `extends Date`: the runtime's capsule view answers the accessors and + // method values. + if (eventTargetInheritedMember(info, name) || dateInheritedMember(lowerer, info, name)) { const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; if (!write) return [ diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index cc8700469..abbaf3a75 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -1,3 +1,4 @@ +import { isErrorPrototypeValue } from "./class-prototypes.js"; import * as ts from "../ts7/adapter.js"; import { literalValues } from "../literal-values.js"; import { isJsSourceFile } from "../program.js"; @@ -377,6 +378,8 @@ export function collectClassMethodMutations( node.initializer.kind !== ts.SyntaxKind.MissingDeclaration ) markTarget(node.initializer, node); + if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 4cbd8e5cf..2a05cf977 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -457,6 +457,28 @@ export function isCompiledPrototypeMember( ); } +/** `Error.prototype` read as the checked-dynamic object, as a value or for + * a member other than `toString` and `constructor`, which keep the Error + * class's prototype data (`this.toString !== Error.prototype.toString` + * compares class method values). */ +export function isErrorPrototypeValue( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + return ( + expr.name.text === "prototype" && + lowerer.isStdlibGlobal(expr.expression, "Error") && + !( + ts.isPropertyAccessExpression(parent) && + parent.expression === node && + (parent.name.text === "toString" || parent.name.text === "constructor") + ) + ); +} + /** Named data and method slots have native descriptors. Bare reflection and * accessor replacement still require a complete view of the prototype. */ export function lowerClassPrototypeData( @@ -469,6 +491,11 @@ export function lowerClassPrototypeData( exactClassOfReceiver(lowerer, expr.expression) ?? (stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined); if (!info) return null; + // Error.prototype as a value: the checked-dynamic object an ES5 + // constructor function inherits from (`F.prototype = + // Object.create(Error.prototype)`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; // EventEmitter.prototype and the stream prototypes: the untyped // constructor's prototype object (builtin-class-dyn.ts). const builtinKind = builtinClassKind(info.def.name); diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index f3a6b23fa..0b107ab4e 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -1,10 +1,21 @@ import { varRef } from "../../ir/build.js"; -import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + STRING, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isClassCallback } from "./class-callbacks.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; /** The ToPrimitive hint of a conversion: `String(x)` and template literals * take "string", `+` "default", `Number(x)` "number". */ @@ -13,9 +24,12 @@ export type StringConversionHint = "string" | "default"; type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; -/** How an instance of one class converts: through one of its methods, or - * through Object.prototype.toString ("[object Object]"). */ -type Plan = { method: ConversionMethod; ret: IrType } | { method: null }; +/** How an instance of one class converts: through one of its methods, + * through the Date.prototype method `date` of a class declared `extends + * Date`, or through Object.prototype.toString ("[object Object]"). */ +type Plan = + | { method: ConversionMethod; ret: IrType } + | { method: null; date?: "toString" | "valueOf" }; const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); @@ -138,6 +152,18 @@ function classConversion( } const convert = (cls: ClassInfo, object: IrExpr): IrExpr => { const plan = plans.get(cls)!; + if (plan.method === null && plan.date) { + const root = dateRootOf(cls)!; + const time: IrExpr = { + kind: "fieldGet", + obj: lowerer.upcastTo(object, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + return finish(dateValueMethod(plan.date, time, loc)!); + } if (plan.method === null) return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); const args: IrExpr[] = @@ -202,7 +228,10 @@ function depth(info: ClassInfo): number { } function samePlan(a: Plan, b: Plan): boolean { - return a.method === b.method; + return ( + a.method === b.method && + (a.method !== null || b.method !== null || a.date === (b as { date?: string }).date) + ); } /** A conversion method the class declares or inherits (a field of that @@ -227,11 +256,17 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t // OrdinaryToPrimitive: "string" tries toString first, which every object // has (Object.prototype.toString answers a string); "default" and // "number" try valueOf first, whose Object.prototype version answers the - // object itself. - const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"]; + // object itself. Date.prototype[Symbol.toPrimitive] takes "default" as + // "string", and Date.prototype answers both methods with primitives. + const date = dateRootOf(info) !== null; + const order: ConversionMethod[] = + hint === "string" || (date && hint === "default") ? ["toString"] : ["valueOf", "toString"]; for (const method of order) { const found = declared(lowerer, info, method); - if (!found) continue; + if (!found) { + if (date && method !== "sym:toPrimitive") return { method: null, date: method }; + continue; + } if (!primitive(lowerer, found.ret)) refuse(lowerer, node, info, `a '${method}' method that can return an object`); return { method, ret: found.ret }; diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695..ead313a78 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -902,10 +902,15 @@ export function lowerCompoundValueToTarget( // operator on both values, then the checked conversion into the // target's declared type (a string result for a number target throws the // catchable TypeError, as the expanded assignment does). + // A union operand (`fraction += "0"` on a binding the program assigns + // both a number and a string) takes the same runtime operator. if ( - (target.type.kind === "dyn" || rhs.type.kind === "dyn") && - (isJsSourceFile(ts.getSourceFileOfNode(expr)) || - (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type))) + ((target.type.kind === "dyn" || rhs.type.kind === "dyn") && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || + (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))) || + ((target.type.kind === "union" || rhs.type.kind === "union") && + lowerer.dynConvertible(read.type) && + lowerer.dynConvertible(rhs.type)) ) { const left = lowerer.coerceInto(expr.left, read, DYN); const right = lowerer.coerceInto(expr.right, rhs, DYN); @@ -1276,6 +1281,48 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } + // `a[k] = b[k] = v` in a JavaScript file: a member write in value + // position lowers as its statement form and yields the value. The + // receiver, the key and the value are evaluated in that order into + // temporaries first. A receiver held in a temporary must be a + // checked-dynamic value, which the temporary shares; any other receiver + // must be a variable or `this`. + if ( + op === ts.SyntaxKind.EqualsToken && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) && + !expr.left.questionDotToken + ) { + const target = expr.left; + const receiverNode = target.expression; + const bareReceiver = + ts.isIdentifier(receiverNode) || receiverNode.kind === ts.SyntaxKind.ThisKeyword; + if (bareReceiver || tryLowerExpression(lowerer, receiverNode)?.type.kind === "dyn") { + const stmts: IrStmt[] = []; + const overrides: { node: ts.Expression; value: IrExpr }[] = []; + const hold = (node: ts.Expression, name: string): IrExpr => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + const held = varRef(local.id, value.type, value.loc); + overrides.push({ node, value: held }); + return held; + }; + if (!bareReceiver) hold(receiverNode, "%assignReceiver"); + if ( + ts.isElementAccessExpression(target) && + !ts.isIdentifier(target.argumentExpression) && + !ts.isStringLiteral(target.argumentExpression) && + !ts.isNumericLiteral(target.argumentExpression) + ) + hold(target.argumentExpression, "%assignKey"); + const result = hold(expr.right, "%assignValue"); + stmts.push( + lowerer.withExpressionOverrides(overrides, () => lowerer.lowerExprStatement(expr)), + ); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; + } + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 74f0be955..3ca1471ae 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -616,9 +616,25 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); + const rawIndex = lowerer.lowerExpr(expr.argumentExpression); + // A checked-dynamic index (an untyped JavaScript parameter, a parsed + // value) reads the property its key names, as JavaScript does: a code + // unit for an integer index, the length for "length", else undefined. + // The checker's string type does not describe those results, so the + // value stays checked-dynamic and consumers convert it. + if (rawIndex.type.kind === "dyn" && lowerer.dynConvertible(recv.type)) { + const key = lowerRecordPropertyKey(lowerer, rawIndex, expr.argumentExpression); + return { + kind: "dynKeyGet", + value: lowerer.coerceInto(expr.expression, recv, DYN), + key, + type: DYN, + loc: locOf(expr), + }; + } const index = lowerOptionalNumber( lowerer, - lowerer.lowerExpr(expr.argumentExpression), + rawIndex, locOf(expr.argumentExpression), expr.argumentExpression, ); diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8..e4f7d8675 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -797,6 +797,7 @@ export function lowerObjectLiteral( ): IrExpr { // Unannotated JavaScript accumulator fields have no element contract. // Preserve their checked arrays instead of assigning never[] a numeric ABI. + // An empty object field is a map the program adds keys to. if ( !expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && @@ -805,6 +806,8 @@ export function lowerObjectLiteral( ts.isPropertyAssignment(prop) && ((ts.isArrayLiteralExpression(prop.initializer) && prop.initializer.elements.nodes.length === 0) || + (ts.isObjectLiteralExpression(prop.initializer) && + prop.initializer.properties.nodes.length === 0) || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))), ) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da0..895ff5f45 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -5,7 +5,11 @@ import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; import { isThisParameter } from "../call-signatures.js"; -import { localPrivateBrandClass } from "../private-brands.js"; +import { + checkedPrivateValue, + localPrivateBrandClass, + privateNameClass, +} from "../private-brands.js"; import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; import { ARRAY_METHODS, @@ -90,7 +94,7 @@ import { runtimeConstructorReceiver, } from "../builtins/runtime-constructor.js"; import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js"; -import { lowerClassPrototypeData } from "../class-prototypes.js"; +import { isErrorPrototypeValue, lowerClassPrototypeData } from "../class-prototypes.js"; import { lowerEnumAccess } from "../lower-enums.js"; import { hasOptionalChainGuard, @@ -281,6 +285,24 @@ export function lowerPropertyAccessExpr( expr: ts.PropertyAccessExpression, loc: SrcLoc, ): IrExpr { + // `value.#x` on a checked-dynamic value, or on a checked extraction of + // one: the field of the brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(expr.name)) { + const owner = privateNameClass(lowerer, expr.name); + const field = owner?.fields.get(expr.name.text); + const probed = field ? tryLowerExpression(lowerer, expr.expression) : null; + if (owner && field && probed && checkedDynamicValue(probed)) { + const value = checkedDynamicValue(lowerer.lowerExpr(expr.expression))!; + return { + kind: "fieldGet", + obj: checkedPrivateValue(lowerer, owner, expr, value, loc), + className: owner.def.name, + field: expr.name.text, + type: field, + loc, + }; + } + } if (!expr.questionDotToken) { const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; @@ -1664,6 +1686,19 @@ export function lowerIntrinsicProperty( return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) }; if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype") return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) }; + // Error.prototype as a value (`F.prototype = Object.create(Error.prototype)`) + // and Error.captureStackTrace read as a function (`if + // (Error.captureStackTrace) ...`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; + if (lowerer.stdlibGlobalMember(expr, "Error") === "captureStackTrace") + return { + kind: "libCall", + fn: "dyn.errorCaptureStackTraceFunction", + args: [], + type: DYN, + loc: locOf(expr), + }; // Function.prototype as a VALUE (a prototype-chain link: // `Object.setPrototypeOf(F.prototype, Function.prototype)`): one // anonymous function answering undefined, length 0 — a zero-capture @@ -3533,6 +3568,13 @@ export function classFieldTarget( * known class instance OR a record, and the member is a field or (class * receivers) a declared accessor property. Returns the pieces of a * fieldSet/recordSet/accessor-call (minus value/kind) or null. */ +/** The checked-dynamic value `expr` reads: itself, or the value a checked + * extraction validates. Null for other expressions. */ +function checkedDynamicValue(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} + export function fieldTarget( lowerer: Lowerer, access: ts.PropertyAccessExpression, @@ -3569,6 +3611,30 @@ export function fieldTarget( return classFieldTarget(lowerer, access.expression, ownerType, access.name.text); } } + // `value.#x = v` on a checked-dynamic value: the field of the + // brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(access.name)) { + const owner = privateNameClass(lowerer, access.name); + const probed = owner?.fields.has(access.name.text) + ? tryLowerExpression(lowerer, access.expression) + : null; + if (owner && probed && checkedDynamicValue(probed)) { + const receiver = checkedPrivateValue( + lowerer, + owner, + access, + checkedDynamicValue(lowerer.lowerExpr(access.expression))!, + locOf(access), + ); + return classFieldTarget( + lowerer, + access.expression, + { kind: "object", className: owner.def.name }, + access.name.text, + receiver, + ); + } + } const stored = ts.isIdentifier(access.expression) ? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type) : undefined; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cff..37478be0d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,5 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { dateRootOf } from "../builtins/date-classes.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -265,7 +266,12 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc stmts: [{ kind: "exprStmt", expr: value, loc }], result: { kind: "boolLit", - value: value.type.kind === (date ? "date" : "url"), + value: + value.type.kind === (date ? "date" : "url") || + // An instance of a class declared `extends Date`. + (date && + value.type.kind === "object" && + dateRootOf(lowerer.classes.get(value.type.className)) !== null), type: BOOL, loc, }, @@ -565,7 +571,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; }; - const test = caughtInstanceOf(target); + let test = caughtInstanceOf(target); + // A thrown checked-dynamic object whose prototype chain reaches + // Error.prototype (an ES5 constructor function inheriting from it). + if (target.def.name === "%Error" && lowerer.errorPrototypeValues) { + const value = varRef(caughtLocal.id, CAUGHT, loc); + test = { + kind: "logical", + op: "||", + left: test, + right: { + kind: "logical", + op: "&&", + left: { kind: "caughtTest", value, test: "dynamic", type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [{ kind: "caughtToDyn", value, type: DYN, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } return duplex ? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc } : test; @@ -590,9 +622,35 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ) { return classInstanceOf(lowerer, left, target, loc); } - if (left.type.kind === "dyn" && target.def.name === "%Error") { + if (left.type.kind === "dyn" && target.def.name === "%Error" && !lowerer.errorPrototypeValues) { return { kind: "dynTest", test: "error", value: left, type: BOOL, loc }; } + // An object whose prototype chain reaches Error.prototype (an ES5 + // constructor function inheriting from it) is an instance as well. + if (left.type.kind === "dyn" && target.def.name === "%Error") { + const local = lowerer.declareHiddenLocal("%instanceofValue", DYN); + const value = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }], + result: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "error", value, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [value], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } // `u instanceof TypeError` (and the other BUILTIN error classes) on a // dyn value: the from_error cache holds the checked-dynamic tree↔error identity edge, // so the runtime resolves the encoding back to its error and asks the diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 13baca7d2..128093dad 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -23,6 +23,7 @@ import { type IrExpr, type IrStmt, type IrType, + isUnitType, STRING, type SrcLoc, UNDEFINED_T, @@ -100,6 +101,27 @@ function writeFree(expr: IrExpr): boolean { } } +/** A union unary `+`/`-` converts with the runtime's ToNumber: one with an + * arm other than numbers and units (`string | undefined`). Number and unit + * unions keep their native lowering. */ +function coercedUnion(lowerer: Lowerer, type: IrType): boolean { + if (type.kind !== "union" || !lowerer.dynConvertible(type)) return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm)); +} + +/** ToNumber of a boolean: 1 for true, 0 for false. */ +function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; +} + export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpression): IrExpr { const loc = locOf(expr); switch (expr.operator) { @@ -111,9 +133,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio } // Match the checked numeric boundary used by JS binary arithmetic. const operand = - raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) - ? { kind: "dynCheck" as const, value: raw, type: F64, loc } - : lowerOptionalNumber(lowerer, raw, loc, expr.operand); + raw.type.kind === "bool" + ? booleanToNumber(raw, loc) + : coercedUnion(lowerer, raw.type) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], + type: F64, + loc, + } satisfies IrExpr) + : raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [raw], + type: F64, + loc, + } satisfies IrExpr) + : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc }; return { kind: "unary", op: "-", operand, type: F64, loc }; @@ -123,6 +161,12 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [raw], type: F64, loc }; } + // A union of primitives (`+match[1]` over `string | undefined`): ToNumber + // over the boxed value, as for a checked-dynamic operand. + if (coercedUnion(lowerer, raw.type)) { + const boxed: IrExpr = { kind: "dynFrom", value: raw, type: DYN, loc }; + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [boxed], type: F64, loc }; + } // Unary + is ToNumber; on an already-number operand it's identity, // and a STRING operand runs the runtime's ECMA-exact StringToNumber // (num.fromString — Number(aString)'s lowering, scr_string.c). @@ -130,6 +174,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "string") { return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } + if (raw.type.kind === "bool") return booleanToNumber(raw, loc); const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); @@ -321,9 +366,13 @@ export function lowerIncDec( !isJsSourceFile(ts.getSourceFileOfNode(expr)) && raw.type.kind === "dyn" && lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64"; + // A private name is the field of its class (fieldTarget). + const privateName = + ts.isPropertyAccessExpression(access) && ts.isPrivateIdentifier(access.name); if ( (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) && - raw.type.kind === "dyn" + raw.type.kind === "dyn" && + !privateName ) { const receiver = save(raw, "%incrementReceiver"); const key = save( @@ -814,7 +863,8 @@ export function lowerFieldCompoundValue( probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined; if (probed && staticClass && hasRuntimeStatics(staticClass)) probed = lowerer.coerceToExpected(probed, DYN); - if (probed?.type.kind === "dyn") { + // A private name is the field of its class (fieldTarget). + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(access.name)) { const receiver = save(probed, "%compoundReceiver"); const key: IrExpr = { kind: "strLit", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..21b974e31 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -141,7 +141,7 @@ import { lowerCryptoHashMethodCall, lowerPromisifiedCryptoCall, } from "./builtins/crypto.js"; -import { lowerErrorIsErrorCall } from "./builtins/errors.js"; +import { lowerErrorCaptureStackTraceCall, lowerErrorIsErrorCall } from "./builtins/errors.js"; import { lowerPromisifiedDnsCall } from "./lower-dgram.js"; import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js"; import { @@ -199,6 +199,11 @@ import { import { classCallbackCall, isClassCallback } from "./class-callbacks.js"; import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js"; import { eventTargetApiMethod, lowerEventTargetMethodCall } from "./lower-event-target.js"; +import { + dateRootOf, + isDatePrototypeMethod, + lowerInheritedDateMethodCall, +} from "./builtins/date-classes.js"; import { lowerConsoleInstanceCall, lowerConsoleMethodCall, @@ -1554,6 +1559,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerFilterNarrowCall(expr, expr.expression) ?? lowerArrayIsArrayCall(lowerer, expr, expr.expression) ?? lowerErrorIsErrorCall(lowerer, expr, expr.expression) ?? + lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression) ?? lowerBigIntMethodCall(lowerer, expr, expr.expression) ?? lowerSymbolStaticCall(lowerer, expr, expr.expression) ?? lowerSymbolMethodCall(lowerer, expr, expr.expression) ?? @@ -3727,6 +3733,18 @@ function lowerDynReceiverMethodCall( // IS Node's semantics for every possible dyn value. Shared names would // need a receiver-kind dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { + // An object-typed receiver the member is missing from (a JavaScript + // module object whose members are assigned in a function body, + // `common.repeat(" ", n)`) holds a string or an object with that + // member: dispatch on the receiver's runtime kind. + if ( + !checkerUntyped && + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + !call.questionDotToken && + !access.questionDotToken && + !call.arguments.nodes.some(ts.isSpreadElement) + ) + return lowerStringOrMemberCall(lowerer, call, access, recv); const checked = (): IrExpr => dynStringReceiver(lowerer, recv, access); return ( lowerRegexMethodCall(lowerer, call, access, checked) ?? @@ -3935,6 +3953,98 @@ function lowerDynReceiverMethodCall( }; } +/** `value.name(...args)` on a dyn receiver that may be a string or an + * object: a string runs the String.prototype lowering, any other value + * calls its member. The member is read before the arguments are + * evaluated, and the arguments are evaluated once for either branch. */ +function lowerStringOrMemberCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + recv: IrExpr, +): IrExpr { + const loc = locOf(call); + const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN); + const receiver = varRef(receiverLocal.id, DYN, loc); + const isString: IrExpr = { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc }; + const calleeLocal = lowerer.declareHiddenLocal("%dynCallCallee", DYN); + const callee = varRef(calleeLocal.id, DYN, loc); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: receiverLocal.id, init: recv, loc }, + { + kind: "varDecl", + localId: calleeLocal.id, + init: { + kind: "ternary", + cond: isString, + then: dynUndefinedExpr(loc), + else_: { + kind: "dynKeyGet", + key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) }, + value: receiver, + type: DYN, + loc: locOf(access), + }, + type: DYN, + loc, + }, + loc, + }, + ]; + const args = call.arguments.nodes.map((node) => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal("%dynCallArg", value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return { node, value: varRef(local.id, value.type, value.loc) }; + }); + // String.prototype methods ignore arguments past the ones they read. + const arity = DYN_STRING_ONLY_ARITY[access.name.text] ?? call.arguments.nodes.length; + const stringReceiver = (): IrExpr => ({ kind: "dynCheck", value: receiver, type: STRING, loc }); + const lowerString = (): IrExpr | null => + (call.arguments.nodes.length <= arity + ? lowerRegexMethodCall(lowerer, call, access, stringReceiver) + : null) ?? + lowerStringMethodCall( + lowerer, + call, + access, + stringReceiver, + call.arguments.nodes.slice(0, arity), + ); + const stringCall = lowerer.withExpressionOverrides(args, lowerString); + const memberCall: IrExpr = { + kind: "dynCall", + callee, + receiver, + calleeName: ts.getTextOfNode(access), + args: args.map((arg) => lowerer.coerceToExpected(arg.value, DYN)), + type: DYN, + loc, + }; + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: isString, + then: stringCall + ? lowerer.coerceToExpected(stringCall, DYN) + : nodeThrowExpr( + 0, + "SC1090", + `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${ts.getSourceFileOfNode(call).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(call), ts.tokenStartOfNode(call)).line + 1}]`, + DYN, + loc, + ), + else_: memberCall, + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Prototype method names of the checked-dynamic tree-representable kinds (String, Array, * Object, Function, Number prototypes): a dyn receiver call on one of * these could be a REAL method on a real value, which a stored-member @@ -4372,6 +4482,29 @@ function isStringMethodName(name: string): boolean { ); } +/** How many arguments each DYN_STRING_ONLY_METHODS method reads. */ +const DYN_STRING_ONLY_ARITY: Readonly> = { + charCodeAt: 1, + charAt: 1, + startsWith: 2, + endsWith: 2, + substring: 2, + repeat: 1, + trim: 0, + trimStart: 0, + trimEnd: 0, + split: 2, + padStart: 2, + padEnd: 2, + toLowerCase: 0, + toUpperCase: 0, + replace: 2, + replaceAll: 2, + match: 1, + matchAll: 1, + search: 1, +}; + /** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any * other dyn kind's prototype) also declares. */ const DYN_STRING_ONLY_METHODS = new Set([ @@ -9670,6 +9803,10 @@ export function lowerObjectMethodCall( if (info && !found && eventTargetApiMethod(info, access.name.text)) { return lowerEventTargetMethodCall(lowerer, call, access, info, lowerReceiver()); } + // The Date methods a class declared `extends Date` does not declare. + if (info && !found && dateRootOf(info) && isDatePrototypeMethod(access.name.text)) { + return lowerInheritedDateMethodCall(lowerer, call, access.name.text, info, lowerReceiver()); + } // GENERIC methods (own type parameters) never enter the methods table: // they monomorphize per call site and dispatch statically // (lowerClassGenericMethodCall has the exactness rules). @@ -10458,6 +10595,17 @@ export function lowerStaticNumberGlobal( : lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc }; } + // parseFloat takes one argument; Node ignores the rest after evaluating + // them (`parseFloat(text, 10)`). Literal and identifier extras have + // nothing to evaluate. + if ( + name === "parseFloat" && + args.length > 1 && + args + .slice(1) + .every((arg) => ts.isNumericLiteral(arg) || ts.isStringLiteral(arg) || ts.isIdentifier(arg)) + ) + return lowerStaticNumberGlobal(lowerer, name, call, args.slice(0, 1), loc); // STATIC parseFloat over string arguments — parseInt's sibling // (num.parseFloat is ECMA 19.2.4's decimal-literal prefix parse in // scr_string.c). Other argument types retain their native coercion diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988..9976a37e4 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,11 @@ +import { + DATE_TIME_FIELD, + dateRootOf, + dateSuperCall, + isDatePrototypeMethod, + lowerDateSuperMethodCall, +} from "./builtins/date-classes.js"; +import { lowerDateStaticCall } from "./builtins/dates.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -564,6 +572,11 @@ export interface ClassInfo { * lower-event-target.ts over any class rooted here. Subclass structs * embed the runtime prefix. */ builtinEventTarget?: "target" | "event"; + /** A class declared `extends Date` (builtins/date-classes.ts): its first + * field is the hidden `%time` value, super() sets it with the Date + * constructor's rules, and the Date methods it does not declare lower + * over that value. Subclasses inherit the field. */ + extendsDate?: true; /** The synthetic LifetimeRegion class (@scriptc/experimental, * lifetime-regions.ts): its constructor is synthesized with the class, * and its methods lower at their call sites. */ @@ -1951,6 +1964,7 @@ export function collectClassShapeInner( : (computedBase?.classInfo ?? null); let genericBase: ts.ExpressionWithTypeArguments | undefined; let callableBase: ClassInfo["callableBase"]; + let extendsDate = false; let factoryBaseExpression: ts.Expression | undefined; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; @@ -2151,6 +2165,11 @@ export function collectClassShapeInner( if (t && adoptCallableBase(t.expression)) continue; lowerer.unsupported("SC1090", clause, "extending computed expressions"); } + if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) { + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); + extendsDate = true; + continue; + } const symbol = lowerer.resolveValueSymbol(t.expression); // Extending a REBINDABLE decorated class (analysis already ran — // this collection is a class expression or a generic @@ -4350,7 +4369,8 @@ export function collectClassShapeInner( const ctorParams: ParamShape[] = ctor && !mixinForwarding ? lowerer.paramShapes(ctor.parameters.nodes) - : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); + : (base?.ctorParams ?? + (callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && !mixinForwarding && @@ -4388,6 +4408,7 @@ export function collectClassShapeInner( // Redeclared INHERITED fields contribute no slot (their // initializers assign the prefix slot). fields: [ + ...(extendsDate ? [{ name: DATE_TIME_FIELD, type: DATE_T }] : []), ...(base?.def.fields ?? []), ...fieldOrder .filter((f) => f.redeclared !== true) @@ -4419,6 +4440,7 @@ export function collectClassShapeInner( base, ...(genericBase ? { genericBase } : {}), ...(callableBase ? { callableBase } : {}), + ...(extendsDate ? { extendsDate: true as const } : {}), ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, @@ -5647,6 +5669,15 @@ export function lowerStaticFieldRead( if (info.classDecorators?.valueGlobalId !== undefined) return null; const loc = locOf(expr); const found = findStaticOn(lowerer, info, expr.name.text); + // A Date static read as a value through a class declared `extends Date`: + // the statics lower only as calls. + if ( + !found && + dateRootOf(info) && + ["now", "parse", "UTC"].includes(expr.name.text) && + !(ts.isCallExpression(expr.parent) && expr.parent.expression === expr) + ) + lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", @@ -6422,6 +6453,12 @@ function staticCallOn( const loc = locOf(call); const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue); if (runtimeStatic) return runtimeStatic; + // Date's statics through a class declared `extends Date` + // (`Moment.UTC(...)`), which inherits them. + if (dateRootOf(info) && !findStaticOn(lowerer, info, access.name.text)) { + const date = lowerDateStaticCall(lowerer, call, access.name.text); + if (date) return date; + } if ( (hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) || (info.callableBase && @@ -7683,7 +7720,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { lowerer.ctx.argumentsLocal = local; } body.push(...declared.prologue); - if (!ctorBase && !info.callableBase) { + if (!ctorBase && !info.callableBase && !info.extendsDate) { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a @@ -7695,7 +7732,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } } else { - if (ctorBase || info.callableBase) { + if (ctorBase || info.callableBase || info.extendsDate) { // Synthetic forwarding params (the inherited ABI signature). // Nothing references them by symbol — only the super call below, // which forwards the already-completed values UNCHANGED (defaults @@ -8614,12 +8651,14 @@ export function lowerDerivedCtorBody( loc, }, ]; - if (info.callableBase) { + if (info.callableBase || info.extendsDate) { if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, - "spreading explicit super arguments into an ordinary constructor", + info.extendsDate + ? "spreading super arguments into the Date constructor" + : "spreading explicit super arguments into an ordinary constructor", ); const pack: IrExpr = { kind: "dynArrLit", @@ -8804,6 +8843,7 @@ export function superCallStmt( type: { kind: "object", className: info.def.name }, loc, }; + if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc); if (info.callableBase) return { kind: "exprStmt", @@ -8925,6 +8965,24 @@ export function lowerSuperMethodCall( access: ts.PropertyAccessExpression, ): IrExpr { const cls = lowerer.currentClass; + // `super.toISOString()` in a class declared `extends Date`. + if (cls?.extendsDate && !cls.base) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + const date = lowerDateSuperMethodCall(lowerer, call, access.name.text, cls, thisRef); + if (date) return date; + lowerer.unsupported( + "SC1090", + access, + `'super.${access.name.text}' (Date.prototype does not declare it)`, + ); + } if (!cls?.base) { // tsc rejects super outside derived-class bodies first; defensive. lowerer.unsupported("SC1090", access, "'super' outside a derived class"); @@ -8969,6 +9027,17 @@ export function lowerSuperMethodCall( // `super.dispatchEvent(e)`, `super.preventDefault()`: the inherited // runtime members over this method's own `this` (subclasses cannot // override them, so the static answer is the runtime's). + if (dateRootOf(cls.base) && isDatePrototypeMethod(access.name.text)) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + return lowerDateSuperMethodCall(lowerer, call, access.name.text, cls.base, thisRef)!; + } if (eventTargetApiMethod(cls.base, access.name.text)) { const thisL = lowerer.resolveThis(); if (!thisL) lowerer.unsupported("SC1080", access); diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7e..6f90b198c 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -45,6 +45,7 @@ import type { ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { symbolFieldDisplayName } from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; +import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js"; import { recordMayKeepSource, recordSourceTest } from "./record-sources.js"; @@ -804,8 +805,9 @@ function inspectSupport( const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; // EventTarget and Event subclasses render through Node's inspectors - // for those classes (the runtime's capsule view). - if (eventTargetRootOf(info) !== null) return null; + // for those classes (the runtime's capsule view), and so do classes + // declared `extends Date`. + if (eventTargetRootOf(info) !== null || dateRootOf(info) !== null) return null; if (isErrorClass(lowerer, t.className)) { if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info)) return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet"; @@ -957,7 +959,10 @@ export function inspectExpr( loc, }; case "object": - if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) { + if ( + eventTargetRootOf(lowerer.classes.get(t.className)) !== null || + dateRootOf(lowerer.classes.get(t.className)) !== null + ) { return { kind: "libCall", fn: "insp.dyn", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44..e12d90743 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -148,6 +148,7 @@ import { forkTargetPaths } from "../fork-target.js"; import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; +import { dateClassOperationPrelude } from "./builtins/date-classes.js"; import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; @@ -3376,6 +3377,7 @@ export function buildMain(lowerer: Lowerer): IrFunction { ...lowerer.mainPrelude, ...cjsModuleRegistryPrelude(lowerer, loc), ...handleEventReaderPrelude(lowerer, loc), + ...dateClassOperationPrelude(lowerer, loc), ]; const roots = [lowerer.entry, ...lowerer.forkTargets, ...lowerer.workerTargets]; const isAsync = roots.some((root) => lowerer.asyncInitFiles.has(root)); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c..f1bf7c041 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6110,6 +6110,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar ) ) return true; + // A write that can store null or undefined into a binding the checker + // types as a primitive (`offset = match[3] || null`): JavaScript files + // are checked without strict null checks, so the declared type does not + // admit them. + const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if ( + (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") && + candidates.some( + (node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right), + ) + ) + return true; // A plain JavaScript write of a value the declared type cannot hold (the // checker does not report it): buffer-crc32 replaces its number[] table // with `new Int32Array(table)`. Only a different representation counts; @@ -6140,6 +6152,31 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar }); } +/** Whether `node` can evaluate to null or undefined by its form: the + * literals, `void`, and conditional or logical expressions with such an + * operand in result position. */ +function mayBeNullish(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (node.kind === ts.SyntaxKind.NullKeyword || ts.isVoidExpression(node)) return true; + if (ts.isIdentifier(node)) return node.text === "undefined"; + if (ts.isConditionalExpression(node)) + return mayBeNullish(node.whenTrue) || mayBeNullish(node.whenFalse); + if (ts.isBinaryExpression(node)) { + const op = node.operatorToken.kind; + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) + return ( + mayBeNullish(node.right) || + (op === ts.SyntaxKind.AmpersandAmpersandToken && mayBeNullish(node.left)) + ); + if (op === ts.SyntaxKind.CommaToken) return mayBeNullish(node.right); + } + return false; +} + function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { const guard = immediateAbsenceGuard(lowerer, decl); // `if (!x)`: the falsy test sees a missing value. @@ -7560,8 +7597,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // (closures box), the runtime sets the member on OBJ receivers // and throws Node's TypeErrors on the rest. The receiver is // PROBED (tryLowerExpression) and claimed exactly when it lowers to - // dyn; typed receivers keep their own lowerings and fences. - if (!expr.left.questionDotToken) { + // dyn; typed receivers keep their own lowerings and fences. A + // private name is the field of its class (fieldTarget). + if (!expr.left.questionDotToken && !ts.isPrivateIdentifier(expr.left.name)) { const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") { const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..e7ea87867 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -497,7 +497,8 @@ import { lowerTracingChannelProperty, } from "./builtins/async-context.js"; import { lowerDisposableStackNew } from "./builtins/disposable-stack.js"; -import { lowerErrorCodeProperty } from "./builtins/errors.js"; +import { lowerErrorCodeProperty, lowerErrorFunctionCall } from "./builtins/errors.js"; +import { noteDateClassMethods } from "./builtins/date-classes.js"; import { lowerProcessProperty, lowerProcessMethodCall, @@ -2944,6 +2945,10 @@ export class Lowerer { /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Whether the program reads `Error.prototype`, the checked-dynamic + * object an ES5 constructor function can inherit from: only then can a + * value that is not an Error instance be `instanceof Error`. */ + errorPrototypeValues = false; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ @@ -8123,6 +8128,7 @@ export class Lowerer { // turn queue more instances. Continue to the joint fixpoint; the initial // queue above is the only portion whose discovery order differed from // historical emit order. + noteDateClassMethods(this); const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined); // Units in declaration order, so the passes below meet functions, and the // classes they collect, in an order that does not depend on lowering order. @@ -12758,6 +12764,25 @@ export class Lowerer { ); } + /** withExpressionOverride for several nodes at once. */ + withExpressionOverrides( + overrides: readonly { node: ts.Expression; value: IrExpr }[], + lower: () => T, + ): T { + const previous = overrides.map((override) => this.chainRecvByNode.get(override.node)); + for (const override of overrides) this.chainRecvByNode.set(override.node, override.value); + try { + return lower(); + } finally { + for (let i = overrides.length - 1; i >= 0; i--) { + const node = overrides[i]!.node; + const value = previous[i]; + if (value) this.chainRecvByNode.set(node, value); + else this.chainRecvByNode.delete(node); + } + } + } + withExpressionOverride(node: ts.Expression, value: IrExpr, lower: () => T): T { const previous = this.chainRecvByNode.get(node); this.chainRecvByNode.set(node, value); @@ -12777,6 +12802,7 @@ export class Lowerer { if (webAssembly !== null) return webAssembly; refuseWebAssemblyUse(this, expr.expression); return ( + lowerErrorFunctionCall(this, expr) ?? lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? lowerThreadSlotsCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index 8507cc81a..c7881bce1 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -160,6 +160,60 @@ function brandCall( return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc }; } +/** The class whose body declares the private name `priv` (the nearest + * enclosing one, as JavaScript scoping resolves it), when it is a + * declaration-level class, or null. */ +export function privateNameClass(lowerer: Lowerer, priv: ts.PrivateIdentifier): ClassInfo | null { + for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) { + if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue; + const declares = node.members.nodes.some((member) => { + const name = (member as { name?: ts.PropertyName }).name; + return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text; + }); + if (!declares) continue; + const symbol = node.name ? lowerer.checker.getSymbolAtLocation(node.name) : undefined; + const info = + lowerer.currentClass?.decl === node + ? lowerer.currentClass + : symbol + ? lowerer.classBySymbol.get(symbol) + : undefined; + if (!info || info.localClass || info.generic || info.genericInstance) return null; + return info; + } + return null; +} + +/** `value.#x` on a checked-dynamic `value` in a declaration-level class + * `info`: the receiver as an instance of `info` when the value is one (an + * instance of the class or a subclass), else Node's TypeError for a missing + * private member (also for undefined and null). */ +export function checkedPrivateValue( + lowerer: Lowerer, + info: ClassInfo, + access: ts.PropertyAccessExpression, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read"; + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}${use}:${className}:${access.name.text}`; + if (!lowerer.valueHelpers.has(helper)) { + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: DYN }], + returnType: resultType, + locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }], + // finalizePrivateBrands fills the body. + body: [], + loc: helperLoc(loc), + }); + lowerer.valueHelpers.set(helper, helper); + } + return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; +} + /** A plain assignment, a destructuring target, or a for-in/of target writes * the field. Compound assignments and updates read it first, and Node * reports the failed read. */ @@ -203,7 +257,9 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const name = spec.slice(spec.lastIndexOf(":") + 1); const loc = fn.loc; const value = varRef("value.0", DYN, loc); - const evaluation = varRef("class.0", { kind: "classval", className }, loc); + // A declaration-level class has one brand: the helper takes no class. + const evaluation = + fn.params.length > 1 ? varRef("class.0", { kind: "classval", className }, loc) : null; const ownerType: IrType = { kind: "object", className }; const body: IrStmt[] = []; let matches = 0; @@ -218,21 +274,23 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const local = `receiver.${matches++}`; fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false }); const receiver = varRef(local, ownerType, loc); - const sameEvaluation: IrExpr = { - kind: "bin", - op: "===", - left: { - kind: "fieldGet", - obj: receiver, - className, - field: `%classEnvironment:${className}`, - type: evaluation.type, - loc, - }, - right: evaluation, - type: BOOL, - loc, - }; + const sameEvaluation: IrExpr = evaluation + ? { + kind: "bin", + op: "===", + left: { + kind: "fieldGet", + obj: receiver, + className, + field: `%classEnvironment:${className}`, + type: evaluation.type, + loc, + }, + right: evaluation, + type: BOOL, + loc, + } + : { kind: "boolLit", value: true, type: BOOL, loc }; const match = (typed: IrExpr): IrStmt[] => [ { kind: "varDecl", localId: local, init: typed, loc }, { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c77938337..adc11e35a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2262,6 +2262,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "date.newLocal", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c52118..b899e5277 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -534,6 +534,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", "date.checkedValue", "util.parseArgs", "util.parseEnv", @@ -601,6 +602,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreateWithProperties", "dyn.getPrototype", "dyn.setPrototype", + // Error.captureStackTrace: Node's TypeError for a primitive target, and + // the error string conversion of name and message + "dyn.errorCaptureStackTrace", // a proxy in the prototype chain can throw "dyn.builtinInstanceOf", "dyn.functionInstanceOf", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..182d787c8 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -272,6 +272,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL }, /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one * interned function per name, the same one dynamic values answer for @@ -291,6 +292,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** `new Date(value)` in JavaScript: a string argument parses with V8's * full date parser; every other value constructs as date.nativeNew. */ "date.nativeNewValue": { argTypes: [DYN], result: DYN }, + "date.argumentsTime": { argTypes: [DYN], result: DATE_T }, + "date.classOperation": { argTypes: [DYN, DYN], result: VOID }, "date.checkedValue": { argTypes: [DYN], result: DATE_T }, "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, /** toString() on a checked-dynamic receiver: runtime kind dispatch @@ -3646,6 +3649,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.arrayPrototype": { argTypes: [], result: DYN }, "dyn.arrayConstructor": { argTypes: [], result: DYN }, "dyn.objectPrototype": { argTypes: [], result: DYN }, + "dyn.errorPrototype": { argTypes: [], result: DYN }, + "dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL }, + "dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN }, + "dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN }, "dyn.functionApply": { argTypes: [], result: DYN }, "dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN }, "dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 6ff148c55..21223862a 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -349,6 +349,7 @@ const DYN_READ_ONLY = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", // The dynamic operators run the ToPrimitive and coercion helpers of // dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a // fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result. diff --git a/packages/compiler/src/ir/tiers.ts b/packages/compiler/src/ir/tiers.ts index ce2fdf0f4..13c464149 100644 --- a/packages/compiler/src/ir/tiers.ts +++ b/packages/compiler/src/ir/tiers.ts @@ -72,6 +72,7 @@ const DYN_KIND_TESTS: ReadonlySet = new Set([ "dyn.nativeMapIs", "dyn.nativeUrlIs", "dyn.nativeDateIs", + "dyn.nativeDateHandleIs", "dyn.nativeRegexIs", "dyn.dataViewIs", "dyn.typeof", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json new file mode 100644 index 000000000..1a1a6951c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7040-es5-error-constructor.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json new file mode 100644 index 000000000..f40c04116 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7041-error-called-without-new.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 000000000..c867cd88c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7042-string-method-names-on-objects.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json new file mode 100644 index 000000000..8b369c633 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7043-date-set-time.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 000000000..ee3592ba6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7044-nested-empty-object-maps.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 000000000..530aca0ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7045-instanceof-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 000000000..0f37ad2a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7046-parse-float-extra-arguments.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json new file mode 100644 index 000000000..7cd38cfb9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7047-dynamic-string-index.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json new file mode 100644 index 000000000..10c99f165 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7048-unary-number-coercions.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json new file mode 100644 index 000000000..5dc066127 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7049-member-assignment-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json new file mode 100644 index 000000000..fc73bb479 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7050-date-subclass.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json new file mode 100644 index 000000000..f37cd9763 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7051-date-subclass-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 000000000..7077aa5bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7052-date-subclass-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 000000000..d7d8ba2e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7053-private-fields-untyped-receivers.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 000000000..25dcd3680 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7054-let-bindings-with-null-writes.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f..5d68b2c6d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -662,6 +662,40 @@ bool scr_dyn_native_url_is(const ScrDyn *value); ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); +/* Instances of program classes declared `extends Date` (scr_lib.c). */ +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods); +/* Set by scr_date_class_operation_install; NULL in a program without such + * a class, where every wrapper below answers false or NULL. */ +typedef struct { + bool (*time)(const ScrDyn *value, double *time); + ScrDyn *(*to_json)(const ScrDyn *value, const ScrDyn *key); + ScrDyn *(*to_string)(const ScrDyn *value); + ScrDyn *(*value_of)(const ScrDyn *value); + ScrStr *(*name)(const ScrDyn *value); + ScrDyn *(*method)(const ScrDyn *value, const char *key, size_t key_len); +} ScrDateClassHooks; +extern const ScrDateClassHooks *scr_date_class_hooks; +static inline bool scr_dyn_date_class_time(const ScrDyn *value, double *time) { + return scr_date_class_hooks && scr_date_class_hooks->time(value, time); +} +static inline ScrDyn *scr_dyn_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_hooks->to_json(value, key); +} +static inline ScrDyn *scr_dyn_date_class_to_string(const ScrDyn *value) { + return scr_date_class_hooks->to_string(value); +} +static inline ScrDyn *scr_dyn_date_class_value_of(const ScrDyn *value) { + return scr_date_class_hooks->value_of(value); +} +static inline ScrStr *scr_dyn_date_class_name(const ScrDyn *value) { + return scr_date_class_hooks ? scr_date_class_hooks->name(value) : NULL; +} +static inline ScrDyn *scr_dyn_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + return scr_date_class_hooks ? scr_date_class_hooks->method(value, key, key_len) : NULL; +} +bool scr_dyn_date_instance_is(const ScrDyn *value); +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len); +double scr_dyn_date_arguments_time(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); @@ -1048,6 +1082,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc); * prototype chain has no toString (Node's answer). */ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what); ScrDyn *scr_dyn_object_prototype(void); +ScrDyn *scr_dyn_error_prototype(void); +bool scr_dyn_inherits_error_prototype(const ScrDyn *value); +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target); +ScrDyn *scr_dyn_error_capture_stack_trace_function(void); ScrDyn *scr_dyn_function_apply(void); ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 2a010f213..ec49dd383 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -845,6 +845,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (scr_event_ref_hooks && scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast)) return fast; + /* An inherited Date method of an instance of a class declared `extends + * Date`. */ + ScrDyn *date_method = scr_dyn_date_class_member(recv, method, strlen(method)); + if (date_method) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(date_method, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(date_method); + return result; + } ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 223517e92..a4d124ac3 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1652,7 +1652,40 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * view of its fields does not carry. */ const ScrStr *name = scr_dyn_typed_ref_class_name(d); ScrStr *out; - if (name && materialized->kind == SCR_DYN_OBJ && + double time; + ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time) + ? scr_dyn_date_class_name(d) : NULL; + if (date_class) { + /* An instance of a class declared `extends Date`: the class name and + * the time as Date.prototype.toISOString formats it, then its own + * enumerable properties when it has any. */ + ScrStr *iso = scr_insp_date(time); + InspBuf head = {0}; + ib_bytes(&head, date_class->data, date_class->len); + scr_str_release(date_class); + ib_char(&head, ' '); + ib_bytes(&head, iso->data, iso->len); + scr_str_release(iso); + ScrStr *prefix = ib_take(&head); + bool fields = false; + for (size_t i = 0; i < materialized->v.obj.len && !fields; i++) + fields = materialized->v.obj.entries[i].enumerable; + if (!fields || recurse > depth) { + scr_dyn_release(materialized); + return prefix; + } + char *cls = malloc(prefix->len + 1); + if (!cls) scr_trap("scriptc: out of memory\n"); + memcpy(cls, prefix->data, prefix->len); + cls[prefix->len] = '\0'; + scr_str_release(prefix); + size_t nseen = g_nseen; + g_pending_seen = instance; + out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(cls); + } else if (name && materialized->kind == SCR_DYN_OBJ && scr_dyn_obj_get(materialized, "%error", 6) == NULL) { char *cls = malloc(name->len + 1); if (!cls) scr_trap("scriptc: out of memory\n"); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b..549ca9165 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,6 +37,19 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; +const ScrDateClassHooks *scr_date_class_hooks; + +/* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the + * prototype of scriptc's Error instances, so a change to it would not reach + * them: every mutation throws. */ +static SCR_TL ScrDyn *scr_builtin_error_prototype; +static bool scr_dyn_error_prototype_fence(const ScrDyn *value) { + if (!value || value != scr_builtin_error_prototype) return false; + static const char message[] = "Error.prototype mutation is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; +} + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { @@ -1816,6 +1829,149 @@ ScrDyn *scr_dyn_array_prototype_base(void) { return scr_dyn_retain(scr_builtin_array_prototype); } +/* Error.prototype as a checked-dynamic object, for JavaScript constructor + * functions that inherit from it (`F.prototype = + * Object.create(Error.prototype)`): non-enumerable `constructor`, `name` + * ("Error"), `message` ("") and `toString`, on Object.prototype. The Error + * constructor has no checked-dynamic value, so `constructor` is a function + * named Error that throws when called. */ +static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); +static void scr_builtin_error_prototype_cleanup(void) { + ScrDyn *prototype = scr_builtin_error_prototype; + scr_builtin_error_prototype = NULL; + scr_dyn_release(prototype); +} +static ScrDyn *scr_error_prototype_to_string(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_OBJ || self->kind == SCR_DYN_ARR || self->kind == SCR_DYN_FUNC) { + ScrStr *text = scr_dyn_error_string(self); + if (text) { + result = scr_dyn_new_str(text); + scr_str_release(text); + } + } else { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Method Error.prototype.toString called on incompatible receiver "); + ScrStr *shown = self->kind == SCR_DYN_SYMBOL ? self->v.symbol.render(self->v.symbol.value) + : scr_dyn_string_coerce_js(self); + if (shown) { + scr_jb_put_str(&message, shown); + scr_str_release(shown); + } + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + } + scr_dyn_release(self); + return result; +} +static ScrDyn *scr_error_prototype_constructor(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + static const char message[] = "calling Error.prototype.constructor is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} +static void scr_error_prototype_hide(ScrDyn *object, const char *key, size_t key_len) { + ScrDynEntry *entry = scr_dyn_find_entry(object, key, key_len); + entry->enumerable = false; +} +ScrDyn *scr_dyn_error_prototype(void) { + if (!scr_builtin_error_prototype) { + ScrDyn *prototype = scr_dyn_alloc(SCR_DYN_OBJ); + scr_dyn_obj_set(prototype, "constructor", 11, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_constructor, 1, "native:Error", "Error")); + scr_error_prototype_hide(prototype, "constructor", 11); + ScrStr *name = scr_str_new("Error", 5); + scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(name)); + scr_str_release(name); + scr_error_prototype_hide(prototype, "name", 4); + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(empty)); + scr_str_release(empty); + scr_error_prototype_hide(prototype, "message", 7); + scr_dyn_obj_set(prototype, "toString", 8, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_to_string, 0, "native:Error", "toString")); + scr_error_prototype_hide(prototype, "toString", 8); + scr_builtin_error_prototype = prototype; + scr_atexit(scr_builtin_error_prototype_cleanup); + } + return scr_dyn_retain(scr_builtin_error_prototype); +} + +/* Error.captureStackTrace(target): defines a non-enumerable own `stack` + * on target. Node's value is the Error.prototype.toString text of target + * (read without calling an overriding toString) followed by stack frames; + * here, as for scriptc's own Error instances, it is the text alone. + * Answers undefined, or NULL with the exception pending. */ +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) { + /* An instance of a class that extends Error derives its stack from its + * name and message when the stack is read. */ + if (target->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL; + scr_dyn_release(view); + if (scr_exc_pending()) return NULL; + if (error) return scr_dyn_retain(scr_dyn_undefined()); + } + if (target->kind != SCR_DYN_OBJ) { + if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF || + target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) { + static const char message[] = "Error.captureStackTrace on this object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else { + static const char message[] = "invalid_argument"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + return NULL; + } + ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5); + if (existing ? existing->accessor || !existing->configurable : target->non_extensible) { + static const char message[] = "Cannot redefine property: stack"; + static const char extensible[] = "Cannot define property stack, object is not extensible"; + if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1); + return NULL; + } + ScrStr *text = scr_dyn_error_string(target); + if (!text) return NULL; + scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text)); + scr_str_release(text); + ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5); + entry->enumerable = false; + entry->writable = entry->configurable = true; + return scr_dyn_retain(scr_dyn_undefined()); +} +static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined()); +} +/* Error.captureStackTrace read as a value: one function per thread. */ +static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn; +static void scr_error_capture_stack_trace_fn_cleanup(void) { + ScrDyn *function = scr_error_capture_stack_trace_fn; + scr_error_capture_stack_trace_fn = NULL; + scr_dyn_release(function); +} +ScrDyn *scr_dyn_error_capture_stack_trace_function(void) { + if (!scr_error_capture_stack_trace_fn) { + scr_error_capture_stack_trace_fn = scr_dyn_new_func(scr_closure_new(NULL, 0), + scr_error_capture_stack_trace_call, 2, "native:Error", "captureStackTrace"); + scr_atexit(scr_error_capture_stack_trace_fn_cleanup); + } + return scr_dyn_retain(scr_error_capture_stack_trace_fn); +} + +/* `value instanceof Error` for a value that is not an Error instance: true + * when Error.prototype is on its prototype chain. */ +bool scr_dyn_inherits_error_prototype(const ScrDyn *value) { + if (!scr_builtin_error_prototype || !value || value->kind != SCR_DYN_OBJ) return false; + for (const ScrDyn *current = scr_dyn_proto_next(value); current; current = scr_dyn_proto_next(current)) { + if (current == scr_builtin_error_prototype) return true; + } + return false; +} + ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); } ScrDyn *scr_dyn_new_obj(void) { return scr_dyn_alloc(SCR_DYN_OBJ); } ScrDyn *scr_dyn_new_obj_with_identity( @@ -1847,7 +2003,6 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) { } static const ScrDyn *scr_dyn_chain_link(const ScrDyn *link); -static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); /* A typed capsule can be a prototype when its checked view is an object. */ static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) { @@ -1942,7 +2097,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) { return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL; } - if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; if (object->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(object); if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } @@ -2058,7 +2213,7 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { } ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); @@ -2089,7 +2244,7 @@ bool scr_dyn_is_extensible(ScrDyn *object) { } ScrDyn *scr_dyn_seal(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); @@ -3366,6 +3521,7 @@ const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d) { return ops ? ops->class_name : NULL; } + static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t count) { for (size_t i = 0; i < count; i++) { if (names[i] == key || scr_str_eq(names[i], (ScrStr *)key)) return true; @@ -3457,6 +3613,18 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) { } ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { + /* An instance of a class declared `extends Date` converts through its + * toString, declared or inherited. With an exception pending the view is + * the empty string. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + if (text) return text; + ScrStr *empty = scr_str_new("", 0); + ScrDyn *out = scr_dyn_new_str(empty); + scr_str_release(empty); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL; if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view; @@ -3467,12 +3635,28 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { return out; } +/* An inherited Date method of an instance of a class declared `extends + * Date`, when the instance's own members do not define `key`: +1, or + * NULL. */ +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len) { + ScrDyn *method = scr_dyn_date_class_method(d, key, key_len); + if (!method) return NULL; + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + bool declared = view && !scr_exc_pending() && scr_dyn_property_owner(view, key, key_len); + scr_dyn_release(view); + if (!declared) return method; + scr_dyn_release(method); + return NULL; +} + ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { if (d->v.typed_ref.record) return scr_typed_ref_record_get(d, key); if (scr_event_ref_hooks) { ScrDyn *inherited = scr_event_ref_hooks->get(d, key); if (inherited) return inherited; } + ScrDyn *date_method = scr_dyn_date_class_member(d, key->data, key->len); + if (date_method) return date_method; if (!scr_dyn_typed_ref_array_ops(d)) return NULL; if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len); size_t index; @@ -3708,6 +3892,29 @@ ScrDyn *scr_array_buffer_constructor(void) { return scr_dyn_retain(scr_array_buffer_ctor); } +/* `value instanceof C` for C a global constructor held as a value + * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the + * value's native kind, or -1 when callee is not one of them, or for Object + * and for class instances, which these kinds cannot answer. A typed record + * or array is none of these kinds. */ +static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { + if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || + strcmp(callee->v.fn.name, "Object") == 0) + return -1; + if (value->kind == SCR_DYN_TYPED_REF) + return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; + static const struct { const char *name; int tag; } kinds[] = { + {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, + }; + if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) + return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + } + return -1; +} + bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; @@ -3720,6 +3927,38 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); } + int global = scr_dyn_global_constructor_instanceof(value, callee); + if (global >= 0) return global; + /* An ordinary constructor function (an untyped JavaScript function whose + * prototype is an object): OrdinaryHasInstance walks the value's + * prototype chain for callee.prototype. */ + { + ScrStr *name = scr_str_new("prototype", 9); + ScrDyn *key = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *prototype = scr_dyn_reflect_get((ScrDyn *)callee, key, (ScrDyn *)callee); + scr_dyn_release(key); + if (!prototype) return false; /* the read threw: pending */ + if (prototype->kind == SCR_DYN_OBJ) { + bool found = false; + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) { + ScrDyn *current = scr_dyn_get_prototype((ScrDyn *)value); + while (current && current->kind != SCR_DYN_NULL) { + if (current == prototype) { + found = true; + break; + } + ScrDyn *next = scr_dyn_get_prototype(current); + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + } + scr_dyn_release(prototype); + return found; + } + scr_dyn_release(prototype); + } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; @@ -6191,6 +6430,7 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) { } void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { + if (scr_dyn_error_prototype_fence(value)) return; ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) { if (!scr_exc_pending()) { @@ -6341,6 +6581,7 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { } ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (scr_dyn_error_prototype_fence(target)) return NULL; if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); @@ -7726,6 +7967,9 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { } } +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc); + /* The METHOD-CALL spelling `d.toString(enc?)` — scr_dyn_to_string with * the one receiver whose prototype LACKS the method carved out: a * null-prototype dictionary (Object.create(null)) has no toString at @@ -7755,7 +7999,10 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return scr_str_new("", 0); } - return scr_dyn_to_string_method(d, NULL, what); + /* A toString the program defines receives the argument + * (`error.toString(compact)`); undefined passes none. */ + ScrDyn *args[1] = {(ScrDyn *)argument}; + return scr_dyn_to_string_method_args(d, NULL, what, args, argument->kind == SCR_DYN_UNDEF ? 0 : 1); } /* The METHOD-CALL spelling `d.valueOf()`: a primitive answers itself (the @@ -7995,7 +8242,12 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { return false; } if (d->kind == SCR_DYN_TYPED_REF) { - ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); + /* An instance of a class declared `extends Date` converts through its + * valueOf, declared or inherited. */ + double time; + ScrDyn *materialized = scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_value_of(d) + : scr_dyn_typed_ref_materialize(d); + if (!materialized) return false; bool ok = scr_dyn_number_coerce_js(materialized, out); scr_dyn_release(materialized); return ok; @@ -8098,9 +8350,18 @@ double scr_dyn_number_coerce(const ScrDyn *d) { static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d); /* ToPrimitive with the number hint, which differs from the default hint - * only for a Date: its time value. */ + * only for a Date: its time value, or the valueOf of an instance of a class + * declared `extends Date`. */ static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) { - return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d); + if (scr_dyn_is_date(d)) return scr_dyn_date_value_of(d); + double time; + if (d->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(d, &time)) { + ScrDyn *value = scr_dyn_date_class_value_of(d); + ScrDyn *out = value ? scr_dyn_add_primitive(value) : NULL; + scr_dyn_release(value); + return out; + } + return scr_dyn_add_primitive(d); } double scr_dyn_number_constructor(const ScrDyn *d) { @@ -8161,6 +8422,15 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) { return NULL; } if (d->kind == SCR_DYN_TYPED_REF) { + /* A Date's default hint is string: an instance of a class declared + * `extends Date` adds through its toString. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + ScrDyn *out = text ? scr_dyn_add_primitive(text) : NULL; + scr_dyn_release(text); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); ScrDyn *out = scr_exc_pending() ? NULL : scr_dyn_add_primitive(view); scr_dyn_release(view); @@ -8580,7 +8850,7 @@ void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn } void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } if (recv->kind == SCR_DYN_TYPED_REF) { if (scr_typed_ref_array_set(recv, key, value)) return; @@ -10408,7 +10678,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { * arrays, typed references and handles keep their explicit boundary; * deleting from a materialized snapshot would lose the mutation. */ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; } if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && (!strncmp(recv->v.typed_ref.type_key, "record:", 7) || @@ -10634,6 +10904,16 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { * method, called with the key. Returns the value to serialize (+1), or NULL * with an exception pending. */ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { + /* An instance of a class declared `extends Date` converts through its + * toJSON, declared or inherited. */ + double time; + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) { + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *converted = scr_dyn_date_class_to_json(value, name); + scr_dyn_release(name); + scr_dyn_release(value); + return converted; + } /* Inspect a typed reference without replacing its live identity. Later * members must read the source again after a callback mutates it. A live * array, or a live record without a `toJSON` field, needs no conversion. */ @@ -12826,7 +13106,7 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define) * nothing; index-keyed sources (arrays/strings/bytes) copy their index * keys like Node; the remaining kinds have no own enumerable keys. */ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) { - if (scr_dyn_class_reflection_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; + if (scr_dyn_class_reflection_fence(target) || scr_dyn_error_prototype_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) { const char *m = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m)); @@ -14620,7 +14900,10 @@ ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char return scr_map_retain(value->v.handle.ptr); } -ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { +/* scr_dyn_to_string_method, passing `args` to a toString the receiver's + * chain defines. */ +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc) { /* Object.create receivers must run their inherited formatter before the * default object tag; Effect shard keys use it to produce distinct hashes. */ const bool object_method = d->kind == SCR_DYN_OBJ && scr_dyn_property_owner(d, "toString", 8); @@ -14629,7 +14912,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt if (scr_exc_pending()) { scr_dyn_release(member); return NULL; } if (member && (object_method || member->kind != SCR_DYN_UNDEF)) { scr_dyn_this_push_dyn(d); - ScrDyn *value = scr_dyn_call(member, NULL, 0, what->data); + ScrDyn *value = scr_dyn_call(member, args, argc, what->data); scr_dyn_this_pop(); scr_dyn_release(member); if (!value) return NULL; @@ -14651,8 +14934,12 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string(d, enc); } +ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { + return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); +} + ScrDyn *scr_dyn_freeze(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -14663,7 +14950,7 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { } ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 3441e6142..d856e3000 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12997,9 +12997,116 @@ bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; } +/* Instances of program classes declared `extends Date` keep their time + * value in a compiled field. In checked-dynamic code such an instance is a + * typed reference, and the runtime reaches its time value and methods + * through the functions %main installs (builtins/date-classes.ts), called + * as (op, value, argument): for op 0 the time function answers the time + * value, or undefined for any other value, and for op 1 sets it; for ops 2 + * to 5 the method function calls the instance's toISOString, + * toJSON(argument), toString and valueOf. */ +enum { + SCR_DATE_CLASS_TIME, + SCR_DATE_CLASS_SET_TIME, + SCR_DATE_CLASS_TO_ISO_STRING, + SCR_DATE_CLASS_TO_JSON, + SCR_DATE_CLASS_TO_STRING, + SCR_DATE_CLASS_VALUE_OF, + SCR_DATE_CLASS_NAME, +}; +static SCR_TL ScrDyn *scr_date_class_operation; +static SCR_TL ScrDyn *scr_date_class_methods; + +static void scr_date_class_operation_cleanup(void) { + ScrDyn *operation = scr_date_class_operation; + ScrDyn *methods = scr_date_class_methods; + scr_date_class_operation = scr_date_class_methods = NULL; + scr_dyn_release(operation); + scr_dyn_release(methods); +} + +static bool scr_date_class_time_of(const ScrDyn *value, double *time); +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key); +static ScrDyn *scr_date_class_to_string(const ScrDyn *value); +static ScrDyn *scr_date_class_value_of(const ScrDyn *value); +static ScrStr *scr_date_class_name(const ScrDyn *value); +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len); + +/* The installation also publishes these functions to the rest of the + * runtime (scr_date_class_hooks), so only a program that declares a class + * extending Date links them. */ +static const ScrDateClassHooks scr_date_class_hook_table = { + scr_date_class_time_of, scr_date_class_to_json, scr_date_class_to_string, + scr_date_class_value_of, scr_date_class_name, scr_date_class_method, +}; + +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods) { + scr_date_class_hooks = &scr_date_class_hook_table; + if (!scr_date_class_operation) scr_atexit(scr_date_class_operation_cleanup); + scr_dyn_release(scr_date_class_operation); + scr_dyn_release(scr_date_class_methods); + scr_date_class_operation = scr_dyn_retain(time); + scr_date_class_methods = scr_dyn_retain(methods); +} + +/* The installed operation `op` on `value`: +1, or NULL with an exception + * pending. */ +static ScrDyn *scr_date_class_call(int op, const ScrDyn *value, const ScrDyn *argument) { + ScrDyn *code = scr_dyn_new_num(op); + ScrDyn *args[] = { code, (ScrDyn *)value, (ScrDyn *)(argument ? argument : scr_dyn_undefined()) }; + bool time = op < SCR_DATE_CLASS_TO_ISO_STRING || op == SCR_DATE_CLASS_NAME; + ScrDyn *result = scr_dyn_call(time ? scr_date_class_operation : scr_date_class_methods, args, 3, "Date"); + scr_dyn_release(code); + return result; +} + +/* True, with the time value in *time, when `value` holds an instance of a + * class declared `extends Date`. */ +static bool scr_date_class_time_of(const ScrDyn *value, double *time) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_TIME, value, NULL); + bool found = result && result->kind == SCR_DYN_NUM; + if (found) *time = result->v.num; + scr_dyn_release(result); + return found; +} + +/* `value instanceof Date`: a Date, or an instance of a class declared + * `extends Date`. */ +bool scr_dyn_date_instance_is(const ScrDyn *value) { + double time; + return scr_dyn_native_date_is(value) || scr_dyn_date_class_time(value, &time); +} + +/* toJSON(key), toString() or valueOf() of an instance of a class declared + * `extends Date` (scr_dyn_date_class_time answered true): the class's + * declaration or the Date method. +1, or NULL with an exception pending. */ +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_call(SCR_DATE_CLASS_TO_JSON, value, key); +} +static ScrDyn *scr_date_class_to_string(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_TO_STRING, value, NULL); +} +static ScrDyn *scr_date_class_value_of(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_VALUE_OF, value, NULL); +} + +/* The name of the class of an instance of a class declared `extends + * Date`: +1, or NULL for any other value. */ +static ScrStr *scr_date_class_name(const ScrDyn *value) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return NULL; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_NAME, value, NULL); + ScrStr *name = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL; + scr_dyn_release(result); + return name; +} + double scr_dyn_native_date_value(const ScrDyn *value) { - if (!scr_dyn_native_date_is(value)) { scr_dyn_check_fail(NULL, "Date", value); return NAN; } - return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + double time; + if (scr_dyn_date_class_time(value, &time)) return time; + scr_dyn_check_fail(NULL, "Date", value); + return NAN; } /* Method names without another use in this file, in named arrays (see @@ -13009,13 +13116,14 @@ static const char scr_date_to_time_string_name[] = "toTimeString"; static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - (void)self; (void)args; (void)argc; (void)what; + (void)self; (void)what; double ms = ((ScrNativeDate *)ptr)->milliseconds; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || - !strcmp(method, scr_date_to_time_string_name)) { + !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms) : !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms) + : !strcmp(method, "toUTCString") ? scr_date_to_utc_string(ms) : scr_date_to_time_string(ms); ScrDyn *result = scr_dyn_new_str(text); scr_str_release(text); @@ -13029,6 +13137,12 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho scr_str_release(text); return result; } + if (!strcmp(method, "setTime")) { + double time = NAN; + if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; + ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); DATE_GET("getUTCFullYear", scr_date_get_full_year_utc); @@ -13059,13 +13173,25 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { else { ScrDyn *value = arguments->v.arr.items[0]; if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value); - else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); + else if (scr_dyn_date_class_time(value, &ms)) { + } else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL; ms = scr_date_new_ms(ms); } return scr_dyn_native_date_from_ms(ms); } +/* The time value `new Date(...arguments)` constructs (super(...) in a + * class declared `extends Date`), or NaN with an exception pending. */ +double scr_dyn_date_arguments_time(const ScrDyn *arguments) { + ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) + : scr_dyn_native_date_new_value(arguments); + if (!date) return NAN; + double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + scr_dyn_release(date); + return time; +} + /* new Date(value) in JavaScript: a string parses with the full parser; * every other value takes scr_dyn_native_date_new. */ ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments) { @@ -13092,11 +13218,11 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments) { /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, - scr_date_to_time_string_name, + scr_date_to_time_string_name, "toUTCString", "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", - "getUTCMilliseconds", "getTimezoneOffset", + "getUTCMilliseconds", "getTimezoneOffset", "setTime", }; #define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0]) static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS]; @@ -13111,13 +13237,34 @@ static void scr_native_date_methods_cleanup(void) { } /* A Date method read as a value and called later: the closure's function - * slot holds the method's name, and the receiver must be a Date. */ + * slot holds the method's name, and the receiver must be a Date. On an + * instance of a class declared `extends Date` the method runs over its + * time value; toJSON calls the instance's toISOString. */ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { const char *method = closure->fn; ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; + double time; if (scr_dyn_native_date_is(self)) { result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method); + } else if (scr_dyn_date_class_time(self, &time)) { + if (!strcmp(method, "toJSON")) { + result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) + : scr_dyn_new_null(); + } else { + ScrNativeDate date = { 1, time }; + result = scr_native_date_invoke(&date, self, method, args, argc, method); + if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); + scr_dyn_release(stored); + if (!set) { + scr_dyn_release(result); + result = NULL; + } + scr_dyn_release(set); + } + } } else { static const char message[] = "this is not a Date object."; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -13126,6 +13273,21 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a return result; } +static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len); + +/* The Date method `key` as a function value when `value` holds an instance + * of a class declared `extends Date`, or NULL. The class's own members are + * read first, by the caller. */ +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + if (!scr_date_class_operation) return NULL; + bool named = false; + for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS && !named; i++) + named = strlen(scr_native_date_methods[i]) == key_len && !memcmp(scr_native_date_methods[i], key, key_len); + double time; + if (!named || !scr_dyn_date_class_time(value, &time)) return NULL; + return scr_native_date_get(NULL, key, key_len); +} + /* Property reads of a Date: its methods as function values, one per name * and thread, and NULL (undefined) for other names. */ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3974f346e..2404c3885 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 3.22.2 version: 3.22.2 js-yaml: - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.2 + version: 4.3.2 oxfmt: specifier: 0.71.0 version: 0.71.0 @@ -41,6 +41,9 @@ importers: rollup: specifier: 4.62.2 version: 4.62.2 + smol-toml: + specifier: 1.7.1 + version: 1.7.1 three: specifier: 0.186.1 version: 0.186.1 @@ -2089,8 +2092,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true json-schema-to-ts@1.6.4: @@ -2458,6 +2461,10 @@ packages: resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} engines: {node: '>= 18'} + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4305,7 +4312,7 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -4694,6 +4701,8 @@ snapshots: smol-toml@1.5.2: {} + smol-toml@1.7.1: {} + source-map-js@1.2.1: {} srvx@0.11.16: {} diff --git a/tests/corpus/7040-es5-error-constructor.mjs b/tests/corpus/7040-es5-error-constructor.mjs new file mode 100644 index 000000000..977567125 --- /dev/null +++ b/tests/corpus/7040-es5-error-constructor.mjs @@ -0,0 +1,79 @@ +// A constructor function that inherits from Error.prototype, the pre-class +// way of declaring an error type. +function ParseFailure(reason, position) { + Error.call(this); + this.name = "ParseFailure"; + this.reason = reason; + this.position = position; + this.message = reason + (position === undefined ? "" : " at " + position); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error().stack || ""; + } +} +ParseFailure.prototype = Object.create(Error.prototype); +ParseFailure.prototype.constructor = ParseFailure; +ParseFailure.prototype.toString = function toString(compact) { + return this.name + ": " + this.reason + (compact ? "" : " (position " + this.position + ")"); +}; + +const failure = new ParseFailure("unexpected token", 7); +console.log(failure instanceof ParseFailure, failure instanceof Error, failure.constructor === ParseFailure); +console.log(failure.name, "|", failure.message, "|", failure.reason, failure.position); +console.log(failure.toString(), "|", failure.toString(true), "|", String(failure), "|", `${failure}`); +console.log(Object.keys(failure), JSON.stringify(failure)); +console.log(typeof failure.stack, failure.stack.split("\n")[0], Object.keys(failure).includes("stack")); +console.log(Object.prototype.toString.call(failure), Error.isError(failure)); + +try { + throw new ParseFailure("thrown"); +} catch (caught) { + console.log(caught instanceof Error, caught instanceof ParseFailure, caught.reason, caught.message); +} +const values = [new ParseFailure("in a list"), new Error("plain"), { name: "Fake", message: "x" }, "text"]; +console.log(values.map((value) => value instanceof Error)); + +// The inherited members of Error.prototype. +const bare = Object.create(Error.prototype); +console.log(bare instanceof Error, bare.name, JSON.stringify(bare.message), String(bare), Object.keys(bare)); +console.log(Error.prototype.name, JSON.stringify(Error.prototype.message), Error.prototype instanceof Error); +const record = Object.create(Error.prototype); +record.name = "N"; +record.message = "M"; +console.log(String(record), `${Object.create(record)}`); + +// captureStackTrace on other objects: the header follows name and message. +const target = { name: "Custom", message: "details", toString() { return "ignored"; } }; +Error.captureStackTrace(target); +console.log(target.stack.split("\n")[0], Object.keys(target)); +const empty = {}; +Error.captureStackTrace(empty); +console.log(empty.stack.split("\n")[0], typeof Error.captureStackTrace); + +// A subtype of the subtype. +function StrictFailure(reason) { + ParseFailure.call(this, reason, 0); + this.name = "StrictFailure"; +} +StrictFailure.prototype = Object.create(ParseFailure.prototype); +StrictFailure.prototype.constructor = StrictFailure; +const strict = new StrictFailure("strict mode"); +console.log(strict instanceof StrictFailure, strict instanceof ParseFailure, strict instanceof Error, strict.toString(true)); + +// A class that extends Error calls it too, directly or through `?.`. +class TokenError extends Error { + constructor(message) { + super(message); + this.name = "TokenError"; + Error.captureStackTrace(this, TokenError); + } +} +class LexError extends TokenError { + constructor(message) { + super(message), (this.name = "LexError"), Error.captureStackTrace?.(this, this.constructor); + } +} +for (const error of [new TokenError("bad token"), new LexError("bad char")]) { + console.log(error.name, error.message, error.stack.split("\n")[0], error instanceof TokenError, Object.keys(error)); +} diff --git a/tests/corpus/7041-error-called-without-new.mjs b/tests/corpus/7041-error-called-without-new.mjs new file mode 100644 index 000000000..bf1df8e99 --- /dev/null +++ b/tests/corpus/7041-error-called-without-new.mjs @@ -0,0 +1,40 @@ +// The standard error constructors called as functions construct errors too, +// and Function.prototype.call ignores the receiver it is given. +const plain = Error("plain"); +const typed = TypeError("typed"); +const range = RangeError(); +const withCause = Error("wrapped", { cause: plain }); +console.log(plain instanceof Error, plain.message, String(plain)); +console.log(typed instanceof TypeError, typed instanceof Error, typed.name, typed.message); +console.log(range instanceof RangeError, JSON.stringify(range.message), String(range)); +console.log(withCause.cause === plain, withCause.message); +console.log(SyntaxError("s").name, ReferenceError("r").name, EvalError("e").name, URIError("u").name); + +const receiver = { marker: true }; +const called = Error.call(receiver, "via call"); +console.log(called instanceof Error, called.message, Object.keys(receiver), called === receiver); +const typedCall = TypeError.call(null, "typed via call"); +console.log(typedCall instanceof TypeError, typedCall.message); +const noArguments = Error.call(); +console.log(noArguments instanceof Error, JSON.stringify(noArguments.message)); + +let evaluated = 0; +function effect(value) { + evaluated++; + return value; +} +const ordered = Error.call(effect({}), effect("ordered")); +console.log(evaluated, ordered.message); + +function Wrapper(message) { + const base = Error.call(this, message); + this.message = base.message; + this.kind = "wrapper"; +} +const wrapped = new Wrapper("kept"); +console.log(wrapped.message, wrapped.kind, wrapped instanceof Wrapper, wrapped instanceof Error); +try { + throw Error("thrown without new"); +} catch (error) { + console.log(error instanceof Error, error.message); +} diff --git a/tests/corpus/7042-string-method-names-on-objects.mjs b/tests/corpus/7042-string-method-names-on-objects.mjs new file mode 100644 index 000000000..c623f5417 --- /dev/null +++ b/tests/corpus/7042-string-method-names-on-objects.mjs @@ -0,0 +1,70 @@ +// Objects whose members share names with String.prototype methods, filled +// in after their declaration. A call dispatches on the receiver's runtime +// kind: a string runs the String method, any other value its own member. +var helpers = {}; +var initialized; +function setup() { + if (initialized) return helpers; + initialized = true; + function repeat(text, count) { + let out = ""; + for (let i = 0; i < count; i += 1) out += text; + return out; + } + function padStart(text, width, fill) { + let out = String(text); + while (out.length < width) out = fill + out; + return out; + } + helpers.repeat = repeat; + helpers.padStart = padStart; + helpers.trim = function (value) { + return "<" + value + ">"; + }; + helpers.split = (value, separator) => [value, separator, "own"]; + helpers.toUpperCase = () => "own upper"; + return helpers; +} + +const h = setup(); +console.log("[" + h.repeat("ab", 3) + "]", "[" + h.repeat("-", 0) + "]"); +console.log(h.padStart(7, 3, "0"), h.padStart("long", 2, "*")); +console.log(h.trim(" x "), h.split("a,b", ","), h.toUpperCase()); + +const indent = h.repeat(" ", 2); +console.log("[" + indent + "]", indent.length); +const lines = ["one", "two"].map((line, index) => h.padStart(index + 1, 3, " ") + " | " + line); +console.log(lines.join("\n")); + +// The same names on a missing member throw Node's TypeError. +var partial = {}; +function fillPartial() { + partial.present = true; +} +fillPartial(); +try { + partial.repeat("x", 2); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} +try { + partial.trim(); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} + +// Arguments are evaluated once, in order, after the member is read. +const order = []; +var traced = {}; +function fillTraced() { + traced.replace = (a, b) => { + order.push("call"); + return a + b; + }; +} +fillTraced(); +function arg(value) { + order.push("arg " + value); + return value; +} +console.log(traced.replace(arg(1), arg(2)), order.join(", ")); diff --git a/tests/corpus/7043-date-set-time.mjs b/tests/corpus/7043-date-set-time.mjs new file mode 100644 index 000000000..a673c58dc --- /dev/null +++ b/tests/corpus/7043-date-set-time.mjs @@ -0,0 +1,28 @@ +// Date.prototype.setTime on Dates a JavaScript file constructs: the Date +// is one mutable object, so every reference sees the new time value. +function shifted(year, month, day, minutes) { + const date = new Date(Date.UTC(year, month, day, 12, 30, 15, 250)); + if (minutes) date.setTime(date.getTime() - minutes * 60000); + return date; +} +const unchanged = shifted(2001, 1, 3, 0); +const moved = shifted(2001, 1, 3, 90); +console.log(unchanged.toISOString(), moved.toISOString()); + +const alias = moved; +const record = { when: moved }; +console.log(moved.setTime(1000), alias.getTime(), record.when.toISOString()); +console.log(moved.setTime("2500"), moved.getTime(), moved.setTime(1.9), moved.setTime(-1.9)); +console.log(moved.setTime(NaN), moved.getTime(), String(moved), moved.toJSON()); +console.log(moved.setTime(8.64e15), moved.setTime(8.64e15 + 1), moved.setTime(-8.64e15 - 1)); +console.log(Object.is(moved.setTime(-0), 0), moved.setTime(), moved.setTime(true), moved.setTime(null)); +console.log(moved.setTime({ valueOf: () => 86400000 }), moved.toISOString()); +try { + moved.setTime({ valueOf: () => { throw new Error("no time"); } }); +} catch (error) { + console.log(error.message, moved.toISOString()); +} +console.log(JSON.stringify({ at: shifted(1999, 11, 31, 1) })); +const dates = [shifted(2020, 0, 1, 0), shifted(2020, 0, 1, 0)]; +dates.forEach((date, index) => date.setTime(date.getTime() + index * 1000)); +console.log(dates.map((date) => date.getUTCSeconds())); diff --git a/tests/corpus/7044-nested-empty-object-maps.mjs b/tests/corpus/7044-nested-empty-object-maps.mjs new file mode 100644 index 000000000..e8d330f20 --- /dev/null +++ b/tests/corpus/7044-nested-empty-object-maps.mjs @@ -0,0 +1,46 @@ +// Object literals whose fields start as empty objects, used as maps the +// program adds keys to, including chained assignment into two of them. +function index(entries) { + const table = { + first: {}, + second: {}, + all: {}, + lists: { first: [], second: [], all: [] }, + }; + function add(entry) { + if (entry.many) { + table.lists[entry.group].push(entry.name); + table.lists["all"].push(entry.name); + } else { + table[entry.group][entry.name] = table["all"][entry.name] = entry; + } + } + entries.forEach(add); + return table; +} +const table = index([ + { group: "first", name: "a" }, + { group: "second", name: "b" }, + { group: "first", name: "c", many: true }, + { group: "second", name: "a" }, +]); +console.log(Object.keys(table.first), Object.keys(table.second), Object.keys(table.all), table.lists); +console.log(table.all.a === table.second.a, table.first.a.group, table.all.b.name, "c" in table.first); +console.log(JSON.stringify(table)); + +function counters(words) { + const result = { seen: {}, firstAt: {} }; + words.forEach((word, position) => { + result.seen[word] = (result.seen[word] || 0) + 1; + if (!(word in result.firstAt)) result.firstAt[word] = position; + }); + return result; +} +console.log(JSON.stringify(counters(["x", "y", "x", "z", "y", "x"]))); + +const settings = { values: {}, names: {} }; +const key = String(Math.round(1.6)); +settings.values[key] = 2; +settings["names"][key + "!"] = settings.values[key] * 10; +delete settings.values[key]; +console.log(JSON.stringify(settings), Object.keys(settings.values).length); diff --git a/tests/corpus/7045-instanceof-constructor-values.mjs b/tests/corpus/7045-instanceof-constructor-values.mjs new file mode 100644 index 000000000..6b41cfdca --- /dev/null +++ b/tests/corpus/7045-instanceof-constructor-values.mjs @@ -0,0 +1,43 @@ +// `instanceof` against constructors held as values: a property holding a +// global constructor, and constructor functions with prototype chains. +const kinds = [ + { name: "Date", type: Date }, + { name: "RegExp", type: RegExp }, + { name: "Map", type: Map }, + { name: "Set", type: Set }, + { name: "WeakMap", type: WeakMap }, + { name: "Promise", type: Promise }, + { name: "Array", type: Array }, +]; +function classify(value) { + return kinds + .filter((kind) => typeof value === "object" && value !== null && value instanceof kind.type) + .map((kind) => kind.name) + .join("+") || "none"; +} +const samples = [new Date(0), /x/g, new Map([[1, 2]]), new Set([1]), new WeakMap(), Promise.resolve(1), [1, 2], { a: 1 }, null, "text", 4]; +console.log(samples.map(classify).join(" ")); + +function Shape(sides) { + this.sides = sides; +} +Shape.prototype.describe = function () { + return this.sides + " sides"; +}; +function Square() { + Shape.call(this, 4); +} +Square.prototype = Object.create(Shape.prototype); +Square.prototype.constructor = Square; +const square = new Square(); +const shape = new Shape(3); +const types = { Shape, Square }; +console.log(square instanceof types.Shape, square instanceof types.Square, shape instanceof types.Square); +console.log(square.describe(), shape.describe(), Object.create(Square.prototype) instanceof types.Shape); +console.log([square, shape, {}, []].map((value) => value instanceof types.Shape)); +const notCallable = JSON.parse("{}"); +try { + console.log(square instanceof notCallable); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/corpus/7046-parse-float-extra-arguments.mjs b/tests/corpus/7046-parse-float-extra-arguments.mjs new file mode 100644 index 000000000..9046a9145 --- /dev/null +++ b/tests/corpus/7046-parse-float-extra-arguments.mjs @@ -0,0 +1,9 @@ +// parseFloat reads one argument; Node ignores any others. +const radix = 10; +const inputs = ["3.25", " -0.5e2xyz", "0x10", "Infinityx", ".5", "-.e1", "", "1_000"]; +console.log(inputs.map((text) => parseFloat(text, 10))); +console.log(inputs.map((text) => parseFloat(text, radix, "ignored"))); +const fromJson = JSON.parse('{"value":"12.5kg","n":7}'); +console.log(parseFloat(fromJson.value, 10), parseFloat(fromJson.n, radix), Object.is(parseFloat("-0", 10), -0)); +const values = ["1.5", "2.25"].map((text) => parseFloat(text.replace(/_/g, ""), 10)); +console.log(values, values[0] + values[1]); diff --git a/tests/corpus/7047-dynamic-string-index.mjs b/tests/corpus/7047-dynamic-string-index.mjs new file mode 100644 index 000000000..fc81db3ce --- /dev/null +++ b/tests/corpus/7047-dynamic-string-index.mjs @@ -0,0 +1,21 @@ +// Indexing a string with a key whose type is only known at run time reads +// the property the key names: a code unit, the length, or undefined. +function skipSpaces(text, at) { + let c; + while ((c = text[at]) === " " || c === "\t") at++; + return [at, c]; +} +const where = JSON.parse('{"at":2,"name":"length","past":9,"text":"1","neg":-1,"frac":1.5}'); +console.log(skipSpaces("ab \tcd", where.at), skipSpaces("ab", where.past)); +const s = "hello"; +console.log(s[where.at], s[where.name], s[where.past], s[where.text], s[where.neg], s[where.frac]); +console.log(typeof s[where.past], typeof s[where.name], s[where.at] === "l"); +function scan(text, start) { + let out = ""; + for (let i = start; text[i] !== undefined && text[i] !== ";"; i++) out += text[i]; + return out; +} +console.log(scan("key=value;rest", JSON.parse("4")), scan("abc", JSON.parse("1"))); +const codes = []; +for (let i = JSON.parse("0"); i < s.length; i++) codes.push(s[i].charCodeAt(0)); +console.log(codes); diff --git a/tests/corpus/7048-unary-number-coercions.mjs b/tests/corpus/7048-unary-number-coercions.mjs new file mode 100644 index 000000000..4154ae79e --- /dev/null +++ b/tests/corpus/7048-unary-number-coercions.mjs @@ -0,0 +1,34 @@ +// Unary plus and minus on booleans and on values that may be strings or +// undefined, and += on a binding that holds a number or a string. +function step(text, at) { + const open = text[at] === "["; + at += +open; + return [at, +open + +true + +false, -open]; +} +const at = JSON.parse("1"); +console.log(step("a[b", at), step("ab", at)); +let total = 0; +for (const flag of [true, false, true]) total += +flag; +console.log(total, +true, -true, +(1 > 2), -(2 > 1)); + +const match = "2024-01-02".match(/^(\d+)-(\d+)-(\d+)(?:T(\d+))?$/); +console.log(+match[1], +match[4], -match[2], +match[3] + 1, typeof +match[4], -match[4]); +const optional = [undefined, "7", " 8 ", "", "x"]; +console.log(optional.map((value) => +value), optional.map((value) => -value)); + +let fraction = 0; +const digits = "5"; +if (digits) { + fraction = digits.slice(0, 3); + while (fraction.length < 3) fraction += "0"; + fraction = +fraction; +} +console.log(fraction, typeof fraction); +let width = 0; +width = digits.repeat(2); +width += 1; +console.log(width, typeof width); +let label = "x"; +label += 5; +label += true; +console.log(label); diff --git a/tests/corpus/7049-member-assignment-values.mjs b/tests/corpus/7049-member-assignment-values.mjs new file mode 100644 index 000000000..b614ba308 --- /dev/null +++ b/tests/corpus/7049-member-assignment-values.mjs @@ -0,0 +1,20 @@ +// Member assignments used as values: the receiver, the key and the value are +// evaluated in order, and the expression yields the assigned value. +const log = []; +const holder = { get inner() { log.push("receiver"); return this._inner; }, _inner: {} }; +function value(v) { log.push("value"); return v; } +function key(k) { log.push("key"); return k; } +const r = holder.inner[key("k")] = value(5); +console.log(r, log.join(","), holder._inner.k); +const store = JSON.parse('{"a":{},"b":{}}'); +const t = { id: "x" }; +const out = store["a"][t.id] = store.b[t.id] = t; +console.log(out === t, JSON.stringify(store)); +let n; +const arr = [0, 0, 0]; +n = arr[1] = arr[2] = 7; +console.log(n, arr); +function Counter() { this.count = 0; } +Counter.prototype.bump = function () { return (this.count = this.count + 1); }; +const c = new Counter(); +console.log(c.bump(), c.bump(), c.count); diff --git a/tests/corpus/7050-date-subclass.mjs b/tests/corpus/7050-date-subclass.mjs new file mode 100644 index 000000000..c22250fc1 --- /dev/null +++ b/tests/corpus/7050-date-subclass.mjs @@ -0,0 +1,69 @@ +// A class that extends Date: private fields beside the time value, super() +// with each Date constructor form, an overriding toISOString that +// JSON.stringify reaches through Date.prototype.toJSON, and the inherited +// Date methods. +class Moment extends Date { + #zone = "Z"; + #precise = true; + constructor(value, zone) { + if (value === undefined) super(0); + else super(value); + if (zone !== undefined) this.#zone = zone; + } + get zone() { + return this.#zone; + } + coarse() { + this.#precise = false; + return this; + } + toISOString() { + const iso = super.toISOString(); + const base = this.#precise ? iso : iso.slice(0, 19) + "Z"; + return this.#zone === "Z" ? base : base.slice(0, -1) + this.#zone; + } +} + +const fromString = new Moment("2001-02-03T04:05:06.789Z"); +const fromNumber = new Moment(86400000, "+01:00"); +const fromDate = new Moment(new Date(Date.UTC(1999, 11, 31, 23, 59, 59))); +const fromMoment = new Moment(fromString); +const none = new Moment(); +console.log(fromString.toISOString(), fromNumber.toISOString(), fromDate.toISOString()); +console.log(fromMoment.getTime() === fromString.getTime(), none.getTime(), fromNumber.zone); +console.log(fromString.getUTCFullYear(), fromString.getUTCMonth(), fromString.getUTCDate(), fromString.getUTCHours(), fromString.getUTCMilliseconds()); +console.log(fromString instanceof Date, fromString instanceof Moment, new Date(0) instanceof Moment); +console.log(JSON.stringify({ fromString, fromNumber, list: [fromDate.coarse()] })); +console.log(fromNumber.toJSON(), JSON.stringify(fromNumber)); +console.log(String(fromDate).slice(0, 15), `${fromDate}`.slice(0, 15), fromDate.toUTCString()); +console.log(+fromNumber, +fromNumber - 1000, fromNumber.valueOf(), fromNumber + "" === String(fromNumber)); +console.log(fromString > fromNumber, fromString < fromNumber, fromString >= fromMoment); +console.log(fromNumber.setTime(1000), fromNumber.getTime(), fromNumber.toISOString()); +console.log(new Date(fromString).toISOString(), new Date(fromNumber).getTime()); +console.log(fromString, [fromNumber], { d: fromDate }); + +const invalid = new Moment("not a date"); +console.log(Number.isNaN(invalid.getTime()), JSON.stringify({ invalid }), invalid.toJSON(), String(invalid)); +console.log(invalid); +try { + invalid.toISOString(); +} catch (error) { + console.log(error instanceof RangeError, error.message); +} + +class LocalMoment extends Moment { + constructor(value) { + super(value, "local"); + } + toISOString() { + return super.toISOString().replace("local", ""); + } +} +const local = new LocalMoment(0); +console.log(local.toISOString(), JSON.stringify([local]), local instanceof Moment, local.zone); + +// The field constructor and Date's statics, which the subclass inherits. +class Plain extends Date {} +const fields = new Plain(2020, 0, 15, 10, 30); +console.log(fields.getFullYear(), fields.getMonth(), fields.getDate(), fields.getHours(), fields.getMinutes()); +console.log(Plain.UTC(2000, 0, 1), Plain.parse("2000-01-01T00:00:00Z"), typeof Plain.now(), Object.keys(fields)); diff --git a/tests/corpus/7051-date-subclass-typed.ts b/tests/corpus/7051-date-subclass-typed.ts new file mode 100644 index 000000000..e39b5f534 --- /dev/null +++ b/tests/corpus/7051-date-subclass-typed.ts @@ -0,0 +1,47 @@ +// A TypeScript class that extends Date, and a subclass of it: typed calls +// of the inherited Date methods, conversions, comparisons, JSON and +// console output, with the toISOString override visible to toJSON. +class Stamp extends Date { + #precise: boolean; + label = "stamp"; + constructor(value: number | string, precise = false) { + super(value); + this.#precise = precise; + } + override toISOString(): string { + const iso = super.toISOString(); + return this.#precise ? iso : iso.slice(0, 19) + "Z"; + } + shifted(ms: number): Stamp { + return new Stamp(this.getTime() + ms, this.#precise); + } +} + +class Tick extends Stamp { + constructor(value: number) { + super(value, true); + } + seconds(): number { + return this.getUTCSeconds() + this.getUTCMilliseconds() / 1000; + } +} + +const a = new Stamp("2001-02-03T04:05:06.789Z"); +const b = new Tick(1500); +console.log(a.toISOString(), b.toISOString(), a.getTime(), b.seconds()); +console.log(a instanceof Date, b instanceof Stamp, b instanceof Date, a.shifted(1000).toISOString()); +console.log(JSON.stringify({ a, b, list: [a] }), JSON.stringify(a), a.toJSON(), b.toJSON()); +console.log(String(a).slice(0, 15), `${b}`.slice(0, 15), +a, b.valueOf(), a.getTime() - b.getTime()); +console.log(a < b, a > b, a >= b, a == a, a + "!" === String(a) + "!"); +console.log(a); +console.log([b], { b }); +console.log(a.setTime(5000), a.getTime(), a.toISOString(), a.label); + +const items: Stamp[] = [a, b, new Tick(0)]; +items.sort((x, y) => x.getTime() - y.getTime()); +console.log(items.map((s) => s.toISOString()).join(",")); +console.log(items.filter((s) => s instanceof Tick).length); +function describe(value: Stamp | string): string { + return typeof value === "string" ? value : value.toISOString(); +} +console.log(describe(b), describe("text")); diff --git a/tests/corpus/7052-date-subclass-dynamic.mjs b/tests/corpus/7052-date-subclass-dynamic.mjs new file mode 100644 index 000000000..267c01ded --- /dev/null +++ b/tests/corpus/7052-date-subclass-dynamic.mjs @@ -0,0 +1,43 @@ +// Instances of a class that extends Date held in values typed only at run +// time: method calls, method values, conversions and instanceof through +// those values, and JSON and console output of the containers. +class Day extends Date { + #label; + constructor(value, label) { + super(value); + this.#label = label; + } + get label() { + return this.#label; + } + toISOString() { + return super.toISOString().slice(0, 10); + } +} +class Plain extends Date {} + +const box = JSON.parse('{"items":[],"byName":{}}'); +box.items.push(new Day(Date.UTC(2020, 1, 29), "leap"), new Plain(0), new Date(1000), "text"); +box.byName.first = box.items[0]; + +function inspect(value) { + if (!(value instanceof Date)) return "not a date: " + value; + return [value.toISOString(), value.getTime(), value.getUTCDate(), value instanceof Day, value.toJSON()].join(" | "); +} +for (const item of box.items) console.log(inspect(item)); +console.log(JSON.stringify(box)); +console.log(box.byName.first.label, box.byName.first.getUTCFullYear(), String(box.items[1]).slice(0, 15)); +console.log(+box.items[0], box.items[0] - box.items[1], box.items[0] > box.items[2], `${box.items[1]}`.slice(0, 3)); + +const getTime = box.items[0].getTime; +console.log(getTime.call(box.items[1]), getTime.call(box.items[0]), typeof getTime); +const toISO = box.items[0].toISOString; +console.log(toISO.call(box.items[0])); +console.log(box.items[1].setTime(5000), box.items[1].getTime(), box.items[1].toISOString()); +try { + getTime.call({}); +} catch (error) { + console.log(error instanceof TypeError); +} +console.log(box.items); +console.log(new Date(box.items[0]).toISOString(), new Date(box.items[1]).getTime()); diff --git a/tests/corpus/7053-private-fields-untyped-receivers.mjs b/tests/corpus/7053-private-fields-untyped-receivers.mjs new file mode 100644 index 000000000..815f612b0 --- /dev/null +++ b/tests/corpus/7053-private-fields-untyped-receivers.mjs @@ -0,0 +1,72 @@ +// Private fields read and written through receivers whose type is known only +// at run time: static factories and helpers that take the instance as an +// argument, compound assignments and updates, `#x in value`, subclasses, and +// Node's TypeErrors for values that do not carry the field. +class Counter { + #count = 0; + #label = null; + constructor(label) { + this.#label = label; + } + static labeled(label, start) { + let counter = new Counter("pending"); + counter.#label = label; + counter.#count = start; + return counter; + } + static read(counter) { + return [counter.#label, counter.#count]; + } + static bump(counter, by) { + counter.#count += by; + counter.#count++; + ++counter.#count; + return counter.#count; + } + static clear(counter) { + counter.#label = null; + return counter; + } + static owns(value) { + return #count in value; + } + describe() { + return `${this.#label}:${this.#count}`; + } +} +class LimitedCounter extends Counter { + #limit = 3; + static limit(value) { + return value.#limit; + } +} + +const made = Counter.labeled("made", 5); +console.log(made.describe(), Counter.read(made), Counter.bump(made, 10), made.describe()); +const held = JSON.parse('{"list":[]}'); +held.list.push(new Counter("held"), new LimitedCounter("limited")); +for (const item of held.list) console.log(Counter.read(item), Counter.bump(item, 1), item.describe(), Counter.owns(item)); +console.log(LimitedCounter.limit(held.list[1]), Counter.clear(held.list[0]).describe()); +for (const value of [{}, [], null, undefined, 1, "text", held.list[0]]) { + try { + console.log(Counter.read(value)); + } catch (error) { + console.log("read", error instanceof TypeError, error.message); + } + try { + Counter.bump(value, 1); + console.log("bumped"); + } catch (error) { + console.log("bump", error instanceof TypeError, error.message); + } + try { + console.log(LimitedCounter.limit(value)); + } catch (error) { + console.log("limit", error instanceof TypeError, error.message); + } + try { + console.log("owns", Counter.owns(value)); + } catch (error) { + console.log("owns", error instanceof TypeError, error.message); + } +} diff --git a/tests/corpus/7054-let-bindings-with-null-writes.mjs b/tests/corpus/7054-let-bindings-with-null-writes.mjs new file mode 100644 index 000000000..c8ca26a9d --- /dev/null +++ b/tests/corpus/7054-let-bindings-with-null-writes.mjs @@ -0,0 +1,37 @@ +// `let` bindings initialized with a string, number or boolean and later +// assigned values that can be null or undefined. JavaScript files are +// checked without strict null checks, so the declared type of each binding +// is the primitive alone. +const PATTERN = /^(\d+)(?:\.(\d+))?([+-]\d\d:\d\d)?$/; +function parts(text) { + let offset = "Z"; + let fraction = 0; + let signed = false; + const match = text.match(PATTERN); + if (match) { + offset = match[3] || null; + fraction = match[2] ? +match[2] : undefined; + signed = match[3] ? match[3][0] === "-" : null; + } + return [offset, fraction, signed]; +} +console.log(parts("12"), parts("12.5"), parts("12.25-01:00"), parts("x")); + +function pick(flag) { + let name = "default"; + name = flag ? name.toUpperCase() : void 0; + let count = 1; + count = flag && count + 1; + let ready = true; + ready = flag === undefined ? undefined : !flag; + return [name, count, ready, typeof name, typeof count]; +} +console.log(pick(true), pick(false), pick(undefined)); + +let last = "start"; +const seen = []; +for (const value of ["a", null, "b", undefined]) { + last = value ?? null; + seen.push(last === null ? "null" : last.length); +} +console.log(seen, last); diff --git a/tests/fixtures/npm-static/js-yaml-cli.mjs b/tests/fixtures/npm-static/js-yaml-cli.mjs new file mode 100644 index 000000000..8c958cf80 --- /dev/null +++ b/tests/fixtures/npm-static/js-yaml-cli.mjs @@ -0,0 +1,63 @@ +// The published js-yaml package compiled as static modules: loading, dumping, +// schemas, custom types and the YAMLException errors it throws. +import yaml from "js-yaml"; +const text = ` +# config +name: demo +version: 1.2 +count: 0x1F +octal: 0o17 +neg: -12_000 +flt: 6.02e+23 +inf: .inf +nan: .NaN +on: yes +off: false +nil: ~ +date: 2001-12-14 +stamp: 2001-12-14t21:59:43.10-05:00 +str: "quoted \\u263A \\t tab" +single: 'it''s' +multi: | + line one + line two +folded: >- + folded + text +base: &base { a: 1, b: [x, y] } +derived: + <<: *base + c: 3 +list: + - one + - { k: v } + - [1, 2, 3] + - !!str 123 +set: !!set { a, b } +omap: !!omap [ { x: 1 }, { y: 2 } ] +binary: !!binary aGVsbG8= +`; +const doc = yaml.load(text); +console.log(JSON.stringify(doc, (k, v) => (v instanceof Uint8Array ? Array.from(v) : v))); +console.log(doc.stamp instanceof Date, doc.stamp.toISOString(), doc.date.toISOString(), Object.is(doc.nan, NaN) || Number.isNaN(doc.nan)); +const docs = yaml.loadAll("---\na: 1\n---\nb: 2\n...\n"); +console.log(JSON.stringify(docs)); +const items = []; +yaml.loadAll("--- 1\n--- two\n", (d) => items.push(d)); +console.log(items); +console.log(yaml.dump(doc, { sortKeys: true, lineWidth: 40, skipInvalid: true })); +console.log(yaml.dump({ a: "multi\nline\n", b: " lead", c: "yes", d: "123", e: null, f: [], g: {}, h: "x: y", i: -0, j: 1e21 }, { indent: 4 })); +console.log(yaml.dump([{ deep: { er: [1, { z: true }] } }], { flowLevel: 2 })); +console.log(yaml.dump({ s: "a'b\"c", u: "é☺", long: "word ".repeat(30).trim() }, { quotingType: '"', forceQuotes: true })); +for (const bad of ["a: [1", "a: b: c", "- a\nb: 1", "key: 'open", "a: !!int x", "? a\n? a\n", "&x a: *y"]) { + try { yaml.load(bad); console.log("no error", bad); } + catch (e) { console.log(e.name, "|", e.reason, "|", e.mark && e.mark.line, e.mark && e.mark.column, "|", e.message.split("\n")[0]); console.log(e.toString(true)); console.log(String(e).split("\n").length); } +} +try { yaml.load("a: 1\na: 2"); } catch (e) { console.log(e instanceof yaml.YAMLException, e instanceof Error, e.message); } +console.log(JSON.stringify(yaml.load("a: 1\na: 2", { json: true }))); +console.log(JSON.stringify(yaml.load("x: 1", { schema: yaml.JSON_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.FAILSAFE_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.CORE_SCHEMA }))); +const Point = new yaml.Type("!point", { kind: "sequence", construct: (d) => ({ x: d[0], y: d[1] }), instanceOf: Object, represent: (p) => [p.x, p.y] }); +const schema = yaml.DEFAULT_SCHEMA.extend([Point]); +console.log(JSON.stringify(yaml.load("p: !point [3, 4]", { schema }))); +try { yaml.dump({ f() {} }); } catch (e) { console.log(e.name, e.message); } +console.log(yaml.dump({ f() {}, u: undefined, k: 1 }, { skipInvalid: true })); diff --git a/tests/fixtures/npm-static/smol-toml-cli.mjs b/tests/fixtures/npm-static/smol-toml-cli.mjs new file mode 100644 index 000000000..453e16801 --- /dev/null +++ b/tests/fixtures/npm-static/smol-toml-cli.mjs @@ -0,0 +1,68 @@ +// The published smol-toml package compiled as static modules: parsing every +// value kind, the TomlDate subclass of Date, stringify and TomlError. +import { parse, stringify, TomlDate, TomlError } from "smol-toml"; +const text = ` +# a document +title = "TOML \\"example\\"" +int = +99 +neg = -17 +hex = 0xDEADBEEF +oct = 0o755 +bin = 0b1101 +under = 1_000_000 +float = 6.626e-34 +inf = inf +nan = nan +bool = true +odt1 = 1979-05-27T07:32:00Z +odt2 = 1979-05-27T00:32:00-07:00 +odt3 = 1979-05-27T00:32:00.999999-07:00 +ldt = 1979-05-27T07:32:00 +ld = 1979-05-27 +lt = 07:32:00.123 +space = 1979-05-27 07:32:00Z +literal = 'C:\\Users\\nodejs\\templates' +multi = """ +Roses are red +Violets are blue""" +lmulti = ''' +The first newline is +trimmed in raw strings.''' + +[owner] +name = "Tom" +"quoted key" = 1 +dotted.inner.key = "x" + +[database] +ports = [ 8000, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[[products]] +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] +name = "Nail" +color = "gray" +`; +const doc = parse(text); +console.log(JSON.stringify(doc, null, 1)); +for (const key of ["odt1", "odt2", "odt3", "ldt", "ld", "lt", "space"]) { + const d = doc[key]; + console.log(key, d instanceof Date, d instanceof TomlDate, d.isDateTime(), d.isLocal(), d.isDate(), d.isTime(), d.isValid(), d.toISOString(), Number.isNaN(d.getTime()) ? "NaN" : d.getTime()); +} +console.log(Number.isNaN(doc.nan), doc.inf, typeof doc.hex, doc.under); +console.log(stringify(doc)); +console.log(stringify({ a: new Date(0), b: TomlDate.wrapAsLocalDate(new Date(86400000)), c: new TomlDate("10:20:30"), n: [1, [2, 3]], t: { u: { v: "w" } }, s: "quote\" and \\ backslash", f: 1.5, i: 2, big: 2n ** 40n })); +const d = new TomlDate("2020-02-29T12:00:00+01:00"); +console.log(d.toISOString(), JSON.stringify({ d }), String(d.getUTCHours()), d.valueOf()); +const bad = new TomlDate("not a date"); +console.log(bad.isValid(), Number.isNaN(bad.getTime())); +for (const src of ["a = ", "a = 1\na = 2", "[t]\n[t]", "x = [1,", "k = 1979-13-01", "s = \"unterminated", "= 1", "a = 25:00:00"]) { + try { parse(src); console.log("parsed", JSON.stringify(src)); } + catch (e) { console.log(e instanceof TomlError, e instanceof Error, e.name, e.line, e.column, e.message.split("\n")[0]); } +} diff --git a/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json new file mode 100644 index 000000000..40dc942dc --- /dev/null +++ b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json @@ -0,0 +1 @@ +{ "plain": 3648, "san": 32315 } diff --git a/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json new file mode 100644 index 000000000..d5a7a927c --- /dev/null +++ b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3259 } diff --git a/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 000000000..e85a6cc3e --- /dev/null +++ b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1 @@ +{ "plain": 2776, "san": 20012 } diff --git a/tests/harness/corpus-costs/7043-date-set-time.mjs.json b/tests/harness/corpus-costs/7043-date-set-time.mjs.json new file mode 100644 index 000000000..cdb4d4f92 --- /dev/null +++ b/tests/harness/corpus-costs/7043-date-set-time.mjs.json @@ -0,0 +1 @@ +{ "plain": 2423, "san": 3371 } diff --git a/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 000000000..a7951b37e --- /dev/null +++ b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1 @@ +{ "plain": 2671, "san": 3931 } diff --git a/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 000000000..1776c882d --- /dev/null +++ b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2726, "san": 4330 } diff --git a/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 000000000..2e8f43953 --- /dev/null +++ b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1 @@ +{ "plain": 2204, "san": 3269 } diff --git a/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json new file mode 100644 index 000000000..d73f3cccc --- /dev/null +++ b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json @@ -0,0 +1 @@ +{ "plain": 2451, "san": 3411 } diff --git a/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json new file mode 100644 index 000000000..710783451 --- /dev/null +++ b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json @@ -0,0 +1 @@ +{ "plain": 2478, "san": 3675 } diff --git a/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json new file mode 100644 index 000000000..551cfbf7b --- /dev/null +++ b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2456, "san": 3617 } diff --git a/tests/harness/corpus-costs/7050-date-subclass.mjs.json b/tests/harness/corpus-costs/7050-date-subclass.mjs.json new file mode 100644 index 000000000..e21289735 --- /dev/null +++ b/tests/harness/corpus-costs/7050-date-subclass.mjs.json @@ -0,0 +1 @@ +{ "plain": 2932, "san": 3727 } diff --git a/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json new file mode 100644 index 000000000..fad282486 --- /dev/null +++ b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 2533, "san": 3429 } diff --git a/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 000000000..cabbe475e --- /dev/null +++ b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2740, "san": 3689 } diff --git a/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 000000000..8ebe6846a --- /dev/null +++ b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1 @@ +{ "plain": 2415, "san": 3338 } diff --git a/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 000000000..d313a64f1 --- /dev/null +++ b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3012 } diff --git a/tests/harness/npm-static-costs.json b/tests/harness/npm-static-costs.json index 2f1cdde1e..1546f8d57 100644 --- a/tests/harness/npm-static-costs.json +++ b/tests/harness/npm-static-costs.json @@ -84,6 +84,8 @@ "the published Effect Function subpath initializes and runs statically": 9097, "the published Effect generator has deterministic native results": 105577, "the published Effect generator logs and completes statically": 112814, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 12728, + "the published smol-toml parses and stringifies statically and matches Node": 11649, "untyped package methods preserve virtual overrides (llvm)": 3065 }, "san": { @@ -171,6 +173,8 @@ "the published Effect Function subpath initializes and runs statically": 8280, "the published Effect generator has deterministic native results": 136502, "the published Effect generator logs and completes statically": 181378, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 35621, + "the published smol-toml parses and stringifies statically and matches Node": 25130, "untyped package methods preserve virtual overrides (llvm)": 2730 } } diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index df82efbcf..7ee76307f 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1175,6 +1175,38 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { effectBuildTimeout, ); + test("the published js-yaml loads, dumps and reports errors statically and matches Node", async () => { + const entry = join(pilotRoot, "js-yaml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["js-yaml"] }); + expect(coverage.npmStatic).toEqual([{ package: "js-yaml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["js-yaml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + + test("the published smol-toml parses and stringifies statically and matches Node", async () => { + const entry = join(pilotRoot, "smol-toml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["smol-toml"] }); + expect(coverage.npmStatic).toEqual([{ package: "smol-toml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["smol-toml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + test("inferred variadic factories preserve checked callable results", async () => { const entry = join(pilotRoot, "variadic-factory-cli.ts"); const binary = await buildStatic(entry, ["variadic-factory"]); From c4d91bf2d9a65e65579fbc87c588beb2eadeb101 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:18:35 -0500 Subject: [PATCH 2/9] Give runtime globals definitions, export only the declared library symbols, and fence the new libFn spellings --- packages/compiler/src/frontend/lowering/surfaces.ts | 1 + packages/runtime/src/scr_json.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index adc11e35a..64227ed08 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2263,6 +2263,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "date.nativeNewFields", "date.nativeNewValue", "date.argumentsTime", + "date.classOperation", ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 549ca9165..cab709bf6 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,7 +37,7 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; -const ScrDateClassHooks *scr_date_class_hooks; +const ScrDateClassHooks *scr_date_class_hooks = NULL; /* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the * prototype of scriptc's Error instances, so a change to it would not reach From ebbc6c1831827649525ff5b3aff5b875a6062a80 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:56:15 -0500 Subject: [PATCH 3/9] Fix recursive JSON unions and primitive constructors as array callbacks --- .../lowering/containers/callback-arguments.ts | 59 +++++++++++++ .../lowering/expressions/array-literals.ts | 15 ++++ .../lowering/expressions/identifiers.ts | 51 ++++++++++-- .../src/frontend/lowering/lower-calls.ts | 34 ++++++++ .../src/frontend/lowering/lower-exprs.ts | 8 +- .../6826-map-primitive-constructors.ts.json | 6 ++ ...27-recursive-union-nested-literals.ts.json | 6 ++ ...828-recursive-union-object-walkers.ts.json | 6 ++ .../corpus/6826-map-primitive-constructors.ts | 25 ++++++ .../6827-recursive-union-nested-literals.ts | 29 +++++++ .../6828-recursive-union-object-walkers.ts | 83 +++++++++++++++++++ .../6826-map-primitive-constructors.ts.json | 1 + ...27-recursive-union-nested-literals.ts.json | 1 + ...828-recursive-union-object-walkers.ts.json | 1 + 14 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json create mode 100644 tests/corpus/6826-map-primitive-constructors.ts create mode 100644 tests/corpus/6827-recursive-union-nested-literals.ts create mode 100644 tests/corpus/6828-recursive-union-object-walkers.ts create mode 100644 tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json create mode 100644 tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json create mode 100644 tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index f39c8e86c..9db04e95b 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -6,7 +6,9 @@ import { varRef } from "../../../ir/build.js"; import { typeKey } from "../../type-mapper.js"; import { BOOL, + DYN, F64, + STRING, type IrExpr, type IrType, type SrcLoc, @@ -69,6 +71,20 @@ export function lowerArrayCallback( if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 }; } + // The ambient `String` (`values.map(String)`): String(value) converts its + // first argument as a direct call does, whatever the element type; the + // constructor's own value form takes a string. + if ( + ts.isIdentifier(argNode) && + argNode.text === "String" && + !isJsSourceFile(ts.getSourceFileOfNode(argNode)) && + lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) + ) { + const valueT = lead[0]; + if (valueT === undefined || valueT.kind === "void") + lowerer.badType(argNode, lowerer.typeOf(argNode)); + return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 }; + } const full = [...lead, F64, arrT]; // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`): // the contextual signature types the unannotated param `any` (the @@ -274,6 +290,49 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped: return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped)); } +/** `String` as a one-parameter callback over `valueT`: the conversion a + * direct `String(value)` call lowers to (the runtime's String() for unknown + * values and unions holding a symbol, ToString otherwise), interned per + * value type. */ +function stringConversionCallback( + lowerer: Lowerer, + valueT: IrType, + node: ts.Expression, +): IrExpr & { type: IrType & { kind: "func" } } { + const loc = locOf(node); + const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: STRING }; + const key = `mapString:${typeKey(valueT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%arr.mapString.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const value = varRef("v.0", valueT, loc); + const runtimeString = + valueT.kind === "dyn" || + (valueT.kind === "union" && + lowerer.unions.get(valueT.unionId)?.arms.some((arm) => arm.kind === "symbol") === true && + lowerer.dynConvertible(valueT)); + const converted: IrExpr = runtimeString + ? { + kind: "libCall", + fn: "dyn.stringConstructor", + args: [lowerer.coerceToExpected(value, DYN)], + type: STRING, + loc, + } + : lowerer.ensureString(value, node); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: valueT }], + returnType: STRING, + locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }], + body: [{ kind: "return", value: converted, loc }], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type: fnT, loc }; +} + /** A native closure for the ambient Boolean callback. Reifying the * JavaScript constructor itself is unnecessary; Array#filter only observes * its one-argument ToBoolean result. */ diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 00eae1041..8f119504c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -184,6 +184,21 @@ export function lowerArrayLiteral( if (arms.length === 1) mapped = arms[0]!; } } + // A NON-EMPTY literal whose own type is an array, under a union with + // exactly one array arm and no tuple arm (`[1, { n: true }]` in a + // `Json[]` arm of a recursive `Json`): the checker types each element + // from the arm, so its object literals lower as the arm's elements, + // and the literal's own element type (`number | { n: boolean }`) + // cannot hold them. Build it as the arm; its value only flows into the + // union slot. + if (ctxUnion && expr.elements.nodes.length > 0 && mapped?.kind === "array") { + const def = lowerer.unions.get(ctxUnion.unionId); + const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; + const tupleArms = + def?.arms.filter((a) => a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple) ?? + []; + if (arrayArms.length === 1 && tupleArms.length === 0) mapped = arrayArms[0]!; + } } // A Date's toJSON() element keeps its null in a literal with no declared // destination: the lib types say string. diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 16cca3b13..1f74cea42 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -11,6 +11,7 @@ import { DYN, F64, type IrExpr, + type IrLocal, type IrType, STRING, type SrcLoc, @@ -226,7 +227,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (use?.kind === "element") { const key = runtimeOptionalElementKey(use.access.argumentExpression); if (key === null) { - return varRef(local.id, local.type, loc); + return ( + runtimeOptionalKeyedRecord(lowerer, expr, local, loc) ?? + varRef(local.id, local.type, loc) + ); } return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc); } @@ -938,10 +942,43 @@ function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { : sf.fileName; } -/** Object.keys/Object.entries require an object receiver, but their first - * argument is not syntactically a member receiver. Recognize that one - * standard-library position so a runtime-optional loop element can be - * checked against the checker-narrowed record arm before builtin lowering. */ +/** A computed-key access (`value[key]` after `typeof value === "object"`) + * of a runtime-optional binding the checker narrowed to one record arm of a + * wider union. The binding re-tags to that record or undefined, so the keyed + * read or write evaluates its operands and then throws Node's TypeError for + * an undefined receiver. The other arms, which the checker excluded, fail + * the checked re-tag. Null when the binding stores only the record and + * undefined, or the narrowed record is not one of its arms. */ +function runtimeOptionalKeyedRecord( + lowerer: Lowerer, + expr: ts.Identifier, + local: IrLocal, + loc: SrcLoc, +): IrExpr | null { + if (local.type.kind !== "union") return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (narrowed?.kind !== "record") return null; + const fromId = local.type.unionId; + const arms = lowerer.unions.get(fromId)?.arms; + const recordTag = lowerer.armTag(fromId, narrowed); + const undefTag = lowerer.armTag(fromId, UNDEFINED_T); + if (!arms || arms.length <= 2 || recordTag < 0 || undefTag < 0) return null; + const target = lowerer.withUndefinedArm(narrowed); + if (target.kind !== "union") return null; + const trappable = new Set(); + for (let tag = 0; tag < arms.length; tag++) { + if (tag !== recordTag && tag !== undefTag) trappable.add(tag); + } + const helper = lowerer.unionRetagHelper(fromId, target.unionId, loc, trappable); + if (!helper) return null; + return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), target, loc); +} + +/** Object.keys, Object.values and Object.entries require an object + * receiver, but their first argument is not syntactically a member + * receiver. Recognize that one standard-library position so a + * runtime-optional loop element can be checked against the checker-narrowed + * record arm before builtin lowering. */ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): boolean { let arg = expr; while ( @@ -962,7 +999,9 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): return false; const access = call.expression; return ( - (access.name.text === "keys" || access.name.text === "entries") && + (access.name.text === "keys" || + access.name.text === "values" || + access.name.text === "entries") && lowerer.isStdlibMember(access) ); } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c5e34306c..46f98871e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -173,6 +173,7 @@ import { stringWrapperToString, symbolFieldInfo, templateRawTextOf, + primitiveCtorClosure, } from "./lower-exprs.js"; import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; @@ -4343,7 +4344,25 @@ export function lowerDynDispatchMethodCall( } } } + // `String`, `Number` or `Boolean` as the callback of an array method + // (`values.map(String)` over an unknown array): the method calls it with + // each element, whatever its type, and cannot observe which function + // value it received, so it gets the constructor's form that converts any + // argument rather than the string-input form typed code stores. + const arrayCallbackReceiver = + ARRAY_CALLBACK_METHODS.has(method) && + lowerer.checker.isArrayType(lowerer.typeOf(access.expression)); const args = call.arguments.nodes.map((arg, i) => { + const primitive = + i === 0 && arrayCallbackReceiver && ts.isIdentifier(arg) + ? stdlibGlobalNameOf(lowerer, arg) + : null; + if (primitive === "String" || primitive === "Number" || primitive === "Boolean") + return lowerer.coerceInto( + arg, + primitiveCtorClosure(lowerer, primitive, locOf(arg), true), + DYN, + ); if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN); const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg); return undefinedArg @@ -4361,6 +4380,21 @@ export function lowerDynDispatchMethodCall( }; } +/** Array.prototype methods whose first argument is a callback they call + * with the elements. */ +const ARRAY_CALLBACK_METHODS = new Set([ + "map", + "filter", + "forEach", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flatMap", +]); + /** Every String.prototype method name a typed or dyn string lowering * answers. */ function isStringMethodName(name: string): boolean { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ba96bea8c..b5acaf9e6 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -3057,8 +3057,10 @@ export function lowerFieldCompound( * returns the zero-capture closure over it. Typed sources use the mapped * `(value: string) => primitive` signature; a recognized JavaScript * concise-arrow selector (`() => String`, the picocolors fallback) takes - * dyn so its ordinary coercion signature survives inference residue. The - * dyn bodies run exact ToString/ToNumber/ToBoolean, including object hooks. + * dyn so its ordinary coercion signature survives inference residue, and a + * boxed constructor value takes dyn too (coerceToExpected). The dyn bodies + * run what the constructors do for any argument: String() (a symbol's + * description form included), ToNumber and ToBoolean, with object hooks. * Interning makes every reference under one ABI the SAME immortal closure, * so `opt.type === String` compares like JS function identity. */ export function primitiveCtorClosure( @@ -3081,7 +3083,7 @@ export function primitiveCtorClosure( ? name === "BigInt" ? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc } : name === "String" - ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc } + ? { kind: "libCall", fn: "dyn.stringConstructor", args: [v], type: STRING, loc } : name === "Number" ? { kind: "libCall", fn: "dyn.numberConstructor", args: [v], type: F64, loc } : { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json new file mode 100644 index 000000000..0bf7b0871 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6826-map-primitive-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 000000000..6e11aa685 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6827-recursive-union-nested-literals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 000000000..65c965b9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6828-recursive-union-object-walkers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6826-map-primitive-constructors.ts b/tests/corpus/6826-map-primitive-constructors.ts new file mode 100644 index 000000000..f6d61bd48 --- /dev/null +++ b/tests/corpus/6826-map-primitive-constructors.ts @@ -0,0 +1,25 @@ +// The String, Number and Boolean constructors passed as callbacks: over +// unknown arrays they convert any element, over typed arrays String converts +// each element as String(value) does, and the boxed values keep identity. +const values: unknown[] = [1, "b", true, null, undefined, Symbol("s"), [1, 2], { a: 1 }, 2n]; +console.log(values.map(String).join("|")); +console.log(values.slice(0, 5).map(Number), values.map(Boolean)); + +function joinAll(...parts: unknown[]): string { + return parts.map(String).join(","); +} +console.log(joinAll(1, "a", false, null)); + +const numbers = [1, 2.5, -0, NaN, Infinity]; +console.log(numbers.map(String)); +const mixed: (number | string | boolean)[] = [1, "x", false]; +console.log(mixed.map(String)); +const symbols: (symbol | string)[] = [Symbol("q"), "r", Symbol()]; +console.log(symbols.map(String)); +console.log([10n, -3n].map(String)); +console.log(["1", " 2 ", "x", ""].map(Number), ["", "0", "a"].map(Boolean)); + +const boxed: unknown = String; +const table: { [name: string]: unknown } = { s: String, n: Number, b: Boolean }; +console.log(boxed === String, table.n === Number, table.b === String, typeof boxed); +console.log(values.filter(Boolean).length, values.some(Boolean), numbers.every(Boolean)); diff --git a/tests/corpus/6827-recursive-union-nested-literals.ts b/tests/corpus/6827-recursive-union-nested-literals.ts new file mode 100644 index 000000000..900bbf977 --- /dev/null +++ b/tests/corpus/6827-recursive-union-nested-literals.ts @@ -0,0 +1,29 @@ +// A recursive JSON-like union with an array arm and an index-signature +// record arm: nested literals mix records into arrays and arrays into +// records at every depth. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function show(value: Json): string { + return JSON.stringify(value); +} + +const config: Json = { list: [1, "two", null, { nested: [true, { deeper: [] }] }], flag: false }; +console.log(show(config)); +console.log(show({ list: [{ n: true }] })); +console.log(show([1, { k: null }, [{ a: [2] }]])); +console.log(show({ a: { b: [{ c: "x" }, 3] } })); + +const rows: Json[] = [{ id: 1, tags: ["a", { t: 2 }] }, [null, { x: [] }]]; +console.log(rows.map(show).join(" | ")); + +let value: Json = []; +value = { replaced: [1, { again: [true] }] }; +console.log(show(value)); + +const doc: { [key: string]: Json } = { items: [{ name: "x", parts: [1, { p: "q" }] }] }; +console.log(show(doc)); +const items = doc["items"]; +if (Array.isArray(items)) { + items.push({ added: [9] }); + console.log(items.length, show(items)); +} diff --git a/tests/corpus/6828-recursive-union-object-walkers.ts b/tests/corpus/6828-recursive-union-object-walkers.ts new file mode 100644 index 000000000..5c854eb94 --- /dev/null +++ b/tests/corpus/6828-recursive-union-object-walkers.ts @@ -0,0 +1,83 @@ +// Recursive walkers over a JSON-like union: the array branch calls the +// walker for each element, and the record branch reads the narrowed record +// with Object.values, Object.keys and Object.entries. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function depth(value: Json): number { + if (Array.isArray(value)) return 1 + Math.max(0, ...value.map(depth)); + if (value !== null && typeof value === "object") return 1 + Math.max(0, ...Object.values(value).map(depth)); + return 0; +} + +function count(value: Json): number { + let total = 1; + if (Array.isArray(value)) { + for (const item of value) total += count(item); + } else if (value !== null && typeof value === "object") { + for (const item of Object.values(value)) total += count(item); + } + return total; +} + +function paths(value: Json, prefix: string): string[] { + if (Array.isArray(value)) return value.flatMap((item, index) => paths(item, `${prefix}[${index}]`)); + if (value !== null && typeof value === "object") { + const out: string[] = []; + for (const key of Object.keys(value)) out.push(...paths(value[key]!, `${prefix}.${key}`)); + for (const [key, item] of Object.entries(value)) if (item === null) out.push(`${prefix}.${key}=null`); + return out; + } + return [`${prefix}=${String(value)}`]; +} + +const doc: Json = JSON.parse('{"a":[1,{"b":[true,null]}],"c":{"d":"x"},"e":[]}'); +console.log(depth(doc), count(doc)); +console.log(paths(doc, "$").join(" ")); +console.log(depth(3), count([[[]]]), paths([], "$")); + +// A computed-key read or write in a branch that an array hole reaches throws +// Node's TypeError after the key (and, for a write, the value) evaluates. +type Bag = number | string | number[] | { [key: string]: number }; +const keys: string[] = []; +function keyOf(k: string): string { + keys.push(k); + return k; +} +function isBag(value: Bag): value is { [key: string]: number } { + return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number"; +} +function bump(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = (value[keyOf(k)] ?? 0) + 1; + return String(value[keyOf(k)]); + } + return "-"; +} +function put(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = keys.length; + return "put"; + } + return "-"; +} +function peek(value: Bag, k: string): string { + if (isBag(value)) return typeof value[keyOf(k)]; + return "-"; +} +const bags: Bag[] = [{ a: 1 }, "s", [1]]; +bags.length = 4; +for (const which of ["bump", "put", "peek"]) { + const out: string[] = []; + for (let i = 0; i < bags.length; i++) { + try { + const k = "a" + which.length; + if (which === "bump") out.push(bump(bags[i], k)); + else if (which === "put") out.push(put(bags[i], k)); + else out.push(peek(bags[i], k)); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } + } + console.log(out.join(" | ")); +} +console.log(keys.join(","), JSON.stringify(bags)); diff --git a/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json new file mode 100644 index 000000000..d2c795829 --- /dev/null +++ b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3500 } diff --git a/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 000000000..f44e89d4f --- /dev/null +++ b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3400 } diff --git a/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 000000000..77ddc81f8 --- /dev/null +++ b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1 @@ +{ "plain": 4200, "san": 5800 } From b99094543f67f874867a3eece221c8b2fde463e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:04:16 -0500 Subject: [PATCH 4/9] Revert "Read error constructors as values and destructure workerData in JavaScript" This reverts #1776, which made main CI fail. --- .../lowering/expressions/object-literals.ts | 25 +----- .../lowering/expressions/property-access.ts | 20 +---- .../src/frontend/lowering/lower-stmts.ts | 3 - .../6870-error-constructor-values.js.json | 6 -- .../6871-worker-data-destructuring.cjs.json | 6 -- packages/runtime/src/scr_dyn.h | 3 - packages/runtime/src/scr_dyn_invoke.c | 2 - packages/runtime/src/scr_json.c | 79 +++---------------- tests/corpus/6870-error-constructor-values.js | 55 ------------- .../corpus/6871-worker-data-destructuring.cjs | 30 ------- .../6870-error-constructor-values.js.json | 1 - .../6871-worker-data-destructuring.cjs.json | 1 - 12 files changed, 15 insertions(+), 216 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json delete mode 100644 tests/corpus/6870-error-constructor-values.js delete mode 100644 tests/corpus/6871-worker-data-destructuring.cjs delete mode 100644 tests/harness/corpus-costs/6870-error-constructor-values.js.json delete mode 100644 tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index a2f399e34..93bf43e95 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -36,7 +36,6 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; -import { builtinGlobalValue } from "./property-access.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -395,11 +394,9 @@ export function lowerDynObjectLiteral( const lowerValue = (): IrExpr => ts.isMethodDeclaration(prop) ? lowerer.lowerLambda(prop, true) - : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) - ? shorthandBuiltinValue(lowerer, prop)! - : !boxValue && ts.isObjectLiteralExpression(valueExpr) - ? lowerer.lowerExprExpecting(valueExpr, DYN) - : lowerer.lowerExpr(valueExpr as ts.Expression); + : !boxValue && ts.isObjectLiteralExpression(valueExpr) + ? lowerer.lowerExprExpecting(valueExpr, DYN) + : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); } catch (err) { // A PURE member read a JS file cannot lower (a namespace object @@ -3157,20 +3154,6 @@ function lowerDeclaredSpreadMerge( /** The value of a shorthand property (`{ x }`): the binding `x` refers to. * The property name's own symbol is the PROPERTY, so resolution goes * through getShorthandAssignmentValueSymbol. */ -/** `{ TypeError }`: a stdlib global in shorthand position reads as the value - * its name reads as elsewhere (the interned constructor or function value), - * or null when the global has no such value. The shorthand's name carries - * the property's symbol, so the identifier path cannot resolve it. */ -function shorthandBuiltinValue( - lowerer: Lowerer, - prop: ts.ShorthandPropertyAssignment, -): IrExpr | null { - const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); - if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); - return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); -} - export function lowerShorthandValue( lowerer: Lowerer, prop: ts.ShorthandPropertyAssignment, @@ -3187,8 +3170,6 @@ export function lowerShorthandValue( const callable = lowerer.lowerBuiltinCallableValue(builtin, loc); if (callable) return callable; } - const builtinValue = shorthandBuiltinValue(lowerer, prop); - if (builtinValue) return builtinValue; if (symbol) { if (lowerer.ctx.selfSymbol === symbol) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index d66110950..06186c4dc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4024,26 +4024,10 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } -/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics - * (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own - * lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */ -const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ - "Error", - "TypeError", - "RangeError", - "SyntaxError", - "ReferenceError", - "EvalError", - "URIError", - "AggregateError", -]); - /** The builtin constructors JavaScript sources read as real function * values rather than identity tokens: the runtime interns one function per * constructor, and dynamic values answer `.constructor` with the same one, - * so `o.constructor === Object` and `err.constructor === TypeError` are - * exact identity tests, and `err.constructor.name` is the constructor's - * name. */ + * so `o.constructor === Object` is an exact identity test. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -4054,7 +4038,6 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", - ...ERROR_CONSTRUCTOR_VALUES, ]); /** The builtin global functions JavaScript sources read as interned @@ -4197,7 +4180,6 @@ function fenceBuiltinConstructorStaticRead( symbol !== undefined && ts.symbolName(symbol).endsWith("Constructor") && GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && - !ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 7dc0b87cc..84c54307a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3889,9 +3889,6 @@ function stdlibGlobalTokenDestructure( if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; - // `workerData` is a value (the data the worker was started with), not an - // API surface: its members destructure as any untyped value's do. - if (globalName === "workerData") return null; if (!ts.isObjectBindingPattern(decl.name)) { lowerer.unsupported( "SC1031", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json deleted file mode 100644 index a2f0e02ea..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/6870-error-constructor-values.js" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json deleted file mode 100644 index b610a5878..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/6871-worker-data-destructuring.cjs" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 1d8a7f1d4..cafc8d07e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -725,9 +725,6 @@ ScrDyn *scr_dyn_array_constructor(void); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); -/* The interned function value of an error constructor ("TypeError"), or - * NULL for other names. */ -ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 7b375018b..2a010f213 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1913,8 +1913,6 @@ static ScrDyn *dyn_function_constructor(void) { ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); - ScrDyn *error = scr_dyn_error_constructor(name->data); - if (error) return error; ScrDyn *ctor = dyn_global_ctor(name->data); if (!ctor) scr_trap(dyn_s3); return ctor; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 57846b041..194ade887 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1801,55 +1801,6 @@ static void scr_builtin_object_prototype_cleanup(void) { scr_dyn_release(prototype); } -/* The error constructors as values (an error's `constructor`, and the - * globals in JavaScript): one interned function per constructor and thread, - * indexed like scr_error_vts, with the constructor's name and length. - * Calling one through the value is not supported yet, as for the other - * interned constructors (scr_dyn_global_constructor). */ -static const char *const scr_error_ctor_names[] = { - "Error", "TypeError", "RangeError", "SyntaxError", NULL, - "ReferenceError", "EvalError", "URIError", "AggregateError", -}; -#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0]) -static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS]; -static SCR_TL bool scr_error_ctors_registered; - -static void scr_error_ctors_cleanup(void) { - for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - scr_dyn_release(scr_error_ctor_values[i]); - scr_error_ctor_values[i] = NULL; - } -} - -static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { - (void)args; - (void)argc; - char message[128]; - int length = snprintf(message, sizeof message, - "calling the %s constructor through a stored value is not supported yet", - (const char *)closure->fn); - scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); - return NULL; -} - -ScrDyn *scr_dyn_error_constructor(const char *name) { - for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; - if (!scr_error_ctors_registered) { - scr_error_ctors_registered = true; - scr_atexit(scr_error_ctors_cleanup); - } - if (!scr_error_ctor_values[i]) { - uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1; - scr_error_ctor_values[i] = - scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, - "native:constructor", scr_error_ctor_names[i]); - } - return scr_dyn_retain(scr_error_ctor_values[i]); - } - return NULL; -} - ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -7756,17 +7707,6 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } case SCR_DYN_FUNC: { - /* The interned builtin constructors print with their names, as in Node - * (function Map() { [native code] }); other functions without a source - * text print the anonymous native form. */ - if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) { - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, "function "); - scr_jb_puts(&b, d->v.fn.name); - scr_jb_puts(&b, "() { [native code] }"); - return scr_jb_finish(&b); - } static const char f[] = "function () { [native code] }"; return scr_str_new(f, sizeof f - 1); } @@ -11720,18 +11660,21 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ ScrDyn *own = scr_dyn_obj_get(d, key, key_len); if (own) return scr_dyn_retain(own); if (key_len == 11 && memcmp(key, "constructor", 11) == 0) { - /* An error's constructor: the interned function value its global - * reads as (scr_dyn_error_constructor). */ - static const char *const names[] = { - "Error", "TypeError", "RangeError", "SyntaxError", NULL, - "ReferenceError", "EvalError", "URIError", "AggregateError", + static const char *const tokens[] = { + "[builtin Error]", "[builtin TypeError]", + "[builtin RangeError]", "[builtin SyntaxError]", + NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]", + "[builtin AggregateError]", }; size_t hit = scr_errdyn_find(d, false); const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt; - for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) { - if (!names[kind]) continue; + for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) { + if (!tokens[kind]) continue; if (vt != &scr_error_vts[kind]) continue; - return scr_dyn_error_constructor(names[kind]); + ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind])); + ScrDyn *result = scr_dyn_new_str(token); + scr_str_release(token); + return result; } } return scr_dyn_retain(scr_dyn_undefined()); diff --git a/tests/corpus/6870-error-constructor-values.js b/tests/corpus/6870-error-constructor-values.js deleted file mode 100644 index 81f5086d2..000000000 --- a/tests/corpus/6870-error-constructor-values.js +++ /dev/null @@ -1,55 +0,0 @@ -// The error constructors as values in JavaScript: an error's `constructor` -// is the same function the global names, whose name, length, typeof and -// String() are Node's, for errors the program throws, errors the runtime -// throws, and errors read back from catch bindings and containers. -function describe(label, error) { - const ctor = error.constructor; - console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor)); -} - -describe('Error', new Error('e')); -describe('TypeError', new TypeError('t')); -describe('RangeError', new RangeError('r')); -describe('SyntaxError', new SyntaxError('s')); -describe('ReferenceError', new ReferenceError('f')); -describe('EvalError', new EvalError('v')); -describe('URIError', new URIError('u')); -describe('AggregateError', new AggregateError([], 'a')); - -const thrown = [ - () => JSON.parse('{'), - () => { - const holder = JSON.parse('{"value":null}'); - return holder.value.missing; - }, - () => new Array(-1), - () => decodeURIComponent('%'), - () => { - throw new TypeError('own'); - }, -]; -const constructors = { SyntaxError, TypeError, RangeError, URIError }; -const caught = []; -for (const run of thrown) { - try { - run(); - } catch (error) { - caught.push(error); - console.log('caught', error.constructor.name, error.constructor === constructors[error.name]); - } -} - -const byName = new Map(); -for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1); -console.log('kinds', JSON.stringify([...byName])); - -console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError); -console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError)); - -const renamed = new TypeError('x'); -renamed.name = 'Custom'; -console.log('renamed', renamed.name, renamed.constructor.name); - -class ParseFailure extends SyntaxError {} -const failure = new ParseFailure('p'); -console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError); diff --git a/tests/corpus/6871-worker-data-destructuring.cjs b/tests/corpus/6871-worker-data-destructuring.cjs deleted file mode 100644 index 63ae789bd..000000000 --- a/tests/corpus/6871-worker-data-destructuring.cjs +++ /dev/null @@ -1,30 +0,0 @@ -// Destructuring the members of a worker's workerData in JavaScript: plain, -// renamed, defaulted, nested and rest bindings read the data the worker was -// started with, as reading `workerData.` does. -'use strict'; -const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); - -if (isMainThread) { - const sab = new SharedArrayBuffer(8); - const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' }; - const worker = new Worker(__filename, { workerData: data }); - worker.on('message', (lines) => { - for (const line of lines) console.log(line); - console.log('main sees', new Int32Array(sab)[0]); - }); -} else { - const lines = []; - const { name, size } = workerData; - lines.push(`plain ${name} ${size}`); - const { name: jobName, missing = 'fallback', size: count = 10 } = workerData; - lines.push(`renamed ${jobName} ${missing} ${count}`); - const { - options: { verbose, tags: [firstTag, ...otherTags] }, - } = workerData; - lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`); - const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData; - lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`); - counters[0] = 41; - counters[0] += 1; - parentPort.postMessage(lines); -} diff --git a/tests/harness/corpus-costs/6870-error-constructor-values.js.json b/tests/harness/corpus-costs/6870-error-constructor-values.js.json deleted file mode 100644 index c608ce33a..000000000 --- a/tests/harness/corpus-costs/6870-error-constructor-values.js.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1200, "san": 3400 } diff --git a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json deleted file mode 100644 index d6c0cb9ee..000000000 --- a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1300, "san": 3490 } From cab2db4f96f3205f35326e069a68bfc6df589953 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:27:50 -0500 Subject: [PATCH 5/9] Drop the Cipheriv stream fence from the node-types fixture --- tests/fixtures/node-types/fenced.ts | 4 -- .../__snapshots__/node-types-fenced.txt | 41 ++++++++----------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index dfd07d446..274d57272 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -93,10 +93,6 @@ void inspectReadableStream; import { callbackify } from "node:util"; const callbackified = callbackify(async (n: number) => n); void callbackified; -/* Cipheriv values support update(), final(), setAAD(), setAutoPadding(), - * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ -const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); -cipherStream.on("data", () => {}); /* ECDH on a curve Node knows but the runtime does not implement, and * computeSecret(view, encoding), which answers a Buffer where @types/node * declares a string. */ diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 2f1f8cb2e..3198db366 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -205,47 +205,38 @@ fenced.ts:94:23 - error SC2020: 'util.callbackify into a callback whose error pa hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void -fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard library types but has no scriptc lowering yet +fenced.ts:100:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); - 99 | cipherStream.on("data", () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | /* ECDH on a curve Node knows but the runtime does not implement, and - - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not - -fenced.ts:104:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - - 103 | import { createECDH } from "node:crypto"; - 104 | createECDH("sect163k1"); + 99 | import { createECDH } from "node:crypto"; + 100 | createECDH("sect163k1"); | ^~~~~~~~~~~ - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); hint: the supported curves are prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1 -fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet +fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet - 104 | createECDH("sect163k1"); - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 100 | createECDH("sect163k1"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole + 102 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet +fenced.ts:106:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); + 105 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 106 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ - 111 | } + 107 | } hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:111:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 114 | * compile; the other members fence by name. */ - 115 | const cores = navigator.hardwareConcurrency; + 110 | * compile; the other members fence by name. */ + 111 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 116 | void cores; + 112 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file From b03e965bf99716c4f353053526fa6ab67182ceda Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:35:27 -0500 Subject: [PATCH 6/9] Resolve 7058's promises with undefined so both checkers agree --- tests/corpus/7058-pipeline-function-stages.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/corpus/7058-pipeline-function-stages.mjs b/tests/corpus/7058-pipeline-function-stages.mjs index f0a61e7c2..f75abac39 100644 --- a/tests/corpus/7058-pipeline-function-stages.mjs +++ b/tests/corpus/7058-pipeline-function-stages.mjs @@ -60,7 +60,7 @@ await new Promise((resolve) => { }, (err, value) => { console.log("array source:", err ?? null, value); - resolve(); + resolve(undefined); }, ); }); @@ -114,7 +114,7 @@ function ticker(log) { await new Promise((resolve) => pipeline(["a", "b"], new PassThrough(), sink(log), () => { stop(); - resolve(); + resolve(undefined); }), ); console.log("array ticks:", log.join(" ")); @@ -134,7 +134,7 @@ function ticker(log) { sink(log), () => { stop(); - resolve(); + resolve(undefined); }, ), ); From feec4bf4df9734d23aa0c919429e5de42d902802 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:44:34 -0500 Subject: [PATCH 7/9] Call typed-array callbacks directly and visit on an integer counter The helpers of the typed-array callback methods (forEach, map, filter, some, every, find, findIndex, findLast, findLastIndex, reduce, reduceRight) called their callback through the closure's code pointer on every element, and one helper served every call with the same signature, so the callback was never inlined. A literal callback now gets a helper of its own, as for the array methods, and the constant-callback census accepts the typed-array helpers, so the helper calls that function directly. The visit is now a loop on its own counter, `for (let i = 0; i < a.length; i++)` or down from the last index, instead of a loop on a counter declared before it, so the counter is an integer and element reads need no index test. reduce and reduceRight without an initial value read their seed at the first visited index and start one past it. Adds reduce, some and map callbacks to the typed-array-kernels workload. --- benchmarks/runtime/README.md | 2 +- .../runtime/workloads/typed-array-kernels.ts | 11 +++ .../backend/llvm/constant-callbacks.test.ts | 23 +++-- .../src/backend/llvm/constant-callbacks.ts | 12 ++- .../lowering/containers/array-callbacks.ts | 2 +- .../src/frontend/lowering/containers/bytes.ts | 84 +++++++++++++---- .../test/bytes-callback-helpers.test.ts | 68 ++++++++++++++ .../6999-typed-array-callback-loops.ts.json | 6 ++ .../corpus/6999-typed-array-callback-loops.ts | 93 +++++++++++++++++++ .../6999-typed-array-callback-loops.ts.json | 1 + 10 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/bytes-callback-helpers.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json create mode 100644 tests/corpus/6999-typed-array-callback-loops.ts create mode 100644 tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index cbf47675a..34baccf5b 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and `reduce`/`some`/`map` callbacks | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index ba68f7241..d865ec914 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -21,6 +21,14 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; } +// Callback methods over a whole typed array. +function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number { + const energy = samples.reduce((sum, v) => sum + v * v, 0); + const loud = samples.some((v) => v > limit) ? 1 : 0; + const halved = data.map((v) => v >> 1); + return energy / samples.length + loud + halved[halved.length >> 1]!; +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -73,6 +81,7 @@ let byteSum = 0; let intSum = 0; let brightSum = 0; let lumaSum = 0; +let callbackSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. const at = (r * 7919) & (n - 1); @@ -88,6 +97,7 @@ for (let r = 0; r < rounds; r++) { addWrap(ia, ib, io); brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); + if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; @@ -102,3 +112,4 @@ console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); +console.log(`callbacks ${callbackSum.toFixed(3)}`); diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts index aa491c711..e8ca5571f 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts @@ -314,16 +314,19 @@ test("virtual calls admit every same-named method", () => { expect(new AmbientReceiverReaders(functions).ignores(caller)).toBe(false); }); -test("synthetic array HOF helpers are constant-callback consumers", () => { - const mod = fixture(); - const consumer = mod.functions[1]!; - const entry = mod.functions[2]!; - delete consumer.ownsPrototype; - consumer.name = "%arr.some.0"; - const call = entry.body[0]!; - if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = "%arr.some.0"; - expect(facts(mod).get("%arr.some.0")?.get("test")).toBe("predicate"); -}); +test.each(["%arr.some.0", "%bytes.some.0"])( + "synthetic array and typed-array HOF helpers are constant-callback consumers (%s)", + (name) => { + const mod = fixture(); + const consumer = mod.functions[1]!; + const entry = mod.functions[2]!; + delete consumer.ownsPrototype; + consumer.name = name; + const call = entry.body[0]!; + if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = name; + expect(facts(mod).get(name)?.get("test")).toBe("predicate"); + }, +); function factory(name: string, returns: IrExpr[]): IrFunction { return { diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index 6e6155460..8a898d6a0 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -29,12 +29,16 @@ export function findConstantCallbacks( mod.functions .filter( (fn) => - // Authored declarations, plus the frontend's synthetic array - // HOF loops (`%arr..`): those are plain direct-call - // functions, and a literal callback gets a helper of its own. + // Authored declarations, plus the frontend's synthetic array and + // typed-array HOF loops (`%arr..`, `%bytes..`): + // those are plain direct-call functions, and a literal callback + // gets a helper of its own. // Generator declarations are entered through their spawn wrapper, // which moves the same arguments into the generator's frame. - (fn.ownsPrototype || fn.name.startsWith("%arr.") || fn.generator !== undefined) && + (fn.ownsPrototype || + fn.name.startsWith("%arr.") || + fn.name.startsWith("%bytes.") || + fn.generator !== undefined) && (!fn.async || fn.generator !== undefined) && !fn.captures && !fn.classCaptures && diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 7ca69d9ba..612c47ea0 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -452,7 +452,7 @@ function withFastCallback(fn: IrFunction, fastT: CallbackType | null, elem: IrTy * constant-callback census then calls it directly and LLVM can inline the * callback into the loop (TurboFan's builtin+callback inlining). Callback * values keep the shared, type-interned helper. */ -function callbackSite(fnArg: IrExpr): string { +export function callbackSite(fnArg: IrExpr): string { return fnArg.kind === "closure" ? `@${fnArg.fnName}` : ""; } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 8a4364754..fd4420b69 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -43,7 +43,7 @@ import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-constructi import { lowerToNumberArgument } from "../lower-exprs.js"; import { lowerBytesIteratorValue } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; -import { findPredicateOk, predicateToBoolean } from "./array-callbacks.js"; +import { callbackSite, findPredicateOk, predicateToBoolean } from "./array-callbacks.js"; import { typeKey } from "../../type-mapper.js"; /** TypedArray.prototype.sort and toSorted. The receiver and comparator @@ -251,13 +251,28 @@ function lowerBytesCallbackCall( : method === "findIndex" || method === "findLastIndex" ? F64 : lowerer.irTypeOf(call)); - const helper = bytesCallbackHelper(lowerer, method, bytesT, resultT, fnRet, arity, hasInit, loc); + const helper = bytesCallbackHelper( + lowerer, + method, + bytesT, + resultT, + fnRet, + arity, + hasInit, + loc, + callbackSite(fnArg), + ); const args: IrExpr[] = [receiver, fnArg]; if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, resultT)); return { kind: "call", callee: helper, args, type: resultT, loc }; } -/** The interned helper of one typed-array callback method shape. */ +/** The interned helper of one typed-array callback method shape. A literal + * callback gets a helper of its own (`site`), which the backend then calls + * directly, as for the array methods. The visit is a loop on its own + * counter (`for (let i = 0; i < a.length; i++)`, or down from the last + * index), so the counter is an integer and the element reads need no + * index test. */ function bytesCallbackHelper( lowerer: Lowerer, method: BytesCallbackMethod, @@ -267,8 +282,9 @@ function bytesCallbackHelper( arity: number, hasInit: boolean, loc: SrcLoc, + site: string, ): string { - const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; + const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}${site}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%bytes.${method}`, key); @@ -300,10 +316,20 @@ function bytesCallbackHelper( type: fnRet, loc, }); + // The forward bound is the receiver's length itself (fixed for a typed + // array, and equal to `n`), which the emitter's typed-array loops use. + const length = (): IrExpr => ({ + kind: "bytesIntrinsic", + method: "length", + receiver: a(), + args: [], + type: F64, + loc, + }); const inRange = (): IrExpr => backward ? { kind: "bin", op: ">=", left: i(), right: numLit(0, loc), type: BOOL, loc } - : { kind: "bin", op: "<", left: i(), right: n(), type: BOOL, loc }; + : { kind: "bin", op: "<", left: i(), right: length(), type: BOOL, loc }; const advance = (): IrStmt => ({ kind: "assign", localId: "i.0", @@ -344,6 +370,7 @@ function bytesCallbackHelper( ...(hasInit ? [{ localId: "z.0", name: "z", type: resultT }] : []), ]; const visit: IrStmt[] = [{ kind: "assign", localId: "v.0", value: at(i()), loc }]; + let seeded = false; const prologue: IrStmt[] = []; let epilogue: IrStmt[]; switch (method) { @@ -459,10 +486,12 @@ function bytesCallbackHelper( loc, }); } else { + // The seed is the first visited element; the loop starts past it. + seeded = true; prologue.push( { kind: "if", - cond: { kind: "unary", op: "!", operand: inRange(), type: BOOL, loc }, + cond: { kind: "bin", op: "===", left: n(), right: numLit(0, loc), type: BOOL, loc }, then: [ { kind: "throw", @@ -486,8 +515,16 @@ function bytesCallbackHelper( else_: null, loc, }, - { kind: "varDecl", localId: "acc.0", init: at(i()), loc }, - advance(), + { + kind: "varDecl", + localId: "acc.0", + init: at( + backward + ? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc } + : numLit(0, loc), + ), + loc, + }, ); } visit.push({ @@ -506,17 +543,30 @@ function bytesCallbackHelper( init: { kind: "bytesIntrinsic", method: "length", receiver: a(), args: [], type: F64, loc }, loc, }, - { - kind: "varDecl", - localId: "i.0", - init: backward - ? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc } - : numLit(0, loc), - loc, - }, { kind: "varDecl", localId: "v.0", init: numLit(0, loc), loc }, ...prologue, - { kind: "for", init: null, cond: inRange(), update: advance(), body: visit, loc }, + { + kind: "for", + init: { + kind: "varDecl", + localId: "i.0", + init: backward + ? { + kind: "bin", + op: "-", + left: n(), + right: numLit(seeded ? 2 : 1, loc), + type: F64, + loc, + } + : numLit(seeded ? 1 : 0, loc), + loc, + }, + cond: inRange(), + update: advance(), + body: visit, + loc, + }, ...epilogue, ]; lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc }); diff --git a/packages/compiler/test/bytes-callback-helpers.test.ts b/packages/compiler/test/bytes-callback-helpers.test.ts new file mode 100644 index 000000000..c47d106c4 --- /dev/null +++ b/packages/compiler/test/bytes-callback-helpers.test.ts @@ -0,0 +1,68 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import type { IrFunction, IrModule, IrStmt } from "../src/ir/ir.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-callbacks-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(mod)).toEqual([]); + return mod; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const helpers = (mod: IrModule, method: string): IrFunction[] => + mod.functions.filter((f) => f.name.startsWith(`%bytes.${method}.`)); + +const visitLoop = (fn: IrFunction): IrStmt & { kind: "for" } => { + const loop = fn.body.find((s): s is IrStmt & { kind: "for" } => s.kind === "for"); + if (!loop) throw new Error(`no loop in ${fn.name}`); + return loop; +}; + +test("literal callbacks get helpers of their own; function values share one", async () => { + const mod = await lower(` + const a = new Int32Array([1, 2, 3]); + const double = (v: number): number => v * 2; + console.log(a.map((v) => v + 1).join(","), a.map((v) => v - 1).join(",")); + console.log(a.map(double).join(","), new Int32Array([4]).map(double).join(",")); + `); + expect(helpers(mod, "map")).toHaveLength(3); +}); + +test("typed-array callback loops run on their own counter", async () => { + const mod = await lower(` + const a = new Float64Array([1.5, 2.5, 3.5]); + console.log(a.some((v) => v > 2), a.reduce((s, v) => s + v)); + console.log(a.reduceRight((s, v) => s - v), a.findLastIndex((v) => v < 3)); + `); + for (const method of ["some", "reduce"]) { + const loop = visitLoop(helpers(mod, method)[0]!); + // for (let i = <0 or 1 past reduce's seed>; i < a.length; i = i + 1) + expect(loop.init).toMatchObject({ kind: "varDecl", localId: "i.0", init: { kind: "numLit" } }); + expect(loop.cond).toMatchObject({ + kind: "bin", + op: "<", + right: { kind: "bytesIntrinsic", method: "length" }, + }); + } + expect(visitLoop(helpers(mod, "reduce")[0]!).init).toMatchObject({ init: { value: 1 } }); + // Backward: from n - 2 past reduceRight's seed, and from n - 1. + expect(visitLoop(helpers(mod, "reduceRight")[0]!).init).toMatchObject({ + init: { kind: "bin", op: "-", right: { value: 2 } }, + }); + expect(visitLoop(helpers(mod, "findLastIndex")[0]!).init).toMatchObject({ + init: { kind: "bin", op: "-", right: { value: 1 } }, + }); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json new file mode 100644 index 000000000..de7ab0a69 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6999-typed-array-callback-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6999-typed-array-callback-loops.ts b/tests/corpus/6999-typed-array-callback-loops.ts new file mode 100644 index 000000000..84adfd995 --- /dev/null +++ b/tests/corpus/6999-typed-array-callback-loops.ts @@ -0,0 +1,93 @@ +// Typed-array callback methods with literal callbacks, function values shared +// by several calls, and callbacks chosen at run time: visit order and +// arguments, writes during the visit, early exits, reduce seeds and the +// empty-array TypeError, map conversions, Buffer results, throwing +// callbacks, empty arrays and subarray receivers. + +const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]); +const floats = new Float32Array([0.1, 2.5, -1.25, 3.75, 9.5]); + +const visits: string[] = []; +ints.forEach((v, i, a) => { + visits.push(`${i}:${v}:${a === ints}`); +}); +console.log("forEach", visits.join(" ")); + +const writes = new Uint8Array([1, 2, 3, 4, 5, 6]); +writes.forEach((v, i, a) => { + if (i + 1 < a.length) a[i + 1] = a[i + 1]! + v; +}); +console.log("forEach writes ahead", writes.join(",")); + +let calls = 0; +const counted = (v: number): boolean => { + calls++; + return v > 7; +}; +console.log("some", ints.some(counted), calls); +calls = 0; +console.log("every", ints.every((v) => (calls++, v < 10)), calls); +calls = 0; +console.log("find", ints.find(counted), calls, ints.find((v) => v > 100)); +calls = 0; +console.log("findIndex", ints.findIndex(counted), calls, ints.findIndex((v) => v > 100)); + +const order: number[] = []; +console.log( + "findLast", + ints.findLast((v, i) => (order.push(i), v < 0)), + ints.findLastIndex((v) => v > 100), + order.join(","), +); + +console.log("reduce", ints.reduce((acc, v) => acc + v), ints.reduce((acc, v, i) => acc + v * i, 100)); +const right: number[] = []; +console.log( + "reduceRight", + floats.reduceRight((acc, v, i) => (right.push(i), acc - v)), + right.join(","), + floats.reduceRight((acc, v) => acc + v, -1), +); +console.log("single", new Int8Array([42]).reduce(() => -1), new Int8Array([42]).reduceRight(() => -1)); +function tryReduce(label: string, run: () => number): void { + try { + console.log(label, run()); + } catch (error) { + console.log(label, (error as Error).name, (error as Error).message); + } +} +const emptyU16 = new Uint16Array(0); +const emptyF64 = new Float64Array(0); +tryReduce("empty u16 reduce", () => emptyU16.reduce((a, b) => a + b)); +tryReduce("empty f64 reduceRight", () => emptyF64.reduceRight((a, b) => a + b)); +tryReduce("empty with init", () => emptyU16.reduce((a, b) => a + b, 7) + emptyF64.reduceRight((a, b) => a + b, 1)); +console.log("empty", emptyU16.some(() => true), emptyF64.every(() => false), emptyU16.map((v) => v).length); + +console.log("map u8", new Uint8Array([250, 3, 128]).map((v) => v * 3).join(",")); +console.log("map clamped", new Uint8ClampedArray([250, 3, 128]).map((v) => v * 1.5 - 10).join(",")); +console.log("map i16", new Int16Array([30000, -30000, 7]).map((v, i) => v + i * 10000).join(",")); +console.log("map f32", floats.map((v) => v / 3).join(",")); +console.log("filter", ints.filter((v, i) => v > 0 && i % 2 === 0).join(","), floats.filter((v) => v > 1).join(",")); + +const buf = Buffer.from("scriptc"); +const upper = buf.map((c) => c - 32); +console.log("buffer map", Buffer.isBuffer(upper), upper.join(","), Buffer.isBuffer(buf.filter((c) => c > 112))); + +// One function value at several calls, and callbacks chosen at run time. +const double = (v: number): number => v * 2; +console.log("shared", new Int32Array([1, 2, 3]).map(double).join(","), new Int32Array([4, 5]).map(double).join(",")); +const pick = (k: number): ((v: number) => boolean) => (k > 0 ? (v) => v > k : (v) => v < k); +console.log("chosen", ints.filter(pick(4)).join(","), ints.filter(pick(-4)).join(",")); + +try { + let seen = 0; + floats.forEach((v) => { + seen++; + if (v < 0) throw new RangeError(`negative ${v} after ${seen}`); + }); +} catch (error) { + console.log("throw", (error as Error).name, (error as Error).message); +} + +const view = ints.subarray(2, 5); +console.log("subarray", view.map((v) => v + 1).join(","), view.reduce((a, v) => a * 10 + v, 0), view.findLastIndex((v) => v === 8)); diff --git a/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json b/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json new file mode 100644 index 000000000..63d9448b4 --- /dev/null +++ b/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 5000 } From 883f22ddd62005a26bb0fff3e9b83991e3cf6b7e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:50:13 -0500 Subject: [PATCH 8/9] Run two-counter for loops on integer counters `for (let lo = a, hi = b; lo < hi; lo++, hi--)` kept both counters as doubles: the counted-loop match takes a single counter, so every `a[lo]`/`a[hi]` converted and tested its index. A loop whose two counters close in on each other by positive literal steps, are not written by the body and index something now stores both as i64: on every body entry both lie between the first start and the second. A start without a proven integer range is tested at run time on a versioned loop, as a counted loop's is. --- .../src/backend/llvm/counted-loops.ts | 81 ++++++++----- .../src/backend/llvm/emit-statements.ts | 58 ++++++++- .../compiler/src/ir/integer-loops.test.ts | 73 ++++++++++- packages/compiler/src/ir/integer-loops.ts | 114 ++++++++++++++++++ .../corpus/7140-paired-loop-counters.ts.json | 6 + tests/corpus/7140-paired-loop-counters.ts | 81 +++++++++++++ .../7140-paired-loop-counters.ts.json | 1 + 7 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json create mode 100644 tests/corpus/7140-paired-loop-counters.ts create mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 36fc63e1f..02943b852 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,5 @@ -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -59,37 +60,51 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) { - // An exact integer of at most 2^53 - 1 in magnitude, and not -0 - // (the bit patterns of the value and its round trip agree). - // NaN and infinities fail the ordered range tests. - const start = host.emitExpr(loop.start); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - if (safe === "true") safe = ok; - else { - const both = B.tmp(); - B.line(`${both} = and i1 ${safe}, ${ok}`); - safe = both; - } - } + if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); return safe; } + +/** The run-time test of a guarded paired loop: each unproven start is an + * exact safe integer. */ +export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { + let safe = "true"; + for (const counter of [loop.low, loop.high]) + if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); + return safe; +} + +function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (safe === "true") return ok; + const both = host.B.tmp(); + host.B.line(`${both} = and i1 ${safe}, ${ok}`); + return both; +} + +/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit + * patterns of the value and its round trip agree). NaN and infinities fail + * the ordered range tests. */ +function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { + const B = host.B; + const start = host.emitExpr(expr); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return ok; +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7f189c066..7239db7d8 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,10 +42,15 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, + matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitPairedLoopSafety, +} from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -868,6 +873,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; + // Two counters closing in on each other (`lo++, hi--`) take the same + // integer storage as a counted loop's one. + const pairedLoop = + !integerLoop && !countedLoop && emitter.countedLoopsEnabled + ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) + : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -1000,8 +1011,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { - const safe = emitCountedLoopSafety(emitter, countedLoop); + if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { + const safe = countedLoop + ? emitCountedLoopSafety(emitter, countedLoop) + : emitPairedLoopSafety(emitter, pairedLoop!); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); @@ -1016,6 +1029,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; + const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1060,6 +1074,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); + } else if (pairedLoop) { + for (const counter of [pairedLoop.low, pairedLoop.high]) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, + ); + const value = emitter.emitExpr(counter.start); + const start = B.tmp(); + B.line(`${start} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${start}, ptr ${slot}`); + pairedSlots.push(slot); + emitter.integerLoopBindings.set(counter.localId, { + slot, + type: "i64", + signed: true, + range: pairedLoop.range, + }); + } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1141,6 +1173,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); + } else if (pairedLoop) { + const low = B.tmp(), + high = B.tmp(), + open = B.tmp(); + B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); + B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); + B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); + B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1172,6 +1212,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + } else if (pairedLoop) { + [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); + B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); + B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); + }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1180,6 +1228,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (pairedLoop) { + emitter.integerLoopBindings.delete(pairedLoop.low.localId); + emitter.integerLoopBindings.delete(pairedLoop.high.localId); + } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index aada3b053..24bbf5be0 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,6 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; +import { + matchIntegerArrayForLoop, + matchIntegerCountedForLoop, + matchIntegerPairedForLoop, +} from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -355,3 +359,70 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); + +test("paired counters closing in on each other run as integers", () => { + const vars = new Map(locals); + for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const element = (index: IrExpr): IrExpr => ({ + kind: "arrayGet", + arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, + index, + type: F64, + loc, + }); + const paired = ( + lowStart: IrExpr, + highStart: IrExpr, + update = [step("lo", "+"), step("hi", "-")], + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo", init: lowStart, loc }, + { kind: "varDecl", localId: "hi", init: highStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, + update: { kind: "block", body: update, loc }, + body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], + }) as IrStmt & { kind: "for" }; + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ + low: { localId: "lo", step: 1, startGuarded: false }, + high: { localId: "hi", step: -1, startGuarded: false }, + guarded: false, + range: { min: 0, max: 9 }, + }); + // A start without a known range is tested at run time. + expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ + high: { startGuarded: true }, + guarded: true, + }); + // Counters moving the same way, written by the body, captured, started + // from each other, or never used as an index are refused. + expect( + matchIntegerPairedForLoop( + paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), + vars, + ), + ).toBeNull(); + const writing = paired(number(0), number(9)); + writing.body.push(step("hi", "-", 2)); + expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("hi", { ...vars.get("hi")!, boxed: true }); + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); + expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); + const unused = paired(number(0), number(9)); + unused.body = []; + expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 9b4675685..e80680d6e 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,6 +281,120 @@ export function matchIntegerCountedForLoop( }; } +/** A for loop whose two counters close in on each other: + * + * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } + * + * The first counter only grows and the second only shrinks, by positive + * literal steps, the body writes neither, and the loop runs while the first + * is below (or at most) the second. Every body entry therefore holds both + * between the first start and the second start: exact integers when the + * starts are. A start without a proven integer range is tested at run time + * on a versioned loop, as a counted loop's is. */ +export interface IntegerPairedCounter { + localId: string; + start: IrExpr; + /** The signed step of each update. */ + step: number; + startGuarded: boolean; +} +export interface IntegerPairedForLoop { + low: IntegerPairedCounter; + high: IntegerPairedCounter; + inclusive: boolean; + guarded: boolean; + /** Both counters' values on every body entry. */ + range: IntegerRange; +} + +export function matchIntegerPairedForLoop( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, + ranges: IntegerRanges = new Map(), +): IntegerPairedForLoop | null { + const init = stmt.init, + update = stmt.update, + cond = stmt.cond; + if (init?.kind !== "block" || init.body.length !== 2) return null; + if (update?.kind !== "block" || update.body.length !== 2) return null; + if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const lowId = cond.left.localId, + highId = cond.right.localId; + if (lowId === highId) return null; + const counter = (id: string): IntegerPairedCounter | null => { + const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + // Each start is evaluated once in the head and again on a fallback path. + if ( + !stableLoopValue(start, locals, stmt.body, false, lowId) || + !stableLoopValue(start, locals, stmt.body, false, highId) + ) + return null; + const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); + if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + if (!known && !startGuarded) return null; + if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) + return null; + return { localId: id, start, step: steps[0]!, startGuarded }; + }; + const low = counter(lowId), + high = counter(highId); + if (!low || !high || low.step <= 0 || high.step >= 0) return null; + const startRange = (c: IntegerPairedCounter): IntegerRange => + c.startGuarded + ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } + : c.start.kind === "numLit" + ? { min: c.start.value, max: c.start.value } + : ranges.get(c.start)!; + const guarded = low.startGuarded || high.startGuarded; + // Only a loop whose counters index something gains from integer storage. + let useful = false, + cost = 0; + const isCounter = (e: IrExpr): boolean => + (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || + (e.kind === "bin" && + (e.op === "+" || e.op === "-") && + (isCounter(e.left) || isCounter(e.right))); + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; + if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + useful = true; + if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) + useful = true; + return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); + }; + const bodyStmt = (s: IrStmt): boolean => { + if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; + if ( + guarded && + (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + ) + return false; + return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); + }; + if (!stmt.body.every(bodyStmt) || !useful) return null; + return { + low, + high, + inclusive: cond.op === "<=", + guarded, + // A loop whose starts already fail the condition never enters the body. + range: { + min: startRange(low).min, + max: Math.max(startRange(low).min, startRange(high).max), + }, + }; +} + /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json new file mode 100644 index 000000000..122d64bb6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7140-paired-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts new file mode 100644 index 000000000..ec1610d1a --- /dev/null +++ b/tests/corpus/7140-paired-loop-counters.ts @@ -0,0 +1,81 @@ +// For loops with two counters closing in on each other (`lo++, hi--`): +// in-place reversals, inclusive bounds, larger steps, early exits, starts +// that are fractions, negative zero, NaN, huge or already crossed, nested +// reversals, and counters a closure captures or the body writes. +function reverse(a: number[], from: number, to: number): number[] { + for (let lo = from, hi = to; lo < hi; lo++, hi--) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +// Unproven starts: fractions, negative zero, NaN and huge values keep JS +// semantics (charCodeAt truncates its index; out-of-range reads are NaN). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + return out.join(","); +} +const word = "abcdefgh"; +console.log(codes(word, 0.5, 4.5)); +console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); +console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); +console.log(codes(word, -3, 2)); + +function pairs(n: number): string { + const out: string[] = []; + for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); + return out.join(" "); +} +console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); + +function firstGap(a: number[]): number { + let found = -1; + for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { + if (a[lo] === a[hi]) continue; + found = lo; + break; + } + return found; +} +console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); + +// The flip loop of a permutation puzzle: the next index comes from the array. +function flips(perm: number[]): number { + const work = perm.slice(); + let count = 0; + let head = work[0]!; + while (head !== 0) { + for (let lo = 0, hi = head; lo < hi; lo++, hi--) { + const t = work[lo]!; + work[lo] = work[hi]!; + work[hi] = t; + } + count++; + head = work[0]!; + } + return count; +} +console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); + +// Counters a closure reads, or the body writes, stay ordinary bindings. +const readers: (() => number)[] = []; +for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); +console.log(readers.map((f) => f()).join(",")); +const seen: number[] = []; +for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { + seen.push(lo, hi); + if (lo === 2) hi -= 3; +} +console.log(seen.join(",")); +const text = "abcdefgh"; +let mirrored = ""; +for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); +console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json new file mode 100644 index 000000000..549fd6a4a --- /dev/null +++ b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3880, "san": 6000 } From c09801c06402160567da0632419460a038ae1546 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:08:24 -0500 Subject: [PATCH 9/9] Map a JS binding's declared type only for a nullish write --- packages/compiler/src/frontend/lowering/lower-stmts.ts | 10 ++++++---- packages/compiler/src/frontend/lowering/lowerer.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 2840b0b45..687420baf 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6119,14 +6119,16 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar // types as a primitive (`offset = match[3] || null`): JavaScript files // are checked without strict null checks, so the declared type does not // admit them. - const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + // The declared type is mapped only when such a write exists. if ( - (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") && candidates.some( (node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right), ) - ) - return true; + ) { + const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") + return true; + } // A plain JavaScript write of a value the declared type cannot hold (the // checker does not report it): buffer-crc32 replaces its number[] table // with `new Int32Array(table)`. Only a different representation counts; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f3ab173f8..edf0a694b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3596,7 +3596,7 @@ export class Lowerer { this.genericClassInstanceType(decl, ref, mapArgument), runtimeClassValue: (type) => runtimeClassValueOfType(this, type), localClassValue: (type) => - this.instanceResultClass?.declared === type.id + this.instanceResultClass !== null && this.instanceResultClass.declared === type.id ? this.instanceResultClass.value : (this.localClassValueTypes.get(type.id) ?? null), mixinClassInstance: (decl) =>