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
59 changes: 58 additions & 1 deletion Compilation/AsyncGeneratorMoveNextEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public AsyncGeneratorMoveNextEmitter(AsyncGeneratorStateMachineBuilder builder,
/// <summary>
/// Emits the complete MoveNextAsync method body.
/// </summary>
public void EmitMoveNextAsync(List<Stmt>? body, CompilationContext ctx)
public void EmitMoveNextAsync(List<Stmt>? body, CompilationContext ctx, List<Stmt.Parameter>? parameters = null)
{
if (body == null)
{
Expand Down Expand Up @@ -126,6 +126,13 @@ public void EmitMoveNextAsync(List<Stmt>? body, CompilationContext ctx)
// Emit state dispatch switch
EmitStateSwitch();

// Apply parameter defaults on initial entry only (state -1 falls through here; resume
// states jump past via the switch, the completed state short-circuits above). (#737)
if (parameters != null)
{
EmitDefaultParameters(parameters);
}

// Emit the function body (will emit yield/await points inline)
foreach (var stmt in body)
{
Expand Down Expand Up @@ -169,6 +176,56 @@ public void EmitMoveNextAsync(List<Stmt>? body, CompilationContext ctx)
EmitReturnValueTaskBool(false);
}

/// <summary>
/// Applies parameter defaults at async-generator entry — the async-generator analogue of the sync
/// generator and async function versions (this state machine previously ran no default prologue, so
/// defaults never fired in compiled mode). A defaulted parameter whose argument was omitted or
/// explicitly <c>undefined</c> arrives in its hoisted field as null / the <c>$Undefined</c> sentinel
/// and is replaced with the evaluated default. Evaluated in declaration order so a later default may
/// reference an earlier (already-defaulted) parameter. (#737)
/// </summary>
private void EmitDefaultParameters(List<Stmt.Parameter> parameters)
{
if (!parameters.Any(p => p.DefaultValue != null))
return;

foreach (var param in parameters)
{
if (param.DefaultValue == null) continue;

var field = _builder.GetVariableField(param.Name.Lexeme);
if (field == null) continue; // Parameter not hoisted (unused in body) — default is moot.

var applyDefault = _il.DefineLabel();
var checkUndefined = _il.DefineLabel();
var skipDefault = _il.DefineLabel();

// if (field == null) apply; else if (field is $Undefined) apply; else keep.
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldfld, field);
_il.Emit(OpCodes.Dup);
_il.Emit(OpCodes.Brtrue, checkUndefined);
_il.Emit(OpCodes.Pop); // pop the null
_il.Emit(OpCodes.Br, applyDefault);

_il.MarkLabel(checkUndefined);
_il.Emit(OpCodes.Isinst, _ctx!.Runtime!.UndefinedType);
_il.Emit(OpCodes.Brtrue, applyDefault);
_il.Emit(OpCodes.Br, skipDefault);

_il.MarkLabel(applyDefault);
EmitExpression(param.DefaultValue);
EnsureBoxed();
var temp = _il.DeclareLocal(_types.Object);
_il.Emit(OpCodes.Stloc, temp);
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldloc, temp);
_il.Emit(OpCodes.Stfld, field);

_il.MarkLabel(skipDefault);
}
}

private void EmitStateSwitch()
{
if (_analysis.SuspensionPointCount == 0) return;
Expand Down
2 changes: 1 addition & 1 deletion Compilation/CallHandlers/AsyncFunctionCallHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call)
}

for (int i = call.Arguments.Count; i < paramCount; i++)
emitter.EmitDefaultForType(asyncMethodParams[i].ParameterType);
emitter.EmitOmittedArgument(asyncMethodParams[i].ParameterType);

il.Emit(OpCodes.Call, asyncMethod);

Expand Down
2 changes: 1 addition & 1 deletion Compilation/CallHandlers/ClassExprStaticHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call)
}

