mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Keep named properties written to arrays through checked views
This commit is contained in:
@@ -176,6 +176,7 @@ export function streamTypedRefCommitAdapter(
|
||||
const check = host.dyn.dynCheckHelper(t);
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
host.declare(`declare void @scr_arr_outline(ptr)`);
|
||||
host.declare(`declare void @scr_arr_adopt_view_properties(ptr, ptr, ptr)`);
|
||||
const lines = [
|
||||
`define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit live stream element ${typeKey(t)}`,
|
||||
`entry:`,
|
||||
@@ -183,6 +184,8 @@ export function streamTypedRefCommitAdapter(
|
||||
` %missing = icmp eq ptr %next, null`,
|
||||
` br i1 %missing, label %done, label %swap`,
|
||||
`swap:`,
|
||||
// Properties move with the storage (in its out-of-line part).
|
||||
` call void @scr_arr_adopt_view_properties(ptr %next, ptr %d, ptr %target)`,
|
||||
// Inline slots live in each header's block; move them out first.
|
||||
` call void @scr_arr_outline(ptr %target)`,
|
||||
` call void @scr_arr_outline(ptr %next)`,
|
||||
|
||||
@@ -864,6 +864,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "events":
|
||||
case "blob":
|
||||
case "wasm":
|
||||
case "array":
|
||||
case "record":
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
|
||||
@@ -1519,6 +1519,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.groupNumber": "scr_regex_group_number",
|
||||
"regex.groupParseInt": "scr_regex_group_parse_int",
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
"array.namedKeys": "scr_arr_named_keys",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
|
||||
@@ -105,6 +105,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"regex.groupNumber",
|
||||
"regex.groupParseInt",
|
||||
"regex.resultProp",
|
||||
"array.namedKeys",
|
||||
"process.cwd",
|
||||
"process.argv",
|
||||
"process.execArgv",
|
||||
|
||||
@@ -11594,10 +11594,23 @@ function lowerForInArray(
|
||||
try {
|
||||
const arr = lowerer.declareHiddenLocal("%inarr", arrExpr.type);
|
||||
const len = lowerer.declareHiddenLocal("%inlen", F64);
|
||||
const keysT: IrType = { kind: "array", elem: STRING };
|
||||
const keys = lowerer.declareHiddenLocal("%inkeys", keysT);
|
||||
const total = lowerer.declareHiddenLocal("%intotal", F64);
|
||||
const i = lowerer.declareHiddenLocal("%ini", F64);
|
||||
i.mutable = true;
|
||||
const arrRef = (): IrExpr => ({ kind: "varRef", localId: arr.id, type: arrExpr.type, loc });
|
||||
const keysRef = (): IrExpr => ({ kind: "varRef", localId: keys.id, type: keysT, loc });
|
||||
const lenRef = (): IrExpr => ({ kind: "varRef", localId: len.id, type: F64, loc });
|
||||
const iRef = (): IrExpr => ({ kind: "varRef", localId: i.id, type: F64, loc });
|
||||
const isIndex = (): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: iRef(),
|
||||
right: lenRef(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const liveLen = (): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
@@ -11606,7 +11619,44 @@ function lowerForInArray(
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const keyInit: IrExpr = { kind: "toString", operand: iRef(), type: STRING, loc };
|
||||
// Indices first, then the named keys the array had when the loop began.
|
||||
const keyInit: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: isIndex(),
|
||||
then: { kind: "toString", operand: iRef(), type: STRING, loc },
|
||||
else_: {
|
||||
kind: "arrayGet",
|
||||
arr: keysRef(),
|
||||
index: { kind: "bin", op: "-", left: iRef(), right: lenRef(), type: F64, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
// An index is visited while it is below the live length and not a hole.
|
||||
const visits: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: isIndex(),
|
||||
then: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "bin", op: "<", left: iRef(), right: liveLen(), type: BOOL, loc },
|
||||
right: {
|
||||
kind: "bin",
|
||||
op: "!==",
|
||||
left: { kind: "arrayState", arr: arrRef(), index: iRef(), type: F64, loc },
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
else_: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
|
||||
// The three binding forms, sharing the per-visit key declaration.
|
||||
let keyDecl: IrStmt;
|
||||
@@ -11670,6 +11720,32 @@ function lowerForInArray(
|
||||
body: [
|
||||
{ kind: "varDecl", localId: arr.id, init: arrExpr, loc },
|
||||
{ kind: "varDecl", localId: len.id, init: liveLen(), loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: keys.id,
|
||||
init: { kind: "libCall", fn: "array.namedKeys", args: [arrRef()], type: keysT, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: total.id,
|
||||
init: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: lenRef(),
|
||||
right: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: keysRef(),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "for",
|
||||
init: {
|
||||
@@ -11682,7 +11758,7 @@ function lowerForInArray(
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: iRef(),
|
||||
right: { kind: "varRef", localId: len.id, type: F64, loc },
|
||||
right: { kind: "varRef", localId: total.id, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
@@ -11702,7 +11778,7 @@ function lowerForInArray(
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "bin", op: "<", left: iRef(), right: liveLen(), type: BOOL, loc },
|
||||
cond: visits,
|
||||
then: [keyDecl, ...writes, ...body],
|
||||
else_: null,
|
||||
loc,
|
||||
|
||||
@@ -3724,6 +3724,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 },
|
||||
// `.index` / `.input` (args[1]) of a match result. Borrowed; never throws.
|
||||
"regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN },
|
||||
// The string keys `for...in` visits on an array (args[0], any element
|
||||
// type) after its indices. Borrowed; never throws.
|
||||
"array.namedKeys": { argTypes: [null], result: arrayOf(STRING) },
|
||||
// node:events EventEmitter: receivers are emitter-hierarchy objects and
|
||||
// the chaining forms (on/off/removeAll/setMax) return the receiver's
|
||||
// own class — program-dependent object types, checked in the libCall
|
||||
|
||||
@@ -4262,6 +4262,12 @@ function validateFunction(
|
||||
if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "array.namedKeys") {
|
||||
if (e.args[0]?.type.kind !== "array") {
|
||||
err(`libCall array.namedKeys arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "fetch.streamFrom") {
|
||||
const t = e.args[0]?.type;
|
||||
const ok =
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6987-array-view-named-properties.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -4795,11 +4795,67 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
|
||||
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE &&
|
||||
scr_arr_match_copy(a, target))
|
||||
return;
|
||||
if (!SCR_AX(a)->metadata) return;
|
||||
for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) {
|
||||
ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i];
|
||||
metadata = SCR_AX(a)->metadata;
|
||||
if (!metadata) return;
|
||||
for (size_t i = 0; i < metadata->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &metadata->v.obj.entries[i];
|
||||
ScrStr *key = scr_str_new(entry->key, entry->key_len);
|
||||
scr_dyn_key_set(target, key, entry->value);
|
||||
if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) {
|
||||
scr_dyn_key_set(target, key, entry->value);
|
||||
} else {
|
||||
/* Properties defined through a view keep their attributes. */
|
||||
ScrDyn *name = scr_dyn_new_str(key);
|
||||
ScrDyn *descriptor = scr_dyn_own_descriptor(metadata, key);
|
||||
if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor));
|
||||
scr_dyn_release(descriptor);
|
||||
scr_dyn_release(name);
|
||||
}
|
||||
scr_str_release(key);
|
||||
}
|
||||
}
|
||||
|
||||
ScrArr *scr_arr_named_keys(ScrArr *a) {
|
||||
/* Most arrays have no out-of-line storage: they share one immortal empty
|
||||
* result, which `for...in` only reads. */
|
||||
static ScrArr none = {.rc = SIZE_MAX, .elem = SCR_ELEM_STR};
|
||||
const ScrArrExt *x = scr_arr_ext_of(a);
|
||||
if (!x) return &none;
|
||||
ScrArr *keys = scr_arr_new(SCR_ELEM_STR, 0);
|
||||
for (size_t i = 0; i < x->prop_len; i++)
|
||||
scr_arr_push_ref(keys, scr_str_new(x->props[i].key, x->props[i].key_len));
|
||||
const ScrDyn *metadata = x->metadata;
|
||||
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) {
|
||||
static const char *const names[] = {"index", "input", "groups"};
|
||||
for (size_t i = 0; i < 3; i++) scr_arr_push_ref(keys, scr_str_new(names[i], strlen(names[i])));
|
||||
return keys;
|
||||
}
|
||||
if (!metadata || metadata->kind != SCR_DYN_OBJ) return keys;
|
||||
for (size_t i = 0; i < metadata->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &metadata->v.obj.entries[i];
|
||||
if (!entry->enumerable || scr_arr_prop_index(a, entry->key, entry->key_len) != SIZE_MAX) continue;
|
||||
scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) {
|
||||
ScrArrExt *from = scr_arr_ext_of(target);
|
||||
if (from && from->prop_len) {
|
||||
/* Both arrays have the same element type, so the slots keep their owner. */
|
||||
ScrArrExt *x = scr_arr_ext(a);
|
||||
ScrArrProp *props = x->props;
|
||||
size_t len = x->prop_len, cap = x->prop_cap;
|
||||
x->props = from->props;
|
||||
x->prop_len = from->prop_len;
|
||||
x->prop_cap = from->prop_cap;
|
||||
from->props = props;
|
||||
from->prop_len = len;
|
||||
from->prop_cap = cap;
|
||||
}
|
||||
ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL;
|
||||
if (!named || named->v.obj.len == 0) return;
|
||||
ScrArrExt *x = scr_arr_ext(a);
|
||||
ScrDyn *old = x->metadata;
|
||||
x->metadata = scr_dyn_retain(named);
|
||||
scr_dyn_release(old);
|
||||
}
|
||||
|
||||
@@ -317,6 +317,15 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) {
|
||||
* the candidate-root buffer like every headered type. */
|
||||
void scr_arr_release(ScrArr *a);
|
||||
void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
|
||||
/* The commit of a live array view calls this on `a`, the array converted
|
||||
* from `view`, before `target` takes its storage: `a` takes the view's named
|
||||
* (non-index) properties as its metadata and the noncanonical numeric
|
||||
* properties of `target`, so neither is lost when the storage is swapped. */
|
||||
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
|
||||
/* The enumerable string keys `for...in` visits after the indices: the
|
||||
* noncanonical numeric properties, then the named properties in insertion
|
||||
* order. +1. */
|
||||
ScrArr *scr_arr_named_keys(ScrArr *a);
|
||||
/* Installed by scr_regex.c when it records a pending match source; this
|
||||
* unit links without the regex runtime. Writes the pending metadata of
|
||||
* `a` into its dynamic copy or materializes it, answering whether it
|
||||
|
||||
@@ -10269,6 +10269,23 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* A live native array keeps its named properties in its metadata, so
|
||||
* deleting one goes through the view and its commit. Elements and
|
||||
* `length` keep the boundary below. */
|
||||
size_t element;
|
||||
if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && scr_dyn_typed_ref_array_ops(recv) &&
|
||||
!scr_typed_ref_is_length(key) && !scr_typed_ref_canonical_index(key->data, key->len, &element)) {
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(recv);
|
||||
if (!scr_exc_pending() && view->kind == SCR_DYN_ARR) {
|
||||
bool own = view->v.arr.properties && scr_dyn_obj_get(view->v.arr.properties, key->data, key->len);
|
||||
if (own) scr_dyn_key_delete(view, key, strict);
|
||||
if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv);
|
||||
scr_dyn_release(view);
|
||||
return;
|
||||
}
|
||||
scr_dyn_release(view);
|
||||
if (scr_exc_pending()) return;
|
||||
}
|
||||
if (recv->kind == SCR_DYN_FUNC) {
|
||||
if (scr_dyn_fn_property_fence(key)) return;
|
||||
ScrDyn *table = scr_dyn_fn_properties(recv);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Named properties written to a typed array through an `unknown` view stay
|
||||
// on the array: later views read them, key walks list them after the
|
||||
// indices, and `for...in` over the typed array visits them. Holes are
|
||||
// skipped by `for...in`.
|
||||
|
||||
type Bag = Record<string, unknown>;
|
||||
|
||||
const numbers: number[] = [1, 2, 3];
|
||||
const numbersView = numbers as unknown as Bag;
|
||||
numbersView.extra = "named";
|
||||
console.log(numbersView.extra, (numbers as unknown as Bag).extra);
|
||||
console.log(Object.keys(numbersView), JSON.stringify(numbers), numbers.length);
|
||||
console.log("extra" in numbersView, Object.hasOwn(numbersView, "extra"));
|
||||
|
||||
numbers.push(4);
|
||||
numbers[0] = 10;
|
||||
console.log((numbers as unknown as Bag).extra, numbers.join(","));
|
||||
console.log((numbers.slice() as unknown as Bag).extra);
|
||||
|
||||
(numbers as unknown as Bag).extra = "again";
|
||||
(numbers as unknown as Bag).count = 2;
|
||||
console.log(Object.entries(numbers as unknown as Bag));
|
||||
for (const key in numbers) console.log("numbers key", key);
|
||||
|
||||
delete (numbers as unknown as Bag).extra;
|
||||
console.log((numbers as unknown as Bag).extra, Object.keys(numbers as unknown as Bag));
|
||||
|
||||
const words: string[] = ["a", "b"];
|
||||
(words as unknown as Bag).label = { nested: [1, 2] };
|
||||
const label = (words as unknown as Bag).label as { nested: number[] };
|
||||
console.log(label.nested.length, words.length);
|
||||
for (const key in words) console.log("words key", key);
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
const points: Point[] = [{ x: 1, y: 2 }];
|
||||
(points as unknown as Bag).origin = "zero";
|
||||
console.log((points as unknown as Bag).origin, points[0]!.x);
|
||||
for (const key in points) console.log("points key", key);
|
||||
|
||||
class Item {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
const items: Item[] = [new Item("first")];
|
||||
(items as unknown as Bag).kind = "items";
|
||||
console.log((items as unknown as Bag).kind, items[0]!.name);
|
||||
|
||||
const flags: boolean[] = [true, false];
|
||||
Object.defineProperty(flags as unknown as Bag, "hidden", { value: 1, enumerable: false });
|
||||
(flags as unknown as Bag).shown = 2;
|
||||
console.log((flags as unknown as Bag).hidden, (flags as unknown as Bag).shown);
|
||||
for (const key in flags) console.log("flags key", key);
|
||||
|
||||
const holes: (number | undefined)[] = [1, , 3, undefined];
|
||||
for (const key in holes) console.log("holes key", key);
|
||||
(holes as unknown as Bag).tail = true;
|
||||
for (const key in holes) console.log("holes key after", key);
|
||||
|
||||
const offsets: number[] = [1, 2];
|
||||
offsets[-1] = 5;
|
||||
offsets[1.5] = 6;
|
||||
(offsets as unknown as Bag).name = "offsets";
|
||||
console.log(offsets[-1], offsets[1.5], (offsets as unknown as Bag).name);
|
||||
for (const key in offsets) console.log("offsets key", key);
|
||||
|
||||
const shrinking: number[] = [1, 2, 3, 4];
|
||||
(shrinking as unknown as Bag).after = "kept";
|
||||
for (const key in shrinking) {
|
||||
console.log("shrinking key", key);
|
||||
if (key === "1") shrinking.length = 2;
|
||||
}
|
||||
|
||||
const match = /b(?<rest>c)?/.exec("abc");
|
||||
if (match) for (const key in match) console.log("match key", key);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 4400 }
|
||||
Reference in New Issue
Block a user