diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 9a85bda1a..4db6ee175 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -62,6 +62,12 @@ const VIEW_HOLES = "%view"; /** The fact name of values stored through dynamic views. */ const VIEW_VALUES = "%view-values"; +/** Element keys whose arrays store a value written through a dynamic view + * that the element type cannot hold, as a foreign element: `number[]` and + * `string[]`. Arrays of other element types reject such a write with the + * checked conversion's TypeError. */ +const FOREIGN_ELEMENT_KEYS: ReadonlySet = new Set(["f64", "string"]); + /** What an array value can contain besides present values. */ interface Contents { undefined: boolean; @@ -373,7 +379,7 @@ export class ArrayElementStates { /** Values of any type stored through a dynamic view can be in arrays of * this key. */ private viewedValues(key: string): boolean { - return this.viewValues && this.isViewed(key); + return this.viewValues && FOREIGN_ELEMENT_KEYS.has(key) && this.isViewed(key); } private isViewed(key: string): boolean { @@ -382,11 +388,11 @@ export class ArrayElementStates { } /** Element keys whose arrays can hold elements outside their element - * type, stored through dynamic views; "*" for every array. Final once - * the fixed point is reached. */ + * type, stored through dynamic views (of FOREIGN_ELEMENT_KEYS). Final + * once the fixed point is reached. */ foreignKeys(): ReadonlySet { if (!this.viewValues) return new Set(); - return (this.viewKeys ??= this.host.nativeViewKeys()); + return new Set([...FOREIGN_ELEMENT_KEYS].filter((key) => this.isViewed(key))); } /** A store through `receiver`, which has no native element key, can diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index e83477142..c7399bcea 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -269,6 +269,25 @@ static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { return scr_elem_retain_with(a->elem, scr_arr_ops(a), slot); } +/* ToNumber and ToString of a foreign element. Both can run user code and + * pull in the dynamic tier, so they are reached through this table, which + * scr_arr_set_foreign installs: every foreign element is stored by it, and + * a program that never stores one links neither conversion. */ +typedef struct ScrArrForeignConversions { + double (*to_number)(const ScrDyn *); + ScrStr *(*to_string)(const ScrDyn *); +} ScrArrForeignConversions; + +static const ScrArrForeignConversions scr_arr_dyn_conversions = { + scr_dyn_number_coerce, + scr_dyn_string_coerce_js, +}; +static const ScrArrForeignConversions *scr_arr_conversions; + +static const ScrArrForeignConversions *scr_arr_foreign_conversions(void) { + return __atomic_load_n(&scr_arr_conversions, __ATOMIC_RELAXED); +} + /* The array may hold SCR_ARR_FOREIGN elements (ScrArrExt.foreign). Such * an array always has states. */ static inline bool scr_arr_may_hold_foreign(const ScrArr *a) { @@ -2056,7 +2075,7 @@ static double *scr_arr_coerced_numbers(const ScrArr *a) { uint64_t slot; uint8_t state = scr_arr_f64_state_at(a, i, &slot); if (state == SCR_ARR_FOREIGN) { - out[i] = scr_dyn_number_coerce(scr_slot_to_ptr(slot)); + out[i] = scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); if (scr_exc_pending()) { free(out); return NULL; @@ -2252,7 +2271,7 @@ double scr_arr_get_number(const ScrArr *a, double i) { } else if (!scr_arr_prop_get_state(a, i, &slot, &state)) { return NAN; } - if (state == SCR_ARR_FOREIGN) return scr_dyn_number_coerce(scr_slot_to_ptr(slot)); + if (state == SCR_ARR_FOREIGN) return scr_arr_foreign_conversions()->to_number(scr_slot_to_ptr(slot)); return state == SCR_ARR_VALUE ? scr_slot_to_f64(slot) : NAN; } @@ -2368,6 +2387,7 @@ bool scr_arr_set_foreign(ScrArr *a, double i, ScrDyn *v) { * doubles first, and 1-byte and compact slots cannot hold one. */ if (a->elem == SCR_ELEM_I32) scr_arr_widen_i32(a); if (scr_arr_slot_width(a) != sizeof(uint64_t)) return false; + __atomic_store_n(&scr_arr_conversions, &scr_arr_dyn_conversions, __ATOMIC_RELAXED); scr_dyn_retain(v); scr_arr_need_states(a); if (idx >= a->len) a->len = idx + 1; @@ -3411,7 +3431,7 @@ static ScrArr *scr_arr_sort_foreign(ScrArr *a, bool copy) { if (state != SCR_ARR_VALUE && state != SCR_ARR_FOREIGN) continue; ScrStr *key; if (state == SCR_ARR_FOREIGN) { - key = scr_dyn_string_coerce_js(scr_slot_to_ptr(slot)); + key = scr_arr_foreign_conversions()->to_string(scr_slot_to_ptr(slot)); if (!key) failed = true; } else if (a->elem == SCR_ELEM_STR) { key = scr_str_retain(scr_slot_to_ptr(slot)); @@ -5372,7 +5392,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { /* join converts every element but undefined and null with ToString. */ const ScrDyn *value = scr_slot_to_ptr(slot); if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) continue; - ScrStr *text = scr_dyn_string_coerce_js(value); + ScrStr *text = scr_arr_foreign_conversions()->to_string(value); if (!text) continue; scr_join_append(&out, text->data, text->len); scr_str_release(text); diff --git a/tests/corpus/6068-foreign-elements-through-views.ts b/tests/corpus/6068-foreign-elements-through-views.ts index 21cfe25a2..e6676eea4 100644 --- a/tests/corpus/6068-foreign-elements-through-views.ts +++ b/tests/corpus/6068-foreign-elements-through-views.ts @@ -1,8 +1,8 @@ -// A typed array seen as `unknown[]` holds the values written through the -// view that its element type cannot hold, as in Node. The view and the typed -// array are the same array: length, the view's reads and iteration, -// printing, JSON, join, searches, copies, Math over a spread, sorting and -// truncation see the written values. +// A number[] or string[] seen as `unknown[]` holds the values written +// through the view that its element type cannot hold, as in Node. The view +// and the typed array are the same array: length, the view's reads and +// iteration, printing, JSON, join, searches, copies, Math over a spread, +// sorting and truncation see the written values. const numbers: number[] = [1, 2]; const numbersView: unknown[] = numbers; @@ -49,19 +49,8 @@ truncated.length = 1; truncated.length = 3; console.log(truncated, truncatedView.length); -// string[] and class arrays hold values of other types the same way. +// string[] holds values of other types the same way. const words: string[] = ["a"]; const wordsView: unknown[] = words; wordsView.push(1, false); console.log(words, words.join("-"), words.length, typeof wordsView[1]); - -class Item { - id: string; - constructor(id: string) { - this.id = id; - } -} -const items: Item[] = [new Item("a")]; -const itemsView: unknown[] = items; -itemsView.push({ id: "plain" }); -console.log(items, items.length, items[0]!.id, itemsView[1] instanceof Item);