From b1be8c0faa2c2e798424a3d357c9bce8755f9052 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Fri, 1 Dec 2023 11:48:37 +0100 Subject: [PATCH] Multi line chained operator syntax (#8415) --- .../java/org/enso/compiler/core/TreeToIr.java | 87 ++++++++++++++----- .../enso/compiler/core/EnsoParserTest.java | 56 ++++++++++++ .../org/enso/compiler/ExecCompilerTest.java | 29 +++++++ 3 files changed, 152 insertions(+), 20 deletions(-) diff --git a/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java b/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java index 38f0398ee34..74eb9f34bc8 100644 --- a/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java +++ b/engine/runtime-parser/src/main/java/org/enso/compiler/core/TreeToIr.java @@ -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(); 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 items = nil(); if (arr.getFirst() != null) { @@ -998,6 +1026,21 @@ final class TreeToIr { }; } + private Operator applyOperator(Token.Operator op, CallArgument lhs, CallArgument rhs, Option 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) { diff --git a/engine/runtime-parser/src/test/java/org/enso/compiler/core/EnsoParserTest.java b/engine/runtime-parser/src/test/java/org/enso/compiler/core/EnsoParserTest.java index 4f5f987770a..254577a4541 100644 --- a/engine/runtime-parser/src/test/java/org/enso/compiler/core/EnsoParserTest.java +++ b/engine/runtime-parser/src/test/java/org/enso/compiler/core/EnsoParserTest.java @@ -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 moduleCodes = List.of( diff --git a/engine/runtime/src/test/java/org/enso/compiler/ExecCompilerTest.java b/engine/runtime/src/test/java/org/enso/compiler/ExecCompilerTest.java index 27353b5519b..73e3efc3e7c 100644 --- a/engine/runtime/src/test/java/org/enso/compiler/ExecCompilerTest.java +++ b/engine/runtime/src/test/java/org/enso/compiler/ExecCompilerTest.java @@ -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 =