Skip to content

js: uniform this-parameter for plain functions (callback thisArg) - #76

Merged
hiett merged 3 commits into
experiment/js-frontendfrom
js262/wave14-thisparam
Jul 15, 2026
Merged

js: uniform this-parameter for plain functions (callback thisArg)#76
hiett merged 3 commits into
experiment/js-frontendfrom
js262/wave14-thisparam

Conversation

@hiett

@hiett hiett commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Uniform this parameter for plain functions (callback thisArg)

A plain (non-method) function's this was baked to globalThis at lower time, so
it could never be a call-time value. That made every this-arg / *-5-* cluster in
test262's Array iteration methods fail (this === thisArg for a named callback), and
dropped the receiver on f.call/apply/bind.

Approach — bake this at the value boundary (no FFI ABI change)

Rather than widening the whole call ABI, this mirrors how class methods already
work
: this is an explicit leading parameter of the implementation, and is baked
as a capture at the value boundary — so every callable BEAM value keeps its
user-arity and call_cb / apply_fn / func_* are unchanged.

  • A top-level function whose body reads this is lowered as <name>%this/(n+1)
    taking this first, with the exported <name>/n a thin wrapper that forwards the
    sloppy-mode default this (the globalThis singleton). A plain f(…) call is
    therefore behaviour-identical.
  • An Array iteration method (map/filter/forEach/some/every/find*) with a
    named callback + thisArg bakes the thisArg as the callback's this.
  • f.call(t, …) → direct call of f%this; f.apply(t, arr) → this-baked closure
    applied over arr; f.bind(t, …) → closure with this fixed to t.
  • Arrows are untouched (they correctly ignore a thisArg — lexical this).

Landed in two green stages: (1) the behaviour-preserving %this+wrapper split, then
(2) the thisArg wiring.

Results (test262, absolute-path suite)

Buckets: Array map/forEach/filter/every/some + Map/Set forEach.

before after
PASS (loaded+executed) 261 306 (+45)
fail_assert 65 20
  • 0 regressions across those 7 buckets and across Function.prototype
    call/apply/bind + find/findIndex (checked pass→fail set-diff: none).
  • The +45 are the Array -5- this-arg cluster (named callback asserting
    this === thisArg).

Tests

12 spec-driven tests appended under // ==== this parameter (wave 14) ====
(inline & named callback thisArg, f.call/.apply/.bind with a receiver, arrow
in a method sees the method's this, and a plain call's this still globalThis).

Green

Full gleam test: 2821 passed, 3 failures — the 3 are the pre-existing WASM
spec-suite failures (spec_suite_safe/unsafe, skip_count), identical to the base
branch. gleam format --check, gleam build (no warnings), and the erlc -Wall
FFI gate all clean.

Covered vs deferred

  • Covered: named-callback thisArg for Array iteration; f.call/apply/bind(t, …)
    on a named this-reading function; inline-callback thisArg (wave 12, preserved);
    arrows ignore thisArg.
  • Deferred (documented v1 gaps): a thisArg for a callback held in a VARIABLE
    (a function-expression value or let g = f; g.call(t) — the value carries no
    overridable this); Map/Set forEach thisArg (its test262 files use a variable
    callback → the same value gap); sloppy-mode this→global and primitive-this
    boxing for a directly-invoked function; bound-function .length/.name.

Merge-conflict surface

Touches function-lowering + call-site dispatch in
src/twocore/frontend/js/lower.gleam: lower_function (now returns
#(exported, extras)), program, lower_class (statics), lower_instance_method
and lower_instance_method_thisarg (new interception + lower_named_call/
_apply/_bind/_this_iter), plus a new Ctx.this_fns field. Also adds
apply_arg_list to rt_js.gleam, the FFI export list, and the emit js-op allowlist.

hiett added 3 commits July 15, 2026 07:20
A plain function's `this` was baked to globalThis at lower time, so it could
never be a call-time value. Give every top-level function whose body reads
`this` an internal `<name>%this` implementation that takes `this` as an
explicit leading parameter, and make the exported `<name>/n` a thin wrapper
that forwards the sloppy-mode default `this` (the globalThis singleton).

This is behaviour-identical on its own — an ordinary `f(...)` call still runs
with `this === globalThis` — but it is the groundwork for letting an iteration
method's thisArg or `.call`/`.apply`/`.bind` supply a real receiver: those
sites can now target the `%this` impl instead of dropping the receiver.
Now that a this-reading top-level function has a <name>%this implementation
taking this as a leading parameter, wire the sites that supply a dynamic
receiver to target it:

  - an Array iteration method (map/filter/forEach/some/every/find*) called with
    a NAMED callback + thisArg bakes the thisArg as the callback's this;
  - f.call(thisArg, ...args) becomes a direct call of f%this with the receiver;
  - f.apply(thisArg, argArray) applies a this-baked closure over f%this to the
    array's elements (via the now-exported apply_arg_list);
  - f.bind(thisArg, ...bound) returns a closure with this fixed to thisArg.

An arrow, a this-less function, or a plain-function VALUE variable is not
matched and keeps the prior behaviour (arrows correctly ignore a thisArg; a
value variable's thisArg is still dropped, a documented v1 gap).

apply_arg_list is exported from the runtime and registered in the emit js-op
allowlist so the apply path can reach it.
Update the module header and the call/apply/iteration dispatch docs to
describe the new %this-impl-plus-wrapper lowering, which sites thread a
dynamic thisArg, and what is still deferred (a thisArg for a callback held in
a variable, and sloppy-mode this->global / primitive-this boxing).
@hiett
hiett merged commit 10e5aa1 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