for (int i = call.Arguments.Count; i < paramCount; i++)
emitter.EmitDefaultForType(methodParams[i].ParameterType);
emitter.EmitOmittedArgument(methodParams[i].ParameterType);

il.Emit(OpCodes.Call, exprStaticMethod);
emitter.SetStackUnknown();
Expand Down
2 changes: 1 addition & 1 deletion Compilation/CallHandlers/ImportedClassStaticHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call)
}

for (int i = call.Arguments.Count; i < paramCount; i++)
emitter.EmitDefaultForType(methodParams[i].ParameterType);
emitter.EmitOmittedArgument(methodParams[i].ParameterType);

il.Emit(OpCodes.Call, callableMethod);
emitter.SetStackUnknown();
Expand Down
2 changes: 1 addition & 1 deletion Compilation/CallHandlers/SuperConstructorHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private static void EmitSuperCtorCall(IEmitterContext emitter, ConstructorBuilde
}

for (int i = arguments.Count; i < ctorParams.Length; i++)
emitter.EmitDefaultForType(ctorParams[i].ParameterType);
emitter.EmitOmittedArgument(ctorParams[i].ParameterType);

System.Reflection.ConstructorInfo ctorToCall = parentCtor;
Type? baseType = ctx.CurrentClassBuilder?.BaseType;
Expand Down
2 changes: 1 addition & 1 deletion Compilation/CallHandlers/ThisStaticContextHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call)
}

for (int i = call.Arguments.Count; i < paramCount; i++)
emitter.EmitDefaultForType(methodParams[i].ParameterType);
emitter.EmitOmittedArgument(methodParams[i].ParameterType);

il.Emit(OpCodes.Call, thisStaticMethod);
emitter.SetStackUnknown();
Expand Down
7 changes: 7 additions & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ public class EmittedRuntime
public TypeBuilder PadUndefinedAttrType { get; set; } = null!;
public ConstructorBuilder PadUndefinedAttrCtor { get; set; } = null!;

// Marker attribute applied to a user function-expression / `this`-bearing arrow method whose
// first emitted parameter is the synthetic `__this` receiver slot. $TSFunction reads it back via
// IsDefined so it can detect that slot without the parameter name (a --ref-asm rewrite strips
// parameter names, breaking the name-based check and shifting value-call arguments). (#738)
public TypeBuilder ExpectsThisAttrType { get; set; } = null!;
public ConstructorBuilder ExpectsThisAttrCtor { get; set; } = null!;

// String methods
public MethodBuilder StringCharAt { get; set; } = null!;
public MethodBuilder StringSubstring { get; set; } = null!;
Expand Down
8 changes: 8 additions & 0 deletions Compilation/Emitters/IEmitterContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ public interface IEmitterContext
/// </summary>
void EmitDefaultForType(Type type);

/// <summary>
/// Emits the value for a trailing parameter slot the call site omits: the <c>$Undefined</c>
/// sentinel for an <c>object</c> slot (JS: an omitted argument is <c>undefined</c>), else the
/// type's CLR default. Use this instead of <see cref="EmitDefaultForType"/> when padding omitted
/// call arguments. (#739/#705)
/// </summary>
void EmitOmittedArgument(Type slotType);

/// <summary>
/// Emits IL that constructs a delegate of the given type pointing at the
/// arrow's compiled body. For non-capturing arrows: <c>new Func(null, ldftn staticMethod)</c>.
Expand Down
35 changes: 28 additions & 7 deletions Compilation/ExpressionEmitterBase.CallHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ promisesGet.Object is Expr.Variable promisesModuleVar &&
}

for (int i = c.Arguments.Count; i < paramCount; i++)
EmitDefaultForType(staticMethodParams[i].ParameterType);
EmitOmittedArgument(staticMethodParams[i].ParameterType);

