Clone Maps and Sets in structuredClone

This commit is contained in:
Chris Tate
2026-10-30 11:31:53 -05:00
parent 13b959a4c7
commit fe453ea0bc
6 changed files with 175 additions and 3 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/structured-clone-collections.ts"
],
"diags": []
}
+88 -1
View File
@@ -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);
+6 -2
View File
@@ -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();
}
@@ -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<string, Item>([["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<number, string>([
[1, "one"],
[-0, "zero"],
]);
const byNumberCopy = structuredClone(byNumber);
console.log(byNumberCopy.get(1), byNumberCopy.get(0), byNumberCopy.has(2));
const flags = new Set<boolean>([true]);
console.log(structuredClone(flags).has(true), structuredClone(flags).has(false));
// Collections nested in collections and records.
type Outer = { inner: { m: Map<string, number>; when: Date }; tags: Set<string>; 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<string, Set<number>>([["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<string, number>()).size, structuredClone(new Set<string>()).size);
const handlers = new Map<string, () => void>([["run", () => {}]]);
try {
structuredClone(handlers);
console.log("cloned");
} catch (error) {
console.log((error as Error).name);
}
@@ -0,0 +1 @@
{ "plain": 4900, "san": 55300 }
@@ -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", () => {