Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down Expand Up @@ -553,6 +564,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<object> — 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!;
Expand Down
3 changes: 2 additions & 1 deletion Compilation/ExpressionEmitterBase.CallHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1270,8 +1270,9 @@ protected void EmitAmbiguousMethodCall(Expr obj, string methodName, List<Expr> 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":
Expand Down
9 changes: 9 additions & 0 deletions Compilation/ILCompiler.Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion Compilation/ILEmitter.Calls.AmbiguousDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
29 changes: 29 additions & 0 deletions Compilation/ILEmitter.Calls.MethodDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,35 @@ private void EmitMethodCall(Expr.Get methodGet, List<Expr> arguments)
return;
}

// Iterator protocol (.next() / .return()) for any/unknown receivers.
// Array .values()/.keys()/.entries() return a bare IEnumerator<object>
// 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

Expand Down
10 changes: 10 additions & 0 deletions Compilation/RuntimeEmitter.Arrays.Iterators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
49 changes: 39 additions & 10 deletions Compilation/RuntimeEmitter.Arrays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Compilation/RuntimeEmitter.Objects.Index.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@

il.MarkLabel(doArrayGetLabel);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, runtime.TSArrayType);

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.

Check warning on line 387 in Compilation/RuntimeEmitter.Objects.Index.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Possible null reference argument for parameter 'cls' in 'void ILGenerator.Emit(OpCode opcode, Type cls)'.
il.Emit(OpCodes.Ldloc, tsArrayGetIdx);
il.Emit(OpCodes.Callvirt, runtime.TSArrayGetLong);
il.Emit(OpCodes.Ret);
Expand Down Expand Up @@ -459,6 +459,21 @@
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<object> 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);
}

Expand Down
Loading
Loading