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
63 changes: 63 additions & 0 deletions Compilation/AsyncArrowMoveNextEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ public AsyncArrowMoveNextEmitter(
/// Emits the complete MoveNext method body.
/// </summary>
public void EmitMoveNext(List<Stmt> body, CompilationContext ctx, Type returnType)
=> EmitMoveNext(body, ctx, returnType, null);

public void EmitMoveNext(List<Stmt> body, CompilationContext ctx, Type returnType, List<Stmt.Parameter>? parameters)
{
// Note: _il is initialized in constructor via GetILGenerator()
_ctx = ctx;
Expand Down Expand Up @@ -155,6 +158,16 @@ public void EmitMoveNext(List<Stmt> body, CompilationContext ctx, Type returnTyp
// State dispatch switch
EmitStateDispatch();

// Apply parameter defaults on initial entry (#646). Placed after the state-dispatch
// switch so a resume (state >= 0) jumps straight to its await label past this code; on
// initial entry (state -1) the switch falls through to here. Arrow parameter fields are
// always object-typed (AsyncArrowStateMachineBuilder), so the null / $Undefined check
// is valid — unlike the sync-arrow path, no slot widening is needed.
if (parameters != null)
{
EmitDefaultParameters(parameters);
}

// Emit the body
foreach (var stmt in body)
{
Expand Down Expand Up @@ -196,6 +209,56 @@ private void EmitStateDispatch()
_il.MarkLabel(defaultLabel);
}

/// <summary>
/// Applies parameter defaults at async-arrow entry (#646). For each parameter with a default
/// value, if its hoisted state-machine field holds null or the <c>$Undefined</c> sentinel, the
/// default expression is evaluated and stored. Mirrors the async-function path
/// (<see cref="AsyncMoveNextEmitter"/>); parameter fields are object-typed so the null /
/// <c>$Undefined</c> check is sound. Invoked only on initial entry (see EmitMoveNext placement),
/// so no defaults-applied guard field is required.
/// </summary>
private void EmitDefaultParameters(List<Stmt.Parameter> parameters)
{
foreach (var param in parameters)
{
if (param.DefaultValue == null) continue;

var field = _builder.GetVariableField(param.Name.Lexeme);
if (field == null) continue; // parameter not hoisted

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

// Load the parameter field value.
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldfld, field);

// null -> apply default; otherwise test for the $Undefined sentinel.
_il.Emit(OpCodes.Dup);
_il.Emit(OpCodes.Brtrue, checkUndefined);
_il.Emit(OpCodes.Pop);
_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);

// field = <default>
_il.MarkLabel(applyDefault);
EmitExpression(param.DefaultValue);
EnsureBoxed();
var temp = _il.DeclareLocal(typeof(object));
_il.Emit(OpCodes.Stloc, temp);
_il.Emit(OpCodes.Ldarg_0);
_il.Emit(OpCodes.Ldloc, temp);
_il.Emit(OpCodes.Stfld, field);

_il.MarkLabel(skipDefault);
}
}

#region StatementEmitterBase Overrides

protected override FieldBuilder? GetHoistedVariableField(string name) => _builder.GetVariableField(name);
Expand Down
9 changes: 9 additions & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,15 @@ public class EmittedRuntime
public TypeBuilder CapturesArgumentsAttrType { get; set; } = null!;
public ConstructorBuilder CapturesArgumentsAttrCtor { get; set; } = null!;

// Marker attribute applied to USER TypeScript function methods (declarations,
// arrows/function expressions, methods, async stubs). When the wrapped method
// carries it, $TSFunction.AdjustArgs pads omitted trailing arguments with the
// `undefined` sentinel ($Undefined.Instance) instead of CLR null, matching JS
// semantics and the direct-call path. Runtime built-ins stay unmarked and keep
// null padding (their bodies use null-checks for optional-arg absence). (#640)
public TypeBuilder PadUndefinedAttrType { get; set; } = null!;
public ConstructorBuilder PadUndefinedAttrCtor { get; set; } = null!;

// String methods
public MethodBuilder StringCharAt { get; set; } = null!;
public MethodBuilder StringSubstring { get; set; } = null!;
Expand Down
9 changes: 9 additions & 0 deletions Compilation/ILCompiler.ArrowFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ private void FinalizeArrowFunctionCollection()
var resolvedParamTypes = ParameterTypeResolver.ResolveParameters(
arrow.Parameters, _typeMapper, null, _typeMap); // null funcType - use annotations only

// Arrows/function expressions get no OverloadGenerator forwarding, so a parameter
// default is applied by the runtime entry prologue, which needs an object slot to
// detect the missing/undefined argument (#646).
ParameterTypeResolver.WidenDefaultedParamsToObject(resolvedParamTypes, arrow.Parameters, _types.Object);

// Store resolved types for use during arrow body emission
_closures.ArrowParameterTypes[arrow] = resolvedParamTypes;

Expand Down Expand Up @@ -403,6 +408,10 @@ private void FinalizeArrowFunctionCollection()
_closures.DisplayClasses[arrow] = displayClass;
_closures.ArrowMethods[arrow] = invokeMethod;
}

