Skip to content

Commit

Permalink
[MOREL-10] Implicit labels in record expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
julianhyde committed Jan 23, 2020
1 parent 268d123 commit b311d20
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 8 deletions.
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,17 @@ Bugs:
* Runtime should throw when divide by zero
* Validator should give good user error when it cannot type an expression

## Postfix labels
## Extensions

As an extension to Standard ML, Morel allows '.' for field references.
Morel has a few extensions to Standard ML: postfix labels,
implicit labels in record expressions, and relational extensions.
Postfix labels and implicit labels are intended to make relational
expressions more concise and more similar to SQL but they can be used
anywhere in Morel, not just in relational expressions.

### Postfix labels

Morel allows '.' for field references.
Thus `e.deptno` is equivalent to `#deptno e`.

(Postfix labels are implemented as syntactic sugar; both expressions
Expand All @@ -137,12 +145,24 @@ 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.
### Implicit labels in record expressions

In standard ML, a record expression is of the form
`{label1 = exp1, label2 = exp2, ...}`; in Morel, you can omit `label =`
if the expression is an identifier, label application, or field reference.

Thus
```
{#deptno e, e.name, d}
```
is short-hand for
```
{deptno = #deptno e, name = e.name, d = d}
```

## Relational extensions
### Relational extensions

The `from` expression (and associated `as`, `where` and `yield` keywords)
The `from` expression (and associated `in`, `where` and `yield` keywords)
is a language extension to support relational algebra.
It iterates over a list and generates another list.

Expand Down Expand Up @@ -192,7 +212,7 @@ a join or a cartesian product:
```
from e in emps, d in depts
where e.deptno = d.deptno
yield {id = e.id, deptno = e.deptno, ename = e.name, dname = d.name};
yield {e.id, e.deptno, ename = e.name, dname = d.name};
```

As in any ML expression, you can define functions within a `from` expression,
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/net/hydromatic/morel/ast/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ public enum AstBuilder {
// CHECKSTYLE: IGNORE 1
ast;

public String implicitLabel(Ast.Exp e) {
if (e instanceof Ast.Apply) {
final Ast.Apply apply = (Ast.Apply) e;
if (apply.fn instanceof Ast.RecordSelector) {
final Ast.RecordSelector selector = (Ast.RecordSelector) apply.fn;
return selector.name;
}
}
if (e instanceof Ast.Id) {
return ((Ast.Id) e).name;
}
throw new IllegalArgumentException("cannot derive label for expression "
+ e);
}

/** Creates a call to an infix operator. */
private Ast.InfixCall infix(Op op, Ast.Exp a0, Ast.Exp a1) {
return new Ast.InfixCall(a0.pos.plus(a1.pos), op, a0, a1);
Expand Down
6 changes: 6 additions & 0 deletions src/main/javacc/MorelParser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,14 @@ void recordExp(Map/*<String, Exp>*/ map) :
final Ast.Exp exp;
}
{
LOOKAHEAD(2)
( <NATURAL_LITERAL> | <IDENTIFIER> ) { id = token.image; }
<EQ> exp = expression() { map.put(id, exp); }
|
exp = expression() {
final String label = ast.implicitLabel(exp);
map.put(label, exp);
}
}

/** Parses a value declaration, and adds it to a list. */
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/net/hydromatic/morel/MainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ public void describeTo(Description description) {
containsString("Encountered \"(\" at line 1, column 8.")));
}

/** Tests that the abbreviated record syntax "{a, e.b, #c e, d = e}"
* is expanded to "{a = a, b = e.b, c = #c e, d = e}". */
@Test public void testParseAbbreviatedRecord() {
ml("{a, e.b, #c e, #d (e + 1), e = f + g}")
.assertParse("{a = a, b = #b e, c = #c e, d = #d (e + 1), e = f + g}");
ml("{v = a, w = e.b, x = #c e, y = #d (e + 1), z = (#f 2)}")
.assertParse("{v = a, w = #b e, x = #c e, y = #d (e + 1), z = #f 2}");
ml("{w = a = b + c, a = b + c}")
.assertParse("{a = b + c, w = a = b + c}");
ml("{1}")
.assertParseThrows(
throwsA(IllegalArgumentException.class,
is("cannot derive label for expression 1")));
ml("{a, b + c}")
.assertParseThrows(
throwsA(IllegalArgumentException.class,
is("cannot derive label for expression b + c")));
}

