Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<ProjectReference Include="..\Expressif.Syntax\Expressif.Syntax.csproj" />
<Content Include="..\..\..\src\node-types.json" Link="node-types.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<Target Name="CopyExpressifGrammar" AfterTargets="Build" Condition="'$(TargetFramework)' != ''">
Expand Down
66 changes: 66 additions & 0 deletions bindings/csharp/Expressif.Syntax.Tests/SyntaxBindingTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Expressif.Syntax.Tests;

using System.Text.Json;

public class SyntaxBindingTests
{
[TestCase("lower", typeof(OpenExpressionSyntax))]
Expand Down Expand Up @@ -111,6 +113,70 @@ public void PositionalElementAccessCanBeAnArgumentAndPipelineSource()
});
}

[TestCase(".name", false, "name", null)]
[TestCase(".10", false, null, 10)]
[TestCase("^.name", true, "name", null)]
[TestCase("^.10", true, null, 10)]
public void RecordAccessExposesRootAndField(string source, bool original, string? name, int? index)
{
var root = (ClosedExpressionSyntax)ExpressifSyntax.Parse(source);
var access = (RecordAccessSyntax)root.Value;

Assert.Multiple(() =>
{
Assert.That(access.IsOriginalInput, Is.EqualTo(original));
Assert.That(access.Fields.Single().Name, Is.EqualTo(name));
Assert.That(access.Fields.Single().Index, Is.EqualTo(index));
});
}

[Test]
public void NestedRecordAccessPreservesEveryField()
{
var access = (RecordAccessSyntax)((ClosedExpressionSyntax)ExpressifSyntax.Parse("^.customer.0")).Value;

Assert.That(access.Fields, Is.EqualTo(new[]
{
new RecordFieldSelector("customer", null),
new RecordFieldSelector(null, 0),
}));
}

[TestCase("@value", typeof(VariableSyntax))]
[TestCase("{}", typeof(ArrayLiteralSyntax))]
[TestCase("T(1, 2)", typeof(TupleLiteralSyntax))]
[TestCase("{:}", typeof(RecordLiteralSyntax))]
public void RemainingGrammarValuesHaveManagedSyntaxNodes(string source, Type expected)
=> Assert.That(((ClosedExpressionSyntax)ExpressifSyntax.Parse(source)).Value, Is.TypeOf(expected));

[Test]
public void CompoundValuesBindNestedValuesAndRecordFields()
{
var record = (RecordLiteralSyntax)((ClosedExpressionSyntax)ExpressifSyntax.Parse(
"{name := @value, `scores` := {1, ^.total}}")).Value;

Assert.Multiple(() =>
{
Assert.That(record.Fields.Select(field => field.Name), Is.EqualTo(new[] { "name", "scores" }));
Assert.That(record.Fields[1].QuotingStyle, Is.EqualTo(QuotingStyle.Backtick));
Assert.That(record.Fields[0].Value, Is.TypeOf<VariableSyntax>());
Assert.That(((ArrayLiteralSyntax)record.Fields[1].Value).Values[1], Is.TypeOf<RecordAccessSyntax>());
Assert.That(record.Children, Is.EqualTo(record.Fields));
});
}

[Test]
public void BinderSupportsEveryGrammarValueNodeType()
{
using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "node-types.json")));
var value = document.RootElement.EnumerateArray().Single(node => node.GetProperty("type").GetString() == "value");
var grammarTypes = value.GetProperty("subtypes").EnumerateArray()
.Select(node => node.GetProperty("type").GetString()!)
.ToHashSet();

Assert.That(ExpressifSyntax.SupportedValueNodeTypes, Is.EquivalentTo(grammarTypes));
}

