Skip to content

runtime: make Map constructor consume iterable entries and throw on malformed init #2770

Description

@andrewtdiz

Summary

new Map(init) currently lowers any provided argument to Expr::MapNewFromArray and routes it through js_map_from_array. The runtime supports two narrow shapes:

That still misses Node's Map constructor semantics:

  • any iterable of entry objects should be accepted, not just arrays / Maps,
  • short entry objects such as ['key'] or [] should still add entries with undefined values/keys,
  • malformed entry values such as 1 should throw TypeError, not be skipped,
  • non-null, non-undefined, non-iterable init values should throw TypeError, not produce an empty/partial Map.

Node behavior

Checked with Node v25.9.0:

const m = new Map([['a', 1], ['b', 2]]);
m.size;      // 2
m.get('a');  // 1
m.get('b');  // 2

const clone = new Map(m);
clone.size;     // 2
clone.get('a'); // 1

const fromSet = new Map(new Set([['s', 3]]));
fromSet.size;     // 1
fromSet.get('s'); // 3

const short = new Map([['short']]);
short.size;                 // 1
short.has('short');         // true
short.get('short') === undefined; // true

const emptyPair = new Map([[]]);
emptyPair.size;                   // 1
emptyPair.has(undefined);         // true
emptyPair.get(undefined) === undefined; // true

new Map(null).size;      // 0
new Map(undefined).size; // 0

new Map(1);
// TypeError: number 1 is not iterable (cannot read property Symbol(Symbol.iterator))

new Map({ a: 1 });
// TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

new Map([1]);
// TypeError: Iterator value 1 is not an entry object

new Map(Symbol('x'));
// TypeError: symbol is not iterable (cannot read property Symbol(Symbol.iterator))

Perry implementation

Lowering sends any provided Map constructor argument to the same runtime helper:

  • crates/perry-hir/src/lower/expr_new.rs:283 handles new Map() / new Map(entries).
  • crates/perry-hir/src/lower/expr_new.rs:295 returns Expr::MapNew for no arguments.
  • crates/perry-hir/src/lower/expr_new.rs:298 returns Expr::MapNewFromArray(firstArg) for any argument.
  • crates/perry-codegen/src/expr/misc_methods.rs:57 lowers Expr::MapNewFromArray by unboxing the argument and calling js_map_from_array.

The runtime helper documents and implements only the narrow supported shapes:

  • crates/perry-runtime/src/map.rs:1191 says js_map_from_array supports array-of-pair arrays and another Map.
  • crates/perry-runtime/src/map.rs:1204 detects registered Map arguments and copies them, which is the fix(runtime): new Map(existingMap) copies entries (#33/#321) — needed by, but not sufficient for, Context/Layer #1871 fix.
  • crates/perry-runtime/src/map.rs:1223 otherwise treats the input as an ArrayHeader and iterates by array length.
  • crates/perry-runtime/src/map.rs:1236 / :1247 skip entries whose element is not an array-like pointer, so new Map([1]) cannot throw the required TypeError.
  • crates/perry-runtime/src/map.rs:1252 / :1254 skip nested arrays with fewer than two elements, so new Map([['short']]) and new Map([[]]) lose entries that Node keeps with undefined components.

Expected fix direction

The Map constructor path should implement AddEntriesFromIterable semantics more closely:

  • treat null and undefined as empty init,
  • obtain and consume a real iterator for other init values,
  • throw TypeError for non-iterable init values,
  • require each iterator value to be an object and throw when it is not,
  • read entry properties 0 and 1 without requiring an array length of at least 2,
  • keep the existing fast paths for array-of-pairs and new Map(existingMap) when they preserve the same behavior.

Duplicate search

Searched existing issues/PRs for:

  • Map constructor iterable
  • new Map iterable
  • Map entries constructor
  • new Map malformed entry
  • Map constructor Set iterable
  • Map constructor TypeError
  • js_map_from_array
  • PR search for Map constructor iterable OR js_map_from_array

No existing issue covered the remaining constructor semantics. Related but not duplicate: PR #1871 fixed only new Map(existingMap) copying, and #2645 tracks Headers constructor iterable/cloning behavior rather than Map itself.

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