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
131 changes: 131 additions & 0 deletions bindings/csharp/Expressif.Syntax.Tests/SyntaxBindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,132 @@ public void ParenthesizedPipelineIsLossless()
});
}

[Test]
public void UnaryShorthandPreservesOperatorOperandAndAuthoredSource()
{
const string source = " ! less-than(5) ";
var root = (OpenExpressionSyntax)ExpressifSyntax.Parse(source);
var unary = (UnaryExpressionSyntax)root.Pipeline.Single();

Assert.Multiple(() =>
{
Assert.That(unary.Kind, Is.EqualTo(SyntaxKind.UnaryExpression));
Assert.That(unary.Operator.Kind, Is.EqualTo(SyntaxKind.UnaryOperator));
Assert.That(unary.Operator.Text, Is.EqualTo("!"));
Assert.That(unary.Operator.Span, Is.EqualTo(new SourceSpan(1, 1)));
Assert.That(unary.Operand, Is.TypeOf<FunctionCallSyntax>()
.With.Property(nameof(FunctionCallSyntax.Name)).EqualTo("less-than"));
Assert.That(unary.Children, Is.EqualTo(new SyntaxNode[] { unary.Operator, unary.Operand }));
Assert.That(unary.Text, Is.EqualTo("! less-than(5)"));
Assert.That(unary.Span, Is.EqualTo(new SourceSpan(1, 14)));
});
}

[Test]
public void RepeatedUnaryShorthandRemainsNestedSyntax()
{
var outer = (UnaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse("!!is-null")).Pipeline.Single();
var inner = (UnaryExpressionSyntax)outer.Operand;

Assert.Multiple(() =>
{
Assert.That(inner.Operand, Is.TypeOf<FunctionCallSyntax>()
.With.Property(nameof(FunctionCallSyntax.Name)).EqualTo("is-null"));
Assert.That(outer.Text, Is.EqualTo("!!is-null"));
Assert.That(inner.Text, Is.EqualTo("!is-null"));
});
}

[TestCase("|AND")]
[TestCase("|OR")]
[TestCase("|XOR")]
public void BinaryShorthandPreservesOperatorAndOperands(string operatorText)
{
var source = $"foo {operatorText} bar";
var binary = (BinaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse(source)).Pipeline.Single();

Assert.Multiple(() =>
{
Assert.That(binary.Kind, Is.EqualTo(SyntaxKind.BinaryExpression));
Assert.That(binary.Left, Is.TypeOf<FunctionCallSyntax>()
.With.Property(nameof(FunctionCallSyntax.Name)).EqualTo("foo"));
Assert.That(binary.Operator.Kind, Is.EqualTo(SyntaxKind.BinaryOperator));
Assert.That(binary.Operator.Text, Is.EqualTo(operatorText));
Assert.That(binary.Right, Is.TypeOf<FunctionCallSyntax>()
.With.Property(nameof(FunctionCallSyntax.Name)).EqualTo("bar"));
Assert.That(binary.Children, Is.EqualTo(new SyntaxNode[] { binary.Left, binary.Operator, binary.Right }));
Assert.That(binary.Text, Is.EqualTo(source));
Assert.That(binary.Span, Is.EqualTo(new SourceSpan(0, source.Length)));
});
}

[Test]
public void BinaryShorthandChainsLeftAssociatively()
{
var outer = (BinaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse("a |AND b |OR c")).Pipeline.Single();
var left = (BinaryExpressionSyntax)outer.Left;

Assert.Multiple(() =>
{
Assert.That(left.Operator.Text, Is.EqualTo("|AND"));
Assert.That(outer.Operator.Text, Is.EqualTo("|OR"));
Assert.That(((FunctionCallSyntax)outer.Right).Name, Is.EqualTo("c"));
});
}

[Test]
public void BinaryShorthandAcceptsGenericExpressionOperands()
{
var binary = (BinaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse("$0 |AND .active")).Pipeline.Single();

Assert.Multiple(() =>
{
Assert.That(binary.Left, Is.TypeOf<TupleProjectionSyntax>());
Assert.That(binary.Right, Is.TypeOf<RecordAccessSyntax>());
});
}

[Test]
public void UnaryShorthandBindsMoreTightlyThanBinaryShorthand()
{
var binary = (BinaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse("!foo |AND bar")).Pipeline.Single();

Assert.That(binary.Left, Is.TypeOf<UnaryExpressionSyntax>());
}