[Test]
public void SourceTextAndRangesAreLossless()
{
Expand Down
42 changes: 42 additions & 0 deletions bindings/csharp/Expressif.Syntax/ExpressifSyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ namespace Expressif.Syntax;

public static class ExpressifSyntax
{
internal static IReadOnlySet<string> SupportedValueNodeTypes { get; } = new HashSet<string>
{
"array_literal", "boolean_literal", "numeric_literal", "positional_element_access",
"quoted_literal", "record_access", "record_literal", "temporal_literal", "tuple_literal", "variable",
};

public static RootExpressionSyntax Parse(string source)
{
ArgumentNullException.ThrowIfNull(source);
Expand Down Expand Up @@ -77,6 +83,8 @@ private static PositionalArgumentSyntax BindArgument(TsNode node)

private static ValueSyntax BindValue(TsNode node) => node.Type switch
{
"variable" => new VariableSyntax(Span(node), node.Text),
"record_access" => BindRecordAccess(node),
"positional_element_access" => new PositionalElementAccessSyntax(Span(node), node.Text),
"numeric_literal" => new NumericLiteralSyntax(Span(node), node.Text),
"boolean_literal" => new BooleanLiteralSyntax(Span(node), node.Text),
Expand All @@ -85,10 +93,44 @@ private static PositionalArgumentSyntax BindArgument(TsNode node)
"date_literal" => new DateLiteralSyntax(Span(node), node.Text),
"date_time_literal" => new DateTimeLiteralSyntax(Span(node), node.Text),
"time_literal" => new TimeLiteralSyntax(Span(node), node.Text),
"array_literal" => new ArrayLiteralSyntax(Span(node), node.Text, node.NamedChildren.Select(BindValue).ToArray()),
"tuple_literal" => new TupleLiteralSyntax(Span(node), node.Text, node.NamedChildren.Select(BindValue).ToArray()),
"record_literal" => new RecordLiteralSyntax(Span(node), node.Text, node.NamedChildren.Select(BindRecordField).ToArray()),
"value" or "quoted_literal" or "temporal_literal" => BindValue(SingleNamedChild(node, node.Type)),
_ => throw Unknown(node),
};

private static RecordAccessSyntax BindRecordAccess(TsNode node)
{
var fields = node.NamedChildren
.Where(child => child.Type != "original_input")
.Select(selector => SingleNamedChild(selector, selector.Type))
.Select(field => field.Type switch
{
"named_record_field" => new RecordFieldSelector(field.Text.TrimStart('.'), null),
"positional_record_field" => new RecordFieldSelector(null,
int.Parse(field.Text.AsSpan(field.Text[0] == '.' ? 1 : 0), System.Globalization.CultureInfo.InvariantCulture)),
_ => throw Unknown(field),
});
return new(Span(node), node.Text, node.NamedChildren.Any(child => child.Type == "original_input"), fields);
}

private static RecordFieldSyntax BindRecordField(TsNode node)
{
var nameContainer = node.GetChildForField("name") ?? throw Unknown(node);
var name = SingleNamedChild(nameContainer, nameContainer.Type);
var value = node.GetChildForField("value") ?? throw Unknown(node);
QuotingStyle? quotingStyle = name.Type switch
{
"double_quoted_literal" => QuotingStyle.DoubleQuote,
"backtick_quoted_literal" => QuotingStyle.Backtick,
"unquoted_record_field_name" => null,
_ => throw Unknown(name),
};
var nameText = quotingStyle is null ? name.Text : name.Text[1..^1];
return new(Span(node), node.Text, nameText, quotingStyle, BindValue(value));
}

private static TsNode SingleNamedChild(TsNode node, string container)
{
var children = node.NamedChildren.ToArray();
Expand Down
82 changes: 80 additions & 2 deletions bindings/csharp/Expressif.Syntax/SyntaxNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public enum SyntaxKind
DateTimeLiteral,
TimeLiteral,
PositionalElementAccess,
Variable,
RecordAccess,
ArrayLiteral,
TupleLiteral,
RecordLiteral,
RecordField,
}

public readonly record struct SourceSpan(int Start, int Length)
Expand Down Expand Up @@ -105,8 +111,80 @@ internal PositionalArgumentSyntax(SourceSpan span, string text, ValueSyntax valu

public abstract class ValueSyntax : SyntaxNode
{
protected ValueSyntax(SyntaxKind kind, SourceSpan span, string text)
: base(kind, span, text) { }
protected ValueSyntax(SyntaxKind kind, SourceSpan span, string text, IEnumerable<SyntaxNode>? children = null)
: base(kind, span, text, children) { }
}

public sealed class VariableSyntax : ValueSyntax
{
internal VariableSyntax(SourceSpan span, string text) : base(SyntaxKind.Variable, span, text)
=> Name = text[1..];

public string Name { get; }
}

public readonly record struct RecordFieldSelector(string? Name, int? Index)
{
public bool IsNamed => Name is not null;
public bool IsPositional => Index is not null;
}

public sealed class RecordAccessSyntax : ValueSyntax
{
internal RecordAccessSyntax(SourceSpan span, string text, bool isOriginalInput, IEnumerable<RecordFieldSelector> fields)
: base(SyntaxKind.RecordAccess, span, text)
{
IsOriginalInput = isOriginalInput;
Fields = Array.AsReadOnly(fields.ToArray());
}

public bool IsOriginalInput { get; }
public IReadOnlyList<RecordFieldSelector> Fields { get; }
}

public abstract class SequenceLiteralSyntax : ValueSyntax
{
protected SequenceLiteralSyntax(SyntaxKind kind, SourceSpan span, string text, IEnumerable<ValueSyntax> values)
: base(kind, span, text, values)
=> Values = Array.AsReadOnly(values.ToArray());

public IReadOnlyList<ValueSyntax> Values { get; }
}

public sealed class ArrayLiteralSyntax : SequenceLiteralSyntax
{
internal ArrayLiteralSyntax(SourceSpan span, string text, IEnumerable<ValueSyntax> values)
: base(SyntaxKind.ArrayLiteral, span, text, values) { }
}

public sealed class TupleLiteralSyntax : SequenceLiteralSyntax
{
internal TupleLiteralSyntax(SourceSpan span, string text, IEnumerable<ValueSyntax> values)
: base(SyntaxKind.TupleLiteral, span, text, values) { }
}

public sealed class RecordLiteralSyntax : ValueSyntax
{
internal RecordLiteralSyntax(SourceSpan span, string text, IEnumerable<RecordFieldSyntax> fields)
: base(SyntaxKind.RecordLiteral, span, text, fields)
=> Fields = Array.AsReadOnly(fields.ToArray());

public IReadOnlyList<RecordFieldSyntax> Fields { get; }
}

public sealed class RecordFieldSyntax : SyntaxNode
{
internal RecordFieldSyntax(SourceSpan span, string text, string name, QuotingStyle? quotingStyle, ValueSyntax value)
: base(SyntaxKind.RecordField, span, text, [value])
{
Name = name;
QuotingStyle = quotingStyle;
Value = value;
}

public string Name { get; }
public QuotingStyle? QuotingStyle { get; }
public ValueSyntax Value { get; }
}

public sealed class PositionalElementAccessSyntax : ValueSyntax
Expand Down
Loading