// User arrow / function-expression body: when invoked as a value, omitted trailing
// args must pad with the `undefined` sentinel (JS semantics), not CLR null. (#640)
MarkPadsUndefined(_closures.ArrowMethods[arrow]);
}
}

Expand Down
9 changes: 6 additions & 3 deletions Compilation/ILCompiler.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ private void DefineAsyncArrowStateMachines(

// Define the stub method that will be called to invoke the async arrow
arrowBuilder.DefineStubMethod(_programType);
MarkPadsUndefined(arrowBuilder.StubMethod); // #640

_async.ArrowBuilders[arrowInfo.Arrow] = arrowBuilder;
continue; // Already handled the full setup
Expand All @@ -185,6 +186,7 @@ private void DefineAsyncArrowStateMachines(

// Define the stub method that will be called to invoke the async arrow
arrowBuilder.DefineStubMethod(_programType);
MarkPadsUndefined(arrowBuilder.StubMethod); // #640

_async.ArrowBuilders[arrowInfo.Arrow] = arrowBuilder;
}
Expand Down Expand Up @@ -656,7 +658,7 @@ private void EmitAsyncArrowMoveNext(AsyncArrowStateMachineBuilder arrowBuilder,
bodyStatements = [];
}

arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object, arrow.Parameters);
ILLabelValidator.Validate(arrowBuilder.MoveNextMethod.GetILGenerator(),
$"async arrow MoveNext");
}
Expand Down Expand Up @@ -956,7 +958,7 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth
false, // UsesThis
[] // AsyncArrows - handled separately via _async.ArrowBuilders
), _types);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object, arrow.Parameters);
ILLabelValidator.Validate(arrowBuilder.MoveNextMethod.GetILGenerator(),
$"async arrow MoveNext in {methodBuilder.DeclaringType?.Name}::{method.Name.Lexeme}");
}
Expand Down Expand Up @@ -1009,6 +1011,7 @@ private void DefineTopLevelAsyncArrows()

// Define the stub method
arrowBuilder.DefineStubMethod(_programType);
MarkPadsUndefined(arrowBuilder.StubMethod); // #640

// Store the builder
_async.ArrowBuilders[arrow] = arrowBuilder;
Expand Down Expand Up @@ -1130,7 +1133,7 @@ private void EmitTopLevelAsyncArrowBodies()
bodyStatements = [];
}

arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object, arrow.Parameters);
ILLabelValidator.Validate(arrowBuilder.MoveNextMethod.GetILGenerator(),
$"standalone async arrow MoveNext");

Expand Down
2 changes: 1 addition & 1 deletion Compilation/ILCompiler.Classes.Static.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ private void EmitStaticAsyncMethodBody(string className, Stmt.Function method)
false, // UsesThis
[] // AsyncArrows - handled separately via _async.ArrowBuilders
), _types);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object);
arrowEmitter.EmitMoveNext(bodyStatements, ctx, _types.Object, arrow.Parameters);
}
}

