ufcpp / TextTemplateSourceGenerator

Text Template Preprocessor by C# Source Generator
MIT License
2 stars 0 forks source link

direct generator #3

Open ufcpp opened 3 years ago

ufcpp commented 3 years ago

TextTemplatePreprocessor is something like TextTemplatingFilePreprocessor in T4 template engine. I need also TextTemplatingFileGenerator in T4.

ufcpp commented 1 year ago

Maybe possible by using CSharpScript.RunAsync.

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); } } ```