[Test]
public void ParenthesesGroupShorthandWithoutLoweringItToFunctions()
{
var outer = (BinaryExpressionSyntax)((OpenExpressionSyntax)ExpressifSyntax.Parse("(a |OR b) |AND c")).Pipeline.Single();
var parenthesized = (ParenthesizedExpressionSyntax)outer.Left;
var innerRoot = (OpenExpressionSyntax)parenthesized.Expression;
var inner = (BinaryExpressionSyntax)innerRoot.Pipeline.Single();

Assert.Multiple(() =>
{
Assert.That(parenthesized.Text, Is.EqualTo("(a |OR b)"));
Assert.That(inner.Operator.Text, Is.EqualTo("|OR"));
Assert.That(outer.Operator.Text, Is.EqualTo("|AND"));
Assert.That(innerRoot.Pipeline, Has.Count.EqualTo(1));
});
}

[Test]
public void ShorthandComposesAsAnArgumentAndWithAnOrdinaryPipeline()
{
var argumentRoot = (OpenExpressionSyntax)ExpressifSyntax.Parse("foo(!less-than(5))");
var pipelineRoot = (OpenExpressionSyntax)ExpressifSyntax.Parse("foo |AND bar | some-function");

Assert.Multiple(() =>
{
var foo = (FunctionCallSyntax)argumentRoot.Pipeline.Single();
Assert.That(foo.Arguments.Single().Value, Is.TypeOf<UnaryExpressionSyntax>());
Assert.That(pipelineRoot.Pipeline[0], Is.TypeOf<BinaryExpressionSyntax>());
Assert.That(pipelineRoot.Pipeline[1], Is.TypeOf<FunctionCallSyntax>()
.With.Property(nameof(FunctionCallSyntax.Name)).EqualTo("some-function"));
});
}

[Test]
public void ParenthesizedPipelineComposesAsAnArgumentAndPipelineOperation()
{
Expand Down Expand Up @@ -884,6 +1010,11 @@ public void BareDateLookingIntervalBoundsAreRejected()
[TestCase("foo(name :=)", false)]
[TestCase("foo(, 5)", false)]
[TestCase("foo(5,,6)", false)]
[TestCase("!", false)]
[TestCase("foo |AND", true)]
[TestCase("|AND foo", false)]
[TestCase("foo |BAD bar", false)]
[TestCase("(foo |AND bar", false)]
[TestCase("....", false)]
[TestCase("{ ..., }", false)]
public void MalformedInputExposesTreeSitterErrors(string source, bool hasMissingError)
Expand Down
26 changes: 26 additions & 0 deletions bindings/csharp/Expressif.Syntax/ExpressifSyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,30 @@ private static MapShorthandSyntax BindMapShorthand(TsNode node)
return new(Span(node), node.Text, BindOpen(expression));
}

private static UnaryExpressionSyntax BindUnaryExpression(TsNode node)
{
var @operator = node.GetChildForField("operator") ?? throw Unknown(node);
var operand = node.GetChildForField("operand") ?? throw Unknown(node);
return new(
Span(node),
node.Text,
new UnaryOperatorSyntax(Span(@operator), @operator.Text),
BindExpression(operand));
}

private static BinaryExpressionSyntax BindBinaryExpression(TsNode node)
{
var left = node.GetChildForField("left") ?? throw Unknown(node);
var @operator = node.GetChildForField("operator") ?? throw Unknown(node);
var right = node.GetChildForField("right") ?? throw Unknown(node);
return new(
Span(node),
node.Text,
BindExpression(left),
new BinaryOperatorSyntax(Span(@operator), @operator.Text),
BindExpression(right));
}

