Build Sets, Maps and weak collections from checked-dynamic iterables

`new Set(x)`, `new Map(x)`, `new WeakSet(x)`, `new WeakMap(x)` and
`Object.fromEntries(x)` now accept any checked-dynamic iterable, as in
Node: a native iterator (`map.keys()`, `set.values()`), a compiled
generator, an object with a Symbol.iterator method, or a Set. Before, a
Set seed that was not an array or a string threw "is not iterable", and a
Map, weak collection or Object.fromEntries seed that was a generator or a
collection other than a Map refused with SC2020.

The runtime drains native iterators, packs arrays, strings and
collections, and steps every other iterable through its iterator
(scr_dyn_seed_values). A program that builds these from an untyped seed
registers the compiled generators' next and return methods with the
runtime, as destructuring rest already did, so a generator seed can be
stepped.

The seed's values are all taken before the first one is added, so when an
entry of a Map seed is not an object the TypeError comes after the
iterable is exhausted; Node closes the iterator at that entry. The
limitations page says so.
This commit is contained in:
Chris Tate
2026-10-22 13:09:01 -05:00
parent 5266cb7cbe
commit 8f08cbbec5
9 changed files with 85 additions and 14 deletions
@@ -7,4 +7,6 @@
They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`.
They also support the iterator helper methods, such as `.map()` and `.take()`.
Detached collection methods are unsupported.
Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one.
When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry.
The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
@@ -11,6 +11,18 @@ import { everyStmtList } from "../../ir/traverse.js";
import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
/** Runtime entry points that step a checked-dynamic iterable's iterator:
* destructuring rest, and the collection constructors' and
* Object.fromEntries' seeds. */
const RUNTIME_STEP_LIB_FNS: ReadonlySet<string> = new Set([
"dyn.destructureRest",
"dyn.nativeSetNew",
"dyn.mapSeedEntries",
"dyn.fromEntries",
"weakMap.new",
"weakSet.new",
]);
/** What one function's body holds for the class dispatch pass and its
* caught-value, generator and class construction sub-passes, collected in
* one walk. Lists keep the order the walk meets their entries. */
@@ -186,7 +198,7 @@ function scanSites(statements: IrStmt[]): FunctionSites {
expr.key.kind === "strLit" &&
HELPER_METHODS.has(expr.key.value)) ||
expr.kind === "dynIterN" ||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
(expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn))
)
sites.runtimeSteps = true;
switch (expr.kind) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6926-dynamic-collection-seeds.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6927-dynamic-collection-seeds.ts"
],
"diags": []
}
+19 -13
View File
@@ -1577,6 +1577,20 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) {
static ScrDyn *scr_native_iterator_drain(const ScrDyn *src);
/* The values a collection constructor's iterable seed gives, in order: +1
* array, or NULL with the exception pending. A native iterator (a Map's
* keys(), a Set's values()) drains, which runs no program code; arrays,
* strings, typed arrays and collections pack; other iterables (a
* generator, an object with a Symbol.iterator method) step their
* iterator to the end, so their values are all taken before the
* constructor adds the first one. */
static ScrDyn *scr_dyn_seed_values(const ScrDyn *src) {
if (src->kind == SCR_DYN_HANDLE &&
(src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step))
return scr_native_iterator_drain(src);
return scr_dyn_destructure_rest(src, NULL);
}
/* The for-of-over-dyn pack accessors: the emitted index loop drives them
* over a scr_dyn_iter_pack result (ARR by construction — the defensive
* arms cover nothing reachable from that lowering). Never throw. */
@@ -1592,18 +1606,10 @@ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src) {
if (scr_exc_pending()) return NULL;
}
/* A native iterator (sp.entries(), a Map's entries()) or a handle that
* steps through live entries (URLSearchParams): producing its entries
* runs no program code, so draining them first is what the consumer
* observes. */
if (src->kind == SCR_DYN_HANDLE &&
(src->v.handle.tag == SCR_DYNH_ITERATOR || scr_dyn_handle_ops_of(src)->iter_step))
return scr_native_iterator_drain(src);
if (src->kind == SCR_DYN_HANDLE || src->kind == SCR_DYN_TYPED_REF) {
static const char message[] = "new Map(entries) over native non-array iterables has no lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
return scr_dyn_iter_pack(src, NULL);
* steps through live entries (URLSearchParams) drains; a Set, a
* generator or another iterable gives its values in order
* (scr_dyn_seed_values). */
return scr_dyn_seed_values(src);
}
ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry) {
@@ -14836,7 +14842,7 @@ ScrDyn *scr_dyn_native_set(ScrMap *value) {
ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed) {
ScrDyn *items = NULL;
if (seed->kind != SCR_DYN_NULL && seed->kind != SCR_DYN_UNDEF) {
items = scr_dyn_iter_pack(seed, NULL);
items = scr_dyn_seed_values(seed);
if (!items) return NULL;
}
ScrMap *set = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops);
@@ -0,0 +1,28 @@
// Set, Map, WeakSet and Object.fromEntries over checked-dynamic iterables:
// native iterators, generators, objects with a Symbol.iterator method and
// collections, as seeds that reach the runtime untyped.
function* pairs() { yield ["g", 1]; yield ["h", 2]; }
function* values() { yield 1; yield 2; yield 2; }
const iterable = { *[Symbol.iterator]() { yield ["i", 3]; } };
const valueIterable = { *[Symbol.iterator]() { yield "a"; yield "b"; } };
function attempt(label, make) {
try { console.log(label, make()); } catch (e) { console.log(label, "threw", e.name, e.message); }
}
function viaSet(x) { return new Set(x).size; }
function viaMap(x) { return [...new Map(x).keys()].join(); }
function viaWeakSet(x) { return new WeakSet(x).has(key); }
const key = {};
const m = new Map([["a", 1], ["b", 2]]);
attempt("set keys()", () => viaSet(m.keys()));
attempt("set values()", () => viaSet(new Set([1, 2]).values()));
attempt("set generator", () => viaSet(values()));
attempt("set iterable", () => viaSet(valueIterable));
attempt("set string", () => viaSet("abca"));
attempt("set number", () => viaSet(5));
attempt("map generator", () => viaMap(pairs()));
attempt("map iterable", () => viaMap(iterable));
attempt("map set", () => viaMap(new Set([["s", 1]])));
attempt("map entries()", () => viaMap(m.entries()));
attempt("map bad entry", () => viaMap([1]));
attempt("weakset generator", () => viaWeakSet((function* () { yield key; })()));
attempt("fromEntries generator", () => JSON.stringify(Object.fromEntries(pairs())));
@@ -0,0 +1,9 @@
// Set and Map seeds held as any: native iterators and generators.
function* values(): Generator<number> { yield 1; yield 2; yield 2; }
function* pairs(): Generator<[string, number]> { yield ["a", 1]; yield ["b", 2]; }
const m = new Map<string, number>([["x", 1], ["y", 2]]);
const anyKeys: any = m.keys();
const anyGen: any = values();
const anyPairs: any = pairs();
console.log(new Set(anyKeys).size, new Set(anyGen).size, new Map(anyPairs).size);
console.log(new Set(values()).size, new Map(pairs()).get("b"), new Set(m.values()).size);
@@ -0,0 +1 @@
{ "plain": 3813, "san": 3374 }
@@ -0,0 +1 @@
{ "plain": 2436, "san": 2273 }