IL.Emit(OpCodes.Call, callableMethod);
SetStackUnknown();
Expand Down Expand Up @@ -1525,7 +1525,7 @@ protected bool TryEmitDirectMethodCall(Expr receiver, string methodName, List<Ex

for (int i = arguments.Count; i < expectedParamCount; i++)
{
EmitDefaultForType(methodParams[i].ParameterType);
EmitOmittedArgument(methodParams[i].ParameterType);
}

IL.Emit(OpCodes.Callvirt, callTarget);
Expand Down Expand Up @@ -1608,7 +1608,7 @@ protected virtual bool TryEmitSuperMethodCall(string methodName, List<Expr> argu

for (int i = arguments.Count; i < methodParams.Length; i++)
{
EmitDefaultForType(methodParams[i].ParameterType);
EmitOmittedArgument(methodParams[i].ParameterType);
}

IL.Emit(OpCodes.Call, superTarget);
Expand Down Expand Up @@ -1802,7 +1802,7 @@ protected bool TryEmitDerivedPromiseStatic(string resolvedClassName, Type classB
}

for (int i = 1; i < ctorParams.Length; i++)
EmitDefaultForType(ctorParams[i].ParameterType);
EmitOmittedArgument(ctorParams[i].ParameterType);

IL.Emit(OpCodes.Newobj, ctorToCall);
SetStackUnknown();
Expand Down Expand Up @@ -1983,6 +1983,27 @@ protected void EmitDefaultForType(Type type)
else { IL.Emit(OpCodes.Ldnull); }
}

/// <summary>
/// Emits the value for a trailing parameter slot that the call site omits. JS semantics: an
/// omitted argument is <c>undefined</c>, so an <c>object</c> slot — which every optional or
/// value-type-defaulted parameter uses after widening (#705/#739) — is padded with the
/// <c>$Undefined</c> sentinel. That is observable through <c>typeof</c>/<c>=== undefined</c> for a
/// plain optional (#739) and fires the default-parameter prologue for a defaulted one
/// (#705/#723/#737). A non-<c>object</c> slot (a required value-type slot, or a not-widened
/// reference-typed default such as <c>string</c>) takes its CLR default — for a defaulted
/// reference slot the prologue still fires on the resulting null. Mirrors the existing
/// $TSFunction.Invoke value-call padding and <see cref="EmitPrivateCallUndefinedPadding"/>.
/// Public (unlike the sibling <see cref="EmitDefaultForType"/>) so the ILCompiler can pad an
/// implicit base-constructor call directly; also satisfies <see cref="IEmitterContext"/>.
/// </summary>
public void EmitOmittedArgument(Type slotType)
{
if (slotType == Types.Object)
EmitUndefinedConstant();
else
EmitDefaultForType(slotType);
}

/// <summary>
/// Emits conversion from the current stack value to the target parameter type.
/// Handles boxing for object, unboxing for value types, union types, and pass-through for matching types.
Expand Down Expand Up @@ -2119,7 +2140,7 @@ protected void EmitSuperConstructorCall(ConstructorBuilder parentCtor, List<Expr
}

for (int i = arguments.Count; i < ctorParams.Length; i++)
EmitDefaultForType(ctorParams[i].ParameterType);
EmitOmittedArgument(ctorParams[i].ParameterType);

ConstructorInfo ctorToCall = parentCtor;
Type? baseType = Ctx.CurrentClassBuilder?.BaseType;
Expand Down Expand Up @@ -2149,7 +2170,7 @@ protected void EmitAsyncFunctionCall(System.Reflection.MethodInfo asyncMethod, L
}

for (int i = arguments.Count; i < paramCount; i++)
EmitDefaultForType(asyncMethodParams[i].ParameterType);
EmitOmittedArgument(asyncMethodParams[i].ParameterType);

IL.Emit(OpCodes.Call, asyncMethod);

Expand Down Expand Up @@ -2215,7 +2236,7 @@ protected void EmitInnerFunctionDirectCall(string funcName, MethodBuilder innerM
}

for (int i = arguments.Count; i < parameters.Length; i++)
EmitDefaultForType(parameters[i].ParameterType);
EmitOmittedArgument(parameters[i].ParameterType);

IL.Emit(isCapturing ? OpCodes.Callvirt : OpCodes.Call, innerMethod);
SetStackUnknown();
Expand Down
6 changes: 3 additions & 3 deletions Compilation/ExpressionEmitterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,10 +1398,10 @@ protected virtual void EmitUserClassNew(List<string> namespaceParts, string clas
}
}

