feat(compiler): lower borrowed Array.prototype calls (#558)

This commit is contained in:
Chris Tate
2026-09-28 20:13:02 -05:00
committed by GitHub
parent 31332427b6
commit e1da936307
12 changed files with 440 additions and 16 deletions
+2
View File
@@ -4449,6 +4449,8 @@ function emitDynamicLibCall(state: LibCallState): Temp {
return finish(`scr_dyn_define_property(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "dyn.getOwnPropertyDescriptor":
return finish(`scr_dyn_get_own_property_descriptor(${arg(0)}, ${arg(1)})`);
case "dyn.arrayProtoCall":
return finish(`scr_dyn_array_proto_call(${arg(0)}, ${arg(1)}, ${arg(2)})`);
case "dyn.hasKey":
// `k in v` with a runtime key: the dyn presence answer (both
// borrowed, no allocation, never throws).
+10 -2
View File
@@ -1295,6 +1295,14 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
break;
}
d.push(` if (d->kind != SCR_DYN_OBJ) { scr_dyn_check_fail(path, ${want}, d); return NULL; }`);
if (shape.fields.length === 0 && !shape.indexValue) {
const sourceAccessor = `${emitter.toDynHelper(t)}_source_access`;
const rc = vAdapters(t);
d.push(` {`);
d.push(` ${cDecl(t, "sc_source")} = (${cType(t).trim()})scr_dyn_obj_source_cast(d, &${sourceAccessor}, &${rc.retain});`);
d.push(` if (sc_source) return sc_source;`);
d.push(` }`);
}
d.push(` ${cDecl(t, "r")} = ${mangleRecordNew(t.shapeId)}();`);
for (const f of shape.fields) {
const keyLit = cStringLiteral(Buffer.from(f.name, "utf8"));
@@ -1591,14 +1599,14 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
const carriesListenerIdentity = shape.fields.some(
(f) => f.name === "handleEvent" && f.type.kind === "func",
);
if (carriesListenerIdentity) {
if ((shape.fields.length === 0 && !shape.indexValue) || carriesListenerIdentity) {
const rc = vAdapters(t);
sourceAccessor = {
name: `${name}_source_access`,
release: rc.release,
};
emitter.walkerProtos.push(
`static ScrDyn *${sourceAccessor.name}(void *v, bool materialize); /* live listener source ${key} */`,
`static ScrDyn *${sourceAccessor.name}(void *v, bool materialize); /* record source ${key} */`,
);
d.push(
` ScrDyn *d = scr_dyn_new_obj_with_identity(v, &${rc.retain}, &${sourceAccessor.name});`,
+17 -2
View File
@@ -920,6 +920,21 @@ export class LlDyn {
break;
}
requireKind(DYN_KIND.OBJ, "dcr");
if (shape.fields.length === 0 && !shape.indexValue) {
const sourceAccessor = `${this.toDynHelper(t)}_source_access`;
const rc = vAdapters(host, t);
host.declare(`declare ptr @scr_dyn_obj_source_cast(ptr, ptr, ptr)`);
const source = B.tmp();
const found = B.tmp();
const lSource = B.newLabel("dcr.source");
const lCopy = B.newLabel("dcr.copy");
B.line(`${source} = call ptr @scr_dyn_obj_source_cast(ptr %d, ptr @${sourceAccessor}, ptr ${rc.retain})`);
B.line(`${found} = icmp ne ptr ${source}, null`);
B.condBr(found, lSource, lCopy);
B.startBlock(lSource);
B.terminate(`ret ptr ${source}`);
B.startBlock(lCopy);
}
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
for (const f of shape.fields) {
const fieldWant = host.cstr(dynDesc(f.type, this.host.recordsById, this.host.unionsById));
@@ -1398,7 +1413,7 @@ export class LlDyn {
const carriesListenerIdentity = shape.fields.some(
(f) => f.name === "handleEvent" && f.type.kind === "func",
);
if (carriesListenerIdentity) {
if ((shape.fields.length === 0 && !shape.indexValue) || carriesListenerIdentity) {
const rc = vAdapters(host, t);
sourceAccessor = {
name: `${name}_source_access`,
@@ -1664,7 +1679,7 @@ export class LlDyn {
);
if (sourceAccessor) {
this.defs.push(
`define internal ptr @${sourceAccessor.name}(ptr %v, i1 %materialize) ${FN_ATTRS} { ; live listener source ${key}`,
`define internal ptr @${sourceAccessor.name}(ptr %v, i1 %materialize) ${FN_ATTRS} { ; record source ${key}`,
`entry:`,
` br i1 %materialize, label %snapshot, label %release`,
`snapshot:`,
@@ -545,6 +545,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.defineProps": "scr_dyn_define_props",
"dyn.defineProperty": "scr_dyn_define_property",
"dyn.getOwnPropertyDescriptor": "scr_dyn_get_own_property_descriptor",
"dyn.arrayProtoCall": "scr_dyn_array_proto_call",
"dyn.typeof": "scr_dyn_typeof",
"dyn.objectTag": "scr_dyn_object_tag",
"dyn.toString": "scr_dyn_to_string_method",
@@ -4706,6 +4706,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// TestContext surface (t.test/t.skip/t.diagnostic), t.assert.*.
lowerer.lowerTestMethodCall(expr, expr.expression) ??
lowerer.lowerTimeoutMethodCall(expr, expr.expression) ??
lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ??
lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ??
lowerObjectPrototypeCall(lowerer, expr, expr.expression) ??
lowerStringPrototypeCall(lowerer, expr, expr.expression) ??
@@ -5330,6 +5331,40 @@ function immediateObjectTagReceiver(lowerer: Lowerer, node: ts.Expression): { ta
return null;
}
const ARRAY_BORROW_METHODS = new Set([
"at", "every", "filter", "find", "findIndex", "forEach", "includes",
"indexOf", "lastIndexOf", "map", "reduce", "reduceRight", "some",
]);
function lowerArrayPrototypeBorrowCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.dynamic || !isJsSourceFile(call.getSourceFile()) || call.questionDotToken ||
access.questionDotToken || access.name.text !== "call" ||
!ts.isPropertyAccessExpression(access.expression) ||
call.arguments.some(ts.isSpreadElement)) return null;
const method = access.expression;
if (method.questionDotToken || !ARRAY_BORROW_METHODS.has(method.name.text) ||
!ts.isPropertyAccessExpression(method.expression) ||
method.expression.questionDotToken || method.expression.name.text !== "prototype" ||
!lowerer.isStdlibGlobal(method.expression.expression, "Array") ||
!lowerer.isStdlibMember(method)) return null;
const loc = locOf(call);
const receiver = call.arguments[0]
? lowerer.lowerExprExpecting(call.arguments[0]!, DYN)
: { kind: "dynFrom", value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: DYN, loc } as IrExpr;
const args = call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN));
return {
kind: "libCall", fn: "dyn.arrayProtoCall", args: [
receiver,
{ kind: "strLit", value: method.name.text, type: STRING, loc },
{ kind: "dynArrLit", elems: args, type: DYN, loc },
], type: DYN, loc,
};
}
function lowerObjectOwnPrototypeCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -6742,7 +6742,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
// dyn vs dyn (`context.actual !== context.exact` —
// test/common's exit accounting): the runtime's whole-dyn
// strict equality — scalars by value, units by kind,
// reference kinds by node identity (scr_dyn_strict_eq).
// reference kinds by identity (scr_dyn_strict_eq).
scalarSide.type.kind === "dyn")
) {
return {
+4 -1
View File
@@ -2099,6 +2099,7 @@ export type IrLibFn =
| "dyn.defineProps"
| "dyn.defineProperty"
| "dyn.getOwnPropertyDescriptor"
| "dyn.arrayProtoCall"
/** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value,
* borrowed; result: an owned string) — the dyn kind's JS answer:
* undefined→"undefined", null/object/array/bytes→"object" (JS's oldest
@@ -6820,7 +6821,8 @@ export function moduleUsesDynInvoke(mod: IrModule): boolean {
return;
}
const node = v as { kind?: unknown; fn?: unknown };
if (node.kind === "dynInvoke" || (node.kind === "libCall" && node.fn === "dyn.defineProps")) {
if (node.kind === "dynInvoke" || (node.kind === "libCall" &&
(node.fn === "dyn.defineProps" || node.fn === "dyn.arrayProtoCall"))) {
found = true;
return;
}
@@ -7896,6 +7898,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.defineProps",
"dyn.defineProperty",
"dyn.getOwnPropertyDescriptor",
"dyn.arrayProtoCall",
"process.chdir",
"fs.realpathSync",
"fs.realpathNativeSync",
+1
View File
@@ -127,6 +127,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.defineProps": { argTypes: [DYN, DYN], result: DYN },
"dyn.defineProperty": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.getOwnPropertyDescriptor": { argTypes: [DYN, DYN], result: DYN },
"dyn.arrayProtoCall": { argTypes: [DYN, STRING, DYN], result: DYN },
"dyn.typeof": { argTypes: [DYN], result: STRING },
"dyn.objectTag": { argTypes: [DYN], result: STRING },
"module.registryInit": { argTypes: [F64], result: VOID },
+273
View File
@@ -214,6 +214,276 @@ static bool dyn_cb_check(ScrDyn *const *args, size_t argc) {
return false;
}
static bool dyn_borrow_is(const ScrStr *method, const char *name) {
size_t len = strlen(name);
return method->len == len && memcmp(method->data, name, len) == 0;
}
static bool dyn_borrow_index(const ScrStr *key, size_t *out) {
if (!key->len || (key->len > 1 && key->data[0] == '0')) return false;
size_t n = 0;
for (size_t i = 0; i < key->len; i++) {
unsigned digit = (unsigned)(key->data[i] - '0');
if (digit > 9 || n > (SIZE_MAX - digit) / 10) return false;
n = n * 10 + digit;
}
*out = n;
return true;
}
static ScrDyn *dyn_borrow_get(ScrDyn *recv, const ScrStr *key) {
if (recv->kind == SCR_DYN_TYPED_REF) {
ScrDyn *snapshot = scr_dyn_typed_ref_materialize(recv);
if (!snapshot) return NULL;
ScrDyn *result = dyn_borrow_get(snapshot, key);
scr_dyn_release(snapshot);
return result;
}
if (recv->kind == SCR_DYN_OBJ) return scr_dyn_obj_read(recv, key->data, key->len);
if (recv->kind == SCR_DYN_PROXY) return scr_dyn_proxy_get(recv, key);
if (recv->kind == SCR_DYN_JSVAL) return scr_dyn_isl_key_get(recv, key);
if (recv->kind == SCR_DYN_FUNC) {
ScrDyn *value = scr_dyn_fn_get(recv, key->data, key->len);
return value ? value : scr_dyn_retain(scr_dyn_undefined());
}
if (recv->kind == SCR_DYN_ARR || recv->kind == SCR_DYN_STR || recv->kind == SCR_DYN_BYTES) {
if (key->len == 6 && memcmp(key->data, "length", 6) == 0) {
double length = recv->kind == SCR_DYN_ARR ? (double)recv->v.arr.len
: recv->kind == SCR_DYN_STR ? scr_str_utf16_len(recv->v.str)
: (double)recv->v.bytes->len;
return scr_dyn_new_num(length);
}
size_t index;
if (!dyn_borrow_index(key, &index)) return scr_dyn_retain(scr_dyn_undefined());
if (recv->kind == SCR_DYN_ARR) return scr_dyn_arr_at(recv, (double)index);
if (recv->kind == SCR_DYN_BYTES) {
return index < recv->v.bytes->len
? scr_dyn_new_num(scr_bytes_get(recv->v.bytes, (double)index))
: scr_dyn_retain(scr_dyn_undefined());
}
if ((double)index >= scr_str_utf16_len(recv->v.str)) return scr_dyn_retain(scr_dyn_undefined());
ScrStr *unit = scr_str_slice(recv->v.str, (double)index, (double)index + 1);
ScrDyn *value = scr_dyn_new_str(unit);
scr_str_release(unit);
return value;
}
return scr_dyn_retain(scr_dyn_undefined());
}
static bool dyn_borrow_has(ScrDyn *recv, const ScrStr *key) {
if (recv->kind == SCR_DYN_STR || recv->kind == SCR_DYN_BYTES) {
size_t index;
return dyn_borrow_index(key, &index) &&
(recv->kind == SCR_DYN_STR
? (double)index < scr_str_utf16_len(recv->v.str)
: index < recv->v.bytes->len);
}
if (recv->kind == SCR_DYN_FUNC) {
ScrDyn *value = scr_dyn_fn_get(recv, key->data, key->len);
bool present = value != NULL;
scr_dyn_release(value);
return present;
}
if (recv->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(recv, "Array.prototype borrowed property presence");
return scr_dyn_has_key(recv, key);
}
static ScrStr *dyn_borrow_key(size_t index) {
char text[32];
int n = snprintf(text, sizeof text, "%zu", index);
return scr_str_new(text, (size_t)n);
}
static bool dyn_borrow_length(ScrDyn *recv, size_t *out) {
ScrStr *key = scr_str_new("length", 6);
ScrDyn *value = dyn_borrow_get(recv, key);
scr_str_release(key);
if (!value) return false;
double number;
bool ok = scr_dyn_number_coerce_js(value, &number);
scr_dyn_release(value);
if (!ok) return false;
if (isnan(number) || number <= 0) { *out = 0; return true; }
number = fmin(trunc(number), 9007199254740991.0);
if (number > (double)SIZE_MAX) {
static const char msg[] = "array-like length exceeds the native index range";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return false;
}
*out = (size_t)number;
return true;
}
static bool dyn_borrow_iteration(size_t step) {
if (step < 1000000) return true;
static const char msg[] = "array-like iteration over one million entries is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return false;
}
static bool dyn_borrow_number(const ScrDyn *value, double *out) {
if (!scr_dyn_number_coerce_js(value, out)) return false;
if (isnan(*out)) *out = 0;
else if (isfinite(*out)) *out = trunc(*out);
return true;
}
static bool dyn_borrow_present(ScrDyn *recv, size_t index) {
ScrStr *key = dyn_borrow_key(index);
bool found = dyn_borrow_has(recv, key);
scr_str_release(key);
return found;
}
static ScrDyn *dyn_borrow_item(ScrDyn *recv, size_t index) {
ScrStr *key = dyn_borrow_key(index);
ScrDyn *value = dyn_borrow_get(recv, key);
scr_str_release(key);
return value;
}
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";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
if (!args || args->kind != SCR_DYN_ARR) scr_trap("scriptc: invalid Array.prototype arguments\n");
ScrDyn *const *argv = args->v.arr.items;
size_t argc = args->v.arr.len;
size_t len;
if (!dyn_borrow_length(recv, &len)) return NULL;
bool index_of = dyn_borrow_is(method, "indexOf");
bool last_index_of = dyn_borrow_is(method, "lastIndexOf");
bool includes = dyn_borrow_is(method, "includes");
if (index_of || last_index_of || includes) {
if (len == 0) return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
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;
double first = from < 0 ? (double)len + from : from;
if (last_index_of) {
if (from >= 0) first = fmin(from, (double)len - 1);
if (first >= (double)len) first = (double)len - 1;
for (size_t step = 0; first >= 0 && step <= (size_t)first; step++) {
if (len > 1000000 && !dyn_borrow_iteration(step)) return NULL;
size_t i = (size_t)first - step;
if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) return NULL; continue; }
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) return NULL;
bool found = scr_dyn_strict_eq(item, needle);
scr_dyn_release(item);
if (found) return scr_dyn_new_num((double)i);
}
return scr_dyn_new_num(-1);
}
if (first < 0) first = 0;
if (first >= (double)len) return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
for (size_t i = (size_t)first; i < len; i++) {
if (len > 1000000 && !dyn_borrow_iteration(i - (size_t)first)) return NULL;
if (!includes && !dyn_borrow_present(recv, i)) { if (scr_exc_pending()) return NULL; continue; }
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) return NULL;
bool found = scr_dyn_strict_eq(item, needle) ||
(includes && item->kind == SCR_DYN_NUM && needle->kind == SCR_DYN_NUM &&
isnan(item->v.num) && isnan(needle->v.num));
scr_dyn_release(item);
if (found) return includes ? scr_dyn_new_bool(true) : scr_dyn_new_num((double)i);
}
return includes ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1);
}
if (dyn_borrow_is(method, "at")) {
double offset = 0;
if (argc && !dyn_borrow_number(argv[0], &offset)) return NULL;
double index = offset < 0 ? (double)len + offset : offset;
return index >= 0 && index < (double)len
? dyn_borrow_item(recv, (size_t)index)
: scr_dyn_retain(scr_dyn_undefined());
}
bool each = dyn_borrow_is(method, "forEach");
bool map = dyn_borrow_is(method, "map");
bool filter = dyn_borrow_is(method, "filter");
bool some = dyn_borrow_is(method, "some");
bool every = dyn_borrow_is(method, "every");
bool find = dyn_borrow_is(method, "find");
bool find_index = dyn_borrow_is(method, "findIndex");
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)) {
static const char msg[] = "Array.prototype method is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
if (!dyn_cb_check(argv, argc)) return NULL;
ScrDyn *cb = argv[0];
if (reduce || reduce_right) {
ScrDyn *acc = argc > 1 ? scr_dyn_retain(argv[1]) : NULL;
for (size_t step = 0; step < len; step++) {
if (len > 1000000 && !dyn_borrow_iteration(step)) { scr_dyn_release(acc); return NULL; }
size_t i = reduce_right ? len - 1 - step : step;
if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(acc); return NULL; } continue; }
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) { scr_dyn_release(acc); return NULL; }
if (!acc) { acc = item; continue; }
ScrDyn *idx = scr_dyn_new_num((double)i);
ScrDyn *cbargs[4] = { acc, item, idx, recv };
ScrDyn *next = scr_dyn_call(cb, cbargs, 4, "callback");
scr_dyn_release(idx);
scr_dyn_release(item);
scr_dyn_release(acc);
if (!next) return NULL;
acc = next;
}
if (acc) return acc;
static const char msg[] = "Reduce of empty array with no initial value";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
}
ScrDyn *out = (map || filter) ? scr_dyn_new_arr() : NULL;
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);
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
if (!present) {
if (map) {
scr_dyn_release(out);
static const char msg[] = "Array.prototype.map over sparse array-like values is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
continue;
}
ScrDyn *item = dyn_borrow_item(recv, i);
if (!item) { scr_dyn_release(out); return NULL; }
scr_dyn_this_push_dyn(this_arg);
ScrDyn *result = dyn_call_cb(cb, item, i, recv);
scr_dyn_this_pop();
if (!result) { scr_dyn_release(item); scr_dyn_release(out); return NULL; }
if (map) {
scr_dyn_arr_push(out, result);
} else {
bool truthy = scr_dyn_truthy(result);
scr_dyn_release(result);
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); }
}
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);
return scr_dyn_retain(scr_dyn_undefined());
}
static bool dyn_name_is(const char *m, const char *n) { return strcmp(m, n) == 0; }
/* Handle receivers: hand the whole call to the tag's ops (installed by
@@ -484,6 +754,9 @@ static ScrDyn *scr_dyn_invoke_impl(
return NULL;
}
if ((recv->kind == SCR_DYN_NUM || recv->kind == SCR_DYN_BOOL || recv->kind == SCR_DYN_STR) &&
dyn_name_is(method, "valueOf")) return scr_dyn_retain(recv);
if (recv->kind == SCR_DYN_STR) {
ScrStr *s = recv->v.str;
if (dyn_name_is(method, "slice")) {
+18 -3
View File
@@ -737,6 +737,13 @@ bool scr_dyn_obj_same_source(const ScrDyn *a, const ScrDyn *b) {
a->v.obj.source_identity == b->v.obj.source_identity;
}
void *scr_dyn_obj_source_cast(const ScrDyn *d,
ScrDyn *(*source_access)(void *, bool), void *(*source_retain)(void *)) {
if (!d || d->kind != SCR_DYN_OBJ || !d->v.obj.source_identity ||
d->v.obj.source_access != source_access) return NULL;
return source_retain(d->v.obj.source_identity);
}
ScrDyn *scr_dyn_new_obj_null_proto(void) {
ScrDyn *d = scr_dyn_alloc(SCR_DYN_OBJ);
d->null_proto = true;
@@ -3870,10 +3877,16 @@ void *scr_dyn_retain_v(void *d) { return scr_dyn_retain((ScrDyn *)d); }
void scr_dyn_release_v(void *d) { scr_dyn_release((ScrDyn *)d); }
/* JS === over two dyn values: scalars by value (NaN false, ±0 equal via
* C ==; strings bytewise), units by kind, everything reference-shaped by
* node IDENTITY (the checked-dynamic tree's object identity). Never throws. */
* C ==; strings bytewise), units by kind, references by node identity or
* by their retained static record source when one exists. Never throws. */
bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) {
if (a->kind != b->kind) return false;
if (a->kind != b->kind) {
if (a->kind == SCR_DYN_TYPED_REF && b->kind == SCR_DYN_OBJ)
return b->v.obj.source_identity && a->v.typed_ref.ptr == b->v.obj.source_identity;
if (a->kind == SCR_DYN_OBJ && b->kind == SCR_DYN_TYPED_REF)
return a->v.obj.source_identity && a->v.obj.source_identity == b->v.typed_ref.ptr;
return false;
}
switch (a->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL: return true;
@@ -3882,6 +3895,8 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) {
case SCR_DYN_STR:
return a->v.str->len == b->v.str->len &&
memcmp(a->v.str->data, b->v.str->data, a->v.str->len) == 0;
case SCR_DYN_OBJ:
return a == b || scr_dyn_obj_same_source(a, b);
case SCR_DYN_FUNC:
/* The ScrDyn box is a boundary artifact — one closure crossing the
* dyn boundary twice is still ONE JS function value, so identity
+8 -7
View File
@@ -3445,9 +3445,8 @@ struct ScrDyn {
size_t cap;
ScrDynEntry *entries;
/* Optional identity of the typed record that produced this ordinary
* deep-copy snapshot. EventTarget uses it to recognize the same
* EventListenerObject across repeated static→dyn crossings without
* changing generic dyn-object `===` semantics. */
* snapshot. Empty records and callback interfaces can retain their
* source across repeated static-to-dynamic crossings. */
/* Owned through source_access(source_identity, false). Callback
* interfaces may request a fresh snapshot with the true arm. */
void *source_identity;
@@ -3576,14 +3575,15 @@ ScrDyn *scr_dyn_new_num(double n);
ScrDyn *scr_dyn_new_str(ScrStr *s);
ScrDyn *scr_dyn_new_arr(void);
ScrDyn *scr_dyn_new_obj(void);
/* The ordinary deep-copy object plus a retained typed source. source_access
* releases that source when materialize=false and returns a fresh dyn snapshot
* when true. Generic dyn member reads and strict equality remain snapshot/node
* based; callback-interface consumers opt into the live arm explicitly. */
/* The ordinary object snapshot plus a retained typed source. source_access
* releases that source when materialize=false and returns a fresh snapshot
* when true. */
ScrDyn *scr_dyn_new_obj_with_identity(
void *source, void *(*source_retain)(void *),
ScrDyn *(*source_access)(void *, bool materialize));
bool scr_dyn_obj_same_source(const ScrDyn *a, const ScrDyn *b);
void *scr_dyn_obj_source_cast(const ScrDyn *d,
ScrDyn *(*source_access)(void *, bool), void *(*source_retain)(void *));
/* Object.create(null): the fresh null-prototype dictionary (see the
* null_proto flavor flag above). */
ScrDyn *scr_dyn_new_obj_null_proto(void);
@@ -3650,6 +3650,7 @@ ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i);
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);
ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args);
/* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets
* the member (JS: later writes win, insertion order); undefined/null and
* non-object kinds throw Node's catchable TypeErrors (strict-mode
@@ -0,0 +1,70 @@
const arrayLike = { 0: 'a', 1: 'b', 2: 'a', length: 3 };
console.log(Array.prototype.indexOf.call(arrayLike, 'a'));
console.log(Array.prototype.indexOf.call(arrayLike, 'a', 1));
console.log(Array.prototype.lastIndexOf.call(arrayLike, 'a'));
console.log(Array.prototype.lastIndexOf.call(arrayLike, 'a', -2));
console.log(Array.prototype.includes.call(arrayLike, 'b'));
console.log(Array.prototype.at.call(arrayLike, -1));
console.log(Array.prototype.indexOf.call('aba', 'b'));
console.log(Array.prototype.includes.call('aba', 'z'));
try { Array.prototype.indexOf.call(null, 'a'); } catch (error) { console.log(error.name); }
const numbers = { 0: 2, 1: 3, 2: 2, length: 3 };
console.log(Array.prototype.map.call(numbers, (value) => value * 2).join(','));
console.log(Array.prototype.filter.call(numbers, (value) => value > 2).join(','));
console.log(Array.prototype.some.call(numbers, (value) => value === 3));
console.log(Array.prototype.every.call(numbers, (value) => value > 1));
console.log(Array.prototype.find.call(numbers, (value) => value === 3));
console.log(Array.prototype.findIndex.call(numbers, (value) => value === 3));
let sum = 0;
Array.prototype.forEach.call(numbers, (value) => { sum += value; });
console.log(sum);
console.log(Array.prototype.reduce.call(numbers, (acc, value) => acc + value, 0));
console.log(Array.prototype.reduceRight.call(numbers, (acc, value) => acc * 10 + value, 0));
console.log(Array.prototype.indexOf.call({ 0: true, length: 'Infinity' }, true));
console.log(Array.prototype.lastIndexOf.call({ 4294967295: 'last', length: 4294967296 }, 'last'));
console.log(Array.prototype.some.call({ 0: 11, length: 'Infinity' }, (value) => value > 10));
console.log(Array.prototype.every.call({ 0: 9, length: Infinity }, (value) => value > 10));
console.log(Array.prototype.every.call({ 0: 11, length: 1 }, function () { return this.valueOf() === 5; }, 5));
console.log(Array.prototype.some.call({ 0: 11, length: 1 }, function () { return this.valueOf() === 'yes'; }, 'yes'));
let booleanReceiver;
Array.prototype.forEach.call({ 0: 11, length: 1 }, function () { booleanReceiver = this.valueOf(); }, false);
console.log(booleanReceiver);
const marker = {};
const sparseBorrow = { 5: marker, length: 100 };
console.log(Array.prototype.some.call(sparseBorrow, (value, index) => index === 5 && value === marker));
const filteredBorrow = Array.prototype.filter.call(sparseBorrow, (value, index) => index === 5 && value === marker);
console.log(filteredBorrow.length);
console.log(filteredBorrow[0] === marker);
console.log(Array.prototype.indexOf.call({ 0: marker, length: 1 }, marker));
console.log(Array.prototype.indexOf.call({ 0: marker, length: 1 }, {}));
console.log(Array.prototype.lastIndexOf.call({ 0: marker, 4294967295: marker, length: 4294967296 }, marker));
let fromIndexConversions = 0;
const unusedFromIndex = { valueOf() { fromIndexConversions++; return 1; } };
console.log(Array.prototype.indexOf.call({ length: 0 }, 'a', unusedFromIndex));
console.log(Array.prototype.lastIndexOf.call({ length: 0 }, 'a', unusedFromIndex));
console.log(Array.prototype.includes.call({ length: 0 }, 'a', unusedFromIndex));
console.log(fromIndexConversions);
let visits = 0;
console.log(Array.prototype.findIndex.call({ length: 2 }, (value, index) => { visits++; return index === 1 && value === undefined; }));
console.log(visits);
visits = 0;
console.log(Array.prototype.find.call({ length: 2 }, (value, index) => { visits++; return index === 1 && value === undefined; }));
console.log(visits);
let presenceChecks = 0;
const proxiedArrayLike = new Proxy({ 0: 'proxy', length: 1 }, {
has() { presenceChecks++; return true; },
get(target, key) { return target[key]; },
});
console.log(Array.prototype.find.call(proxiedArrayLike, (value) => value === 'proxy'));
console.log(Array.prototype.findIndex.call(proxiedArrayLike, (value) => value === 'proxy'));
console.log(presenceChecks);
console.log(Array.prototype.includes.call(proxiedArrayLike, 'proxy'));
console.log(presenceChecks);
console.log(Array.prototype.indexOf.call(proxiedArrayLike, 'proxy'));
console.log(presenceChecks);