Skip to content

Feature/runtime value struct#113

Merged
nickna merged 6 commits into
mainfrom
feature/runtime-value-struct
Jun 4, 2026
Merged

Feature/runtime value struct#113
nickna merged 6 commits into
mainfrom
feature/runtime-value-struct

Conversation

@nickna
Copy link
Copy Markdown
Owner

@nickna nickna commented Jun 4, 2026

No description provided.

nickna added 6 commits June 3, 2026 14:37
Compiled `.values()`/`.keys()`/`.entries()` return a bare
IEnumerator<object> (via NormalizeToEnumerator). On the dynamic/any-typed
dispatch path — which Test262 .js sources always take, since they run
without type-checking — GetProperty cannot resolve the JS iterator
protocol (next/value/done) on a BCL enumerator, so `it.next()` resolved
to undefined and the call yielded null. The typed path (driven by the
iterator emitter) was already correct, which masked this in .ts repros.

Adds $Runtime.IteratorProtocolCall(recv, name, args), called from
ILEmitter for any-typed `.next()`/`.return()` receivers. It resolves the
real JS member first — so generators and user-defined iterators keep
their own next(value)/return(value) semantics — and only synthesizes the
{value, done} result object when the receiver is a bare enumerator with
no JS-level method. Typed iterators continue to use the fast path.

Flips 3 compiled baseline entries Fail->Pass:
  Array/prototype/{values,keys,entries}/iteration.js

Verified: 0 regressions across 325 affected Test262 tests (iterators,
generators, Map/Set) and 1469 unit tests (iterator/generator + array/
for-of/spread/destructuring). Generator return-value handling is
actually improved by the real-member-first ordering.
$Runtime.ArrayIncludes returns an already-boxed bool (object), but the
ambiguous-method dispatch call sites emitted a second `Box Boolean` on
it. Boxing reinterprets the non-null object reference's bits as a bool,
so `arr.includes(x)` returned true almost always — and flakily false
only when the boxed-bool's heap pointer happened to have a zero low
byte. This hit the any-typed path that Test262 .js sources always take;
the typed ArrayEmitter path was already correct (no re-box), and the
adjacent indexOf path is fine because ArrayIndexOf returns a native
double, making its Box legitimate.

Removes the redundant Box in both ambiguous dispatchers
(ILEmitter.Calls.AmbiguousDispatch + ExpressionEmitterBase.CallHelpers).

Flips 4 compiled baseline entries Fail->Pass:
  Array/prototype/includes/{length-zero-returns-false,
  search-found-returns-true, search-not-found-returns-false,
  fromIndex-minus-zero}.js

Verified: 0 regressions across 57 includes Test262 tests and 951
array/includes/indexOf unit tests.
On the any-typed dispatch path, $BoundArrayMethod routes unshift through
EmitVariadicElementCase, which looped args front-to-back calling the
single-element ArrayUnshift helper. Since ArrayUnshift prepends one
element, forward iteration reversed the arguments:
`x.unshift(a, b, c)` produced [c, b, a, ...orig] instead of
[a, b, c, ...orig]. The typed ArrayEmitter path was already correct — it
calls EmitVariadicListMutation(..., reverse: true).

Adds a `reverse` option to EmitVariadicElementCase and passes it for
unshift (push stays forward). Now both paths prepend in argument order.

Flips Array/prototype/unshift/S15.4.4.13_A1_T2.js Fail->Pass.

Verified: 0 regressions across 46 unshift/push Test262 tests and 939
array unit tests; push ordering unaffected.
The $Array index-get unholes $ArrayHole.Instance to undefined (via
TSArrayGetLong), but the descriptor-driven List<object> branch of
$Runtime.GetIndex returned the raw element. ArrayMap returns a plain
List<object>, and `delete arr[i]` leaves $ArrayHole.Instance in-bounds,
so reading a map-result or deleted hole on the any-typed path produced a
typeof-"object" sentinel that compared unequal to `undefined`:
`[1,2,3,4,5].map(cbThatDeletesAnElement)[i] === undefined` was false.
(Literal-hole reads worked because [1,,3] is a $Array.)

Adds an unhole check to the in-range List<object> read: an element that
is $ArrayHole.Instance reads as $Undefined.Instance per ECMA-262. The
isinst is a no-op for value-typed backing lists, which never hold holes.

Flips Array/prototype/map/15.4.4.19-8-3.js Fail->Pass.

Verified: map target + map/filter/forEach/every/some/indexOf/reduce
hole tests Pass with 0 regressions; 1277 array/index/hole unit tests
pass.

Note: Array/from/calling-from-valid-1-noStrict.js (the other cluster-E
test) remains Fail — a separate sloppy-mode global-`this` coercion gap
(top-level `this` is null; mapFn `this` not coerced to global), left as
a documented known gap.
The per-iteration skip-index-box optimization (skip boxing args[1] for
unary callbacks) keyed on $TSFunction._expectsThis as a proxy for "arrow
that can't observe the index". But function DECLARATIONS are emitted
without a __this param, so _expectsThis=false — yet their bodies can read
the index via JS `arguments`. The optimization therefore dropped the
index, so `[...].map(function(){ ...arguments[1]... })` and
`Array.prototype.X.call(arrayLike, fn)` callbacks saw a null index.
Function expressions were unaffected (HasOwnThis → __this →
_expectsThis=true). Broad bug: it hit plain real arrays too.

Adds a $CapturesArguments marker attribute, applied to
function-declaration methods that reference `arguments`, surfaced as a
public _capturesArguments field on $TSFunction (computed once at
construction via MethodInfo.IsDefined). The skip-index-box detection now
also bails when _capturesArguments is set. The change is additive —
suppressing the optimization only adds a box, never wrong behavior — and
arrows never set the flag, so their fast path is preserved.

Flips 6 compiled baseline entries Fail->Pass:
  Array/prototype/{every(x2),filter,forEach,map,some}/*-c-ii-6/13

Verified: 6 targets Fail->Pass with a clean regression sample; full
non-Test262 unit suite shows only 2 failures, both pre-existing
PackagingTests.IntegrationTests (reproduce on a clean tree, unrelated to
this change). 973 function/closure/arguments/arrow + 1221 array-callback
unit tests pass.
$Runtime.StringReplaceWithFunction (the ECMA-262 22.1.3.18 step 3
functional-replacement implementation — invokes the callback with
[matched, ...captures, position, string] per match) was emitted but had
no callers. The dynamic-path entry point StringReplaceRegExp always
ToJsString'd the replacement, so `str.replace(/re/, fn)` spliced in the
stringified function ("function () {...}") instead of calling it. The
typed string path already routed callables correctly; this fixes the
any-typed dynamic path that Test262 .js sources take.

StringReplaceRegExp now checks TypeOf(replacement) === "function" and
delegates to StringReplaceWithFunction (for both regex and string
patterns). Emit order swapped so the WithFunction MethodBuilder is
assigned before StringReplaceRegExp references it.

Flips 7 compiled baseline entries Fail->Pass:
  String/prototype/replace/{S15.5.4.11_A4_T1..T4, A1_T4, A1_T9,
  cstm-replace-is-null}

Verified: 7 improved / 0 regressed across 100 replace/replaceAll Test262
tests; 730 string/replace/regex unit tests pass. (Capture-group
forwarding to a function-declaration callback relies on the
$CapturesArguments fix from the prior commit.)
@nickna nickna merged commit 66df5a3 into main Jun 4, 2026
3 checks 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