Skip to content

Commit 3dc923b

Browse files
authored
Fixes async expression compiler bugs and updates nugets (#133)
* Fix value-type boxing in Transition (#126), add failing test for #123, expand ExpressionRuntimeOptions - Fix #126: Add EnsureConvert helper to Transition base class that wraps value-type expressions in Convert() when assigning to reference-type variables (boxing). Applied in Transition.SetResult and FinalTransition.AddExpressions. - Add failing test for #123: IfThen with Return(label, value) in BlockAsync returns null instead of expected value. Root cause is in the lowering phase where Returns inside non-lowered conditionals are not converted to _finalResultVariable assignments. - Expand ExpressionRuntimeOptions with Optimize flag (default true) to conditionally skip StateOptimizer, and SourceHandler callback to capture the generated state machine expression for debugging. * Change SourceHandler to Action<string> with UnsafeAccessor for DebugView, add tests * fix(expressions): Ensure Return gotos inside non-lowered expressions set final result * test(expressions): Add regression test for Return labels in async TryCatch * test(compiler): Add FastExpressionCompiler compatibility test for Return gotos with assignments * Update nugets and cleanup
1 parent a139db8 commit 3dc923b

18 files changed

Lines changed: 315 additions & 28 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,7 @@ FodyWeavers.xsd
395395
*.msp
396396

397397
# JetBrains Rider
398-
*.sln.iml
398+
*.sln.iml
399+
400+
# Claude Code
401+
.claude/

Directory.Packages.props

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@
55
<ItemGroup>
66
<!-- Development Tools -->
77
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
8-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.102">
8+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103">
99
<PrivateAssets>all</PrivateAssets>
1010
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1111
</PackageVersion>
1212
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
1313
<PackageVersion Include="DotNext.Metaprogramming" Version="5.26.2" />
1414
<PackageVersion Include="FastExpressionCompiler" Version="5.3.0" />
15-
<PackageVersion Include="coverlet.collector" Version="6.0.4">
15+
<PackageVersion Include="coverlet.collector" Version="8.0.0">
1616
<PrivateAssets>all</PrivateAssets>
1717
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1818
</PackageVersion>
19-
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
20-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
21-
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
22-
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
19+
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
20+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
21+
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
22+
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
2323
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
24-
<PackageVersion Include="MSTest.TestAdapter" Version="4.0.2" />
25-
<PackageVersion Include="MSTest.TestFramework" Version="4.0.2" />
26-
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
24+
<PackageVersion Include="MSTest.TestAdapter" Version="4.1.0" />
25+
<PackageVersion Include="MSTest.TestFramework" Version="4.1.0" />
26+
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
2727
<!-- Roslyn: use consistent version for all frameworks -->
2828
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
2929
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />

src/Hyperbee.Expressions.Lab/Hyperbee.Expressions.Lab.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@
4848
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
4949
<PackageReference Include="Microsoft.Extensions.Http" />
5050
<ProjectReference Include="..\Hyperbee.Expressions\Hyperbee.Expressions.csproj" />
51-
<PackageReference Update="Microsoft.SourceLink.GitHub" />
51+
<PackageReference Update="Microsoft.SourceLink.GitHub">
52+
<PrivateAssets>all</PrivateAssets>
53+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
54+
</PackageReference>
5255
</ItemGroup>
5356

5457
</Project>

src/Hyperbee.Expressions/AsyncBlockExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private AsyncLoweringInfo LoweringTransformer()
5858
{
5959
try
6060
{
61-
var visitor = new AsyncLoweringVisitor();
61+
var visitor = new AsyncLoweringVisitor { Optimize = RuntimeOptions?.Optimize ?? true };
6262

6363
return visitor.Transform(
6464
Result.Type,

src/Hyperbee.Expressions/CompilerServices/AsyncStateMachineBuilder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ internal static Expression Create<TResult>( AsyncLoweringTransformer loweringTra
415415
var stateMachineBuilder = new AsyncStateMachineBuilder<TResult>( moduleBuilder, typeName );
416416
var stateMachineExpression = stateMachineBuilder.CreateStateMachine( loweringTransformer, __id );
417417

418+
if ( options.SourceHandler != null )
419+
{
420+
var debugView = GetDebugView( stateMachineExpression );
421+
options.SourceHandler( debugView );
422+
}
423+
418424
return stateMachineExpression; // the-best expression breakpoint ever
419425
}
426+
427+
[UnsafeAccessor( UnsafeAccessorKind.Method, Name = "get_DebugView" )]
428+
private static extern string GetDebugView( Expression expression );
420429
}

src/Hyperbee.Expressions/CompilerServices/Lowering/AsyncLoweringVisitor.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ internal class AsyncLoweringVisitor : BaseLoweringVisitor<AsyncLoweringInfo>
1212

1313
private int _awaitCount;
1414

15+
public bool Optimize { get; init; } = true;
16+
1517
public override AsyncLoweringInfo Transform(
1618
Type resultType,
1719
ParameterExpression[] localVariables,
@@ -27,7 +29,8 @@ public override AsyncLoweringInfo Transform(
2729

2830
VisitExpressions( expressions );
2931

30-
StateOptimizer.Optimize( States );
32+
if ( Optimize )
33+
StateOptimizer.Optimize( States );
3134

3235
ThrowIfInvalid();
3336

src/Hyperbee.Expressions/CompilerServices/Transitions/FinalTransition.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public override void AddExpressions( List<Expression> expressions, StateMachineC
2424
if ( expressions.Count <= 1 || expressions[^1].Type == typeof( void ) )
2525
{
2626
value ??= Constant( null, typeof( IVoidResult ) );
27-
expressions.Add( Assign( variable, value ) );
27+
expressions.Add( Assign( variable, EnsureConvert( value, variable.Type ) ) );
2828
return;
2929
}
3030

@@ -34,7 +34,7 @@ public override void AddExpressions( List<Expression> expressions, StateMachineC
3434

3535
if ( variable.Type.IsAssignableFrom( lastExpression.Type ) )
3636
{
37-
expressions[^1] = Assign( variable, lastExpression );
37+
expressions[^1] = Assign( variable, EnsureConvert( lastExpression, variable.Type ) );
3838
}
3939
}
4040
}

src/Hyperbee.Expressions/CompilerServices/Transitions/Transition.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,26 @@ private static void SetResult( List<Expression> expressions, StateMachineContext
3030

3131
if ( variable.Type.IsAssignableFrom( lastExpression.Type ) )
3232
{
33-
expressions[^1] = Assign( variable, lastExpression );
33+
expressions[^1] = Assign( variable, EnsureConvert( lastExpression, variable.Type ) );
3434
}
3535

3636
return;
3737
}
3838

3939
if ( value != null && variable.Type.IsAssignableFrom( value.Type ) )
4040
{
41-
expressions.Add( Assign( variable, value ) );
41+
expressions.Add( Assign( variable, EnsureConvert( value, variable.Type ) ) );
4242
}
4343
}
4444

45+
protected static Expression EnsureConvert( Expression expression, Type targetType )
46+
{
47+
if ( expression.Type != targetType && expression.Type.IsValueType )
48+
return Convert( expression, targetType );
49+
50+
return expression;
51+
}
52+
4553
internal abstract void Optimize( HashSet<LabelTarget> references );
4654

4755
protected static StateNode OptimizeGotos( StateNode node )

src/Hyperbee.Expressions/CompilerServices/VariableResolver.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ private static class VariableName
3131
private const int InitialCapacity = 8;
3232

3333
private int _variableId;
34+
private ParameterExpression _finalResultVariable;
3435

3536
private readonly StateContext _states;
3637

@@ -100,7 +101,8 @@ public Expression GetExceptionVariable( int stateId )
100101
[MethodImpl( MethodImplOptions.AggressiveInlining )]
101102
public ParameterExpression GetFinalResult( Type type )
102103
{
103-
return AddVariable( Variable( type, VariableName.FinalResult ) );
104+
_finalResultVariable = AddVariable( Variable( type, VariableName.FinalResult ) );
105+
return _finalResultVariable;
104106
}
105107

106108
// Resolver
@@ -150,6 +152,14 @@ protected override Expression VisitGoto( GotoExpression node )
150152
if ( _labels.TryGetValue( node.Target, out var label ) )
151153
return label;
152154

155+
// BUG FIX: Return gotos inside non-lowered expressions (e.g. IfThen without Await)
156+
// must assign _finalResultVariable so the state machine result is set.
157+
if ( _finalResultVariable != null && node.Kind == GotoExpressionKind.Return && node.Value != null )
158+
{
159+
var visitedValue = Visit( node.Value );
160+
return node.Update( node.Target, Assign( _finalResultVariable, visitedValue ) );
161+
}
162+
153163
return base.VisitGoto( node );
154164
}
155165

src/Hyperbee.Expressions/ExpressionRuntimeOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,17 @@ public class ExpressionRuntimeOptions
1010
/// Defaults to <see cref="DefaultModuleBuilderProvider"/>
1111
/// </summary>
1212
public IModuleBuilderProvider ModuleBuilderProvider { get; init; } = DefaultModuleBuilderProvider.Instance;
13+
14+
/// <summary>
15+
/// Gets or sets whether state machine optimizations are enabled.
16+
/// When false, the goto optimizer is skipped, preserving the raw lowered state graph.
17+
/// Defaults to true.
18+
/// </summary>
19+
public bool Optimize { get; init; } = true;
20+
21+
/// <summary>
22+
/// Gets or sets an optional callback to receive the generated state machine source.
23+
/// When set, the state machine expression debug view is passed as a string for inspection.
24+
/// </summary>
25+
public Action<string> SourceHandler { get; init; }
1326
}

0 commit comments

Comments
 (0)