Expand Down
18 changes: 18 additions & 0 deletions Compilation/ILCompiler.Functions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ private void DefineFunction(Stmt.Function funcStmt)

_functions.Builders[qualifiedFunctionName] = methodBuilder;

// User TS function: when invoked as a value, omitted trailing args must pad with the
// `undefined` sentinel (JS semantics), not CLR null. (#640)
MarkPadsUndefined(methodBuilder);

// Flag eagerly (phase 3) so direct-call sites emitted in phase 7 can publish
// caller args to the thread-static before OpCodes.Call. Uses the same scanner
// the prologue consults, keeping the two sides in sync. Overload signatures
Expand Down Expand Up @@ -1412,6 +1416,20 @@ private void EmitFunctionOverloads(Stmt.Function funcStmt)
}
}

/// <summary>
/// Marks a user TypeScript function method with the <c>$PadUndefined</c> attribute so that
/// <c>$TSFunction.AdjustArgs</c> pads omitted trailing arguments with the <c>undefined</c>
/// sentinel (JS semantics) when the function is invoked as a value (cross-module imports,
/// callbacks, <c>$TSFunction.Invoke</c>) — matching the direct-call path. Runtime built-ins
/// stay unmarked and keep CLR-null padding. No-op when the runtime attribute is unavailable. (#640)
/// </summary>
internal void MarkPadsUndefined(MethodBuilder method)
{
if (_runtime?.PadUndefinedAttrCtor != null)
method.SetCustomAttribute(
new System.Reflection.Emit.CustomAttributeBuilder(_runtime.PadUndefinedAttrCtor, []));
}

/// <summary>
/// Gets the index of the first parameter with a default value.
/// Returns -1 if no default parameters exist.
Expand Down
4 changes: 4 additions & 0 deletions Compilation/ILCompiler.InnerFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ private void DefineInnerFunctions()
_innerFunctionDisplayClasses[func] = displayClass;
_innerFunctionMethods[func] = invokeMethod;
}

// User inner-function body: when invoked as a value, omitted trailing args must
// pad with the `undefined` sentinel (JS semantics), not CLR null. (#640)
MarkPadsUndefined(_innerFunctionMethods[func]);
}
}

Expand Down
22 changes: 22 additions & 0 deletions Compilation/ParameterTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ public static Type[] ResolveParameters(
.ToArray();
}

/// <summary>
/// Widens any parameter that has a <b>default value</b> to an <c>object</c> slot. Required for
/// function kinds that apply parameter defaults via the runtime entry prologue
/// (<see cref="ILEmitter.EmitDefaultParameters"/>) rather than via <see cref="OverloadGenerator"/>
/// lower-arity forwarding — i.e. arrow functions and function expressions, which get no overloads.
/// The prologue detects a missing/undefined argument by comparing the slot against null and the
/// <c>$Undefined</c> sentinel, and value-call padding (<c>$TSFunction.AdjustArgs</c>) fills omitted
/// slots with that sentinel. Only an <c>object</c> slot can hold it: a value-type slot can never be
/// null (and emits invalid <c>ldarg; brfalse</c> IL), and a typed reference slot (e.g. <c>string</c>)
/// coerces the sentinel to a real value (e.g. the string <c>"undefined"</c>) before the prologue can
/// observe it. Either way the default could never fire. Mirrors the optional-without-default widening
/// already applied in <see cref="ResolveParameters"/>. (#646)
/// </summary>
public static void WidenDefaultedParamsToObject(Type[] resolved, List<Stmt.Parameter> parameters, Type objectType)
{
for (int i = 0; i < parameters.Count && i < resolved.Length; i++)
{
if (parameters[i].DefaultValue != null && resolved[i] != objectType)
resolved[i] = objectType;
}
}

/// <summary>
/// #372: widen a <c>number</c>/<c>boolean</c> parameter slot (unboxed <c>double</c>/<c>bool</c>) to
/// <c>object</c> when the type checker flagged it as possibly holding the runtime <c>undefined</c>
Expand Down
Loading
Loading