diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index ce33aa2cd..b72875aaa 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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)`, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 924027ee2..1868ca402 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -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": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index feb499203..be7a4973b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1519,6 +1519,7 @@ export const LIB_FN_SYMS: Record = { "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. diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index a0a153da1..c77a5a191 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -105,6 +105,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupNumber", "regex.groupParseInt", "regex.resultProp", + "array.namedKeys", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index b04ba0b09..0960b9d6a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9f383c695..1e6270366 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index f562d20e0..0004be761 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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 = diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json new file mode 100644 index 000000000..c12126453 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6987-array-view-named-properties.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f8b5813a2..fa98dd38a 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -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); +} diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 2068a324d..d9f5f49bc 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -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 diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index adcc96560..8577a96fb 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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); diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts new file mode 100644 index 000000000..5cbea5a0a --- /dev/null +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -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; + +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(?c)?/.exec("abc"); +if (match) for (const key in match) console.log("match key", key); diff --git a/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json b/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json new file mode 100644 index 000000000..1e9e64ec5 --- /dev/null +++ b/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4400 }