// Pad missing optional arguments
// Pad omitted trailing arguments (object slot → `undefined` sentinel). (#739/#705)
for (int i = n.Arguments.Count; i < expectedParamCount; i++)
{
EmitDefaultForType(ctorParams[i].ParameterType);
EmitOmittedArgument(ctorParams[i].ParameterType);
}

IL.Emit(OpCodes.Newobj, targetCtor);
Expand Down Expand Up @@ -1512,7 +1512,7 @@ private void EmitClassExprConstruction(ConstructorBuilder classExprCtor, Expr.Ne
}

for (int i = n.Arguments.Count; i < expectedParamCount; i++)
EmitDefaultForType(ctorParams[i].ParameterType);
EmitOmittedArgument(ctorParams[i].ParameterType);

IL.Emit(OpCodes.Newobj, classExprCtor);
SetStackUnknown();
Expand Down
65 changes: 64 additions & 1 deletion Compilation/GeneratorMoveNextEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ public GeneratorMoveNextEmitter(GeneratorStateMachineBuilder builder, GeneratorS
/// <summary>
/// Emits the complete MoveNext method body.
/// </summary>
public void EmitMoveNext(List<Stmt>? body, CompilationContext ctx)
/// <param name="parameters">The generator's declared parameters. When supplied, a default-parameter
/// prologue runs on initial entry so an omitted or explicit-<c>undefined</c> argument fires its
/// default (#737). Null skips it (callers with no params).</param>
public void EmitMoveNext(List<Stmt>? body, CompilationContext ctx, List<Stmt.Parameter>? parameters = null)
{
if (body == null) return;

Expand Down Expand Up @@ -101,6 +104,15 @@ public void EmitMoveNext(List<Stmt>? body, CompilationContext ctx)
// Emit state dispatch switch
EmitStateSwitch();

// Apply parameter defaults on initial entry. The state switch jumps every resume state
// (>= 0) to its yield label and the completed state (-2) short-circuits above, so only the
// initial entry (state -1) falls through to here — the defaults run exactly once, before the
// body, with no extra guard field needed. (#737)
if (parameters != null)
{
EmitDefaultParameters(parameters);
}

// Emit the function body (will emit yield points inline)
foreach (var stmt in body)
{
Expand Down Expand Up @@ -152,6 +164,57 @@ public void EmitMoveNext(List<Stmt>? body, CompilationContext ctx)
_il.Emit(OpCodes.Ret);
}

/// <summary>
/// Applies parameter defaults at generator entry. A defaulted parameter whose argument was
/// omitted or explicitly <c>undefined</c> arrives in its hoisted state-machine field as null /
/// the <c>$Undefined</c> sentinel; this replaces it with the evaluated default expression — the
/// generator analogue of <see cref="ILEmitter.EmitDefaultParameters"/> and the async emitter's
/// own version, both of which a generator's state machine previously lacked (defaults never
/// fired in compiled mode). Defaults are evaluated in declaration order, so a later default may
/// reference an earlier (already-defaulted) parameter via its field. (#737)
/// </summary>
private void EmitDefaultParameters(List<Stmt.Parameter> parameters)
{
if (!parameters.Any(p => p.DefaultValue != null))
return;

foreach (var param in parameters)
{
if (param.DefaultValue == null) continue;

var field = _builder.GetVariableField(param.Name.Lexeme);
if (field == null) continue; // Parameter not hoisted (unused in body) — default is moot.

var applyDefault = _il.DefineLabel();
var checkUndefined = _il.DefineLabel();
var skipDefault = _il.DefineLabel();

// if (field == null) apply; else if (field is $Undefined) apply; else keep.
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldfld, field);
_il.Emit(OpCodes.Dup);
_il.Emit(OpCodes.Brtrue, checkUndefined);
_il.Emit(OpCodes.Pop); // pop the null
_il.Emit(OpCodes.Br, applyDefault);

_il.MarkLabel(checkUndefined);
_il.Emit(OpCodes.Isinst, _ctx!.Runtime!.UndefinedType);
_il.Emit(OpCodes.Brtrue, applyDefault);
_il.Emit(OpCodes.Br, skipDefault);

_il.MarkLabel(applyDefault);
EmitExpression(param.DefaultValue);
EnsureBoxed();
var temp = _il.DeclareLocal(_types.Object);
_il.Emit(OpCodes.Stloc, temp);
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldloc, temp);
_il.Emit(OpCodes.Stfld, field);

_il.MarkLabel(skipDefault);
}
}

