Skip to content

Commit

Permalink
[MOREL-9] Allow '<expr>.<field>' as an alternative syntax for '#<fiel…
Browse files Browse the repository at this point in the history
…d> <expr>'
  • Loading branch information
julianhyde committed Jan 23, 2020
1 parent fda19bf commit 268d123
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 59 deletions.
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ Bugs:
* Runtime should throw when divide by zero
* Validator should give good user error when it cannot type an expression

## Postfix labels

As an extension to Standard ML, Morel allows '.' for field references.
Thus `e.deptno` is equivalent to `#deptno e`.

(Postfix labels are implemented as syntactic sugar; both expressions
become an application of label `#deptno` to expression `e`.)

Because '.' is left-associative, it is a more convenient syntax for
chained references. In the standard syntax, `e.address.zipcode` would
be written `#zipcode (#address e)`.

The following relational examples use postfix labels, but the syntax
is available in any Morel expression.

## Relational extensions

The `from` expression (and associated `as`, `where` and `yield` keywords)
Expand All @@ -150,7 +165,7 @@ val depts =
the expression

```
from e in emps where (#deptno e = 30) yield (#id e)
from e in emps where e.deptno = 30 yield e.id
```

is equivalent to standard ML
Expand All @@ -176,8 +191,8 @@ a join or a cartesian product:

```
from e in emps, d in depts
where (#deptno e) = (#deptno d)
yield {id = (#id e), deptno = (#deptno e), ename = (#name e), dname = (#name d)};
where e.deptno = d.deptno
yield {id = e.id, deptno = e.deptno, ename = e.name, dname = d.name};
```

