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.
Summary
Map.prototype.forEachandSet.prototype.forEachdo not currently match Node's callback contract. Node calls the callback with three arguments and honors the optionalthisArg; 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
Current Perry evidence
crates/perry-codegen/src/lower_call/property_get.rsroutesforEachfor Map/Set only when!args.is_empty(), lowersargs[0], and callsjs_map_foreach(map, callback)/js_set_foreach(set, callback). The optionalargs[1]thisArgis never lowered or passed.crates/perry-runtime/src/map.rs::js_map_foreachcallsjs_closure_call2(closure_ptr, value, key)and the nearby comment saysCall closure with (value, key).crates/perry-runtime/src/set.rs::js_set_foreachcallsjs_closure_call2(closure_ptr, value, value)and similarly omits the Set receiver argument.double_literal(0.0)after dispatch; Node returnsundefined.Suggested test surface
Add node-suite tests for:
Expected output should show the bound
thisArg, the third collection parameter, and anundefinedreturn.Scope / non-goals
This issue is specifically about
Map.prototype.forEachandSet.prototype.forEachcallback semantics. It does not cover general iterator object identity or collection constructor iterable handling.