Skip to content

fix(codemode): honor thisArg, program toString in computed keys, and ToPrimitive in String and Number arguments - #51264

Merged
rekram1-node merged 3 commits into
v2from
tiny-gaps
Sep 25, 2026
Merged

rekram1-node merged 3 commits into
v2from
tiny-gaps

Conversation

@rekram1-node

Copy link
Copy Markdown
Collaborator

Three small gaps where an argument the interpreter already receives was ignored or rejected. One commit each, in review order.

1. thisArg on iteration callbacks

 const c = { n: 0 }
 [1, 2].forEach(function () { this.n++ }, c)
 c.n
-// TypeError: Cannot read properties of undefined (reading 'n')
+2

Array forEach/map/filter/find/findIndex/findLast/findLastIndex/some/every/flatMap, the Uint8Array equivalents, Array.from(items, mapFn, thisArg), and Map/Set/URLSearchParams/Headers forEach pass it as this. reduce/reduceRight keep their initial-value slot. Arrows ignore it, as before.

src/stdlib/array.ts        callbackMethods.iterate   bind args[1] as the callback receiver except for reduce*
                           flatMap, arrayFrom        pass args[1] / args[2]
src/stdlib/collections.ts  Map.forEach, Set.forEach  pass args[1]
src/stdlib/url.ts          URLSearchParams.forEach   pass args[1]
src/stdlib/headers.ts      Headers.forEach           pass args[1]

2. Computed property keys use the object's own toString

 const key = { toString() { return "id" } }
 const o = {}
 o[key] = 1; o[key] += 1
 [o.id, key in o, (({ [key]: v }) => v)(o)]
-// [undefined, false, undefined]   (key was "[object Object]")
+[2, true, 2]

One conversion per access (compound assignment and ++ call toString once), in reads, writes, delete, in, object literals, and destructuring. Primitives keep the synchronous path. Opaque values (functions, promises, tool references) keep their built-in string form and are never invoked. A nullish base throws before the key converts (ToObject precedes ToPropertyKey).

src/interpreter/interpreter.ts  propertyKey            sync ToPropertyKey for primitives / opaque values (module-level)
                                Frame.toPropertyKey    now (value, node) => Effect; data objects go through toPrimitive("string")
                                getMemberReference     evaluate key, throw on nullish base, then convert
                                applyOperator          `in` converts a data-object left operand after the right-operand check
                                destructuringPropertyKey, object literal computed keys  use the Effect form

3. String and Number method arguments convert through ToPrimitive

 "abc".indexOf({ toString() { return "b" } })
-// TypeError: String.indexOf expects argument 1 to be a data value.
+1
 (255).toString({ valueOf() { return 16 } })
-// RangeError (radix became NaN)
+"ff"
 String.prototype.trim.call({ toString() { return " a " } })
-"[object Object]"
+"a"

Receiver first, then each consumed argument in spec order; string positions (search strings, separators, fills, replacements, normalize form, localeCompare) use the string hint, numeric positions (indexes, counts, digits, radix) the number hint. Extra arguments are not converted. A RegExp pattern to split/replace/match/matchAll/search is used as is; includes/startsWith/endsWith reject a RegExp before converting. Top-level opaque values and un-awaited promises still reject as data errors.

src/interpreter/callback.ts  withPrimitives   body may return an Effect (needed for replace's callback path)
src/stdlib/string.ts         simple           optional per-position hints; converts receiver + consumed args
                             searching        includes/startsWith/endsWith: RegExp check, then convert
                             withPattern      match/matchAll/search: RegExp as is, else string hint
                             replace, split   explicit ordering (limit before separator; pattern before replacement)
src/stdlib/number.ts         formatting       toFixed/toPrecision/toExponential/toString: receiver check, then number hint

Leakage probes

Tool calls counted by a host tool handler; every conversion path made zero calls.

Probe Result Tool calls
key toString throws RangeError: bad key 0
key toString recurses (key.toString = () => o[key]) RangeError: Maximum call stack size exceeded 0
key toString/valueOf return objects TypeError: Cannot convert object to primitive value. 0
key is tools.x key "[object Object]" (unchanged) 0
key { toString: tools.x } skipped as non-callable; TypeError: Cannot convert object to primitive value. 0
key toString returns tools.x TypeError: Cannot convert object to primitive value. 0
key toString returns 8M chars key of length 8000000 (built under the repeat cap; no separate key check) 0
key toString returns 2^24+1 chars RangeError: Invalid string length from repeat 0
key is an un-awaited promise key "[object Promise]" (unchanged) 0
thisArg is tools.x bound as this, typeof this === "function", not dispatched 0
thisArg is a promise bound as this 0
String argument toString returns tools.x TypeError: Cannot convert object to primitive value. 0
String argument toString throws a string the string surfaces 0
Number argument valueOf recurses RangeError: Maximum call stack size exceeded 0

Verification

Check Result
bun test (packages/codemode) 14539 pass, 1697 skip, 0 fail
test262 committed set 12996 → 13167 pass of 14865; 1869 → 1697 fail; 0 new failures
skipped.txt lines removed 172 (79 thisArg, 2 computed key, 91 String/Number)
bun run check (root) 35 successful, 35 total

Decisions

  • Headers.prototype.forEach got thisArg too: same one-line pattern as URLSearchParams, WebIDL-specified.
  • String method receivers convert too (String.prototype.trim.call(obj)): it is the same withPrimitives call, spec order requires receiver first, and it recovered ~25 test262 files. Say if this should be split out.
  • An array holding a tool reference as a String argument ("abc".indexOf([tools.x])) now converts through the array's toString instead of rejecting, matching the documented "operators inspect only their direct operands" rule. Top-level tools.x / promise arguments still reject.
  • Error.prototype.toString on an object message is not done: errorToString is also used synchronously at the result boundary, so it would need splitting rather than a one-line change. Left unchecked in interpreter-support.md.

Noticed, not fixed

  • The expected compound-assignment/S11.13.2_A7.* recoveries are blocked by new DummyError() (user-defined constructors), not by key conversion; 3 assignment/target-member-computed-reference*.js files likewise.
  • tools.x === tools.x is false (each member read creates a new ToolReference); pre-existing.
  • Number.prototype.toPrecision(p) style calls on the prototype itself still throw (no boxed Number.prototype); s.at(function () {}) still rejects the function argument by design.
  • String.prototype[Symbol.iterator] still uses the built-in string form of an object receiver.

@rekram1-node
rekram1-node merged commit 962c14a into v2 Sep 25, 2026
10 checks passed
@rekram1-node
rekram1-node deleted the tiny-gaps branch September 25, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant