diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 4b38c78f4..72b3de163 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -51,6 +51,8 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ ...(features.utilTiming ? { utilTiming: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), + ...(features.arrayLikeDispatch ? { arrayLikeDispatch: true } : {}), + ...(features.builtinPrototypeValue ? { builtinPrototypeValue: true } : {}), ...(features.computedRequire ? { computedRequire: true } : {}), // Island code's signal and exit listeners join the events unit's // registries (scope-B NI-3). diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0b5b897ec..715a23a7d 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1190,6 +1190,8 @@ export const LIB_FN_SYMS: Record = { "dyn.defineProperty": "scr_dyn_define_property", "dyn.getOwnPropertyDescriptor": "scr_dyn_get_own_property_descriptor", "dyn.arrayProtoCall": "scr_dyn_array_proto_call", + "dyn.arrayLikeProtoCall": "scr_dyn_array_like_call", + "dyn.builtinPrototype": "scr_dyn_builtin_prototype", "dyn.promiseAll": "scr_dyn_promise_all", "dyn.promiseAllSettled": "scr_dyn_promise_all_settled", "dyn.promiseAny": "scr_dyn_promise_any", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 043a6d4e1..e05d76963 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -128,6 +128,13 @@ export interface NativeLinkFeatures { utilPromisify: boolean; dynBuiltinClass: boolean; dynBuiltinStream: boolean; + /** An Array.prototype method runs over an array-like receiver: + * scr_dyn_array_like.c. Absent (false) otherwise, so programs that never + * call one over an array-like keep their exact link line. */ + arrayLikeDispatch?: boolean; + /** Number.prototype or Boolean.prototype read as a value: + * scr_dyn_builtin_prototype.c. Absent (false) otherwise. */ + builtinPrototypeValue?: boolean; parseArgs: boolean; stream: boolean; net: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index abc830712..7ac56b091 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -161,6 +161,13 @@ export interface CcOptions { computedRequire?: boolean; /** Builtin functions read as values: compiles scr_builtin_values.c. */ builtinValues?: boolean; + /** An Array.prototype method runs over an array-like receiver: + * compiles scr_dyn_array_like.c, which the dyn-invoke unit's array + * dispatch calls for receivers that are not arrays. */ + arrayLikeDispatch?: boolean; + /** Number.prototype or Boolean.prototype read as a value in a JavaScript + * source: compiles scr_dyn_builtin_prototype.c. */ + builtinPrototypeValue?: boolean; /** The program constructs EventTarget/Event/CustomEvent values or * calls events.addAbortListener (the event.* libCalls), or uses the * native fetch AbortSignal built on the same engine: compiles diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 3bcd82e40..e75d38426 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -213,6 +213,14 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"), // Builtin functions read as values in JavaScript dependencies. unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"), + // Array.prototype's generic algorithms over array-like receivers, and the + // Number/Boolean prototype objects as values: the dynamic tier's gated + // units, whose work is JavaScript-only. + unit((u) => u.opts.arrayLikeDispatch === true && u.opts.dynInvoke, "scr_dyn_array_like.c"), + unit( + (u) => u.opts.builtinPrototypeValue === true && u.opts.dynInvoke, + "scr_dyn_builtin_prototype.c", + ), // createRequire's computed require and require.resolve. unit((u) => u.opts.computedRequire === true, "scr_require.c"), unit((u) => u.opts.dc, "scr_dc.c"), diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 709b32562..3b490895a 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -107,6 +107,12 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { utilTiming: boolean; dynBuffer: boolean; builtinValues: boolean; + /** Array.prototype's generic algorithms over array-likes + * (scr_dyn_array_like.c). */ + arrayLikeDispatch?: boolean; + /** Number.prototype and Boolean.prototype as values + * (scr_dyn_builtin_prototype.c). */ + builtinPrototypeValue?: boolean; computedRequire: boolean; warningReplace: boolean; recordViewPartial: boolean; @@ -331,6 +337,8 @@ export function effectiveRuntimeFeatures( utilTiming: features.utilTiming === true, dynBuffer: features.dynBuffer === true, builtinValues: features.builtinValues === true, + arrayLikeDispatch: features.arrayLikeDispatch === true, + builtinPrototypeValue: features.builtinPrototypeValue === true, computedRequire: features.computedRequire === true, warningReplace: features.warningReplace === true, recordViewPartial: features.recordViewPartial === true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 08f62e3b5..76764fe66 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -110,6 +110,11 @@ export interface EarlyExecutableNativeFeatures { utilPromisify: boolean; dynBuiltinClass: boolean; dynBuiltinStream: boolean; + /** Array.prototype methods over array-like receivers: scr_dyn_array_like.c. */ + arrayLikeDispatch?: boolean; + /** Number.prototype and Boolean.prototype as values: + * scr_dyn_builtin_prototype.c. */ + builtinPrototypeValue?: boolean; parseArgs: boolean; stream: boolean; net: boolean; diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 5fc67138e..fbb53722c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -389,6 +389,15 @@ export function lowerTernary( } } +/** Whether `expr` is the checked-dynamic Number/Boolean prototype value a + * JavaScript source read (jsPrimitivePrototypeValue), or one of its + * members: the value is the prototype OBJECT, not the wrapper's primitive, + * so the checker's wrapper type must not extract a scalar from it. */ +function isPrimitivePrototypeValue(expr: IrExpr): boolean { + if (expr.kind === "libCall") return expr.fn === "dyn.builtinPrototype"; + return expr.kind === "dynKeyGet" && isPrimitivePrototypeValue(expr.value); +} + /** Checker-driven union narrowing. tsc's control-flow analysis narrows a * union-typed reference at use sites (`if (r.kind === "ok") { ...r... }` * types `r` as the ok-arm inside the branch); the IR value is still the @@ -430,6 +439,10 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // instead of misreading the payload (the dyn boundary's usual stance). // Object/array narrowings stay dyn-typed and keep their fences. if (expr.type.kind === "dyn") { + // Number.prototype and Boolean.prototype (and their members) are the + // prototype OBJECTS, though @types/node declares them with the wrapper + // interface: extracting the wrapper's numeric value would throw. + if (isPrimitivePrototypeValue(expr)) return expr; // A process.versions entry declared as a string can be absent here. if (isOptionalProcessVersionsRead(lowerer, node)) return lowerer.coerceToExpected(expr, lowerer.withUndefinedArm(STRING)); diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 9daeaf83a..a59c6ea1e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1938,6 +1938,8 @@ export function lowerIntrinsicProperty( lowerer.stdlibGlobalMember(expr.expression, "Object") === "prototype" ) return builtinGlobalValue("Object", locOf(expr)); + const primitivePrototype = jsPrimitivePrototypeValue(lowerer, expr); + if (primitivePrototype) return primitivePrototype; const builtinPrototype = builtinPrototypeMethod(lowerer, expr); if (builtinPrototype) return { @@ -5425,3 +5427,56 @@ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set ({ + kind: "libCall", + fn: "dyn.builtinPrototype", + args: [{ kind: "strLit", value: kind, type: STRING, loc }], + type: DYN, + loc, + }); + const receiver = expr.expression; + if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "prototype") { + const kind = primitivePrototypeKind(lowerer, receiver); + if (!kind) return null; + return { + kind: "dynKeyGet", + value: value(kind), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, + type: DYN, + loc, + }; + } + const kind = primitivePrototypeKind(lowerer, expr); + return kind ? value(kind) : null; +} + +/** Whether `expr` names `Number.prototype` or `Boolean.prototype` in a + * JavaScript source, which lowers to the prototype object above rather than + * to the wrapper interface's numeric/boolean value. */ +export function isPrimitivePrototypeRead(lowerer: Lowerer, expr: ts.Expression): boolean { + return primitivePrototypeKind(lowerer, expr) !== null; +} + +/** The kind whose prototype `expr` names (`Number.prototype`) in a + * JavaScript source, if any. */ +function primitivePrototypeKind( + lowerer: Lowerer, + expr: ts.Expression, +): "Number" | "Boolean" | null { + if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "prototype") return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; + if (lowerer.stdlibGlobalMember(expr, "Number") === "prototype") return "Number"; + if (lowerer.stdlibGlobalMember(expr, "Boolean") === "prototype") return "Boolean"; + return null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 1f0498cc4..1372e0040 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -139,6 +139,7 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; +import { isPrimitivePrototypeRead } from "./expressions/property-access.js"; import { lowerBufferConstruct, lowerBytesPrototypeSetCall, @@ -2889,6 +2890,30 @@ function immediateObjectTagReceiver( return null; } +/** Array.prototype methods whose `..call(receiver, ...)` form the + * array-like unit runs generically when `receiver` is not a native array: + * the rest of the prototype's methods besides the invoke unit's own set + * (at, slice, indexOf, lastIndexOf, includes and the higher-order + * callbacks). */ +const ARRAY_LIKE_BORROW_METHODS = new Set([ + "concat", + "copyWithin", + "fill", + "flat", + "flatMap", + "join", + "pop", + "push", + "reverse", + "shift", + "splice", + "toReversed", + "toSorted", + "toSpliced", + "unshift", + "with", +]); + const ARRAY_BORROW_METHODS = new Set([ "at", "every", @@ -2925,7 +2950,9 @@ function lowerArrayPrototypeBorrowCall( const method = access.expression; if ( method.questionDotToken || - !ARRAY_BORROW_METHODS.has(method.name.text) || + !( + ARRAY_BORROW_METHODS.has(method.name.text) || ARRAY_LIKE_BORROW_METHODS.has(method.name.text) + ) || !ts.isPropertyAccessExpression(method.expression) || method.expression.questionDotToken || method.expression.name.text !== "prototype" || @@ -2940,7 +2967,12 @@ function lowerArrayPrototypeBorrowCall( const args = call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "libCall", - fn: "dyn.arrayProtoCall", + // The array-like unit runs an extended method's algorithm over a + // receiver that is not a native array and delegates the rest to the + // invoke unit's dispatch. + fn: ARRAY_LIKE_BORROW_METHODS.has(method.name.text) + ? "dyn.arrayLikeProtoCall" + : "dyn.arrayProtoCall", args: [ receiver, { kind: "strLit", value: method.name.text, type: STRING, loc }, @@ -4010,6 +4042,12 @@ function lowerDynReceiverMethodCall( ) recv = lowerer.coerceToExpected(recv, DYN); if (recv.type.kind !== "dyn") return null; + // `Number.prototype.m(...)` reads the prototype OBJECT, whose `m` is its + // own member, so every primitive specialization below — which assumes a + // primitive receiver and coerces the value it is given — would be wrong. + // The shared dispatch reads the member and calls it with the prototype as + // `this`, which is Node's answer for the wrapper around +0 / false. + const prototypeReceiver = isPrimitivePrototypeRead(lowerer, access.expression); if ( access.name.text === "normalize" && call.arguments.nodes.length <= 1 && @@ -4120,7 +4158,7 @@ function lowerDynReceiverMethodCall( // prototype declares (Array carries includes/indexOf/slice too), so no // other kind inherits them. Shared names would need a receiver-kind // dispatch — they keep the fence. - if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { + if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable && !prototypeReceiver) { // A checked-dynamic object can still carry its OWN member of that // name: zod's schemas assign `trim`, `toLowerCase` and `startsWith` // in their constructors (`z.string().trim().min(1)`), an object's @@ -4151,6 +4189,7 @@ function lowerDynReceiverMethodCall( // runtime dispatch. if ( !ownCallable && + !prototypeReceiver && isDynPrimitiveKindMethod(access.name.text) && !lowerer.chainBlocked(access, call) ) { @@ -4171,6 +4210,7 @@ function lowerDynReceiverMethodCall( access.name.text === "toString" && call.arguments.nodes.length <= 1 && !ownCallable && + !prototypeReceiver && !(checkerUntyped && jsAssignsOwnToString(lowerer, access.expression)) ) { const argument = call.arguments.nodes[0]; @@ -4189,7 +4229,7 @@ function lowerDynReceiverMethodCall( // Every dyn kind's prototype chain declares valueOf, and the runtime // answers each kind's own: primitives and plain objects themselves, a // Date its time value, an own or inherited valueOf member its result. - if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) { + if (access.name.text === "valueOf" && call.arguments.nodes.length === 0 && !prototypeReceiver) { const webAssembly = lowerWebAssemblyValueOf(lowerer, access.expression, recv, locOf(call)); if (webAssembly !== null) return webAssembly; return { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 27d829674..a3a3ab80a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2193,6 +2193,19 @@ function builtinMemberValueName( return builtinModuleFnOf(lowerer, module, member)?.valueName ?? null; } +/** Whether `node` is a function, arrow or class DEFINITION that carries no + * name of its own, so the binding or property it initializes names it + * (V8's SetFunctionName). Every other expression evaluates to a value that + * already has whatever name it has. */ +function isAnonymousCallableDefinition(node: ts.Node): boolean { + if (ts.isArrowFunction(node)) return true; + return (ts.isFunctionExpression(node) || ts.isClassExpression(node)) && node.name === undefined; +} + +/** The name V8 gives a function value read from this expression: its own + * definition, through parenthesized, rebindable and const bindings and + * property reads (`process.cwd`), or the binding an anonymous definition + * initializes. Null when the source does not determine it. */ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { let n: ts.Node = node; while (ts.isParenthesizedExpression(n)) n = n.expression; @@ -2206,20 +2219,31 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { n = declaration; break; } - if ( - !declaration || - !ts.isVariableDeclaration(declaration) || - !declaration.initializer || - (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0 - ) + if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) break; + let value: ts.Node = declaration.initializer; + while (ts.isParenthesizedExpression(value)) value = value.expression; + // Name inference (V8's SetFunctionName) covers an ANONYMOUS function or + // class definition and nothing else: the binding names it, so a + // rebindable binding keeps the binding's spelling below. Any other + // initializer is a value that carries its own name — a named + // definition, a builtin function (`var a = String; a.name` is + // "String") or another binding (`var a = b` keeps b's name) — and + // resolution follows it. + if (isAnonymousCallableDefinition(value)) { + // The binding this definition initializes named it when it was + // created, so answer that binding and not the alias the reference + // reached it through: `import b from "./m"` keeps the "root" of + // `const root = (text) => text;` in `m`. + if (ts.isIdentifier(declaration.name)) return declaration.name.text; break; - n = declaration.initializer; - while (ts.isParenthesizedExpression(n)) n = n.expression; + } + n = value; } if (ts.isIdentifier(n)) return n.text; if (ts.isPropertyAccessExpression(n)) return n.name.text; if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) return n.name.text; - if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) { + if (ts.isClassExpression(n) && n.name) return n.name.text; + if (ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isClassExpression(n)) { // An anonymous function takes the name of the binding or property it // initializes, through parentheses (`const f = (function () {})`). let outer: ts.Node = n; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index ec0e3c57d..caec4b53d 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -815,6 +815,8 @@ async function compileExecutableNative( ...(features.utilTiming ? { utilTiming: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), + ...(features.arrayLikeDispatch ? { arrayLikeDispatch: true } : {}), + ...(features.builtinPrototypeValue ? { builtinPrototypeValue: true } : {}), ...(features.computedRequire ? { computedRequire: true } : {}), events: features.events, emitter: features.emitter, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index f205937fd..7a02f69ce 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -796,6 +796,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "child.propsDescriptor", "child.propsOnDyn", "dyn.arrayProtoCall", + "dyn.arrayLikeProtoCall", "process.chdir", "os.getPriority", "os.setPriority", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index bad4bc117..9d43e72d9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -404,6 +404,13 @@ function buildLibFnSignatures() { "dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN }, "dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** The same call through the array-like unit (JavaScript sources): + * Array.prototype's generic algorithms over a receiver that is not a + * native array, and the invoke unit's dispatch for everything else. */ + "dyn.arrayLikeProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** Number.prototype / Boolean.prototype as JavaScript values: the + * prototype object (dyn.builtinPrototype unit). */ + "dyn.builtinPrototype": { argTypes: [STRING], result: DYN }, /** Promise.all / Promise.allSettled / Promise.any over a spread * checked-dynamic array: the values, the settled-record array, or the * first fulfillment (AggregateError once every entry rejects). diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 53aa7fecb..c6e4afeb4 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -331,6 +331,8 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea /** Entry points that link the dyn-invoke unit. */ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ "dyn.arrayProtoCall", + "dyn.arrayLikeProtoCall", + "dyn.builtinPrototype", "dyn.arrayPrototype", "dyn.builtinFunction", "dyn.builtinMethod", @@ -1064,6 +1066,13 @@ const RUNTIME_FEATURE_RULES = { * half (scr_dyn_builtin_stream.c): native stream state for constructor * functions, over the stream unit. */ dynBuiltinStream: rule({ fns: ["dyn.builtinStreamClass"] }), + /** Array.prototype's generic algorithms over array-like receivers + * (scr_dyn_array_like.c), which delegates every other name to the + * dyn-invoke unit's array dispatch. */ + arrayLikeDispatch: rule({ fns: ["dyn.arrayLikeProtoCall"] }), + /** Number.prototype and Boolean.prototype as values + * (scr_dyn_builtin_prototype.c). */ + builtinPrototypeValue: rule({ fns: ["dyn.builtinPrototype"] }), dynInvoke: rule({ initial: (mod) => mod.workers === true, fns: [ @@ -1460,6 +1469,8 @@ function noRuntimeFeatures(): RuntimeFeatures { dynBuiltinClass: false, dynBuiltinStream: false, dynInvoke: false, + arrayLikeDispatch: false, + builtinPrototypeValue: false, dynTier: false, ecdh: false, x509: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json new file mode 100644 index 000000000..27e53c33a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-array-proto-array-likes.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-array-proto-array-likes.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-inference.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-inference.js.json new file mode 100644 index 000000000..86b98a4fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-inference.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-function-name-inference.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-module-alias/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-module-alias/main.js.json new file mode 100644 index 000000000..c4615fffd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-function-name-module-alias/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/js-function-name-module-alias/names.js", + "/tests/corpus/js-function-name-module-alias/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-primitive-prototype-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-primitive-prototype-values.js.json new file mode 100644 index 000000000..f8b16dac2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/js-primitive-prototype-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/js-primitive-prototype-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json new file mode 100644 index 000000000..152d85349 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/structured-clone-collections.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index dec75c32a..3e050cf36 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -82,6 +82,11 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // Island libCalls on native values typed by island declarations invoke // through the checked-dynamic path (scr_node_island_handle.c). ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch", "nodeIsland")], + // Array.prototype generic algorithms over array-like receivers + // (scr_dyn_array_like.c); it calls the invoke unit's dispatch. + ["scr_dyn_array_like.c", all("arrayLikeDispatch", "dynInvoke")], + // Number.prototype and Boolean.prototype read as values in JavaScript. + ["scr_dyn_builtin_prototype.c", all("builtinPrototypeValue", "dynInvoke")], // Buffer.prototype methods called on checked-dynamic values. ["scr_bytes_dyn.c", all("dynBuffer", "dynInvoke")], // Builtin functions read as values in JavaScript dependencies. diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index a3f0e9d33..5db285d0e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1577,6 +1577,14 @@ void scr_class_throw_read_only(ScrStr *field, ScrStr *owner); void scr_class_throw_not_extensible(ScrStr *field); ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key); ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args); +/* Array.prototype's generic methods over an array-like receiver + * (scr_dyn_array_like.c, linked only into programs that call them from a + * JavaScript source): +1, or NULL with the exception pending. */ +ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args); +/* Number.prototype or Boolean.prototype as a value, one object per thread + * (scr_dyn_builtin_prototype.c, linked only into programs that read them + * from a JavaScript source); `kind` is "Number" or "Boolean". +1. */ +ScrDyn *scr_dyn_builtin_prototype(const ScrStr *kind); ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity */ /* Names the prototype object of a compiled class `name` for util.inspect diff --git a/packages/runtime/src/scr_dyn_array_like.c b/packages/runtime/src/scr_dyn_array_like.c new file mode 100644 index 000000000..a238239ce --- /dev/null +++ b/packages/runtime/src/scr_dyn_array_like.c @@ -0,0 +1,773 @@ +/* Array.prototype's generic algorithms over array-like receivers. + * + * `Array.prototype..call(obj, ...)` (and a stored method value + * called on an object) reaches scr_dyn_array_proto_call, which runs + * at/slice/indexOf/lastIndexOf/includes and the higher-order methods over + * any receiver. Every other method answered "not supported yet", so a call + * on an array-like — an ordinary object with a length, or a primitive's + * ToObject wrapper — threw instead of running the algorithm. + * + * This unit runs those methods the way the specification writes them, as + * Get/HasProperty/Set/DeletePropertyOrThrow steps bounded by ToLength, and + * hands every other name back to scr_dyn_array_proto_call so the two units + * keep one implementation each. It links only into programs that use the + * dynamic array-like path (the frontend marks dyn.arrayLikeProtoCall for + * JavaScript sources), so typed programs keep their exact executables and + * scr_dyn_invoke.c is not touched: a program that only uses the older + * borrow methods links exactly what it linked before. + * + * The algorithms only use the checked-dynamic tree's public surface; the + * few helpers they share (an index key, Get/Has/Set at an index, the + * array-like length, a callback gate) are private to this unit. */ +#include "scr_runtime.h" +#include "scr_dyn_methods.h" + +#include +#include +#include + +/* One million indexed steps per call: past it the iteration reports + * SC2020 rather than running an unbounded loop over a length property the + * program chose. */ +static bool al_iteration(size_t step) { + if (step < 1000000) return true; + static const char msg[] = "array-like iteration over one million entries is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return false; +} + +static bool al_is(const ScrStr *method, const char *name) { + size_t len = strlen(name); + return method->len == len && memcmp(method->data, name, len) == 0; +} + +/* The canonical array index a property key spells, if it does. */ +static bool al_index(const ScrStr *key, size_t *out) { + if (!key->len || (key->len > 1 && key->data[0] == '0')) return false; + size_t n = 0; + for (size_t i = 0; i < key->len; i++) { + unsigned digit = (unsigned)(key->data[i] - '0'); + if (digit > 9 || n > (SIZE_MAX - digit) / 10) return false; + n = n * 10 + digit; + } + *out = n; + return true; +} + +/* Get(receiver, key) through the receiver's checked-dynamic kind. A + * primitive answers an ordinary property read on its ToObject wrapper, + * which is fresh and unobservable: an absent member is undefined. The + * result is owned (+1). */ +static ScrDyn *al_get(ScrDyn *recv, const ScrStr *key) { + if (recv->kind == SCR_DYN_TYPED_REF) { + ScrDyn *snapshot = scr_dyn_typed_ref_materialize(recv); + if (!snapshot) return NULL; + ScrDyn *result = al_get(snapshot, key); + scr_dyn_release(snapshot); + return result; + } + if (recv->kind == SCR_DYN_OBJ) return scr_dyn_obj_read(recv, key->data, key->len); + if (recv->kind == SCR_DYN_PROXY) return scr_dyn_proxy_get(recv, key); + if (recv->kind == SCR_DYN_FUNC) { + ScrDyn *value = scr_dyn_fn_get(recv, key->data, key->len); + return value ? value : scr_dyn_retain(scr_dyn_undefined()); + } + if (recv->kind == SCR_DYN_ARR || recv->kind == SCR_DYN_STR || recv->kind == SCR_DYN_BYTES) { + if (key->len == 6 && memcmp(key->data, "length", 6) == 0) { + double length = recv->kind == SCR_DYN_ARR ? (double)recv->v.arr.len + : recv->kind == SCR_DYN_STR ? scr_str_utf16_len(recv->v.str) + : (double)recv->v.bytes->len; + return scr_dyn_new_num(length); + } + size_t index; + if (!al_index(key, &index)) { + if (recv->kind == SCR_DYN_ARR && recv->v.arr.properties) + return scr_dyn_arr_named_get(recv, key); + return scr_dyn_retain(scr_dyn_undefined()); + } + if (recv->kind == SCR_DYN_ARR && index >= recv->v.arr.len && recv->v.arr.properties) + return scr_dyn_arr_named_get(recv, key); + if (recv->kind == SCR_DYN_ARR) return scr_dyn_arr_at(recv, (double)index); + if (recv->kind == SCR_DYN_BYTES) { + return index < recv->v.bytes->len + ? scr_dyn_new_num(scr_bytes_get(recv->v.bytes, (double)index)) + : scr_dyn_retain(scr_dyn_undefined()); + } + if ((double)index >= scr_str_utf16_len(recv->v.str)) + return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *unit = scr_str_slice(recv->v.str, (double)index, (double)index + 1); + ScrDyn *value = scr_dyn_new_str(unit); + scr_str_release(unit); + return value; + } + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* HasProperty(receiver, key). */ +static bool al_has(ScrDyn *recv, const ScrStr *key) { + if (recv->kind == SCR_DYN_STR || recv->kind == SCR_DYN_BYTES) { + size_t index; + return al_index(key, &index) && + (recv->kind == SCR_DYN_STR + ? (double)index < scr_str_utf16_len(recv->v.str) + : index < recv->v.bytes->len); + } + if (recv->kind == SCR_DYN_FUNC) { + ScrDyn *value = scr_dyn_fn_get(recv, key->data, key->len); + bool present = value != NULL; + scr_dyn_release(value); + return present; + } + return scr_dyn_has_key(recv, key); +} + +static ScrStr *al_key(size_t index) { + char text[32]; + int n = snprintf(text, sizeof text, "%zu", index); + return scr_str_new(text, (size_t)n); +} + +/* ToLength(Get(receiver, "length")). */ +static bool al_length(ScrDyn *recv, size_t *out) { + ScrStr *key = scr_str_new("length", 6); + ScrDyn *value = al_get(recv, key); + scr_str_release(key); + if (!value) return false; + double number; + bool ok = scr_dyn_number_coerce_js(value, &number); + scr_dyn_release(value); + if (!ok) return false; + if (isnan(number) || number <= 0) { *out = 0; return true; } + number = fmin(trunc(number), 9007199254740991.0); + if (number > (double)SIZE_MAX) { + static const char msg[] = "array-like length exceeds the native index range"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return false; + } + *out = (size_t)number; + return true; +} + +/* ToIntegerOrInfinity of an argument, truncated. */ +static bool al_number(const ScrDyn *value, double *out) { + if (!scr_dyn_number_coerce_js(value, out)) return false; + if (isnan(*out)) *out = 0; + else if (isfinite(*out)) *out = trunc(*out); + return true; +} + +static bool al_present(ScrDyn *recv, size_t index) { + ScrStr *key = al_key(index); + bool found = al_has(recv, key); + scr_str_release(key); + return found; +} + +static ScrDyn *al_item(ScrDyn *recv, size_t index) { + ScrStr *key = al_key(index); + ScrDyn *value = al_get(recv, key); + scr_str_release(key); + return value; +} + +/* A primitive receiver's ToObject wrapper is fresh and unobservable: the + * generic array algorithms read through it, and a write the wrapper accepts + * is discarded with it. */ +static bool al_primitive(const ScrDyn *recv) { + switch (recv->kind) { + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return true; + default: + return false; + } +} + +/* The "not supported yet" Error for a method whose result would be a + * primitive receiver's wrapper object. */ +static void al_refuse_primitive(const ScrStr *method) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Array.prototype."); + scr_jb_write(&b, method->data, method->len); + scr_jb_puts(&b, " on a primitive receiver is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); +} + +/* A String wrapper's indexes below its UTF-16 length are read-only and + * non-configurable, so Set throws on them and DeletePropertyOrThrow does + * too; an index at or past the length is an ordinary property of the + * wrapper. Number, boolean, bigint and symbol wrappers carry no index. */ +static bool al_string_index(const ScrDyn *recv, size_t index) { + return recv->kind == SCR_DYN_STR && (double)index < scr_str_utf16_len(recv->v.str); +} + +/* V8's TypeError for a write or delete the String wrapper refuses; `key` + * is the property's name. */ +static void al_string_refuse(const char *key, bool deleting) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, deleting ? "Cannot delete property '" : "Cannot assign to read only property '"); + scr_jb_puts(&b, key); + scr_jb_puts(&b, deleting ? "' of [object String]" : "' of object '[object String]'"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + +static void al_string_refuse_index(size_t index, bool deleting) { + char text[32]; + snprintf(text, sizeof text, "%zu", index); + al_string_refuse(text, deleting); +} + +/* Set(O, index, value, true): a failing write throws from scr_dyn_key_set + * and the caller checks scr_exc_pending(). */ +static void al_set(ScrDyn *recv, size_t index, ScrDyn *value) { + if (al_primitive(recv)) { + if (al_string_index(recv, index)) al_string_refuse_index(index, false); + return; + } + ScrStr *key = al_key(index); + scr_dyn_key_set(recv, key, value); + scr_str_release(key); +} + +/* DeletePropertyOrThrow(O, index). */ +static void al_delete(ScrDyn *recv, size_t index) { + if (al_primitive(recv)) { + if (al_string_index(recv, index)) al_string_refuse_index(index, true); + return; + } + ScrStr *key = al_key(index); + scr_dyn_key_delete(recv, key, true); + scr_str_release(key); +} + +/* Set(O, "length", length, true). A String wrapper's length is read-only + * whatever the value. */ +static void al_set_length(ScrDyn *recv, size_t length) { + if (al_primitive(recv)) { + if (recv->kind == SCR_DYN_STR) al_string_refuse("length", false); + return; + } + ScrStr *key = scr_str_new("length", 6); + ScrDyn *value = scr_dyn_new_num((double)length); + scr_dyn_key_set(recv, key, value); + scr_dyn_release(value); + scr_str_release(key); +} + +/* fill, copyWithin and reverse return O, which for a primitive receiver is + * its ToObject wrapper. A compiled program has no wrapper object to return, + * so for a primitive the method throws the "not supported yet" Error. */ +static ScrDyn *al_receiver_result(ScrDyn *recv, const ScrStr *method) { + if (!al_primitive(recv)) return scr_dyn_retain(recv); + al_refuse_primitive(method); + return NULL; +} + +/* The receiver's elements in index order as a fresh dyn array, each read + * with Get alone (an index the receiver does not carry reads undefined), as + * toSorted and with read through holes; the element at `skip` (SIZE_MAX: + * none) is `replacement` and is never read. Owned (+1), NULL with the + * exception pending. */ +static ScrDyn *al_snapshot(ScrDyn *recv, size_t len, size_t skip, ScrDyn *replacement) { + ScrDyn *out = scr_dyn_new_arr(); + for (size_t i = 0; i < len; i++) { + if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; } + ScrDyn *item = i == skip ? scr_dyn_retain(replacement) : al_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + scr_dyn_arr_push(out, item); + } + return out; +} + +/* CreateDataProperty(out, index, Get(receiver, index)) — an ordinary + * property read, with an absent index kept as a hole. */ +static bool al_copy_item(ScrDyn *out, ScrDyn *recv, size_t index) { + if (!al_present(recv, index)) { + if (scr_exc_pending()) return false; + scr_dyn_arr_push_hole(out); + return true; + } + ScrDyn *item = al_item(recv, index); + if (!item) return false; + scr_dyn_arr_push(out, item); + return true; +} + +/* JS relative-index normalization (slice's rule). */ +static size_t al_rel_index(double rel, size_t len) { + if (rel < 0) { + double r = (double)len + rel; + return r < 0 ? 0 : (size_t)r; + } + return rel > (double)len ? len : (size_t)rel; +} + +/* The callable-callback gate: JS's " is not a function". */ +static bool al_cb_check(ScrDyn *const *args, size_t argc) { + ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (scr_dyn_is_callable(cb)) return true; + ScrJsonBuf b; + scr_jb_init(&b); + ScrStr *text = scr_dyn_string_coerce_js(cb); + if (text) { + scr_jb_write(&b, text->data, text->len); + scr_str_release(text); + } + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return false; +} + +/* The methods this unit runs; every other name is the invoke unit's. */ +static bool al_owns(const ScrStr *method) { + static const char *const names[] = { + "concat", "copyWithin", "fill", "flat", "flatMap", "join", "pop", "push", "reverse", + "shift", "splice", "toReversed", "toSorted", "toSpliced", "unshift", "with", + }; + for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) + if (al_is(method, names[i])) return true; + return false; +} + +/* One packed argument list, borrowed: the array dispatch hands the + * algorithm its arguments as a dyn array. */ +ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { + if (recv->kind == SCR_DYN_ARR || + (recv->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(recv))) { + /* An array receiver runs this unit's methods through the array + * dispatch, which implements each of them for arrays. */ + char name[16]; + if (al_owns(method) && args && args->kind == SCR_DYN_ARR) { + memcpy(name, method->data, method->len); + name[method->len] = '\0'; + return scr_dyn_invoke(recv, name, args->v.arr.items, args->v.arr.len, name); + } + return scr_dyn_array_proto_call(recv, method, args); + } + /* Step 1 of each of these algorithms: `? ToObject(this value)`. The + * array dispatch answers it for its own receivers; a nullish one never + * reaches a length read. */ + if (recv->kind == SCR_DYN_NULL || recv->kind == SCR_DYN_UNDEF) { + static const char msg[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + if (!args || args->kind != SCR_DYN_ARR) scr_trap("scriptc: invalid Array.prototype arguments\n"); + ScrDyn *const *argv = args->v.arr.items; + size_t argc = args->v.arr.len; + if (al_is(method, "concat")) { + /* A receiver that is not an array is not spreadable: it is the result's + * first element, and its length is never read. The arguments then + * spread as they do for an array receiver, so the array dispatch + * answers `[receiver].concat(...args)`. A primitive receiver's first + * element would be its wrapper object. */ + if (al_primitive(recv)) { al_refuse_primitive(method); return NULL; } + ScrDyn *head = scr_dyn_new_arr(); + scr_dyn_arr_push(head, scr_dyn_retain(recv)); + ScrDyn *out = scr_dyn_invoke(head, "concat", argv, argc, "concat"); + scr_dyn_release(head); + return out; + } + size_t len; + if (!al_length(recv, &len)) return NULL; + + if (al_is(method, "pop")) { + if (len == 0) { + al_set_length(recv, 0); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *item = al_item(recv, len - 1); + if (!item) return NULL; + al_delete(recv, len - 1); + if (!scr_exc_pending()) al_set_length(recv, len - 1); + if (scr_exc_pending()) { scr_dyn_release(item); return NULL; } + return item; + } + if (al_is(method, "push") || al_is(method, "unshift")) { + if (al_is(method, "push")) { + for (size_t i = 0; i < argc; i++) { + if (len > 1000000 && !al_iteration(i)) return NULL; + al_set(recv, len + i, argv[i]); + if (scr_exc_pending()) return NULL; + } + al_set_length(recv, len + argc); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_num((double)(len + argc)); + } + if (argc > 0) { + /* Right to left, so each element lands one `argc` slot further. */ + for (size_t k = len; k > 0; k--) { + size_t from = k - 1; + size_t to = from + argc; + if (len > 1000000 && !al_iteration(len - k)) return NULL; + if (scr_exc_pending()) return NULL; + if (!al_present(recv, from)) { al_delete(recv, to); } + else { + ScrDyn *item = al_item(recv, from); + if (!item) return NULL; + al_set(recv, to, item); + scr_dyn_release(item); + } + if (scr_exc_pending()) return NULL; + } + for (size_t j = 0; j < argc; j++) { + al_set(recv, j, argv[j]); + if (scr_exc_pending()) return NULL; + } + } + al_set_length(recv, len + argc); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_num((double)(len + argc)); + } + if (al_is(method, "shift")) { + if (len == 0) { + al_set_length(recv, 0); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *first = al_item(recv, 0); + if (!first) return NULL; + for (size_t k = 1; k < len; k++) { + if (len > 1000000 && !al_iteration(k - 1)) { scr_dyn_release(first); return NULL; } + size_t to = k - 1; + if (scr_exc_pending()) { scr_dyn_release(first); return NULL; } + if (!al_present(recv, k)) { al_delete(recv, to); } + else { + ScrDyn *item = al_item(recv, k); + if (!item) { scr_dyn_release(first); return NULL; } + al_set(recv, to, item); + scr_dyn_release(item); + } + if (scr_exc_pending()) { scr_dyn_release(first); return NULL; } + } + al_delete(recv, len - 1); + if (!scr_exc_pending()) al_set_length(recv, len - 1); + if (scr_exc_pending()) { scr_dyn_release(first); return NULL; } + return first; + } + if (al_is(method, "reverse")) { + size_t middle = len / 2; + for (size_t lower = 0; lower < middle; lower++) { + if (len > 1000000 && !al_iteration(lower)) return NULL; + size_t upper = len - lower - 1; + bool has_lower = al_present(recv, lower); + if (scr_exc_pending()) return NULL; + ScrDyn *lower_value = has_lower ? al_item(recv, lower) : NULL; + if (has_lower && !lower_value) return NULL; + bool has_upper = al_present(recv, upper); + ScrDyn *upper_value = has_upper && !scr_exc_pending() ? al_item(recv, upper) : NULL; + if (scr_exc_pending() || (has_upper && !upper_value)) { + scr_dyn_release(lower_value); + return NULL; + } + if (has_lower && has_upper) { + al_set(recv, lower, upper_value); + if (!scr_exc_pending()) al_set(recv, upper, lower_value); + } else if (has_upper) { + al_set(recv, lower, upper_value); + if (!scr_exc_pending()) al_delete(recv, upper); + } else if (has_lower) { + al_delete(recv, lower); + if (!scr_exc_pending()) al_set(recv, upper, lower_value); + } + scr_dyn_release(lower_value); + scr_dyn_release(upper_value); + if (scr_exc_pending()) return NULL; + } + return al_receiver_result(recv, method); + } + if (al_is(method, "join")) { + ScrStr *sep = argc > 0 && argv[0]->kind != SCR_DYN_UNDEF + ? scr_dyn_string_coerce_js(argv[0]) + : scr_str_new(",", 1); + if (!sep) return NULL; + /* Get alone at each index: a missing index reads undefined, which + * joins as the empty string. */ + ScrJsonBuf buffer; + scr_jb_init(&buffer); + for (size_t i = 0; i < len; i++) { + if (i) scr_jb_write(&buffer, sep->data, sep->len); + ScrDyn *item = len > 1000000 && !al_iteration(i) ? NULL : al_item(recv, i); + if (!item) { + scr_str_release(scr_jb_finish(&buffer)); + scr_str_release(sep); + return NULL; + } + if (item->kind != SCR_DYN_UNDEF && item->kind != SCR_DYN_NULL) { + ScrStr *text = scr_dyn_string_coerce_js(item); + if (text) { + scr_jb_write(&buffer, text->data, text->len); + scr_str_release(text); + } + } + scr_dyn_release(item); + if (scr_exc_pending()) { + scr_str_release(scr_jb_finish(&buffer)); + scr_str_release(sep); + return NULL; + } + } + scr_str_release(sep); + ScrStr *text = scr_jb_finish(&buffer); + ScrDyn *answer = scr_dyn_new_str(text); + scr_str_release(text); + return answer; + } + if (al_is(method, "fill")) { + ScrDyn *value = argc > 0 ? argv[0] : scr_dyn_undefined(); + double start_d = 0; + double end_d = (double)len; + if (argc > 1 && !al_number(argv[1], &start_d)) return NULL; + if (argc > 2 && argv[2]->kind != SCR_DYN_UNDEF && !al_number(argv[2], &end_d)) return NULL; + size_t start = al_rel_index(trunc(start_d), len); + size_t end = al_rel_index(trunc(end_d), len); + for (size_t i = start; i < end; i++) { + if (len > 1000000 && !al_iteration(i - start)) return NULL; + al_set(recv, i, value); + if (scr_exc_pending()) return NULL; + } + return al_receiver_result(recv, method); + } + if (al_is(method, "copyWithin")) { + double target_d = 0; + double start_d = 0; + double end_d = (double)len; + if (argc > 0 && !al_number(argv[0], &target_d)) return NULL; + if (argc > 1 && !al_number(argv[1], &start_d)) return NULL; + if (argc > 2 && argv[2]->kind != SCR_DYN_UNDEF && !al_number(argv[2], &end_d)) return NULL; + size_t to = al_rel_index(trunc(target_d), len); + size_t from = al_rel_index(trunc(start_d), len); + size_t end = al_rel_index(trunc(end_d), len); + if (from >= end || to >= len) return al_receiver_result(recv, method); + size_t count = end - from < len - to ? end - from : len - to; + bool backwards = from < to && to < from + count; + for (size_t step = 0; step < count; step++) { + if (len > 1000000 && !al_iteration(step)) return NULL; + size_t i = backwards ? count - 1 - step : step; + size_t source = from + i; + size_t target = to + i; + if (!al_present(recv, source)) { al_delete(recv, target); } + else { + ScrDyn *item = al_item(recv, source); + if (!item) return NULL; + al_set(recv, target, item); + scr_dyn_release(item); + } + if (scr_exc_pending()) return NULL; + } + return al_receiver_result(recv, method); + } + if (al_is(method, "splice")) { + /* No arguments is the one form whose actualStart is len, not 0: it + * deletes nothing and only rewrites the same length. Starting at 0 + * would delete — and copy out — the whole receiver. */ + size_t start = len; + size_t removed = 0; + if (argc > 0) { + double start_d = 0; + if (!al_number(argv[0], &start_d)) return NULL; + start = al_rel_index(trunc(start_d), len); + removed = len - start; + if (argc > 1) { + double count_d = 0; + if (!al_number(argv[1], &count_d)) return NULL; + double clamped = count_d < 0 ? 0 : count_d; + removed = clamped > (double)removed ? removed : (size_t)clamped; + } + } + size_t items = argc > 2 ? argc - 2 : 0; + /* ArraySpeciesCreate(O, actualDeleteCount) runs before any index is + * read or written, and ArrayCreate refuses a length past 2^32-1. */ + if (removed > 4294967295u) { + static const char msg[] = "Invalid array length"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + ScrDyn *out = scr_dyn_new_arr(); + for (size_t i = 0; i < removed; i++) { + if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; } + if (!al_copy_item(out, recv, start + i)) { scr_dyn_release(out); return NULL; } + } + if (items < removed) { + for (size_t k = start; k < len - removed; k++) { + if (len > 1000000 && !al_iteration(k - start)) { scr_dyn_release(out); return NULL; } + size_t target = k + items; + if (!al_present(recv, k + removed)) { al_delete(recv, target); } + else { + ScrDyn *item = al_item(recv, k + removed); + if (!item) { scr_dyn_release(out); return NULL; } + al_set(recv, target, item); + scr_dyn_release(item); + } + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + } + for (size_t k = len; k > len - removed + items; k--) { + al_delete(recv, k - 1); + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + } + } else if (items > removed) { + for (size_t k = len - removed; k > start; k--) { + if (len > 1000000 && !al_iteration(len - k)) { scr_dyn_release(out); return NULL; } + size_t source = k + removed - 1; + size_t target = k + items - 1; + if (!al_present(recv, source)) { al_delete(recv, target); } + else { + ScrDyn *item = al_item(recv, source); + if (!item) { scr_dyn_release(out); return NULL; } + al_set(recv, target, item); + scr_dyn_release(item); + } + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + } + } + for (size_t j = 0; j < items; j++) { + al_set(recv, start + j, argv[2 + j]); + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + } + al_set_length(recv, len - removed + items); + if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } + return out; + } + if (al_is(method, "with")) { + double index_d = 0; + if (!al_number(argc > 0 ? argv[0] : scr_dyn_undefined(), &index_d)) return NULL; + double index = index_d < 0 ? (double)len + index_d : index_d; + if (index < 0 || index >= (double)len) { + static const char msg[] = "Invalid index"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + return al_snapshot(recv, len, (size_t)index, argc > 1 ? argv[1] : scr_dyn_undefined()); + } + if (al_is(method, "toReversed")) { + /* Get(O, len - k - 1) for k ascending: the reads go from the last + * index down. */ + ScrDyn *out = scr_dyn_new_arr(); + for (size_t k = 0; k < len; k++) { + if (len > 1000000 && !al_iteration(k)) { scr_dyn_release(out); return NULL; } + ScrDyn *item = al_item(recv, len - k - 1); + if (!item) { scr_dyn_release(out); return NULL; } + scr_dyn_arr_push(out, item); + } + return out; + } + if (al_is(method, "toSorted")) { + ScrDyn *cmp = argc > 0 ? argv[0] : scr_dyn_undefined(); + if (cmp->kind != SCR_DYN_UNDEF && !scr_dyn_is_callable(cmp)) { + static const char msg[] = "The comparison function must be either a function or undefined"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + ScrDyn *out = al_snapshot(recv, len, SIZE_MAX, NULL); + if (!out) return NULL; + if (out->v.arr.len < 2) return out; + ScrDyn *sorted = scr_dyn_invoke(out, "sort", argv, argc, "toSorted"); + scr_dyn_release(out); + return sorted; + } + if (al_is(method, "toSpliced")) { + double start_d = 0; + if (argc > 0 && !al_number(argv[0], &start_d)) return NULL; + size_t start = al_rel_index(trunc(start_d), len); + /* Without `start` nothing is skipped: the result is a copy. */ + size_t removed = argc > 0 ? len - start : 0; + if (argc > 1) { + double count_d = 0; + if (!al_number(argv[1], &count_d)) return NULL; + double clamped = count_d < 0 ? 0 : count_d; + removed = clamped > (double)removed ? removed : (size_t)clamped; + } + size_t items = argc > 2 ? argc - 2 : 0; + /* The result array is created up front: a length past 2^53-1 is a + * TypeError of its own, and ArrayCreate refuses anything past + * 2^32-1 with a RangeError — both before any index is read. */ + double new_len = (double)len - (double)removed + (double)items; + if (new_len > 9007199254740991.0) { + static const char msg[] = "Invalid array length"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + if (new_len > 4294967295.0) { + static const char msg[] = "Invalid array length"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + /* Every copied index is read with Get and defined on the result, so a + * missing one becomes an own undefined, not a hole. */ + ScrDyn *out = scr_dyn_new_arr(); + for (size_t i = 0; i < start; i++) { + if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; } + ScrDyn *item = al_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + scr_dyn_arr_push(out, item); + } + for (size_t j = 2; j < argc; j++) scr_dyn_arr_push(out, scr_dyn_retain(argv[j])); + for (size_t i = start + removed; i < len; i++) { + if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; } + ScrDyn *item = al_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + scr_dyn_arr_push(out, item); + } + return out; + } + if (al_is(method, "flat") || al_is(method, "flatMap")) { + /* FlattenIntoArray: after the length, flat converts its depth and + * flatMap checks its callback. Then each present index is read and, + * for flatMap, mapped, and its value is flattened into the result + * before the next index is read. The array dispatch flattens each + * value as `[value].flat(depth)`, so the flattening rules (nested + * arrays and wrapped array views) keep one implementation. */ + bool mapping = al_is(method, "flatMap"); + double depth = 1; + if (mapping) { + if (!al_cb_check(argv, argc)) return NULL; + } else if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) { + if (!al_number(argv[0], &depth)) return NULL; + if (depth < 0) depth = 0; + } + ScrDyn *depth_arg = scr_dyn_new_num(depth); + ScrDyn *out = scr_dyn_new_arr(); + for (size_t i = 0; i < len; i++) { + if (len > 1000000 && !al_iteration(i)) goto flat_failed; + if (!al_present(recv, i)) { + if (scr_exc_pending()) goto flat_failed; + continue; + } + ScrDyn *item = al_item(recv, i); + if (!item) goto flat_failed; + if (mapping) { + ScrDyn *index = scr_dyn_new_num((double)i); + ScrDyn *cbargs[3] = { item, index, recv }; + scr_dyn_this_push_dyn(argc > 1 ? argv[1] : scr_dyn_undefined()); + ScrDyn *result = scr_dyn_call(argv[0], cbargs, 3, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(index); + scr_dyn_release(item); + if (!result) goto flat_failed; + item = result; + } + ScrDyn *one = scr_dyn_new_arr(); + scr_dyn_arr_push(one, item); + ScrDyn *flattened = scr_dyn_invoke(one, "flat", &depth_arg, 1, mapping ? "flatMap" : "flat"); + scr_dyn_release(one); + if (!flattened) goto flat_failed; + for (size_t j = 0; j < flattened->v.arr.len; j++) + scr_dyn_arr_push(out, scr_dyn_retain(flattened->v.arr.items[j])); + scr_dyn_release(flattened); + } + scr_dyn_release(depth_arg); + return out; + flat_failed: + scr_dyn_release(depth_arg); + scr_dyn_release(out); + return NULL; + } + /* Every other name keeps the one implementation in the invoke unit + * (at, slice, indexOf, the higher-order methods, and the "not supported + * yet" answers). */ + return scr_dyn_array_proto_call(recv, method, args); +} diff --git a/packages/runtime/src/scr_dyn_builtin_prototype.c b/packages/runtime/src/scr_dyn_builtin_prototype.c new file mode 100644 index 000000000..1bb87746f --- /dev/null +++ b/packages/runtime/src/scr_dyn_builtin_prototype.c @@ -0,0 +1,170 @@ +/* Number.prototype and Boolean.prototype as values. + * + * A JavaScript source reading `Number.prototype` or `Boolean.prototype` + * used to fall through to an ordinary member read of the constructor + * value, which answered undefined: there was no prototype object to read. + * This unit builds the two prototype objects the way the language defines + * them — an ordinary object whose prototype is Object.prototype, carrying + * the kind's methods as non-enumerable, writable, configurable data + * properties — and answers them from one box per thread, so every read is + * the same object (`Number.prototype === Number.prototype`). + * + * A method called on the prototype object itself runs on the kind's empty + * primitive (`Number.prototype.toString(2)` is "0", `Boolean.prototype + * .valueOf()` is false), which is what Node's Number and Boolean prototype + * objects are: wrappers around +0 and false. The methods are not generic, + * so any other receiver — a plain object, null, a primitive of the other + * kind — is the TypeError Node answers. + * + * The unit links only into programs that read those values in a JavaScript + * source (the frontend marks dyn.builtinPrototype there), so typed programs + * keep their exact executables and no always-linked unit changes. + * + * Wrapper objects (`new Number(1)`, the primitiveWrapper unit) build a + * prototype object of their own for their instances' chain. A program that + * does both should see ONE object: when that unit lands, this builder + * should answer its prototype for a program that links it rather than + * building a second one. */ +#include "scr_runtime.h" +#include "scr_dyn_methods.h" + +#include +#include + +/* The methods each prototype declares, with Node's `length` values. */ +static const struct { const char *name; uint32_t length; } bp_number_methods[] = { + {"toString", 1}, + {"toFixed", 1}, + {"toExponential", 1}, + {"toPrecision", 1}, + {"valueOf", 0}, +}; +static const struct { const char *name; uint32_t length; } bp_boolean_methods[] = { + {"toString", 0}, + {"valueOf", 0}, +}; + +static SCR_TL ScrDyn *bp_number_prototype; +static SCR_TL ScrDyn *bp_boolean_prototype; + +static void bp_prototypes_cleanup(void) { + scr_dyn_release(bp_number_prototype); + scr_dyn_release(bp_boolean_prototype); + bp_number_prototype = NULL; + bp_boolean_prototype = NULL; +} + +/** A prototype method of the kind: caps[0] is the kind's name ("Number" or + * "Boolean") and caps[1] the method name. The method runs on the receiver's + * primitive value as `this`: the receiver itself when the call passed a + * primitive of the kind (`Number.prototype.toString.call(255)`), else the + * empty primitive (+0 / false) the prototype object wraps, which is its + * [[PrimitiveValue]] in Node too. Boolean's two methods are answered here, + * since the checked-dynamic dispatch keeps no Boolean prototype table. */ +/** The prototype methods are not generic: each one reads the receiver's + * [[NumberData]] / [[BooleanData]], so only a primitive of the kind or its + * prototype object — the wrapper around +0 / false — is a valid `this`. + * Anything else, another primitive included, is a TypeError in Node. */ +static void bp_bad_this(const ScrStr *kind, const ScrStr *method) { + char message[128]; + snprintf(message, sizeof message, "%s.prototype.%s requires that 'this' be a %s", + kind->data, method->data, kind->data); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +static ScrDyn *bp_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + /* Both reads are +1, so the two strings are released below. */ + ScrStr *kind = scr_box_get_ref(closure->caps[0]); + ScrStr *method = scr_box_get_ref(closure->caps[1]); + ScrDyn *receiver = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (!strcmp(kind->data, "Boolean")) { + if (receiver->kind != SCR_DYN_BOOL && receiver != bp_boolean_prototype) { + bp_bad_this(kind, method); + scr_dyn_release(receiver); + scr_str_release(kind); + scr_str_release(method); + return NULL; + } + bool value = receiver->kind == SCR_DYN_BOOL ? receiver->v.b : false; + if (!strcmp(method->data, "valueOf")) { + result = scr_dyn_new_bool(value); + } else { + const char *text = value ? "true" : "false"; + ScrStr *string = scr_str_new(text, strlen(text)); + result = scr_dyn_new_str(string); + scr_str_release(string); + } + } else if (receiver->kind != SCR_DYN_NUM && receiver != bp_number_prototype) { + bp_bad_this(kind, method); + scr_dyn_release(receiver); + scr_str_release(kind); + scr_str_release(method); + return NULL; + } else { + ScrDyn *self = receiver->kind == SCR_DYN_NUM ? scr_dyn_retain(receiver) : scr_dyn_new_num(0); + if (!strcmp(method->data, "toString")) { + /* Number.prototype.toString is the radix form, which the number + * dispatch answers through its own helper rather than as a method. */ + ScrStr *value = scr_dyn_to_string_argument(self, argc ? args[0] : scr_dyn_undefined(), method); + if (value) { + result = scr_dyn_new_str(value); + scr_str_release(value); + } + } else { + result = scr_dyn_invoke(self, method->data, args, argc, method->data); + } + scr_dyn_release(self); + } + scr_dyn_release(receiver); + scr_str_release(kind); + scr_str_release(method); + return result; +} + +/** Defines one non-enumerable data property on `object`: the attributes + * every built-in method has ({ writable, configurable }, not enumerable). */ +static void bp_define(ScrDyn *object, const char *name, size_t name_len, ScrDyn *value) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, value); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + ScrStr *key = scr_str_new(name, name_len); + ScrDyn *key_value = scr_dyn_new_str(key); + ScrDyn *defined = scr_dyn_define_property(object, key_value, descriptor); + scr_dyn_release(defined); + scr_dyn_release(key_value); + scr_dyn_release(descriptor); + scr_str_release(key); +} + +/** The kind's prototype object, built on first use; +1, one per thread (the + * slot keeps its own reference, as `scr_dyn_array_prototype_base` does). + * `kind` is "Number" or "Boolean" (the only two the frontend marks). */ +ScrDyn *scr_dyn_builtin_prototype(const ScrStr *kind) { + bool number = !strcmp(kind->data, "Number"); + ScrDyn **slot = number ? &bp_number_prototype : &bp_boolean_prototype; + if (*slot) return scr_dyn_retain(*slot); + ScrDyn *prototype = scr_dyn_new_obj(); + *slot = prototype; + scr_atexit(bp_prototypes_cleanup); + ScrStr *kind_str = scr_str_new(kind->data, strlen(kind->data)); + const struct { const char *name; uint32_t length; } *methods = + number ? bp_number_methods : bp_boolean_methods; + size_t count = number ? sizeof bp_number_methods / sizeof bp_number_methods[0] + : sizeof bp_boolean_methods / sizeof bp_boolean_methods[0]; + for (size_t i = 0; i < count; i++) { + ScrStr *name = scr_str_new(methods[i].name, strlen(methods[i].name)); + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->caps[0] = scr_box_new(SCR_BOX_STR); + closure->caps[1] = scr_box_new(SCR_BOX_STR); + scr_box_set_ref(closure->caps[0], scr_str_retain(kind_str)); + scr_box_set_ref(closure->caps[1], scr_str_retain(name)); + ScrDyn *method = scr_dyn_new_func(closure, bp_method_call, methods[i].length, + number ? "native:Number.prototype" : "native:Boolean.prototype", methods[i].name); + bp_define(prototype, methods[i].name, strlen(methods[i].name), method); + scr_str_release(name); + } + scr_str_release(kind_str); + return scr_dyn_retain(prototype); +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index a560f09d8..042b59bf9 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15623,6 +15623,73 @@ typedef struct ScrScParent { const struct ScrScParent *up; } ScrScParent; +static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region); + +static const char scr_sc_cycle_message[] = + "structuredClone of cyclic values (the checked-dynamic tree cannot represent cycles) is not supported yet"; + +/* The clone a handle's clone hook runs inside: its parent chain and region, + * so a Map or Set clones its entries with the cycle check and region of the + * value that holds it. */ +static SCR_TL struct { + const ScrScParent *up; + ScrDynRegion *region; + /* scr_sc_clone, reached through the context so that the Map and Set + * handle ops do not link the structured-clone code into programs that + * never clone (volatile, so the call is not resolved at compile time). */ + ScrDyn *(*volatile clone)(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region); +} scr_sc_nested; + +/* A Map or Set clones entry by entry, cloning each key and value. The copy + * keeps the source's storage kinds and typed slot adapters, so a typed + * collection checks back into its own type; a typed slot converts the + * cloned value into a fresh element. The Map and Set handle ops install it + * as their clone hook. */ +static ScrMap *scr_sc_clone_collection(ScrMap *source, bool is_map) { + const ScrScParent *up = scr_sc_nested.up; + ScrDynRegion *region = scr_sc_nested.region; + ScrDyn *(*clone)(const ScrDyn *, const ScrScParent *, ScrDynRegion *) = scr_sc_nested.clone; + ScrMap *out = scr_map_new((ScrMapKeyKind)source->key_kind, (ScrMapValKind)source->val_kind, source->ops); + out->dyn_ops = source->dyn_ops; + scr_map_iter_enter(source); + for (double i = 0; i < scr_map_iter_count(source); i++) { + if (!scr_map_iter_live(source, i)) continue; + ScrDyn *key = scr_map_dyn_key(source, i); + ScrDyn *key_copy = clone(key, up, region); + scr_dyn_release(key); + ScrDyn *value_copy = NULL; + if (key_copy && is_map) { + ScrDyn *value = scr_map_dyn_value(source, i); + value_copy = clone(value, up, region); + scr_dyn_release(value); + } + if (key_copy && (value_copy || !is_map)) scr_map_dyn_set(out, key_copy, value_copy, !is_map); + scr_dyn_release(key_copy); + scr_dyn_release(value_copy); + if (scr_exc_pending()) break; + } + scr_map_iter_exit(source); + if (scr_exc_pending()) { + scr_map_release(out); + return NULL; + } + return out; +} + +static ScrDyn *scr_native_map_clone(void *ptr) { + ScrMap *copy = scr_sc_clone_collection(ptr, true); + ScrDyn *result = copy ? scr_dyn_native_map(copy) : NULL; + scr_map_release(copy); + return result; +} + +static ScrDyn *scr_native_set_clone(void *ptr) { + ScrMap *copy = scr_sc_clone_collection(ptr, false); + ScrDyn *result = copy ? scr_dyn_native_set(copy) : NULL; + scr_map_release(copy); + return result; +} + /* The clone is a fresh tree (shared inputs are copied per path, cycles * throw), so it is tagged like a parse result (ScrDynRegion); a leaf the * region may not hold clears it. */ @@ -15740,7 +15807,25 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion case SCR_DYN_HANDLE: if (scr_dyn_handle_ops_of(v)->clone) { region->intact = false; /* a handle is not a leaf the region may hold */ - return scr_dyn_handle_ops_of(v)->clone(v->v.handle.ptr); + /* A handle that holds values (a Map or Set) clones them with this + * value as their parent; one that holds its own storage is cut off + * by its parent's identity. */ + for (const ScrScParent *p = up; p != NULL; p = p->up) { + if (p->node->kind == SCR_DYN_HANDLE && p->node->v.handle.ptr == v->v.handle.ptr) { + scr_throw_error_msg(SCR_ERR_ERROR, scr_sc_cycle_message, sizeof scr_sc_cycle_message - 1); + return NULL; + } + } + ScrScParent self = {v, up}; + const ScrScParent *saved_up = scr_sc_nested.up; + ScrDynRegion *saved_region = scr_sc_nested.region; + scr_sc_nested.up = &self; + scr_sc_nested.region = region; + scr_sc_nested.clone = scr_sc_clone; + ScrDyn *out = scr_dyn_handle_ops_of(v)->clone(v->v.handle.ptr); + scr_sc_nested.up = saved_up; + scr_sc_nested.region = saved_region; + return out; } /* fall through */ case SCR_DYN_FUNC: @@ -19144,6 +19229,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) { .iter_pack = &scr_native_set_pack, /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ .json_empty_object = true, + .clone = &scr_native_set_clone, }; scr_dyn_handle_install(SCR_DYNH_SET, &ops); return scr_dyn_new_handle(value, SCR_DYNH_SET); @@ -19261,6 +19347,7 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) { .iter_pack = &scr_native_map_pack, /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ .json_empty_object = true, + .clone = &scr_native_map_clone, }; scr_dyn_handle_install(SCR_DYNH_MAP, &ops); return scr_dyn_new_handle(value, SCR_DYNH_MAP); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index eb4af774f..db2aef965 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -357,6 +357,8 @@ export const DYNAMIC_TIER_PREFIXES = [ "scr_dyn_call", "scr_dyn_apply", "scr_dyn_invoke", + "scr_dyn_array_like", + "scr_dyn_builtin_prototype", "scr_dyn_construct", "scr_dyn_function_apply", "scr_dyn_fn_get", @@ -406,7 +408,9 @@ export const ICU_CPP_NAMESPACE = "6icu_78"; * spells `6icu_78` wherever the name occurs — as the function's own * namespace (`_ZN6icu_78...`), in its parameter types, in vtables, typeinfo * and guard variables, or in a libc++ instantiation over an ICU type. No - * symbol outside the archive carries either mark. + * symbol outside the archive carries either mark, except emitted program + * functions (`sc_` names), whose numbered helpers can end in `_78` + * (`sc_f__x25_dyn_class_property_78`); the suffix test skips them. * * scr_intl.c's own functions are deliberately not tested by name. The * engine-free Intl paths share their prefix (`scr_intl_num_format_en_us` in @@ -420,7 +424,9 @@ export function icuSymbols(names) { return [...new Set(names)] .filter( (name) => - name.endsWith("_78") || name.includes(ICU_CPP_NAMESPACE) || name.startsWith("icudt78"), + (name.endsWith("_78") && !name.startsWith("sc_")) || + name.includes(ICU_CPP_NAMESPACE) || + name.startsWith("icudt78"), ) .sort(); } diff --git a/tests/corpus/js-array-proto-array-likes.js b/tests/corpus/js-array-proto-array-likes.js new file mode 100644 index 000000000..79a008b49 --- /dev/null +++ b/tests/corpus/js-array-proto-array-likes.js @@ -0,0 +1,187 @@ +// Array.prototype's methods are generic: they run over any object with a +// length, reading and writing ordinary properties. A call on such an +// object used to answer "not supported yet" for every method other than +// at/slice/indexOf/includes and the callback methods. +const resolved = []; +const report = (label, value) => resolved.push(label + '=' + value); + +const box = { length: 3, 0: 'a', 1: 'b', 2: 'c' }; +report('join', Array.prototype.join.call(box, '-')); +report('push', Array.prototype.push.call(box, 'd')); +report('after-push', Array.prototype.join.call(box, '')); +report('pop', Array.prototype.pop.call(box)); +report('length', box.length); +report('shift', Array.prototype.shift.call(box)); +report('unshift', Array.prototype.unshift.call(box, 'x', 'y')); +report('after-unshift', Array.prototype.join.call(box, ',')); +report('reverse', Array.prototype.join.call(Array.prototype.reverse.call(box), '/')); +report('concat', Array.prototype.concat.call(box, ['z']).join('+')); +report('slice', Array.prototype.slice.call(box).join('|')); + +const filler = { length: 4, 0: 1, 1: 2, 2: 3, 3: 4 }; +report('fill', Array.prototype.join.call(Array.prototype.fill.call(filler, 0, 1, 3), '')); +report('copyWithin', Array.prototype.join.call(Array.prototype.copyWithin.call(filler, 0, 2), '')); + +const spliced = { length: 4, 0: 'a', 1: 'b', 2: 'c', 3: 'd' }; +report('splice', Array.prototype.splice.call(spliced, 1, 2, 'X').join('')); +report('after-splice', Array.prototype.join.call(spliced, '')); +report('splice-none', Array.prototype.splice.call(spliced).length); + +const ordered = { length: 3, 0: 3, 1: 1, 2: 2 }; +report('toSorted', Array.prototype.toSorted.call(ordered).join('')); +report('toReversed', Array.prototype.toReversed.call(ordered).join('')); +report('toSpliced', Array.prototype.toSpliced.call(ordered, 1, 1).join('')); +report('with', Array.prototype.with.call(ordered, 0, 9).join('')); +report('with-negative', Array.prototype.with.call(ordered, -1, 9).join('')); +report('unsorted', Array.prototype.join.call(ordered, '')); + +const nested = { length: 3, 0: [1, 2], 1: 3, 2: [4, [5]] }; +report('flat', Array.prototype.flat.call(nested, 1).join('')); +report('flat-deep', Array.prototype.flat.call(nested, 2).join('')); +report('flatMap', Array.prototype.flatMap.call(nested, (value) => [value, value]).length); + +// toReversed defines every index on its result, so a hole becomes undefined. +const sparse = { length: 3, 0: 'a', 2: 'c' }; +const copied = Array.prototype.toReversed.call(sparse); +report('holes', '1' in copied ? 'present' : 'absent'); +report('holes-join', copied.join('')); + +// Writes beyond the receiver's length extend it, as the length assignment +// at the end of push/splice does. +const extended = { length: 1, 0: 'only' }; +Array.prototype.push.call(extended, 'next'); +report('extended', JSON.stringify(extended)); + +// Every one of the algorithms starts with `? ToObject(this value)`, so a +// nullish receiver is a TypeError before any length is read. +const attempt = (label, run) => { + try { + report(label, 'no-throw ' + JSON.stringify(run())); + } catch (error) { + report(label, error.constructor.name + ': ' + error.message); + } +}; +attempt('null-join', () => Array.prototype.join.call(null)); +attempt('undefined-push', () => Array.prototype.push.call(undefined, 1)); +attempt('null-copyWithin', () => Array.prototype.copyWithin.call(null, 0, 0)); +attempt('undefined-fill', () => Array.prototype.fill.call(undefined, 0)); +attempt('null-toReversed', () => Array.prototype.toReversed.call(null)); +attempt('undefined-with', () => Array.prototype.with.call(undefined, 0, 1)); +attempt('null-splice', () => Array.prototype.splice.call(null, 0, 1)); + +// A proxy over `target` that logs its string-keyed reads and its has checks. +const traced = (target) => { + const log = []; + const proxy = new Proxy(target, { + get(object, key, receiver) { + if (typeof key === 'string') log.push('get:' + key); + return Reflect.get(object, key, receiver); + }, + has(object, key) { + log.push('has:' + String(key)); + return key in object; + }, + }); + return { proxy, log }; +}; +// concat appends a receiver that is not spreadable as one value, without +// reading its length. +const unread = traced({ length: 2, 0: 'a', 1: 'b' }); +const joined = Array.prototype.concat.call(unread.proxy, [1]); +report('concat-length', joined.length); +report('concat-reads', unread.log.join(',')); + +// flatMap calls the callback with thisArg as `this`. +const tagged = { length: 2, 0: 'a', 1: 'b' }; +report('flatMap-this', Array.prototype.flatMap.call(tagged, function (value) { + return [value, this.tag]; +}, { tag: 't' }).join('')); + +// A string's ToObject wrapper has read-only indexes below its length and a +// read-only length, so the mutating methods throw on them. +attempt('string-push', () => Array.prototype.push.call('ab', 'c')); +attempt('string-pop', () => Array.prototype.pop.call('ab')); +attempt('string-pop-empty', () => Array.prototype.pop.call('')); +attempt('string-shift', () => Array.prototype.shift.call('ab')); +attempt('string-unshift', () => Array.prototype.unshift.call('ab', 'x')); +attempt('string-unshift-none', () => Array.prototype.unshift.call('ab')); +attempt('string-reverse', () => Array.prototype.reverse.call('ab')); +attempt('string-fill', () => Array.prototype.fill.call('ab', 'x')); +attempt('string-copyWithin', () => Array.prototype.copyWithin.call('abc', 0, 1)); +attempt('string-splice', () => Array.prototype.splice.call('abc', 1, 1)); +attempt('string-splice-insert', () => Array.prototype.splice.call('', 0, 0, 'x')); +attempt('string-splice-none', () => Array.prototype.splice.call('abc')); +// Other primitives' wrappers carry no indexes and a writable length. +attempt('number-push', () => Array.prototype.push.call(5, 'x')); +attempt('boolean-pop', () => Array.prototype.pop.call(true)); + +// toReversed reads with Get alone, from the last index down. +const backwards = traced({ length: 3, 0: 'a', 2: 'c' }); +report('toReversed', JSON.stringify(Array.prototype.toReversed.call(backwards.proxy))); +report('toReversed-reads', backwards.log.join(',')); + +// with never reads the replaced index, and reads every other one with Get. +const replaced = traced({ length: 3, 0: 'a', 2: 'c' }); +report('with', JSON.stringify(Array.prototype.with.call(replaced.proxy, 0, 'z'))); +report('with-reads', replaced.log.join(',')); + +// toSorted reads every index with Get, holes included. +const sorted = traced({ length: 3, 0: 'b', 2: 'a' }); +report('toSorted', JSON.stringify(Array.prototype.toSorted.call(sorted.proxy))); +report('toSorted-reads', sorted.log.join(',')); + +// toSpliced without arguments copies the receiver; a missing index becomes +// an own undefined, read with Get alone. +const copiedOut = traced({ length: 3, 0: 'a', 2: 'c' }); +const copy = Array.prototype.toSpliced.call(copiedOut.proxy); +report('toSpliced-none', JSON.stringify(copy) + ' ' + ('1' in copy)); +report('toSpliced-none-reads', copiedOut.log.join(',')); +const skipped = traced({ length: 4, 0: 'a', 3: 'd' }); +report('toSpliced-holes', JSON.stringify(Array.prototype.toSpliced.call(skipped.proxy, 1, 1, 'X'))); +report('toSpliced-reads', skipped.log.join(',')); + +// An array receiver held as an untyped value runs the array methods. +const either = (useArray) => (useArray ? ['p', 'q'] : { length: 0 }); +const list = either(resolved.length > 0); +report('array-push', Array.prototype.push.call(list, 'r')); +report('array-reverse', Array.prototype.join.call(Array.prototype.reverse.call(list), '')); +report('array-concat', Array.prototype.concat.call(list, ['s'], 't').join('')); +report('array-toSpliced', Array.prototype.toSpliced.call(list, 0, 1).join('')); +report('array-with', Array.prototype.with.call(list, 0, 'z').join('')); + +// join reads every index with Get alone; a has trap never runs. +const joinedLog = traced({ length: 3, 0: 'a', 2: 'c' }); +report('join', Array.prototype.join.call(joinedLog.proxy, '-')); +report('join-reads', joinedLog.log.join(',')); +const hidden = new Proxy({ length: 2, 0: 'x', 1: 'y' }, { has() { throw new Error('has trap'); } }); +report('join-has-trap', Array.prototype.join.call(hidden, '')); + +// flat converts its depth before it reads any index. +const order = []; +const depthSource = { length: 2, get 0() { order.push('read 0'); return [1, [2]]; }, 1: 3 }; +const depth = { valueOf() { order.push('depth'); return 2; } }; +report('flat-depth', JSON.stringify(Array.prototype.flat.call(depthSource, depth))); +report('flat-order', order.join(',')); +const failing = { length: 1, get 0() { order.push('never'); return 1; } }; +try { + Array.prototype.flat.call(failing, { valueOf() { throw new Error('depth failed'); } }); +} catch (error) { + report('flat-depth-throws', error.message + ' ' + order.includes('never')); +} + +// flatMap flattens each callback's result before the next callback runs. +const shared = [1, 2]; +const mapped = Array.prototype.flatMap.call({ length: 2, 0: 'a', 1: 'b' }, (value) => { + if (value === 'a') return shared; + shared[0] = 9; + return []; +}); +report('flatMap-order', JSON.stringify(mapped)); +// flat also flattens a nested array when its index is read. +const nestedArray = [1, 2]; +const nestedSource = { length: 2, 0: nestedArray, get 1() { nestedArray[0] = 9; return 3; } }; +report('flat-nested-order', JSON.stringify(Array.prototype.flat.call(nestedSource))); +report('flat-negative', JSON.stringify(Array.prototype.flat.call({ length: 1, 0: [1, [2]] }, -1))); +report('flat-infinity', JSON.stringify(Array.prototype.flat.call({ length: 1, 0: [1, [2, [3]]] }, Infinity))); + +console.log(resolved.join('\n')); diff --git a/tests/corpus/js-function-name-inference.js b/tests/corpus/js-function-name-inference.js new file mode 100644 index 000000000..bf82ae1b6 --- /dev/null +++ b/tests/corpus/js-function-name-inference.js @@ -0,0 +1,33 @@ +// V8 names a function by its definition: an anonymous function or class +// expression takes the name of the binding or property it initializes, and +// every other value keeps the name it already has. A builtin function bound +// to a variable is therefore still `String`, not the variable's name. +var FromVar = String; +console.log(FromVar.name, FromVar.length, FromVar === String); +let FromLet = Number; +console.log(FromLet.name, FromLet === Number); +const FromConst = Boolean; +console.log(FromConst.name); +var Chained = FromVar; +console.log(Chained.name); +let ThroughParens = (Number); +console.log(ThroughParens.name); +console.log(String.name, Number.name, Boolean.name); + +function declared() {} +var Held = declared; +console.log(Held.name); + +// Anonymous definitions still take the binding's name. +var Anonymous = function () {}; +console.log(Anonymous.name); +let Arrow = () => {}; +console.log(Arrow.name); +const Parenthesized = (function () {}); +console.log(Parenthesized.name); +const Class = class {}; +console.log(Class.name); +var Named = function inner() {}; +console.log(Named.name); +const NamedClass = class InnerClass {}; +console.log(NamedClass.name); diff --git a/tests/corpus/js-function-name-module-alias/main.js b/tests/corpus/js-function-name-module-alias/main.js new file mode 100644 index 000000000..f1b274db6 --- /dev/null +++ b/tests/corpus/js-function-name-module-alias/main.js @@ -0,0 +1,7 @@ +// A function imported under a different name keeps the name of the binding +// that created it: V8's SetFunctionName runs once, at the definition. +import builder from "./names.js"; + +console.log(builder.name); +const alias = builder; +console.log(alias.name); diff --git a/tests/corpus/js-function-name-module-alias/names.js b/tests/corpus/js-function-name-module-alias/names.js new file mode 100644 index 000000000..842e8c28b --- /dev/null +++ b/tests/corpus/js-function-name-module-alias/names.js @@ -0,0 +1,3 @@ +const root = (text) => text; + +export default root; diff --git a/tests/corpus/js-function-name-module-alias/package.json b/tests/corpus/js-function-name-module-alias/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/corpus/js-function-name-module-alias/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/corpus/js-primitive-prototype-values.js b/tests/corpus/js-primitive-prototype-values.js new file mode 100644 index 000000000..cd21f855b --- /dev/null +++ b/tests/corpus/js-primitive-prototype-values.js @@ -0,0 +1,37 @@ +// Number.prototype and Boolean.prototype are objects in JavaScript: the +// wrappers around +0 and false, carrying their methods as non-enumerable +// data properties. Reading one used to answer undefined. +console.log(typeof Number.prototype, typeof Boolean.prototype); +console.log(Number.prototype === Number.prototype, Boolean.prototype === Boolean.prototype); +console.log(Number.prototype.toString(), Number.prototype.toString(2), Number.prototype.toFixed(2)); +console.log(Number.prototype.valueOf(), Boolean.prototype.valueOf(), Boolean.prototype.toString()); +console.log(Object.getPrototypeOf(Number.prototype) === Object.prototype); +console.log(Object.getPrototypeOf(Boolean.prototype) === Object.prototype); +console.log(Number.prototype.hasOwnProperty('toString'), Number.prototype.hasOwnProperty('nope')); +console.log(Object.keys(Number.prototype).length, Object.keys(Boolean.prototype).length); +console.log(typeof Number.prototype.toString, Number.prototype.toString.name, Number.prototype.toString.length); +console.log(Boolean.prototype.valueOf.name, Boolean.prototype.toString.name, Boolean.prototype.toString.length); + +// The methods are not generic: only a primitive of the kind, or the +// prototype object itself (the wrapper around +0 / false), is a `this`. +const attempt = (label, run) => { + try { + console.log(label, 'no-throw', JSON.stringify(run())); + } catch (error) { + console.log(label, error.constructor.name, error.message); + } +}; +attempt('num-null', () => Number.prototype.toString.call(null)); +attempt('num-object', () => Number.prototype.toString.call({})); +attempt('num-string', () => Number.prototype.valueOf.call('5')); +attempt('num-bool', () => Number.prototype.toFixed.call(true)); +attempt('num-number', () => Number.prototype.toString.call(255, 16)); +attempt('bool-object', () => Boolean.prototype.valueOf.call({})); +attempt('bool-number', () => Boolean.prototype.toString.call(1)); +attempt('bool-boolean', () => Boolean.prototype.valueOf.call(true)); +const holder = new Object(); +holder.toString = Number.prototype.toString; +attempt('holder', () => holder.toString()); +const other = new Object(); +other.myValueOf = Number.prototype.valueOf; +attempt('holder-valueOf', () => other.myValueOf()); diff --git a/tests/corpus/structured-clone-collections.ts b/tests/corpus/structured-clone-collections.ts new file mode 100644 index 000000000..5f6f11c7f --- /dev/null +++ b/tests/corpus/structured-clone-collections.ts @@ -0,0 +1,71 @@ +// structuredClone of Maps and Sets, alone and in fields of typed records: +// keys and values are cloned, the copy is a new collection of the same +// type, and Dates, typed arrays and nested records in the same records +// clone too. +const m = new Map([ + ["a", 1], + ["b", 2], +]); +const mc = structuredClone(m); +mc.set("c", 3); +console.log(mc.get("a"), mc.size, m.size, mc !== m, [...mc.keys()].join(",")); +console.log(structuredClone({ m }).m.get("b")); + +const s = new Set([1, 2, 3]); +const sc = structuredClone(s); +sc.delete(1); +console.log(sc.has(2), sc.size, s.size, [...sc].join(",")); +console.log(structuredClone({ s }).s.has(3)); + +const d = new Date(86400000); +console.log(structuredClone({ d }).d.toISOString(), structuredClone(d).getTime()); + +const t = new Uint8Array([1, 2, 3]); +console.log(structuredClone({ t }).t[2], structuredClone(t).length); + +// Record and array values are cloned, not shared. +type Item = { id: number; tags: string[] }; +const items = new Map([["x", { id: 7, tags: ["red"] }]]); +const itemsCopy = structuredClone(items); +itemsCopy.get("x")!.tags.push("blue"); +console.log(items.get("x")!.tags.join(","), itemsCopy.get("x")!.tags.join(","), itemsCopy.get("x")!.id); + +// Number, boolean and string keys keep their types. +const byNumber = new Map([ + [1, "one"], + [-0, "zero"], +]); +const byNumberCopy = structuredClone(byNumber); +console.log(byNumberCopy.get(1), byNumberCopy.get(0), byNumberCopy.has(2)); +const flags = new Set([true]); +console.log(structuredClone(flags).has(true), structuredClone(flags).has(false)); + +// Collections nested in collections and records. +type Outer = { inner: { m: Map; when: Date }; tags: Set; bytes: Float64Array }; +const outer: Outer = { + inner: { m: new Map([["x", 5]]), when: new Date(0) }, + tags: new Set(["t"]), + bytes: new Float64Array([1.5]), +}; +const outerCopy = structuredClone(outer); +console.log( + outerCopy.inner.m.get("x"), + outerCopy.inner.when.getTime(), + outerCopy.tags.has("t"), + outerCopy.bytes[0], + outerCopy.inner.m !== outer.inner.m, +); +const groups = new Map>([["evens", new Set([2, 4])]]); +const groupsCopy = structuredClone(groups); +groupsCopy.get("evens")!.add(6); +console.log(groups.get("evens")!.size, groupsCopy.get("evens")!.size); + +// An empty collection, and an entry that cannot be cloned. +console.log(structuredClone(new Map()).size, structuredClone(new Set()).size); +const handlers = new Map void>([["run", () => {}]]); +try { + structuredClone(handlers); + console.log("cloned"); +} catch (error) { + console.log((error as Error).name); +} diff --git a/tests/harness/array-like-primitive-refusals.test.ts b/tests/harness/array-like-primitive-refusals.test.ts new file mode 100644 index 000000000..c4523e30a --- /dev/null +++ b/tests/harness/array-like-primitive-refusals.test.ts @@ -0,0 +1,57 @@ +import { execFile } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); + +// fill, copyWithin and reverse return their receiver, and concat's result +// starts with it: for a primitive receiver that is its wrapper object, which +// a compiled program has no value for, so these calls refuse with a catchable +// error. A write the String wrapper refuses still throws its TypeError first. +test("Array.prototype methods whose result is a primitive's wrapper refuse", async () => { + const outDir = mkdtempSync(join(tmpdir(), "scriptc-array-like-refusals-")); + const entry = join(outDir, "main.js"); + writeFileSync( + entry, + ` +function attempt(run) { + try { run(); console.log("returned"); } catch (error) { console.log(error.name, error.message); } +} +attempt(() => Array.prototype.reverse.call("a")); +attempt(() => Array.prototype.fill.call(5, 0)); +attempt(() => Array.prototype.fill.call("ab", "x", 2)); +attempt(() => Array.prototype.copyWithin.call(true, 0, 0)); +attempt(() => Array.prototype.concat.call("ab", [1])); +attempt(() => Array.prototype.reverse.call("ab")); +attempt(() => Array.prototype.join.call("ab", "-")); +`, + ); + const result = await compile(entry, { + backend: "llvm", + sanitize: process.env["SCRIPTC_SAN"] === "1", + outDir, + outPath: join(outDir, "program"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" }); + const refused = (method: string): string => + `Error Array.prototype.${method} on a primitive receiver is not supported yet`; + expect(stdout).toBe( + [ + refused("reverse"), + refused("fill"), + refused("fill"), + refused("copyWithin"), + refused("concat"), + "TypeError Cannot assign to read only property '0' of object '[object String]'", + "returned", + "", + ].join("\n"), + ); + expect(stderr).toBe(""); +}); diff --git a/tests/harness/corpus-costs/js-array-proto-array-likes.js.json b/tests/harness/corpus-costs/js-array-proto-array-likes.js.json new file mode 100644 index 000000000..6d743409f --- /dev/null +++ b/tests/harness/corpus-costs/js-array-proto-array-likes.js.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 6200 } diff --git a/tests/harness/corpus-costs/js-function-name-inference.js.json b/tests/harness/corpus-costs/js-function-name-inference.js.json new file mode 100644 index 000000000..0477ef4d5 --- /dev/null +++ b/tests/harness/corpus-costs/js-function-name-inference.js.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3600 } diff --git a/tests/harness/corpus-costs/js-function-name-module-alias/main.js.json b/tests/harness/corpus-costs/js-function-name-module-alias/main.js.json new file mode 100644 index 000000000..0c31a05f7 --- /dev/null +++ b/tests/harness/corpus-costs/js-function-name-module-alias/main.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4000 } diff --git a/tests/harness/corpus-costs/js-primitive-prototype-values.js.json b/tests/harness/corpus-costs/js-primitive-prototype-values.js.json new file mode 100644 index 000000000..c428eb8f1 --- /dev/null +++ b/tests/harness/corpus-costs/js-primitive-prototype-values.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4600 } diff --git a/tests/harness/corpus-costs/structured-clone-collections.ts.json b/tests/harness/corpus-costs/structured-clone-collections.ts.json new file mode 100644 index 000000000..d57898b27 --- /dev/null +++ b/tests/harness/corpus-costs/structured-clone-collections.ts.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 55300 } diff --git a/tests/harness/zero-cost-icu-symbols.test.ts b/tests/harness/zero-cost-icu-symbols.test.ts index ee9fac2a1..7e01c9cdc 100644 --- a/tests/harness/zero-cost-icu-symbols.test.ts +++ b/tests/harness/zero-cost-icu-symbols.test.ts @@ -36,6 +36,9 @@ const NON_ICU_SYMBOLS = [ "scr_intl_segmenter_refusing", "scr_dyn_call", "scr_string_concat", + // Emitted program functions are numbered, and a number can be 78. + "sc_f__x25_dyn_class_property_78", + "sc_bf__x25_arr_loop_78", ]; describe("icuSymbols", () => {