Merge remote-tracking branch 'origin/main' into merge/1197

This commit is contained in:
Malte Ubl
2026-10-30 11:38:02 -07:00
40 changed files with 1694 additions and 16 deletions
@@ -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).
@@ -1190,6 +1190,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
@@ -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;
@@ -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
@@ -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"),
@@ -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,
@@ -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;
@@ -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));
@@ -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<stri
}
return names;
}
/** `Number.prototype` / `Boolean.prototype` in a JavaScript source: the
* prototype object the checked-dynamic world builds (with its methods), or
* one of its members read as a value. Typed sources keep their own
* lowering, so no typed program links the unit that builds it. */
function jsPrimitivePrototypeValue(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const loc = locOf(expr);
const value = (kind: string): IrExpr => ({
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;
}
@@ -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 `.<name>.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 {
@@ -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;
+2
View File
@@ -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,
@@ -796,6 +796,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.propsDescriptor",
"child.propsOnDyn",
"dyn.arrayProtoCall",
"dyn.arrayLikeProtoCall",
"process.chdir",
"os.getPriority",
"os.setPriority",
@@ -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).
@@ -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<IrLibFn> = 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,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/js-array-proto-array-likes.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/js-function-name-inference.js"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/js-function-name-module-alias/names.js",
"<repo>/tests/corpus/js-function-name-module-alias/main.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/js-primitive-prototype-values.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/structured-clone-collections.ts"
],
"diags": []
}
@@ -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.
+8
View File
@@ -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
+773
View File
@@ -0,0 +1,773 @@
/* Array.prototype's generic algorithms over array-like receivers.
*
* `Array.prototype.<method>.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 <math.h>
#include <stdio.h>
#include <string.h>
/* 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 "<String(cb)> 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);
}
@@ -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 <stdio.h>
#include <string.h>
/* 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);
}
+88 -1
View File
@@ -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);
+8 -2
View File
@@ -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();
}
+187
View File
@@ -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'));
@@ -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);
@@ -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);
@@ -0,0 +1,3 @@
const root = (text) => text;
export default root;
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -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());
@@ -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<string, Item>([["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<number, string>([
[1, "one"],
[-0, "zero"],
]);
const byNumberCopy = structuredClone(byNumber);
console.log(byNumberCopy.get(1), byNumberCopy.get(0), byNumberCopy.has(2));
const flags = new Set<boolean>([true]);
console.log(structuredClone(flags).has(true), structuredClone(flags).has(false));
// Collections nested in collections and records.
type Outer = { inner: { m: Map<string, number>; when: Date }; tags: Set<string>; 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<string, Set<number>>([["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<string, number>()).size, structuredClone(new Set<string>()).size);
const handlers = new Map<string, () => void>([["run", () => {}]]);
try {
structuredClone(handlers);
console.log("cloned");
} catch (error) {
console.log((error as Error).name);
}
@@ -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("");
});
@@ -0,0 +1 @@
{ "plain": 3700, "san": 6200 }
@@ -0,0 +1 @@
{ "plain": 2400, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 4600 }
@@ -0,0 +1 @@
{ "plain": 4900, "san": 55300 }
@@ -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", () => {