Multi line chained operator syntax (#8415)

This commit is contained in:
Jaroslav Tulach 2023-12-01 11:48:37 +01:00 committed by GitHub
parent 65daaf6f0c
commit b1be8c0faa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 152 additions and 20 deletions

View File

@ -552,7 +552,7 @@ final class TreeToIr {
method = buildName(id);
loc = getIdentifiedLocation(sig);
}
case Tree.OprApp app when ".".equals(app.getOpr().getRight().codeRepr()) -> {
case Tree.OprApp app when isDotOperator(app.getOpr().getRight()) -> {
type = Option.apply(buildQualifiedName(app.getLhs()));
method = buildName(app.getRhs());
if (alwaysLocation) {
@ -568,7 +568,7 @@ final class TreeToIr {
);
}
private Expression translateCall(Tree ast) {
private Expression translateCall(Tree ast, boolean isMethod) {
var args = new java.util.ArrayList<CallArgument>();
var hasDefaultsSuspended = false;
var tree = ast;
@ -597,25 +597,48 @@ final class TreeToIr {
args.add(new CallArgument.Specified(Option.empty(), expr, loc, meta(), diag()));
tree = app.getFunc();
}
case Tree.OperatorBlockApplication app -> {
var at = args.size();
var self = translateExpression(app.getLhs(), false);
for (var l : app.getExpressions()) {
var invoke = isDotOperator(l.getExpression().getOperator().getRight());
if (self == null || !invoke) {
return null;
}
var expr = switch (translateExpression(l.getExpression().getExpression(), true)) {
case Application.Prefix pref -> {
var arg = new CallArgument.Specified(Option.empty(), self, self.location(), meta(), diag());
yield new Application.Prefix(pref.function(), join(arg, pref.arguments()), false, pref.location(), meta(), diag());
}
case Expression any -> {
var arg = new CallArgument.Specified(Option.empty(), self, self.location(), meta(), diag());
yield new Application.Prefix(any, join(arg, nil()), false, any.location(), meta(), diag());
}
};
var loc = getIdentifiedLocation(l.getExpression().getExpression());
args.add(at, new CallArgument.Specified(Option.empty(), expr, loc, meta(), diag()));
self = expr;
}
return self;
}
default -> {
Expression func;
if (tree instanceof Tree.OprApp oprApp
&& oprApp.getOpr().getRight() != null
&& ".".equals(oprApp.getOpr().getRight().codeRepr())
&& isDotOperator(oprApp.getOpr().getRight())
&& oprApp.getRhs() instanceof Tree.Ident) {
func = translateExpression(oprApp.getRhs(), true);
if (oprApp.getLhs() == null && args.isEmpty()) {
return func;
}
if (oprApp.getLhs() != null) {
var self = translateExpression(oprApp.getLhs(), false);
var self = translateExpression(oprApp.getLhs(), isMethod);
var loc = getIdentifiedLocation(oprApp.getLhs());
args.add(new CallArgument.Specified(Option.empty(), self, loc, meta(), diag()));
}
} else if (args.isEmpty()) {
return null;
} else {
func = translateExpression(tree, false);
func = translateExpression(tree, isMethod);
}
java.util.Collections.reverse(args);
var argsList = CollectionConverters.asScala(args.iterator()).toList();
@ -682,7 +705,7 @@ final class TreeToIr {
if (tree == null) {
return null;
}
var callExpression = translateCall(tree);
var callExpression = translateCall(tree, isMethod);
if (callExpression != null) {
return callExpression;
}
@ -750,22 +773,27 @@ final class TreeToIr {
default -> {
var lhs = unnamedCallArgument(app.getLhs());
var rhs = unnamedCallArgument(app.getRhs());
var name = new Name.Literal(
op.codeRepr(), true, getIdentifiedLocation(op), Option.empty(), meta(), diag()
);
var loc = getIdentifiedLocation(app);
if (lhs == null && rhs == null) {
yield new Section.Sides(name, loc, meta(), diag());
} else if (lhs == null) {
yield new Section.Right(name, rhs, loc, meta(), diag());
} else if (rhs == null) {
yield new Section.Left(lhs, name, loc, meta(), diag());
} else {
yield new Operator.Binary(lhs, name, rhs, loc,meta(), diag());
}
yield applyOperator(op, lhs, rhs, loc);
}
};
}
case Tree.OperatorBlockApplication app -> {
Expression expr = null;
var lhs = unnamedCallArgument(app.getLhs());
for (var l : app.getExpressions()) {
var op = l.getExpression().getOperator().getRight();
if (op == null || isDotOperator(op)) {
yield translateSyntaxError(l.getExpression().getExpression(), Syntax.UnexpectedExpression$.MODULE$);
}
var rhs = unnamedCallArgument(l.getExpression().getExpression());
var loc = getIdentifiedLocation(app);
var both = applyOperator(op, lhs, rhs, loc);
expr = both;
lhs = new CallArgument.Specified(Option.empty(), expr, loc, meta(), diag());
}
yield expr;
}
case Tree.Array arr -> {
List<Expression> items = nil();
if (arr.getFirst() != null) {
@ -998,6 +1026,21 @@ final class TreeToIr {
};
}
private Operator applyOperator(Token.Operator op, CallArgument lhs, CallArgument rhs, Option<IdentifiedLocation> loc) {
var name = new Name.Literal(
op.codeRepr(), true, getIdentifiedLocation(op), Option.empty(), meta(), diag()
);
if (lhs == null && rhs == null) {
return new Section.Sides(name, loc, meta(), diag());
} else if (lhs == null) {
return new Section.Right(name, rhs, loc, meta(), diag());
} else if (rhs == null) {
return new Section.Left(lhs, name, loc, meta(), diag());
} else {
return new Operator.Binary(lhs, name, rhs, loc,meta(), diag());
}
}
Tree applySkip(Tree tree) {
// Termination:
// Every iteration either breaks, or reduces [`tree`] to a substructure of [`tree`].
@ -1360,7 +1403,7 @@ final class TreeToIr {
);
}
case Tree.Ident id -> new Pattern.Name(buildName(id), getIdentifiedLocation(id), meta(), diag());
case Tree.OprApp app when ".".equals(app.getOpr().getRight().codeRepr()) -> {
case Tree.OprApp app when isDotOperator(app.getOpr().getRight()) -> {
var qualifiedName = buildQualifiedName(app);
yield new Pattern.Constructor(
qualifiedName, fields, getIdentifiedLocation(app), meta(), diag()
@ -1795,6 +1838,10 @@ final class TreeToIr {
return scala.collection.immutable.$colon$colon$.MODULE$.apply(head, tail);
}
private static boolean isDotOperator(Token.Operator op) {
return op != null && ".".equals(op.codeRepr());
}
private static Tree maybeManyParensed(Tree t) {
for (;;) {
switch (t) {

View File

@ -1245,6 +1245,62 @@ public class EnsoParserTest {
equivalenceTest("a = x", "a = SKIP FREEZE x.f y");
}
@Test
public void testBlockSyntax() throws Exception {
equivalenceTest("""
nums v fm ff n = v . map fm . filter ff . take n
""", """
nums v fm ff n = v
. map fm
. filter ff
. take n
""");
}
@Test
public void testBlockSyntaxOperators() throws Exception {
equivalenceTest("""
value = nums * each random + constant
""", """
value = nums
* each random
+ constant
""");
}
@Test
public void testBlockSyntaxOperators2() throws Exception {
equivalenceTest("""
value = (nums + each random) * constant
""", """
value = nums
+ each random
* constant
""");
}
@Test
public void testBlockSyntaxOperators3() throws Exception {
equivalenceTest("""
v = (rect1 . width) . center
""", """
v = rect1
. width
. center
""");
}
@Test
public void testBlockSyntaxOperators4() throws Exception {
equivalenceTest("""
v = (rect1 . width 4) . center 3 2
""", """
v = rect1
. width 4
. center 3 2
""");
}
@Test
public void testPrivateModules() throws Exception {
List<String> moduleCodes = List.of(

View File

@ -126,6 +126,35 @@ public class ExecCompilerTest {
}
}
@Test
public void chainedSyntax() throws Exception {
var module = ctx.eval("enso", """
from Standard.Base import all
nums n = [1, 2, 3, 4, 5]
. map (x-> x*2)
. filter (x-> x % 3 == 0)
. take n
""");
var run = module.invokeMember("eval_expression", "nums");
var six = run.execute(1);
assertTrue(six.hasArrayElements());
assertEquals(1, six.getArraySize());
assertEquals(6, six.getArrayElement(0).asInt());
}
@Test
public void chainedSyntaxOperator() throws Exception {
var module = ctx.eval("enso", """
nums n = n
* 2
% 3
""");
var run = module.invokeMember("eval_expression", "nums");
var result = run.execute(5);
assertEquals("10 % 3 is one", 1, result.asInt());
}
@Test
public void testInvalidEnsoProjectRef() throws Exception {
var module =