From 1d4fca1e7347a38207bf72497a6588ca13246816 Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 14:37:26 -0700 Subject: [PATCH 1/6] Fix dynamic .next()/.return() on array iterators in compiled mode (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiled `.values()`/`.keys()`/`.entries()` return a bare IEnumerator (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. --- Compilation/EmittedRuntime.cs | 5 + Compilation/ILEmitter.Calls.MethodDispatch.cs | 29 +++ .../RuntimeEmitter.Objects.Invocation.cs | 172 ++++++++++++++++++ Compilation/RuntimeEmitter.RuntimeClass.cs | 3 + SharpTS.Test262/baselines/compiled.txt | 6 +- 5 files changed, 212 insertions(+), 3 deletions(-) diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index 886196a0..01b6d08c 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -553,6 +553,11 @@ public class EmittedRuntime public MethodBuilder InvokeMethodValue { get; set; } = null!; public MethodBuilder GetSuperMethod { get; set; } = null!; + // Dynamic JS iterator-protocol bridge (.next()/.return()) for any-typed + // receivers that are bare IEnumerator — array .values()/.keys()/ + // .entries() return these. See EmitIteratorProtocolCall. + public MethodBuilder IteratorProtocolCall { get; set; } = null!; + // Function methods (bind/call/apply) public TypeBuilder BoundTSFunctionType { get; set; } = null!; public ConstructorBuilder BoundTSFunctionCtor { get; set; } = null!; diff --git a/Compilation/ILEmitter.Calls.MethodDispatch.cs b/Compilation/ILEmitter.Calls.MethodDispatch.cs index 484f3613..e5a66536 100644 --- a/Compilation/ILEmitter.Calls.MethodDispatch.cs +++ b/Compilation/ILEmitter.Calls.MethodDispatch.cs @@ -142,6 +142,35 @@ private void EmitMethodCall(Expr.Get methodGet, List arguments) return; } + // Iterator protocol (.next() / .return()) for any/unknown receivers. + // Array .values()/.keys()/.entries() return a bare IEnumerator + // that has no JS-shaped next/value/done members, so the generic + // GetProperty+InvokeMethodValue fallback below would resolve `next` to + // undefined and yield null. IteratorProtocolCall synthesizes the result + // object for enumerator receivers and falls back to the normal dynamic + // dispatch for everything else (generators, user iterators). Typed + // iterators are already handled by the type-first dispatch above, so + // only any/unknown receivers reach here. + if (methodName is "next" or "return") + { + EmitExpression(methodGet.Object); + EmitBoxIfNeeded(methodGet.Object); + IL.Emit(OpCodes.Ldstr, methodName); + IL.Emit(OpCodes.Ldc_I4, arguments.Count); + IL.Emit(OpCodes.Newarr, _ctx.Types.Object); + for (int i = 0; i < arguments.Count; i++) + { + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Ldc_I4, i); + EmitExpression(arguments[i]); + EmitBoxIfNeeded(arguments[i]); + IL.Emit(OpCodes.Stelem_Ref); + } + IL.Emit(OpCodes.Call, _ctx.Runtime!.IteratorProtocolCall); + SetStackUnknown(); + return; + } + // For object method calls, we need to pass the receiver as 'this' // Stack order: receiver, function, args diff --git a/Compilation/RuntimeEmitter.Objects.Invocation.cs b/Compilation/RuntimeEmitter.Objects.Invocation.cs index cfad58a8..8b2cd807 100644 --- a/Compilation/RuntimeEmitter.Objects.Invocation.cs +++ b/Compilation/RuntimeEmitter.Objects.Invocation.cs @@ -743,6 +743,178 @@ void EmitWrapperCheck(Type? checkType, MethodInfo? invokeMethod) il.Emit(OpCodes.Ret); } + /// + /// Emits object IteratorProtocolCall(object recv, string name, object[] args). + /// + /// Bridges the JS iterator protocol (.next() / .return()) for + /// any-typed receivers that are a bare + /// of object — which is exactly what array .values() / .keys() / + /// .entries() return (via NormalizeToEnumerator). A BCL + /// enumerator has no JS-shaped next/value/done members, + /// so the generic GetProperty + InvokeMethodValue fallback resolves + /// next to undefined and the call yields null. The typed + /// call path (handled by the iterator emitter) is unaffected; this only kicks + /// in when the receiver's type is unknown at compile time (e.g. Test262 .js + /// sources run without type-checking, so var it = arr.values() is + /// any). + /// + /// Non-enumerator receivers fall through to the normal dynamic dispatch, + /// preserving existing behavior for user objects that carry their own + /// next/return method (generators, custom iterators). + /// + private void EmitIteratorProtocolCall(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "IteratorProtocolCall", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.String, _types.ObjectArray]); + runtime.IteratorProtocolCall = method; + + var il = method.GetILGenerator(); + var setItem = _types.GetMethod(_types.DictionaryStringObject, "set_Item", _types.String, _types.Object); + + var enumLocal = il.DeclareLocal(_types.IEnumeratorOfObject); + var resultLocal = il.DeclareLocal(_types.DictionaryStringObject); + var fnLocal = il.DeclareLocal(_types.Object); + + var notEnumeratorLabel = il.DefineLabel(); + var returnBranchLabel = il.DefineLabel(); + + // Resolve the JS-level member first. Generators and user-defined + // iterators expose a real `next`/`return` callable here; only when the + // receiver has NO such member (a bare BCL enumerator) do we synthesize + // the result object below. This ordering guarantees generators keep + // their own next(value)/return(value) semantics — the enumerator branch + // is a pure rescue for values()/keys()/entries(). + var fn = fnLocal; + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.GetProperty); + il.Emit(OpCodes.Stloc, fn); + // if (fn == null) goto notEnumerator-or-synth + il.Emit(OpCodes.Ldloc, fn); + il.Emit(OpCodes.Brfalse, notEnumeratorLabel); + // if (fn == $Undefined.Instance) goto notEnumerator-or-synth + il.Emit(OpCodes.Ldloc, fn); + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Beq, notEnumeratorLabel); + // Real JS method present → normal dispatch. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, fn); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Ret); + + // No JS-level next/return. var en = recv as IEnumerator; + // if (en == null) fall back to normal (null-yielding) dispatch. + il.MarkLabel(notEnumeratorLabel); + var synthLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.IEnumeratorOfObject); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, enumLocal); + il.Emit(OpCodes.Brtrue, synthLabel); + // Not an enumerator and no JS method → preserve prior behavior: + // InvokeMethodValue(recv, fn, args) (fn is null/undefined → null). + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, fn); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + il.Emit(OpCodes.Ret); + + il.MarkLabel(synthLabel); + // result = new Dictionary() + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.DictionaryStringObject, _types.EmptyTypes)); + il.Emit(OpCodes.Stloc, resultLocal); + + // if (name == "return") goto returnBranch + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "return"); + il.Emit(OpCodes.Call, _types.StringOpEquality); + il.Emit(OpCodes.Brtrue, returnBranchLabel); + + // --- next(): { value: en.Current, done: false } | { value: undefined, done: true } --- + var nextDoneLabel = il.DefineLabel(); + var nextEndLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, enumLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.IEnumerator, "MoveNext")); + il.Emit(OpCodes.Brfalse, nextDoneLabel); + + // result["value"] = en.Current + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "value"); + il.Emit(OpCodes.Ldloc, enumLocal); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.IEnumeratorOfObject, "Current")); + il.Emit(OpCodes.Callvirt, setItem); + // result["done"] = false + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "done"); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Callvirt, setItem); + il.Emit(OpCodes.Br, nextEndLabel); + + il.MarkLabel(nextDoneLabel); + // result["value"] = undefined + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "value"); + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.Emit(OpCodes.Callvirt, setItem); + // result["done"] = true + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "done"); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Callvirt, setItem); + + il.MarkLabel(nextEndLabel); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + // --- return(value): close the iterator, yield { value: value, done: true } --- + il.MarkLabel(returnBranchLabel); + // if (en is IDisposable d) d.Dispose(); + var disposableLocal = il.DeclareLocal(_types.IDisposable); + var notDisposableLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, enumLocal); + il.Emit(OpCodes.Isinst, _types.IDisposable); + il.Emit(OpCodes.Stloc, disposableLocal); + il.Emit(OpCodes.Ldloc, disposableLocal); + il.Emit(OpCodes.Brfalse, notDisposableLabel); + il.Emit(OpCodes.Ldloc, disposableLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.IDisposable, "Dispose")); + il.MarkLabel(notDisposableLabel); + + // result["value"] = (args != null && args.Length > 0) ? args[0] : undefined + var useUndefLabel = il.DefineLabel(); + var haveValueLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "value"); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Brfalse, useUndefLabel); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brfalse, useUndefLabel); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Br, haveValueLabel); + il.MarkLabel(useUndefLabel); + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.MarkLabel(haveValueLabel); + il.Emit(OpCodes.Callvirt, setItem); + // result["done"] = true + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldstr, "done"); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Callvirt, setItem); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + } + private void EmitGetSuperMethod(TypeBuilder typeBuilder, EmittedRuntime runtime) { // GetSuperMethod(object instance, string methodName) -> object diff --git a/Compilation/RuntimeEmitter.RuntimeClass.cs b/Compilation/RuntimeEmitter.RuntimeClass.cs index be4e7c26..f37a42ed 100644 --- a/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -619,6 +619,9 @@ void EmitLinkProto(FieldBuilder child) // String/Number/Boolean populate shells already defined above // (before cctor) so the cctor can call them eagerly. EmitGetProperty(typeBuilder, runtime); + // Dynamic iterator-protocol bridge — must come after GetProperty + + // InvokeMethodValue since its non-enumerator fallback calls both. + EmitIteratorProtocolCall(typeBuilder, runtime); // GetSymbolDict / IsSymbol already emitted above (moved earlier so // HasOwnPropertyHelper's Symbol-key arm can call them). // ToJsString depends on GetProperty + InvokeMethodValue + Stringify; emit after those. diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 2cde0786..bb6df0d6 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -399,7 +399,7 @@ test/built-ins/Array/prototype/copyWithin/return-abrupt-from-this.js Fail test/built-ins/Array/prototype/copyWithin/return-this.js Fail test/built-ins/Array/prototype/copyWithin/undefined-end.js Pass test/built-ins/Array/prototype/entries/iteration-mutable.js Fail -test/built-ins/Array/prototype/entries/iteration.js Fail +test/built-ins/Array/prototype/entries/iteration.js Pass test/built-ins/Array/prototype/entries/length.js Pass test/built-ins/Array/prototype/entries/name.js Pass test/built-ins/Array/prototype/entries/not-a-constructor.js Pass @@ -1475,7 +1475,7 @@ test/built-ins/Array/prototype/join/not-a-constructor.js Pass test/built-ins/Array/prototype/join/prop-desc.js Pass test/built-ins/Array/prototype/join/resizable-buffer.js RuntimeError test/built-ins/Array/prototype/keys/iteration-mutable.js Fail -test/built-ins/Array/prototype/keys/iteration.js Fail +test/built-ins/Array/prototype/keys/iteration.js Pass test/built-ins/Array/prototype/keys/length.js Pass test/built-ins/Array/prototype/keys/name.js Pass test/built-ins/Array/prototype/keys/not-a-constructor.js Pass @@ -3048,7 +3048,7 @@ test/built-ins/Array/prototype/unshift/set-length-zero-array-length-is-non-writa test/built-ins/Array/prototype/unshift/throws-if-integer-limit-exceeded.js Fail test/built-ins/Array/prototype/unshift/throws-with-string-receiver.js Fail test/built-ins/Array/prototype/values/iteration-mutable.js Fail -test/built-ins/Array/prototype/values/iteration.js Fail +test/built-ins/Array/prototype/values/iteration.js Pass test/built-ins/Array/prototype/values/length.js Pass test/built-ins/Array/prototype/values/name.js Pass test/built-ins/Array/prototype/values/not-a-constructor.js Pass From 751408342541658cffc4bf546350fb5050a79c4a Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 14:55:54 -0700 Subject: [PATCH 2/6] Fix Array.prototype.includes double-boxing on dynamic dispatch (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $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. --- Compilation/ExpressionEmitterBase.CallHelpers.cs | 3 ++- Compilation/ILEmitter.Calls.AmbiguousDispatch.cs | 7 ++++++- SharpTS.Test262/baselines/compiled.txt | 8 ++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Compilation/ExpressionEmitterBase.CallHelpers.cs b/Compilation/ExpressionEmitterBase.CallHelpers.cs index 7880c94b..9ab54824 100644 --- a/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -1270,8 +1270,9 @@ protected void EmitAmbiguousMethodCall(Expr obj, string methodName, List a { case "includes": if (arguments.Count > 0) { EmitExpression(arguments[0]); EnsureBoxed(); } else { IL.Emit(OpCodes.Ldnull); } + // ArrayIncludes already returns a boxed bool — do not re-box + // (double-boxing reinterprets the object reference as a bool). IL.Emit(OpCodes.Call, Ctx.Runtime!.ArrayIncludes); - IL.Emit(OpCodes.Box, typeof(bool)); break; case "indexOf": case "lastIndexOf": diff --git a/Compilation/ILEmitter.Calls.AmbiguousDispatch.cs b/Compilation/ILEmitter.Calls.AmbiguousDispatch.cs index 5f3c88dd..cc1366c5 100644 --- a/Compilation/ILEmitter.Calls.AmbiguousDispatch.cs +++ b/Compilation/ILEmitter.Calls.AmbiguousDispatch.cs @@ -261,8 +261,13 @@ public partial class ILEmitter { IL.Emit(OpCodes.Ldnull); } + // ArrayIncludes already returns a boxed bool (object). The + // earlier `Box Boolean` here double-boxed it — reinterpreting + // the non-null object reference's bits as a bool, which yields + // `true` almost always and flakily `false` when the pointer's + // low byte happened to be 0. (Contrast ArrayIndexOf below, which + // returns a native double, so its Box is correct.) IL.Emit(OpCodes.Call, _ctx.Runtime!.ArrayIncludes); - IL.Emit(OpCodes.Box, _ctx.Types.Boolean); break; case "indexOf": diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index bb6df0d6..94210d9a 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -1224,10 +1224,10 @@ test/built-ins/Array/prototype/includes/call-with-boolean.js Pass test/built-ins/Array/prototype/includes/coerced-searchelement-fromindex-resize.js RuntimeError test/built-ins/Array/prototype/includes/fromIndex-equal-or-greater-length-returns-false.js Fail test/built-ins/Array/prototype/includes/fromIndex-infinity.js Fail -test/built-ins/Array/prototype/includes/fromIndex-minus-zero.js Fail +test/built-ins/Array/prototype/includes/fromIndex-minus-zero.js Pass test/built-ins/Array/prototype/includes/get-prop.js RuntimeError test/built-ins/Array/prototype/includes/length-boundaries.js Fail -test/built-ins/Array/prototype/includes/length-zero-returns-false.js Fail +test/built-ins/Array/prototype/includes/length-zero-returns-false.js Pass test/built-ins/Array/prototype/includes/length.js Pass test/built-ins/Array/prototype/includes/name.js Pass test/built-ins/Array/prototype/includes/no-arg.js Fail @@ -1242,8 +1242,8 @@ test/built-ins/Array/prototype/includes/return-abrupt-tointeger-fromindex.js Fai test/built-ins/Array/prototype/includes/return-abrupt-tonumber-length-symbol.js Pass test/built-ins/Array/prototype/includes/return-abrupt-tonumber-length.js Pass test/built-ins/Array/prototype/includes/samevaluezero.js Fail -test/built-ins/Array/prototype/includes/search-found-returns-true.js Fail -test/built-ins/Array/prototype/includes/search-not-found-returns-false.js Fail +test/built-ins/Array/prototype/includes/search-found-returns-true.js Pass +test/built-ins/Array/prototype/includes/search-not-found-returns-false.js Pass test/built-ins/Array/prototype/includes/sparse.js Fail test/built-ins/Array/prototype/includes/this-is-not-object.js Fail test/built-ins/Array/prototype/includes/tointeger-fromindex.js Fail From 05a84f0d6c187164c38ad98a948fce3adccb5d6f Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 15:03:51 -0700 Subject: [PATCH 3/6] Fix dynamic Array.prototype.unshift multi-arg ordering (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Compilation/RuntimeEmitter.Arrays.cs | 49 ++++++++++++++++++++------ SharpTS.Test262/baselines/compiled.txt | 2 +- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Compilation/RuntimeEmitter.Arrays.cs b/Compilation/RuntimeEmitter.Arrays.cs index 82b236d3..43d87735 100644 --- a/Compilation/RuntimeEmitter.Arrays.cs +++ b/Compilation/RuntimeEmitter.Arrays.cs @@ -1525,7 +1525,12 @@ void EmitSearchCase(string methodName, MethodBuilder runtimeMethod) // behave the same as JS `arr.push(1, 2, 3)`. // forEach arg in args: runtime.Method(_list, arg) // return (double)_list.Count - void EmitVariadicElementCase(string methodName, MethodBuilder runtimeMethod) + // reverse: iterate args from last to first. Required for unshift — + // ArrayUnshift prepends a single element, so `arr.unshift(a, b, c)` + // must insert c, then b, then a to land as [a, b, c, ...orig]. Forward + // iteration would reverse the arguments. Mirrors ArrayEmitter's typed + // path, which calls EmitVariadicListMutation(..., reverse: true). + void EmitVariadicElementCase(string methodName, MethodBuilder runtimeMethod, bool reverse = false) { var skipLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); @@ -1535,18 +1540,42 @@ void EmitVariadicElementCase(string methodName, MethodBuilder runtimeMethod) il.Emit(OpCodes.Brfalse, skipLabel); var indexLocal = il.DeclareLocal(_types.Int32); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc, indexLocal); + if (reverse) + { + // index = args.Length - 1 + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Stloc, indexLocal); + } + else + { + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + } var loopStartLabel = il.DefineLabel(); var loopEndLabel = il.DefineLabel(); il.MarkLabel(loopStartLabel); - il.Emit(OpCodes.Ldloc, indexLocal); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Bge, loopEndLabel); + if (reverse) + { + // while (index >= 0) + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, loopEndLabel); + } + else + { + // while (index < args.Length) + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, loopEndLabel); + } // runtime.Method(_list, args[index]) — return value popped (use Count afterward) il.Emit(OpCodes.Ldarg_0); @@ -1559,7 +1588,7 @@ void EmitVariadicElementCase(string methodName, MethodBuilder runtimeMethod) il.Emit(OpCodes.Ldloc, indexLocal); il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Add); + il.Emit(reverse ? OpCodes.Sub : OpCodes.Add); il.Emit(OpCodes.Stloc, indexLocal); il.Emit(OpCodes.Br, loopStartLabel); @@ -1609,7 +1638,7 @@ void EmitArgsArrayCase(string methodName, MethodBuilder runtimeMethod) // JS-variadic methods — loop through args, calling the single-element helper // for each. Matches JS semantics: `arr.push(1, 2, 3)` pushes three elements. EmitVariadicElementCase("push", runtime.ArrayPush); - EmitVariadicElementCase("unshift", runtime.ArrayUnshift); + EmitVariadicElementCase("unshift", runtime.ArrayUnshift, reverse: true); // Single-arg methods (runtime helper takes `object`, not `object[]`). // Aligns with Emitters/ArrayEmitter.cs which also uses EmitSingleArgOrNull diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 94210d9a..64f589c5 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -3026,7 +3026,7 @@ test/built-ins/Array/prototype/toString/non-callable-join-string-tag.js Fail test/built-ins/Array/prototype/toString/not-a-constructor.js Pass test/built-ins/Array/prototype/toString/prop-desc.js Pass test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js Pass -test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T2.js Fail +test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T2.js Pass test/built-ins/Array/prototype/unshift/S15.4.4.13_A2_T1.js Fail test/built-ins/Array/prototype/unshift/S15.4.4.13_A2_T2.js Fail test/built-ins/Array/prototype/unshift/S15.4.4.13_A2_T3.js Pass From 8719c81b6a10d6acdf61079ad526c54fd07f2d3f Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 15:41:22 -0700 Subject: [PATCH 4/6] Fix hole-read on plain List index access in compiled mode (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The $Array index-get unholes $ArrayHole.Instance to undefined (via TSArrayGetLong), but the descriptor-driven List branch of $Runtime.GetIndex returned the raw element. ArrayMap returns a plain List, 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 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. --- Compilation/RuntimeEmitter.Objects.Index.cs | 15 +++++++++++++++ SharpTS.Test262/baselines/compiled.txt | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Compilation/RuntimeEmitter.Objects.Index.cs b/Compilation/RuntimeEmitter.Objects.Index.cs index bef12ef8..a35cdabc 100644 --- a/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/Compilation/RuntimeEmitter.Objects.Index.cs @@ -459,6 +459,21 @@ void EmitProtoSymbolFallback(Type receiverType, FieldBuilder protoField, MethodB il.Emit(OpCodes.Ldloc, idxLocal); il.Emit(OpCodes.Callvirt, _types.GetMethod(listType, "get_Item", _types.Int32)); desc.EmitBoxElement(il, _types); + // Unhole: an in-range slot holding $ArrayHole.Instance reads as + // `undefined` per ECMA-262 (holes are absent, not present-with-hole). + // The $Array path already unholes via TSArrayGetLong; plain + // List receivers (e.g. the List returned by ArrayMap, or a + // list mutated by `delete arr[i]`) reached here and leaked the raw + // sentinel — so `[1,2,3,4,5].map(cb-that-deletes)[i]` compared + // unequal to `undefined`. The isinst is a no-op for value-typed + // backing lists, which never contain holes. + var notHoleLabel = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Isinst, runtime.ArrayHoleType); + il.Emit(OpCodes.Brfalse, notHoleLabel); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldsfld, runtime.UndefinedInstance); + il.MarkLabel(notHoleLabel); il.Emit(OpCodes.Ret); } diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 64f589c5..d6abbd93 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -1784,7 +1784,7 @@ test/built-ins/Array/prototype/map/15.4.4.19-6-1.js Pass test/built-ins/Array/prototype/map/15.4.4.19-6-2.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-1.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-2.js Pass -test/built-ins/Array/prototype/map/15.4.4.19-8-3.js Fail +test/built-ins/Array/prototype/map/15.4.4.19-8-3.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-4.js Fail test/built-ins/Array/prototype/map/15.4.4.19-8-5.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-6.js Fail From e5cf2bce08384c9170c2ae88a88769e48c8acb9b Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 19:19:07 -0700 Subject: [PATCH 5/6] Fix index arg dropped for function-declaration array callbacks (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Compilation/EmittedRuntime.cs | 11 ++++ Compilation/ILCompiler.Functions.cs | 9 ++++ .../RuntimeEmitter.Arrays.Iterators.cs | 10 ++++ Compilation/RuntimeEmitter.TSFunction.cs | 52 +++++++++++++++++++ Compilation/RuntimeEmitter.cs | 5 ++ SharpTS.Test262/baselines/compiled.txt | 12 ++--- 6 files changed, 93 insertions(+), 6 deletions(-) diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index 01b6d08c..fe77a53f 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -227,6 +227,17 @@ public class EmittedRuntime // path — read at runtime to detect callback arity without reflection. public FieldBuilder TSFunctionParamCountField { get; set; } = null!; public FieldBuilder TSFunctionExpectsThisField { get; set; } = null!; + // True when the wrapped method's body reads JS `arguments`. Set via the + // $CapturesArguments marker attribute (function declarations only — function + // expressions already get _expectsThis=true via their __this param). Read by + // the iterator-helper skip-index-box detection so it never drops the index + // arg for a callback that could observe it through `arguments`. + public FieldBuilder TSFunctionCapturesArgumentsField { get; set; } = null!; + // Marker attribute applied to function-declaration methods that reference + // `arguments`. Its ctor is invoked via CustomAttributeBuilder at method + // definition; the type token is read back via MethodInfo.IsDefined. + public TypeBuilder CapturesArgumentsAttrType { get; set; } = null!; + public ConstructorBuilder CapturesArgumentsAttrCtor { get; set; } = null!; // String methods public MethodBuilder StringCharAt { get; set; } = null!; diff --git a/Compilation/ILCompiler.Functions.cs b/Compilation/ILCompiler.Functions.cs index 3f9486c8..b378ec11 100644 --- a/Compilation/ILCompiler.Functions.cs +++ b/Compilation/ILCompiler.Functions.cs @@ -110,6 +110,15 @@ private void DefineFunction(Stmt.Function funcStmt) if (funcStmt.Body != null && ReferencesArgumentsIdentifier(funcStmt.Body)) { _functions.CapturingArguments.Add(qualifiedFunctionName); + // Mark the method so $TSFunction can detect (at runtime, via + // IsDefined) that this callback may observe the iteration index + // through `arguments`. Without it, the iterator-helper + // skip-index-box optimization treats this `this`-less declaration + // like an arrow and drops args[1], so `function(){...arguments[1]...}` + // used as a map/forEach/every callback reads a null index (#101). + if (_runtime?.CapturesArgumentsAttrCtor != null) + methodBuilder.SetCustomAttribute( + new System.Reflection.Emit.CustomAttributeBuilder(_runtime.CapturesArgumentsAttrCtor, [])); } // Generate overloads for functions with default parameters diff --git a/Compilation/RuntimeEmitter.Arrays.Iterators.cs b/Compilation/RuntimeEmitter.Arrays.Iterators.cs index 42e9cfdf..b76f7832 100644 --- a/Compilation/RuntimeEmitter.Arrays.Iterators.cs +++ b/Compilation/RuntimeEmitter.Arrays.Iterators.cs @@ -306,6 +306,16 @@ private void EmitDetectSkipIndexBox(ILGenerator il, EmittedRuntime runtime, out il.Emit(OpCodes.Ldfld, runtime.TSFunctionExpectsThisField); il.Emit(OpCodes.Brtrue, doneLabel); + // if (tsFn._capturesArguments) goto doneLabel — `this`-less function + // DECLARATIONS have _expectsThis=false (no __this param) yet can still + // observe the index via `arguments`. Without this guard the index box + // was skipped and `function(){...arguments[1]...}` callbacks read a null + // index (#101). Arrows never set this flag (they can't bind their own + // `arguments`), so the unary-arrow fast path is preserved. + il.Emit(OpCodes.Ldloc, tsFnLocal); + il.Emit(OpCodes.Ldfld, runtime.TSFunctionCapturesArgumentsField); + il.Emit(OpCodes.Brtrue, doneLabel); + // if (tsFn._paramCount > 1) goto doneLabel il.Emit(OpCodes.Ldloc, tsFnLocal); il.Emit(OpCodes.Ldfld, runtime.TSFunctionParamCountField); diff --git a/Compilation/RuntimeEmitter.TSFunction.cs b/Compilation/RuntimeEmitter.TSFunction.cs index 1d52b5e7..70fddcb0 100644 --- a/Compilation/RuntimeEmitter.TSFunction.cs +++ b/Compilation/RuntimeEmitter.TSFunction.cs @@ -30,6 +30,32 @@ private void EmitArgumentsContextClass(ModuleBuilder moduleBuilder, EmittedRunti typeBuilder.CreateType(); } + /// + /// Emits a minimal marker attribute $CapturesArguments (empty + /// subclass). Applied to function-declaration + /// methods whose body reads JS arguments; read back via + /// at + /// $TSFunction construction. Lives in the output assembly so the + /// compiled DLL stays standalone. + /// + private void EmitCapturesArgumentsAttribute(ModuleBuilder moduleBuilder, EmittedRuntime runtime) + { + var typeBuilder = moduleBuilder.DefineType( + "$CapturesArguments", + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + typeof(System.Attribute)); + var ctor = typeBuilder.DefineConstructor( + MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var ctorIl = ctor.GetILGenerator(); + ctorIl.Emit(OpCodes.Ldarg_0); + ctorIl.Emit(OpCodes.Call, typeof(System.Attribute).GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null)!); + ctorIl.Emit(OpCodes.Ret); + runtime.CapturesArgumentsAttrCtor = ctor; + runtime.CapturesArgumentsAttrType = typeBuilder; + typeBuilder.CreateType(); + } + private void EmitTSFunctionClass(ModuleBuilder moduleBuilder, EmittedRuntime runtime) { // Define class: public sealed class $TSFunction @@ -57,6 +83,11 @@ private void EmitTSFunctionClass(ModuleBuilder moduleBuilder, EmittedRuntime run // for the "unary-arrow fast path" without going through a method call. var expectsThisField = typeBuilder.DefineField("_expectsThis", _types.Boolean, FieldAttributes.Public); runtime.TSFunctionExpectsThisField = expectsThisField; + // True when the wrapped method's body reads JS `arguments` (marked with + // the $CapturesArguments attribute). Public so the iterator-helper + // skip-index-box detection can read it without a method call. + var capturesArgumentsField = typeBuilder.DefineField("_capturesArguments", _types.Boolean, FieldAttributes.Public); + runtime.TSFunctionCapturesArgumentsField = capturesArgumentsField; // Cached MethodInvoker. .NET 8+'s MethodInvoker.Create() pre-builds // the JIT'd dispatch stub for a method, then Invoke(...) calls it // directly — measured ~10× faster than MethodInfo.Invoke per call. @@ -201,6 +232,8 @@ private void EmitTSFunctionClass(ModuleBuilder moduleBuilder, EmittedRuntime run ctorIL.Emit(OpCodes.Stfld, cachedNameField); // this._expectsThis = (method.GetParameters().Length > 0 && params[0].Name == "__this") EmitComputeExpectsThis(ctorIL, expectsThisField, methodArgIndex: 2); + // this._capturesArguments = method.IsDefined($CapturesArguments) + EmitComputeCapturesArguments(ctorIL, capturesArgumentsField, runtime, methodArgIndex: 2); // this._paramCount, _hasListRest, _hasArrayRest: cached by AdjustArgs. EmitComputeAdjustArgsCache(ctorIL, paramCountField, hasListRestField, hasArrayRestField, methodArgIndex: 2); EmitComputeNeedsArgConversion(ctorIL, needsArgConversionField, methodArgIndex: 2); @@ -239,6 +272,7 @@ private void EmitTSFunctionClass(ModuleBuilder moduleBuilder, EmittedRuntime run ctorCacheIL.Emit(OpCodes.Stfld, cachedLengthField); // this._expectsThis = (method.GetParameters().Length > 0 && params[0].Name == "__this") EmitComputeExpectsThis(ctorCacheIL, expectsThisField, methodArgIndex: 2); + EmitComputeCapturesArguments(ctorCacheIL, capturesArgumentsField, runtime, methodArgIndex: 2); EmitComputeAdjustArgsCache(ctorCacheIL, paramCountField, hasListRestField, hasArrayRestField, methodArgIndex: 2); EmitComputeNeedsArgConversion(ctorCacheIL, needsArgConversionField, methodArgIndex: 2); // this._invoker = LookupOrAdd(_invokerCache, method) @@ -1233,6 +1267,24 @@ private void EmitComputeExpectsThis(ILGenerator il, FieldBuilder expectsThisFiel il.Emit(OpCodes.Stfld, expectsThisField); } + /// + /// Emits this._capturesArguments = method.IsDefined(typeof($CapturesArguments), false). + /// True for function-declaration methods whose body reads JS arguments; + /// see . + /// + private void EmitComputeCapturesArguments(ILGenerator il, FieldBuilder capturesArgumentsField, EmittedRuntime runtime, int methodArgIndex) + { + // this._capturesArguments = method.IsDefined(attrType, false) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg, methodArgIndex); + il.Emit(OpCodes.Ldtoken, runtime.CapturesArgumentsAttrType); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle)); + il.Emit(OpCodes.Ldc_I4_0); + // MemberInfo.IsDefined(Type, bool) — looked up via MethodInfo (inherited). + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "IsDefined", _types.Type, _types.Boolean)); + il.Emit(OpCodes.Stfld, capturesArgumentsField); + } + /// /// Emits an instance helper method $TSFunction.AdjustArgs(object[] args) /// that adjusts argument arrays for rest parameters and padding/trimming diff --git a/Compilation/RuntimeEmitter.cs b/Compilation/RuntimeEmitter.cs index 32ba292b..c944ec76 100644 --- a/Compilation/RuntimeEmitter.cs +++ b/Compilation/RuntimeEmitter.cs @@ -57,6 +57,11 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea // that type's field layout; isolating keeps $TSFunction's layout unchanged. EmitArgumentsContextClass(moduleBuilder, runtime); + // Marker attribute for "this method's body reads JS `arguments`". + // Must be defined+created before EmitTSFunctionClass so its ctor IL can + // ldtoken the type for the IsDefined read. + EmitCapturesArgumentsAttribute(moduleBuilder, runtime); + // Emit TSFunction class first (other methods depend on it) EmitTSFunctionClass(moduleBuilder, runtime); diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index d6abbd93..17b580cb 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -562,7 +562,7 @@ test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-1.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-10.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-11.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-12.js Pass -test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-13.js Fail +test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-13.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-16.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-17.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-18.js Pass @@ -575,7 +575,7 @@ test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-23.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-3.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-4.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-5.js Pass -test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-6.js Fail +test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-6.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-7.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-8.js Pass test/built-ins/Array/prototype/every/15.4.4.16-7-c-ii-9.js Pass @@ -829,7 +829,7 @@ test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-22.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-23.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-4.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-5.js Pass -test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-6.js Fail +test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-6.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-7.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-8.js Pass test/built-ins/Array/prototype/filter/15.4.4.20-9-c-ii-9.js Pass @@ -1192,7 +1192,7 @@ test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-22.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-23.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-4.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-5.js Pass -test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-6.js Fail +test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-6.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-7.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-8.js Pass test/built-ins/Array/prototype/forEach/15.4.4.18-7-c-ii-9.js Pass @@ -1852,7 +1852,7 @@ test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-22.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-23.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-4.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-5.js Pass -test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-6.js Fail +test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-6.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-7.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-8.js Pass test/built-ins/Array/prototype/map/15.4.4.19-8-c-ii-9.js Pass @@ -2746,7 +2746,7 @@ test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-23.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-3.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-4.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-5.js Pass -test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-6.js Fail +test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-6.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-7.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-8.js Pass test/built-ins/Array/prototype/some/15.4.4.17-7-c-ii-9.js Pass From 6dc834b945bf198d5d592d16ef984b8bb475b9eb Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 3 Jun 2026 19:41:11 -0700 Subject: [PATCH 6/6] Wire callable replacement into String.prototype.replace (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $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.) --- Compilation/RuntimeEmitter.RegExp.cs | 25 ++++++++++++++++++++++++- SharpTS.Test262/baselines/compiled.txt | 14 +++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Compilation/RuntimeEmitter.RegExp.cs b/Compilation/RuntimeEmitter.RegExp.cs index 795941c8..92586d96 100644 --- a/Compilation/RuntimeEmitter.RegExp.cs +++ b/Compilation/RuntimeEmitter.RegExp.cs @@ -28,8 +28,10 @@ private void EmitRegExpMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) EmitRegExpSetLastIndex(typeBuilder, runtime); EmitStringMatchRegExp(typeBuilder, runtime); EmitStringMatchAllRegExp(typeBuilder, runtime); - EmitStringReplaceRegExp(typeBuilder, runtime); + // WithFunction first: StringReplaceRegExp delegates to it for callable + // replacements, so its MethodBuilder must be assigned beforehand. EmitStringReplaceWithFunction(typeBuilder, runtime); + EmitStringReplaceRegExp(typeBuilder, runtime); EmitStringReplaceAllRegExp(typeBuilder, runtime); EmitStringSearchRegExp(typeBuilder, runtime); EmitStringSplitRegExp(typeBuilder, runtime); @@ -1053,6 +1055,27 @@ private void EmitStringReplaceRegExp(TypeBuilder typeBuilder, EmittedRuntime run var idxLocal = il.DeclareLocal(_types.Int32); var notFoundLabel = il.DefineLabel(); + // ECMA-262 22.1.3.18 step 3: when replaceValue is callable, the + // per-match substitution invokes it with (matched, ...captures, position, + // string) rather than ToString-coercing it. StringReplaceWithFunction + // implements that for both regex and string patterns. Without this the + // function was ToJsString'd to "function () {...}" and spliced in + // literally (e.g. `s.replace(/re/, fn)` yielded "[Function] ..."). The + // typed string path already routes callables correctly; this covers the + // any-typed dynamic path that Test262 .js sources take. + var notCallableLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.TypeOf); + il.Emit(OpCodes.Ldstr, "function"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notCallableLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.StringReplaceWithFunction); + il.Emit(OpCodes.Ret); + il.MarkLabel(notCallableLabel); + // var regexp = pattern as $RegExp il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Isinst, runtime.TSRegExpType); diff --git a/SharpTS.Test262/baselines/compiled.txt b/SharpTS.Test262/baselines/compiled.txt index 17b580cb..2e2f0cfe 100644 --- a/SharpTS.Test262/baselines/compiled.txt +++ b/SharpTS.Test262/baselines/compiled.txt @@ -10516,12 +10516,12 @@ test/built-ins/String/prototype/replace/S15.5.4.11_A1_T15.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A1_T16.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A1_T2.js Fail -test/built-ins/String/prototype/replace/S15.5.4.11_A1_T4.js Fail +test/built-ins/String/prototype/replace/S15.5.4.11_A1_T4.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A1_T5.js Fail test/built-ins/String/prototype/replace/S15.5.4.11_A1_T6.js Fail test/built-ins/String/prototype/replace/S15.5.4.11_A1_T7.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A1_T8.js Pass -test/built-ins/String/prototype/replace/S15.5.4.11_A1_T9.js Fail +test/built-ins/String/prototype/replace/S15.5.4.11_A1_T9.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js Pass @@ -10535,16 +10535,16 @@ test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js Pass -test/built-ins/String/prototype/replace/S15.5.4.11_A4_T1.js Fail -test/built-ins/String/prototype/replace/S15.5.4.11_A4_T2.js Fail -test/built-ins/String/prototype/replace/S15.5.4.11_A4_T3.js Fail -test/built-ins/String/prototype/replace/S15.5.4.11_A4_T4.js Fail +test/built-ins/String/prototype/replace/S15.5.4.11_A4_T1.js Pass +test/built-ins/String/prototype/replace/S15.5.4.11_A4_T2.js Pass +test/built-ins/String/prototype/replace/S15.5.4.11_A4_T3.js Pass +test/built-ins/String/prototype/replace/S15.5.4.11_A4_T4.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A6.js Pass test/built-ins/String/prototype/replace/S15.5.4.11_A7.js Pass test/built-ins/String/prototype/replace/cstm-replace-get-err.js Fail test/built-ins/String/prototype/replace/cstm-replace-invocation.js Fail -test/built-ins/String/prototype/replace/cstm-replace-is-null.js Fail +test/built-ins/String/prototype/replace/cstm-replace-is-null.js Pass test/built-ins/String/prototype/replace/cstm-replace-on-bigint-primitive.js RuntimeError test/built-ins/String/prototype/replace/cstm-replace-on-boolean-primitive.js Pass test/built-ins/String/prototype/replace/cstm-replace-on-number-primitive.js Pass