Skip to content

js: this-binding for callbacks + generic .call/.apply - #69

Merged
hiett merged 1 commit into
experiment/js-frontendfrom
js262/wave12-thisarg
Jul 15, 2026
Merged

js: this-binding for callbacks + generic .call/.apply#69
hiett merged 1 commit into
experiment/js-frontendfrom
js262/wave12-thisarg

Conversation

@hiett

@hiett hiett commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What

Two related pieces of this-plumbing, targeting the largest remaining test262 blocker after wave 10 bound the built-in constructors.

Bucket 2 — generic .call/.apply on a built-in prototype method value (landed)

A static <Ctor>.prototype.<m>.call(recv, …args) / .apply(recv, [args]) on a built-in prototype method (Array/String/Object/Number/Boolean/Date/RegExp/Map/Set/Function) is rewritten to the ordinary method call recv.<m>(…args). In this receiver-first model the unbound method must run with recv as its this, so the rewrite reuses the entire tested method-dispatch path and forwards the receiver first. Previously func_call/func_apply dropped the receiver as the ignored thisArg (e.g. String.prototype.trim.call(x) trimmed undefined).

.apply is unrolled only when its argument array is a literal; otherwise it falls through.

Also gave the String receiver-coercion hook str_this its full spec meaning — ToString(RequireObjectCoercible(this)): undefined/null throw a TypeError, every non-string primitive/array/object is ToString-coerced, plain strings pass through. This flows to every string method already routing through str_this (upper/lower/charAt/split/trim family), so trim.call(0)"0", .call(false)"false", .call([1])"1", .call(undefined) throws.

Bucket 1 — thisArg on iteration callbacks (partial)

The thisArg of the Array iteration methods (map/filter/forEach/some/every/find/findIndex/findLast/findLastIndex) is now honored when the callback is an INLINE function(){…} expression that reads this — the callback closure's this capture is bound to the thisArg, the same mechanism object getters/setters already use. Arrows correctly ignore thisArg (lexical this).

Deferred (documented): a callback passed by NAME (arr.map(callbackfn, thisArg) — the shape most test262 this-arg files use) still ignores thisArg. A top-level function carries no this parameter (its this resolves to globalThis at lower time), so binding a dynamic receiver would require making this a call-time parameter of every plain function — a cross-cutting ABI change touching all call sites, apply_fn/call_cb, func_call/func_apply, and every iteration op. Out of scope for this wave.

Before → after (test262)

built-ins/{Array/prototype/{map,filter,forEach}, String/prototype/trim, Object/prototype/hasOwnProperty}:

202 → 241 pass (+39). Nearly all of the movement is bucket 2 (the String.prototype.trim.call(primitive) cluster + slice/hasOwnProperty .call). Bucket 1's inline-only subset moves ~0 test262 files (they use named callbacks) but is spec-correct and lands the requested unit behavior.

Tests

Full gleam test green (2781 passed; the only 3 failures are the pre-existing missing WASM .wat conformance fixtures, unrelated). Added 9 spec-driven tests under // ==== this-binding + call/apply (wave 12) ==== covering Array.prototype.slice.call/.apply, String.prototype.trim.call (string + coercion + RequireObjectCoercible throw), Object.prototype.hasOwnProperty.call, inline-callback map/forEach thisArg, and arrow-ignores-thisArg.

Files

  • src/twocore/frontend/js/lower.gleam.call/.apply rewrite + inline-callback thisArg binding + header docs
  • src/twocore_rt_js_ffi.erlstr_this full ToString/RequireObjectCoercible; trim family routes through it (no -export changes)
  • test/js_compiler_test.gleam — wave-12 tests

…totype methods

Rewrite a static <Ctor>.prototype.<m>.call(recv, ...args) / .apply(recv, [args])
on a built-in prototype method (Array/String/Object/Number/...) to the ordinary
method call recv.<m>(...args). In this receiver-first model the unbound method
must run with recv as its this, so this reuses the whole tested method-dispatch
path and forwards the receiver correctly. Previously func_call/func_apply dropped
the receiver as the ignored thisArg, so e.g. String.prototype.trim.call(x) trimmed
undefined instead of x.

Give the String.prototype receiver-coercion hook str_this its full spec meaning
(ToString(RequireObjectCoercible(this))): undefined/null throw a TypeError, every
non-string primitive/array/object is ToString-coerced, plain strings pass through.
This flows to every string method that already routes through str_this (upper,
lower, char_at, split, trim family) and is what makes trim.call(0) yield "0",
.call(false) "false", .call([1]) "1".

Honor the thisArg of the Array iteration methods for an INLINE function callback
that reads this, by binding the callback closure's this capture to the thisArg
(the same mechanism object accessors use). A callback passed by name still ignores
thisArg, since a top-level function has no this parameter; documented as a v1 gap.

test262 built-ins/{Array/prototype/{map,filter,forEach},String/prototype/trim,
Object/prototype/hasOwnProperty}: 202 -> 241 pass.
@hiett
hiett merged commit d2d4ac4 into experiment/js-frontend Jul 15, 2026
1 check passed
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