private void EmitStateSwitch()
{
if (_analysis.YieldPointCount == 0) return;
Expand Down
5 changes: 5 additions & 0 deletions Compilation/ILCompiler.ArrowFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ private void FinalizeArrowFunctionCollection()
methodBuilder.DefineParameter(1, ParameterAttributes.None, "__this");
for (int i = 0; i < arrow.Parameters.Count; i++)
methodBuilder.DefineParameter(i + 2, ParameterAttributes.None, arrow.Parameters[i].Name.Lexeme);
// Back the name-based __this detection with an attribute that survives a --ref-asm
// parameter-name strip, so a value-call doesn't shift its arguments. (#738)
MarkExpectsThis(methodBuilder);
}
else
{
Expand Down Expand Up @@ -395,6 +398,8 @@ private void FinalizeArrowFunctionCollection()
invokeMethod.DefineParameter(1, ParameterAttributes.None, "__this");
for (int i = 0; i < arrow.Parameters.Count; i++)
invokeMethod.DefineParameter(i + 2, ParameterAttributes.None, arrow.Parameters[i].Name.Lexeme);
// Survive a --ref-asm parameter-name strip (see the sibling site above). (#738)
MarkExpectsThis(invokeMethod);
}
else
{
Expand Down
4 changes: 2 additions & 2 deletions Compilation/ILCompiler.AsyncGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private void EmitAsyncGeneratorMoveNextAsyncBody(AsyncGeneratorStateMachineBuild

// Use the new emitter for full async generator body emission
var emitter = new AsyncGeneratorMoveNextEmitter(smBuilder, analysis, _types);
emitter.EmitMoveNextAsync(funcStmt.Body, ctx);
emitter.EmitMoveNextAsync(funcStmt.Body, ctx, funcStmt.Parameters);
}

/// <summary>
Expand Down Expand Up @@ -246,7 +246,7 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func

// Emit MoveNextAsync body
var moveNextEmitter = new AsyncGeneratorMoveNextEmitter(smBuilder, analysis, _types);
moveNextEmitter.EmitMoveNextAsync(method.Body, ctx);
moveNextEmitter.EmitMoveNextAsync(method.Body, ctx, method.Parameters);

// Finalize the state machine type
smBuilder.CreateType();
Expand Down
2 changes: 1 addition & 1 deletion Compilation/ILCompiler.Classes.ClassExpressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde
if (baseCtor != null)
{
foreach (var p in baseCtor.GetParameters())
emitter.EmitDefaultForType(p.ParameterType);
emitter.EmitOmittedArgument(p.ParameterType);
il.Emit(OpCodes.Call, baseCtor);
}
else
Expand Down
Loading
Loading