Skip to content

Commit 1ba841e

Browse files
Adds XS expression provider
Adds a new expression provider leveraging Hyperbee.XS for template processing. This allows for more complex and flexible template expressions. Also includes tests and benchmarks to validate and measure performance.
1 parent ef630c7 commit 1ba841e

9 files changed

Lines changed: 517 additions & 3 deletions

File tree

Hyperbee.Templating.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Templating.Benchma
3737
EndProject
3838
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "docs", "docs\docs.shproj", "{8409DDE0-C540-4A94-BF7F-9403888BEDAC}"
3939
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hyperbee.Templating.Provider.XS", "src\Hyperbee.Templating.Provider.XS\Hyperbee.Templating.Provider.XS.csproj", "{EA264696-D88A-288E-0D8E-C834780D5F9E}"
41+
EndProject
4042
Global
4143
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4244
Debug|Any CPU = Debug|Any CPU
@@ -55,6 +57,10 @@ Global
5557
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
5658
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
5759
{EB7D2A85-3C82-444A-84CD-D245DCF951CE}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61+
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
62+
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
63+
{EA264696-D88A-288E-0D8E-C834780D5F9E}.Release|Any CPU.Build.0 = Release|Any CPU
5864
EndGlobalSection
5965
GlobalSection(SolutionProperties) = preSolution
6066
HideSolutionNode = FALSE
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System.Collections.Concurrent;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
using System.Runtime.CompilerServices;
5+
using FastExpressionCompiler;
6+
using Hyperbee.Templating.Compiler;
7+
using Hyperbee.Templating.Text;
8+
using Hyperbee.XS;
9+
using Hyperbee.XS.Core;
10+
using Hyperbee.XS.Core.Parsers;
11+
using Parlot.Fluent;
12+
using static System.Linq.Expressions.Expression;
13+
using static Parlot.Fluent.Parsers;
14+
15+
namespace Hyperbee.Templating.Provider.XS.Compiler;
16+
17+
public sealed class XsTokenExpressionProvider : ITokenExpressionProvider
18+
{
19+
private readonly bool _fastCompile;
20+
private ConcurrentDictionary<string, TokenExpression> TokenExpressions { get; } = new();
21+
22+
public XsTokenExpressionProvider( bool fastCompile = false )
23+
{
24+
_fastCompile = fastCompile;
25+
}
26+
27+
[MethodImpl( MethodImplOptions.AggressiveInlining )]
28+
public TokenExpression GetTokenExpression( string codeExpression, MemberDictionary members )
29+
{
30+
return TokenExpressions.GetOrAdd( codeExpression, Compile( codeExpression, members, _fastCompile ) );
31+
}
32+
33+
[MethodImpl( MethodImplOptions.AggressiveInlining )]
34+
public void Reset()
35+
{
36+
TokenExpressions.Clear();
37+
}
38+
39+
private static TokenExpression Compile( ReadOnlySpan<char> codeExpression, MemberDictionary members, bool fastCompile = false )
40+
{
41+
var xsParser = new XsParser( new XsConfig( TypeResolver.Create( Assembly.GetExecutingAssembly() ) )
42+
{
43+
Extensions = [new MemberDictionaryParseExtension( members )]
44+
} );
45+
46+
var start = codeExpression.IndexOf( "=>" );
47+
var argument = codeExpression[..start].Trim().ToString();
48+
var body = codeExpression[(start + 2)..].Trim().ToString();
49+
50+
var scope = new ParseScope();
51+
52+
try
53+
{
54+
scope.EnterScope( FrameType.Method );
55+
56+
var argumentParameter = Parameter( typeof(IReadOnlyMemberDictionary), argument );
57+
58+
scope.Variables.Add( argument, argumentParameter );
59+
60+
var expressionBody = xsParser.Parse( body, scope: scope ) as BlockExpression;
61+
62+
if ( expressionBody == null )
63+
throw new InvalidOperationException( $"Failed to parse expression body: {body}" );
64+
65+
var lambdaParameter = Parameter( typeof(IReadOnlyMemberDictionary) );
66+
67+
var newExpressionBody = expressionBody.Expressions.Prepend(
68+
Assign( argumentParameter, lambdaParameter )
69+
);
70+
71+
var lambda = Lambda<TokenExpression>(
72+
Convert( Block(
73+
expressionBody.Variables,
74+
newExpressionBody
75+
), typeof(object) ),
76+
lambdaParameter );
77+
78+
return fastCompile
79+
? lambda.CompileFast()
80+
: lambda.Compile();
81+
}
82+
finally
83+
{
84+
scope.ExitScope();
85+
}
86+
}
87+
88+
internal class MemberDictionaryParseExtension : IParseExtension
89+
{
90+
public ExtensionType Type => ExtensionType.Expression;
91+
public string Key => "vars";
92+
93+
private readonly MethodInfo _getValueAsMethodInfo = typeof(MemberDictionary).GetMethod( nameof(MemberDictionary.GetValueAs), [typeof(string)] )!;
94+
private readonly MethodInfo _invokeMethodInfo = typeof(MemberDictionary).GetMethod( nameof(MemberDictionary.Invoke), [typeof(string), typeof(object[])] )!;
95+
private readonly MemberDictionary _member;
96+
97+
public MemberDictionaryParseExtension( MemberDictionary member )
98+
{
99+
_member = member;
100+
}
101+
102+
public Parser<Expression> CreateParser( ExtensionBinder binder )
103+
{
104+
var (expression, _) = binder;
105+
// var v = vars::myValue;
106+
// var v = vars<bool>::myValue;
107+
// var v = vars<bool>::method( arg );
108+
109+
return ZeroOrOne(
110+
Between(
111+
Terms.Char( '<' ),
112+
XsParsers.TypeRuntime(),
113+
Terms.Char( '>' )
114+
)
115+
)
116+
.AndSkip( Terms.Text( "::" ) )
117+
.And( Terms.NamespaceIdentifier() )
118+
.And(
119+
ZeroOrOne(
120+
Between(
121+
Terms.Char( '(' ),
122+
ZeroOrOne(
123+
Separated(
124+
Terms.Char( ',' ),
125+
expression
126+
127+
)
128+
),
129+
Terms.Char( ')' )
130+
)
131+
) )
132+
.Then<Expression>( ( _, parts ) =>
133+
{
134+
var (type, name, args) = parts;
135+
136+
if ( name == null )
137+
throw new InvalidOperationException( "Name must be specified." );
138+
139+
if ( args == null )
140+
{
141+
return Call(
142+
Constant( _member ),
143+
type != null
144+
? _getValueAsMethodInfo.MakeGenericMethod( type )
145+
: _getValueAsMethodInfo.MakeGenericMethod( typeof(object) ),
146+
Constant( name.ToString() )
147+
);
148+
}
149+
150+
var invokeExpression = Call(
151+
Constant( _member ),
152+
_invokeMethodInfo,
153+
Constant( name.ToString() ),
154+
NewArrayInit( typeof(object), args )
155+
);
156+
157+
return type != null
158+
? Convert( invokeExpression, type )
159+
: invokeExpression; // normally defaults to typeof(object)
160+
161+
}
162+
)
163+
.Named( "vars" );
164+
}
165+
}
166+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<IsPackable>true</IsPackable>
6+
<Authors>Stillpoint Software, Inc.</Authors>
7+
<PackageReadmeFile>README.md</PackageReadmeFile>
8+
<PackageTags>templating;token;xs</PackageTags>
9+
<PackageIcon>icon.png</PackageIcon>
10+
<PackageProjectUrl>https://stillpoint-software.github.io/hyperbee.templating/</PackageProjectUrl>
11+
<TargetFrameworks>net9.0</TargetFrameworks>
12+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
13+
<Copyright>Stillpoint Software, Inc.</Copyright>
14+
<Title>Hyperbee Templating Provider XS</Title>
15+
<Description>
16+
Adds an ITokenExpressionProvider powder by Hyperbee.XS
17+
</Description>
18+
<RepositoryUrl>https://github.com/Stillpoint-Software/Hyperbee.Templating</RepositoryUrl>
19+
<RepositoryType>git</RepositoryType>
20+
<PackageReleaseNotes>https://github.com/Stillpoint-Software/hyperbee.templating/releases/latest</PackageReleaseNotes>
21+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
26+
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
27+
</AssemblyAttribute>
28+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
29+
<_Parameter1>$(AssemblyName).Benchmark</_Parameter1>
30+
</AssemblyAttribute>
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<None Update="$(MSBuildProjectName).csproj.DotSettings" Visible="false" />
35+
</ItemGroup>
36+
<ItemGroup>
37+
<None Include="..\..\assets\icon.png" Pack="true" Visible="false" PackagePath="/" />
38+
<None Include="..\..\README.md" Pack="true" Visible="true" PackagePath="/" Link="README.md" />
39+
<None Include="..\..\LICENSE" Pack="true" Visible="false" PackagePath="/" />
40+
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="8.0.0">
41+
<PrivateAssets>all</PrivateAssets>
42+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
43+
</PackageReference>
44+
<PackageReference Include="FastExpressionCompiler" Version="5.1.1" />
45+
<PackageReference Include="Hyperbee.Resources" Version="2.0.2" />
46+
<PackageReference Include="Hyperbee.XS" Version="1.3.1" />
47+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
48+
</ItemGroup>
49+
<ItemGroup>
50+
<ProjectReference Include="..\Hyperbee.Templating\Hyperbee.Templating.csproj" />
51+
</ItemGroup>
52+
</Project>

test/Hyperbee.Templating.Benchmark/Hyperbee.Templating.Benchmark.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17+
<ProjectReference Include="..\..\src\Hyperbee.Templating.Provider.XS\Hyperbee.Templating.Provider.XS.csproj" />
1718
<ProjectReference Include="..\..\src\Hyperbee.Templating\Hyperbee.Templating.csproj" />
1819
<ProjectReference Include="..\Hyperbee.Templating.Tests\Hyperbee.Templating.Tests.csproj" />
1920
</ItemGroup>

test/Hyperbee.Templating.Benchmark/TemplateBenchmarks.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using BenchmarkDotNet.Attributes;
2+
using Hyperbee.Templating.Provider.XS.Compiler;
23
using Hyperbee.Templating.Text;
34

45
namespace Hyperbee.Templating.Benchmark;
@@ -68,5 +69,33 @@ public void InlineBlockExpression()
6869
}
6970
} );
7071
}
72+
73+
[Benchmark]
74+
public void InlineBlockExpressionXs()
75+
{
76+
const string expression = "{{name}}";
77+
const string definition =
78+
"""
79+
{{name:{{_ => {
80+
return switch( vars<string>::choice )
81+
{
82+
case "1": "me";
83+
case "2": "you";
84+
default: "default";
85+
};
86+
} }} }}
87+
""";
88+
89+
const string template = $"{definition}hello {expression}.";
90+
91+
Template.Render( template, new()
92+
{
93+
Variables =
94+
{
95+
["choice"] = "2"
96+
},
97+
TokenExpressionProvider = new XsTokenExpressionProvider(true)
98+
} );
99+
}
71100
}
72101

0 commit comments

Comments
 (0)