diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json new file mode 100644 index 000000000..152d85349 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/structured-clone-collections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/structured-clone-collections.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index a560f09d8..042b59bf9 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -15623,6 +15623,73 @@ typedef struct ScrScParent { const struct ScrScParent *up; } ScrScParent; +static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region); + +static const char scr_sc_cycle_message[] = + "structuredClone of cyclic values (the checked-dynamic tree cannot represent cycles) is not supported yet"; + +/* The clone a handle's clone hook runs inside: its parent chain and region, + * so a Map or Set clones its entries with the cycle check and region of the + * value that holds it. */ +static SCR_TL struct { + const ScrScParent *up; + ScrDynRegion *region; + /* scr_sc_clone, reached through the context so that the Map and Set + * handle ops do not link the structured-clone code into programs that + * never clone (volatile, so the call is not resolved at compile time). */ + ScrDyn *(*volatile clone)(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region); +} scr_sc_nested; + +/* A Map or Set clones entry by entry, cloning each key and value. The copy + * keeps the source's storage kinds and typed slot adapters, so a typed + * collection checks back into its own type; a typed slot converts the + * cloned value into a fresh element. The Map and Set handle ops install it + * as their clone hook. */ +static ScrMap *scr_sc_clone_collection(ScrMap *source, bool is_map) { + const ScrScParent *up = scr_sc_nested.up; + ScrDynRegion *region = scr_sc_nested.region; + ScrDyn *(*clone)(const ScrDyn *, const ScrScParent *, ScrDynRegion *) = scr_sc_nested.clone; + ScrMap *out = scr_map_new((ScrMapKeyKind)source->key_kind, (ScrMapValKind)source->val_kind, source->ops); + out->dyn_ops = source->dyn_ops; + scr_map_iter_enter(source); + for (double i = 0; i < scr_map_iter_count(source); i++) { + if (!scr_map_iter_live(source, i)) continue; + ScrDyn *key = scr_map_dyn_key(source, i); + ScrDyn *key_copy = clone(key, up, region); + scr_dyn_release(key); + ScrDyn *value_copy = NULL; + if (key_copy && is_map) { + ScrDyn *value = scr_map_dyn_value(source, i); + value_copy = clone(value, up, region); + scr_dyn_release(value); + } + if (key_copy && (value_copy || !is_map)) scr_map_dyn_set(out, key_copy, value_copy, !is_map); + scr_dyn_release(key_copy); + scr_dyn_release(value_copy); + if (scr_exc_pending()) break; + } + scr_map_iter_exit(source); + if (scr_exc_pending()) { + scr_map_release(out); + return NULL; + } + return out; +} + +static ScrDyn *scr_native_map_clone(void *ptr) { + ScrMap *copy = scr_sc_clone_collection(ptr, true); + ScrDyn *result = copy ? scr_dyn_native_map(copy) : NULL; + scr_map_release(copy); + return result; +} + +static ScrDyn *scr_native_set_clone(void *ptr) { + ScrMap *copy = scr_sc_clone_collection(ptr, false); + ScrDyn *result = copy ? scr_dyn_native_set(copy) : NULL; + scr_map_release(copy); + return result; +} + /* The clone is a fresh tree (shared inputs are copied per path, cycles * throw), so it is tagged like a parse result (ScrDynRegion); a leaf the * region may not hold clears it. */ @@ -15740,7 +15807,25 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion case SCR_DYN_HANDLE: if (scr_dyn_handle_ops_of(v)->clone) { region->intact = false; /* a handle is not a leaf the region may hold */ - return scr_dyn_handle_ops_of(v)->clone(v->v.handle.ptr); + /* A handle that holds values (a Map or Set) clones them with this + * value as their parent; one that holds its own storage is cut off + * by its parent's identity. */ + for (const ScrScParent *p = up; p != NULL; p = p->up) { + if (p->node->kind == SCR_DYN_HANDLE && p->node->v.handle.ptr == v->v.handle.ptr) { + scr_throw_error_msg(SCR_ERR_ERROR, scr_sc_cycle_message, sizeof scr_sc_cycle_message - 1); + return NULL; + } + } + ScrScParent self = {v, up}; + const ScrScParent *saved_up = scr_sc_nested.up; + ScrDynRegion *saved_region = scr_sc_nested.region; + scr_sc_nested.up = &self; + scr_sc_nested.region = region; + scr_sc_nested.clone = scr_sc_clone; + ScrDyn *out = scr_dyn_handle_ops_of(v)->clone(v->v.handle.ptr); + scr_sc_nested.up = saved_up; + scr_sc_nested.region = saved_region; + return out; } /* fall through */ case SCR_DYN_FUNC: @@ -19144,6 +19229,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) { .iter_pack = &scr_native_set_pack, /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ .json_empty_object = true, + .clone = &scr_native_set_clone, }; scr_dyn_handle_install(SCR_DYNH_SET, &ops); return scr_dyn_new_handle(value, SCR_DYNH_SET); @@ -19261,6 +19347,7 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) { .iter_pack = &scr_native_map_pack, /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ .json_empty_object = true, + .clone = &scr_native_map_clone, }; scr_dyn_handle_install(SCR_DYNH_MAP, &ops); return scr_dyn_new_handle(value, SCR_DYNH_MAP); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index eb4af774f..3dee288c8 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -406,7 +406,9 @@ export const ICU_CPP_NAMESPACE = "6icu_78"; * spells `6icu_78` wherever the name occurs — as the function's own * namespace (`_ZN6icu_78...`), in its parameter types, in vtables, typeinfo * and guard variables, or in a libc++ instantiation over an ICU type. No - * symbol outside the archive carries either mark. + * symbol outside the archive carries either mark, except emitted program + * functions (`sc_` names), whose numbered helpers can end in `_78` + * (`sc_f__x25_dyn_class_property_78`); the suffix test skips them. * * scr_intl.c's own functions are deliberately not tested by name. The * engine-free Intl paths share their prefix (`scr_intl_num_format_en_us` in @@ -420,7 +422,9 @@ export function icuSymbols(names) { return [...new Set(names)] .filter( (name) => - name.endsWith("_78") || name.includes(ICU_CPP_NAMESPACE) || name.startsWith("icudt78"), + (name.endsWith("_78") && !name.startsWith("sc_")) || + name.includes(ICU_CPP_NAMESPACE) || + name.startsWith("icudt78"), ) .sort(); } diff --git a/tests/corpus/structured-clone-collections.ts b/tests/corpus/structured-clone-collections.ts new file mode 100644 index 000000000..5f6f11c7f --- /dev/null +++ b/tests/corpus/structured-clone-collections.ts @@ -0,0 +1,71 @@ +// structuredClone of Maps and Sets, alone and in fields of typed records: +// keys and values are cloned, the copy is a new collection of the same +// type, and Dates, typed arrays and nested records in the same records +// clone too. +const m = new Map([ + ["a", 1], + ["b", 2], +]); +const mc = structuredClone(m); +mc.set("c", 3); +console.log(mc.get("a"), mc.size, m.size, mc !== m, [...mc.keys()].join(",")); +console.log(structuredClone({ m }).m.get("b")); + +const s = new Set([1, 2, 3]); +const sc = structuredClone(s); +sc.delete(1); +console.log(sc.has(2), sc.size, s.size, [...sc].join(",")); +console.log(structuredClone({ s }).s.has(3)); + +const d = new Date(86400000); +console.log(structuredClone({ d }).d.toISOString(), structuredClone(d).getTime()); + +const t = new Uint8Array([1, 2, 3]); +console.log(structuredClone({ t }).t[2], structuredClone(t).length); + +// Record and array values are cloned, not shared. +type Item = { id: number; tags: string[] }; +const items = new Map([["x", { id: 7, tags: ["red"] }]]); +const itemsCopy = structuredClone(items); +itemsCopy.get("x")!.tags.push("blue"); +console.log(items.get("x")!.tags.join(","), itemsCopy.get("x")!.tags.join(","), itemsCopy.get("x")!.id); + +// Number, boolean and string keys keep their types. +const byNumber = new Map([ + [1, "one"], + [-0, "zero"], +]); +const byNumberCopy = structuredClone(byNumber); +console.log(byNumberCopy.get(1), byNumberCopy.get(0), byNumberCopy.has(2)); +const flags = new Set([true]); +console.log(structuredClone(flags).has(true), structuredClone(flags).has(false)); + +// Collections nested in collections and records. +type Outer = { inner: { m: Map; when: Date }; tags: Set; bytes: Float64Array }; +const outer: Outer = { + inner: { m: new Map([["x", 5]]), when: new Date(0) }, + tags: new Set(["t"]), + bytes: new Float64Array([1.5]), +}; +const outerCopy = structuredClone(outer); +console.log( + outerCopy.inner.m.get("x"), + outerCopy.inner.when.getTime(), + outerCopy.tags.has("t"), + outerCopy.bytes[0], + outerCopy.inner.m !== outer.inner.m, +); +const groups = new Map>([["evens", new Set([2, 4])]]); +const groupsCopy = structuredClone(groups); +groupsCopy.get("evens")!.add(6); +console.log(groups.get("evens")!.size, groupsCopy.get("evens")!.size); + +// An empty collection, and an entry that cannot be cloned. +console.log(structuredClone(new Map()).size, structuredClone(new Set()).size); +const handlers = new Map void>([["run", () => {}]]); +try { + structuredClone(handlers); + console.log("cloned"); +} catch (error) { + console.log((error as Error).name); +} diff --git a/tests/harness/corpus-costs/structured-clone-collections.ts.json b/tests/harness/corpus-costs/structured-clone-collections.ts.json new file mode 100644 index 000000000..d57898b27 --- /dev/null +++ b/tests/harness/corpus-costs/structured-clone-collections.ts.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 55300 } diff --git a/tests/harness/zero-cost-icu-symbols.test.ts b/tests/harness/zero-cost-icu-symbols.test.ts index ee9fac2a1..7e01c9cdc 100644 --- a/tests/harness/zero-cost-icu-symbols.test.ts +++ b/tests/harness/zero-cost-icu-symbols.test.ts @@ -36,6 +36,9 @@ const NON_ICU_SYMBOLS = [ "scr_intl_segmenter_refusing", "scr_dyn_call", "scr_string_concat", + // Emitted program functions are numbered, and a number can be 78. + "sc_f__x25_dyn_class_property_78", + "sc_bf__x25_arr_loop_78", ]; describe("icuSymbols", () => {