/** Tests the name of {@link TypeVar}. */
@Test public void testTypeVarName() {
assertError(() -> new TypeVar(-1).description(),
Expand Down
3 changes: 3 additions & 0 deletions src/test/resources/script/foreign.sml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

scott;
#dept scott;
scott.dept;
from d in scott.dept;
from d in scott.dept yield {d.dname, d.loc};
foodmart;
#days foodmart;

Expand Down
15 changes: 15 additions & 0 deletions src/test/resources/script/foreign.sml.out
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ val it = {bonus=<relation>,dept=<relation>,emp=<relation>,salgrade=<relation>} :
#dept scott;
val it = <relation> : {deptno:int, dname:string, loc:string} list

scott.dept;
val it = <relation> : {deptno:int, dname:string, loc:string} list

from d in scott.dept;
val it =
[{deptno=10,dname="ACCOUNTING",loc="NEW YORK"},
{deptno=20,dname="RESEARCH",loc="DALLAS"},
{deptno=30,dname="SALES",loc="CHICAGO"},
{deptno=40,dname="OPERATIONS",loc="BOSTON"}] : {deptno:int, dname:string, loc:string} list

from d in scott.dept yield {d.dname, d.loc};
val it =
[{dname="ACCOUNTING",loc="NEW YORK"},{dname="RESEARCH",loc="DALLAS"},
{dname="SALES",loc="CHICAGO"},{dname="OPERATIONS",loc="BOSTON"}] : {dname:string, loc:string} list

foodmart;
val it =
{account=<relation>,agg_c_10_sales_fact_1997=<relation>,agg_c_14_sales_fact_1997=
Expand Down
9 changes: 8 additions & 1 deletion src/test/resources/script/relational.sml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ 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 {e.deptno, one = 1};

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

from e in emps yield (e.id + e.deptno);
Expand Down Expand Up @@ -113,6 +115,11 @@ from e in emps, d in depts
where e.deptno = d.deptno
yield {id = e.id, deptno = e.deptno, ename = e.name, dname = d.name};
(*) as above, using abbreviated record syntax
from e in emps, d in depts
where e.deptno = d.deptno
yield {e.id, e.deptno, ename = e.name, dname = d.name};
(*) exists (defining the "exists" function ourselves)
(*) and correlated sub-query
(* disabled due to "::"
Expand Down Expand Up @@ -230,7 +237,7 @@ group (#deptno e) as deptno
compute sum of e.id as sumId,
count of e as count
as g
yield {deptno = (#deptno g), avgId = (#sumId g) / (#count g)}
yield {(#deptno g), avgId = (#sumId g) / (#count g)}
*)
(*) End relational.sml
15 changes: 15 additions & 0 deletions src/test/resources/script/relational.sml.out
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ from e in emps yield {deptno = e.deptno, one = 1};
val it = [{deptno=10,one=1},{deptno=20,one=1},{deptno=30,one=1},{deptno=30,one=1}] : {deptno:int, one:int} list


from e in emps yield {e.deptno, one = 1};
val it = [{deptno=10,one=1},{deptno=20,one=1},{deptno=30,one=1},{deptno=30,one=1}] : {deptno:int, one:int} list


from e in emps yield ((#id e) + (#deptno e));
val it = [110,121,132,133] : int list

Expand Down Expand Up @@ -197,6 +201,17 @@ val it =
{deptno=30,dname="Engineering",ename="Scooby",id=103}] : {deptno:int, dname:string, ename:string, id:int} list


(*) as above, using abbreviated record syntax
from e in emps, d in depts
where e.deptno = d.deptno
yield {e.id, e.deptno, ename = e.name, dname = d.name};
val it =
[{deptno=10,dname="Sales",ename="Fred",id=100},
{deptno=20,dname="HR",ename="Velma",id=101},
{deptno=30,dname="Engineering",ename="Shaggy",id=102},
{deptno=30,dname="Engineering",ename="Scooby",id=103}] : {deptno:int, dname:string, ename:string, id:int} list


(*) exists (defining the "exists" function ourselves)
(*) and correlated sub-query
(* disabled due to "::"
Expand Down

0 comments on commit b311d20

Please sign in to comment.