Store foreign elements only in number[] and string[]

ToNumber and ToString of a foreign element are reached through a table
that scr_arr_set_foreign installs, so a program that never stores a
foreign element no longer links the dynamic tier through the array
runtime's joins, sorts and numeric reads.

Only number[] and string[] store a value written through a dynamic view
that their element type cannot hold. Other element types keep the
checked conversion's TypeError at the write. Counting every viewed
element type as possibly undefined made callbacks over records whose
arrays reach a package (zod3-infer) take optional elements and refuse
their spreads.
This commit is contained in:
Chris Tate
2026-10-23 06:06:50 -05:00
parent 029845e9c0
commit 956292c627
3 changed files with 40 additions and 25 deletions
@@ -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<string> = 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<string> {
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
+24 -4
View File
@@ -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);
@@ -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);