private static RootExpressionSyntax BindRootExpression(TsNode node)
{
if (node.Type == "root_expression")
Expand All @@ -132,12 +156,14 @@ private static RootExpressionSyntax BindRootExpression(TsNode node)
private static ExpressionSyntax BindExpression(TsNode node) => node.Type switch
{
"closed_expression" => BindClosed(node),
"binary_expression" => BindBinaryExpression(node),
"function_call" => BindFunctionCall(node),
"map_shorthand" => BindMapShorthand(node),
"open_expression" => BindOpen(node),
"parameterized_expression" => BindParameterizedExpression(node),
"parenthesized_expression" => BindParenthesizedExpression(node),
"tuple_projection" => BindTupleProjection(node),
"unary_expression" => BindUnaryExpression(node),
_ => BindValue(node),
};

Expand Down
53 changes: 53 additions & 0 deletions bindings/csharp/Expressif.Syntax/SyntaxNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public enum SyntaxKind
ParenthesizedExpression,
IntervalLiteral,
MapShorthand,
UnaryExpression,
UnaryOperator,
BinaryExpression,
BinaryOperator,
}

public readonly record struct SourceSpan(int Start, int Length)
Expand Down Expand Up @@ -140,6 +144,55 @@ internal MapShorthandSyntax(SourceSpan span, string text, OpenExpressionSyntax e
public OpenExpressionSyntax Expression { get; }
}

public sealed class UnaryOperatorSyntax : SyntaxNode
{
internal UnaryOperatorSyntax(SourceSpan span, string text)
: base(SyntaxKind.UnaryOperator, span, text) { }
}

public sealed class UnaryExpressionSyntax : ExpressionSyntax
{
internal UnaryExpressionSyntax(
SourceSpan span,
string text,
UnaryOperatorSyntax @operator,
ExpressionSyntax operand)
: base(SyntaxKind.UnaryExpression, span, text, [@operator, operand])
{
Operator = @operator;
Operand = operand;
}

public UnaryOperatorSyntax Operator { get; }
public ExpressionSyntax Operand { get; }
}

public sealed class BinaryOperatorSyntax : SyntaxNode
{
internal BinaryOperatorSyntax(SourceSpan span, string text)
: base(SyntaxKind.BinaryOperator, span, text) { }
}

public sealed class BinaryExpressionSyntax : ExpressionSyntax
{
internal BinaryExpressionSyntax(
SourceSpan span,
string text,
ExpressionSyntax left,
BinaryOperatorSyntax @operator,
ExpressionSyntax right)
: base(SyntaxKind.BinaryExpression, span, text, [left, @operator, right])
{
Left = left;
Operator = @operator;
Right = right;
}

public ExpressionSyntax Left { get; }
public BinaryOperatorSyntax Operator { get; }
public ExpressionSyntax Right { get; }
}

public abstract class ArgumentSyntax : SyntaxNode
{
protected ArgumentSyntax(SyntaxKind kind, SourceSpan span, string text, IEnumerable<SyntaxNode> children)
Expand Down
38 changes: 34 additions & 4 deletions grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
/// <reference types="tree-sitter-cli/dsl" />
// @ts-check

const ordinaryExpression = ($) => choice(
$.function_call,
$.map_shorthand,
$.tuple_projection,
$.parenthesized_expression,
);

export default grammar({
name: "expressif",

Expand All @@ -22,6 +29,7 @@ export default grammar({
conflicts: ($) => [
[$.record_spread, $.incoming_value],
[$.expression, $._pipeline_expression],
[$.expression, $._shorthand_operand],
[$.root_expression, $._parenthesized_pipeline_expression],
],

Expand Down Expand Up @@ -66,10 +74,30 @@ export default grammar({
),

expression: ($) => choice(
$.function_call,
$.map_shorthand,
$.tuple_projection,
$.parenthesized_expression,
$.binary_expression,
$.unary_expression,
ordinaryExpression($),
),

binary_expression: ($) => prec.left(1, seq(
field("left", choice($.binary_expression, $._shorthand_operand)),
field("operator", $.binary_operator),
field("right", $._shorthand_operand),
)),

binary_operator: (_) => choice("|AND", "|OR", "|XOR"),

unary_expression: ($) => prec.right(2, seq(
field("operator", $.unary_operator),
field("operand", choice($.unary_expression, ordinaryExpression($), $.value)),
)),

unary_operator: (_) => "!",

_shorthand_operand: ($) => choice(
$.unary_expression,
ordinaryExpression($),
$.value,
),

_pipeline_expression: ($) => choice(
Expand Down Expand Up @@ -149,6 +177,8 @@ export default grammar({
),

_argument_value: ($) => choice(
$.binary_expression,
$.unary_expression,
alias($._nested_closed_expression, $.closed_expression),
$.value,
$.tuple_projection,
Expand Down
Loading
Loading