diff --git a/src/Draco.Compiler.DevHost/Program.cs b/src/Draco.Compiler.DevHost/Program.cs index bf12e5ab0..886f6fe46 100644 --- a/src/Draco.Compiler.DevHost/Program.cs +++ b/src/Draco.Compiler.DevHost/Program.cs @@ -7,12 +7,26 @@ using Draco.Compiler.Api; using Draco.Compiler.Api.Scripting; using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Api.Syntax.Quoting; using static Basic.Reference.Assemblies.Net80; namespace Draco.Compiler.DevHost; internal class Program { + private enum CliQuoteMode + { + file, + decl, + stmt, + expr, + } + + private enum CliOutputLanguage + { + cs, + } + private static IEnumerable BclReferences => ReferenceInfos.All .Select(r => MetadataReference.FromPeStream(new MemoryStream(r.ImageBytes))); @@ -27,6 +41,10 @@ private static RootCommand ConfigureCommands() var referencesOption = new Option(["-r", "--reference"], Array.Empty, "Specifies additional assembly references to use when compiling"); var filesArgument = new Argument("source files", Array.Empty, "Specifies draco source files that should be compiled"); var rootModuleOption = new Option(["-m", "--root-module"], () => null, "Specifies the root module folder of the compiled files"); + var quoteModeOption = new Option(["-m", "--quote-mode"], () => CliQuoteMode.file, "Specifies the kind of syntactic element to quote"); + var outputLanguageOption = new Option(["-l", "--language"], () => CliOutputLanguage.cs, "Specifies the language to output the quoted code as"); + var prettyPrintOption = new Option(["-p", "--pretty-print"], () => false, "Whether to append whitespace to the output code"); + var staticImportOption = new Option(["-s", "--require-static-import"], () => false, "Whether to require the SyntaxFactory class to be imported statically in the output code"); // Compile @@ -88,6 +106,18 @@ private static RootCommand ConfigureCommands() }; formatCommand.SetHandler(FormatCommand, fileArgument, optionalOutputOption); + // Quoting + + var quoteCommand = new Command("quote", "Generates a string of code in an output language which produces syntax nodes equivalent to the input code") + { + fileArgument, + quoteModeOption, + outputLanguageOption, + prettyPrintOption, + staticImportOption, + }; + quoteCommand.SetHandler(QuoteCommand, fileArgument, quoteModeOption, outputLanguageOption, prettyPrintOption, staticImportOption); + return new RootCommand("CLI for the Draco compiler") { compileCommand, @@ -95,7 +125,8 @@ private static RootCommand ConfigureCommands() irCommand, symbolsCommand, declarationsCommand, - formatCommand + formatCommand, + quoteCommand, }; } @@ -179,6 +210,34 @@ private static void FormatCommand(FileInfo input, FileInfo? output) new StreamWriter(outputStream).Write(syntaxTree.Format().ToString()); } + private static void QuoteCommand( + FileInfo input, + CliQuoteMode mode, + CliOutputLanguage outputLanguage, + bool prettyPrint, + bool staticImport) + { + var sourceText = SourceText.FromFile(input.FullName); + var quotedText = SyntaxQuoter.Quote( + sourceText, + mode switch + { + CliQuoteMode.file => QuoteMode.File, + CliQuoteMode.decl => QuoteMode.Declaration, + CliQuoteMode.stmt => QuoteMode.Statement, + CliQuoteMode.expr => QuoteMode.Expression, + _ => throw new ArgumentOutOfRangeException(nameof(mode)) + }, + outputLanguage switch + { + CliOutputLanguage.cs => OutputLanguage.CSharp, + _ => throw new ArgumentOutOfRangeException(nameof(outputLanguage)) + }, + prettyPrint, + staticImport); + Console.WriteLine(quotedText); + } + private static ImmutableArray GetSyntaxTrees(params FileInfo[] input) { var result = ImmutableArray.CreateBuilder(); diff --git a/src/Draco.Compiler/Api/Syntax/Quoting/CSharpQuoterTemplate.cs b/src/Draco.Compiler/Api/Syntax/Quoting/CSharpQuoterTemplate.cs new file mode 100644 index 000000000..a704808be --- /dev/null +++ b/src/Draco.Compiler/Api/Syntax/Quoting/CSharpQuoterTemplate.cs @@ -0,0 +1,181 @@ +using System; +using System.Text; +using Draco.Compiler.Internal.Utilities; + +namespace Draco.Compiler.Api.Syntax.Quoting; + +/// +/// Outputs quoted code as C# code. +/// +/// The string builder for the template to append text to. +/// Whether to append whitespace. +/// Whether to append SyntaxFactory. before properties and function calls. +internal sealed class CSharpQuoterTemplate(StringBuilder builder, bool prettyPrint, bool staticImport) +{ + private int indentLevel = 0; + + /// + /// Generates C# code from a . + /// + /// The expression to generate code from. + /// Whether to append whitespace. + /// Whether to append SyntaxFactory. before properties and function calls. + /// + public static string Generate(QuoteExpression expr, bool prettyPrint, bool staticImport) + { + var builder = new StringBuilder(); + var template = new CSharpQuoterTemplate(builder, prettyPrint, staticImport); + template.AppendExpr(expr); + return builder.ToString(); + } + + private void AppendExpr(QuoteExpression expr) + { + switch (expr) + { + case QuoteFunctionCall call: + this.AppendFunctionCall(call); + break; + + case QuoteProperty(var property): + this.TryAppendSyntaxFactory(); + builder.Append(property); + break; + + case QuoteList list: + this.AppendList(list); + break; + + case QuoteNull: + builder.Append("null"); + break; + + case QuoteTokenKind(var kind): + builder.Append("TokenKind."); + builder.Append(kind.ToString()); + break; + + case QuoteInteger(var value): + builder.Append(value); + break; + + case QuoteFloat(var value): + builder.Append(value); + break; + + case QuoteBoolean(var value): + builder.Append(value ? "true" : "false"); + break; + + case QuoteCharacter(var value): + builder.Append('\''); + // Need this for escape characters. + builder.Append(StringUtils.Unescape(value.ToString())); + builder.Append('\''); + break; + + case QuoteString(var value): + builder.Append('"'); + builder.Append(StringUtils.Unescape(value)); + builder.Append('"'); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(expr)); + } + } + + private void AppendFunctionCall(QuoteFunctionCall call) + { + var (function, typeArgs, args) = call; + + this.TryAppendSyntaxFactory(); + builder.Append(function); + + if (typeArgs is not []) + { + builder.Append('<'); + for (var i = 0; i < typeArgs.Length; i++) + { + builder.Append(typeArgs[i]); + + if (i < typeArgs.Length - 1) builder.Append(", "); + } + builder.Append('>'); + } + + // Place function calls with a single simple expression as its argument on a single line. + if (call.Arguments is [var singleArg] && IsSimple(singleArg)) + { + builder.Append('('); + this.AppendExpr(singleArg); + builder.Append(')'); + + return; + } + + builder.Append('('); + this.indentLevel += 1; + for (var i = 0; i < args.Length; i++) + { + this.TryAppendNewLine(); + this.TryAppendIndentation(); + + this.AppendExpr(args[i]); + + if (i < args.Length - 1) builder.Append(prettyPrint ? "," : ", "); + } + this.indentLevel -= 1; + builder.Append(')'); + } + + private void AppendList(QuoteList list) + { + var values = list.Values; + + builder.Append('['); + this.indentLevel += 1; + for (var i = 0; i < values.Length; i++) + { + this.TryAppendNewLine(); + this.TryAppendIndentation(); + + this.AppendExpr(values[i]); + + if (i < values.Length - 1) builder.Append(prettyPrint ? "," : ", "); + } + this.indentLevel -= 1; + builder.Append(']'); + } + + private void TryAppendSyntaxFactory() + { + if (!staticImport) builder.Append("SyntaxFactory."); + } + + private void TryAppendIndentation() + { + // Todo: perhaps parameterize indentation size + if (prettyPrint) builder.Append(' ', this.indentLevel * 2); + } + + private void TryAppendNewLine() + { + if (prettyPrint) builder.Append(Environment.NewLine); + } + + /// + /// Checks whether an expression is "simple" and can be placed on the same line as a function call when passed as an argument. + /// + /// The expression to check. + /// Whether the expression is simple. + private static bool IsSimple(QuoteExpression expr) => expr + is QuoteProperty + or QuoteNull + or QuoteTokenKind + or QuoteInteger + or QuoteFloat + or QuoteBoolean + or QuoteCharacter + or QuoteString; +} diff --git a/src/Draco.Compiler/Api/Syntax/Quoting/QuoteExpressionModel.cs b/src/Draco.Compiler/Api/Syntax/Quoting/QuoteExpressionModel.cs new file mode 100644 index 000000000..457a90638 --- /dev/null +++ b/src/Draco.Compiler/Api/Syntax/Quoting/QuoteExpressionModel.cs @@ -0,0 +1,79 @@ +using System.Collections.Immutable; + +namespace Draco.Compiler.Api.Syntax.Quoting; + +/// +/// An expression generated by the quoter. +/// +internal abstract record QuoteExpression; + +/// +/// A function call quote expression. +/// +/// The name of the function in . +/// The type arguments to the function. +/// The arguments to the function. +internal sealed record QuoteFunctionCall( + string Function, + ImmutableArray TypeArguments, + ImmutableArray Arguments) + : QuoteExpression +{ + public QuoteFunctionCall(string function, ImmutableArray arguments) + : this(function, [], arguments) + { + } +} + +/// +/// A property access quote expression. +/// +/// The name of the property in . +internal sealed record QuoteProperty(string Property) : QuoteExpression; + +/// +/// A list quote expression. +/// +/// The values in the list. +internal sealed record QuoteList(ImmutableArray Values) : QuoteExpression; + +/// +/// A null quote expression. +/// +internal sealed record QuoteNull : QuoteExpression; + +/// +/// A token kind quote expression. +/// +/// The token kind. +internal sealed record QuoteTokenKind(TokenKind Value) : QuoteExpression; + +/// +/// An integer quote expression. +/// +/// The integer value. +internal sealed record QuoteInteger(int Value) : QuoteExpression; + +/// +/// A float quote expression. +/// +/// The float value. +internal sealed record QuoteFloat(float Value) : QuoteExpression; + +/// +/// A boolean quote expression. +/// +/// The boolean value. +internal sealed record QuoteBoolean(bool Value) : QuoteExpression; + +/// +/// A character quote expression. +/// +/// The character value. +internal sealed record QuoteCharacter(char Value) : QuoteExpression; + +/// +/// A string quote expression. +/// +/// The string value. +internal sealed record QuoteString(string Value) : QuoteExpression; diff --git a/src/Draco.Compiler/Api/Syntax/Quoting/SyntaxQuoter.cs b/src/Draco.Compiler/Api/Syntax/Quoting/SyntaxQuoter.cs new file mode 100644 index 000000000..b28fcae9f --- /dev/null +++ b/src/Draco.Compiler/Api/Syntax/Quoting/SyntaxQuoter.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Draco.Compiler.Internal.Syntax; +using Draco.Compiler.Internal.Utilities; + +namespace Draco.Compiler.Api.Syntax.Quoting; + +/// +/// Specifies the output language for the quoter. +/// +public enum OutputLanguage +{ + /// + /// Output as C# code. + /// + CSharp + + // Probably going to add an option for Draco at some point +} + +/// +/// Specifies what the quoter should +/// +public enum QuoteMode +{ + /// + /// Quote an entire file. + /// + File, + + /// + /// Quote a single declaration. + /// + Declaration, + + /// + /// Quote a single statement. + /// + Statement, + + /// + /// Quote a single expression. + /// + Expression +} + +/// +/// Produces quoted text from s or s. +/// +/// The target output language for the quoter. +public static partial class SyntaxQuoter +{ + /// + /// Produces a string containing the factory method calls required to produce a string of source code. + /// + /// The to quote. + /// What kind of syntactic element to quote. + /// The language to output the quoted code as. + /// Whether to append whitespace to the output quote. + /// Whether to require a static import of in the quoted code. + /// A string containing the quoted text. + public static string Quote( + SourceText text, + QuoteMode mode, + OutputLanguage outputLanguage, + bool prettyPrint, + bool requireStaticImport) + { + // Todo: probably factor out this duplicate code + var diags = new SyntaxDiagnosticTable(); + var srcReader = text.SourceReader; + var lexer = new Lexer(srcReader, diags); + var tokenSource = TokenSource.From(lexer); + var parser = new Parser(tokenSource, diags); + + Internal.Syntax.SyntaxNode node = mode switch + { + QuoteMode.File => parser.ParseCompilationUnit(), + QuoteMode.Declaration => parser.ParseDeclaration(), + QuoteMode.Statement => parser.ParseStatement(false), // Todo: allow declarations? + QuoteMode.Expression => parser.ParseExpression(), + _ => throw new ArgumentOutOfRangeException(nameof(mode)) + }; + + var red = node.ToRedNode(null!, null, 0); + + return Quote(red, outputLanguage, prettyPrint, requireStaticImport); + } + + /// + /// Produces a string containing the factory method calls required to produce a syntax node. + /// + /// The to quote. + /// The language to output the quoted code as. + /// Whether to append whitespace to the output quote. + /// Whether to require a static import of in the quoted code. + /// A string containing the quoted text. + public static string Quote( + SyntaxNode node, + OutputLanguage outputLanguage, + bool prettyPrint, + bool requireStaticImport) + { + var expr = node.Accept(new QuoteVisitor()); + + return outputLanguage switch + { + OutputLanguage.CSharp => CSharpQuoterTemplate.Generate(expr, prettyPrint, requireStaticImport), + _ => throw new ArgumentOutOfRangeException(nameof(outputLanguage)) + }; + } + + private static QuoteExpression QuoteObjectLiteral(object? value) => value switch + { + null => new QuoteNull(), + int @int => new QuoteInteger(@int), + float @float => new QuoteFloat(@float), + string @string => new QuoteString(@string), + bool @bool => new QuoteBoolean(@bool), + // Todo: are there any more literal values tokens can have? + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + + private sealed partial class QuoteVisitor : SyntaxVisitor + { + public override QuoteExpression VisitSyntaxToken(SyntaxToken node) + { + return (node.Kind, SyntaxFacts.GetTokenText(node.Kind), node.Value) switch + { + // Identifiers, integers, floats, and character literals all have special factory methods. + + (TokenKind.Identifier, _, _) => new QuoteString(node.Text!), + + (TokenKind.LiteralInteger, _, var value) => new QuoteFunctionCall("Integer", [ + new QuoteInteger((int)value!) + ]), + + (TokenKind.LiteralFloat, _, var value) => new QuoteFunctionCall("Float", [ + new QuoteFloat((float)value!) + ]), + (TokenKind.LiteralCharacter, _, var value) => new QuoteFunctionCall("Character", [ + new QuoteCharacter((char)value!) + ]), + + // True and false have their respective values in the token, but we don't want that. + (TokenKind.KeywordTrue or TokenKind.KeywordFalse, _, _) => new QuoteProperty(node.Kind.ToString()), + + // Token kind does not require any text nor a value + (_, not null, null) => new QuoteProperty(node.Kind.ToString()), + + // Token kind requires text + (_, null, null) => new QuoteFunctionCall(node.Kind.ToString(), [ + new QuoteString(StringUtils.Unescape(node.Text)) + ]), + + // Token kind requires a value + (_, not null, not null) => new QuoteFunctionCall(node.Kind.ToString(), [ + QuoteObjectLiteral(node.Value) + ]), + + // Token kind requires both text and a value + (_, null, not null) => new QuoteFunctionCall(node.Kind.ToString(), [ + new QuoteString(StringUtils.Unescape(node.Text)), + QuoteObjectLiteral(node.Value) + ]) + }; + } + + public override QuoteExpression VisitSyntaxTrivia(SyntaxTrivia node) => + throw new NotSupportedException("Quoter does currently not support quoting syntax trivia."); + + public override QuoteExpression VisitSyntaxList(SyntaxList node) => + new QuoteList(node.Select(n => n.Accept(this)).ToImmutableArray()); + + public override QuoteExpression VisitSeparatedSyntaxList(SeparatedSyntaxList node) => + new QuoteFunctionCall( + "SeparatedSyntaxList", + [typeof(TNode).FullName!], // Todo: hack + [ + new QuoteList(node.Separators.Select(x => x.Accept(this)).ToImmutableArray()), + new QuoteList(node.Values.Select(x => x.Accept(this)).ToImmutableArray())]); + } +} diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs b/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs index 022e76259..dfcc676d7 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxFactory.cs @@ -64,6 +64,8 @@ public static SyntaxToken Missing(TokenKind kind) => public static SyntaxToken Identifier(string text) => Token(TokenKind.Identifier, text); public static SyntaxToken Integer(int value) => Token(TokenKind.LiteralInteger, value.ToString(), value); + public static SyntaxToken Float(float value) => Token(TokenKind.LiteralFloat, value.ToString(), value); + public static SyntaxToken Character(char value) => Token(TokenKind.LiteralCharacter, value.ToString(), value); public static TokenKind? Visibility(Visibility visibility) => visibility switch { @@ -82,6 +84,16 @@ public static SyntaxList SyntaxList(IEnumerable elements) public static SyntaxList SyntaxList(params TNode[] elements) where TNode : SyntaxNode => SyntaxList(elements.AsEnumerable()); + public static SeparatedSyntaxList SeparatedSyntaxList(IEnumerable separators, IEnumerable elements) + where TNode : SyntaxNode => new( + tree: null!, + parent: null, + fullPosition: 0, + green: Syntax.SeparatedSyntaxList.MakeGreen( + Internal.Syntax.SeparatedSyntaxList.CreateInterleavedSequence( + separators.Select(x => x.Green), + elements.Select(x => x.Green)))); + public static SeparatedSyntaxList SeparatedSyntaxList(SyntaxToken separator, IEnumerable elements) where TNode : SyntaxNode => new( tree: null!, diff --git a/src/Draco.Compiler/Internal/Syntax/SeparatedSyntaxList.cs b/src/Draco.Compiler/Internal/Syntax/SeparatedSyntaxList.cs index 9f7ed53a1..0dd270f6b 100644 --- a/src/Draco.Compiler/Internal/Syntax/SeparatedSyntaxList.cs +++ b/src/Draco.Compiler/Internal/Syntax/SeparatedSyntaxList.cs @@ -19,6 +19,34 @@ internal static class SeparatedSyntaxList /// The created builder. public static SeparatedSyntaxList.Builder CreateBuilder() where TNode : SyntaxNode => new(); + + /// + /// Creates an of syntax ndoes by interleaving a sequence of values with a sequence of separators. + /// + /// The node type. + /// The separator tokens. + /// The value nodes. Has to be equal to or one more than the length of . + /// The constructed interleaved sequence. + public static IEnumerable CreateInterleavedSequence(IEnumerable separators, IEnumerable values) + { + using var valuesEnum = values.GetEnumerator(); + using var separatorsEnum = separators.GetEnumerator(); + + while (valuesEnum.MoveNext()) + { + yield return valuesEnum.Current; + + if (!separatorsEnum.MoveNext()) + { + if (valuesEnum.MoveNext()) throw new ArgumentException("Found more elements than separators.", nameof(values)); + yield break; + } + + yield return separatorsEnum.Current; + } + + if (separatorsEnum.MoveNext()) throw new ArgumentException("Found more separators than elements.", nameof(separators)); + } } /// diff --git a/src/Draco.SourceGeneration/SyntaxTree/SyntaxTreeSourceGenerator.cs b/src/Draco.SourceGeneration/SyntaxTree/SyntaxTreeSourceGenerator.cs index 3b2302e5f..9de603c76 100644 --- a/src/Draco.SourceGeneration/SyntaxTree/SyntaxTreeSourceGenerator.cs +++ b/src/Draco.SourceGeneration/SyntaxTree/SyntaxTreeSourceGenerator.cs @@ -18,12 +18,14 @@ protected override IEnumerable> GenerateSources(obj var tokenCode = Template.GenerateTokens(domainModel); var greenTreeCode = Template.GenerateGreenTree(domainModel); var redTreeCode = Template.GenerateRedTree(domainModel); + var quoterCode = Template.GenerateQuoter(domainModel); return [ new("Tokens.Generated.cs", tokenCode), new("GreenSyntaxTree.Generated.cs", greenTreeCode), new("RedSyntaxTree.Generated.cs", redTreeCode), + new("Quoter.Generated.cs", quoterCode), ]; } } diff --git a/src/Draco.SourceGeneration/SyntaxTree/Template.cs b/src/Draco.SourceGeneration/SyntaxTree/Template.cs index 2ae7b25b2..ce7dd22ab 100644 --- a/src/Draco.SourceGeneration/SyntaxTree/Template.cs +++ b/src/Draco.SourceGeneration/SyntaxTree/Template.cs @@ -125,6 +125,22 @@ public static partial class SyntaxFactory } #nullable restore +"""); + + public static string GenerateQuoter(Tree tree) => FormatCSharp($$""" +using System.Collections.Generic; + +namespace Draco.Compiler.Api.Syntax.Quoting; + +#nullable enable + +public static partial class SyntaxQuoter +{ + private sealed partial class QuoteVisitor + { + {{ForEach(tree.Nodes.Where(x => !x.IsAbstract), node => QuoterMethod(node, tree))}} + } +} """); private static string GreenNodeClass(Tree tree, Node node) => $$""" @@ -371,7 +387,7 @@ public override IEnumerable<{{tree.Root.Name}}> Children yield break; } } - + """); private static string Accept(Node node) => When(node.IsAbstract && node.Base is null, @@ -429,6 +445,36 @@ private static string FieldPrefix(Field field) => $$""" private static string InternalType(string type) => $"Internal.Syntax.{type.Replace("<", " $$""" +public override QuoteExpression {{VisitorName(node)}}({{node.Name}} node) => + new QuoteFunctionCall("{{FactoryName(node)}}", [ + {{ForEach(node.Fields, field => $$""" + {{When(!FieldCanBeOmitted(field, tree), $$""" + {{QuoterMethodField(field, tree)}}, + """)}} + """)}} + ]); +"""; + + private static string QuoterMethodField(Field field, Tree tree) => $$""" +{{When(field.IsNullable, $$""" + {{When(FieldCanBeTokenKind(field, tree), $$""" + node.{{field.Name}} is not null ? new QuoteTokenKind(node.{{field.Name}}.Kind) : new QuoteNull() + """, + $$""" + node.{{field.Name}}?.Accept(this) ?? new QuoteNull() + """)}} + """, +$$""" + {{When(FieldCanBeTokenKind(field, tree), $$""" + new QuoteTokenKind(node.{{field.Name}}.Kind) + """, + $$""" + node.{{field.Name}}.Accept(this) + """)}} + """)}} +"""; + private static string FactoryName(Node node) => RemoveSuffix(node.Name, "Syntax"); private static string AbstractSealed(Node node) => When(node.IsAbstract, "abstract", "sealed"); private static string ProtectedPublic(Node node) => When(node.IsAbstract, "protected", "public"); @@ -438,6 +484,18 @@ private static string InternalType(string type) => private static string DocName(FieldFacade field) => field.ParameterName is null ? string.Empty : RemovePrefix(field.ParameterName, "@"); + private static bool FieldCanBeOmitted(Field field, Tree tree) => + field.IsToken && + field.TokenKinds is [var kind] && + tree.Tokens + .First(token => token.Name == kind) + .Text is not null; + private static bool FieldCanBeTokenKind(Field field, Tree tree) => + field.IsToken && + field.TokenKinds.All(kind => + tree.Tokens + .First(token => token.Name == kind) + .Text is not null); private readonly record struct FieldFacade( bool IsOriginal,