Skip to content

runtime: complete Map and Set forEach callback semantics #2830

Description

@andrewtdiz

Summary

Map.prototype.forEach and Set.prototype.forEach do not currently match Node's callback contract. Node calls the callback with three arguments and honors the optional thisArg; Perry's runtime path only calls the closure with two arguments and the codegen path ignores the second argument entirely.

This breaks userland that uses the third callback parameter to compare the receiver, or passes a receiver object for non-arrow callbacks.

Node behavior

const m = new Map([["k", "v"]]);
const ctx = { tag: "ctx" };
const seen = [];
m.forEach(function (value, key, self) {
  seen.push([this.tag, value, key, self === m]);
}, ctx);
console.log(seen);
// [["ctx", "v", "k", true]]

const s = new Set(["x"]);
s.forEach(function (value, key, self) {
  console.log(this.tag, value, key, self === s);
}, ctx);
// ctx x x true

Current Perry evidence

  • crates/perry-codegen/src/lower_call/property_get.rs routes forEach for Map/Set only when !args.is_empty(), lowers args[0], and calls js_map_foreach(map, callback) / js_set_foreach(set, callback). The optional args[1] thisArg is never lowered or passed.
  • crates/perry-runtime/src/map.rs::js_map_foreach calls js_closure_call2(closure_ptr, value, key) and the nearby comment says Call closure with (value, key).
  • crates/perry-runtime/src/set.rs::js_set_foreach calls js_closure_call2(closure_ptr, value, value) and similarly omits the Set receiver argument.
  • The same codegen branch returns double_literal(0.0) after dispatch; Node returns undefined.

Suggested test surface

Add node-suite tests for:

const m = new Map([["k", "v"]]);
const ctx = { tag: "ctx" };
const out: unknown[] = [];
const ret = m.forEach(function (this: any, value, key, self) {
  out.push(this.tag, value, key, self === m);
}, ctx);
console.log(JSON.stringify(out), ret === undefined);

const s = new Set(["x"]);
const out2: unknown[] = [];
s.forEach(function (this: any, value, key, self) {
  out2.push(this.tag, value, key, self === s);
}, ctx);
console.log(JSON.stringify(out2));

Expected output should show the bound thisArg, the third collection parameter, and an undefined return.

Scope / non-goals

This issue is specifically about Map.prototype.forEach and Set.prototype.forEach callback semantics. It does not cover general iterator object identity or collection constructor iterable handling.

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