Skip to content

runtime: make Set constructor consume arbitrary iterables and throw on non-iterables #2771

Description

@andrewtdiz

Summary

new Set(iterable) currently routes through js_set_from_iterable, but that helper only handles a few input shapes directly:

  • arrays,
  • strings,
  • null / undefined as 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:304 handles new Set() / new Set(iterable).
  • crates/perry-hir/src/lower/expr_new.rs:316 returns Expr::SetNew for no arguments.
  • crates/perry-hir/src/lower/expr_new.rs:318 returns Expr::SetNewFromArray(firstArg) for any argument.
  • crates/perry-codegen/src/expr/logical_collections.rs:551 routes Expr::SetNewFromArray through js_set_from_iterable.

The runtime helper is intentionally lenient rather than Node-compatible:

  • crates/perry-runtime/src/set.rs:786 documents js_set_from_iterable as creating a Set from any iterable JS value.
  • crates/perry-runtime/src/set.rs:788 lists the actually handled shapes: Array, String, undefined/null, and anything else.
  • crates/perry-runtime/src/set.rs:795 explicitly says anything else becomes an empty set with no error, even though JS would throw TypeError.
  • crates/perry-runtime/src/set.rs:859 treats 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:866 returns a fresh empty set for undefined/null/number/etc., so new 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 null and undefined as 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 TypeError for non-iterable numbers, symbols, and plain objects.

Duplicate search

Searched existing issues/PRs for:

  • Set constructor iterable
  • new Set iterable
  • Set constructor TypeError
  • new Set non-iterable
  • js_set_from_iterable TypeError
  • new Set Map iterable
  • new 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions