Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quoter #470

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ac85bc4
Quoter skeleton
thinker227 Sep 3, 2024
f7c98e3
Implement quoting syntax tokens
thinker227 Sep 4, 2024
6bdf066
Add NotSupportedException for quoting syntax trivia
thinker227 Sep 4, 2024
87a3f47
Implement quoting syntax lists
thinker227 Sep 4, 2024
8959cd9
Add Float and Character methods to SyntaxFactory
thinker227 Sep 4, 2024
8961f1d
Add CreateInterleaved utility for SeparatedSyntaxList
thinker227 Sep 4, 2024
a13dd3d
Implement quoting separated syntax lists
thinker227 Sep 4, 2024
02ac879
Add QuoteList, QuoteTokenKind, and QuoteFloat
thinker227 Sep 4, 2024
d180c9c
Fix string escape issues
thinker227 Sep 4, 2024
7c07f5f
Make quoting a syntax list use SyntaxList method
thinker227 Sep 4, 2024
6bd7495
Use red tree instead of green tree
thinker227 Sep 4, 2024
bb095f8
Add type arguments
thinker227 Sep 4, 2024
9687afa
Little conundrum
thinker227 Sep 4, 2024
865dab8
Fix type issue with SeparatedSyntaxList in syntax factory
thinker227 Sep 5, 2024
5b22347
Merge branch 'main' into quoter
thinker227 Sep 10, 2024
f96fd5a
Vestigial stuff from merge
thinker227 Sep 10, 2024
8fc4aa9
Rewrite quoter sg
thinker227 Sep 10, 2024
4a42f4d
Fix some additional stuff
thinker227 Sep 10, 2024
a91bfc0
Update SyntaxToken quoting
thinker227 Sep 10, 2024
45b4635
Add C# output for quoter
thinker227 Sep 10, 2024
78b8267
Update indentation size
thinker227 Sep 10, 2024
785661d
Add quote command to dev host CLI
thinker227 Sep 10, 2024
1cb4faf
Merge branch 'main' into quoter
thinker227 Sep 11, 2024
cde4f2e
Add QuoteCharacter
thinker227 Sep 12, 2024
9a4778f
Fix some stuff for identifiers and literals
thinker227 Sep 12, 2024
3bdcb0f
Add CLI enums
thinker227 Sep 12, 2024
ca796d9
Add a couple simplification rules
thinker227 Sep 12, 2024
7f84576
Fix QuoteTokenKind not appending TokenKind.
thinker227 Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion src/Draco.Compiler.DevHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataReference> BclReferences => ReferenceInfos.All
.Select(r => MetadataReference.FromPeStream(new MemoryStream(r.ImageBytes)));

Expand All @@ -27,6 +41,10 @@ private static RootCommand ConfigureCommands()
var referencesOption = new Option<FileInfo[]>(["-r", "--reference"], Array.Empty<FileInfo>, "Specifies additional assembly references to use when compiling");
var filesArgument = new Argument<FileInfo[]>("source files", Array.Empty<FileInfo>, "Specifies draco source files that should be compiled");
var rootModuleOption = new Option<DirectoryInfo?>(["-m", "--root-module"], () => null, "Specifies the root module folder of the compiled files");
var quoteModeOption = new Option<CliQuoteMode>(["-m", "--quote-mode"], () => CliQuoteMode.file, "Specifies the kind of syntactic element to quote");
var outputLanguageOption = new Option<CliOutputLanguage>(["-l", "--language"], () => CliOutputLanguage.cs, "Specifies the language to output the quoted code as");
var prettyPrintOption = new Option<bool>(["-p", "--pretty-print"], () => false, "Whether to append whitespace to the output code");
var staticImportOption = new Option<bool>(["-s", "--require-static-import"], () => false, "Whether to require the SyntaxFactory class to be imported statically in the output code");

// Compile

Expand Down Expand Up @@ -88,14 +106,27 @@ 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,
runCommand,
irCommand,
symbolsCommand,
declarationsCommand,
formatCommand
formatCommand,
quoteCommand,
};
}

Expand Down Expand Up @@ -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<SyntaxTree> GetSyntaxTrees(params FileInfo[] input)
{
var result = ImmutableArray.CreateBuilder<SyntaxTree>();
Expand Down
181 changes: 181 additions & 0 deletions src/Draco.Compiler/Api/Syntax/Quoting/CSharpQuoterTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using System;
using System.Text;
using Draco.Compiler.Internal.Utilities;

namespace Draco.Compiler.Api.Syntax.Quoting;

/// <summary>
/// Outputs quoted code as C# code.
/// </summary>
/// <param name="builder">The string builder for the template to append text to.</param>
/// <param name="prettyPrint">Whether to append whitespace.</param>
/// <param name="staticImport">Whether to append <c>SyntaxFactory.</c> before properties and function calls.</param>
internal sealed class CSharpQuoterTemplate(StringBuilder builder, bool prettyPrint, bool staticImport)
{
private int indentLevel = 0;

/// <summary>
/// Generates C# code from a <see cref="QuoteExpression"/>.
/// </summary>
/// <param name="expr">The expression to generate code from.</param>
/// <param name="prettyPrint">Whether to append whitespace.</param>
/// <param name="staticImport">Whether to append <c>SyntaxFactory.</c> before properties and function calls.</param>
/// <returns></returns>
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);
}

/// <summary>
/// Checks whether an expression is "simple" and can be placed on the same line as a function call when passed as an argument.
/// </summary>
/// <param name="expr">The expression to check.</param>
/// <returns>Whether the expression is simple.</returns>
private static bool IsSimple(QuoteExpression expr) => expr
is QuoteProperty
or QuoteNull
or QuoteTokenKind
or QuoteInteger
or QuoteFloat
or QuoteBoolean
or QuoteCharacter
or QuoteString;
}
79 changes: 79 additions & 0 deletions src/Draco.Compiler/Api/Syntax/Quoting/QuoteExpressionModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections.Immutable;

namespace Draco.Compiler.Api.Syntax.Quoting;

/// <summary>
/// An expression generated by the quoter.
/// </summary>
internal abstract record QuoteExpression;

/// <summary>
/// A function call quote expression.
/// </summary>
/// <param name="Function">The name of the function in <see cref="SyntaxFactory"/>.</param>
/// <param name="TypeArguments">The type arguments to the function.</param>
/// <param name="Arguments">The arguments to the function.</param>
internal sealed record QuoteFunctionCall(
string Function,
ImmutableArray<string> TypeArguments,
ImmutableArray<QuoteExpression> Arguments)
: QuoteExpression
{
public QuoteFunctionCall(string function, ImmutableArray<QuoteExpression> arguments)
: this(function, [], arguments)
{
}
}

/// <summary>
/// A property access quote expression.
/// </summary>
/// <param name="Property">The name of the property in <see cref="SyntaxFactory"/>.</param>
internal sealed record QuoteProperty(string Property) : QuoteExpression;

/// <summary>
/// A list quote expression.
/// </summary>
/// <param name="Values">The values in the list.</param>
internal sealed record QuoteList(ImmutableArray<QuoteExpression> Values) : QuoteExpression;

/// <summary>
/// A null quote expression.
/// </summary>
internal sealed record QuoteNull : QuoteExpression;

/// <summary>
/// A token kind quote expression.
/// </summary>
/// <param name="Value">The token kind.</param>
internal sealed record QuoteTokenKind(TokenKind Value) : QuoteExpression;

/// <summary>
/// An integer quote expression.
/// </summary>
/// <param name="Value">The integer value.</param>
internal sealed record QuoteInteger(int Value) : QuoteExpression;

/// <summary>
/// A float quote expression.
/// </summary>
/// <param name="Value">The float value.</param>
internal sealed record QuoteFloat(float Value) : QuoteExpression;

/// <summary>
/// A boolean quote expression.
/// </summary>
/// <param name="Value">The boolean value.</param>
internal sealed record QuoteBoolean(bool Value) : QuoteExpression;

/// <summary>
/// A character quote expression.
/// </summary>
/// <param name="Value">The character value.</param>
internal sealed record QuoteCharacter(char Value) : QuoteExpression;

/// <summary>
/// A string quote expression.
/// </summary>
/// <param name="Value">The string value.</param>
internal sealed record QuoteString(string Value) : QuoteExpression;
Loading