POC code
```cs
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System;
var runner = new Runner();
Console.WriteLine(runner.Run("""
var x = 3;
var sum = 0;
foreach (var i in new[] {1,2,3,5,7,11,13})
{
_env.Append($"{x} × {i}\n");
sum += x * i;
}
_env.Append($"{sum}\n");
"""));
for (int i = 0; i < 10; i++)
{
Console.WriteLine(runner.Run("""
var r = new System.Random();
for (var i = 0; i < 10; ++i) _env.Append($"{r.Next():X}\n");
"""));
}
public class Runner
{
private readonly MemoryStream _memory = new(64 * 1024);
private readonly StringBuilder _environment = new(32* 1024);
public string Run(string source)
{
source = $$"""
public class Template
{
public static string Generate(System.Text.StringBuilder _env)
{
{{source}}
return _env.ToString();
}
}
""";
var s = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview));
using var c = Compile(s);
return Invoke(c.Assembly);
}
private string Invoke(Assembly a)
{
var t = a.GetType("Template")!;
var m = t.GetMethod("Generate")!;
_environment.Clear();
m.Invoke(null, new[] { _environment });
return _environment.ToString();
}
private Context Compile(SyntaxTree syntaxTree)
{
var references = AppDomain.CurrentDomain.GetAssemblies();
var mrefs = references
.Where(a => !string.IsNullOrEmpty(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location));
var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), new[] { syntaxTree }, mrefs, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var ms = _memory;
ms.Seek(0, SeekOrigin.Begin);
ms.SetLength(0);
var result = compilation.Emit(ms);
if (result.Success)
{
ms.Seek(0, SeekOrigin.Begin);
return new(ms);
}
else
{
throw new InvalidOperationException(string.Join("\n", result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error).Select(d => $"{d.Id}: {d.GetMessage()}")));
}
}
private struct Context : IDisposable
{
private readonly AssemblyLoadContext _context;
public Context(Stream assembly)
{
var context = new AssemblyLoadContext("", true);
context.LoadFromStream(assembly);
_context = context;
}
public Assembly Assembly => _context.Assemblies.First();
public void Dispose() => _context.Unload();
}
}
```
POC code
```cs
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
{ }
var text = """
$[
var names = new[] { "bool", "int", "string" };
var values = new[] { 2, 3, 5, 7, 11 };
]/
class X
{
$/ foreach (var x in names) {
public $x _$x { get; init; }
$/ }
public static IEnumerable Values = new[] { ${string.Join(", ", values)} };
}
""";
var spans = Parser.Parse(text);
foreach (var s in spans) Console.WriteLine((s.Type, text[s.Range]));
Console.WriteLine("---------------------------");
var emitted = Emitter.Emit(text, spans);
Console.WriteLine(emitted);
Console.WriteLine("---------------------------");
var opt = ScriptOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
var xxx = await CSharpScript.RunAsync(emitted, opt);
Console.WriteLine(xxx.ReturnValue);
enum SpanType
{
Raw,
Expression,
BraceExpression,
OneLineStatement,
Statements,
}
record struct Span(SpanType Type, Range Range);
class Emitter
{
public static string Emit(ReadOnlySpan text, IEnumerable spans)
{
var sb = new StringBuilder();
sb.Append("""
var _env = new System.Text.StringBuilder();
""");
foreach (var s in spans)
{
var span = text[s.Range];
if (span.Length == 0) continue;
switch (s.Type)
{
case SpanType.Raw:
sb.Append($""""
_env.Append("""
{span}
""");
"""");
break;
case SpanType.Expression:
case SpanType.BraceExpression:
sb.Append($"""
_env.Append({span});
""");
break;
case SpanType.OneLineStatement:
case SpanType.Statements:
sb.Append(span);
break;
}
}
sb.Append("""
return _env.ToString();
""");
return sb.ToString();
}
}
class Parser
{
public static List Parse(string text)
{
var tokens = new List();
SpanType type = SpanType.Raw;
var span = text.AsSpan();
var start = 0;
while (true)
{
var (nextType, charsRead, range) = type switch
{
SpanType.OneLineStatement => ParseOneLineStatement(span),
SpanType.Statements => ParseStatements(span),
SpanType.Expression => ParseExpression(span),
SpanType.BraceExpression => ParseBraceExpression(span),
_ => ParseRaw(span),
};
tokens.Add(new Span(type, Shift(range, start)));
start += charsRead;
span = span[charsRead..];
if (nextType is not { } t) break;
type = t;
}
return tokens;
}
private static Range Shift(Range range, int offset) => (range.Start.Value + offset)..(range.End.Value + offset);
private static (SpanType? nextType, int charsRead, Range range) ParseBraceExpression(ReadOnlySpan text)
{
if (text.Length == 0) return default;
int i;
for (i = 2; i < text.Length; i++)
{
if (text[i] is not (' ' or '\t' or '\r' or '\n')) break;
}
var tokenStart = i;
var tokenEnd = text.Length;
SpanType? type = null;
var nest = 0;
for (; i < text.Length; i++)
{
var c = text[i];
if (c == '{') nest++;
else if (c == '}')
{
nest--;
if (nest < 0)
{
type = SpanType.Raw;
tokenEnd = i;
break;
}
}
}
i++;
if (i < text.Length && text[i] == '/')
{
i++;
for (; i < text.Length; i++)
{
if (text[i] is not (' ' or '\t' or '\r' or '\n')) break;
}
}
return (type, i, tokenStart..tokenEnd);
}
private static (SpanType? nextType, int charsRead, Range range) ParseExpression(ReadOnlySpan text)
{
if (text.Length == 0) return default;
int i;
for (i = 1; i < text.Length; i++)
{
if (text[i] is not (' ' or '\t' or '\r' or '\n')) break;
}
var tokenStart = i;
for (; i < text.Length; i++)
{
var c = text[i];
if (!char.IsAsciiLetter(c))
{
return (SpanType.Raw, i, tokenStart..i);
}
}
return (null, text.Length, tokenStart..text.Length);
}
private static (SpanType? nextType, int charsRead, Range range) ParseOneLineStatement(ReadOnlySpan text)
{
if (text.Length == 0) return default;
for (var i = 0; i < text.Length; i++)
{
var c = text[i];
if (c == '\n')
{
return (SpanType.Raw, i + 1, 2..(i + 1));
}
}
return (null, text.Length, 2..text.Length);
}
private static (SpanType? nextType, int charsRead, Range range) ParseStatements(ReadOnlySpan text)
{
if (text.Length == 0) return default;
var tokenEnd = text.Length;
SpanType? type = null;
var nest = 0;
int i;
for (i = 2; i < text.Length; i++)
{
var c = text[i];
if (c == '[') nest++;
else if (c == ']')
{
nest--;
if (nest < 0)
{
tokenEnd = i;
type = SpanType.Raw;
break;
}
}
}
i++;
if (i < text.Length && text[i] == '/')
{
i++;
for (; i < text.Length; i++)
{
if (text[i] is not (' ' or '\t' or '\r' or '\n')) break;
}
}
return (type, i, 2..tokenEnd);
}
private static (SpanType? nextType, int charsRead, Range range) ParseRaw(ReadOnlySpan text)
{
if (text.Length == 0) return default;
for (var i = 0; i < text.Length; i++)
{
var c = text[i];
if (c == '$')
{
// EOF
if (i == text.Length) return (null, i, ..i);
var c1 = text[i + 1];
var nextType = c1 switch
{
'/' => SpanType.OneLineStatement,
'[' => SpanType.Statements,
'{' => SpanType.BraceExpression,
_ => SpanType.Expression,
};
return (nextType, i, ..i);
}
}
return (null, text.Length, ..text.Length);
}
}
```
TextTemplatePreprocessor
is something like TextTemplatingFilePreprocessor in T4 template engine. I need also TextTemplatingFileGenerator in T4.