Expand static native object and array semantics (#569)

* Support primitive and function property descriptors

* Dispatch literal bracket calls to native class methods

* Handle numeric class method keys in bracket calls

* Call function values returned by class accessors

* Expand native object and array semantics

* Keep new corpus fixtures aligned with compiler gates

* Lower Object.isFrozen arguments once
This commit is contained in:
Chris Tate
2026-09-29 08:30:54 -05:00
committed by GitHub
parent cbb8fc380a
commit f8906d0b75
38 changed files with 1644 additions and 183 deletions
+22
View File
@@ -4645,10 +4645,32 @@ function emitDynamicLibCall(state: LibCallState): Temp {
// Object.create(null): the fresh null-prototype dictionary
// (+1). Never throws.
return finish(`scr_dyn_new_obj_null_proto()`);
case "dyn.objCreate":
return finish(`scr_dyn_obj_create(${arg(0)})`);
case "dyn.objCreateWithProperties":
return finish(`scr_dyn_obj_create_with_properties(${arg(0)}, ${arg(1)})`);
case "dyn.getPrototype":
return finish(`scr_dyn_get_prototype(${arg(0)})`);
case "dyn.setPrototype":
return finish(`scr_dyn_set_prototype(${arg(0)}, ${arg(1)})`);
case "dyn.getOwnPropertyNames":
return finish(`scr_dyn_get_own_property_names(${arg(0)})`);
case "dyn.getOwnPropertyDescriptors":
return finish(`scr_dyn_get_own_property_descriptors(${arg(0)})`);
case "dyn.preventExtensions":
return finish(`scr_dyn_prevent_extensions(${arg(0)})`);
case "dyn.isExtensible":
return finish(`scr_dyn_is_extensible(${arg(0)})`);
case "dyn.seal":
return finish(`scr_dyn_seal(${arg(0)})`);
case "dyn.isSealed":
return finish(`scr_dyn_is_sealed(${arg(0)})`);
case "dyn.hasOwn":
// Object.hasOwn over a dyn receiver (throws on nullish, like
// Node's ToObject).
return finish(`scr_dyn_has_own(${arg(0)}, ${arg(1)})`);
case "dyn.propertyIsEnumerable":
return finish(`scr_dyn_property_is_enumerable(${arg(0)}, ${arg(1)})`);
case "dyn.objValues":
return finish(`scr_dyn_obj_values(${arg(0)})`);
case "dyn.objEntries":
@@ -1134,6 +1134,8 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return r;`);
d.push(` }`);
d.push(` }`);
d.push(` if (d->kind == SCR_DYN_ARR && d->v.arr.properties)`);
d.push(` return scr_dyn_obj_read(d->v.arr.properties, k->data, k->len);`);
d.push(` }`);
d.push(` return scr_dyn_retain(scr_dyn_undefined());`);
d.push(`}`, ``);
@@ -2674,6 +2674,15 @@ export class LlDyn {
B.line(`call void @scr_str_release(ptr ${ch})`);
B.terminate(`ret ptr ${r2}`);
B.startBlock(lMiss);
const lArrNamed = B.newLabel("kg.an");
const lOther = B.newLabel("kg.ao");
B.condBr(isA, lArrNamed, lOther);
B.startBlock(lArrNamed);
host.declare(`declare ptr @scr_dyn_arr_named_get(ptr, ptr)`);
const named = B.tmp();
B.line(`${named} = call ptr @scr_dyn_arr_named_get(ptr %d, ptr %k)`);
B.terminate(`ret ptr ${named}`);
B.startBlock(lOther);
retainUndef();
}
B.startBlock(lNext);
@@ -168,14 +168,14 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
const retTy = host.llType(e.type);
const retDecl = retTy === "i1" ? "zeroext i1" : retTy;
host.declare(`declare ${retDecl} @${sym}(${argDecls.join(", ")})`);
const argList = args.map((a) => `${host.llType(a.type)} ${a.name}`).join(", ");
const argList = args.map((a, index) => `${argDecls[index]} ${a.name}`).join(", ");
if (retTy === "void") {
B.line(`call void @${sym}(${argList})`);
if (MAY_THROW_LIB_FNS.has(e.fn)) host.emitPendingCheck();
return { name: "", type: e.type };
}
const t = B.tmp();
B.line(`${t} = call ${retTy} @${sym}(${argList})`);
B.line(`${t} = call ${retDecl} @${sym}(${argList})`);
// The result joins its frame BEFORE the pending check so an unwind
// releases the dummy (NULL for refcounted returns) harmlessly.
const out = host.own({ name: t, type: e.type });
@@ -871,6 +871,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.objKeys": "scr_dyn_obj_keys",
"dyn.forInKeys": "scr_dyn_for_in_keys",
"dyn.hasOwn": "scr_dyn_has_own",
"dyn.propertyIsEnumerable": "scr_dyn_property_is_enumerable",
"dyn.assign": "scr_dyn_assign",
"dyn.copyDataProperties": "scr_dyn_copy_data_properties",
// variadic Object.assign: the source pack (push never throws; the
@@ -881,6 +882,16 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.packPushSpreadIter": "scr_dyn_pack_push_spread_iter",
"dyn.assignAll": "scr_dyn_assign_all",
"dyn.objCreateNullProto": "scr_dyn_new_obj_null_proto",
"dyn.objCreate": "scr_dyn_obj_create",
"dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties",
"dyn.getPrototype": "scr_dyn_get_prototype",
"dyn.setPrototype": "scr_dyn_set_prototype",
"dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names",
"dyn.getOwnPropertyDescriptors": "scr_dyn_get_own_property_descriptors",
"dyn.preventExtensions": "scr_dyn_prevent_extensions",
"dyn.isExtensible": "scr_dyn_is_extensible",
"dyn.seal": "scr_dyn_seal",
"dyn.isSealed": "scr_dyn_is_sealed",
"dyn.objValues": "scr_dyn_obj_values",
"dyn.objEntries": "scr_dyn_obj_entries",
"dyn.structuredClone": "scr_structured_clone",
@@ -8,7 +8,7 @@ import { AstNode } from "../ts7/ast-node.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
import { lowerGenMethodCall } from "./lower-generators.js";
import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, NULL_T, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js";
import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js";
import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
@@ -4825,6 +4825,18 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// monomorphized against the defining literal's declaration.
lowerObjLitGenericMethodCall(lowerer, expr, expr.expression);
if (intrinsic) return intrinsic;
const accessorTarget = lowerer.fieldTarget(expr.expression);
if (accessorTarget?.container === "accessor") {
const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression);
if (callee.type.kind === "func") {
const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr));
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) };
}
if (callee.type.kind === "dyn" && !expr.arguments.some((arg) => ts.isSpreadElement(arg))) {
const args = expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
return { kind: "dynCall", callee, calleeName: expr.expression.getText(), args, type: DYN, loc: locOf(expr) };
}
}
// A method call rooted at an initializer-less ambient `declare
// const/var` whose declared type has no mapping: Node throws the
// catchable ReferenceError at the ROOT read before the member, the
@@ -4898,9 +4910,38 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
ts.isElementAccessExpression(expr.expression) &&
!expr.expression.questionDotToken &&
!expr.questionDotToken &&
ts.isStringLiteralLike(expr.expression.argumentExpression)
(ts.isStringLiteralLike(expr.expression.argumentExpression) || ts.isNumericLiteral(expr.expression.argumentExpression))
) {
const memberName = expr.expression.argumentExpression.text;
const memberName = ts.isNumericLiteral(expr.expression.argumentExpression)
? String(Number(expr.expression.argumentExpression.text))
: expr.expression.argumentExpression.text;
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression));
if (receiverType?.kind === "object") {
const info = lowerer.classes.get(receiverType.className);
const found = info ? lowerer.findMethodOn(info, memberName) : null;
if (info && found) {
const virtual = lowerer.overrideBelow(info, memberName);
if (found.sig.abstract === true && !virtual) {
lowerer.unsupported("SC1090", expr, `calls of the abstract method '${memberName}' with no concrete implementation below the receiver's static class`);
}
if (virtual) lowerer.noteVirtualEdge(info, memberName);
else lowerer.noteEdge(`%${found.declarer.def.name}.${memberName}`);
const receiver = lowerer.lowerExpr(expr.expression.expression);
const args = lowerer.completeArgs(expr.arguments, found.sig.params, locOf(expr), expr);
const callArgs = [lowerer.upcastTo(receiver, virtual ? info.def.name : found.declarer.def.name), ...args];
const loc = locOf(expr);
if (virtual) {
return reconcileOverloadReturn(lowerer, expr, {
kind: "virtualCall", className: info.def.name, method: memberName,
args: callArgs, type: found.sig.ret, loc,
});
}
return reconcileOverloadReturn(lowerer, expr, {
kind: "call", callee: `%${found.declarer.def.name}.${memberName}`,
args: callArgs, type: found.sig.ret, loc,
});
}
}
// ts7's getSymbolAtLocation does not resolve element accesses; the
// member symbol comes from the receiver's (apparent) type instead —
// same provenance answer as the dot spelling's name symbol.
@@ -5344,8 +5385,8 @@ function immediateObjectTagReceiver(lowerer: Lowerer, node: ts.Expression): { ta
}
const ARRAY_BORROW_METHODS = new Set([
"at", "every", "filter", "find", "findIndex", "forEach", "includes",
"indexOf", "lastIndexOf", "map", "reduce", "reduceRight", "some",
"at", "every", "filter", "find", "findIndex", "findLast", "findLastIndex", "forEach", "includes",
"indexOf", "lastIndexOf", "map", "reduce", "reduceRight", "slice", "some",
]);
function lowerArrayPrototypeBorrowCall(
@@ -5401,7 +5442,11 @@ function lowerObjectOwnMethodCall(
const method = access.name.text;
if (lowerer.dynamic || call.questionDotToken || access.questionDotToken ||
(method !== "hasOwnProperty" && method !== "propertyIsEnumerable") ||
!lowerer.isStdlibMember(access) || call.arguments.some(ts.isSpreadElement)) return null;
call.arguments.some(ts.isSpreadElement)) return null;
if (!lowerer.isStdlibMember(access)) {
const receiver = tryLowerExpression(lowerer, access.expression);
if (receiver?.type.kind !== "dyn") return null;
}
return lowerObjectOwnCall(lowerer, call, access.expression, call.arguments[0], call.arguments.slice(1), method);
}
@@ -5476,8 +5521,8 @@ function lowerObjectOwnCall(
lowerer.noLowering("Object.prototype." + method + " over this record shape", receiverNode ?? call);
}
result = { kind: "call", callee: recordHasOwnHelper(lowerer, type.shapeId, loc), args: [receiverRef, key], type: BOOL, loc };
} else if (type.kind === "dyn" && !enumerable) {
result = { kind: "libCall", fn: "dyn.hasOwn", args: [receiverRef, key], type: BOOL, loc };
} else if (type.kind === "dyn") {
result = { kind: "libCall", fn: enumerable ? "dyn.propertyIsEnumerable" : "dyn.hasOwn", args: [receiverRef, key], type: BOOL, loc };
} else if (type.kind === "string" || type.kind === "array") {
const needleLocal = lowerer.declareHiddenLocal("%ownIndex", F64);
stmts.push({ kind: "varDecl", localId: needleLocal.id, init: { kind: "libCall", fn: "num.fromString", args: [key], type: F64, loc }, loc });
@@ -8829,6 +8874,28 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibGlobal(access.expression, "Object")) return null;
const member = access.name.text;
if ((member === "getOwnPropertyNames" || member === "getOwnPropertyDescriptors") && call.arguments.length === 1 &&
!call.arguments.some(ts.isSpreadElement) && !lowerer.dynamic) {
let object = lowerer.lowerExpr(call.arguments[0]!);
if (object.type.kind === "string" || object.type.kind === "f64" ||
object.type.kind === "bool" || isUnitType(object.type))
object = { kind: "dynFrom", value: object, type: DYN, loc: locOf(call) };
if (object.type.kind === "dyn")
return { kind: "libCall", fn: member === "getOwnPropertyNames" ? "dyn.getOwnPropertyNames" : "dyn.getOwnPropertyDescriptors", args: [object], type: DYN, loc: locOf(call) };
}
if ((member === "getPrototypeOf" || member === "setPrototypeOf") &&
call.arguments.length === (member === "getPrototypeOf" ? 1 : 2) &&
!call.arguments.some(ts.isSpreadElement) && !lowerer.dynamic) {
const object = lowerer.lowerExpr(call.arguments[0]!);
if (object.type.kind === "dyn") {
const loc = locOf(call);
if (member === "getPrototypeOf")
return { kind: "libCall", fn: "dyn.getPrototype", args: [object], type: DYN, loc };
const prototype = lowerer.lowerExpr(call.arguments[1]!);
if (prototype.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.setPrototype", args: [object, prototype], type: DYN, loc };
}
}
if (member === "getOwnPropertyDescriptor" && call.arguments.length === 2) {
const [receiver, key] = call.arguments;
if (receiver && key && ts.isPropertyAccessExpression(receiver) && receiver.name.text === "prototype" &&
@@ -8959,27 +9026,14 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
"the operands must share one comparable kind (numbers, strings, booleans, units, one union shape, or one reference type)",
);
}
// Object.create — the null-prototype DICTIONARY (`Object.create(null)`
// then keyed assignment, the memo-table idiom prettier's index/
// group-mode maps spell) and, under --dynamic, the engine's own
// Object.create for engine-held prototypes. Everything else is a
// NAMED fence: the compiled representations have no prototype chain,
// and the own-copy stand-in would answer WRONG observably — Node's
// Object.keys/inspect/JSON of the created object list NO own keys,
// and mutating the prototype afterwards is visible through the
// created object (live delegation), which no copy can honor.
// Native checked objects retain a live prototype and can install an
// own-property descriptor table during creation. Engine-held values
// use the engine's Object.create path below.
if (member === "create") {
if (call.arguments.some((a) => ts.isSpreadElement(a))) {
lowerer.noLowering("Object.create with spread arguments", call);
}
if (call.arguments.length >= 2) {
lowerer.noLowering(
"Object.create with a properties-descriptor argument",
call,
"create first, then assign: const o = Object.create(null); o.k = v",
);
}
if (call.arguments.length !== 1) {
if (call.arguments.length < 1 || call.arguments.length > 2) {
lowerer.noLowering(`Object.create with ${call.arguments.length} arguments`, call);
}
const loc = locOf(call);
@@ -8987,6 +9041,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
while (ts.isParenthesizedExpression(protoNode)) protoNode = protoNode.expression;
const nullProto = protoNode.kind === ts.SyntaxKind.NullKeyword;
if (lowerer.dynamic) {
if (call.arguments.length === 2) lowerer.noLowering("Object.create with properties under --dynamic", call);
// The checker types the result `any` — an ENGINE value under
// --dynamic — and the engine's own Object.create answers with
// REAL prototype semantics: reads delegate LIVE, writes shadow,
@@ -9011,15 +9066,24 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
"prototype reads delegate LIVE in Node (mutating the prototype shows through the created object), which the boundary's deep copy cannot honor — only null and engine-held ('any') prototypes lower",
);
}
if (nullProto) {
if (nullProto && call.arguments.length === 1) {
return { kind: "libCall", fn: "dyn.objCreateNullProto", args: [], type: DYN, loc };
}
const proto = lowerer.lowerExpr(protoNode);
lowerer.noLowering(
`Object.create over '${lowerer.fmt(proto.type)}' prototypes`,
call,
"the compiled representations have no prototype chain, and an own-copy would answer wrong observably (Node lists NO own keys on the created object, and prototype mutations show through it live) — only Object.create(null) lowers",
);
let proto = nullProto
? { kind: "dynFrom", value: { kind: "unitLit", unit: "null", type: NULL_T, loc }, type: DYN, loc } as IrExpr
: ts.isObjectLiteralExpression(protoNode)
? lowerer.lowerExprExpecting(protoNode, DYN)
: lowerer.lowerExpr(protoNode);
if (isUnitType(proto.type) || proto.type.kind === "f64" || proto.type.kind === "bool" || proto.type.kind === "string")
proto = { kind: "dynFrom", value: proto, type: DYN, loc };
if (proto.type.kind === "dyn") {
if (call.arguments.length === 1)
return { kind: "libCall", fn: "dyn.objCreate", args: [proto], type: DYN, loc };
const descriptors = lowerer.lowerExprExpecting(call.arguments[1]!, DYN);
if (descriptors.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.objCreateWithProperties", args: [proto, descriptors], type: DYN, loc };
}
lowerer.noLowering(`Object.create over '${lowerer.fmt(proto.type)}' prototypes`, call);
}
// `Object.assign(fn, { props })` whose RESULT type maps to the hybrid
// (function-with-properties) record: the chalk-shape CONSTRUCTOR.
@@ -9201,7 +9265,9 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const fnName = jsFuncNameOf(call.arguments[0]!);
target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!), ...(fnName !== null ? { fnName } : {}) };
}
if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type))) return null;
const descriptorPrimitive = member === "getOwnPropertyDescriptor" && target &&
(target.type.kind === "bool" || target.type.kind === "f64" || target.type.kind === "string");
if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type) && !descriptorPrimitive)) return null;
if (target.type.kind !== "dyn") target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) };
const key = lowerer.lowerExprExpecting(call.arguments[1]!, DYN);
if (key.type.kind !== "dyn") return null;
@@ -9244,6 +9310,28 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
}
return null;
}
if (["preventExtensions", "isExtensible", "seal", "isSealed"].includes(member) &&
call.arguments.length === 1 && !ts.isSpreadElement(call.arguments[0]!)) {
const value = lowerer.lowerExpr(call.arguments[0]!);
if (value.type.kind === "string" || value.type.kind === "f64" ||
value.type.kind === "bool" || value.type.kind === "symbol" || isUnitType(value.type)) {
if (member === "preventExtensions" || member === "seal") return value;
const loc = locOf(call);
const answer: IrExpr = { kind: "boolLit", value: member !== "isExtensible", type: BOOL, loc };
return isSafeToDiscard(value) ? answer : {
kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: value, loc: value.loc }],
result: answer, type: BOOL, loc,
};
}
if (value.type.kind === "dyn") {
const operation = {
preventExtensions: "dyn.preventExtensions", isExtensible: "dyn.isExtensible",
seal: "dyn.seal", isSealed: "dyn.isSealed",
} as const;
const fn = operation[member as keyof typeof operation];
return { kind: "libCall", fn, args: [value], type: member.startsWith("is") ? BOOL : DYN, loc: locOf(call) };
}
}
// Object.freeze: on a FRESH literal (object or array) the result IS
// the argument — no alias exists, so the frozen bit is unobservable
// (writes through the Readonly<T> result are compile errors, and no
@@ -9377,9 +9465,11 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const isDyn = probed?.type.kind === "dyn";
// Unit-typed arguments (Object.keys(null)) ride the same runtime
// walk: it throws Node's catchable TypeError.
const isUnit = probed !== null && probed !== undefined && isUnitType(probed.type);
const isFunction = probed?.type.kind === "func" && lowerer.dynConvertible(probed.type);
if (isDyn || isUnit || isFunction) {
const isPrimitive = probed !== null && probed !== undefined &&
(isUnitType(probed.type) || probed.type.kind === "string" ||
probed.type.kind === "f64" || probed.type.kind === "bool");
if (isDyn || isPrimitive || isFunction) {
const fn = member === "keys" ? "dyn.objKeys" : member === "values" ? "dyn.objValues" : "dyn.objEntries";
let v = lowerer.lowerExpr(argNode);
if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
@@ -4696,6 +4696,17 @@ export function lowerOptionalNumber(
}
}
if (receiverIr?.kind !== "array") {
{
const receiver = tryLowerExpression(lowerer, expr.expression);
if (receiver?.type.kind === "func" &&
canBoxFuncIntoDyn(receiver.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
const key = lowerRecordPropertyKey(lowerer, lowerer.lowerExpr(expr.argumentExpression), expr.argumentExpression);
if (key.type.kind === "string") {
const boxed: IrExpr = { kind: "dynFrom", value: receiver, type: DYN, loc: locOf(expr.expression) };
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, value: boxed, type: DYN, loc: locOf(expr) }, expr);
}
}
}
if (receiverIr?.kind === "string") {
// `s[i]` with a number index reads a UTF-16 code unit — charAt's
// exact job (the UTF-16-exact runtime). Without
@@ -8063,7 +8063,7 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
type: arrayOf(STRING), loc,
};
const loop = lowerForInOverKeys(lowerer, stmt, keys, labels, (key) => ({
kind: "libCall", fn: "dyn.hasOwn", args: [ref, key], type: BOOL, loc,
kind: "libCall", fn: "dyn.hasKey", args: [ref, key], type: BOOL, loc,
}));
return { kind: "block", body: [{ kind: "varDecl", localId: recv.id, init: receiver, loc }, loop], loc };
}
@@ -632,6 +632,7 @@ function exactValueParams(...types: IrType[]): BuiltinValueParam[] {
export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined> = {
defineProperty: { fn: "dyn.defineProperty", params: [DYN, DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN, DYN) },
getOwnPropertyDescriptor: { fn: "dyn.getOwnPropertyDescriptor", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
getOwnPropertyDescriptors: { fn: "dyn.getOwnPropertyDescriptors", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
defineProperties: { fn: "dyn.defineProps", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
+25 -1
View File
@@ -3953,6 +3953,7 @@ export type IrLibFn =
| "dyn.objKeys"
| "dyn.forInKeys"
| "dyn.hasOwn"
| "dyn.propertyIsEnumerable"
| "dyn.assign"
| "dyn.copyDataProperties"
/** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign(
@@ -3982,6 +3983,16 @@ export type IrLibFn =
* and deepStrictEqual's prototype gate. Never throws. Static builds
* only; --dynamic routes Object.create through the engine instead. */
| "dyn.objCreateNullProto"
| "dyn.objCreate"
| "dyn.objCreateWithProperties"
| "dyn.getPrototype"
| "dyn.setPrototype"
| "dyn.getOwnPropertyNames"
| "dyn.getOwnPropertyDescriptors"
| "dyn.preventExtensions"
| "dyn.isExtensible"
| "dyn.seal"
| "dyn.isSealed"
| "dyn.objValues"
| "dyn.objEntries"
/** structuredClone over the checked-dynamic tree (scr_json.c): the JSON-safe subset plus
@@ -6871,7 +6882,9 @@ export function moduleUsesDynInvoke(mod: IrModule): boolean {
}
const node = v as { kind?: unknown; fn?: unknown };
if (node.kind === "dynInvoke" || (node.kind === "libCall" &&
(node.fn === "dyn.defineProps" || node.fn === "dyn.arrayProtoCall"))) {
(node.fn === "dyn.defineProps" || node.fn === "dyn.defineProperty" ||
node.fn === "dyn.objCreateWithProperties" ||
node.fn === "dyn.arrayProtoCall"))) {
found = true;
return;
}
@@ -7938,6 +7951,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objKeys",
"dyn.forInKeys",
"dyn.hasOwn",
"dyn.propertyIsEnumerable",
"dyn.assign",
"dyn.copyDataProperties",
// variadic Object.assign: spread flattening throws V8's spread-call
@@ -7945,6 +7959,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.packPushSpread",
"dyn.packPushSpreadIter",
"dyn.assignAll",
"dyn.objCreate",
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
"dyn.getOwnPropertyNames",
"dyn.getOwnPropertyDescriptors",
"dyn.preventExtensions",
"dyn.isExtensible",
"dyn.seal",
"dyn.isSealed",
"dyn.objValues",
"dyn.objEntries",
"error.domClone",
+11
View File
@@ -1146,6 +1146,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.objKeys": { argTypes: [DYN], result: DYN },
"dyn.forInKeys": { argTypes: [DYN], result: DYN },
"dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL },
"dyn.propertyIsEnumerable": { argTypes: [DYN, STRING], result: BOOL },
"dyn.assign": { argTypes: [DYN, DYN], result: DYN },
"dyn.copyDataProperties": { argTypes: [DYN, DYN], result: DYN },
"dyn.packPush": { argTypes: [DYN, DYN], result: VOID },
@@ -1153,6 +1154,16 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID },
"dyn.assignAll": { argTypes: [DYN, DYN], result: DYN },
"dyn.objCreateNullProto": { argTypes: [], result: DYN },
"dyn.objCreate": { argTypes: [DYN], result: DYN },
"dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN },
"dyn.getPrototype": { argTypes: [DYN], result: DYN },
"dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN },
"dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN },
"dyn.preventExtensions": { argTypes: [DYN], result: DYN },
"dyn.isExtensible": { argTypes: [DYN], result: BOOL },
"dyn.seal": { argTypes: [DYN], result: DYN },
"dyn.isSealed": { argTypes: [DYN], result: BOOL },
"dyn.objValues": { argTypes: [DYN], result: DYN },
"dyn.objEntries": { argTypes: [DYN], result: DYN },
"dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN },
@@ -9492,12 +9492,6 @@
],
"diags": []
},
"<repo>/tests/diagnostics/expressions.ts": {
"order": [
"<repo>/tests/diagnostics/expressions.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/fetch-static.ts": {
"order": [
"<repo>/tests/diagnostics/fetch-static.ts"
@@ -11463,6 +11457,126 @@
"<repo>/tests/corpus/typed-rest-filtered-return.ts"
],
"diags": []
},
"<repo>/tests/corpus/class-accessor-returned-function.js": {
"order": [
"<repo>/tests/corpus/class-accessor-returned-function.js"
],
"diags": []
},
"<repo>/tests/corpus/class-bracket-method-calls.js": {
"order": [
"<repo>/tests/corpus/class-bracket-method-calls.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-borrowed-find-last.js": {
"order": [
"<repo>/tests/corpus/native-array-borrowed-find-last.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-borrowed-slice.js": {
"order": [
"<repo>/tests/corpus/native-array-borrowed-slice.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-copying-methods.js": {
"order": [
"<repo>/tests/corpus/native-array-copying-methods.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-fill-copywithin.js": {
"order": [
"<repo>/tests/corpus/native-array-fill-copywithin.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-frozen-noops.js": {
"order": [
"<repo>/tests/corpus/native-array-frozen-noops.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-index-boundaries.js": {
"order": [
"<repo>/tests/corpus/native-array-index-boundaries.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-integrity.js": {
"order": [
"<repo>/tests/corpus/native-array-integrity.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-length.js": {
"order": [
"<repo>/tests/corpus/native-array-length.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-named-properties.js": {
"order": [
"<repo>/tests/corpus/native-array-named-properties.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-reduce-find-last.js": {
"order": [
"<repo>/tests/corpus/native-array-reduce-find-last.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-search-from-index.js": {
"order": [
"<repo>/tests/corpus/native-array-search-from-index.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-splice-coercion.js": {
"order": [
"<repo>/tests/corpus/native-array-splice-coercion.js"
],
"diags": []
},
"<repo>/tests/corpus/native-array-splice-flat.js": {
"order": [
"<repo>/tests/corpus/native-array-splice-flat.js"
],
"diags": []
},
"<repo>/tests/corpus/native-object-descriptors.js": {
"order": [
"<repo>/tests/corpus/native-object-descriptors.js"
],
"diags": []
},
"<repo>/tests/corpus/native-object-for-in-minimal.js": {
"order": [
"<repo>/tests/corpus/native-object-for-in-minimal.js"
],
"diags": []
},
"<repo>/tests/corpus/native-object-property-attributes.js": {
"order": [
"<repo>/tests/corpus/native-object-property-attributes.js"
],
"diags": []
},
"<repo>/tests/corpus/native-object-prototype.js": {
"order": [
"<repo>/tests/corpus/native-object-prototype.js"
],
"diags": []
},
"<repo>/tests/corpus/primitive-and-function-descriptors.js": {
"order": [
"<repo>/tests/corpus/primitive-and-function-descriptors.js"
],
"diags": []
}
}
}
+7 -1
View File
@@ -545,10 +545,16 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
for (size_t i = 0; i < a->v.arr.len; i++) {
if (!scr_assert_dyn_deep_eq(a->v.arr.items[i], b->v.arr.items[i])) return false;
}
if (a->v.arr.properties || b->v.arr.properties) {
if (!a->v.arr.properties || !b->v.arr.properties) {
const ScrDyn *table = a->v.arr.properties ? a->v.arr.properties : b->v.arr.properties;
if (table->v.obj.len != 0) return false;
} else if (!scr_assert_dyn_deep_eq(a->v.arr.properties, b->v.arr.properties)) return false;
}
return true;
}
case SCR_DYN_OBJ: {
if (a->null_proto != b->null_proto) return false; /* the prototype gate */
if (a->null_proto != b->null_proto || a->prototype != b->prototype) return false;
if (a->v.obj.len != b->v.obj.len) return false;
for (size_t i = 0; i < a->v.obj.len; i++) {
const ScrDynEntry *ent = &a->v.obj.entries[i];
+397 -42
View File
@@ -165,23 +165,14 @@ static void dyn_throw_unsupported(const char *proto, const char *method) {
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
}
/* ToIntegerOrInfinity over an OPTIONAL index argument: missing/undefined
* answers dflt; a NUM truncates toward zero (NaN -> 0, like JS); any
* other kind throws the loud fence (Node would ToNumber-coerce — a
* documented gap, never a silent misread). */
/* ToIntegerOrInfinity over an optional index argument. Missing and
* undefined use the operation's default; present values run ToNumber once. */
static double dyn_index_arg(ScrDyn *const *args, size_t argc, size_t i, double dflt, const char *what) {
(void)what;
if (i >= argc || args[i]->kind == SCR_DYN_UNDEF) return dflt;
if (args[i]->kind == SCR_DYN_NUM) {
double n = args[i]->v.num;
if (n != n) return 0;
return trunc(n);
}
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, what);
scr_jb_puts(&b, ": non-number index arguments on a dynamic receiver are not supported yet");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
return 0;
double number;
if (!scr_dyn_number_coerce_js(args[i], &number)) return 0;
return isnan(number) ? 0 : trunc(number);
}
static double dyn_bytes_index_arg(ScrDyn *const *args, size_t argc, size_t index, double fallback) {
@@ -200,6 +191,13 @@ static size_t dyn_rel_index(double rel, size_t len) {
return rel > (double)len ? len : (size_t)rel;
}
static bool dyn_arr_length_unchanged(const ScrDyn *recv, size_t length) {
if (recv->v.arr.len == length) return true;
static const char message[] = "Array method after an argument changes the receiver length is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return false;
}
/* The array callback runner (forEach/map/filter/some/every/find/
* findIndex): calls cb(item, i, recv) per element through the boxed
* thunk. Returns the owned result or NULL with the exception pending. */
@@ -269,7 +267,13 @@ static ScrDyn *dyn_borrow_get(ScrDyn *recv, const ScrStr *key) {
return scr_dyn_new_num(length);
}
size_t index;
if (!dyn_borrow_index(key, &index)) return scr_dyn_retain(scr_dyn_undefined());
if (!dyn_borrow_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
@@ -359,7 +363,7 @@ static ScrDyn *dyn_borrow_item(ScrDyn *recv, size_t index) {
ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
if (recv->kind == SCR_DYN_NULL || recv->kind == SCR_DYN_UNDEF) {
static const char msg[] = "Array.prototype method called on null or undefined";
static const char msg[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
@@ -377,6 +381,8 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
ScrDyn *needle = argc ? argv[0] : scr_dyn_undefined();
double from = last_index_of ? (double)len - 1 : 0;
if (argc > 1 && !dyn_borrow_number(argv[1], &from)) return NULL;
if (isnan(from)) from = 0;
else from = trunc(from);
double first = from < 0 ? (double)len + from : from;
if (last_index_of) {
if (from >= 0) first = fmin(from, (double)len - 1);
@@ -416,6 +422,41 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
? dyn_borrow_item(recv, (size_t)index)
: scr_dyn_retain(scr_dyn_undefined());
}
if (dyn_borrow_is(method, "slice")) {
if (recv->kind == SCR_DYN_PROXY) {
scr_dyn_proxy_unsupported("Array.prototype.slice");
return NULL;
}
double startD = 0;
double endD = (double)len;
if (argc && !dyn_borrow_number(argv[0], &startD)) return NULL;
if (argc > 1 && argv[1]->kind != SCR_DYN_UNDEF && !dyn_borrow_number(argv[1], &endD)) return NULL;
if (isnan(startD)) startD = 0;
if (isnan(endD)) endD = 0;
size_t start = dyn_rel_index(trunc(startD), len);
size_t end = dyn_rel_index(trunc(endD), len);
if (end > start && end - start > 1000000) {
static const char message[] = "Array.prototype.slice exceeds the native iteration limit";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = start; i < end; i++) {
if (!dyn_borrow_iteration(i - start)) { scr_dyn_release(out); return NULL; }
bool present = dyn_borrow_present(recv, i);
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
if (!present) {
static const char message[] = "Array.prototype.slice over sparse array-like values is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
scr_dyn_release(out);
return NULL;
}
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) { scr_dyn_release(out); return NULL; }
scr_dyn_arr_push(out, item);
}
return out;
}
bool each = dyn_borrow_is(method, "forEach");
bool map = dyn_borrow_is(method, "map");
@@ -424,9 +465,11 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
bool every = dyn_borrow_is(method, "every");
bool find = dyn_borrow_is(method, "find");
bool find_index = dyn_borrow_is(method, "findIndex");
bool find_last = dyn_borrow_is(method, "findLast");
bool find_last_index = dyn_borrow_is(method, "findLastIndex");
bool reduce = dyn_borrow_is(method, "reduce");
bool reduce_right = dyn_borrow_is(method, "reduceRight");
if (!(each || map || filter || some || every || find || find_index || reduce || reduce_right)) {
if (!(each || map || filter || some || every || find || find_index || find_last || find_last_index || reduce || reduce_right)) {
static const char msg[] = "Array.prototype method is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
@@ -461,7 +504,8 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
ScrDyn *this_arg = argc > 1 ? argv[1] : scr_dyn_undefined();
for (size_t i = 0; i < len; i++) {
if (len > 1000000 && !dyn_borrow_iteration(i)) { scr_dyn_release(out); return NULL; }
bool present = find || find_index || dyn_borrow_present(recv, i);
size_t index = find_last || find_last_index ? len - 1 - i : i;
bool present = find || find_index || find_last || find_last_index || dyn_borrow_present(recv, index);
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
if (!present) {
if (map) {
@@ -472,10 +516,10 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
}
continue;
}
ScrDyn *item = dyn_borrow_item(recv, i);
ScrDyn *item = dyn_borrow_item(recv, index);
if (!item) { scr_dyn_release(out); return NULL; }
scr_dyn_this_push_dyn(this_arg);
ScrDyn *result = dyn_call_cb(cb, item, i, recv);
ScrDyn *result = dyn_call_cb(cb, item, index, recv);
scr_dyn_this_pop();
if (!result) { scr_dyn_release(item); scr_dyn_release(out); return NULL; }
if (map) {
@@ -486,16 +530,16 @@ ScrDyn *scr_dyn_array_proto_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
if (filter && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item));
if (some && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); }
if (every && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); }
if (find && truthy) return item;
if (find_index && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); }
if ((find || find_last) && truthy) return item;
if ((find_index || find_last_index) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)index); }
}
scr_dyn_release(item);
}
if (out) return out;
if (some) return scr_dyn_new_bool(false);
if (every) return scr_dyn_new_bool(true);
if (find) return scr_dyn_retain(scr_dyn_undefined());
if (find_index) return scr_dyn_new_num(-1);
if (find || find_last) return scr_dyn_retain(scr_dyn_undefined());
if (find_index || find_last_index) return scr_dyn_new_num(-1);
return scr_dyn_retain(scr_dyn_undefined());
}
@@ -591,18 +635,39 @@ static bool dyn_arr_sort(ScrDyn *recv, ScrDyn *cmp) {
return ok;
}
static bool dyn_arr_flatten(ScrDyn *out, const ScrDyn *source, double depth, size_t *visits) {
for (size_t i = 0; i < source->v.arr.len; i++) {
if (++*visits > 1000000) {
static const char message[] = "Array.prototype.flat exceeds the native iteration limit";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return false;
}
const ScrDyn *item = source->v.arr.items[i];
if (depth > 0 && item->kind == SCR_DYN_ARR) {
if (!dyn_arr_flatten(out, item, depth - 1, visits)) return false;
} else {
scr_dyn_arr_push(out, scr_dyn_retain((ScrDyn *)item));
}
}
return true;
}
static void dyn_arr_reversed_copy(ScrDyn *out, const ScrDyn *source) {
for (size_t i = source->v.arr.len; i > 0; i--)
scr_dyn_arr_push(out, scr_dyn_retain(source->v.arr.items[i - 1]));
}
/* Names each prototype declares BEYOND what's implemented here — these
* fence loudly instead of mis-answering "is not a function". */
static bool dyn_arr_proto_unimpl(const char *m) {
static const char *names[] = { "splice", "reduce", "reduceRight", "flat",
"fill", "copyWithin", "keys", "values", "entries", "toReversed", "toSorted", "toSpliced",
"with", "toString", "toLocaleString", NULL };
static const char *names[] = { "keys", "values", "entries",
"toString", "toLocaleString", NULL };
for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true;
return false;
}
static bool dyn_arr_proto_mutates(const char *m) {
static const char *names[] = {
"push", "pop", "shift", "unshift", "reverse", "sort", NULL
"push", "pop", "shift", "unshift", "reverse", "sort", "fill", "copyWithin", "splice", NULL
};
for (size_t i = 0; names[i]; i++) if (dyn_name_is(m, names[i])) return true;
return false;
@@ -695,10 +760,10 @@ static ScrDyn *scr_dyn_invoke_impl(
return result;
}
/* OBJ: the own member calls (own properties shadow prototypes in JS
* too); anything else is Node's is-not-a-function. */
/* OBJ method calls follow the native prototype chain while binding the
* original receiver as `this`. */
if (recv->kind == SCR_DYN_OBJ) {
ScrDyn *m = scr_dyn_obj_get(recv, method, strlen(method));
ScrDyn *m = scr_dyn_obj_read(recv, method, strlen(method));
if (m && (m->kind == SCR_DYN_FUNC ||
/* a WRAPPED engine function stored as a dyn member: the
* routed call (scr_dyn_call's JSVAL arm) runs it. */
@@ -708,8 +773,10 @@ static ScrDyn *scr_dyn_invoke_impl(
scr_dyn_this_push_dyn(recv);
ScrDyn *r = scr_dyn_call(m, args, argc, what);
scr_dyn_this_pop();
scr_dyn_release(m);
return r;
}
scr_dyn_release(m);
dyn_throw_not_fn(what);
return NULL;
}
@@ -803,6 +870,63 @@ static ScrDyn *scr_dyn_invoke_impl(
if (recv->kind == SCR_DYN_ARR) {
size_t len = recv->v.arr.len;
size_t splice_start = 0;
size_t splice_removed = 0;
if (dyn_arr_proto_mutates(method)) {
bool grows = dyn_name_is(method, "push") && argc > 0;
grows = grows || (dyn_name_is(method, "unshift") && argc > 0);
bool shrinks = (dyn_name_is(method, "pop") || dyn_name_is(method, "shift")) && len > 0;
if (dyn_name_is(method, "splice")) {
double startD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
splice_start = dyn_rel_index(startD, len);
splice_removed = len - splice_start;
if (argc > 1) {
double countD = dyn_index_arg(args, argc, 1, 0, what);
if (scr_exc_pending()) return NULL;
splice_removed = countD <= 0 ? 0 : countD >= (double)splice_removed ? splice_removed : (size_t)countD;
}
if (recv->v.arr.len != len) {
static const char message[] = "Array.prototype.splice after an argument changes the receiver length is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
size_t inserted = argc > 2 ? argc - 2 : 0;
if (inserted > splice_removed && inserted - splice_removed > 1000000) {
static const char message[] = "Array.prototype.splice exceeds the native array growth limit";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
grows = grows || inserted > splice_removed;
shrinks = shrinks || splice_removed > inserted;
if (splice_removed > inserted && recv->v.arr.sealed && !recv->v.arr.frozen) {
for (size_t i = splice_start; i < len - splice_removed + inserted; i++) {
size_t source = i < splice_start + inserted ? i : i + splice_removed - inserted;
ScrDyn *replacement = i < splice_start + inserted ? args[i - splice_start + 2] : recv->v.arr.items[source];
ScrDyn *old = recv->v.arr.items[i];
recv->v.arr.items[i] = scr_dyn_retain(replacement);
scr_dyn_release(old);
}
static const char message[] = "Cannot delete non-configurable property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
}
if (recv->v.arr.frozen && (dyn_name_is(method, "push") ||
dyn_name_is(method, "pop") || dyn_name_is(method, "shift") ||
dyn_name_is(method, "unshift") || dyn_name_is(method, "splice"))) {
static const char message[] = "Cannot assign to read only property 'length'";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !dyn_name_is(method, "shift")) ||
(recv->v.arr.frozen && ((dyn_name_is(method, "sort") && len > 1) ||
(dyn_name_is(method, "reverse") && len > 1)))) {
static const char message[] = "Cannot modify a sealed or frozen array";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
}
if (dyn_name_is(method, "push")) {
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i]));
return scr_dyn_new_num((double)recv->v.arr.len);
@@ -813,26 +937,68 @@ static ScrDyn *scr_dyn_invoke_impl(
}
if (dyn_name_is(method, "shift")) {
if (len == 0) return scr_dyn_retain(scr_dyn_undefined());
if (recv->v.arr.sealed) {
if (recv->v.arr.frozen) {
static const char message[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
for (size_t i = 1; i < len; i++) {
scr_dyn_release(recv->v.arr.items[i - 1]);
recv->v.arr.items[i - 1] = scr_dyn_retain(recv->v.arr.items[i]);
}
static const char message[] = "Cannot delete non-configurable property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
ScrDyn *first = recv->v.arr.items[0];
memmove(recv->v.arr.items, recv->v.arr.items + 1, (len - 1) * sizeof(ScrDyn *));
recv->v.arr.len = len - 1;
return first; /* ownership moves out */
}
if (dyn_name_is(method, "unshift")) {
/* Append first (the push path grows capacity and takes the +1s),
* then rotate: the old block moves up and the SAME retained
* pointers land at the front (args[] still names them, so the
* memmove clobbering the appended slots loses nothing). */
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(args[i]));
/* Grow the backing store before shifting the old elements. The
* appended placeholders are immortal, so overwriting them does not
* abandon retained argument references. */
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(recv, scr_dyn_retain(scr_dyn_undefined()));
memmove(recv->v.arr.items + argc, recv->v.arr.items, len * sizeof(ScrDyn *));
for (size_t i = 0; i < argc; i++) recv->v.arr.items[i] = args[i];
for (size_t i = 0; i < argc; i++) recv->v.arr.items[i] = scr_dyn_retain(args[i]);
return scr_dyn_new_num((double)recv->v.arr.len);
}
if (dyn_name_is(method, "splice")) {
size_t start = splice_start;
size_t removed = splice_removed;
size_t inserted = argc > 2 ? argc - 2 : 0;
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = start; i < start + removed; i++)
scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i]));
size_t next_len = len - removed + inserted;
if (next_len > recv->v.arr.cap) {
size_t capacity = recv->v.arr.cap ? recv->v.arr.cap : 4;
while (capacity < next_len) {
if (capacity > SIZE_MAX / 2) scr_trap("scriptc: array capacity overflow\n");
capacity *= 2;
}
ScrDyn **items = realloc(recv->v.arr.items, capacity * sizeof *items);
if (!items) scr_trap("scriptc: out of memory\n");
recv->v.arr.items = items;
recv->v.arr.cap = capacity;
}
for (size_t i = start; i < start + removed; i++) scr_dyn_release(recv->v.arr.items[i]);
memmove(recv->v.arr.items + start + inserted,
recv->v.arr.items + start + removed,
(len - start - removed) * sizeof(ScrDyn *));
for (size_t i = 0; i < inserted; i++)
recv->v.arr.items[start + i] = scr_dyn_retain(args[i + 2]);
recv->v.arr.len = next_len;
return out;
}
if (dyn_name_is(method, "slice")) {
double startD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
double endD = dyn_index_arg(args, argc, 1, (double)len, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
size_t start = dyn_rel_index(startD, len);
size_t end = dyn_rel_index(endD, len);
ScrDyn *out = scr_dyn_new_arr();
@@ -842,20 +1008,32 @@ static ScrDyn *scr_dyn_invoke_impl(
if (dyn_name_is(method, "at")) {
double iD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
double idx = iD < 0 ? (double)len + iD : iD;
if (idx < 0 || idx >= (double)len) return scr_dyn_retain(scr_dyn_undefined());
return scr_dyn_retain(recv->v.arr.items[(size_t)idx]);
}
if (dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") ||
dyn_name_is(method, "includes")) {
if (len == 0) return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined();
double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0;
if (argc > 1) {
from = dyn_index_arg(args, argc, 1, from, what);
if (scr_exc_pending()) return NULL;
}
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
if (dyn_name_is(method, "lastIndexOf")) {
for (size_t i = len; i > 0; i--) {
double first = from < 0 ? (double)len + from : from;
if (first >= (double)len) first = (double)len - 1;
for (size_t i = first < 0 ? 0 : (size_t)first + 1; i > 0; i--) {
if (scr_dyn_strict_eq(recv->v.arr.items[i - 1], needle)) return scr_dyn_new_num((double)(i - 1));
}
return scr_dyn_new_num(-1);
}
for (size_t i = 0; i < len; i++) {
double first = from < 0 ? (double)len + from : from;
if (first < 0) first = 0;
for (size_t i = first >= (double)len ? len : (size_t)first; i < len; i++) {
const ScrDyn *item = recv->v.arr.items[i];
bool nan_match = dyn_name_is(method, "includes") &&
item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM &&
@@ -866,6 +1044,28 @@ static ScrDyn *scr_dyn_invoke_impl(
}
return dyn_name_is(method, "includes") ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
}
if (dyn_name_is(method, "findLast") || dyn_name_is(method, "findLastIndex")) {
if (!dyn_cb_check(args, argc)) return NULL;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t i = len; i > 0; i--) {
size_t index = i - 1;
ScrDyn *item = index < recv->v.arr.len
? scr_dyn_retain(recv->v.arr.items[index])
: scr_dyn_retain(scr_dyn_undefined());
ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv);
if (!result) { scr_dyn_release(item); return NULL; }
bool matched = scr_dyn_truthy(result);
scr_dyn_release(result);
if (matched) {
if (dyn_name_is(method, "findLast")) return item;
scr_dyn_release(item);
return scr_dyn_new_num((double)index);
}
scr_dyn_release(item);
}
return dyn_name_is(method, "findLast")
? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1);
}
if (dyn_name_is(method, "join")) {
ScrJsonBuf b;
scr_jb_init(&b);
@@ -897,6 +1097,18 @@ static ScrDyn *scr_dyn_invoke_impl(
}
return out;
}
if (dyn_name_is(method, "flat")) {
double depth = dyn_index_arg(args, argc, 0, 1, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
ScrDyn *out = scr_dyn_new_arr();
size_t visits = 0;
if (!dyn_arr_flatten(out, recv, depth < 0 ? 0 : depth, &visits)) {
scr_dyn_release(out);
return NULL;
}
return out;
}
if (dyn_name_is(method, "reverse")) {
for (size_t i = 0; i < len / 2; i++) {
ScrDyn *tmp = recv->v.arr.items[i];
@@ -905,6 +1117,98 @@ static ScrDyn *scr_dyn_invoke_impl(
}
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "toReversed")) {
ScrDyn *out = scr_dyn_new_arr();
dyn_arr_reversed_copy(out, recv);
return out;
}
if (dyn_name_is(method, "with")) {
double indexD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
double index = indexD < 0 ? (double)len + indexD : indexD;
if (index < 0 || index >= (double)len) {
static const char message[] = "Invalid index";
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
return NULL;
}
ScrDyn *replacement = argc > 1 ? args[1] : scr_dyn_undefined();
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = 0; i < len; i++)
scr_dyn_arr_push(out, scr_dyn_retain((size_t)index == i ? replacement : recv->v.arr.items[i]));
return out;
}
if (dyn_name_is(method, "toSpliced")) {
double startD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
size_t start = dyn_rel_index(startD, len);
size_t delete_count = len - start;
if (argc > 1) {
double countD = dyn_index_arg(args, argc, 1, 0, what);
if (scr_exc_pending()) return NULL;
delete_count = countD <= 0 ? 0 : countD >= (double)delete_count ? delete_count : (size_t)countD;
}
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = 0; i < start; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i]));
for (size_t i = 2; i < argc; i++) scr_dyn_arr_push(out, scr_dyn_retain(args[i]));
for (size_t i = start + delete_count; i < len; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i]));
return out;
}
if (dyn_name_is(method, "fill")) {
double startD = dyn_index_arg(args, argc, 1, 0, what);
if (scr_exc_pending()) return NULL;
double endD = dyn_index_arg(args, argc, 2, (double)len, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
size_t start = dyn_rel_index(startD, len);
size_t end = dyn_rel_index(endD, len);
if (recv->v.arr.frozen && start < end) {
static const char message[] = "Cannot assign to read only array index";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
ScrDyn *fill = argc ? args[0] : scr_dyn_undefined();
for (size_t i = start; i < end; i++) {
ScrDyn *old = recv->v.arr.items[i];
recv->v.arr.items[i] = scr_dyn_retain(fill);
scr_dyn_release(old);
}
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "copyWithin")) {
double targetD = dyn_index_arg(args, argc, 0, 0, what);
if (scr_exc_pending()) return NULL;
double startD = dyn_index_arg(args, argc, 1, 0, what);
if (scr_exc_pending()) return NULL;
double endD = dyn_index_arg(args, argc, 2, (double)len, what);
if (scr_exc_pending()) return NULL;
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
size_t target = dyn_rel_index(targetD, len);
size_t start = dyn_rel_index(startD, len);
size_t end = dyn_rel_index(endD, len);
size_t count = end > start ? end - start : 0;
if (count > len - target) count = len - target;
if (recv->v.arr.frozen && count > 0) {
static const char message[] = "Cannot assign to read only array index";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
if (start < target && target < start + count) {
for (size_t i = count; i > 0; i--) {
ScrDyn *old = recv->v.arr.items[target + i - 1];
recv->v.arr.items[target + i - 1] = scr_dyn_retain(recv->v.arr.items[start + i - 1]);
scr_dyn_release(old);
}
} else {
for (size_t i = 0; i < count; i++) {
ScrDyn *old = recv->v.arr.items[target + i];
recv->v.arr.items[target + i] = scr_dyn_retain(recv->v.arr.items[start + i]);
scr_dyn_release(old);
}
}
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "forEach") || dyn_name_is(method, "map") ||
dyn_name_is(method, "filter") || dyn_name_is(method, "some") ||
dyn_name_is(method, "every") || dyn_name_is(method, "find") ||
@@ -920,7 +1224,9 @@ static ScrDyn *scr_dyn_invoke_impl(
* array argument then commit directly to the original static array. */
size_t n = len;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t i = 0; i < n && i < recv->v.arr.len; i++) {
for (size_t step = 0; step < n; step++) {
size_t i = step;
if (i >= recv->v.arr.len) continue;
ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]);
ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv);
if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; }
@@ -945,6 +1251,30 @@ static ScrDyn *scr_dyn_invoke_impl(
if (dyn_name_is(method, "findIndex")) return scr_dyn_new_num(-1);
return scr_dyn_retain(scr_dyn_undefined()); /* forEach */
}
if (dyn_name_is(method, "reduce") || dyn_name_is(method, "reduceRight")) {
if (!dyn_cb_check(args, argc)) return NULL;
bool backward = dyn_name_is(method, "reduceRight");
ScrDyn *acc = argc > 1 ? scr_dyn_retain(args[1]) : NULL;
ScrDyn *visible_recv = callback_recv ? callback_recv : recv;
for (size_t step = 0; step < len; step++) {
size_t i = backward ? len - 1 - step : step;
if (i >= recv->v.arr.len) continue;
ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]);
if (!acc) { acc = item; continue; }
ScrDyn *index = scr_dyn_new_num((double)i);
ScrDyn *call_args[] = { acc, item, index, visible_recv };
ScrDyn *next = scr_dyn_call(args[0], call_args, 4, "callback");
scr_dyn_release(index);
scr_dyn_release(item);
scr_dyn_release(acc);
if (!next) return NULL;
acc = next;
}
if (acc) return acc;
static const char message[] = "Reduce of empty array with no initial value";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
if (dyn_name_is(method, "flatMap")) {
/* JS Array.prototype.flatMap over a dyn array: map + a depth-1
* flatten. Native dyn-array results flatten element-by-element; a
@@ -1007,6 +1337,21 @@ static ScrDyn *scr_dyn_invoke_impl(
if (len > 1 && !dyn_arr_sort(recv, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) return NULL;
return scr_dyn_retain(recv);
}
if (dyn_name_is(method, "toSorted")) {
ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined();
if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) {
static const char message[] = "The comparison function must be either a function or undefined";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
ScrDyn *out = scr_dyn_new_arr();
for (size_t i = 0; i < len; i++) scr_dyn_arr_push(out, scr_dyn_retain(recv->v.arr.items[i]));
if (len > 1 && !dyn_arr_sort(out, cmp->kind == SCR_DYN_FUNC ? cmp : NULL)) {
scr_dyn_release(out);
return NULL;
}
return out;
}
if (dyn_arr_proto_unimpl(method)) {
dyn_throw_unsupported("Array", method);
return NULL;
@@ -1112,7 +1457,7 @@ ScrDyn *scr_dyn_define_props(ScrDyn *target, ScrDyn *descs) {
scr_dyn_isl_fence(target, "Object.defineProperties");
if (!scr_exc_pending()) scr_dyn_isl_fence(descs, "Object.defineProperties");
if (scr_exc_pending()) return NULL;
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC) {
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR) {
scr_throw_error_msg(SCR_ERR_TYPE, "Object.defineProperties called on non-object",
strlen("Object.defineProperties called on non-object"));
return NULL;
@@ -1193,3 +1538,13 @@ fail:
scr_dyn_release(pending);
return NULL;
}
ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors) {
ScrDyn *object = scr_dyn_obj_create(prototype);
if (!object) return NULL;
if (descriptors->kind == SCR_DYN_UNDEF) return object;
ScrDyn *defined = scr_dyn_define_props(object, descriptors);
if (!defined) { scr_dyn_release(object); return NULL; }
scr_dyn_release(defined);
return object;
}
+472 -54
View File
@@ -335,9 +335,11 @@ static SCR_TL size_t scr_dyn_free_count;
* capsules keep their conservative ownership edges. */
void scr_dyn_trace_v(void *ptr, ScrTraceVisit visit, void *ctx) {
ScrDyn *d = ptr;
visit(d->prototype, ctx);
switch (d->kind) {
case SCR_DYN_ARR:
for (size_t i = 0; i < d->v.arr.len; i++) visit(d->v.arr.items[i], ctx);
visit(d->v.arr.properties, ctx);
break;
case SCR_DYN_OBJ:
for (size_t i = 0; i < d->v.obj.len; i++) {
@@ -377,10 +379,14 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
d->kind = kind;
d->buffer = false;
d->null_proto = false;
d->prototype = NULL;
d->copied_from_native = false;
d->non_extensible = false;
if (kind == SCR_DYN_ARR) {
d->v.arr.len = 0; /* cap/items preserved from the node's last life */
d->v.arr.properties = NULL;
d->v.arr.sealed = false;
d->v.arr.frozen = false;
} else if (kind == SCR_DYN_OBJ) {
d->v.obj.len = 0; /* cap/entries preserved */
d->v.obj.source_identity = NULL;
@@ -395,6 +401,8 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
if (!fresh) scr_json_oom();
fresh->rc = 1;
fresh->kind = kind;
fresh->non_extensible = false;
fresh->prototype = NULL;
#ifdef SCR_RC_AUDIT
scr_live_dyns++;
#endif
@@ -405,6 +413,7 @@ static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag);
static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d);
static void scr_dyn_dispose(ScrDyn *d, bool collected) {
if (!collected) scr_dyn_release(d->prototype);
switch (d->kind) {
case SCR_DYN_BIGINT:
scr_bigint_release(d->v.bigint);
@@ -416,8 +425,10 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
scr_bytes_release(d->v.bytes);
break;
case SCR_DYN_ARR:
if (!collected)
if (!collected) {
for (size_t i = 0; i < d->v.arr.len; i++) scr_dyn_release(d->v.arr.items[i]);
scr_dyn_release(d->v.arr.properties);
}
break;
case SCR_DYN_OBJ:
for (size_t i = 0; i < d->v.obj.len; i++) {
@@ -527,6 +538,9 @@ bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len) {
return false;
}
static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *key, size_t length);
static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver);
/* Read through a typed capsule without exposing its native layout to a
* matcher for another type. The returned member owns its reference after
* the materialized view is released. */
@@ -538,8 +552,8 @@ static ScrDyn *scr_dyn_discriminant(const ScrDyn *d, const ScrStr *key) {
return out;
}
if (!d || d->kind != SCR_DYN_OBJ) return NULL;
ScrDyn *value = scr_dyn_obj_get(d, key->data, key->len);
return value ? scr_dyn_retain(value) : NULL;
return scr_dyn_property_owner(d, key->data, key->len)
? scr_dyn_obj_read(d, key->data, key->len) : NULL;
}
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value) {
@@ -750,6 +764,12 @@ ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i) {
return scr_dyn_retain(d->v.arr.items[(size_t)i]);
}
ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) {
return d->v.arr.properties
? scr_dyn_obj_read_receiver(d->v.arr.properties, key->data, key->len, d)
: scr_dyn_retain(scr_dyn_undefined());
}
/* Takes ownership of key (malloc'd) and value. Duplicate keys: the LATER
* value wins (like JS JSON.parse) — the old value is released and the new
* key buffer freed (the surviving entry keeps its original, equal key). */
@@ -757,6 +777,13 @@ static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *valu
for (size_t i = 0; i < obj->v.obj.len; i++) {
ScrDynEntry *e = &obj->v.obj.entries[i];
if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) {
if (e->accessor || !e->writable) {
free(key);
scr_dyn_release(value);
static const char message[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
ScrDyn *old = e->value;
e->value = value;
scr_dyn_release(old);
@@ -764,6 +791,13 @@ static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *valu
return;
}
}
if (obj->non_extensible) {
free(key);
scr_dyn_release(value);
static const char message[] = "Cannot add property, object is not extensible";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
if (obj->v.obj.len == obj->v.obj.cap) {
size_t cap = obj->v.obj.cap ? obj->v.obj.cap * 2 : 4;
ScrDynEntry *entries = realloc(obj->v.obj.entries, cap * sizeof *entries);
@@ -788,7 +822,7 @@ static void scr_dyn_obj_put(ScrDyn *obj, char *key, size_t key_len, ScrDyn *valu
/* THE undefined value: one immortal node (rc == SIZE_MAX skips every
* retain/release and the freelists never see it). */
ScrDyn *scr_dyn_undefined(void) {
static ScrDyn undef = { SIZE_MAX, SCR_DYN_UNDEF, { false } };
static ScrDyn undef = { .rc = SIZE_MAX, .kind = SCR_DYN_UNDEF };
return &undef;
}
@@ -848,6 +882,122 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) {
return d;
}
ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) {
if (scr_dyn_class_reflection_fence(prototype)) return NULL;
if (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_NULL) {
static const char message[] = "Object prototype may only be an Object or null";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
ScrDyn *object = prototype->kind == SCR_DYN_NULL ? scr_dyn_new_obj_null_proto() : scr_dyn_new_obj();
if (prototype->kind == SCR_DYN_OBJ) object->prototype = scr_dyn_retain(prototype);
return object;
}
ScrDyn *scr_dyn_get_prototype(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (object->kind == SCR_DYN_OBJ) {
if (object->prototype) return scr_dyn_retain(object->prototype);
if (object->null_proto) return scr_dyn_new_null();
}
if (object->kind == SCR_DYN_NULL || object->kind == SCR_DYN_UNDEF) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
static const char message[] = "Object.getPrototypeOf on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
return NULL;
}
ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) {
if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL;
if (object->kind != SCR_DYN_OBJ ||
(prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_NULL)) {
static const char message[] = "Object prototype may only be an Object or null";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
for (ScrDyn *current = prototype; current && current->kind == SCR_DYN_OBJ; current = current->prototype) {
if (current == object) {
static const char message[] = "Cyclic __proto__ value";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
}
if (object->non_extensible && (prototype->kind == SCR_DYN_NULL ? object->prototype != NULL || !object->null_proto : object->prototype != prototype)) {
static const char message[] = "Cannot set prototype of a non-extensible object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
ScrDyn *replacement = prototype->kind == SCR_DYN_NULL ? NULL : scr_dyn_retain(prototype);
scr_dyn_release(object->prototype);
object->prototype = replacement;
object->null_proto = replacement ? false : true;
return scr_dyn_retain(object);
}
ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; }
if (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) {
object->non_extensible = true;
if (object->kind == SCR_DYN_ARR && object->v.arr.properties)
object->v.arr.properties->non_extensible = true;
}
return scr_dyn_retain(object);
}
bool scr_dyn_is_extensible(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return false;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; }
return (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) && !object->non_extensible;
}
ScrDyn *scr_dyn_seal(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; }
if (object->kind == SCR_DYN_OBJ) {
object->non_extensible = true;
for (size_t i = 0; i < object->v.obj.len; i++) object->v.obj.entries[i].configurable = false;
}
if (object->kind == SCR_DYN_ARR) {
object->non_extensible = true;
object->v.arr.sealed = true;
if (object->v.arr.properties) scr_dyn_release(scr_dyn_seal(object->v.arr.properties));
}
return scr_dyn_retain(object);
}
bool scr_dyn_is_sealed(const ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return false;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; }
if (object->kind == SCR_DYN_ARR)
return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) &&
(!object->v.arr.properties || scr_dyn_is_sealed(object->v.arr.properties));
if (object->kind != SCR_DYN_OBJ) return true;
if (!object->non_extensible) return false;
for (size_t i = 0; i < object->v.obj.len; i++) if (object->v.obj.entries[i].configurable) return false;
return true;
}
static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *key, size_t length) {
for (const ScrDyn *current = object; current; current = current->prototype) {
if (current->kind != SCR_DYN_OBJ) continue;
for (size_t i = 0; i < current->v.obj.len; i++) {
const ScrDynEntry *entry = &current->v.obj.entries[i];
if (entry->key_len == length && memcmp(entry->key, key, length) == 0) return current;
}
}
return NULL;
}
static bool scr_dyn_has_object_prototype(const ScrDyn *object) {
const ScrDyn *current = object;
while (current->prototype) current = current->prototype;
return !current->null_proto;
}
void scr_dyn_proxy_unsupported(const char *operation) {
ScrJsonBuf msg;
scr_jb_init(&msg);
@@ -965,6 +1115,8 @@ bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key) {
return has;
}
static bool scr_dyn_canonical_own_index(const ScrStr *key, size_t length);
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (value->kind == SCR_DYN_FUNC) {
@@ -988,11 +1140,52 @@ ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (value->kind == SCR_DYN_ARR && key->len == 6 && memcmp(key->data, "length", 6) == 0) {
ScrDyn *out = scr_dyn_new_obj();
scr_dyn_obj_set(out, "value", 5, scr_dyn_new_num((double)value->v.arr.len));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(true));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(!value->v.arr.frozen));
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(false));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(false));
return out;
}
if (value->kind == SCR_DYN_ARR) {
if (value->v.arr.properties &&
scr_dyn_obj_get(value->v.arr.properties, key->data, key->len))
return scr_dyn_own_descriptor(value->v.arr.properties, key);
if (scr_dyn_canonical_own_index(key, value->v.arr.len)) {
size_t index = 0;
for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0');
ScrDyn *out = scr_dyn_new_obj();
scr_dyn_obj_set(out, "value", 5, scr_dyn_retain(value->v.arr.items[index]));
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(!value->v.arr.frozen));
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(true));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(!value->v.arr.sealed));
return out;
}
return scr_dyn_retain(scr_dyn_undefined());
}
if (value->kind == SCR_DYN_STR) {
size_t length = (size_t)scr_str_utf16_len(value->v.str);
bool length_key = key->len == 6 && memcmp(key->data, "length", 6) == 0;
size_t index = 0;
bool index_key = !length_key && scr_dyn_canonical_own_index(key, length);
if (!length_key && !index_key) return scr_dyn_retain(scr_dyn_undefined());
if (index_key) {
for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0');
}
ScrDyn *out = scr_dyn_new_obj();
if (length_key) {
scr_dyn_obj_set(out, "value", 5, scr_dyn_new_num((double)length));
} else {
ScrStr *character = scr_str_char_at(value->v.str, (double)index);
scr_dyn_obj_set(out, "value", 5, scr_dyn_new_str(character));
scr_str_release(character);
}
scr_dyn_obj_set(out, "writable", 8, scr_dyn_new_bool(false));
scr_dyn_obj_set(out, "enumerable", 10, scr_dyn_new_bool(index_key));
scr_dyn_obj_set(out, "configurable", 12, scr_dyn_new_bool(false));
return out;
}
if (value->kind == SCR_DYN_BOOL || value->kind == SCR_DYN_NUM) {
return scr_dyn_retain(scr_dyn_undefined());
}
if (value->kind == SCR_DYN_OBJ) {
ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key);
if (!entry) return scr_dyn_retain(scr_dyn_undefined());
@@ -1945,8 +2138,8 @@ static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) {
static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) {
for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]);
}
static bool scr_dyn_key_is_index(const char *key, size_t len, double *out);
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key);
static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver);
/* Property tables belong to closures, not temporary checked wrappers. Seed
* the configurable, non-writable builtin members once so redefinitions and
@@ -2011,6 +2204,25 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor)
scr_dyn_release(defined);
return scr_dyn_retain(target);
}
if (target->kind == SCR_DYN_ARR) {
ScrStr *name = scr_dyn_property_key(key);
if (!name) return NULL;
double index;
bool special = name->len == 6 && memcmp(name->data, "length", 6) == 0;
special = special || scr_dyn_key_is_index(name->data, name->len, &index);
scr_str_release(name);
if (special) {
static const char message[] = "Object.defineProperty on an array index or length is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
return NULL;
}
if (!target->v.arr.properties) target->v.arr.properties = scr_dyn_new_obj();
target->v.arr.properties->non_extensible = target->non_extensible;
ScrDyn *defined = scr_dyn_define_property(target->v.arr.properties, key, descriptor);
if (!defined) return NULL;
scr_dyn_release(defined);
return scr_dyn_retain(target);
}
if (target->kind != SCR_DYN_OBJ) {
static const char msg[] = "Object.defineProperty called on non-object";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
@@ -2140,7 +2352,7 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key) {
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR) {
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_PROXY && target->kind != SCR_DYN_FUNC && target->kind != SCR_DYN_ARR && target->kind != SCR_DYN_STR && target->kind != SCR_DYN_BOOL && target->kind != SCR_DYN_NUM) {
static const char msg[] = "Object.getOwnPropertyDescriptor on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
@@ -2733,7 +2945,7 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons
return NULL;
}
}
if (d->kind == SCR_DYN_OBJ && d->null_proto) {
if (d->kind == SCR_DYN_OBJ && !scr_dyn_property_owner(d, "toString", 8) && !scr_dyn_has_object_prototype(d)) {
ScrJsonBuf b;
scr_jb_init(&b);
for (size_t i = 0; i < what->len; i++) scr_jb_putc(&b, what->data[i]);
@@ -2802,16 +3014,18 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
if (d->kind == SCR_DYN_OBJ) {
static const char *const hint[2] = { "toString", "valueOf" };
for (int i = 0; i < 2; i++) {
ScrDyn *m = scr_dyn_obj_get(d, hint[i], strlen(hint[i])); /* borrowed */
if (!m && i == 0 && !d->null_proto) {
const ScrDyn *owner = scr_dyn_property_owner(d, hint[i], strlen(hint[i]));
ScrDyn *m = owner ? scr_dyn_obj_read(d, hint[i], strlen(hint[i])) : NULL;
if (!m && i == 0 && scr_dyn_has_object_prototype(d)) {
return scr_str_new("[object Object]", 15);
}
if (!m || m->kind != SCR_DYN_FUNC) continue;
if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; }
/* OrdinaryToPrimitive performs a method call, not a bare function
* call: an own coercion hook observes the source object as `this`. */
scr_dyn_this_push_dyn(d);
ScrDyn *r = scr_dyn_call(m, NULL, 0, hint[i]);
scr_dyn_this_pop();
scr_dyn_release(m);
if (!r) return NULL; /* the method threw — pending */
if (scr_dyn_to_primitive_result_is_object(r)) {
scr_dyn_release(r); /* non-primitive answer: try the next method */
@@ -2869,23 +3083,25 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
case SCR_DYN_OBJ: {
static const char *const hint[2] = { "valueOf", "toString" };
for (int i = 0; i < 2; i++) {
ScrDyn *m = scr_dyn_obj_get(d, hint[i], strlen(hint[i])); /* borrowed */
const ScrDyn *owner = scr_dyn_property_owner(d, hint[i], strlen(hint[i]));
ScrDyn *m = owner ? scr_dyn_obj_read(d, hint[i], strlen(hint[i])) : NULL;
if (!m) {
if (!d->null_proto && i == 0) {
if (scr_dyn_has_object_prototype(d) && i == 0) {
/* Inherited Object.prototype.valueOf returns the object, so the
* number-hint protocol advances to toString. */
continue;
}
if (!d->null_proto && i == 1) {
if (scr_dyn_has_object_prototype(d) && i == 1) {
*out = NAN; /* Number("[object Object]") */
return true;
}
continue;
}
if (m->kind != SCR_DYN_FUNC) continue;
if (m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; }
scr_dyn_this_push_dyn(d);
ScrDyn *r = scr_dyn_call(m, NULL, 0, hint[i]);
scr_dyn_this_pop();
scr_dyn_release(m);
if (!r) return false;
if (scr_dyn_to_primitive_result_is_object(r)) {
scr_dyn_release(r);
@@ -2950,9 +3166,10 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) {
if (d->kind == SCR_DYN_OBJ) {
static const char *const methods[] = { "valueOf", "toString" };
for (size_t i = 0; i < 2; i++) {
ScrDyn *method = scr_dyn_obj_get(d, methods[i], strlen(methods[i]));
const ScrDyn *owner = scr_dyn_property_owner(d, methods[i], strlen(methods[i]));
ScrDyn *method = owner ? scr_dyn_obj_read(d, methods[i], strlen(methods[i])) : NULL;
if (!method) {
if (i == 1 && !d->null_proto) {
if (i == 1 && scr_dyn_has_object_prototype(d)) {
ScrStr *str = scr_str_new("[object Object]", 15);
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
@@ -2960,8 +3177,7 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) {
}
continue;
}
if (method->kind != SCR_DYN_FUNC) continue;
scr_dyn_retain(method);
if (method->kind != SCR_DYN_FUNC) { scr_dyn_release(method); continue; }
scr_dyn_this_push_dyn(d);
ScrDyn *out = scr_dyn_call(method, NULL, 0, methods[i]);
scr_dyn_this_pop();
@@ -3039,7 +3255,12 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
scr_dyn_release(materialized);
return out;
}
if (v->kind == SCR_DYN_OBJ) return scr_dyn_obj_get(v, key->data, key->len) != NULL;
if (v->kind == SCR_DYN_OBJ) {
for (const ScrDyn *current = v; current && current->kind == SCR_DYN_OBJ; current = current->prototype) {
if (scr_dyn_obj_get(current, key->data, key->len)) return true;
}
return false;
}
if (v->kind == SCR_DYN_FUNC) {
ScrDyn *descriptor = scr_dyn_own_descriptor(v, key);
bool present = descriptor && descriptor->kind != SCR_DYN_UNDEF;
@@ -3058,16 +3279,20 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
}
if (v->kind == SCR_DYN_ARR) {
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true;
if (key->len == 0 || key->len > 15) return false;
if (v->v.arr.properties && scr_dyn_obj_get(v->v.arr.properties, key->data, key->len)) return true;
if (key->len == 0 || key->len > 10) return false;
size_t idx = 0;
for (size_t i = 0; i < key->len; i++) {
char c = key->data[i];
if (c < '0' || c > '9') return false;
if (i > 0 && idx == 0) return false; /* a leading zero is no canonical index */
if (idx > (4294967294ULL - (size_t)(c - '0')) / 10) return false;
idx = idx * 10 + (size_t)(c - '0');
}
return idx < v->v.arr.len;
}
if (v->kind == SCR_DYN_STR)
return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(v->v.str));
return false;
}
@@ -3091,6 +3316,28 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con
return;
}
}
if (!scr_dyn_entry(recv, key)) {
for (const ScrDyn *current = recv->prototype; current && current->kind == SCR_DYN_OBJ; current = current->prototype) {
ScrDynEntry *inherited = scr_dyn_entry((ScrDyn *)current, key);
if (!inherited) continue;
if (inherited->accessor && inherited->setter) {
ScrDyn *args[] = {value};
ScrDyn *setter = scr_dyn_retain(inherited->setter);
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(setter, args, 1, "setter");
scr_dyn_this_pop();
scr_dyn_release(setter);
scr_dyn_release(result);
return;
}
if (inherited->accessor || !inherited->writable) {
static const char message[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
break;
}
}
if (recv->non_extensible && !scr_dyn_entry(recv, key)) {
ScrJsonBuf buffer;
scr_jb_init(&buffer);
@@ -3127,17 +3374,63 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
return;
}
if (recv->kind == SCR_DYN_ARR) {
/* An INDEX write on a dyn array (`args[i] = v` — the variadic-rest
* rebuild): a canonical numeric key sets/extends the element, holes
* padding with undefined exactly like JS length growth. Non-index
* keys keep the throw below (dyn arrays carry no expando table). */
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) {
if (recv->v.arr.frozen) {
static const char message[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
double number;
if (!scr_dyn_number_coerce_js(value, &number)) return;
if (!isfinite(number) || number < 0 || number > 4294967295.0 || trunc(number) != number) {
static const char message[] = "Invalid array length";
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
return;
}
size_t length = (size_t)number;
if (length > recv->v.arr.len && recv->non_extensible) {
static const char message[] = "Cannot add property, object is not extensible";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
if (length > recv->v.arr.len) {
static const char message[] = "Sparse array lengths are not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return;
}
if (length < recv->v.arr.len && recv->v.arr.sealed) {
static const char message[] = "Cannot delete non-configurable property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
while (recv->v.arr.len > length) scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]);
return;
}
/* Canonical indexes extend the dense portion; other names live in the
* array's own property table. */
size_t idx = 0;
int is_index = key->len > 0 && !(key->len > 1 && key->data[0] == '0');
for (size_t i = 0; is_index && i < key->len; i++) {
if (key->data[i] < '0' || key->data[i] > '9') is_index = 0;
else if (idx > (4294967295ULL - (size_t)(key->data[i] - '0')) / 10) is_index = 0;
else idx = idx * 10 + (size_t)(key->data[i] - '0');
}
if (is_index) {
if (is_index && idx < 4294967295ULL) {
if (recv->v.arr.frozen && idx < recv->v.arr.len) {
static const char message[] = "Cannot assign to read only property";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
if (recv->non_extensible && idx >= recv->v.arr.len) {
static const char message[] = "Cannot add property, object is not extensible";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
if (idx > recv->v.arr.len + 1000000) {
static const char message[] = "Sparse array writes are not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return;
}
while (recv->v.arr.len <= idx) {
scr_dyn_arr_push(recv, scr_dyn_retain(scr_dyn_undefined()));
}
@@ -3146,6 +3439,10 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
scr_dyn_release(old);
return;
}
if (!recv->v.arr.properties) recv->v.arr.properties = scr_dyn_new_obj();
recv->v.arr.properties->non_extensible = recv->non_extensible;
scr_dyn_key_set(recv->v.arr.properties, key, value);
return;
}
if (recv->kind == SCR_DYN_HANDLE) {
scr_dyn_handle_key_set(recv, key, value);
@@ -3993,6 +4290,11 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
scr_error_sync_cause(recv, key);
return;
}
if (recv->kind == SCR_DYN_ARR && recv->v.arr.properties &&
scr_dyn_obj_get(recv->v.arr.properties, key->data, key->len)) {
scr_dyn_key_delete(recv->v.arr.properties, key, strict);
return;
}
if (recv->kind == SCR_DYN_UNDEF || recv->kind == SCR_DYN_NULL) {
const char *message = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
@@ -4480,17 +4782,19 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) {
}
static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver) {
for (size_t i = 0; i < d->v.obj.len; i++) {
ScrDynEntry *entry = &d->v.obj.entries[i];
if (entry->key_len != key_len || memcmp(entry->key, key, key_len) != 0) continue;
if (!entry->accessor) return scr_dyn_retain(entry->value);
if (!entry->getter) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *getter = scr_dyn_retain(entry->getter);
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter");
scr_dyn_this_pop();
scr_dyn_release(getter);
return result;
for (const ScrDyn *current = d; current && current->kind == SCR_DYN_OBJ; current = current->prototype) {
for (size_t i = 0; i < current->v.obj.len; i++) {
ScrDynEntry *entry = &current->v.obj.entries[i];
if (entry->key_len != key_len || memcmp(entry->key, key, key_len) != 0) continue;
if (!entry->accessor) return scr_dyn_retain(entry->value);
if (!entry->getter) return scr_dyn_retain(scr_dyn_undefined());
ScrDyn *getter = scr_dyn_retain(entry->getter);
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter");
scr_dyn_this_pop();
scr_dyn_release(getter);
return result;
}
}
ScrDyn *own = scr_dyn_obj_get(d, key, key_len);
if (own) return scr_dyn_retain(own);
@@ -4587,6 +4891,68 @@ ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) {
return keys;
}
ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) {
if (value->kind == SCR_DYN_OBJ) return scr_dyn_obj_own_keys(value);
if (value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES) {
ScrDyn *keys = scr_dyn_new_arr();
size_t length = value->kind == SCR_DYN_ARR ? value->v.arr.len :
value->kind == SCR_DYN_BYTES ? value->v.bytes->len :
(size_t)scr_str_utf16_len(value->v.str);
for (size_t i = 0; i < length; i++) {
char name[24];
int size = snprintf(name, sizeof name, "%zu", i);
scr_dyn_arr_push(keys, scr_dyn_objwalk_key(name, (size_t)size));
}
if (value->kind != SCR_DYN_BYTES)
scr_dyn_arr_push(keys, scr_dyn_objwalk_key("length", 6));
if (value->kind == SCR_DYN_ARR && value->v.arr.properties) {
ScrDyn *properties = scr_dyn_obj_own_keys(value->v.arr.properties);
for (size_t i = 0; i < properties->v.arr.len; i++)
scr_dyn_arr_push(keys, scr_dyn_retain(properties->v.arr.items[i]));
scr_dyn_release(properties);
}
return keys;
}
if (value->kind == SCR_DYN_BOOL || value->kind == SCR_DYN_NUM)
return scr_dyn_new_arr();
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
static const char message[] = "Object.getOwnPropertyNames on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
return NULL;
}
ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (object->kind == SCR_DYN_UNDEF || object->kind == SCR_DYN_NULL) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
if (object->kind == SCR_DYN_PROXY || object->kind == SCR_DYN_TYPED_REF ||
object->kind == SCR_DYN_JSVAL || object->kind == SCR_DYN_HANDLE) {
static const char message[] = "Object.getOwnPropertyDescriptors on this value is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
return NULL;
}
ScrDyn *names = scr_dyn_get_own_property_names(object);
if (!names) return NULL;
ScrDyn *result = scr_dyn_new_obj();
for (size_t i = 0; i < names->v.arr.len; i++) {
ScrStr *name = names->v.arr.items[i]->v.str;
ScrDyn *descriptor = scr_dyn_own_descriptor(object, name);
if (!descriptor) { scr_dyn_release(names); scr_dyn_release(result); return NULL; }
if (descriptor->kind != SCR_DYN_UNDEF)
scr_dyn_obj_set(result, name->data, name->len, descriptor);
else scr_dyn_release(descriptor);
}
scr_dyn_release(names);
return result;
}
static void scr_dyn_objwalk_push(ScrDyn *out, ScrObjWalk mode, const char *key,
size_t key_len, ScrDyn *value /* borrowed */) {
switch (mode) {
@@ -4748,24 +5114,30 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
scr_dyn_arr_push(out, pair);
}
}
if (v->kind == SCR_DYN_ARR && v->v.arr.properties) {
ScrDyn *names = scr_dyn_obj_own_keys(v->v.arr.properties);
for (size_t i = 0; i < names->v.arr.len; i++) {
if (!scr_dyn_objwalk_entry(out, v->v.arr.properties, mode, names->v.arr.items[i]->v.str, v)) {
scr_dyn_release(names);
scr_dyn_release(out);
return NULL;
}
}
scr_dyn_release(names);
}
return out;
}
if (v->kind == SCR_DYN_STR) {
/* JS indexes strings by UTF-16 code units; the checked-dynamic tree stores UTF-8.
* Code points walk one at a time — an astral code point stays WHOLE
* (one entry where JS lists two lone surrogates; documented
* approximation, the keys stay dense). */
const ScrStr *s = v->v.str;
size_t unit = 0;
for (size_t i = 0; i < s->len;) {
unsigned char c = (unsigned char)s->data[i];
size_t step = c < 0x80 ? 1 : c < 0xe0 ? 2 : c < 0xf0 ? 3 : 4;
size_t length = (size_t)scr_str_utf16_len(v->v.str);
for (size_t unit = 0; unit < length; unit++) {
char key[24];
int klen = snprintf(key, sizeof key, "%zu", unit);
if (mode == SCR_OBJWALK_KEYS) {
scr_dyn_arr_push(out, scr_dyn_objwalk_key(key, (size_t)klen));
} else {
ScrDyn *val = scr_dyn_objwalk_key(s->data + i, step);
ScrStr *character = scr_str_char_at(v->v.str, (double)unit);
ScrDyn *val = scr_dyn_new_str(character);
scr_str_release(character);
if (mode == SCR_OBJWALK_VALUES) {
scr_dyn_arr_push(out, val);
} else {
@@ -4775,8 +5147,6 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
scr_dyn_arr_push(out, pair);
}
}
unit++;
i += step;
}
return out;
}
@@ -4787,13 +5157,29 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_KEYS); }
/* Native dictionaries have only Object.prototype or a null prototype;
* neither contributes enumerable keys. Snapshot names without invoking
* getters; the loop rechecks live ownership before visiting each name. */
/* Snapshot enumerable names from the receiver and each live prototype.
* Non-enumerable own names shadow names farther up the chain. */
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) {
if (scr_dyn_class_reflection_fence(v)) return NULL;
switch (v->kind) {
case SCR_DYN_OBJ:
case SCR_DYN_OBJ: {
ScrDyn *keys = scr_dyn_new_arr();
ScrDyn *seen = scr_dyn_new_obj_null_proto();
for (const ScrDyn *current = v; current; current = current->prototype) {
ScrDyn *own = scr_dyn_obj_own_keys(current);
for (size_t i = 0; i < own->v.arr.len; i++) {
ScrDyn *key = own->v.arr.items[i];
ScrStr *name = key->v.str;
if (scr_dyn_has_own(seen, name)) continue;
scr_dyn_obj_set(seen, name->data, name->len, scr_dyn_retain(scr_dyn_undefined()));
if (scr_dyn_obj_enumerable(current, name->data, name->len))
scr_dyn_arr_push(keys, scr_dyn_retain(key));
}
scr_dyn_release(own);
}
scr_dyn_release(seen);
return keys;
}
case SCR_DYN_ARR:
return scr_dyn_obj_keys(v);
case SCR_DYN_STR: {
@@ -5067,6 +5453,7 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
}
if (v->kind == SCR_DYN_ARR) {
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true;
if (v->v.arr.properties && scr_dyn_obj_get(v->v.arr.properties, key->data, key->len)) return true;
return scr_dyn_canonical_own_index(key, v->v.arr.len);
}
if (v->kind == SCR_DYN_STR) {
@@ -5090,6 +5477,25 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) {
}
return false;
}
bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key) {
if (scr_dyn_class_reflection_fence(value)) return false;
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return false;
}
if (value->kind == SCR_DYN_OBJ)
return scr_dyn_obj_enumerable(value, key->data, key->len);
if (value->kind == SCR_DYN_STR)
return scr_dyn_canonical_own_index(key, (size_t)scr_str_utf16_len(value->v.str));
if (value->kind == SCR_DYN_ARR)
return scr_dyn_canonical_own_index(key, value->v.arr.len) ||
(value->v.arr.properties && scr_dyn_obj_enumerable(value->v.arr.properties, key->data, key->len));
if (value->kind == SCR_DYN_BYTES)
return scr_dyn_canonical_own_index(key, value->v.bytes->len);
return false;
}
ScrDyn *scr_dyn_obj_values(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_VALUES); }
ScrDyn *scr_dyn_obj_entries(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_ENTRIES); }
@@ -6261,7 +6667,7 @@ ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path) {
}
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) {
if (d->kind == SCR_DYN_OBJ && d->null_proto) {
if (d->kind == SCR_DYN_OBJ && !scr_dyn_property_owner(d, "toString", 8) && !scr_dyn_has_object_prototype(d)) {
ScrJsonBuf b;
scr_jb_init(&b);
for (size_t i = 0; i < what->len; i++) scr_jb_putc(&b, what->data[i]);
@@ -6273,6 +6679,14 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
}
ScrDyn *scr_dyn_freeze(ScrDyn *value) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (value->kind == SCR_DYN_ARR) {
value->non_extensible = true;
value->v.arr.sealed = true;
value->v.arr.frozen = true;
if (value->v.arr.properties) scr_dyn_release(scr_dyn_freeze(value->v.arr.properties));
return scr_dyn_retain(value);
}
if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) {
value->non_extensible = true;
for (size_t i = 0; i < value->v.obj.len; i++) {
@@ -6291,6 +6705,10 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) {
}
bool scr_dyn_is_frozen(const ScrDyn *value) {
if (scr_dyn_class_reflection_fence(value)) return false;
if (value->kind == SCR_DYN_ARR)
return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) &&
(!value->v.arr.properties || scr_dyn_is_frozen(value->v.arr.properties));
if (value->kind == SCR_DYN_OBJ && !value->copied_from_native) {
if (!value->non_extensible) return false;
for (size_t i = 0; i < value->v.obj.len; i++) {
+17 -2
View File
@@ -3454,14 +3454,14 @@ struct ScrDyn {
/* A deep typed-to-dyn copy has no stable native reference identity.
* Weak collections must refuse it instead of weakly tracking its box. */
bool copied_from_native;
bool non_extensible; /* ordinary checked objects; fits the header padding */
bool non_extensible; /* native objects and arrays; fits the header padding */
union {
bool b;
double num;
ScrStr *str; /* owned */
ScrBytes *bytes; /* owned (SCR_DYN_BYTES) */
ScrBigInt *bigint; /* owned (SCR_DYN_BIGINT) */
struct { size_t len; size_t cap; ScrDyn **items; } arr; /* owned */
struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; } arr; /* owned */
struct {
size_t len;
size_t cap;
@@ -3517,6 +3517,9 @@ struct ScrDyn {
struct { ScrJsval *cell; } jsval;
struct { ScrDyn *target; ScrDyn *handler; } proxy; /* both owned */
} v;
/* Keep the checked value payload at its established offset: generated
* native converters read array and object fields directly. */
struct ScrDyn *prototype;
};
static inline ScrDyn *scr_dyn_retain(ScrDyn *d) {
@@ -3559,10 +3562,12 @@ ScrDyn *scr_dyn_obj_keys(const ScrDyn *v);
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v);
/* Snapshot all own string keys of a SCR_DYN_OBJ in JS order. Returns +1. */
ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v);
ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value);
/* Object.hasOwn over a dyn receiver: OBJ member presence, ARR index
* bounds ("length" included); nullish receivers throw Node's ToObject
* TypeError; every other kind answers false. */
bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key);
bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key);
/* Object.assign over dyn values (+1 target back; ToObject TypeError on a
* nullish target). Sources visit own keys in order and copy entries that
* are enumerable when reached; ARR/STR/BYTES expose index keys. Nullish
@@ -3623,6 +3628,15 @@ void *scr_dyn_obj_source_cast(const ScrDyn *d,
/* Object.create(null): the fresh null-prototype dictionary (see the
* null_proto flavor flag above). */
ScrDyn *scr_dyn_new_obj_null_proto(void);
ScrDyn *scr_dyn_obj_create(ScrDyn *prototype);
ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors);
ScrDyn *scr_dyn_get_prototype(ScrDyn *object);
ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object);
ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype);
ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object);
bool scr_dyn_is_extensible(ScrDyn *object);
ScrDyn *scr_dyn_seal(ScrDyn *object);
bool scr_dyn_is_sealed(const ScrDyn *object);
ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler);
ScrDyn *scr_dyn_proxy_get(const ScrDyn *proxy, const ScrStr *key);
bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key);
@@ -3698,6 +3712,7 @@ void scr_weak_dispose(void *object);
* +1. */
double scr_dyn_arr_len(const ScrDyn *d);
ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i);
ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key);
void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value);
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor);
ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key);
@@ -0,0 +1,14 @@
let reads = 0;
class Provider {
get callable() {
reads++;
return function(value) { return value + 1; };
}
}
const provider = new Provider();
console.log(provider.callable(4), reads);
class PrivateProvider {
#method(value = 2) { return value + 3; }
get method() { return this.#method; }
}
console.log(new PrivateProvider().method());
@@ -0,0 +1,19 @@
class Base {
value() { return 'base'; }
}
class Derived extends Base {
value() { return 'derived'; }
}
const base = new Base();
const derived = new Derived();
console.log(base['value'](), derived['value']());
let asBase = derived;
console.log(asBase['value']());
class Keyword {
w\u0069th() { return 42; }
}
console.log(new Keyword()['with']());
class Numeric {
[7]() { return 'seven'; }
}
console.log(new Numeric()[7]());
@@ -0,0 +1,13 @@
const record = { 0: 2, 1: 4, 2: 6, length: 3 };
console.log(Array.prototype.findLast.call(record, value => value < 6));
console.log(Array.prototype.findLastIndex.call(record, value => value < 6));
console.log(Array.prototype.findLast.call(record, value => value > 9));
console.log(Array.prototype.findLastIndex.call(record, value => value > 9));
const sparse = { 0: 1, 2: 3, length: 3 };
const seen = [];
console.log(Array.prototype.findLastIndex.call(sparse, (value, index) => {
seen.push(`${index}:${value}`);
return value === undefined;
}));
console.log(seen.join(','));
@@ -0,0 +1,13 @@
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 };
console.log(Array.prototype.slice.call(arrayLike).join(','));
console.log(Array.prototype.slice.call(arrayLike, 1, 3).join(','));
console.log(Array.prototype.slice.call(arrayLike, -2).join(','));
console.log(Array.prototype.slice.call(arrayLike, 0, -1).join(','));
console.log(Array.prototype.slice.call(arrayLike, 8).length);
console.log(Array.prototype.slice.call(arrayLike, -9, 2).join(','));
console.log(Array.prototype.slice.call(arrayLike, 3, 1).length);
console.log(Array.prototype.slice.call(arrayLike, 1, undefined).join(','));
const values = JSON.parse('[1,2,3,4]');
console.log(Array.prototype.slice.call(values, -3, -1).join(','));
console.log(values.join(','));
@@ -0,0 +1,15 @@
const values = JSON.parse('[4,1,3,2]');
console.log(values.toReversed().join(','), values.join(','));
console.log(values.toSorted().join(','), values.join(','));
console.log(values.toSorted((a, b) => b - a).join(','));
console.log(values.toSpliced(1, 2, 8, 9).join(','), values.join(','));
console.log(values.toSpliced(-2, 1).join(','));
console.log(values.with(-1, 7).join(','), values.join(','));
try { values.with(9, 0); } catch (error) { console.log(error.name); }
Object.freeze(values);
console.log(values.toReversed().join(','));
console.log(values.toSorted().join(','));
console.log(values.toSpliced(0, 1, 6).join(','));
console.log(values.with(0, 5).join(','));
console.log(values.join(','));
@@ -0,0 +1,17 @@
const values = JSON.parse('[1,2,3,4,5]');
console.log(values.fill(8, 1, -1) === values, values.join(','));
console.log(values.copyWithin(2, 0, 3) === values, values.join(','));
console.log(values.copyWithin(0, 2, 5).join(','));
console.log(values.copyWithin(-2, 0, 2).join(','));
const frozen = JSON.parse('[1,2,3]');
Object.freeze(frozen);
try { frozen.fill(9); } catch (error) { console.log(error.name); }
try { frozen.copyWithin(1, 0); } catch (error) { console.log(error.name); }
console.log(frozen.join(','));
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
sealed.fill(7, 1);
sealed.copyWithin(0, 1);
console.log(sealed.join(','));
+21
View File
@@ -0,0 +1,21 @@
const empty = JSON.parse('[]');
Object.freeze(empty);
console.log(empty.sort().length, empty.reverse().length);
console.log(empty.fill(4).length, empty.copyWithin(0, 0).length);
for (const operation of [() => empty.push(), () => empty.pop(), () => empty.shift(), () => empty.unshift(), () => empty.splice(0, 0)]) {
try { operation(); } catch (error) { console.log(error.name); }
}
const one = JSON.parse('[1]');
Object.freeze(one);
console.log(one.sort().join(','), one.reverse().join(','));
console.log(one.fill(9, 0, 0).join(','), one.copyWithin(0, 1).join(','));
try { one.fill(9); } catch (error) { console.log(error.name); }
try { one.copyWithin(0, 0); } catch (error) { console.log(error.name); }
console.log(one.join(','));
const two = JSON.parse('[2,1]');
Object.freeze(two);
try { two.sort(); } catch (error) { console.log(error.name); }
try { two.reverse(); } catch (error) { console.log(error.name); }
console.log(two.join(','));
@@ -0,0 +1,28 @@
const values = JSON.parse('[4,2,4,1]');
console.log(values.indexOf(4), values.indexOf(4, 1), values.indexOf(4, -2));
console.log(values.lastIndexOf(4), values.lastIndexOf(4, 1), values.lastIndexOf(4, -3));
console.log(values.includes(4, 3), values.includes(4, -2), values.includes(4, Infinity));
console.log(values.findLast((value) => value > 1), values.findLastIndex((value) => value > 1));
console.log(values.findLast((value) => value > 9), values.findLastIndex((value) => value > 9));
console.log(values.slice(-3, -1).join(','), values.slice(99).join(','));
console.log(values.toReversed().join(','), values.join(','));
console.log(values.toSpliced(0, 0, 8).join(','), values.join(','));
console.log(values.with(-4, 7).join(','), values.join(','));
const shifted = JSON.parse('[1,2,3]');
console.log(shifted.unshift(8, 9), shifted.join(','));
console.log(shifted.unshift(), shifted.join(','));
console.log(shifted.splice(1, 2, 4, 5, 6).join(','), shifted.join(','));
console.log(shifted.splice(-1, 1, 7).join(','), shifted.join(','));
const copied = JSON.parse('[0,1,2,3,4]');
console.log(copied.copyWithin(1, 3).join(','));
console.log(copied.copyWithin(-2, 0, 2).join(','));
console.log(copied.fill(6, -3, -1).join(','));
console.log(copied.fill(7, 0, 0).join(','));
const nested = JSON.parse('[1,[2,[3]],4]');
console.log(nested.flat(0).join(','));
console.log(nested.flat(1).join(','));
console.log(nested.flat(2).join(','));
console.log(JSON.stringify(nested));
+27
View File
@@ -0,0 +1,27 @@
const sealed = JSON.parse('[1,2]');
Object.seal(sealed);
console.log(Object.isExtensible(sealed), Object.isSealed(sealed), Object.isFrozen(sealed));
console.log(Object.getOwnPropertyDescriptor(sealed, '0').configurable);
console.log(Object.getOwnPropertyDescriptor(sealed, '0').writable);
sealed[0] = 4;
console.log(sealed[0]);
for (const operation of [() => sealed.pop(), () => sealed.shift(), () => sealed.push(3)]) {
try { operation(); } catch (error) { console.log(error.name); }
}
console.log(sealed.join(','));
const frozen = JSON.parse('[3,5]');
Object.freeze(frozen);
console.log(Object.isSealed(frozen), Object.isFrozen(frozen));
console.log(Object.getOwnPropertyDescriptor(frozen, '0').writable);
console.log(Object.getOwnPropertyDescriptor(frozen, 'length').writable);
for (const operation of [() => { frozen[0] = 9; }, () => frozen.reverse(), () => frozen.sort()]) {
try { operation(); } catch (error) { console.log(error.name); }
}
console.log(frozen.join(','));
const empty = JSON.parse('[]');
Object.preventExtensions(empty);
console.log(Object.isSealed(empty), Object.isFrozen(empty));
try { empty.push(1); } catch (error) { console.log(error.name); }
console.log(empty.length);
+19
View File
@@ -0,0 +1,19 @@
const values = JSON.parse('[1,2,3,4]');
values.length = 2;
console.log(values.length, values.join(','), Object.keys(values).join(','));
values.length = 2;
console.log(values.length, Object.hasOwn(values, '2'));
values[2] = 8;
console.log(values.join(','));
try { values.length = -1; } catch (error) { console.log(error.name); }
console.log(values.length);
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
try { sealed.length = 1; } catch (error) { console.log(error.name); }
console.log(sealed.length, sealed.join(','));
const frozen = JSON.parse('[1,2]');
Object.freeze(frozen);
try { frozen.length = 1; } catch (error) { console.log(error.name); }
console.log(frozen.length, frozen.join(','));
@@ -0,0 +1,21 @@
const values = JSON.parse('[3,5]');
values.extra = 7;
Object.defineProperty(values, 'hidden', { value: 9, writable: true, configurable: true });
console.log(values.extra, values.hidden, values.length);
console.log(Object.keys(values).join(','));
console.log(Object.getOwnPropertyNames(values).join(','));
console.log(Object.hasOwn(values, 'extra'), 'hidden' in values);
console.log(Object.getOwnPropertyDescriptor(values, 'hidden').enumerable);
console.log(Object.getOwnPropertyDescriptors(values).extra.value);
const keys = [];
for (const key in values) keys.push(key);
console.log(keys.join(','));
delete values.hidden;
console.log(Object.hasOwn(values, 'hidden'));
Object.seal(values);
console.log(Object.isSealed(values), Object.isFrozen(values));
console.log(Object.getOwnPropertyDescriptor(values, 'extra').configurable);
values.extra = 8;
console.log(values.extra);
Object.freeze(values);
console.log(Object.isFrozen(values), Object.getOwnPropertyDescriptor(values, 'extra').writable);
@@ -0,0 +1,18 @@
const values = JSON.parse('[1,2,3,4]');
console.log(values.reduce((a, b) => a + b));
console.log(values.reduceRight((a, b) => a - b));
console.log(values.reduce((a, b) => a + b, 10));
console.log(values.findLast(x => x < 4), values.findLastIndex(x => x < 4));
console.log(values.findLast(x => x > 9), values.findLastIndex(x => x > 9));
try { JSON.parse('[]').reduce((a, b) => a + b); } catch (error) { console.log(error.name); }
console.log(JSON.parse('[]').reduce((a, b) => a + b, 5));
console.log(JSON.parse('[7]').reduce((a, b) => a + b));
console.log(JSON.parse('[7]').reduceRight((a, b) => a + b));
const shrinking = JSON.parse('[1,2,3]');
const seen = [];
const found = shrinking.findLast((value, index) => {
seen.push(`${index}:${value}`);
if (index === 2) shrinking.length = 1;
return false;
});
console.log(found, seen.join(','), shrinking.join(','));
@@ -0,0 +1,11 @@
const values = JSON.parse('[1,2,1,3]');
console.log(values.indexOf(1), values.indexOf(1, 1), values.indexOf(1, -2));
console.log(values.indexOf(2, 3), values.indexOf(2, -9));
console.log(values.lastIndexOf(1), values.lastIndexOf(1, 1), values.lastIndexOf(1, -2));
console.log(values.lastIndexOf(3, -9), values.lastIndexOf(3, 99));
console.log(values.includes(1, 2), values.includes(1, 3), values.includes(3, -1));
console.log(values.includes(2, 8), values.includes(2, -8));
const empty = JSON.parse('[]');
let conversions = 0;
const from = { valueOf() { conversions++; return 0; } };
console.log(empty.indexOf(1, from), empty.lastIndexOf(1, from), empty.includes(1, from), conversions);
@@ -0,0 +1,18 @@
const values = JSON.parse('[0,1,2,3]');
let conversions = 0;
const start = { valueOf() { conversions++; return 1; } };
const count = { valueOf() { conversions++; return 2; } };
console.log(values.splice(start, count, 8, 9).join(','));
console.log(values.join(','), conversions);
console.log(values.splice(-1, 0, 7).join(','), values.join(','));
const sealed = JSON.parse('[1,2]');
Object.seal(sealed);
let sealedConversions = 0;
const sealedStart = { valueOf() { sealedConversions++; return 0; } };
console.log(sealed.splice(sealedStart, 1, 3).join(','), sealed.join(','), sealedConversions);
const frozen = JSON.parse('[4,5]');
Object.freeze(frozen);
try { frozen.splice(0, 1); } catch (error) { console.log(error.name); }
console.log(frozen.join(','));
+18
View File
@@ -0,0 +1,18 @@
const values = JSON.parse('[0,1,2,3,4]');
console.log(values.splice(1, 2, 8, 9).join(','), values.join(','));
console.log(values.splice(-2, 1).join(','), values.join(','));
console.log(values.splice(1, 0, 7).join(','), values.join(','));
console.log(values.splice(2).join(','), values.join(','));
console.log(values.splice(9, 2).join(','), values.join(','));
const nested = JSON.parse('[1,[2,[3,[4]]],5]');
console.log(JSON.stringify(nested.flat()));
console.log(JSON.stringify(nested.flat(2)));
console.log(JSON.stringify(nested.flat(0)));
console.log(JSON.stringify(nested));
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
console.log(sealed.splice(1, 1, 7).join(','), sealed.join(','));
try { sealed.splice(1, 1); } catch (error) { console.log(error.name); }
console.log(sealed.join(','));
+12
View File
@@ -0,0 +1,12 @@
const object = JSON.parse('{"first":1,"second":2}');
Object.defineProperty(object, 'hidden', { value: 3 });
const descriptors = Object.getOwnPropertyDescriptors(object);
console.log(Object.keys(descriptors).join(','));
console.log(descriptors.first.value, descriptors.first.writable, descriptors.first.enumerable, descriptors.first.configurable);
console.log(descriptors.hidden.value, descriptors.hidden.writable, descriptors.hidden.enumerable, descriptors.hidden.configurable);
console.log(Object.getOwnPropertyDescriptors(3));
console.log(Object.keys(Object.getOwnPropertyDescriptors('cat')).join(','));
const parent = JSON.parse('{"inherited":4}');
const child = Object.create(parent);
child.own = 5;
console.log(Object.keys(Object.getOwnPropertyDescriptors(child)).join(','));
@@ -0,0 +1,11 @@
const parent = JSON.parse('{"first":1,"second":2}');
const child = Object.create(parent);
console.log(Object.keys(parent).join(','));
console.log(Object.getOwnPropertyNames(parent).join(','));
console.log('first' in child);
const parentKeys = [];
for (const key in parent) parentKeys.push(key);
console.log(parentKeys.join(','));
const keys = [];
for (const key in child) keys.push(key);
console.log(keys.join(','));
@@ -0,0 +1,50 @@
const target = JSON.parse('{"visible":1}');
Object.defineProperty(target, 'hidden', { value: 2, writable: true, configurable: true });
console.log(Object.keys(target).join(','));
const absent = JSON.parse('null');
try { Object.prototype.propertyIsEnumerable.call(absent, 'x'); }
catch (error) { console.log(error.name); }
console.log(Object.getOwnPropertyNames(target).join(','));
console.log(target.hidden, Object.hasOwn(target, 'hidden'), target.propertyIsEnumerable('hidden'));
const first = Object.getOwnPropertyDescriptor(target, 'hidden');
console.log(first.value, first.writable, first.enumerable, first.configurable);
Object.defineProperty(target, 'hidden', { value: 3, enumerable: true });
const second = Object.getOwnPropertyDescriptor(target, 'hidden');
console.log(second.value, second.writable, second.enumerable, second.configurable);
console.log(Object.keys(target).join(','), Object.values(target).join(','));
const parent = JSON.parse('{"inherited":4}');
const child = Object.create(parent);
const omittedDescriptors = Object.create(parent, undefined);
console.log(omittedDescriptors.inherited, Object.getOwnPropertyNames(omittedDescriptors).length);
Object.defineProperty(child, 'own', { value: 5, enumerable: true, writable: true, configurable: true });
console.log(Object.keys(child).join(','), Object.getOwnPropertyNames(child).join(','));
console.log(Object.hasOwn(child, 'inherited'), 'inherited' in child);
const inherited = Object.getOwnPropertyDescriptor(child, 'inherited');
console.log(inherited === undefined, child.inherited, Object.getPrototypeOf(child) === parent);
const visited = [];
for (const key in child) visited.push(key);
console.log(visited.join(','));
Object.defineProperty(child, 'inherited', { value: 6, enumerable: false, writable: true });
const shadowed = [];
for (const key in child) shadowed.push(key);
console.log(child.inherited, parent.inherited, shadowed.join(','));
const fixed = JSON.parse('{"item":7}');
Object.freeze(fixed);
const frozen = Object.getOwnPropertyDescriptors(fixed);
console.log(frozen.item.value, frozen.item.writable, frozen.item.configurable);
console.log(Object.isExtensible(fixed), Object.isSealed(fixed), Object.isFrozen(fixed));
try { Object.defineProperty(fixed, 'item', { value: 8 }); }
catch (error) { console.log(error.name); }
try { Object.defineProperty(fixed, 'new', { value: 9 }); }
catch (error) { console.log(error.name); }
console.log(fixed.item, Object.hasOwn(fixed, 'new'));
const nullProto = Object.create(null);
nullProto.key = 10;
console.log(Object.getPrototypeOf(nullProto) === null, Object.keys(nullProto).join(','));
Object.preventExtensions(nullProto);
console.log(Object.isExtensible(nullProto), Object.isSealed(nullProto), Object.isFrozen(nullProto));
Object.seal(nullProto);
console.log(Object.isSealed(nullProto), Object.isFrozen(nullProto));
+44
View File
@@ -0,0 +1,44 @@
const parent = JSON.parse('{"value": 1}');
const child = Object.create(parent);
console.log(child.value, Object.keys(child).length, Object.hasOwn(child, 'value'), 'value' in child);
console.log(child.propertyIsEnumerable('value'), parent.propertyIsEnumerable('value'));
parent.value = 2;
console.log(child.value);
child.value = 3;
console.log(child.value, parent.value, Object.hasOwn(child, 'value'));
const grandchild = Object.create(child);
console.log(grandchild.value);
Object.defineProperty(parent, 'inherited', { get: function() { return this.value + 1; }, enumerable: true });
console.log(grandchild.inherited, Object.keys(grandchild).length);
console.log(grandchild.propertyIsEnumerable('inherited'), parent.propertyIsEnumerable('inherited'));
console.log(Object.getOwnPropertyNames(grandchild).length, Object.getOwnPropertyDescriptor(grandchild, 'value'));
console.log(Object.getPrototypeOf(child) === parent, Object.getPrototypeOf(grandchild) === child);
const inheritedKeys = [];
for (const key in grandchild) inheritedKeys.push(key);
console.log(inheritedKeys.join(','));
Object.defineProperty(child, 'value', { value: 5, enumerable: false, writable: true, configurable: true });
const shadowedKeys = [];
for (const key in grandchild) shadowedKeys.push(key);
console.log(grandchild.value, shadowedKeys.join(','));
const replacement = JSON.parse('{"replacement": 8}');
Object.setPrototypeOf(grandchild, replacement);
console.log(Object.getPrototypeOf(grandchild) === replacement, grandchild.replacement, 'value' in grandchild);
const descriptors = JSON.parse('{"own":{"value":11,"enumerable":true,"writable":true}}');
const described = Object.create(replacement, descriptors);
console.log(described.own, described.replacement, Object.keys(described).join(','));
const locked = JSON.parse('{"first":1,"second":2}');
console.log(Object.isExtensible(locked), Object.isSealed(locked), Object.isFrozen(locked));
Object.preventExtensions(locked);
console.log(Object.isExtensible(locked), Object.isSealed(locked), Object.isFrozen(locked));
try { locked.third = 3; } catch (error) { console.log(error.name); }
console.log(Object.hasOwn(locked, 'third'));
const sealed = JSON.parse('{"first":1}');
Object.seal(sealed);
console.log(Object.isSealed(sealed), Object.isFrozen(sealed), Object.getOwnPropertyDescriptor(sealed, 'first').configurable);
sealed.first = 4;
console.log(sealed.first);
const frozen = JSON.parse('{"first":1}');
Object.freeze(frozen);
console.log(Object.isSealed(frozen), Object.isFrozen(frozen), Object.getOwnPropertyDescriptor(frozen, 'first').writable);
try { frozen.first = 4; } catch (error) { console.log(error.name); }
console.log(frozen.first);
@@ -0,0 +1,14 @@
console.log(Object.getOwnPropertyDescriptor(true, 'x'));
console.log(Object.getOwnPropertyDescriptor(7, 'x'));
const text = Object.getOwnPropertyDescriptor('abc', '1');
console.log(text.value, text.writable, text.enumerable, text.configurable);
const length = Object.getOwnPropertyDescriptor('abc', 'length');
console.log(length.value, length.writable, length.enumerable, length.configurable);
function item(value) { return value; }
item[1] = 'first';
item[1] = 'second';
const own = Object.getOwnPropertyDescriptor(item, '1');
console.log(own.value, own.writable, own.enumerable, own.configurable);
Object.defineProperty(item, 'fixed', { value: 42, enumerable: false, writable: false, configurable: false });
const fixed = Object.getOwnPropertyDescriptor(item, 'fixed');
console.log(item['fixed'], fixed.value, fixed.writable, fixed.enumerable, fixed.configurable);
+2 -15
View File
@@ -1,17 +1,4 @@
// Object.create's fence battery: only the null-prototype dictionary
// lowers in a static build (Object.create(null) — corpus 2581/2582).
// Every other prototype is a NAMED fence, not an own-copy stand-in: Node
// lists NO own keys on the created object (Object.keys/inspect/JSON all
// answer empty) and mutating the prototype afterwards shows through it
// LIVE — observations a copy answers wrong silently.
// A static record prototype.
// Statically shaped records cannot serve as live checked-dynamic prototypes.
const base = { indent: 2 };
const viaRecord = Object.create(base);
// A checked-dynamic (dyn) prototype.
const dynProto: object = JSON.parse('{"a":1}');
const viaDyn = Object.create(dynProto);
// The properties-descriptor form.
const withDescriptors = Object.create(null, { a: { value: 1, enumerable: true } });
base.indent = 4;
@@ -1,26 +1,8 @@
object-create-fences.ts:10:19 - error SC2020: 'Object.create over '{ indent: number }' prototypes' is part of the standard library types but has no scriptc lowering yet
object-create-fences.ts:3:19 - error SC2020: 'Object.create over '{ indent: number }' prototypes' is part of the standard library types but has no scriptc lowering yet
9 | const base = { indent: 2 };
10 | const viaRecord = Object.create(base);
| ^~~~~~~~~~~~~~~~~~~
11 |
2 | const base = { indent: 2 };
3 | const viaRecord = Object.create(base);
| ^~~~~~~~~~~~~~~~~~~
4 | base.indent = 4;
hint: the compiled representations have no prototype chain, and an own-copy would answer wrong observably (Node lists NO own keys on the created object, and prototype mutations show through it live) — only Object.create(null) lowers
object-create-fences.ts:14:16 - error SC2020: 'Object.create over 'unknown' prototypes' is part of the standard library types but has no scriptc lowering yet
13 | const dynProto: object = JSON.parse('{"a":1}');
14 | const viaDyn = Object.create(dynProto);
| ^~~~~~~~~~~~~~~~~~~~~~~
15 |
hint: the compiled representations have no prototype chain, and an own-copy would answer wrong observably (Node lists NO own keys on the created object, and prototype mutations show through it live) — only Object.create(null) lowers
object-create-fences.ts:17:25 - error SC2020: 'Object.create with a properties-descriptor argument' is part of the standard library types but has no scriptc lowering yet
16 | // The properties-descriptor form.
17 | const withDescriptors = Object.create(null, { a: { value: 1, enumerable: true } });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18 |
hint: create first, then assign: const o = Object.create(null); o.k = v
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)