As in any ML expression, you can define functions within a `from` expression,
Expand All @@ -190,10 +205,10 @@ let
| in_ e (h :: t) = e = h orelse (in_ e t)
in
from e in emps
where in_ (#deptno e) (from d in depts
where (#name d) = "Engineering"
yield (#deptno d))
yield (#name e)
where in_ e.deptno (from d in depts
where d.name = "Engineering"
yield d.deptno)
yield e.name
end
let
Expand All @@ -202,9 +217,9 @@ let
in
from e in emps
where exists (from d in depts
where (#deptno d) = (#deptno e)
andalso (#name d) = "Engineering")
yield (#name e)
where d.deptno = e.deptno
andalso d.name = "Engineering")
yield e.name
end
```

Expand Down
28 changes: 25 additions & 3 deletions src/main/javacc/MorelParser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,12 @@ Match match() :

/** Parses an expression.
*
* <p>8 is the highest level of precedence. The full list is as follows:
* <p>8 is the highest level of precedence in standard ML,
and the '.field' extension is at level 9.
The full list is as follows:
*
* <ul>
* <li>infix 9 {@code .}
* <li>infix 8 (application)
* <li>infix 7 {@code * / div mod}
* <li>infix 6 {@code + - ^}
Expand All @@ -429,21 +433,39 @@ Match match() :
* <li>infix 0 {@code before}
* </ul>
*/
Exp expression9() :
{
Exp e;
Id id;
}
{
e = atom()
(
<DOT> id = identifier() {
final Exp s = ast.recordSelector(pos(), id.name);
e = ast.apply(s, e);
}
)*
{ return e; }
}

/** Parses an expression of precedence level 8 (function application). */
Exp expression8() :
{
Exp e;
Exp e2;
}
{
e = atom()
e = expression9()
(
e2 = atom() {
e2 = expression9() {
e = ast.apply(e, e2);
}
)*
{ return e; }
}

/** Parses an expression of precedence level 7 (*, /, div, mod). */
Exp expression7() :
{
Exp e;
Expand Down
49 changes: 49 additions & 0 deletions src/test/java/net/hydromatic/morel/MainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import net.hydromatic.morel.ast.Ast;
import net.hydromatic.morel.ast.AstNode;
import net.hydromatic.morel.parse.ParseException;
import net.hydromatic.morel.type.TypeVar;

import org.hamcrest.CustomTypeSafeMatcher;
Expand All @@ -46,6 +47,7 @@
import static net.hydromatic.morel.Ml.assertError;
import static net.hydromatic.morel.Ml.ml;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
Expand Down Expand Up @@ -272,6 +274,37 @@ public void describeTo(Description description) {
+ "3 *) 5 + 6").assertParse("5 + 6");
}

/** Tests that the syntactic sugar "exp.field" is de-sugared to
* "#field exp". */
@Test public void testParseDot() {
ml("a . b")
.assertParse("#b a");
ml("a . b . c")
.assertParse("#c (#b a)");
ml("a . b + c . d")
.assertParse("#b a + #d c");
ml("a.b+c.d")
.assertParse("#b a + #d c");
ml("(a.b+c.d*e.f.g).h")
.assertParse("#h (#b a + #d c * #g (#f e))");
ml("a b")
.assertParse("a b");
ml("a b.c")
.assertParse("a (#c b)");
ml("a.b c.d e.f")
.assertParse("#b a (#d c) (#f e)");
ml("(a.b) (c.d) (e.f)")
.assertParse("#b a (#d c) (#f e)");
ml("(a.(b (c.d) (e.f))")
.assertParseThrows(
throwsA(ParseException.class,
containsString("Encountered \"(\" at line 1, column 4.")));
ml("(a.b c.(d (e.f)))")
.assertParseThrows(
throwsA(ParseException.class,
containsString("Encountered \"(\" at line 1, column 8.")));
}

/** Tests the name of {@link TypeVar}. */
@Test public void testTypeVarName() {
assertError(() -> new TypeVar(-1).description(),
Expand Down Expand Up @@ -1239,6 +1272,22 @@ private <T extends Throwable> Matcher<Throwable> throwsA(Class<T> clazz,
list("ACCOUNTING", 7934))));
}

/** As {@link #testScottJoin2()} but using dot notation ('e.field' rather
* than '#field e'). */
@Test public void testScottJoin2Dot() {
final String ml = "from e in scott.emp, d in scott.dept\n"
+ " where e.deptno = d.deptno\n"
+ " andalso e.empno >= 7900\n"
+ " yield {empno = e.empno, dname = d.dname}\n";
ml(ml)
.withBinding("scott", DataSet.SCOTT.foreignValue())
.assertType("{dname:string, empno:int} list")
.assertEval(
is(
list(list("SALES", 7900), list("RESEARCH", 7902),
list("ACCOUNTING", 7934))));
}

@Test public void testError() {
ml("fn x y => x + y")
.assertError(
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/net/hydromatic/morel/Ml.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ Ml assertParseSame() {
return assertParse(ml.replaceAll("[\n ]+", " "));
}

Ml assertParseThrows(Matcher<Throwable> matcher) {
try {
final AstNode statement =
new MorelParserImpl(new StringReader(ml)).statement();
fail("expected error, got " + statement);
} catch (Throwable e) {
assertThat(e, matcher);
}
return this;
}

private Ml withValidate(BiConsumer<Ast.Exp, TypeResolver.TypeMap> action) {
return withParser(parser -> {
try {
Expand Down
66 changes: 38 additions & 28 deletions src/test/resources/script/relational.sml
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,38 @@ from e in emps yield #id e;

from e in emps yield (#id e) - 100;

from e in emps yield e.id - 100;

from e in emps yield #deptno e;

from e in emps yield e.deptno;

from e in emps yield {deptno = #deptno e, one = 1};

from e in emps yield {deptno = e.deptno, one = 1};

from e in emps yield ((#id e) + (#deptno e));

from e in emps yield (e.id + e.deptno);

from e2 in (from e in emps yield #deptno e) yield e2 + 1;

from e2 in (from e in emps yield e.deptno) yield e2 + 1;

(* Disabled: '=' should have lower precedence than '#deptno e' fun application
from e in emps where #deptno e = 30 yield #name e;
*)

from e in emps where false yield (#deptno e);
from e in emps where false yield e.deptno;

(*) Function defined inside query
from e in emps
where #deptno e < 30
where e.deptno < 30
yield
let
fun p1 x = x + 1
in
p1 (#id e)
p1 e.id
end;
(* Disabled due to CCE
Expand All @@ -90,18 +100,18 @@ from i in integers where i mod 2 = 1 yield i;
(*) missing yield
from i in integers where i mod 2 = 1;
from e in emps where (#deptno e) = 30 yield (#id e);
from e in emps where e.deptno = 30 yield e.id;
(*) cartesian product
from e in emps, e2 in emps yield (#name e) ^ "-" ^ (#name e2);
from e in emps, e2 in emps yield e.name ^ "-" ^ e2.name;
(*) cartesian product, missing yield
from d in depts, i in integers;
(*) join
from e in emps, d in depts
where (#deptno e) = (#deptno d)
yield {id = (#id e), deptno = (#deptno e), ename = (#name e), dname = (#name d)};
where e.deptno = d.deptno
yield {id = e.id, deptno = e.deptno, ename = e.name, dname = d.name};
(*) exists (defining the "exists" function ourselves)
(*) and correlated sub-query
Expand All @@ -112,9 +122,9 @@ let
in
from e in emps
where exists (from d in depts
where (#deptno d) = (#deptno e)
andalso (#name d) = "Engineering")
yield (#name e)
where d.deptno = e.deptno
andalso d.name = "Engineering")
yield e.name
end;
val it = ["Shaggy","Scooby"] : string list
*)
Expand All @@ -126,10 +136,10 @@ let
| in_ e (h :: t) = e = h orelse (in_ e t)
in
from e in emps
where in_ (#deptno e) (from d in depts
where (#name d) = "Engineering"
yield (#deptno d))
yield (#name e)
where in_ e.deptno (from d in depts
where d.name = "Engineering"
yield d.deptno)
yield e.name
end;
val it = ["Shaggy","Scooby"] : string list
*)
Expand Down Expand Up @@ -161,8 +171,8 @@ end;
(*) Basic 'group'
from e in emps
group (#deptno e) as deptno
compute sum of (#id e) as sumId,
group e.deptno as deptno
compute sum of e.id as sumId,
count of e as count;
(*
Expand All @@ -173,29 +183,29 @@ val it =
(*) 'group' with no aggregates
from e in emps
group (#deptno e) as deptno;
group e.deptno as deptno;
(*) composite 'group' with no aggregates
from e in emps
group (#deptno e) as deptno, (#id e) mod 2 as idMod2;
group e.deptno as deptno, e.id mod 2 as idMod2;
(*) 'group' with 'where' and complex argument to 'sum'
(*
from e in emps
where (#deptno e) < 30
group (#deptno e) as deptno
compute sum of (#id e) as sumId,
min of (#id e) + (#deptno e) as minIdPlusDeptno;
where e.deptno < 30
group e.deptno as deptno
compute sum of e.id as sumId,
min of e.id + e.deptno as minIdPlusDeptno;
val it = [{deptno=10,id=100,name="Fred"},{deptno=20,id=101,name="Velma"}] : {deptno:int, id:int, name:string} list
*)
(*) 'group' with join
(*
from e in emps, d in depts
where (#deptno e) = (#deptno d)
group (#deptno e) as deptno,
(#name e) as dname
compute sum of (#id e) as sumId;
where e.deptno = d.deptno
group e.deptno as deptno,
e.name as dname
compute sum of e.id as sumId;
val it =
[{d={deptno=10,name="Sales"},e={deptno=10,id=100,name="Fred"}},
{d={deptno=20,name="HR"},e={deptno=20,id=101,name="Velma"}},
Expand All @@ -206,7 +216,7 @@ val it =
(*) empty 'group'
(*
from e in emps
group compute sum of (#id e) as sumId;
group compute sum of e.id as sumId;
val it =
[{deptno=10,id=100,name="Fred"},{deptno=20,id=101,name="Velma"},
{deptno=30,id=102,name="Shaggy"},{deptno=30,id=103,name="Scooby"}] : {deptno:int, id:int, name:string} list
Expand All @@ -217,7 +227,7 @@ val it =
(*
from emps as e
group (#deptno e) as deptno
compute sum of (#id e) as sumId,
compute sum of e.id as sumId,
count of e as count
as g
yield {deptno = (#deptno g), avgId = (#sumId g) / (#count g)}
Expand Down
Loading

0 comments on commit 268d123

Please sign in to comment.