Repository navigation
runtime: make Set constructor consume arbitrary iterables and throw on non-iterables #2771
Description
Summary
new Set(iterable) currently routes through js_set_from_iterable, but that helper only handles a few input shapes directly:
- arrays,
- strings,
null/undefinedas empty,- other pointer-tagged values by treating them as arrays,
- everything else as an empty set with no error.
Node's Set constructor should consume any iterable and should throw TypeError for non-null, non-undefined values that are not iterable.
Node behavior
Checked with Node v25.9.0:
[...new Set([1, 2, 1])];
// [1, 2]
[...new Set(new Set([3, 4]))];
// [3, 4]
[...new Set('aba')];
// ['a', 'b']
[...new Set(new Map([[1, 'a'], [2, 'b']]))].map(v => v.join(':'));
// ['1:a', '2:b']
const customIterable = {
*[Symbol.iterator]() {
yield 'x';
yield 'y';
},
};
[...new Set(customIterable)];
// ['x', 'y']
new Set(null).size; // 0
new Set(undefined).size; // 0
new Set(1);
// TypeError: number 1 is not iterable (cannot read property Symbol(Symbol.iterator))
new Set({ a: 1 });
// TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
new Set(Symbol('x'));
// TypeError: symbol is not iterable (cannot read property Symbol(Symbol.iterator))Perry implementation
Lowering/codegen sends any provided Set constructor argument to js_set_from_iterable:
crates/perry-hir/src/lower/expr_new.rs:304handlesnew Set()/new Set(iterable).crates/perry-hir/src/lower/expr_new.rs:316returnsExpr::SetNewfor no arguments.crates/perry-hir/src/lower/expr_new.rs:318returnsExpr::SetNewFromArray(firstArg)for any argument.crates/perry-codegen/src/expr/logical_collections.rs:551routesExpr::SetNewFromArraythroughjs_set_from_iterable.
The runtime helper is intentionally lenient rather than Node-compatible:
crates/perry-runtime/src/set.rs:786documentsjs_set_from_iterableas creating a Set from any iterable JS value.crates/perry-runtime/src/set.rs:788lists the actually handled shapes: Array, String, undefined/null, and anything else.crates/perry-runtime/src/set.rs:795explicitly says anything else becomes an empty set with no error, even though JS would throwTypeError.crates/perry-runtime/src/set.rs:859treats pointer-tagged values as arrays; non-array objects can be misread as ArrayHeader values rather than using their iterator or throwing.crates/perry-runtime/src/set.rs:866returns a fresh empty set for undefined/null/number/etc., sonew Set(1)cannot throw the required TypeError through this path.
Expected fix direction
new Set(iterable) should implement the Set constructor's iterable consumption semantics:
- keep
nullandundefinedas empty-set inputs, - consume arbitrary iterable objects through their iterator,
- preserve existing array and string fast paths when they match iterator behavior,
- support Map/Set/custom iterables by consuming yielded values,
- throw
TypeErrorfor non-iterable numbers, symbols, and plain objects.
Duplicate search
Searched existing issues/PRs for:
Set constructor iterablenew Set iterableSet constructor TypeErrornew Set non-iterablejs_set_from_iterable TypeErrornew Set Map iterablenew Set custom iterator- PR search for
Set constructor iterable OR js_set_from_iterable
No existing issue covered this Set constructor behavior. Related but not duplicate: #2770 tracks Map constructor iterable/malformed-entry semantics, and older Map/Set iteration bugs (#302, #311, #412) were about for-of/spread behavior rather than constructor validation and arbitrary iterable init.
Activity
fix(collections): Map/Set/Weak constructors consume iterables and val…