diff --git a/docs/reference.md b/docs/reference.md index b703f4067..f5dd1bb5a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -37,7 +37,7 @@ just because they take effort to build. Contributions are welcome! In Morel but not Standard ML: -* `from` expression with `in`, `suchthat`, `join`, `where`, `group`, +* `from` expression with `in`, `join`, `where`, `group`, `compute`, `order`, `skip`, `take`, `yield` clauses * `union`, `except`, `intersect`, `elem`, `notelem` operators * "*lab* `=`" is optional in `exprow` @@ -148,8 +148,7 @@ In Standard ML but not in Morel: match matchItempat => exp scanpat [ in | = ] exp - | var1 , ... , varv suchThat exp - constrained iteration (v ≥ 1) + | var unbounded variable stepwhere exp filter clause | join scan [ on exp ] join clause | group groupKey1 , ... , groupKeyg diff --git a/etc/morel.lang b/etc/morel.lang index 0ee3365f2..906954c55 100644 --- a/etc/morel.lang +++ b/etc/morel.lang @@ -34,7 +34,7 @@ Keywords={ "Word", "Char", "StringCvt", "Word8", "List", "ListPair", "Vector", "TextIO", "BinIO", "div", "mod", "compute", "desc", "elem", "except", "exists", "from", "group", "intersect", - "notelem", "notExists", "order", "suchthat", "union", "yield"}, + "notelem", "notExists", "order", "union", "yield"}, }, { Id=2, List={"bool", "string", "int", "real", "word"}, diff --git a/src/main/java/net/hydromatic/morel/ast/Ast.java b/src/main/java/net/hydromatic/morel/ast/Ast.java index b03845d94..17a8e9dab 100644 --- a/src/main/java/net/hydromatic/morel/ast/Ast.java +++ b/src/main/java/net/hydromatic/morel/ast/Ast.java @@ -1612,15 +1612,15 @@ public abstract static class FromStep extends AstNode { } } - /** A scan (e.g. "e in emps") + /** A scan (e.g. "e in emps", "e") * or scan-and-join (e.g. "left join d in depts on e.deptno = d.deptno") * in a {@code from} expression. */ public static class Scan extends FromStep { public final Pat pat; - public final Exp exp; + public final @Nullable Exp exp; public final @Nullable Exp condition; - Scan(Pos pos, Op op, Pat pat, Exp exp, @Nullable Exp condition) { + Scan(Pos pos, Op op, Pat pat, @Nullable Exp exp, @Nullable Exp condition) { super(pos, op); switch (op) { case INNER_JOIN: @@ -1637,26 +1637,17 @@ public static class Scan extends FromStep { } @Override AstWriter unparse(AstWriter w, int left, int right) { - final String op; - final Exp exp; - switch (this.exp.op) { - case FROM_EQ: - op = " = "; - exp = ((PrefixCall) this.exp).a; - break; - case SUCH_THAT: - op = " suchthat "; - exp = ((PrefixCall) this.exp).a; - break; - default: - op = " in "; - exp = this.exp; - break; + w.append(op.padded) + .append(pat, 0, 0); + if (exp != null) { + if (exp.op == Op.FROM_EQ) { + w.append(" = ") + .append(((PrefixCall) this.exp).a, Op.EQ.right, 0); + } else { + w.append(" in ") + .append(this.exp, Op.EQ.right, 0); + } } - w.append(this.op.padded) - .append(pat, 0, 0) - .append(op) - .append(exp, Op.EQ.right, 0); if (condition != null) { w.append(" on ") .append(condition, 0, 0); @@ -1672,9 +1663,9 @@ public static class Scan extends FromStep { visitor.visit(this); } - public Scan copy(Pat pat, Exp exp, Exp condition) { + public Scan copy(Pat pat, @Nullable Exp exp, @Nullable Exp condition) { return this.pat.equals(pat) - && this.exp.equals(exp) + && Objects.equals(this.exp, exp) && Objects.equals(this.condition, condition) ? this : new Scan(pos, op, pat, exp, condition); diff --git a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java index 5382ede9f..e81b481f0 100644 --- a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java @@ -349,12 +349,6 @@ public Ast.Exp fromEq(Ast.Exp exp) { return new Ast.PrefixCall(exp.pos, Op.FROM_EQ, exp); } - /** Wraps an expression to distinguish "from x suchthat e" from - * "from x in e". */ - public Ast.Exp fromSuchThat(Ast.Exp exp) { - return new Ast.PrefixCall(exp.pos, Op.SUCH_THAT, exp); - } - public Ast.Fn fn(Pos pos, Ast.Match... matchList) { return new Ast.Fn(pos, ImmutableList.copyOf(matchList)); } diff --git a/src/main/java/net/hydromatic/morel/ast/Core.java b/src/main/java/net/hydromatic/morel/ast/Core.java index 88715f356..dce8ad2bd 100644 --- a/src/main/java/net/hydromatic/morel/ast/Core.java +++ b/src/main/java/net/hydromatic/morel/ast/Core.java @@ -20,6 +20,7 @@ import net.hydromatic.morel.compile.BuiltIn; import net.hydromatic.morel.compile.Environment; +import net.hydromatic.morel.compile.Extents; import net.hydromatic.morel.compile.Resolver; import net.hydromatic.morel.eval.Closure; import net.hydromatic.morel.eval.Code; @@ -27,8 +28,10 @@ import net.hydromatic.morel.type.DataType; import net.hydromatic.morel.type.FnType; import net.hydromatic.morel.type.ListType; +import net.hydromatic.morel.type.PrimitiveType; import net.hydromatic.morel.type.RecordLikeType; import net.hydromatic.morel.type.RecordType; +import net.hydromatic.morel.type.TupleType; import net.hydromatic.morel.type.Type; import net.hydromatic.morel.type.TypeSystem; import net.hydromatic.morel.type.TypedValue; @@ -103,11 +106,6 @@ public Type type() { return type; } - /** Returns the type's key. */ - public Type.Key typeKey() { - return type().key(); - } - @Override public abstract Pat accept(Shuttle shuttle); } @@ -383,9 +381,21 @@ public static class Con0Pat extends Pat { public static class TuplePat extends Pat { public final List args; - TuplePat(Type type, ImmutableList args) { + /** Creates a TuplePat. + * + *

Type is {@link PrimitiveType#UNIT} if {@code args} is empty, + * otherwise a {@link TupleType}. */ + TuplePat(RecordLikeType type, ImmutableList args) { super(Op.TUPLE_PAT, type); this.args = requireNonNull(args); + checkArgument(args.size() == type.argNameTypes().size()); + checkArgument(args.isEmpty() + ? type == PrimitiveType.UNIT + : type instanceof TupleType); + } + + @Override public RecordLikeType type() { + return (RecordLikeType) type; } @Override AstWriter unparse(AstWriter w, int left, int right) { @@ -407,6 +417,17 @@ public TuplePat copy(TypeSystem typeSystem, List args) { return args.equals(this.args) ? this : core.tuplePat(typeSystem, args); } + + /** Returns the names of all components that are named. */ + public List fieldNames() { + final ImmutableList.Builder names = ImmutableList.builder(); + for (Pat arg : args) { + if (arg instanceof NamedPat) { + names.add(((NamedPat) arg).name); + } + } + return names.build(); + } } /** List pattern. @@ -1127,7 +1148,11 @@ public static class From extends Exp { } } - public Exp copy(TypeSystem typeSystem, Environment env, + /** Copies this {@code From} with a new set of steps. + * + *

Returns this {@code From} if the steps are the same. + * If {@code env} is not null, performs additional checking. */ + public Exp copy(TypeSystem typeSystem, @Nullable Environment env, List steps) { return steps.equals(this.steps) ? this @@ -1165,17 +1190,28 @@ public static class Scan extends FromStep { Scan(Op op, ImmutableList bindings, Pat pat, Exp exp, Exp condition) { super(op, bindings); - switch (op) { - case INNER_JOIN: - case SUCH_THAT: - break; - default: + if (op != Op.INNER_JOIN) { // SCAN and CROSS_JOIN are valid in ast, not core. - throw new AssertionError("not a join type " + op); + throw new IllegalArgumentException("not a join type " + op); } this.pat = requireNonNull(pat, "pat"); this.exp = requireNonNull(exp, "exp"); this.condition = requireNonNull(condition, "condition"); + if (!(exp.type instanceof ListType)) { + throw new IllegalArgumentException("scan expression must be list: " + + exp.type); + } + final ListType listType = (ListType) exp.type; + if (!canAssign(listType.elementType, pat.type)) { + throw new IllegalArgumentException(exp.type + " + " + pat.type); + } + } + + /** Returns whether you can assign a value of {@code fromType} to a variable + * of type {@code toType}. */ + private static boolean canAssign(Type fromType, Type toType) { + return fromType.equals(toType) + || toType.isProgressive(); } @Override public Scan accept(Shuttle shuttle) { @@ -1188,16 +1224,17 @@ public static class Scan extends FromStep { @Override protected AstWriter unparse(AstWriter w, From from, int ordinal, int left, int right) { - final String prefix = ordinal == 0 ? " " - : op == Op.SUCH_THAT ? " join " - : op.padded; - final String infix = op == Op.SUCH_THAT ? " suchthat " - : " in "; - w.append(prefix) - // for these purposes 'in' and 'suchthat' have same precedence as '=' - .append(pat, 0, Op.EQ.left) - .append(infix) - .append(exp, Op.EQ.right, 0); + w.append(ordinal == 0 ? " " : op.padded) + // for these purposes 'in' has same precedence as '=' + .append(pat, 0, Op.EQ.left); + if (Extents.isInfinite(exp)) { + // Print "from x : int" rather "from x in extent 'int'" + w.append(" : ") + .append(((ListType) exp.type).elementType.moniker()); + } else { + w.append(" in ") + .append(exp, Op.EQ.right, 0); + } if (!isLiteralTrue()) { w.append("on").append(condition, 0, 0); } diff --git a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java index 5e473b55f..fa335046e 100644 --- a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java @@ -20,6 +20,7 @@ import net.hydromatic.morel.compile.BuiltIn; import net.hydromatic.morel.compile.Environment; +import net.hydromatic.morel.compile.Extents; import net.hydromatic.morel.compile.NameGenerator; import net.hydromatic.morel.eval.Unit; import net.hydromatic.morel.type.Binding; @@ -39,6 +40,7 @@ import net.hydromatic.morel.util.PairList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; @@ -56,9 +58,11 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.function.BiFunction; +import java.util.function.Consumer; import static net.hydromatic.morel.type.RecordType.ORDERING; import static net.hydromatic.morel.util.Pair.forEach; +import static net.hydromatic.morel.util.Pair.forEachIndexed; import static net.hydromatic.morel.util.Static.transform; import static com.google.common.collect.Iterables.getOnlyElement; @@ -253,11 +257,12 @@ public Core.Con0Pat con0Pat(DataType type, String tyCon) { return new Core.Con0Pat(type, tyCon); } - public Core.TuplePat tuplePat(Type type, Iterable args) { + public Core.TuplePat tuplePat(RecordLikeType type, + Iterable args) { return new Core.TuplePat(type, ImmutableList.copyOf(args)); } - public Core.TuplePat tuplePat(Type type, Core.Pat... args) { + public Core.TuplePat tuplePat(RecordLikeType type, Core.Pat... args) { return new Core.TuplePat(type, ImmutableList.copyOf(args)); } @@ -540,12 +545,32 @@ public Core.Yield yield_(TypeSystem typeSystem, Core.Exp exp) { switch (exp.type.op()) { case RECORD_TYPE: case TUPLE_TYPE: - ((RecordLikeType) exp.type).argNameTypes().forEach((name, type) -> - bindings.add( - Binding.of(idPat(type, name, typeSystem.nameGenerator)))); + forEachIndexed(((RecordLikeType) exp.type).argNameTypes(), + (i, name, type) -> { + final Core.NamedPat idPat; + if (exp.op == Op.TUPLE + && exp.arg(i) instanceof Core.Id + && ((Core.Id) exp.arg(i)).idPat.name.equals(name)) { + // Use an existing IdPat if we can, rather than generating a new + // IdPat with a different sequence number. (The underlying problem + // is that the fields of record types have only names, no sequence + // numbers.) + idPat = ((Core.Id) exp.arg(i)).idPat; + } else { + idPat = idPat(type, name, typeSystem.nameGenerator); + } + bindings.add(Binding.of(idPat)); + }); break; + default: - bindings.add(Binding.of(idPat(exp.type, typeSystem.nameGenerator))); + switch (exp.op) { + case ID: + bindings.add(Binding.of(((Core.Id) exp).idPat)); + break; + default: + bindings.add(Binding.of(idPat(exp.type, typeSystem.nameGenerator))); + } } return yield_(bindings, exp); } @@ -578,20 +603,32 @@ public Core.Exp list(TypeSystem typeSystem, Core.Exp arg0, Core.Exp... args) { * fall into a given range-set. The range-set might consist of just * {@link Range#all()}, in which case, the list returns all values of the * type. */ - @SuppressWarnings({"UnstableApiUsage", "rawtypes"}) + @SuppressWarnings({"rawtypes", "unchecked"}) public Core.Exp extent(TypeSystem typeSystem, Type type, - RangeSet rangeSet) { + RangeSet rangeSet) { + final Map map; + if (rangeSet.complement().isEmpty()) { + map = ImmutableMap.of(); + } else { + map = ImmutableMap.of("/", ImmutableRangeSet.copyOf(rangeSet)); + } + return extent(typeSystem, type, map); + } + + @SuppressWarnings("rawtypes") + public Core.Exp extent(TypeSystem typeSystem, Type type, + Map rangeSetMap) { final ListType listType = typeSystem.listType(type); // Store an ImmutableRangeSet value inside a literal of type 'unit'. // The value of such literals is usually Unit.INSTANCE, but we cheat. return core.apply(Pos.ZERO, listType, core.functionLiteral(typeSystem, BuiltIn.Z_EXTENT), - core.internalLiteral(new RangeExtent(rangeSet, type))); + core.internalLiteral(new RangeExtent(typeSystem, type, rangeSetMap))); } - @SuppressWarnings({"UnstableApiUsage", "rawtypes", "unchecked"}) - public Pair> mergeExtents(TypeSystem typeSystem, - List exps, boolean intersect) { + @SuppressWarnings({"rawtypes", "unchecked"}) + public Pair> intersectExtents(TypeSystem typeSystem, + List exps) { switch (exps.size()) { case 0: throw new AssertionError(); @@ -600,28 +637,61 @@ public Pair> mergeExtents(TypeSystem typeSystem, return Pair.of(simplify(typeSystem, exps.get(0)), ImmutableList.of()); default: - ImmutableRangeSet rangeSet = intersect - ? ImmutableRangeSet.of(Range.all()) - : ImmutableRangeSet.of(); + final List> rangeSetMaps = + new ArrayList<>(); final List remainingExps = new ArrayList<>(); for (Core.Exp exp : exps) { if (exp.isCallTo(BuiltIn.Z_EXTENT)) { final Core.Literal argLiteral = (Core.Literal) ((Core.Apply) exp).arg; final RangeExtent list = argLiteral.unwrap(RangeExtent.class); - rangeSet = intersect - ? rangeSet.intersection(list.rangeSet) - : rangeSet.union(list.rangeSet); + rangeSetMaps.add(list.rangeSetMap); continue; } remainingExps.add(exp); } final ListType listType = (ListType) exps.get(0).type; + Map rangeSetMap = + Extents.intersect((List) rangeSetMaps); Core.Exp exp = - core.extent(typeSystem, listType.elementType, rangeSet); + core.extent(typeSystem, listType.elementType, rangeSetMap); for (Core.Exp remainingExp : remainingExps) { - exp = intersect - ? core.intersect(typeSystem, exp, remainingExp) - : core.union(typeSystem, exp, remainingExp); + exp = core.intersect(typeSystem, exp, remainingExp); + } + return Pair.of(exp, remainingExps); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public Pair> unionExtents(TypeSystem typeSystem, + List exps) { + switch (exps.size()) { + case 0: + throw new AssertionError(); + + case 1: + return Pair.of(simplify(typeSystem, exps.get(0)), ImmutableList.of()); + + default: + final List> rangeSetMaps = + new ArrayList<>(); + final List remainingExps = new ArrayList<>(); + for (Core.Exp exp : exps) { + if (exp.isCallTo(BuiltIn.Z_EXTENT)) { + final Core.Literal argLiteral = (Core.Literal) ((Core.Apply) exp).arg; + final Core.Wrapper wrapper = (Core.Wrapper) argLiteral.value; + final RangeExtent list = wrapper.unwrap(RangeExtent.class); + rangeSetMaps.add(list.rangeSetMap); + continue; + } + remainingExps.add(exp); + } + final ListType listType = (ListType) exps.get(0).type; + Map rangeSetMap = + Extents.union((List) rangeSetMaps); + Core.Exp exp = + core.extent(typeSystem, listType.elementType, rangeSetMap); + for (Core.Exp remainingExp : remainingExps) { + exp = core.union(typeSystem, exp, remainingExp); } return Pair.of(exp, remainingExps); } @@ -638,7 +708,7 @@ public Pair> mergeExtents(TypeSystem typeSystem, * extent "[[1, 5], [10, 20]]" * } */ - Core.Exp simplify(TypeSystem typeSystem, Core.Exp exp) { + public Core.Exp simplify(TypeSystem typeSystem, Core.Exp exp) { switch (exp.op) { case TUPLE: final Core.Tuple tuple = (Core.Tuple) exp; @@ -655,7 +725,7 @@ Core.Exp simplify(TypeSystem typeSystem, Core.Exp exp) { if (apply.args().stream() .allMatch(exp1 -> exp1.isCallTo(BuiltIn.Z_EXTENT))) { Pair> pair = - mergeExtents(typeSystem, apply.args(), false); + unionExtents(typeSystem, apply.args()); if (pair.right.isEmpty()) { return pair.left; } @@ -665,7 +735,7 @@ Core.Exp simplify(TypeSystem typeSystem, Core.Exp exp) { if (apply.args().stream() .allMatch(exp1 -> exp1.isCallTo(BuiltIn.Z_EXTENT))) { Pair> pair = - mergeExtents(typeSystem, apply.args(), true); + intersectExtents(typeSystem, apply.args()); if (pair.right.isEmpty()) { return pair.left; } @@ -707,7 +777,7 @@ private Core.Apply call(TypeSystem typeSystem, BuiltIn builtIn, } /** Calls a built-in function with one type parameter. */ - private Core.Apply call(TypeSystem typeSystem, BuiltIn builtIn, Type type, + public Core.Apply call(TypeSystem typeSystem, BuiltIn builtIn, Type type, Pos pos, Core.Exp... args) { final Core.Literal literal = functionLiteral(typeSystem, builtIn); final ForallType forallType = (ForallType) literal.type; @@ -756,18 +826,28 @@ public Core.Exp andAlso(TypeSystem typeSystem, Core.Exp a0, Core.Exp a1) { return call(typeSystem, BuiltIn.Z_ANDALSO, a0, a1); } + /** Converts a list of 0 or more expressions into an {@code andalso}; + * simplifies empty list to "true" and singleton list "[e]" to "e". */ public Core.Exp andAlso(TypeSystem typeSystem, Iterable exps) { - return foldRight(ImmutableList.copyOf(exps), - (e1, e2) -> andAlso(typeSystem, e1, e2)); + final List expList = ImmutableList.copyOf(exps); + if (expList.isEmpty()) { + return trueLiteral; + } + return foldRight(expList, (e1, e2) -> andAlso(typeSystem, e1, e2)); } public Core.Exp orElse(TypeSystem typeSystem, Core.Exp a0, Core.Exp a1) { return call(typeSystem, BuiltIn.Z_ORELSE, a0, a1); } + /** Converts a list of 0 or more expressions into an {@code orelse}; + * simplifies empty list to "false" and singleton list "[e]" to "e". */ public Core.Exp orElse(TypeSystem typeSystem, Iterable exps) { - return foldRight(ImmutableList.copyOf(exps), - (e1, e2) -> orElse(typeSystem, e1, e2)); + final ImmutableList expList = ImmutableList.copyOf(exps); + if (expList.isEmpty()) { + return falseLiteral; + } + return foldRight(expList, (e1, e2) -> orElse(typeSystem, e1, e2)); } private E foldRight(List list, BiFunction fold) { @@ -802,6 +882,100 @@ public Core.Exp intersect(TypeSystem typeSystem, Iterable exps) { return foldRight(ImmutableList.copyOf(exps), (e1, e2) -> intersect(typeSystem, e1, e2)); } + + /** Returns an expression substituting every given expression as true. + * + *

For example, if {@code exp} is "{@code x = 1 andalso y > 2}" + * and {@code trueExps} is [{@code x = 1}, {@code z = 2}], + * returns "{@code y > 2}". + */ + public Core.Exp subTrue(TypeSystem typeSystem, Core.Exp exp, + List trueExps) { + List conjunctions = decomposeAnd(exp); + List conjunctions2 = new ArrayList<>(); + for (Core.Exp conjunction : conjunctions) { + if (!trueExps.contains(conjunction)) { + conjunctions2.add(conjunction); + } + } + if (conjunctions.size() == conjunctions2.size()) { + // Don't create a new expression unless we have to. + return exp; + } + return andAlso(typeSystem, conjunctions2); + } + + /** Decomposes an {@code andalso} expression; + * inverse of {@link #andAlso(TypeSystem, Iterable)}. + * + *

Examples: + *

*/ + public List decomposeAnd(Core.Exp exp) { + final ImmutableList.Builder list = ImmutableList.builder(); + flattenAnd(exp, list::add); + return list.build(); + } + + /** Decomposes an {@code orelse} expression; + * inverse of {@link #orElse(TypeSystem, Iterable)}. + * + *

Examples: + *

    + *
  • "p1 orelse p2" becomes "[p1, p2]" (two elements); + *
  • "p1 orelse p2 orelse p3" becomes "[p1, p2, p3]" (three elements); + *
  • "p1 andalso p2" becomes "[p1 andalso p2]" (one element); + *
  • "false" becomes "[]" (no elements); + *
  • "true" becomes "[true]" (one element). + *
*/ + public List decomposeOr(Core.Exp exp) { + final ImmutableList.Builder list = ImmutableList.builder(); + flattenOr(exp, list::add); + return list.build(); + } + + /** Flattens the {@code andalso}s in an expression into a consumer. */ + public void flattenAnd(Core.Exp exp, Consumer consumer) { + //noinspection StatementWithEmptyBody + if (exp.op == Op.BOOL_LITERAL && (boolean) ((Core.Literal) exp).value) { + // don't add 'true' to the list + } else if (exp.op == Op.APPLY + && ((Core.Apply) exp).fn.op == Op.FN_LITERAL + && ((Core.Literal) ((Core.Apply) exp).fn).value == BuiltIn.Z_ANDALSO) { + flattenAnds(((Core.Apply) exp).args(), consumer); + } else { + consumer.accept(exp); + } + } + + /** Flattens the {@code andalso}s in every expression into a consumer. */ + public void flattenAnds(List exps, Consumer consumer) { + exps.forEach(arg -> flattenAnd(arg, consumer)); + } + + /** Flattens the {@code orelse}s in an expression into a consumer. */ + public void flattenOr(Core.Exp exp, Consumer consumer) { + //noinspection StatementWithEmptyBody + if (exp.op == Op.BOOL_LITERAL && !(boolean) ((Core.Literal) exp).value) { + // don't add 'false' to the list + } else if (exp.op == Op.APPLY + && ((Core.Apply) exp).fn.op == Op.FN_LITERAL + && ((Core.Literal) ((Core.Apply) exp).fn).value == BuiltIn.Z_ORELSE) { + flattenOrs(((Core.Apply) exp).args(), consumer); + } else { + consumer.accept(exp); + } + } + + /** Flattens the {@code orelse}s in every expression into a consumer. */ + public void flattenOrs(List exps, Consumer consumer) { + exps.forEach(arg -> flattenOr(arg, consumer)); + } } // End CoreBuilder.java diff --git a/src/main/java/net/hydromatic/morel/ast/FromBuilder.java b/src/main/java/net/hydromatic/morel/ast/FromBuilder.java index 60440d685..4ac6c382f 100644 --- a/src/main/java/net/hydromatic/morel/ast/FromBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/FromBuilder.java @@ -23,10 +23,13 @@ import net.hydromatic.morel.compile.RefChecker; import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.Pair; import net.hydromatic.morel.util.PairList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Range; import org.apache.calcite.util.Util; import org.checkerframework.checker.nullness.qual.Nullable; @@ -37,6 +40,7 @@ import static net.hydromatic.morel.ast.CoreBuilder.core; import static net.hydromatic.morel.util.Pair.forEach; +import static net.hydromatic.morel.util.Static.append; import static com.google.common.collect.Iterables.getLast; @@ -122,56 +126,79 @@ && isTrivial(tuple, previousBindings, yield.bindings)) { return this; } - public FromBuilder suchThat(Core.Pat pat, Core.Exp exp) { - Compiles.acceptBinding(typeSystem, pat, bindings); - return addStep( - core.scan(Op.SUCH_THAT, bindings, pat, exp, core.boolLiteral(true))); + /** Creates an unbounded scan, "from pat". */ + public FromBuilder scan(Core.Pat pat) { + final Core.Exp extent = + core.extent(typeSystem, pat.type, ImmutableRangeSet.of(Range.all())); + return scan(pat, extent, core.boolLiteral(true)); } + /** Creates a bounded scan, "from pat in exp". */ public FromBuilder scan(Core.Pat pat, Core.Exp exp) { return scan(pat, exp, core.boolLiteral(true)); } public FromBuilder scan(Core.Pat pat, Core.Exp exp, Core.Exp condition) { if (exp.op == Op.FROM - && steps.isEmpty() && core.boolLiteral(true).equals(condition) && (pat instanceof Core.IdPat && !((Core.From) exp).steps.isEmpty() && getLast(((Core.From) exp).steps).bindings.size() == 1 || pat instanceof Core.RecordPat && ((Core.RecordPat) pat).args.stream() + .allMatch(a -> a instanceof Core.IdPat) + || pat instanceof Core.TuplePat + && ((Core.TuplePat) pat).args.stream() .allMatch(a -> a instanceof Core.IdPat))) { final Core.From from = (Core.From) exp; + final Core.FromStep lastStep = getLast(from.steps); + final List steps = + lastStep.op == Op.YIELD ? Util.skipLast(from.steps) : from.steps; + final PairList nameExps = PairList.of(); + boolean uselessIfLast = this.bindings.isEmpty(); final List bindings; if (pat instanceof Core.RecordPat) { final Core.RecordPat recordPat = (Core.RecordPat) pat; + this.bindings.forEach(b -> nameExps.add(b.id.name, core.id(b.id))); forEach(recordPat.type().argNameTypes.keySet(), recordPat.args, (name, arg) -> nameExps.add(name, core.id((Core.IdPat) arg))); bindings = null; + } else if (pat instanceof Core.TuplePat) { + final Core.TuplePat tuplePat = (Core.TuplePat) pat; + Pair.forEach(tuplePat.args, lastStep.bindings, + (arg, binding) -> + nameExps.add(((Core.IdPat) arg).name, core.id(binding.id))); + bindings = null; + } else if (!this.bindings.isEmpty()) { + // With at least one binding, and one new variable, the output will be + // a record type. + final Core.IdPat idPat = (Core.IdPat) pat; + this.bindings.forEach(b -> nameExps.add(b.id.name, core.id(b.id))); + lastStep.bindings.forEach(b -> nameExps.add(idPat.name, core.id(b.id))); + bindings = null; } else { final Core.IdPat idPat = (Core.IdPat) pat; - final Core.FromStep lastStep = getLast(from.steps); if (lastStep instanceof Core.Yield && ((Core.Yield) lastStep).exp.op != Op.RECORD) { // The last step is a yield scalar, say 'yield x + 1'. // Translate it to a yield singleton record, say 'yield {y = x + 1}' - addAll(Util.skipLast(from.steps)); - if (((Core.Yield) lastStep).exp.op == Op.ID) { + addAll(steps); + if (((Core.Yield) lastStep).exp.op == Op.ID + && this.bindings.size() == 1) { // The last step is 'yield e'. Skip it. return this; } nameExps.add(idPat.name, ((Core.Yield) lastStep).exp); bindings = ImmutableList.of(Binding.of(idPat)); - return yield_(true, bindings, core.record(typeSystem, nameExps)); + return yield_(false, bindings, core.record(typeSystem, nameExps)); } final Binding binding = Iterables.getOnlyElement(lastStep.bindings); nameExps.add(idPat.name, core.id(binding.id)); - bindings = ImmutableList.of(Binding.of(idPat)); + bindings = append(this.bindings, Binding.of(idPat)); } - addAll(from.steps); - return yield_(true, bindings, core.record(typeSystem, nameExps)); + addAll(steps); + return yield_(uselessIfLast, bindings, core.record(typeSystem, nameExps)); } Compiles.acceptBinding(typeSystem, pat, bindings); return addStep(core.scan(Op.INNER_JOIN, bindings, pat, exp, condition)); @@ -224,7 +251,7 @@ public FromBuilder yield_(Core.Exp exp) { } public FromBuilder yield_(boolean uselessIfLast, Core.Exp exp) { - return yield_(false, null, exp); + return yield_(uselessIfLast, null, exp); } /** Creates a "yield" step. @@ -328,25 +355,14 @@ private static TupleType tupleType(Core.Tuple tuple, List bindings, return identity ? TupleType.IDENTITY : TupleType.RENAME; } - /** Returns whether tuple is something like "{i = j, j = x}". */ - private boolean isRename(Core.Tuple tuple) { - for (int i = 0; i < tuple.args.size(); i++) { - Core.Exp exp = tuple.args.get(i); - if (exp.op != Op.ID) { - return false; - } - } - return true; - } - private Core.Exp build(boolean simplify) { if (removeIfLastIndex == steps.size() - 1) { removeIfLastIndex = Integer.MIN_VALUE; final Core.Yield yield = (Core.Yield) getLast(steps); - assert yield.exp.op == Op.TUPLE - && ((Core.Tuple) yield.exp).args.size() == 1 - && isTrivial((Core.Tuple) yield.exp, bindings, yield.bindings) - : yield.exp; + if (yield.exp.op != Op.TUPLE + || ((Core.Tuple) yield.exp).args.size() != 1) { + throw new AssertionError(yield.exp); + } steps.remove(steps.size() - 1); } if (simplify @@ -380,11 +396,7 @@ private class StepHandler extends Visitor { } @Override protected void visit(Core.Scan scan) { - if (scan.op == Op.SUCH_THAT) { - suchThat(scan.pat, scan.exp); - } else { - scan(scan.pat, scan.exp, scan.condition); - } + scan(scan.pat, scan.exp, scan.condition); } @Override protected void visit(Core.Where where) { diff --git a/src/main/java/net/hydromatic/morel/ast/Op.java b/src/main/java/net/hydromatic/morel/ast/Op.java index 03bddabb7..0dfef84b9 100644 --- a/src/main/java/net/hydromatic/morel/ast/Op.java +++ b/src/main/java/net/hydromatic/morel/ast/Op.java @@ -71,7 +71,6 @@ public enum Op { // internal FROM_EQ("$FROM_EQ "), - SUCH_THAT("$SUCH_THAT "), // value constructors TUPLE(true), diff --git a/src/main/java/net/hydromatic/morel/ast/Shuttle.java b/src/main/java/net/hydromatic/morel/ast/Shuttle.java index 4182d1280..09333a64e 100644 --- a/src/main/java/net/hydromatic/morel/ast/Shuttle.java +++ b/src/main/java/net/hydromatic/morel/ast/Shuttle.java @@ -395,8 +395,7 @@ protected Core.Exp visit(Core.From from) { protected Core.Scan visit(Core.Scan scan) { return scan.copy(scan.bindings, scan.pat.accept(this), - scan.exp.accept(this), - scan.condition == null ? null : scan.condition.accept(this)); + scan.exp.accept(this), scan.condition.accept(this)); } protected Core.Where visit(Core.Where where) { diff --git a/src/main/java/net/hydromatic/morel/ast/Visitor.java b/src/main/java/net/hydromatic/morel/ast/Visitor.java index f12e91361..debc4fba5 100644 --- a/src/main/java/net/hydromatic/morel/ast/Visitor.java +++ b/src/main/java/net/hydromatic/morel/ast/Visitor.java @@ -188,7 +188,9 @@ protected void visit(Ast.From from) { protected void visit(Ast.Scan scan) { scan.pat.accept(this); - scan.exp.accept(this); + if (scan.exp != null) { + scan.exp.accept(this); + } if (scan.condition != null) { scan.condition.accept(this); } diff --git a/src/main/java/net/hydromatic/morel/compile/Compiler.java b/src/main/java/net/hydromatic/morel/compile/Compiler.java index 81ac7f50d..9a69786c5 100644 --- a/src/main/java/net/hydromatic/morel/compile/Compiler.java +++ b/src/main/java/net/hydromatic/morel/compile/Compiler.java @@ -35,7 +35,6 @@ import net.hydromatic.morel.foreign.CalciteFunctions; import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.DataType; -import net.hydromatic.morel.type.FnType; import net.hydromatic.morel.type.Keys; import net.hydromatic.morel.type.PrimitiveType; import net.hydromatic.morel.type.RecordLikeType; @@ -71,7 +70,6 @@ import static net.hydromatic.morel.util.Static.skip; import static net.hydromatic.morel.util.Static.str; import static net.hydromatic.morel.util.Static.toImmutableList; -import static net.hydromatic.morel.util.Static.transform; import static net.hydromatic.morel.util.Static.transformEager; import static com.google.common.collect.Iterables.getLast; @@ -274,11 +272,6 @@ private Code compileFieldName(Context cx, Core.NamedPat idPat) { return Codes.get(idPat.name); } - private Code compileFieldNames(Context cx, List fieldNames) { - return Codes.tuple( - transform(fieldNames, fieldName -> compileFieldName(cx, fieldName))); - } - protected Code compileApply(Context cx, Core.Apply apply) { // Is this is a call to a built-in operator? switch (apply.fn.op) { @@ -343,45 +336,6 @@ && getOnlyElement(bindings).id.type.equals(elementType)) { return () -> Codes.scanRowSink(firstStep.op, scan.pat, code, conditionCode, nextFactory.get()); - case SUCH_THAT: - // Given - // (n, d) suchthat hasNameInDept (n, d) - // that is, - // pat = (n, d), - // exp = hasNameInDept (n, d), - // generate - // (n, d) in List.filter - // (fn x => case x of (n, d) => hasNameInDept (n, d)) - // (extent: (string * int) list) - // - // but we'd prefer to find the extent internally, e.g. given - // (n, d) suchthat (n, d) elem nameDeptPairs - // we generate - // (n, d) in nameDeptPairs - // - final Core.Scan scan2 = (Core.Scan) firstStep; - final Extents.Analysis extentFilter = - Extents.create(typeSystem, scan2.pat, ImmutableSortedMap.of(), - scan2.exp); - final FnType fnType = - typeSystem.fnType(scan2.pat.type, PrimitiveType.BOOL); - final Pos pos = Pos.ZERO; - final Core.Match match = core.match(pos, scan2.pat, scan2.exp); - final Core.Exp lambda = - core.fn(pos, fnType, ImmutableList.of(match), - typeSystem.nameGenerator); - final Core.Exp filterCall = - core.apply(pos, extentFilter.extentExp.type, - core.functionLiteral(typeSystem, BuiltIn.LIST_FILTER), - lambda); - final Core.Exp exp2 = - core.apply(pos, extentFilter.extentExp.type, filterCall, - extentFilter.extentExp); - final Code code2 = compile(cx, exp2); - final Code conditionCode2 = compile(cx, scan2.condition); - return () -> Codes.scanRowSink(Op.INNER_JOIN, scan2.pat, code2, - conditionCode2, nextFactory.get()); - case WHERE: final Core.Where where = (Core.Where) firstStep; final Code filterCode = compile(cx, where.exp); @@ -738,7 +692,8 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, typedVal = new Pretty.TypedVal(pat2.name, typedValue.valueAs(Object.class), - Keys.toProgressive(pat2.typeKey()).toType(typeSystem)); + Keys.toProgressive(pat2.type().key()) + .toType(typeSystem)); } else { typedVal = new Pretty.TypedVal(pat2.name, o2, pat2.type); } @@ -763,41 +718,6 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, newBindings.clear(); } - private void shredValue(Core.Pat pat, Object o, - BiConsumer consumer) { - switch (pat.op) { - case ID_PAT: - consumer.accept((Core.IdPat) pat, o); - return; - case AS_PAT: - final Core.AsPat asPat = (Core.AsPat) pat; - consumer.accept(asPat, o); - shredValue(asPat.pat, o, consumer); - return; - case TUPLE_PAT: - final Core.TuplePat tuplePat = (Core.TuplePat) pat; - forEach(tuplePat.args, (List) o, (pat2, o2) -> - shredValue(pat2, o2, consumer)); - return; - case RECORD_PAT: - final Core.RecordPat recordPat = (Core.RecordPat) pat; - forEach(recordPat.args, (List) o, (pat2, o2) -> - shredValue(pat2, o2, consumer)); - return; - case CON_PAT: - final Core.ConPat conPat = (Core.ConPat) pat; - shredValue(conPat.pat, ((List) o).get(1), consumer); - return; - case CONS_PAT: - final Core.ConPat consPat = (Core.ConPat) pat; - final List list = (List) o; - final Object head = list.get(0); - final List tail = list.subList(1, list.size()); - shredValue(consPat.pat, ImmutableList.of(head, tail), consumer); - return; - } - } - private void link(Map linkCodes, Core.Pat pat, Code code) { if (pat instanceof Core.IdPat) { diff --git a/src/main/java/net/hydromatic/morel/compile/Compiles.java b/src/main/java/net/hydromatic/morel/compile/Compiles.java index 95b3bde38..450e74262 100644 --- a/src/main/java/net/hydromatic/morel/compile/Compiles.java +++ b/src/main/java/net/hydromatic/morel/compile/Compiles.java @@ -110,9 +110,9 @@ private static CompiledStatement prepareDecl(TypeSystem typeSystem, checkPatternCoverage(typeSystem, coreDecl0, warningConsumer); } - // Ensures that once we discover that there is no suchThat, we stop looking; - // makes things a bit more efficient. - boolean mayContainSuchThat = true; + // Ensures that once we discover that there are no unbounded variables, + // we stop looking; makes things a bit more efficient. + boolean mayContainUnbounded = true; Core.Decl coreDecl; tracer.onCore(1, coreDecl0); @@ -143,13 +143,14 @@ private static CompiledStatement prepareDecl(TypeSystem typeSystem, } for (int i = 0; i < inlinePassCount; i++) { final Core.Decl coreDecl2 = coreDecl; - if (mayContainSuchThat) { - if (SuchThatShuttle.containsSuchThat(coreDecl)) { + if (mayContainUnbounded) { + if (SuchThatShuttle.containsUnbounded(coreDecl)) { coreDecl = coreDecl.accept(new SuchThatShuttle(typeSystem, env)); } else { - mayContainSuchThat = false; + mayContainUnbounded = false; } } + coreDecl = Extents.infinitePats(typeSystem, coreDecl); if (coreDecl == coreDecl2) { break; } diff --git a/src/main/java/net/hydromatic/morel/compile/Extents.java b/src/main/java/net/hydromatic/morel/compile/Extents.java index 88735194e..4e598391e 100644 --- a/src/main/java/net/hydromatic/morel/compile/Extents.java +++ b/src/main/java/net/hydromatic/morel/compile/Extents.java @@ -19,30 +19,45 @@ package net.hydromatic.morel.compile; import net.hydromatic.morel.ast.Core; +import net.hydromatic.morel.ast.FromBuilder; import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.ast.Pos; +import net.hydromatic.morel.ast.Shuttle; +import net.hydromatic.morel.type.RangeExtent; +import net.hydromatic.morel.type.Type; import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.ImmutablePairList; +import net.hydromatic.morel.util.Ord; import net.hydromatic.morel.util.Pair; +import net.hydromatic.morel.util.PairList; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import org.checkerframework.checker.nullness.qual.NonNull; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.SortedMap; import static net.hydromatic.morel.ast.CoreBuilder.core; -import static net.hydromatic.morel.util.Pair.allMatch; import static net.hydromatic.morel.util.Static.skip; import static org.apache.calcite.util.Util.minus; +import static java.util.Objects.requireNonNull; + /** Generates an expression for the set of values that a variable can take in * a program. * @@ -60,8 +75,10 @@ * fun isOdd i = i % 2 = 0 * in * from e in emps, - * i suchthat isOdd i andalso i < 100 - * where i = e.deptno + * i + * where isOdd i + * andalso i < 100 + * andalso i = e.deptno * end * } * @@ -70,13 +87,15 @@ *
{@code
  *    from e in emps
  *      yield e.deptno
- *      where deptno % 2 = 0 andalso deptno < 100
+ *      where deptno % 2 = 0
+ *        andalso deptno < 100
  * }
*/ public class Extents { private Extents() {} - /** Returns an expression that generates the extent of a pattern. + /** Analyzes the extent of a pattern in an expression and creates an + * {@link Analysis}. * *

For example, given the program * @@ -84,7 +103,7 @@ private Extents() {} * let * fun f i = i elem [1, 2, 4] * in - * from x suchthat f x + * from x where f x * end * } * @@ -95,9 +114,10 @@ private Extents() {} *

{@code
    *   let
    *     val edges = [(1, 2), (2, 3), (1, 4), (4, 2), (4, 3)]
-  *      fun edge (i, j) = (i, j) elem edges
+   *     fun edge (i, j) = (i, j) elem edges
    *   in
-   *     from (x, y, z) suchthat edge (x, y) andalso edge (y, z) andalso x <> z
+   *     from x, y, z
+   *     where edge (x, y) andalso edge (y, z) andalso x <> z
    *   end
    * }
* @@ -120,24 +140,45 @@ private Extents() {} * z in (from e in edges group e.j) * } */ - public static Core.Exp generator(TypeSystem typeSystem, Core.Pat pat, - Core.Exp exp) { - return create(typeSystem, pat, ImmutableSortedMap.of(), exp).extentExp; - } - public static Analysis create(TypeSystem typeSystem, Core.Pat pat, - SortedMap boundPats, Core.Exp exp) { - final Extent extent = new Extent(typeSystem, pat, boundPats); - - final ListMultimap map = LinkedListMultimap.create(); - extent.g3(map, exp); - final List exps = map.get(pat); - if (exps.isEmpty()) { - throw new AssertionError(); + SortedMap boundPats, + Iterable followingSteps, + PairList idPats) { + final Extent extent = new Extent(typeSystem, pat, boundPats, idPats); + final List remainingFilters = new ArrayList<>(); + + final ExtentMap map = new ExtentMap(); + for (Core.FromStep step : followingSteps) { + if (step instanceof Core.Where) { + extent.g3(map.map, ((Core.Where) step).exp); + } } - final Pair> pair = - core.mergeExtents(typeSystem, exps, true); - return new Analysis(boundPats, extent.goalPats, pair.left, pair.right); + extent.definitions.forEach((namedPat, exp) -> { + // Is this expression better than the existing one? + // Yes, if there's no existing expression, + // or if the existing expression is infinite. + // For example, 'dno = v.deptno' is better than 'dno > 25'. + if (!map.map.containsKey(namedPat) + || Extents.isInfinite(map.map.get(namedPat).left(0))) { + map.map.put(namedPat, + ImmutablePairList.of(core.list(typeSystem, exp), + core.equal(typeSystem, core.id(namedPat), exp))); + } + }); + final PairList foo = map.get(typeSystem, pat); + final Pair extentFilter; + if (foo.isEmpty()) { + extentFilter = + Pair.of( + core.extent(typeSystem, pat.type, + ImmutableRangeSet.of(Range.all())), + core.boolLiteral(true)); + } else { + extentFilter = reduceAnd(typeSystem, foo); + } + return new Analysis(boundPats, extent.goalPats, + extentFilter.left, core.decomposeAnd(extentFilter.right), + remainingFilters); } /** Converts a singleton id pattern "x" or tuple pattern "(x, y)" @@ -157,24 +198,162 @@ private static List flatten(Core.Pat pat) { //noinspection unchecked,rawtypes return (List) tuplePat.args; + case RECORD_PAT: + final Core.RecordPat recordPat = (Core.RecordPat) pat; + for (Core.Pat arg : recordPat.args) { + if (arg.op != Op.ID_PAT) { + throw new CompileException("must be id", false, arg.pos); + } + } + //noinspection unchecked,rawtypes + return (List) recordPat.args; + default: throw new CompileException("must be id", false, pat.pos); } } + /** Returns whether an expression is an infinite extent. */ + public static boolean isInfinite(Core.Exp exp) { + if (!exp.isCallTo(BuiltIn.Z_EXTENT)) { + return false; + } + final Core.Apply apply = (Core.Apply) exp; + final Core.Literal literal = (Core.Literal) apply.arg; + final RangeExtent rangeExtent = literal.unwrap(RangeExtent.class); + return rangeExtent.iterable == null; + } + + public static Core.Decl infinitePats(TypeSystem typeSystem, + Core.Decl node) { + return node.accept( + new Shuttle(typeSystem) { + @Override protected Core.From visit(Core.From from) { + for (Ord step : Ord.zip(from.steps)) { + if (step.e instanceof Core.Scan) { + final Core.Scan scan = (Core.Scan) step.e; + if (isInfinite(scan.exp)) { + final FromBuilder fromBuilder = core.fromBuilder(typeSystem); + List followingSteps = + skip(from.steps, step.i + 1); + final Analysis analysis = + create(typeSystem, scan.pat, ImmutableSortedMap.of(), + followingSteps, ImmutablePairList.of()); + for (Core.FromStep step2 : from.steps) { + if (step2 == scan) { + fromBuilder.scan(scan.pat, analysis.extentExp, + scan.condition); // TODO + } else if (step2 instanceof Core.Where) { + fromBuilder.where( + core.subTrue(typeSystem, + ((Core.Where) step2).exp, + analysis.satisfiedFilters)); + } else { + fromBuilder.addAll(ImmutableList.of(step2)); + } + } + return fromBuilder.build(); + } + } + } + return from; // unchanged + } + }); + } + + /** Intersects a collection of range set maps + * (maps from prefix to {@link RangeSet}) into one. */ + public static > + Map> intersect( + List>> rangeSetMaps) { + switch (rangeSetMaps.size()) { + case 0: + // No filters, therefore the extent allows all values. + // An empty map expresses this. + return ImmutableMap.of(); + + case 1: + return rangeSetMaps.get(0); + + default: + final Multimap> rangeSetMultimap = + HashMultimap.create(); + for (Map> rangeSetMap : rangeSetMaps) { + rangeSetMap.forEach(rangeSetMultimap::put); + } + final ImmutableMap.Builder> rangeSetMap = + ImmutableMap.builder(); + rangeSetMultimap.asMap().forEach((path, rangeSets) -> + rangeSetMap.put(path, intersectRangeSets(rangeSets))); + return rangeSetMap.build(); + } + } + + /** Unions a collection of range set maps + * (maps from prefix to {@link RangeSet}) into one. */ + public static > + Map> union( + List>> rangeSetMaps) { + switch (rangeSetMaps.size()) { + case 0: + // No filters, therefore the extent is empty. + // A map containing an empty RangeSet for path "/" expresses this. + return ImmutableMap.of("/", ImmutableRangeSet.of()); + + case 1: + return rangeSetMaps.get(0); + + default: + final Multimap> rangeSetMultimap = + HashMultimap.create(); + for (Map> rangeSetMap : rangeSetMaps) { + rangeSetMap.forEach(rangeSetMultimap::put); + } + final ImmutableMap.Builder> rangeSetMap = + ImmutableMap.builder(); + rangeSetMultimap.asMap().forEach((path, rangeSets) -> + rangeSetMap.put(path, unionRangeSets(rangeSets))); + return rangeSetMap.build(); + } + } + + /** Intersects a collection of {@link RangeSet} into one. + * + * @see ImmutableRangeSet#intersection(RangeSet) */ + private static > ImmutableRangeSet + intersectRangeSets(Collection> rangeSets) { + return rangeSets.stream().reduce(ImmutableRangeSet.of(Range.all()), + ImmutableRangeSet::intersection); + } + + /** Unions a collection of {@link RangeSet} into one. + * + * @see ImmutableRangeSet#union(RangeSet) */ + private static > ImmutableRangeSet unionRangeSets( + Collection> rangeSets) { + return rangeSets.stream().reduce(ImmutableRangeSet.of(), + ImmutableRangeSet::union); + } + + /** Result of analyzing the variables in a query, pulling filters into the + * extent expression for each variable, so that no variable is over an + * infinite extent. */ public static class Analysis { final SortedMap boundPats; final Set goalPats; final Core.Exp extentExp; + final List satisfiedFilters; // filters satisfied by extentExp final List remainingFilters; - Analysis(SortedMap boundPats, - Set goalPats, Core.Exp extentExp, + private Analysis(SortedMap boundPats, + Set goalPats, + Core.Exp extentExp, List satisfiedFilters, List remainingFilters) { - this.boundPats = boundPats; - this.goalPats = goalPats; + this.boundPats = ImmutableSortedMap.copyOf(boundPats); + this.goalPats = ImmutableSet.copyOf(goalPats); this.extentExp = extentExp; - this.remainingFilters = remainingFilters; + this.satisfiedFilters = ImmutableList.copyOf(satisfiedFilters); + this.remainingFilters = ImmutableList.copyOf(remainingFilters); } Set unboundPats() { @@ -187,43 +366,78 @@ private static class Extent { final Set goalPats; final SortedMap boundPats; + /** New variables introduced as scans over an existing relation (list of + * records). Other variables, which are goals of this extent, are typically + * fields of this variable. */ + final PairList idPats; + + /** Contains definitions, such as "name = d.dname". + * With such a definition, "name" won't need an extent, because we + * can define it (or inline it) as the expression "d.dname". */ + final Map definitions = new HashMap<>(); + Extent(TypeSystem typeSystem, Core.Pat pat, - SortedMap boundPats) { + SortedMap boundPats, + PairList idPats) { this.typeSystem = typeSystem; this.goalPats = ImmutableSet.copyOf(flatten(pat)); this.boundPats = ImmutableSortedMap.copyOf(boundPats); + this.idPats = idPats; } @SuppressWarnings("SwitchStatementWithTooFewBranches") - void g3(Multimap map, Core.Exp exp) { + void g3(Map> map, Core.Exp filter) { final Core.Apply apply; - switch (exp.op) { + switch (filter.op) { case APPLY: - apply = (Core.Apply) exp; + apply = (Core.Apply) filter; switch (apply.fn.op) { case FN_LITERAL: BuiltIn builtIn = ((Core.Literal) apply.fn).unwrap(BuiltIn.class); + final Map> map2; switch (builtIn) { case Z_ANDALSO: - // Expression is 'andalso'. Visit each pattern, and union the - // constraints (intersect the generators). - apply.arg.forEachArg((arg, i) -> g3(map, arg)); + // Expression is 'andalso'. Visit each pattern, and 'and' the + // filters (intersect the extents). + map2 = new LinkedHashMap<>(); + apply.arg.forEachArg((arg, i) -> g3(map2, arg)); + map2.forEach((pat, foo) -> + map.computeIfAbsent(pat, p -> PairList.of()) + .addAll(foo)); break; case Z_ORELSE: // Expression is 'orelse'. Visit each pattern, and intersect the // constraints (union the generators). - final Multimap map2 = - LinkedListMultimap.create(); + map2 = new LinkedHashMap<>(); + final Map> map3 = + new LinkedHashMap<>(); apply.arg.forEachArg((arg, i) -> { - final Multimap map3 = - LinkedListMultimap.create(); g3(map3, arg); - map3.asMap().forEach((k, vs) -> - map2.put(k, core.intersect(typeSystem, vs))); + map3.forEach((pat, foo) -> + map2.computeIfAbsent(pat, p -> PairList.of()) + .add(reduceAnd(typeSystem, foo))); + map3.clear(); + }); + map2.forEach((pat, foo) -> { + final PairList foo1 = + map.computeIfAbsent(pat, p -> PairList.of()); + if (foo1.isEmpty()) { + // [] union [x2, x3, x4] + // => + // [x2, x3, x4] + foo1.add(reduceOr(typeSystem, foo)); + } else { + // [x0, x1] union [x2, x3, x4] + // => + // [union(intersect(x0, x1), intersect(x2, x3, x4))] + PairList intersectExtents = PairList.of(); + intersectExtents.add(reduceAnd(typeSystem, foo1)); + intersectExtents.add(reduceAnd(typeSystem, foo)); + foo1.clear(); + foo1.add(reduceOr(typeSystem, intersectExtents)); + } }); - map2.asMap().forEach((k, vs) -> - map.put(k, core.union(typeSystem, vs))); break; case OP_EQ: @@ -232,7 +446,36 @@ void g3(Multimap map, Core.Exp exp) { case OP_GT: case OP_LT: case OP_LE: - g4(map, builtIn, apply.arg(0), apply.arg(1)); + g4(builtIn, apply.arg(0), apply.arg(1), (pat, filter2, extent) -> + map.computeIfAbsent(pat, p -> PairList.of()) + .add(extent, filter2)); + break; + + case OP_ELEM: + switch (apply.arg(0).op) { + case ID: + final Core.NamedPat pat = ((Core.Id) apply.arg(0)).idPat; + map.computeIfAbsent(pat, p1 -> PairList.of()) + .add(apply.arg(1), apply); + break; + + case TUPLE: + final Core.Tuple tuple = (Core.Tuple) apply.arg(0); + final Core.Id id = core.id(createId(tuple.type, apply.arg(1))); + final Core.Exp elem = core.elem(typeSystem, id, apply.arg(1)); + g3(map, + core.andAlso(typeSystem, elem, + core.equal(typeSystem, id, tuple))); + final List conjunctions = new ArrayList<>(); + conjunctions.add(core.elem(typeSystem, id, apply.arg(1))); + tuple.forEach((i, name, arg) -> + conjunctions.add( + core.equal(typeSystem, + core.field(typeSystem, id, i), + arg))); + g3(map, core.andAlso(typeSystem, conjunctions)); + break; + } break; } } @@ -243,11 +486,23 @@ void g3(Multimap map, Core.Exp exp) { } } + private void g4(BuiltIn builtIn, Core.Exp arg0, Core.Exp arg1, + TriConsumer consumer) { + g5(builtIn, arg0, arg1, consumer); + g5(builtIn.reverse(), arg1, arg0, consumer); + } + @SuppressWarnings("SwitchStatementWithTooFewBranches") - private void g4(Multimap map, BuiltIn builtIn, - Core.Exp arg0, Core.Exp arg1) { + private void g5(BuiltIn builtIn, Core.Exp arg0, Core.Exp arg1, + TriConsumer consumer) { switch (builtIn) { case OP_EQ: + switch (arg0.op) { + case ID: + final Core.Id id = (Core.Id) arg0; + definitions.put(id.idPat, arg1); + } + // fall through case OP_NE: case OP_GE: case OP_GT: @@ -259,14 +514,11 @@ private void g4(Multimap map, BuiltIn builtIn, if (arg1.isConstant()) { // If exp is "id = literal", add extent "id: [literal]"; // if exp is "id > literal", add extent "id: (literal, inf)", etc. - map.put(id.idPat, baz(builtIn, arg1)); + consumer.accept(id.idPat, + core.call(typeSystem, builtIn, arg0.type, Pos.ZERO, arg0, arg1), + baz(builtIn, arg1)); } break; - default: - if (arg0.isConstant() && arg1.op == Op.ID) { - // Try switched, "literal = id". - g4(map, builtIn.reverse(), arg1, arg0); - } } break; @@ -275,7 +527,6 @@ private void g4(Multimap map, BuiltIn builtIn, } } - @SuppressWarnings("UnstableApiUsage") private Core.Exp baz(BuiltIn builtIn, Core.Exp arg) { switch (builtIn) { case OP_EQ: @@ -301,73 +552,161 @@ private Core.Exp baz(BuiltIn builtIn, Core.Exp arg) { } } - @SuppressWarnings("UnstableApiUsage") - ExtentFilter extent(Core.Scan scan) { - final List extents = new ArrayList<>(); - final List filters = new ArrayList<>(); - extent(scan.pat, scan.exp, extents, filters); - final Core.Exp extent; - if (extents.isEmpty()) { - extent = core.extent(typeSystem, scan.pat.type, - ImmutableRangeSet.of(Range.all())); - } else { - extent = extents.get(0); - filters.addAll(skip(extents)); + private Core.IdPat createId(Type type, Core.Exp extent) { + int i = idPats.firstMatch((id, e) -> extent.equals(e)); + if (i >= 0) { + return idPats.leftList().get(i); } - return new ExtentFilter(extent, ImmutableList.copyOf(filters)); + final Core.IdPat idPat = core.idPat(type, typeSystem.nameGenerator); + idPats.add(idPat, extent); + return idPat; } + } - private void extent(Core.Pat pat, Core.Exp exp, List extents, - List filters) { - switch (exp.op) { - case APPLY: - final Core.Apply apply = (Core.Apply) exp; - switch (apply.fn.op) { - case FN_LITERAL: - switch ((BuiltIn) ((Core.Literal) apply.fn).value) { - case OP_ELEM: - final List args = ((Core.Tuple) apply.arg).args; - if (matches(args.get(0), pat)) { - extents.add(args.get(1)); - } - break; - case Z_ANDALSO: - for (Core.Exp e : ((Core.Tuple) apply.arg).args) { - extent(pat, e, extents, filters); - return; - } - } - } + /** Reduces a list of extent-filter pairs [e0, f0, e1, f1, ...] + * to an extent-filter pair [e0 intersect e1 ..., f0 andalso f1 ...]. + * + *

If any of the ei are calls to + * {@link BuiltIn#Z_EXTENT extent}, merges them into a single extent. + * For example, in + * + *

{@code
+   * [extent "int: (0, inf)", x > 0,
+   *   x elem primes, isPrime x,
+   *   extent "int: (-inf, 10)", x < 10]
+   * }
+ * + *

the extents for "(0, inf)" and "(-inf, 10)" are merged into + * extent "(0, 10)": + * + *

{@code
+   * (extent "int: (0, 10)" intersect primes,
+   *   x > 0 andalso isPrime x andalso x < 10)
+   * }
+ */ + static Pair reduceAnd(TypeSystem typeSystem, + PairList extentFilters) { + if (extentFilters.isEmpty()) { + // Empty list would require us to create an infinite extent, but we + // don't know the type. Caller must ensure that the list is non-empty. + throw new IllegalArgumentException(); + } + final List extents = new ArrayList<>(); + core.flattenAnds(extentFilters.leftList(), extents::add); + final Pair> pair = + core.intersectExtents(typeSystem, extents); + return Pair.of(pair.left, + core.andAlso(typeSystem, extentFilters.rightList())); + } + + /** Reduces a list of extent-filter pairs [e0, f0, e1, f1, ...] + * to an extent-filter pair [e0 union e1 ..., f0 orelse f1 ...]. */ + static Pair reduceOr(TypeSystem typeSystem, + PairList extentFilters) { + return Pair.of(core.union(typeSystem, extentFilters.leftList()), + core.orElse(typeSystem, extentFilters.rightList())); + } + + @FunctionalInterface + interface TriConsumer { + void accept(R r, S s, T t); + } + + static class ExtentMap { + final Map> map = + new LinkedHashMap<>(); + + public PairList get(TypeSystem typeSystem, + Core.Pat pat) { + PairList foo = map.get(pat); + if (foo != null && !foo.isEmpty()) { + return foo; + } + if (canGet(pat)) { + return get_(typeSystem, pat); } - filters.add(exp); + return ImmutablePairList.of(); } - /** Returns whether an expression corresponds exactly to a pattern. - * For example "x" matches the pattern "x", - * and "(z, y)" matches the pattern "(x, y)". */ - private static boolean matches(Core.Exp exp, Core.Pat pat) { - if (exp.op == Op.ID && pat.op == Op.ID_PAT) { - return ((Core.Id) exp).idPat.equals(pat); + /** + * Constructs an expression for the extent of a pattern. + * You must have called {@link #canGet} first. + */ + private @NonNull PairList get_(TypeSystem typeSystem, + Core.Pat pat) { + final PairList foo = map.get(pat); + if (foo != null && !foo.isEmpty()) { + return foo; } - if (exp.op == Op.TUPLE && pat.op == Op.TUPLE_PAT) { - final Core.Tuple tuple = (Core.Tuple) exp; + switch (pat.op) { + case TUPLE_PAT: final Core.TuplePat tuplePat = (Core.TuplePat) pat; - if (tuple.args.size() == tuplePat.args.size()) { - return allMatch(tuple.args, tuplePat.args, Extent::matches); + if (tuplePat.args.stream().allMatch(this::canGet)) { + // Convert 'from x, y where p(x) andalso q(y)' + // to 'from x in extentP, y in extentQ' + // and that becomes the extent of '(x, y)'. + final FromBuilder fromBuilder = core.fromBuilder(typeSystem); + final List filters = new ArrayList<>(); + for (Core.Pat p : tuplePat.args) { + PairList f = + requireNonNull(get(typeSystem, p), "contradicts canGet"); + fromBuilder.scan(p, core.union(typeSystem, f.leftList())); + core.flattenAnds(f.rightList(), filters::add); + } + return PairList.of(fromBuilder.build(), + core.andAlso(typeSystem, filters)); + } else { + final PairList foo1 = PairList.of(); + map.forEach((pat1, foo2) -> { + if (pat1.op == Op.TUPLE_PAT) { + final Core.TuplePat tuplePat1 = (Core.TuplePat) pat1; + final List fieldNames = tuplePat1.fieldNames(); + if (tuplePat.args.stream().allMatch(arg -> + arg instanceof Core.NamedPat + && fieldNames.contains(((Core.NamedPat) arg).name))) { + foo1.addAll(foo2); + } + } + }); + return foo1; } + default: + throw new AssertionError("contradicts canGet"); } - return false; } - } - - /** A "suchthat" expression split into an extent and filters. */ - static class ExtentFilter { - final Core.Exp extent; - final ImmutableList filters; - ExtentFilter(Core.Exp extent, ImmutableList filters) { - this.extent = extent; - this.filters = filters; + boolean canGet(Core.Pat pat) { + PairList foo = map.get(pat); + if (foo != null && !foo.isEmpty()) { + return true; + } + if (pat.type.isFinite()) { + return true; + } + switch (pat.op) { + case TUPLE_PAT: + final Core.TuplePat tuplePat = (Core.TuplePat) pat; + if (tuplePat.args.stream().allMatch(this::canGet)) { + return true; + } + // If the map contains a tuple with a field for every one of this + // tuple's fields (not necessarily in the same order) then we can use + // it. + for (Core.Pat pat1 : map.keySet()) { + if (pat1.op == Op.TUPLE_PAT) { + final Core.TuplePat tuplePat1 = (Core.TuplePat) pat1; + final List fieldNames = tuplePat1.fieldNames(); + if (tuplePat.args.stream().allMatch(arg -> + arg instanceof Core.NamedPat + && fieldNames.contains(((Core.NamedPat) arg).name))) { + return true; + } + } + } + return false; + default: + return false; + } } } } diff --git a/src/main/java/net/hydromatic/morel/compile/Inliner.java b/src/main/java/net/hydromatic/morel/compile/Inliner.java index 547a50b2e..896e00181 100644 --- a/src/main/java/net/hydromatic/morel/compile/Inliner.java +++ b/src/main/java/net/hydromatic/morel/compile/Inliner.java @@ -125,6 +125,7 @@ public static Inliner of(TypeSystem typeSystem, Environment env, if (apply2.fn.op == Op.RECORD_SELECTOR && apply2.arg.op == Op.VALUE_LITERAL) { final Core.RecordSelector selector = (Core.RecordSelector) apply2.fn; + @SuppressWarnings("rawtypes") final List list = ((Core.Literal) apply2.arg).unwrap(List.class); final Object o = list.get(selector.slot); if (o instanceof Applicable || o instanceof Macro) { diff --git a/src/main/java/net/hydromatic/morel/compile/Resolver.java b/src/main/java/net/hydromatic/morel/compile/Resolver.java index 1e79d4b9d..692afc8ff 100644 --- a/src/main/java/net/hydromatic/morel/compile/Resolver.java +++ b/src/main/java/net/hydromatic/morel/compile/Resolver.java @@ -23,7 +23,6 @@ import net.hydromatic.morel.ast.FromBuilder; import net.hydromatic.morel.ast.Op; import net.hydromatic.morel.ast.Pos; -import net.hydromatic.morel.ast.Shuttle; import net.hydromatic.morel.ast.Visitor; import net.hydromatic.morel.eval.Session; import net.hydromatic.morel.type.Binding; @@ -41,14 +40,15 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Range; import org.checkerframework.checker.nullness.qual.Nullable; import java.math.BigDecimal; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -612,7 +612,7 @@ private Core.Pat toCore(Ast.Pat pat, Type type, Type targetType) { case TUPLE_PAT: final Ast.TuplePat tuplePat = (Ast.TuplePat) pat; final List argList = transformEager(tuplePat.args, this::toCore); - return core.tuplePat(type, argList); + return core.tuplePat((RecordLikeType) type, argList); default: throw new AssertionError("unknown pat " + pat.op); @@ -866,53 +866,22 @@ Core.Exp run(Ast.From from) { final Resolver r = withEnv(fromBuilder.bindings()); final Core.Exp coreExp; final Core.Pat corePat; - switch (scan.exp.op) { - case SUCH_THAT: - corePat = - r.toCore(scan.pat).accept( - // Converts tuple patterns into copies sorted by field names. - // For example, (b, c, a) becomes (a, b, c). - // Sorting is necessary because "from (b, c, a) suchthat p" will - // add variables a, b, c to the environment (in that order). - new Shuttle(typeMap.typeSystem) { - @Override protected Core.Pat visit(Core.TuplePat tuplePat) { - final Comparator comparator = - Comparator.comparing(pat -> - pat instanceof Core.NamedPat - ? ((Core.NamedPat) pat).name - : ""); - return tuplePat.copy(typeSystem, - ImmutableList.sortedCopyOf(comparator, - visitList(tuplePat.args))); - } - }); - - final List bindings2 = new ArrayList<>(fromBuilder.bindings()); - Compiles.acceptBinding(typeMap.typeSystem, corePat, bindings2); - final Resolver r2 = withEnv(bindings2); - - final Ast.Exp scanExp = ((Ast.PrefixCall) scan.exp).a; - coreExp = r2.toCore(scanExp); - break; - - default: + if (scan.exp == null) { + corePat = r.toCore(scan.pat); + coreExp = + core.extent(typeMap.typeSystem, corePat.type, + ImmutableRangeSet.of(Range.all())); + } else { coreExp = r.toCore(scan.exp); final ListType listType = (ListType) coreExp.type; corePat = r.toCore(scan.pat, listType.elementType); } - final Op op = scan.exp.op == Op.SUCH_THAT ? Op.SUCH_THAT - : scan.op == Op.SCAN ? Op.INNER_JOIN - : scan.op; final List bindings2 = new ArrayList<>(fromBuilder.bindings()); Compiles.acceptBinding(typeMap.typeSystem, corePat, bindings2); Core.Exp coreCondition = scan.condition == null ? core.boolLiteral(true) : r.withEnv(bindings2).toCore(scan.condition); - if (op == Op.SUCH_THAT) { - fromBuilder.suchThat(corePat, coreExp); - } else { - fromBuilder.scan(corePat, coreExp, coreCondition); - } + fromBuilder.scan(corePat, coreExp, coreCondition); } @Override protected void visit(Ast.Where where) { diff --git a/src/main/java/net/hydromatic/morel/compile/SuchThatShuttle.java b/src/main/java/net/hydromatic/morel/compile/SuchThatShuttle.java index 33cd37eaa..3e9c6e167 100644 --- a/src/main/java/net/hydromatic/morel/compile/SuchThatShuttle.java +++ b/src/main/java/net/hydromatic/morel/compile/SuchThatShuttle.java @@ -20,17 +20,13 @@ import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.FromBuilder; -import net.hydromatic.morel.ast.Op; import net.hydromatic.morel.ast.Shuttle; import net.hydromatic.morel.ast.Visitor; -import net.hydromatic.morel.type.ListType; -import net.hydromatic.morel.type.RecordType; +import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.TypeSystem; -import net.hydromatic.morel.util.Ord; -import net.hydromatic.morel.util.Pair; +import net.hydromatic.morel.util.PairList; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import org.apache.calcite.util.Holder; import org.checkerframework.checker.nullness.qual.Nullable; @@ -38,40 +34,47 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.function.UnaryOperator; +import java.util.function.Consumer; import static net.hydromatic.morel.ast.CoreBuilder.core; -import static net.hydromatic.morel.util.Pair.forEach; -import static net.hydromatic.morel.util.Static.append; -import static net.hydromatic.morel.util.Static.plus; import static net.hydromatic.morel.util.Static.skip; -import static com.google.common.collect.Iterables.getLast; -import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.ImmutableSet.toImmutableSet; /** - * Converts {@code suchThat} to {@code in} wherever possible. + * Converts unbounded variables to bounded variables. + * + *

For example, converts + * + *

{@code
+ * from e
+ *   where e elem #dept scott
+ * }
+ * + *

to + * + *

{@code
+ * from e in #dept scott
+ * }
*/ class SuchThatShuttle extends Shuttle { - final Deque fromStates = new ArrayDeque<>(); - final Environment env; + final @Nullable Environment env; - SuchThatShuttle(TypeSystem typeSystem, Environment env) { + SuchThatShuttle(TypeSystem typeSystem, @Nullable Environment env) { super(typeSystem); this.env = env; } - static boolean containsSuchThat(Core.Decl decl) { + static boolean containsUnbounded(Core.Decl decl) { final Holder found = Holder.of(false); decl.accept(new Visitor() { @Override protected void visit(Core.Scan scan) { super.visit(scan); - if (scan.op == Op.SUCH_THAT) { + if (Extents.isInfinite(scan.exp)) { found.set(true); } } @@ -80,332 +83,231 @@ static boolean containsSuchThat(Core.Decl decl) { } @Override protected Core.Exp visit(Core.From from) { - try { - final FromState fromState = new FromState(from); - fromStates.push(fromState); - for (Core.FromStep node : from.steps) { - fromState.steps.add(node.accept(this)); - } - return from.copy(typeSystem, env, fromState.steps); - } finally { - fromStates.pop(); - } + final Core.From from2 = new FromVisitor(typeSystem, env).visit(from); + return from2.equals(from) ? from : from2; } - @Override protected Core.Scan visit(Core.Scan scan) { - if (scan.op != Op.SUCH_THAT) { - return super.visit(scan); - } - final ImmutableSortedMap.Builder boundPatBuilder = - ImmutableSortedMap.orderedBy(Core.NamedPat.ORDERING); - if (!fromStates.element().steps.isEmpty()) { - getLast(fromStates.element().steps).bindings.forEach(b -> - boundPatBuilder.put(b.id, core.id(b.id))); - } - final SortedMap boundPats = boundPatBuilder.build(); - final Core.Exp rewritten = rewrite0(boundPats, scan.pat, scan.exp); - return core.scan(Op.INNER_JOIN, scan.bindings, - scan.pat, rewritten, scan.condition); - } + /** Workspace for converting unbounded variables in a particular + * {@link Core.From} to bounded scans. */ + static class FromVisitor { + final TypeSystem typeSystem; + final FromBuilder fromBuilder; + final List satisfiedFilters = new ArrayList<>(); - private Core.Exp rewrite0(SortedMap boundPats, - Core.Pat pat, Core.Exp exp) { - try { - final Map scans = ImmutableMap.of(); - final List filters = ImmutableList.of(); - final UnaryOperator originalPats = UnaryOperator.identity(); - return rewrite(originalPats, boundPats, scans, filters, - conjunctions(exp)); - } catch (RewriteFailedException e) { - // We could not rewrite. - // Try a different approach. - // Generate an iterator over all values of all variables, - // then filter. - final Core.Exp generator = Extents.generator(typeSystem, pat, exp); - final FromBuilder fromBuilder = core.fromBuilder(typeSystem); - return fromBuilder.scan(pat, generator) - .where(exp) - .build(); + FromVisitor(TypeSystem typeSystem, @Nullable Environment env) { + this.typeSystem = typeSystem; + this.fromBuilder = core.fromBuilder(typeSystem, env); } - } - /** Rewrites a "from vars suchthat condition" expression to a - * "from vars in list" expression; returns null if no rewrite is possible. - * - *

The "filters" argument contains a list of conditions to be applied - * after generating rows. For example, - * "from x suchthat x % 2 = 1 and x elem list" - * becomes "from x in list where x % 2 = 1" with the filter "x % 2 = 1". - * - *

The "scans" argument contains scans to be added. For example, - * "from x suchthat x elem list" adds the scan "(x, list)". - * - *

The "boundPats" argument contains expressions that are bound to - * variables. For example, "from (x, y) suchthat (x, y) elem list" - * will add the scan "(e, list)" and boundPats [(x, #1 e), (y, #2 e)]. - * - * @param mapper Renames variables - * @param boundPats Variables that have been bound to a list - * @param scans Scans (joins) to be appended to the resulting "from" - * @param filters Filters to be appended as "where" in the resulting "from" - * @param exps The condition, decomposed into conjunctions - * @return Rewritten expression - */ - private Core.Exp rewrite(UnaryOperator mapper, - SortedMap boundPats, - Map scans, List filters, - List exps) { - if (exps.isEmpty()) { - final ImmutableSortedMap.Builder b = - ImmutableSortedMap.naturalOrder(); - boundPats.forEach((p, e) -> b.put(mapper.apply(p), e)); - final SortedMap boundPats2 = b.build(); - - final SortedMap nameExps = RecordType.mutableMap(); - if (scans.isEmpty()) { - final Core.Scan scan = (Core.Scan) fromStates.element().currentStep(); - final Extents.Analysis extent = - Extents.create(typeSystem, scan.pat, boundPats2, scan.exp); - final Set unboundPats = extent.unboundPats(); - if (!unboundPats.isEmpty()) { - throw new RewriteFailedException("Cannot implement 'suchthat'; " - + "variables " + unboundPats + " are not grounded" + "]"); - } - boundPats2.forEach((p, e) -> { - if (extent.goalPats.contains(p)) { - nameExps.put(p.name, e); + Core.From visit(Core.From from) { + final List steps = from.steps; + final DeferredStepList deferredScans = + DeferredStepList.create(typeSystem, steps); + + Environment env = Environments.empty(); + final PairList idPats = PairList.of(); + for (int i = 0; i < steps.size(); i++) { + final Core.FromStep step = steps.get(i); + switch (step.op) { + case SCAN: + case INNER_JOIN: + final Core.Scan scan = (Core.Scan) step; + if (Extents.isInfinite(scan.exp)) { + final int idPatCount = idPats.size(); + final Core.Exp rewritten = rewrite1(scan, skip(steps, i), idPats); + + // Create a scan for any new variables introduced by rewrite. + idPats.forEachIndexed((j, pat, extent) -> { + if (j >= idPatCount) { + fromBuilder.scan(pat, extent); + } + }); + deferredScans.scan(env, scan.pat, rewritten, scan.condition); + } else { + deferredScans.scan(env, scan.pat, scan.exp); } - }); - } else { - boundPats2.forEach((p, e) -> nameExps.put(p.name, e)); - } + break; - final FromBuilder fromBuilder = core.fromBuilder(typeSystem); - scans.forEach(fromBuilder::scan); - filters.forEach(fromBuilder::where); - fromBuilder.yield_(nameExps.size() == 1 - ? getOnlyElement(nameExps.values()) - : core.record(typeSystem, nameExps)); + case YIELD: + final Core.Yield yield = (Core.Yield) step; + killTemporaryScans(idPats); + deferredScans.flush(fromBuilder); + fromBuilder.yield_(false, yield.bindings, yield.exp); + break; + + case WHERE: + final Core.Where where = (Core.Where) step; + Core.Exp condition = + core.subTrue(typeSystem, where.exp, satisfiedFilters); + deferredScans.where(env, condition); + break; + + case GROUP: + final Core.Group group = (Core.Group) step; + killTemporaryScans(idPats); + deferredScans.flush(fromBuilder); + fromBuilder.group(group.groupExps, group.aggregates); + break; + + case ORDER: + final Core.Order order = (Core.Order) step; + killTemporaryScans(idPats); + deferredScans.flush(fromBuilder); + fromBuilder.order(order.orderItems); + break; + + default: + throw new AssertionError(step.op); + } + env = Environments.empty().bindAll(step.bindings); + } + deferredScans.flush(fromBuilder); + killTemporaryScans(idPats); return fromBuilder.build(); } - final Core.Exp exp = exps.get(0); - final List exps2 = skip(exps); - switch (exp.op) { - case APPLY: - final Core.Apply apply = (Core.Apply) exp; - if (exp.isCallTo(BuiltIn.OP_ELEM)) { - Core.Exp a0 = apply.args().get(0); - Core.Exp a1 = apply.args().get(1); - Core.@Nullable Exp e = - rewriteElem(typeSystem, mapper, boundPats, scans, filters, a0, a1, - exps2); - if (e != null) { - return e; - } - throw new AssertionError(exp); + + private void killTemporaryScans(PairList idPats) { + if (idPats.isEmpty()) { + return; } - if (exp.isCallTo(BuiltIn.OP_EQ)) { - Core.Exp a0 = apply.args().get(0); - Core.Exp a1 = apply.args().get(1); - if (a1.op == Op.ID && a0.op != Op.ID) { - final Core.Exp tmp = a0; - a0 = a1; - a1 = tmp; - } - Core.Exp a1List = core.list(typeSystem, a1); - Core.@Nullable Exp e = - rewriteElem(typeSystem, mapper, boundPats, scans, - filters, a0, a1List, exps2); - if (e != null) { - return e; + final PairList nameExps = PairList.of(); + for (Binding b : fromBuilder.bindings()) { + Core.IdPat id = (Core.IdPat) b.id; + if (!idPats.leftList().contains(id)) { + nameExps.add(id.name, core.id(id)); } } - final List filters2 = append(filters, exp); - return rewrite(mapper, boundPats, scans, filters2, - exps2); - - case CASE: - final Core.Case case_ = (Core.Case) exp; - if (case_.matchList.size() == 1) { - // A simple renaming case, e.g. "case (e, j) of (a, b) => #job a = b", - // boundPats is "{e}" translate as if the expression were "#job e = j", - // boundPats = "{a}". - final Core.Match match = case_.matchList.get(0); - final SortedMap boundPats2 = - new TreeMap<>(boundPats.comparator()); - boundPats.forEach((p, e) -> boundPats2.put(mapper.apply(p), e)); - final PatMap patMap = PatMap.of(match.pat, case_.exp); - return rewrite(mapper.andThen(patMap::apply)::apply, boundPats2, - scans, filters, plus(match.exp, exps2)); + if (nameExps.size() == 1) { + fromBuilder.yield_(false, null, nameExps.get(0).getValue()); + } else { + fromBuilder.yield_(false, null, core.record(typeSystem, nameExps)); } - break; + idPats.clear(); } - throw new RewriteFailedException("not implemented: suchthat " + exp.op - + " [" + exp + "]"); - } - - private Core.@Nullable Exp rewriteElem(TypeSystem typeSystem, - UnaryOperator mapper, - SortedMap boundPats, - Map scans, - List filters, Core.Exp a0, - Core.Exp a1, List exps2) { - if (a0.op == Op.ID) { - // from ... v suchthat (v elem list) - final Core.IdPat idPat = (Core.IdPat) ((Core.Id) a0).idPat; - if (!boundPats.containsKey(idPat)) { - // "from a, b, c suchthat (b in list-valued-expression)" - // --> remove b from unbound - // add (b, scans.size) to bound - // add list to scans - // from ... v in list - final SortedMap boundPats2 = - plus(boundPats, idPat, core.id(idPat)); - final Map scans2 = plus(scans, idPat, a1); - return rewrite(mapper, boundPats2, scans2, filters, exps2); - } else { - final Core.Exp e = boundPats.get(idPat); - final List filters2 = - append(filters, core.elem(typeSystem, e, a1)); - return rewrite(mapper, boundPats, scans, filters2, exps2); - } - } else if (a0.op == Op.TUPLE) { - // from v, w, x suchthat ((v, w, x) elem list) - // --> - // from e suchthat (e elem list) - // yield (e.v, e.w, e.x) - final Core.Tuple tuple = (Core.Tuple) a0; - final Core.IdPat idPat = - core.idPat(((ListType) a1.type).elementType, - typeSystem.nameGenerator); - final Core.Id id = core.id(idPat); - SortedMap boundPats2 = boundPats; - List filters2 = filters; - for (Ord arg : Ord.zip(tuple.args)) { - final Core.Exp e = core.field(typeSystem, id, arg.i); - if (arg.e instanceof Core.Id) { - final Core.NamedPat idPat2 = ((Core.Id) arg.e).idPat; - if (!boundPats2.containsKey(idPat2)) { - // This variable was not previously bound; bind it. - boundPats2 = plus(boundPats2, idPat2, e); - } else { - // This variable is already bound; now add a filter. - filters2 = - append(filters, core.equal(typeSystem, e, arg.e)); - } - } else { - filters2 = - append(filters, core.equal(typeSystem, e, arg.e)); - } - } - final Map scans2 = plus(scans, idPat, a1); - return rewrite(mapper, boundPats2, scans2, filters2, exps2); - } else { - return null; + /** Rewrites an unbounded scan to a {@code from} expression, + * using predicates in later steps to determine the ranges of variables. */ + private Core.From rewrite1(Core.Scan scan, + List laterSteps, + PairList idPats) { + final Extents.Analysis analysis = + Extents.create(typeSystem, scan.pat, ImmutableSortedMap.of(), + laterSteps, idPats); + satisfiedFilters.addAll(analysis.satisfiedFilters); + final FromBuilder fromBuilder = core.fromBuilder(typeSystem); + fromBuilder.scan(scan.pat, analysis.extentExp); + return fromBuilder.build(); } } - /** - * Returns an expression as a list of conjunctions. + /** Maintains a list of steps that have not been applied yet. * - *

For example - * {@code conjunctions(a andalso b)} - * returns [{@code a}, {@code b}] (two elements); - * {@code conjunctions(a andalso b andalso c)} - * returns [{@code a}, {@code b}, {@code c}] (three elements); - * {@code conjunctions(a orelse b)} - * returns [{@code a orelse b}] (one element); - * {@code conjunctions(true)} - * returns [] (no elements); - * {@code conjunctions(false)} - * returns [{@code false}] (one element). + *

Holds the state necessary for a classic topological sort algorithm: + * For each node, keep the list of unresolved forward references. + * After each reference is resolved, remove it from each node's list. + * Output each node as its unresolved list becomes empty. + * The topological sort is stable. */ - static List conjunctions(Core.Exp e) { - final ImmutableList.Builder b = ImmutableList.builder(); - addConjunctions(b, e); - return b.build(); - } + static class DeferredStepList { + final PairList, Consumer> steps = PairList.of(); + final FreeFinder freeFinder; + final List refs; - private static void addConjunctions(ImmutableList.Builder b, - Core.Exp e) { - if (e.op == Op.APPLY - && ((Core.Apply) e).fn.op == Op.FN_LITERAL - && ((Core.Literal) ((Core.Apply) e).fn).value == BuiltIn.Z_ANDALSO) { - ((Core.Apply) e).args().forEach(a -> addConjunctions(b, a)); - } else if (e.op != Op.BOOL_LITERAL - || !((boolean) ((Core.Literal) e).value)) { - // skip true - b.add(e); + DeferredStepList(FreeFinder freeFinder, List refs) { + this.freeFinder = freeFinder; + this.refs = refs; } - } - /** Workspace for converting a particular {@link Core.From} from "suchthat" - * to "in" form. */ - static class FromState { - final Core.From from; - final List steps = new ArrayList<>(); - - FromState(Core.From from) { - this.from = from; + static DeferredStepList create(TypeSystem typeSystem, + List steps) { + final ImmutableSet forwardRefs = + steps.stream() + .filter(step -> step instanceof Core.Scan) + .map(step -> ((Core.Scan) step).pat) + .collect(toImmutableSet()); + final List refs = new ArrayList<>(); + final Consumer consumer = p -> { + if (forwardRefs.contains(p)) { + refs.add(p); + } + }; + final FreeFinder freeFinder = + new FreeFinder(typeSystem, Environments.empty(), + new ArrayDeque<>(), consumer); + return new DeferredStepList(freeFinder, refs); } - Core.FromStep currentStep() { - // We assume that from.steps are translated 1:1 into steps that get added - // to this.steps. If steps.size() is N, we are currently working on - // from.steps.get(N). - return from.steps.get(steps.size()); + void scan(Environment env, Core.Pat pat, Core.Exp exp, Core.Exp condition) { + final Set unresolvedRefs = unresolvedRefs(env, exp); + steps.add(unresolvedRefs, fromBuilder -> { + fromBuilder.scan(pat, exp, condition); + resolve(pat); + }); } - } - /** Maps patterns from their name in the "from" to their name after a sequence - * of renames. - * - *

For example, in "case (x, y) of (a, b) => a + b", "x" is renamed to "a" - * and "y" is renamed to "b". */ - private static class PatMap { - private final ImmutableMap map; + void scan(Environment env, Core.Pat pat, Core.Exp exp) { + final Set unresolvedRefs = unresolvedRefs(env, exp); + steps.add(unresolvedRefs, fromBuilder -> { + fromBuilder.scan(pat, exp); + resolve(pat); + }); + } - PatMap(ImmutableMap map) { - this.map = map; + void where(Environment env, Core.Exp condition) { + final Set unresolvedRefs = unresolvedRefs(env, condition); + steps.add(unresolvedRefs, + fromBuilder -> fromBuilder.where(condition)); } - static PatMap of(Core.Pat pat, Core.Exp exp) { - final ImmutableMap.Builder builder = - ImmutableMap.builder(); - populate(pat, exp, builder); - return new PatMap(builder.build()); + private Set unresolvedRefs(Environment env, Core.Exp exp) { + refs.clear(); + exp.accept(freeFinder.push(env)); + return new LinkedHashSet<>(refs); } - private static void populate(Core.Pat pat, Core.Exp exp, - ImmutableMap.Builder nameBuilder) { - switch (pat.op) { - case ID_PAT: - nameBuilder.put(Pair.of(((Core.Id) exp).idPat, (Core.IdPat) pat)); - break; - case TUPLE_PAT: - final Core.TuplePat tuplePat = (Core.TuplePat) pat; - final Core.Tuple tuple = (Core.Tuple) exp; - forEach(tuplePat.args, tuple.args, - (pat2, exp2) -> populate(pat2, exp2, nameBuilder)); - break; - } + /** Marks that a pattern has now been defined. + * + *

After this method, it is possible that some steps might have no + * unresolved references. Those steps are now ready to add to the + * builder. */ + void resolve(Core.Pat pat) { + steps.forEach((unresolvedRefs, consumer) -> unresolvedRefs.remove(pat)); } - Core.NamedPat apply(Core.NamedPat p) { - for (Map.Entry pair : map.entrySet()) { - if (pair.getValue().equals(p)) { - p = pair.getKey(); + void flush(FromBuilder fromBuilder) { + // Are there any scans that had forward references previously but + // whose references are now all satisfied? Add them to the builder. + for (;;) { + int j = steps.firstMatch((unresolvedRefs, consumer) -> + unresolvedRefs.isEmpty()); + if (j < 0) { + break; } + final Map.Entry, Consumer> step = + steps.remove(j); + step.getValue().accept(fromBuilder); } - return p; } } - /** Signals that we could not rewrite. */ - private static class RewriteFailedException extends RuntimeException { - RewriteFailedException(String message) { - super(message); + /** Finds free variables in an expression. */ + private static class FreeFinder extends EnvVisitor { + final Consumer consumer; + + FreeFinder(TypeSystem typeSystem, Environment env, + Deque fromStack, Consumer consumer) { + super(typeSystem, env, fromStack); + this.consumer = consumer; + } + + @Override protected EnvVisitor push(Environment env) { + return new FreeFinder(typeSystem, env, fromStack, consumer); + } + + @Override protected void visit(Core.Id id) { + if (env.getOpt(id.idPat) == null) { + consumer.accept(id.idPat); + } } } } diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index 2d6276aed..c100d700d 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -484,49 +484,29 @@ private Pair deduceStepType(TypeEnv env, final Ast.Scan scan = (Ast.Scan) step; final Ast.Exp scanExp; final boolean eq; + final Ast.Exp scanExp3; final Unifier.Variable v15 = unifier.variable(); final Unifier.Variable v16 = unifier.variable(); final Map termMap1 = new HashMap<>(); - switch (scan.exp.op) { - case SUCH_THAT: + if (scan.exp == null) { + scanExp = null; + eq = false; + scanExp3 = null; + } else if (scan.exp.op == Op.FROM_EQ) { scanExp = ((Ast.PrefixCall) scan.exp).a; - final Ast.Pat pat2 = - deducePatType(env2, scan.pat, termMap1, null, v16); - TypeEnv env4 = env2; - for (Map.Entry e : termMap1.entrySet()) { - env4 = env4.bind(e.getKey().name, e.getValue()); - fieldVars.put(ast.id(Pos.ZERO, e.getKey().name), - (Unifier.Variable) e.getValue()); - } - final Ast.Exp scanExp2 = deduceType(env4, scanExp, v15); - final Ast.Exp scanExp3 = ast.fromSuchThat(scanExp2); - reg(scanExp, v15, toTerm(PrimitiveType.BOOL)); - final Ast.Exp scanCondition2; - if (scan.condition != null) { - final Unifier.Variable v5 = unifier.variable(); - scanCondition2 = deduceType(env4, scan.condition, v5); - equiv(v5, toTerm(PrimitiveType.BOOL)); - } else { - scanCondition2 = null; - } - fromSteps.add(scan.copy(pat2, scanExp3, scanCondition2)); - return Pair.of(env4, v); - - case FROM_EQ: eq = true; - scanExp = ((Ast.PrefixCall) scan.exp).a; - break; - - default: - eq = false; + final Ast.Exp scanExp2 = deduceType(env2, scanExp, v15); + scanExp3 = ast.fromEq(scanExp2); + } else { scanExp = scan.exp; - break; + eq = false; + scanExp3 = deduceType(env2, scanExp, v15); } - final Ast.Exp scanExp2 = deduceType(env2, scanExp, v15); - final Ast.Exp scanExp3 = eq ? ast.fromEq(scanExp2) : scanExp2; final Ast.Pat pat2 = deducePatType(env2, scan.pat, termMap1, null, v16); - reg(scanExp, v15, eq ? v16 : unifier.apply(LIST_TY_CON, v16)); + if (scanExp != null) { + reg(scanExp, v15, eq ? v16 : unifier.apply(LIST_TY_CON, v16)); + } TypeEnv env4 = env2; for (Map.Entry e : termMap1.entrySet()) { env4 = env4.bind(e.getKey().name, e.getValue()); diff --git a/src/main/java/net/hydromatic/morel/eval/Codes.java b/src/main/java/net/hydromatic/morel/eval/Codes.java index 5a14badf7..cf2bfe97a 100644 --- a/src/main/java/net/hydromatic/morel/eval/Codes.java +++ b/src/main/java/net/hydromatic/morel/eval/Codes.java @@ -2663,7 +2663,10 @@ private static Applicable vectorFindi(Applicable f) { new ApplicableImpl(BuiltIn.Z_EXTENT) { @Override public List apply(EvalEnv env, Object arg) { final RangeExtent rangeExtent = (RangeExtent) arg; - return Lists.newArrayList(rangeExtent.toIterable()); + if (rangeExtent.iterable == null) { + throw new AssertionError("infinite: " + rangeExtent); + } + return ImmutableList.copyOf(rangeExtent.iterable); } }; diff --git a/src/main/java/net/hydromatic/morel/foreign/Converters.java b/src/main/java/net/hydromatic/morel/foreign/Converters.java index f94a0058b..628686326 100644 --- a/src/main/java/net/hydromatic/morel/foreign/Converters.java +++ b/src/main/java/net/hydromatic/morel/foreign/Converters.java @@ -71,7 +71,7 @@ public static Converter ofRow(RelDataType rowType) { public static Converter ofRow2(RelDataType rowType, RecordLikeType type) { return ofRow3(rowType.getFieldList().iterator(), - new AtomicInteger(), Linq4j.enumerator(type.argNameTypes().values())); + new AtomicInteger(), Linq4j.enumerator(type.argTypes())); } static Converter ofRow3(Iterator fields, diff --git a/src/main/java/net/hydromatic/morel/type/BaseType.java b/src/main/java/net/hydromatic/morel/type/BaseType.java index 17019a773..7301db251 100644 --- a/src/main/java/net/hydromatic/morel/type/BaseType.java +++ b/src/main/java/net/hydromatic/morel/type/BaseType.java @@ -37,6 +37,16 @@ public Op op() { @Override public String toString() { return key().toString(); } + + @Override public boolean equals(Object o) { + return this == o + || o instanceof BaseType + && key().equals(((BaseType) o).key()); + } + + @Override public int hashCode() { + return key().hashCode(); + } } // End BaseType.java diff --git a/src/main/java/net/hydromatic/morel/type/DummyType.java b/src/main/java/net/hydromatic/morel/type/DummyType.java index aca4c5513..ff9236f7b 100644 --- a/src/main/java/net/hydromatic/morel/type/DummyType.java +++ b/src/main/java/net/hydromatic/morel/type/DummyType.java @@ -33,7 +33,7 @@ public Key key() { } public Op op() { - return Op.NAMED_TYPE; + return Op.DUMMY_TYPE; } public R accept(TypeVisitor typeVisitor) { diff --git a/src/main/java/net/hydromatic/morel/type/Keys.java b/src/main/java/net/hydromatic/morel/type/Keys.java index 168dabb44..00f0045a4 100644 --- a/src/main/java/net/hydromatic/morel/type/Keys.java +++ b/src/main/java/net/hydromatic/morel/type/Keys.java @@ -266,7 +266,7 @@ private static class ApplyKey extends Type.Key { final Type.Key key; final ImmutableList args; - ApplyKey(Type.Key key, ImmutableList args) { + ApplyKey(Type.Key key, List args) { super(Op.APPLY_TYPE); this.key = requireNonNull(key); this.args = ImmutableList.copyOf(args); diff --git a/src/main/java/net/hydromatic/morel/type/PrimitiveType.java b/src/main/java/net/hydromatic/morel/type/PrimitiveType.java index b40048b12..e0097d02b 100644 --- a/src/main/java/net/hydromatic/morel/type/PrimitiveType.java +++ b/src/main/java/net/hydromatic/morel/type/PrimitiveType.java @@ -59,10 +59,15 @@ public enum PrimitiveType implements RecordLikeType { return Op.ID; } + public R accept(TypeVisitor typeVisitor) { return typeVisitor.visit(this); } + @Override public boolean isFinite() { + return this == BOOL || this == UNIT; + } + @Override public PrimitiveType copy(TypeSystem typeSystem, UnaryOperator transform) { return this; diff --git a/src/main/java/net/hydromatic/morel/type/RangeExtent.java b/src/main/java/net/hydromatic/morel/type/RangeExtent.java index d7fdf674b..4bd949717 100644 --- a/src/main/java/net/hydromatic/morel/type/RangeExtent.java +++ b/src/main/java/net/hydromatic/morel/type/RangeExtent.java @@ -18,193 +18,196 @@ */ package net.hydromatic.morel.type; -import com.google.common.collect.ContiguousSet; -import com.google.common.collect.DiscreteDomain; -import com.google.common.collect.ImmutableList; +import net.hydromatic.morel.ast.Op; + +import com.google.common.collect.BoundType; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableRangeSet; -import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import org.apache.calcite.runtime.FlatLists; -import org.apache.calcite.util.RangeSets; +import org.apache.calcite.runtime.Unit; +import org.checkerframework.checker.nullness.qual.Nullable; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; -import javax.annotation.CheckForNull; +import java.util.Map; +import java.util.function.Consumer; /** A type and a range set. */ -@SuppressWarnings("UnstableApiUsage") +@SuppressWarnings("rawtypes") public class RangeExtent { - public final RangeSet rangeSet; + /** Map from path to range set. + * + *

The path designates the item within the type. + * + *

For example, consider the type {@code int option list}. + * + *

    + *
  • "/" relates to the {@code int option list} + *
  • "/0/" relates to each {@code int option} within the list + *
  • "/0/SOME/" relates to each {@code int} within a "SOME" element of the + * list + *
  • "/0/NONE/" relates to each "NONE" element of the list + *
+ * + *

Using a map {@code ["/O/SOME/" "[0, 3], [6]"]} we can generate + * {@code SOME 0, SOME 1, SOME 2, SOME 3, SOME 6, NONE} + */ + public final Map rangeSetMap; public final Type type; - private final Iterable iterable; - - private static final List BOOLEANS = ImmutableList.of(false, true); + public final @Nullable Iterable iterable; /** Creates a RangeExtent. */ - public RangeExtent(RangeSet rangeSet, Type type) { - this.rangeSet = ImmutableRangeSet.copyOf(rangeSet); + @SuppressWarnings("unchecked") + public RangeExtent(TypeSystem typeSystem, Type type, + Map rangeSetMap) { + this.rangeSetMap = + ImmutableMap.copyOf( + Maps.transformValues(rangeSetMap, + r -> ImmutableRangeSet.copyOf(r))); this.type = type; - this.iterable = toIterable(type, rangeSet); + this.iterable = toList(type, typeSystem); } @Override public String toString() { - return type + " " + rangeSet; + if (isUnbounded()) { + return type.toString(); // range set is unconstrained; don't print it + } + return type + " " + rangeSetMap; } - /** Returns the collection of values in the range. */ - @SuppressWarnings("unchecked") - public Iterable toIterable() { - return iterable; + /** Whether this extent returns all, or an unbounded number of, the values of + * its type. + * + *

Examples: + * "(-inf,+inf)" (true), + * "(-inf,0]" (x ≤ 0), + * "{(-inf,3),(10,+inf)}" (x < 3 or x > 10) are unbounded; + * "{}" (false), + * "{3, 10}" (x in [3, 10]), + * "(3, 10)" (x ≥ 3 andalso x ≤ 10) are bounded. */ + public boolean isUnbounded() { + return rangeSetMap.isEmpty(); } - /** Returns the collection of values in the range. */ - @SuppressWarnings("unchecked") - private Iterable toIterable(Type type, RangeSet rangeSet) { - final List> setList = new ArrayList<>(); - rangeSet.asRanges() - .forEach(range -> setList.add(toIterable(type, range))); - return concat(setList); + /** Derives the collection of values in the range, or returns empty if + * the range is infinite. */ + private > Iterable toList(Type type, + TypeSystem typeSystem) { + final List list = new ArrayList<>(); + if (populate(typeSystem, type, "/", rangeSetMap, (Consumer) list::add)) { + return list; + } + return null; } - /** Returns the collection of values in the range. */ + /** Populates a list (or other consumer) with all values of this type. Returns + * false if this type is not finite and the range is open above or below. */ @SuppressWarnings("unchecked") - private Iterable toIterable(Type type, Range range) { + private > boolean populate(TypeSystem typeSystem, + Type type, String path, Map rangeSetMap, + Consumer consumer) { + final RangeSet rangeSet = rangeSetMap.get(path); + final Consumer filteredConsumer; + if (rangeSet != null) { + filteredConsumer = e -> { + if (rangeSet.contains(e)) { + consumer.accept(e); + } + }; + } else { + filteredConsumer = consumer; + } switch (type.op()) { case ID: - final PrimitiveType primitiveType = (PrimitiveType) type; - switch (primitiveType) { - case INT: - return ContiguousSet.create( - RangeSets.copy(range, BigDecimal::intValue), - DiscreteDomain.integers()); - + switch ((PrimitiveType) type) { case BOOL: - return Iterables.filter(BOOLEANS, b -> range.contains(b)); - } - break; + filteredConsumer.accept((E) Boolean.FALSE); + filteredConsumer.accept((E) Boolean.TRUE); + return true; - case TUPLE_TYPE: - final TupleType tupleType = (TupleType) type; - // TODO: copy rangeSet, to convert embedded BigDecimal to Integer - return ContiguousSet.create(range, discreteDomain(tupleType)); - } - throw new AssertionError("cannot iterate type '" + type + "'"); - } + case UNIT: + filteredConsumer.accept((E) Unit.INSTANCE); + return true; - private DiscreteDomain discreteDomain(Type type) { - switch (type.op()) { - case ID: - final PrimitiveType primitiveType = (PrimitiveType) type; - switch (primitiveType) { - case BOOL: - return new BooleanDiscreteDomain(); + case CHAR: + for (int i = 0; i < 256; i++) { + filteredConsumer.accept((E) Character.valueOf((char) i)); + } + return true; case INT: - return DiscreteDomain.integers(); + if (rangeSet != null) { + for (Range range : rangeSet.asRanges()) { + if (!range.hasLowerBound() || !range.hasUpperBound()) { + return false; + } + final int lower = + ((BigDecimal) range.lowerEndpoint()).intValue() + + (range.lowerBoundType() == BoundType.OPEN ? 1 : 0); + final int upper = + ((BigDecimal) range.upperEndpoint()).intValue() + - (range.upperBoundType() == BoundType.OPEN ? 1 : 0); + for (int i = lower; i <= upper; i++) { + consumer.accept((E) Integer.valueOf(i)); + } + } + return true; + } + // fall through } - break; - - case TUPLE_TYPE: - final List> domains = new ArrayList<>(); - final TupleType tupleType = (TupleType) type; - tupleType.argTypes.forEach(t -> domains.add(discreteDomain(t))); - return new ProductDiscreteDomain(domains); - } - - throw new AssertionError("cannot convert type '" + type - + "' to discrete domain"); - } - - /** Calls {@link Iterables#concat(Iterable)}, optimizing for the case with 0 - * or 1 entries. */ - private static Iterable concat( - List> iterableList) { - switch (iterableList.size()) { - case 0: - return ImmutableList.of(); - case 1: - return iterableList.get(0); - default: - return Iterables.concat(iterableList); - } - } - - private static class BooleanDiscreteDomain extends DiscreteDomain { - @CheckForNull - @Override public Boolean next(Boolean value) { - return value ? null : true; - } - - @CheckForNull @Override public Boolean previous(Boolean value) { - return value ? false : null; - } - - @Override public long distance(Boolean start, Boolean end) { - return (end == (boolean) start) ? 0 : (end ? 1 : -1); - } - - @Override public Boolean minValue() { return false; - } - @Override public Boolean maxValue() { + case DUMMY_TYPE: + assert type == DummyType.INSTANCE; + filteredConsumer.accept((E) Unit.INSTANCE); return true; - } - } - private static class ProductDiscreteDomain - extends DiscreteDomain> { - private final List domains; - private final FlatLists.ComparableList minValue; - private final FlatLists.ComparableList maxValues; - - ProductDiscreteDomain(List> domains) { - this.domains = ImmutableList.copyOf(domains); - this.minValue = FlatLists.ofComparable( - domains.stream() - .map(DiscreteDomain::minValue) - .collect(Collectors.toList())); - this.maxValues = FlatLists.ofComparable( - domains.stream() - .map(DiscreteDomain::maxValue) - .collect(Collectors.toList())); - } - - @CheckForNull @Override public FlatLists.ComparableList next( - FlatLists.ComparableList values) { - final Comparable[] objects = values.toArray(new Comparable[0]); - for (int i = 0; i < values.size(); i++) { - Comparable value = values.get(i); - final DiscreteDomain domain = domains.get(i); - Comparable next = domain.next(value); - if (next != null) { - objects[i] = next; - return (FlatLists.ComparableList) FlatLists.of(objects); + case DATA_TYPE: + final DataType dataType = (DataType) type; + for (Map.Entry entry + : dataType.typeConstructors(typeSystem).entrySet()) { + final String name = entry.getKey(); + final Type type2 = entry.getValue(); + final Consumer consumer2 = + type2.op() == Op.DUMMY_TYPE + ? v -> filteredConsumer.accept((E) FlatLists.of(name)) + : v -> filteredConsumer.accept((E) FlatLists.of(name, v)); + if (!populate(typeSystem, type2, path + name + "/", rangeSetMap, + consumer2)) { + return false; } - objects[i] = domain.minValue(); } - return null; - } - - @CheckForNull @Override public FlatLists.ComparableList previous( - FlatLists.ComparableList values) { - throw new UnsupportedOperationException(); // TODO implement, like next - } + return true; - @Override public long distance(FlatLists.ComparableList start, - FlatLists.ComparableList end) { - // A better implementation might be to compute distances between each - // pair of values, and multiply by the number of superior values. - long d = 0; - for (FlatLists.ComparableList c = start; - c != null && c.compareTo(end) < 0; - c = next(c)) { - ++d; + case RECORD_TYPE: + case TUPLE_TYPE: + final RecordLikeType recordType = (RecordLikeType) type; + final List> listList = new ArrayList<>(); + for (Map.Entry entry + : recordType.argNameTypes().entrySet()) { + final String name = entry.getKey(); + final Type type2 = entry.getValue(); + final List list2 = new ArrayList<>(); + final Consumer consumer2 = list2::add; + if (!populate(typeSystem, type2, path + name + '/', rangeSetMap, + consumer2)) { + return false; + } + listList.add(list2); } - return d; + Lists.cartesianProduct(listList) + .forEach(list -> + filteredConsumer.accept((E) FlatLists.ofComparable((List) list))); + return true; + + default: + // All other types are not enumerable + return false; } } } diff --git a/src/main/java/net/hydromatic/morel/type/RecordLikeType.java b/src/main/java/net/hydromatic/morel/type/RecordLikeType.java index d1b9ce08e..1b68d7dec 100644 --- a/src/main/java/net/hydromatic/morel/type/RecordLikeType.java +++ b/src/main/java/net/hydromatic/morel/type/RecordLikeType.java @@ -18,14 +18,22 @@ */ package net.hydromatic.morel.type; +import com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.List; import java.util.SortedMap; /** A type that has named fields, as a record type does. */ public interface RecordLikeType extends Type { + /** Returns a map of the field types, keyed by field names. */ SortedMap argNameTypes(); + /** Returns a list of field types, ordered by field names. */ + default List argTypes() { + return ImmutableList.copyOf(argNameTypes().values()); + } + /** Returns the type of the {@code i}th field, or throws. */ Type argType(int i); diff --git a/src/main/java/net/hydromatic/morel/type/TupleType.java b/src/main/java/net/hydromatic/morel/type/TupleType.java index cd8130365..13ac7c7b9 100644 --- a/src/main/java/net/hydromatic/morel/type/TupleType.java +++ b/src/main/java/net/hydromatic/morel/type/TupleType.java @@ -50,6 +50,10 @@ public class TupleType extends BaseType implements RecordLikeType { return map.build(); } + @Override public List argTypes() { + return argTypes; + } + @Override public Type argType(int i) { return argTypes.get(i); } diff --git a/src/main/java/net/hydromatic/morel/type/Type.java b/src/main/java/net/hydromatic/morel/type/Type.java index 4add83ab5..271e73bfe 100644 --- a/src/main/java/net/hydromatic/morel/type/Type.java +++ b/src/main/java/net/hydromatic/morel/type/Type.java @@ -76,6 +76,12 @@ default boolean isProgressive() { return false; } + /** Whether this type has a small, fixed set of instances. + * True for {@code bool}, data types on finite types. */ + default boolean isFinite() { + return false; + } + /** Structural identifier of a type. */ abstract class Key { public final Op op; diff --git a/src/main/java/net/hydromatic/morel/util/Static.java b/src/main/java/net/hydromatic/morel/util/Static.java index 3520b9c71..383805250 100644 --- a/src/main/java/net/hydromatic/morel/util/Static.java +++ b/src/main/java/net/hydromatic/morel/util/Static.java @@ -185,21 +185,7 @@ public static Iterable transform(Iterable elements, * function to each element. */ public static ImmutableList transformEager( Iterable elements, Function mapper) { - if (elements instanceof Collection - && ((Collection) elements).isEmpty()) { - // Save ourselves the effort of creating a Builder. - return ImmutableList.of(); - } - final ImmutableList.Builder b = ImmutableList.builder(); - elements.forEach(e -> b.add(mapper.apply(e))); - return b.build(); - } - - /** Eagerly converts a List to an ImmutableList, applying a mapping - * function to each element. */ - public static ImmutableList transformEager( - List elements, Function mapper) { - if (elements.isEmpty()) { + if (Iterables.isEmpty(elements)) { // Save ourselves the effort of creating a Builder. return ImmutableList.of(); } diff --git a/src/main/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 52bd936d7..8e8c415d9 100644 --- a/src/main/javacc/MorelParser.jj +++ b/src/main/javacc/MorelParser.jj @@ -345,7 +345,10 @@ void fromFirstStep(List steps) : } { patExp = fromSource() { - final Span span = Span.of(patExp.left, patExp.right); + final Span span = + patExp.right != null + ? Span.of(patExp.left, patExp.right) + : Span.of(patExp.left); steps.add(ast.scan(span.pos(), Op.SCAN, patExp.left, patExp.right, null)); } } @@ -424,23 +427,30 @@ void fromStep(List steps) : Pair fromSource() : { + final Id id; final Exp exp; final Pat pat; } { - pat = pat() ( - exp = expression() { - return Pair.of(pat, exp); - } - | - exp = expression() { - return Pair.of(pat, ast.fromEq(exp)); + LOOKAHEAD( + ( | ) + ( | | | | + | | | | )) + id = identifier() { + return Pair.of(ast.idPat(id.pos, id.name), null); } | - exp = expression() { - return Pair.of(pat, ast.fromSuchThat(exp)); - } + pat = pat() + ( + exp = expression() { + return Pair.of(pat, exp); + } + | + exp = expression() { + return Pair.of(pat, ast.fromEq(exp)); + } + ) ) } @@ -1415,7 +1425,6 @@ AstNode statementEof() : | < OF: "of" > | < ORELSE: "orelse" > | < REC: "rec" > -| < SUCH_THAT: "suchthat" > | < THEN: "then" > | < UNION: "union" > | < VAL: "val" > diff --git a/src/test/java/net/hydromatic/morel/FromBuilderTest.java b/src/test/java/net/hydromatic/morel/FromBuilderTest.java index 81f012155..2accc03c8 100644 --- a/src/test/java/net/hydromatic/morel/FromBuilderTest.java +++ b/src/test/java/net/hydromatic/morel/FromBuilderTest.java @@ -22,7 +22,9 @@ import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.FromBuilder; import net.hydromatic.morel.compile.Environments; +import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.PrimitiveType; +import net.hydromatic.morel.type.Type; import net.hydromatic.morel.type.TypeSystem; import net.hydromatic.morel.util.PairList; @@ -30,7 +32,10 @@ import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; import java.util.Arrays; +import java.util.List; +import java.util.function.Function; import static net.hydromatic.morel.ast.CoreBuilder.core; @@ -45,15 +50,25 @@ public class FromBuilderTest { private static class Fixture { final TypeSystem typeSystem = new TypeSystem(); final PrimitiveType intType = PrimitiveType.INT; + final PrimitiveType unitType = PrimitiveType.UNIT; + final Type intPairType = typeSystem.tupleType(intType, intType); final Core.IdPat aPat = core.idPat(intType, "a", 0); final Core.Id aId = core.id(aPat); final Core.IdPat bPat = core.idPat(intType, "b", 0); + final Core.Id bId = core.id(bPat); + final Core.IdPat dPat = core.idPat(intPairType, "d", 0); + final Core.Id dId = core.id(dPat); final Core.IdPat iPat = core.idPat(intType, "i", 0); final Core.Id iId = core.id(iPat); final Core.IdPat jPat = core.idPat(intType, "j", 0); final Core.Id jId = core.id(jPat); + final Core.IdPat uPat = core.idPat(unitType, "u", 0); final Core.Exp list12 = core.list(typeSystem, intLiteral(1), intLiteral(2)); final Core.Exp list34 = core.list(typeSystem, intLiteral(3), intLiteral(4)); + final Core.Exp tuple12 = + core.tuple(typeSystem, intLiteral(1), intLiteral(2)); + final Core.Exp tuple34 = + core.tuple(typeSystem, intLiteral(3), intLiteral(4)); Core.Literal intLiteral(int i) { return core.literal(intType, i); @@ -193,6 +208,108 @@ FromBuilder fromBuilder() { assertThat(e, is(from)); } + @Test void testNested3() { + // from i in (from j in [1, 2]) where i > 1 + // ==> + // from j in [1, 2] yield {i = j} where i > 1 + final Fixture f = new Fixture(); + final Core.From innerFrom = + f.fromBuilder() + .scan(f.jPat, f.list12) + .build(); + + final FromBuilder fromBuilder = f.fromBuilder(); + fromBuilder.scan(f.iPat, innerFrom) + .where(core.greaterThan(f.typeSystem, f.iId, f.intLiteral(1))); + + final Core.From from = fromBuilder.build(); + final String expected = "from j in [1, 2] yield {i = j} where i > 1"; + assertThat(from, hasToString(expected)); + final Core.Exp e = fromBuilder.buildSimplify(); + assertThat(e, is(from)); + + // from j in (from j in [1, 2]) where j > 1 + // ==> + // from j in [1, 2] where j > 1 + final FromBuilder fromBuilder2 = f.fromBuilder(); + fromBuilder2.scan(f.jPat, innerFrom) + .where(core.greaterThan(f.typeSystem, f.jId, f.intLiteral(1))); + + final Core.From from2 = fromBuilder2.build(); + final String expected2 = "from j in [1, 2] where j > 1"; + assertThat(from2, hasToString(expected2)); + final Core.Exp e2 = fromBuilder2.buildSimplify(); + assertThat(e2, is(from2)); + + // from i in (from j in [1, 2]) + // ==> + // from j in [1, 2] + // ==> simplification + // [1, 2] + final FromBuilder fromBuilder3 = f.fromBuilder(); + fromBuilder3.scan(f.iPat, innerFrom); + + final Core.From from3 = fromBuilder3.build(); + final String expected3 = "from j in [1, 2]"; + assertThat(from3, hasToString(expected3)); + final Core.Exp e3 = fromBuilder3.buildSimplify(); + assertThat(e3, is(f.list12)); + } + + @Test void testNested4() { + // from d in [(1, 2), (3, 4)] + // join i in (from i in [#1 d]) + // ==> + // from d in [(1, 2), (3, 4)] + // join i in [#1 d] + final Fixture f = new Fixture(); + final Function, Core.From> innerFrom = bindings -> + core.fromBuilder(f.typeSystem, + Environments.empty().bindAll(bindings)) + .scan(f.iPat, + core.list(f.typeSystem, + core.field(f.typeSystem, f.dId, 0))) + .build(); + final FromBuilder fromBuilder = f.fromBuilder(); + fromBuilder + .scan(f.dPat, + core.list(f.typeSystem, f.tuple12, f.tuple34)) + .scan(f.iPat, innerFrom.apply(fromBuilder.bindings())); + + final Core.From from = fromBuilder.build(); + final String expected = "from d in [(1, 2), (3, 4)] " + + "join i in [#1 d]"; + assertThat(from, hasToString(expected)); + final Core.Exp e = fromBuilder.buildSimplify(); + assertThat(e, is(from)); + + // from d in [(1, 2), (3, 4)] + // join j in (from i in [#1 d]) + // where j > #1 d + // ==> + // from d in [(1, 2), (3, 4)] + // join i in [#1 d] + // yield {d, j = i} + // where j > #1 d + final FromBuilder fromBuilder2 = f.fromBuilder(); + fromBuilder2 + .scan(f.dPat, + core.list(f.typeSystem, f.tuple12, f.tuple34)) + .scan(f.jPat, innerFrom.apply(fromBuilder.bindings())) + .where( + core.greaterThan(f.typeSystem, f.jId, + core.field(f.typeSystem, f.dId, 0))); + + final Core.From from2 = fromBuilder2.build(); + final String expected2 = "from d in [(1, 2), (3, 4)] " + + "join i in [#1 d] " + + "yield {d = d, j = i} " + + "where j > #1 d"; + assertThat(from2, hasToString(expected2)); + final Core.Exp e2 = fromBuilder2.buildSimplify(); + assertThat(e2, is(from2)); + } + /** As {@link #testNested()} but inner and outer variables have the same * name, and therefore no yield is required. */ @Test void testNestedSameName() { @@ -220,17 +337,17 @@ FromBuilder fromBuilder() { } @Test void testNested0() { - // from i in (from) + // from u in (from) // ==> // from final Fixture f = new Fixture(); final Core.From innerFrom = f.fromBuilder().build(); final FromBuilder fromBuilder = f.fromBuilder(); - fromBuilder.scan(f.iPat, innerFrom); + fromBuilder.scan(f.uPat, innerFrom); final Core.From from = fromBuilder.build(); - assertThat(from, hasToString("from i in (from)")); + assertThat(from, hasToString("from u in (from)")); final Core.Exp e = fromBuilder.buildSimplify(); assertThat(e, hasToString("from")); } @@ -266,6 +383,117 @@ FromBuilder fromBuilder() { final Core.Exp e = fromBuilder.buildSimplify(); assertThat(e, is(from)); } + + @Test void testNestedFromTuple() { + // from (a, b) in + // (from (a, b) in + // (from a in [1, 2] join b in [3, 4])) + // where a > b andalso b = 10 + // yield b + // ==> + // from a in [1, 2] + // join b in [3, 4] + // where a > b andalso a = 10 + // yield b + final Fixture f = new Fixture(); + final Core.Pat abPat = + core.tuplePat(f.typeSystem, Arrays.asList(f.aPat, f.bPat)); + + final Core.From innermostFrom = + f.fromBuilder() + .scan(f.aPat, f.list12) + .scan(f.bPat, f.list34) + .build(); + final Core.From innerFrom = + f.fromBuilder() + .scan(abPat, innermostFrom) + .build(); + final FromBuilder fromBuilder = f.fromBuilder(); + fromBuilder + .scan(abPat, innerFrom) + .where( + core.andAlso(f.typeSystem, + core.greaterThan(f.typeSystem, f.aId, f.bId), + core.equal(f.typeSystem, f.aId, + core.intLiteral(BigDecimal.TEN)))) + .yield_(f.bId); + + final Core.From from = fromBuilder.build(); + final String expected = "from a in [1, 2] " + + "join b in [3, 4] " + + "where a > b andalso a = 10 " + + "yield b"; + assertThat(from, hasToString(expected)); + final Core.Exp e = fromBuilder.buildSimplify(); + assertThat(e, is(from)); + + // Tuple where variables are not in alphabetical order. Requires + // a 'yield' step to re-order variables. + // + // from (b, a) in + // (from a in [1, 2] join b in [3, 4]) + // where a > b andalso b = 10 + // yield b + // ==> + // from a in [1, 2] + // join b in [3, 4] + // yield {a = b, b = a} + // where a > b andalso a = 10 + // yield b + final Core.Pat baPat = + core.tuplePat(f.typeSystem, Arrays.asList(f.bPat, f.aPat)); + final FromBuilder fromBuilder2 = f.fromBuilder(); + fromBuilder2 + .scan(baPat, innermostFrom) + .where( + core.andAlso(f.typeSystem, + core.greaterThan(f.typeSystem, f.aId, f.bId), + core.equal(f.typeSystem, f.aId, + core.intLiteral(BigDecimal.TEN)))) + .yield_(f.bId); + + final Core.From from2 = fromBuilder2.build(); + final String expected2 = "from a in [1, 2] " + + "join b in [3, 4] " + + "yield {a = b, b = a} " + + "where a > b andalso a = 10 " + + "yield b"; + assertThat(from2, hasToString(expected2)); + final Core.Exp e2 = fromBuilder2.buildSimplify(); + assertThat(e2, is(from2)); + + // from (i, j) in + // (from a in [1, 2] join b in [3, 4]) + // where i > j andalso j = 10 + // yield i + // ==> + // from a in [1, 2] + // join b in [3, 4] + // yield {i = a, j = b} + // where i > j andalso j = 10 + // yield i + final Core.Pat ijPat = + core.tuplePat(f.typeSystem, Arrays.asList(f.iPat, f.jPat)); + final FromBuilder fromBuilder3 = f.fromBuilder(); + fromBuilder3 + .scan(ijPat, innermostFrom) + .where( + core.andAlso(f.typeSystem, + core.greaterThan(f.typeSystem, f.iId, f.jId), + core.equal(f.typeSystem, f.jId, + core.intLiteral(BigDecimal.TEN)))) + .yield_(f.jId); + + final Core.From from3 = fromBuilder3.build(); + final String expected3 = "from a in [1, 2] " + + "join b in [3, 4] " + + "yield {i = a, j = b} " + + "where i > j andalso j = 10 " + + "yield j"; + assertThat(from3, hasToString(expected3)); + final Core.Exp e3 = fromBuilder3.buildSimplify(); + assertThat(e3, is(from3)); + } } // End FromBuilderTest.java diff --git a/src/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index b31792a4d..5ba69a28f 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -1700,9 +1700,16 @@ private static List node(Object... args) { ml("from e in emps").assertParseSame(); ml("from e in emps where c").assertParseSame(); ml("from e in emps, d in depts").assertParseSame(); - ml("from e in emps, d suchthat hasEmp e").assertParseSame(); - ml("from e in emps, (job, d) suchthat hasEmp (e, d, job)") + ml("from e in emps, d where hasEmp e").assertParseSame(); + ml("from e, d where hasEmp e").assertParseSame(); + ml("from e in emps, job, d where hasEmp (e, d, job)") .assertParseSame(); + ml("from a, b in emps where a > b join c join d in depts where c > d") + .assertParseSame(); + // We ought to allow "join v1, v2", but don't currently. + ml("from a, b in emps where a > b join c, d in depts where c > d") + .assertParseThrowsParseException( + startsWith("Encountered \" \",\" \", \"\" at line 1, column 37.")); ml("from , d in depts").assertError("Xx"); ml("from join d in depts on c").assertError("Xx"); ml("from left join d in depts on c").assertError("Xx"); @@ -1765,6 +1772,35 @@ private static List node(Object... args) { ml("fn f => from i in [1, 2, 3] join j in [3, 4] on f (i, j) yield i + j") .assertParseSame() .assertType("(int * int -> bool) -> int list"); + + // In "from p in exp" and "from p = exp", p can be any pattern + // but in "from v" v can only be an identifier. + ml("from x, y in [1, 2], z").assertParseSame(); + ml("from {x, y} in [{x=1, y=2}], z") + .assertParse("from {x = x, y = y} in [{x = 1, y = 2}], z"); + ml("from {x, y}, z") + .assertParseThrowsParseException( + startsWith("Encountered \" \",\" \", \"\" " + + "at line 1, column 12.")); + ml("from {x, y} group") + .assertParseThrowsParseException( + startsWith("Encountered \" \"group\" \"group \"\" " + + "at line 1, column 13.")); + ml("from {x, y} where true") + .assertParseThrowsParseException( + startsWith("Encountered \" \"where\" \"where \"\" " + + "at line 1, column 13.")); + ml("from (x, y) where true") + .assertParseThrowsParseException( + startsWith("Encountered \" \"where\" \"where \"\" " + + "at line 1, column 13.")); + ml("from w as (x, y) order x") + .assertParseThrowsParseException( + startsWith("Encountered \" \"order\" \"order \"\" " + + "at line 1, column 18.")); + ml("from (x, y)") + .assertParseThrowsParseException( + startsWith("Encountered \"\" at line 1, column 11.")); } @Test void testFoo() { @@ -1859,26 +1895,37 @@ private static List node(Object... args) { + " fun hasEmpNameInDept (n, d) =\n" + " (n, d) elem (from e in emps yield (e.name, e.deptno))\n" + "in\n" - + " from (n, d) suchthat hasEmpNameInDept (n, d)\n" + + " from n, d\n" + + " where hasEmpNameInDept (n, d)\n" + " where d = 30\n" + " yield {d, n}\n" + "end"; final String code = "from(sink\n" - + " join(op join, pat (d_1, n_1),\n" + + " join(op join, pat v0,\n" + " exp from(\n" - + " sink join(op join, pat v0, exp from(sink\n" - + " join(op join, pat e, exp tuple(\n" + + " sink join(op join, pat e, exp tuple(\n" + " tuple(constant(10), constant(100), constant(Fred)),\n" + " tuple(constant(20), constant(101), constant(Velma)),\n" + " tuple(constant(30), constant(102), constant(Shaggy)),\n" + " tuple(constant(30), constant(103), constant(Scooby))),\n" - + " sink collect(tuple(apply(fnValue nth:2, argCode get(name e)),\n" - + " apply(fnValue nth:0, argCode get(name e)))))),\n" - + " sink collect(tuple(apply(fnValue nth:1, argCode get(name v0)),\n" - + " apply(fnValue nth:0, argCode get(name v0)))))),\n" + + " sink collect(tuple(apply(fnValue nth:2, argCode get(name e)), " + + "apply(fnValue nth:0, argCode get(name e)))))), " + + "sink join(op join, pat n_1, exp tuple(\n" + + " apply(fnValue nth:0, argCode get(name v0))), " + + "sink join(op join, pat d_1, exp tuple(constant(30)), " + + "sink where(condition apply2(fnValue elem,\n" + + " tuple(get(name n), get(name d)), " + + "from(sink join(op join, pat e, exp tuple(\n" + + " tuple(constant(10), constant(100), constant(Fred)),\n" + + " tuple(constant(20), constant(101), constant(Velma)),\n" + + " tuple(constant(30), constant(102), constant(Shaggy)),\n" + + " tuple(constant(30), constant(103), constant(Scooby))),\n" + + " sink collect(tuple(apply(fnValue nth:2, argCode get(name e)), " + + "apply(fnValue nth:0, argCode get(name e))))))),\n" + " sink where(condition apply2(fnValue =, get(name d), constant(30)),\n" - + " sink collect(getTuple(names [d, n])))))"; - final List expected = list(list(30, "Shaggy"), list(30, "Scooby")); + + " sink collect(tuple(get(name d), get(name n)))))))))"; + final List expected = + list(list(30, "Shaggy"), list(30, "Scooby")); ml(ml).assertType("{d:int, n:string} list") .assertPlan(isCode2(code)) .assertEval(is(expected)); @@ -1890,17 +1937,17 @@ private static List node(Object... args) { + " (d div 2, job)\n" + " elem (from e in scott.emp yield (e.deptno, e.job))\n" + "in\n" - + " from d in scott.dept," - + " j suchthat hasJob (d.deptno, j)\n" + + " from d in scott.dept, j" + + " where hasJob (d.deptno, j)\n" + " yield j\n" + "end"; final String core = "val it = " + "from d_1 in #dept scott " - + "join j suchthat (" - + "case (#deptno d_1, j) of" + + "join j : string " + + "where case (#deptno d_1, j) of" + " (d, job) => op elem ((op div (d, 2), job)," + " from e in #emp scott" - + " yield (#deptno e, #job e))) yield j"; + + " yield (#deptno e, #job e)) yield j"; final String code = "from(sink join(op join, pat d_1,\n" + " exp apply(fnValue nth:1, argCode get(name scott)),\n" + " sink join(op join, pat j,\n" @@ -1933,8 +1980,10 @@ private static List node(Object... args) { /** Translates a simple {@code suchthat} expression, "d elem list". */ @Test void testFromSuchThat2b() { - final String ml = "from d suchthat d elem scott.dept"; - final String core0 = "val it = from d suchthat (d elem #dept scott)"; + final String ml = "from d where d elem scott.dept"; + final String core0 = "val it = " + + "from d : {deptno:int, dname:string, loc:string} " + + "where d elem #dept scott"; final String core1 = "val it = from d in #dept scott"; ml(ml) .withBinding("scott", BuiltInDataSet.SCOTT) @@ -1946,35 +1995,75 @@ private static List node(Object... args) { /** Translates a simple {@code suchthat} expression, "{x, y} elem list". * Fields are renamed, to disrupt alphabetical ordering. */ @Test void testFromSuchThat2c() { - final String ml = "from (loc, deptno, name) " - + "suchthat {deptno, loc, dname = name} elem scott.dept"; + final String ml = "from loc, deptno, name " + + "where {deptno, loc, dname = name} elem scott.dept"; final String core = "val it = " - + "from (deptno, loc, name) in" - + " (from v0 in #dept scott " - + "yield {deptno = #deptno v0, loc = #loc v0, name = #dname v0})"; + + "from v0 in #dept scott " + + "join loc in [#loc v0] " + + "join deptno in [#deptno v0] " + + "join name in [#dname v0] " + + "where op elem ({deptno = deptno, dname = name, loc = loc}," + + " #dept scott) " + + "yield {deptno = deptno, loc = loc, name = name}"; ml(ml) .withBinding("scott", BuiltInDataSet.SCOTT) .assertType("{deptno:int, loc:string, name:string} list") - .assertCore(-1, hasToString(core)); + .assertCore(-1, hasToString(core)) + .assertEval( + is( + list(list(10, "NEW YORK", "ACCOUNTING"), + list(20, "DALLAS", "RESEARCH"), + list(30, "CHICAGO", "SALES"), + list(40, "BOSTON", "OPERATIONS")))); } /** As {@link #testFromSuchThat2c()} but with a literal. */ @Test void testFromSuchThat2d() { - final String ml = "from (dno, name)\n" - + " suchthat {deptno = dno, dname = name, loc = \"CHICAGO\"}\n" + final String ml = "from dno, name\n" + + " where {deptno = dno, dname = name, loc = \"CHICAGO\"}\n" + + " elem scott.dept\n" + + " andalso dno > 20"; + final String core0 = "val it = " + + "from dno : int " + + "join name : string " + + "where {deptno = dno, dname = name, loc = \"CHICAGO\"} " + + "elem #dept scott " + + "andalso dno > 20"; + final String core1 = "val it = " + + "from v0 in #dept scott " + + "join dno in [#deptno v0] " + + "join name in [#dname v0] " + + "where op elem ({deptno = dno, dname = name, loc = \"CHICAGO\"}," + + " #dept scott) " + + "andalso dno > 20 " + + "yield {dno = dno, name = name}"; + ml(ml) + .withBinding("scott", BuiltInDataSet.SCOTT) + .assertType("{dno:int, name:string} list") + .assertCore(0, hasToString(core0)) + .assertCore(-1, hasToString(core1)); + } + + /** As {@link #testFromSuchThat2c()} but with a literal. */ + @Test void testFromSuchThat2d2() { + final String ml = "from dno, name\n" + + " where {deptno = dno, dname = name, loc = \"CHICAGO\"}\n" + " elem scott.dept\n" - + " andalso dno = 20"; + + " andalso dno > 20"; final String core0 = "val it = " - + "from (dno, name) " - + "suchthat ({deptno = dno, dname = name, loc = \"CHICAGO\"} " + + "from dno : int " + + "join name : string " + + "where {deptno = dno, dname = name, loc = \"CHICAGO\"} " + "elem #dept scott " - + "andalso dno = 20)"; + + "andalso dno > 20"; final String core1 = "val it = " - + "from (dno, name) in (" + "from v0 in #dept scott " - + "where #loc v0 = \"CHICAGO\" " - + "where #deptno v0 = 20 " - + "yield {dno = #deptno v0, name = #dname v0})"; + + "join dno in [#deptno v0] " + + "join name in [#dname v0] " + + "where op elem ({deptno = dno, dname = name, loc = \"CHICAGO\"}," + + " #dept scott) " + + "andalso dno > 20 " + + "yield {dno = dno, name = name}"; ml(ml) .withBinding("scott", BuiltInDataSet.SCOTT) .assertType("{dno:int, name:string} list") @@ -1982,6 +2071,76 @@ private static List node(Object... args) { .assertCore(-1, hasToString(core1)); } + @Test void testFromSuchThat2d3() { + final String ml = "from dno, name, v\n" + + "where v elem scott.dept\n" + + "where v.deptno = dno\n" + + "where name = v.dname\n" + + "where v.loc = \"CHICAGO\"\n" + + "where dno = 30\n" + + "yield {dno = #deptno v, name = #dname v}"; + final String core0 = "val it = " + + "from dno : int " + + "join name : string " + + "join v : {deptno:int, dname:string, loc:string} " + + "where v elem #dept scott " + + "where #deptno v = dno " + + "where name = #dname v " + + "where #loc v = \"CHICAGO\" " + + "where dno = 30 " + + "yield {dno = #deptno v, name = #dname v}"; + final String core1 = "val it = " + + "from dno in [30] " + + "join v in #dept scott " + + "join name in [#dname v] " + + "where #deptno v = dno " + + "where name = #dname v " + + "where #loc v = \"CHICAGO\" " + + "where dno = 30 " + + "yield {dno = #deptno v, name = #dname v}"; + ml(ml) + .withBinding("scott", BuiltInDataSet.SCOTT) + .assertType("{dno:int, name:string} list") + .assertCore(0, hasToString(core0)) + .assertCore(-1, hasToString(core1)) + .assertEval(is(list(list(30, "SALES")))); + } + + @Test void testFromSuchThat2d4() { + final String ml = "from dno, name, v\n" + + "where v elem scott.dept\n" + + "where v.deptno = dno\n" + + "where name = v.dname\n" + + "where v.loc = \"CHICAGO\"\n" + + "where dno > 25\n" + + "yield {dno = #deptno v, name = #dname v}"; + final String core0 = "val it = " + + "from dno : int " + + "join name : string " + + "join v : {deptno:int, dname:string, loc:string} " + + "where v elem #dept scott " + + "where #deptno v = dno " + + "where name = #dname v " + + "where #loc v = \"CHICAGO\" " + + "where dno > 25 " + + "yield {dno = #deptno v, name = #dname v}"; + final String core1 = "val it = " + + "from v in #dept scott " + + "join dno in [#deptno v] " + + "join name in [#dname v] " + + "where #deptno v = dno " + + "where name = #dname v " + + "where #loc v = \"CHICAGO\" " + + "where dno > 25 " + + "yield {dno = #deptno v, name = #dname v}"; + ml(ml) + .withBinding("scott", BuiltInDataSet.SCOTT) + .assertType("{dno:int, name:string} list") + .assertCore(0, hasToString(core0)) + .assertCore(-1, hasToString(core1)) + .assertEval(is(list(list(30, "SALES")))); + } + /** As {@link #testFromSuchThat2d()} but using a function. * (Simple enough that the function can be handled by inlining.) */ @Test void testFromSuchThat2e() { @@ -1989,14 +2148,16 @@ private static List node(Object... args) { + " fun isDept d =\n" + " d elem scott.dept\n" + "in\n" - + " from d suchthat isDept d andalso d.deptno = 20\n" + + " from d\n" + + " where isDept d andalso d.deptno = 20\n" + " yield d.dname\n" + "end"; final String core0 = "val it = " + "let" + " val isDept = fn d => d elem #dept scott " + "in" - + " from d_1 suchthat (isDept d_1 andalso #deptno d_1 = 20)" + + " from d_1 : {deptno:int, dname:string, loc:string}" + + " where isDept d_1 andalso #deptno d_1 = 20" + " yield #dname d_1 " + "end"; final String core1 = "val it = " @@ -2018,7 +2179,8 @@ private static List node(Object... args) { + " fun isEmp e =\n" + " e elem scott.emp\n" + "in\n" - + " from (d, e) suchthat isDept d\n" + + " from d, e\n" + + " where isDept d\n" + " andalso isEmp e\n" + " andalso d.deptno = e.deptno\n" + " andalso d.deptno = 20\n" @@ -2031,18 +2193,21 @@ private static List node(Object... args) { + " let" + " val isEmp = fn e => e elem #emp scott " + "in" - + " from (d_1, e_1) suchthat (isDept d_1 " + + " from d_1 : {deptno:int, dname:string, loc:string}" + + " join e_1 : {comm:real, deptno:int, empno:int, ename:string, " + + "hiredate:string, job:string, mgr:int, sal:real}" + + " where isDept d_1 " + "andalso isEmp e_1 " + "andalso #deptno d_1 = #deptno e_1 " - + "andalso #deptno d_1 = 20)" + + "andalso #deptno d_1 = 20" + " yield #dname d_1" + " end " + "end"; final String core1 = "val it = " - + "from (d_1, e_1) in (from d_1 in #dept scott" - + " join e_1 in #emp scott" - + " where #deptno d_1 = #deptno e_1" - + " where #deptno d_1 = 20) " + + "from d_1 in #dept scott " + + "join e_1 in #emp scott " + + "where #deptno d_1 = #deptno e_1 " + + "andalso #deptno d_1 = 20 " + "yield #dname d_1"; ml(ml) .withBinding("scott", BuiltInDataSet.SCOTT) @@ -2060,13 +2225,14 @@ private static List node(Object... args) { + " fun hasEmpNameInDept (n, d) =\n" + " (n, d) elem (from e in emps yield (e.name, e.deptno))\n" + "in\n" - + " from (n, d) suchthat hasEmpNameInDept (n, d)\n" + + " from n, d\n" + + " where hasEmpNameInDept (n, d)\n" + " where d = 30\n" + " yield {d, n}\n" + "end"; final String core = "val it = " - + "from (n_1, d_1) suchthat" - + " (case (n_1, d_1) of (n, d) => op elem ((n, d), " + + "from n_1, d_1 " + + "where (case (n_1, d_1) of (n, d) => op elem ((n, d), " + "from e in [" + "{deptno = 30, id = 102, name = \"Shaggy\"}, " + "{deptno = 30, id = 103, name = \"Scooby\"}]" @@ -2099,6 +2265,20 @@ private static List node(Object... args) { // .assertEval(is(list())); } + /** A query with an unconstrained scan that is deduced to be of type + * {@code bool option} and therefore iterates over + * {@code [SOME true, SOME false, NONE]}. */ + @Test void testBooleanExtent() { + final String ml = "from i\n" + + "where Option.getOpt (i, false)"; + final String core = "val it = " + + "from i in extent \"bool option\" " + + "where #getOpt Option (i, false)"; + ml(ml).assertType("bool option list") + .assertCore(-1, hasToString(core)) + .assertEval(is(list(list("SOME", true)))); + } + @Test void testFromNoYield() { final String ml = "let\n" + " val emps =\n" diff --git a/src/test/java/net/hydromatic/morel/UtilTest.java b/src/test/java/net/hydromatic/morel/UtilTest.java index ae031e5fb..df940b2a4 100644 --- a/src/test/java/net/hydromatic/morel/UtilTest.java +++ b/src/test/java/net/hydromatic/morel/UtilTest.java @@ -20,6 +20,7 @@ import net.hydromatic.morel.ast.Ast; import net.hydromatic.morel.ast.Pos; +import net.hydromatic.morel.compile.BuiltIn; import net.hydromatic.morel.eval.Codes; import net.hydromatic.morel.type.PrimitiveType; import net.hydromatic.morel.type.RangeExtent; @@ -32,6 +33,7 @@ import net.hydromatic.morel.util.TailList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Lists; import com.google.common.collect.Range; @@ -359,12 +361,17 @@ private void checkShorterThan(Iterable iterable, int size) { @SuppressWarnings("UnstableApiUsage") @Test void testRangeExtent() { + final TypeSystem typeSystem = new TypeSystem(); + BuiltIn.dataTypes(typeSystem, new ArrayList<>()); + // Integer range [(4, 7]] final Range range = Range.openClosed(BigDecimal.valueOf(4), BigDecimal.valueOf(7)); final RangeExtent rangeExtent = - new RangeExtent(ImmutableRangeSet.of(range), PrimitiveType.INT); - assertThat(Lists.newArrayList(rangeExtent.toIterable()), + new RangeExtent(typeSystem, PrimitiveType.INT, + ImmutableMap.of("/", ImmutableRangeSet.of(range))); + assertThat(rangeExtent.iterable, notNullValue()); + assertThat(Lists.newArrayList(rangeExtent.iterable), is(Arrays.asList(5, 6, 7))); // Integer range set [(4, 7], [10, 12]] @@ -372,30 +379,52 @@ private void checkShorterThan(Iterable iterable, int size) { Range.closed(BigDecimal.valueOf(10), BigDecimal.valueOf(12)); final RangeExtent rangeExtent2 = new RangeExtent( - ImmutableRangeSet.unionOf(ImmutableList.of(range, range2)), - PrimitiveType.INT); - assertThat(Lists.newArrayList(rangeExtent2.toIterable()), + typeSystem, PrimitiveType.INT, + ImmutableMap.of("/", + ImmutableRangeSet.unionOf(ImmutableList.of(range, range2)))); + assertThat(rangeExtent2.iterable, notNullValue()); + assertThat(Lists.newArrayList(rangeExtent2.iterable), is(Arrays.asList(5, 6, 7, 10, 11, 12))); // Boolean range set final Range range3 = Range.closed(false, true); final RangeExtent rangeExtent3 = - new RangeExtent(ImmutableRangeSet.of(range3), - PrimitiveType.BOOL); - assertThat(Lists.newArrayList(rangeExtent3.toIterable()), + new RangeExtent(typeSystem, PrimitiveType.BOOL, + ImmutableMap.of("/", ImmutableRangeSet.of(range3))); + assertThat(rangeExtent3.iterable, notNullValue()); + assertThat(Lists.newArrayList(rangeExtent3.iterable), is(Arrays.asList(false, true))); // Range set of (Boolean, Boolean) tuples - final TypeSystem typeSystem = new TypeSystem(); final Range range4 = Range.closed((Comparable) FlatLists.of(false, true), (Comparable) FlatLists.of(true, true)); final RangeExtent rangeExtent4 = - new RangeExtent(ImmutableRangeSet.of(range4), - typeSystem.tupleType(PrimitiveType.BOOL, PrimitiveType.BOOL)); - assertThat(Lists.newArrayList(rangeExtent4.toIterable()), - is(Arrays.asList( - FlatLists.of(false, true), FlatLists.of(true, true)))); + new RangeExtent(typeSystem, + typeSystem.tupleType(PrimitiveType.BOOL, PrimitiveType.BOOL), + ImmutableMap.of("/", ImmutableRangeSet.of(range4))); + assertThat(rangeExtent4.iterable, notNullValue()); + assertThat(Lists.newArrayList(rangeExtent4.iterable), + is( + Arrays.asList(FlatLists.of(false, true), + FlatLists.of(true, false), FlatLists.of(true, true)))); + + // Range set of (boolean option, int) tuples + final RangeExtent rangeExtent5 = + new RangeExtent(typeSystem, + typeSystem.tupleType( + typeSystem.option(PrimitiveType.BOOL), + PrimitiveType.INT), + ImmutableMap.of("/1/SOME/", + ImmutableRangeSet.of(Range.singleton(true)), + "/2/", + ImmutableRangeSet.of( + Range.closed(BigDecimal.valueOf(4), + BigDecimal.valueOf(6))))); + assertThat(rangeExtent5.iterable, notNullValue()); + assertThat(ImmutableList.copyOf(rangeExtent5.iterable), + hasToString("[[[NONE], 4], [[NONE], 5], [[NONE], 6]," + + " [[SOME, true], 4], [[SOME, true], 5], [[SOME, true], 6]]")); } } diff --git a/src/test/java/net/hydromatic/morel/compile/ExtentTest.java b/src/test/java/net/hydromatic/morel/compile/ExtentTest.java index 02627c3f3..a321bcbd1 100644 --- a/src/test/java/net/hydromatic/morel/compile/ExtentTest.java +++ b/src/test/java/net/hydromatic/morel/compile/ExtentTest.java @@ -19,21 +19,34 @@ package net.hydromatic.morel.compile; import net.hydromatic.morel.ast.Core; +import net.hydromatic.morel.ast.FromBuilder; +import net.hydromatic.morel.type.ListType; import net.hydromatic.morel.type.PrimitiveType; +import net.hydromatic.morel.type.RecordLikeType; +import net.hydromatic.morel.type.RecordType; import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.ImmutablePairList; import net.hydromatic.morel.util.PairList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import static net.hydromatic.morel.ast.CoreBuilder.core; -import static net.hydromatic.morel.compile.Extents.generator; +import static org.apache.calcite.linq4j.tree.Expressions.list; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.core.Is.is; @@ -46,11 +59,49 @@ private static class Fixture { final PrimitiveType intType = PrimitiveType.INT; final Core.IdPat aPat = core.idPat(intType, "a", 0); final Core.Id aId = core.id(aPat); + final Core.IdPat bPat = core.idPat(intType, "b", 0); + final Core.Id bId = core.id(bPat); + final Core.IdPat cPat = core.idPat(intType, "c", 0); + final Core.Id cId = core.id(cPat); + final Core.IdPat dPat = core.idPat(intType, "d", 0); + final Core.Id dId = core.id(dPat); final Core.Exp list12 = core.list(typeSystem, intLiteral(1), intLiteral(2)); + final RecordLikeType deptType = + typeSystem.recordType( + RecordType.map("deptno", PrimitiveType.INT, + "dname", PrimitiveType.STRING, + "loc", PrimitiveType.STRING)); + final ListType deptListType = typeSystem.listType(deptType); + final Core.IdPat depts = core.idPat(deptListType, "depts", 0); Core.Literal intLiteral(int i) { return core.intLiteral(BigDecimal.valueOf(i)); } + + void checkFlatten(Core.Exp exp, String s, String s2) { + final List andExps = core.decomposeAnd(exp); + assertThat(andExps, hasToString(s)); + assertThat(core.andAlso(typeSystem, andExps), + hasToString(exp.toString())); + + final List orExps = core.decomposeOr(exp); + assertThat(orExps, hasToString(s2)); + assertThat(core.orElse(typeSystem, orExps), + hasToString(exp.toString())); + } + + void checkSubTrue(Core.Exp exp, List exps, String s) { + final Core.Exp exp2 = core.subTrue(typeSystem, exp, exps); + assertThat(exp2, hasToString(s)); + } + + Core.Exp extent(Core.Pat pat, Core.Exp filterExp) { + final Extents.Analysis analysis = + Extents.create(typeSystem, pat, ImmutableSortedMap.of(), + ImmutableList.of(core.where(ImmutableList.of(), filterExp)), + ImmutablePairList.of()); + return analysis.extentExp; + } } /** Tests whether an expression is constant. @@ -106,19 +157,19 @@ Core.Literal intLiteral(int i) { Core.IdPat xPat = core.idPat(PrimitiveType.INT, "x", 0); Core.Literal ten = f.intLiteral(10); Core.Exp exp = core.equal(f.typeSystem, core.id(xPat), ten); - Core.Exp x = generator(f.typeSystem, xPat, exp); + Core.Exp x = f.extent(xPat, exp); assertThat(x, hasToString("[10]")); // pat = "x", exp = "10 = x", extent = "[10]" Core.Exp exp2 = core.equal(f.typeSystem, ten, core.id(xPat)); - Core.Exp x2 = generator(f.typeSystem, xPat, exp2); + Core.Exp x2 = f.extent(xPat, exp2); assertThat(x2, hasToString("[10]")); } @Test void testBetween() { // pat = "x", exp = "x >= 3 andalso y = 20 andalso x < 10 andalso 5 <> x", // extent of x is "extent [[3..5), (5..10)]"; - // extent of y is "extent [[3..5), (5..10)]"; + // extent of y is "extent [20, 20]"; final Fixture f = new Fixture(); Core.IdPat xPat = core.idPat(PrimitiveType.INT, "x", 0); Core.IdPat yPat = core.idPat(PrimitiveType.INT, "y", 0); @@ -134,14 +185,14 @@ Core.Literal intLiteral(int i) { core.andAlso(f.typeSystem, exp0, core.andAlso(f.typeSystem, exp1, core.andAlso(f.typeSystem, exp2, exp3))); - Core.Exp x = generator(f.typeSystem, xPat, exp); + Core.Exp x = f.extent(xPat, exp); assertThat(x, instanceOf(Core.Apply.class)); assertThat(((Core.Apply) x).fn, instanceOf(Core.Literal.class)); assertThat(((Core.Literal) ((Core.Apply) x).fn).unwrap(BuiltIn.class), is(BuiltIn.Z_EXTENT)); - assertThat(x, hasToString("extent \"int [[3..5), (5..10)]\"")); + assertThat(x, hasToString("extent \"int {/=[[3..5), (5..10)]}\"")); - Core.Exp y = generator(f.typeSystem, yPat, exp); + Core.Exp y = f.extent(yPat, exp); assertThat(y, instanceOf(Core.Apply.class)); assertThat(((Core.Apply) y).fn, instanceOf(Core.Literal.class)); assertThat(((Core.Literal) ((Core.Apply) y).fn).unwrap(BuiltIn.class), @@ -149,6 +200,157 @@ Core.Literal intLiteral(int i) { assertThat(y, hasToString("[20]")); } + @Test void testSubTrue() { + final Fixture f = new Fixture(); + final Core.Exp exp1 = + core.andAlso(f.typeSystem, f.aId, + core.andAlso(f.typeSystem, f.bId, + core.orElse(f.typeSystem, f.cId, f.intLiteral(1)))); + final String expected1a = "a andalso (c orelse 1)"; + final String expected1b = "a andalso (b andalso (c orelse 1))"; + final String expected1c = "b andalso (c orelse 1)"; + f.checkSubTrue(exp1, list(f.bId), expected1a); + f.checkSubTrue(exp1, list(f.cId), expected1b); + f.checkSubTrue(exp1, list(f.cId, f.dId, f.list12), expected1b); + f.checkSubTrue(exp1, list(f.aId), expected1c); + f.checkSubTrue(exp1, list(f.dId), expected1b); + f.checkSubTrue(exp1, list(), expected1b); + + final Core.Exp exp2 = + core.orElse(f.typeSystem, f.aId, + core.orElse(f.typeSystem, f.bId, + core.andAlso(f.typeSystem, f.cId, f.dId))); + final String expected2a = "a orelse (b orelse c andalso d)"; + f.checkSubTrue(exp2, list(f.bId), expected2a); // TODO "a" + f.checkSubTrue(exp2, list(f.cId), + expected2a); // TODO "a orelse (b orelse d)" + f.checkSubTrue(exp2, list(f.bId, f.cId), expected2a); // TODO "a" + f.checkSubTrue(exp2, list(f.aId), expected2a); // TODO "true" + f.checkSubTrue(exp2, list(), expected2a); + } + + @Test void testFlatten() { + final Fixture f = new Fixture(); + f.checkFlatten(f.aId, "[a]", "[a]"); + f.checkFlatten(core.boolLiteral(true), "[]", "[true]"); + f.checkFlatten(core.boolLiteral(false), "[false]", "[]"); + f.checkFlatten( + core.andAlso(f.typeSystem, f.aId, + core.andAlso(f.typeSystem, f.bId, + core.orElse(f.typeSystem, f.cId, f.intLiteral(1)))), + "[a, b, c orelse 1]", + "[a andalso (b andalso (c orelse 1))]"); + f.checkFlatten( + core.orElse(f.typeSystem, f.aId, + core.orElse(f.typeSystem, f.bId, + core.andAlso(f.typeSystem, f.cId, f.intLiteral(1)))), + "[a orelse (b orelse c andalso 1)]", + "[a, b, c andalso 1]"); + } + + @Test void testAnalysis2c() { + final Fixture f = new Fixture(); + final Core.IdPat loc = core.idPat(PrimitiveType.STRING, "loc", 0); + final Core.IdPat deptno = core.idPat(PrimitiveType.INT, "deptno", 0); + final Core.IdPat name = core.idPat(PrimitiveType.STRING, "name", 0); + final Core.TuplePat pat = + core.tuplePat(f.typeSystem, list(loc, deptno, name)); + + final Core.Exp condition0 = + core.elem(f.typeSystem, + core.record(f.typeSystem, + PairList.copyOf("deptno", core.id(deptno), + "dname", core.id(name), + "loc", core.id(loc))), + core.id(f.depts)); + final Core.Exp condition1 = + core.greaterThan(f.typeSystem, core.id(deptno), + core.intLiteral(BigDecimal.valueOf(20))); + + final BiConsumer> fn = (v, action) -> { + final FromBuilder fromBuilder = core.fromBuilder(f.typeSystem); + fromBuilder.scan(pat); + // Apply one of the variants of 'where' clause + action.accept(fromBuilder); + + final PairList idPats = PairList.of(); + Extents.Analysis analysis = + Extents.create(f.typeSystem, pat, ImmutableSortedMap.of(), + fromBuilder.build().steps, idPats); + assertThat(analysis, notNullValue()); + if ("".isEmpty()) { + assertThat(analysis.extentExp, + hasToString(("from loc in [#loc v0] " + + "join deptno in [#deptno v0] " + + "join name in [#dname v0]").replace("v0", v))); + assertThat(analysis.satisfiedFilters, hasSize(3)); + assertThat(analysis.satisfiedFilters.get(0), + hasToString("loc = #loc v0".replace("v0", v))); + } else { + assertThat(analysis.extentExp, hasToString("depts")); + assertThat(analysis.satisfiedFilters, hasSize(1)); + assertThat(analysis.satisfiedFilters.get(0), is(condition0)); + } + assertThat(analysis.remainingFilters, empty()); + assertThat(analysis.boundPats, anEmptyMap()); + assertThat(analysis.goalPats, is(ImmutableSet.of(loc, deptno, name))); + assertThat(idPats, hasSize(1)); + assertThat(idPats.leftList().get(0), hasToString(v)); + }; + + // from (loc, deptno, name) + // where op elem ({deptno = deptno, dname = name, loc = loc}, depts) + fn.accept("v0", fromBuilder -> + fromBuilder.where(condition0)); + + // from (loc, deptno, name) + // where op elem ({deptno = deptno, dname = name, loc = loc}, depts) + // where deptno > 20 + fn.accept("v1", fromBuilder -> + fromBuilder.where(condition0) + .where(condition1)); + + // from (loc, deptno, name) + // where op elem ({deptno = deptno, dname = name, loc = loc}, depts) + // andalso deptno > 20 + fn.accept("v2", fromBuilder -> + fromBuilder.where( + core.andAlso(f.typeSystem, condition0, condition1))); + } + + @Test void testAnalysis2d() { + final Fixture f = new Fixture(); + final Core.IdPat dno = core.idPat(PrimitiveType.INT, "dno", 0); + final Core.IdPat name = core.idPat(PrimitiveType.STRING, "name", 0); + final Core.TuplePat pat = core.tuplePat(f.typeSystem, list(dno, name)); + + // from (dno, name) + // where op elem ({deptno = dno, dname = name, loc = "CHICAGO"}, depts) + // andalso dno > 20 + final FromBuilder fromBuilder = core.fromBuilder(f.typeSystem); + fromBuilder.scan(pat); + final Core.Exp condition0 = + core.elem(f.typeSystem, + core.record(f.typeSystem, + PairList.copyOf("deptno", core.id(dno), + "dname", core.id(name), + "loc", core.stringLiteral("CHICAGO"))), + core.id(f.depts)); + final Core.Exp condition1 = + core.greaterThan(f.typeSystem, core.id(dno), + core.intLiteral(BigDecimal.valueOf(20))); + fromBuilder.where(core.andAlso(f.typeSystem, condition0, condition1)); + + final PairList idPats = PairList.of(); + Extents.Analysis analysis = + Extents.create(f.typeSystem, pat, ImmutableSortedMap.of(), + fromBuilder.build().steps, idPats); + assertThat(analysis, notNullValue()); + assertThat(analysis.extentExp, + hasToString("from dno in [#deptno v0] join name in [#dname v0]")); + assertThat(idPats, hasSize(1)); + assertThat(idPats.leftList().get(0), hasToString("v0")); + } } // End ExtentTest.java diff --git a/src/test/resources/script/suchThat.smli b/src/test/resources/script/suchThat.smli index 32d04aa98..8acad2fcd 100644 --- a/src/test/resources/script/suchThat.smli +++ b/src/test/resources/script/suchThat.smli @@ -16,31 +16,114 @@ * language governing permissions and limitations under the * License. * - * Tests for 'suchthat'. + * Tests for queries with unbounded variables. + * (Originally, such queries used a 'suchthat' keyword, but that keyword + * has since been removed.) *) (*) Convert predicates into ranges -from i suchthat i > 0 andalso i < 10; +from i where i > 0 andalso i < 10; > val it = [1,2,3,4,5,6,7,8,9] : int list -from i suchthat i > 0 andalso i < 10 andalso i mod 3 = 2; +from i where i > 0 andalso i < 4 yield {j = i + 1}; +> val it = [{j=2},{j=3},{j=4}] : {j:int} list +from i where i > 0 andalso i < 4 yield {j = i + 1} order j desc; +> val it = [4,3,2] : int list +from i where i > 0 andalso i < 10 andalso i mod 3 = 2; > val it = [2,5,8] : int list -from i suchthat i > 0 andalso i < 10 orelse i > 12 andalso i <= 15; +from i where i = 0 orelse i = 12; +> val it = [0,12] : int list +from i where i > 0 andalso i < 10 orelse i > 12 andalso i <= 15; > val it = [1,2,3,4,5,6,7,8,9,13,14,15] : int list -from i suchthat i > 0 andalso i < 10, - b suchthat b = true; +from i +where i > 0 andalso i < 10 +join b +where b = true; > val it = -> [{b=[true, 1],i=1},{b=[true, 2],i=2},{b=[true, 3],i=3},{b=[true, 4],i=4}, -> {b=[true, 5],i=5},{b=[true, 6],i=6},{b=[true, 7],i=7},{b=[true, 8],i=8}, -> {b=[true, 9],i=9}] : {b:bool, i:int} list -from i suchthat i > 0 andalso i < 10, - b suchthat b = (i mod 2 = 0); +> [{b=true,i=1},{b=true,i=2},{b=true,i=3},{b=true,i=4},{b=true,i=5}, +> {b=true,i=6},{b=true,i=7},{b=true,i=8},{b=true,i=9}] : {b:bool, i:int} list +from i +where i > 0 andalso i < 10 +join b +where b = (i mod 2 = 0); > val it = -> [{b=[false, 1],i=1},{b=[true, 2],i=2},{b=[false, 3],i=3},{b=[true, 4],i=4}, -> {b=[false, 5],i=5},{b=[true, 6],i=6},{b=[false, 7],i=7},{b=[true, 8],i=8}, -> {b=[false, 9],i=9}] : {b:bool, i:int} list +> [{b=false,i=1},{b=true,i=2},{b=false,i=3},{b=true,i=4},{b=false,i=5}, +> {b=true,i=6},{b=false,i=7},{b=true,i=8},{b=false,i=9}] +> : {b:bool, i:int} list + +from dno, name, loc +where {deptno = dno, dname = name, loc} elem scott.dept + andalso dno > 20; +> val it = +> [{dno=30,loc="CHICAGO",name="SALES"},{dno=40,loc="BOSTON",name="OPERATIONS"}] +> : {dno:int, loc:string, name:string} list + +(*) As previous but with a literal in the record. +from dno, name +where {deptno = dno, dname = name, loc = "CHICAGO"} elem scott.dept + andalso dno > 20; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) Equivalent to previous, rephrasing 'suchthat' as 'where' +from dno, name +where {deptno = dno, dname = name, loc = "CHICAGO"} elem scott.dept + andalso dno > 20; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) Equivalent to previous, splitting 'where' +from dno, name +where {deptno = dno, dname = name, loc = "CHICAGO"} elem scott.dept +where dno > 20; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) Variables 'dno', 'name' are infinite until constrained by conditions. +from v, dno, name +where v elem scott.dept +where v.deptno = dno +where name = v.dname +where v.loc = "CHICAGO" +yield {dno, name = #dname v}; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) Forward references are required. 'dno' is infinite until we see the +(*) condition 'v.deptno = dno', and at that point we haven't declared +(*) 'v'. So we defer 'dno' until after 'v'. +from dno, name, v +where v elem scott.dept +where v.deptno = dno +where name = v.dname +where v.loc = "CHICAGO" +yield {dno = #deptno v, name = #dname v}; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) An extra condition on 'dno' yields empty result. +from dno, name, v +where v elem scott.dept +where v.deptno = dno +where name = v.dname +where v.loc = "CHICAGO" +where dno = 20 +yield {dno = #deptno v, name = #dname v}; +> val it = [] : {dno:int, name:string} list + +(*) Inequality on 'dno' +from dno, name, v +where v elem scott.dept +where v.deptno = dno +where name = v.dname +where v.loc = "CHICAGO" +where dno > 20 +yield {dno = #deptno v, name = #dname v}; +> val it = [{dno=30,name="SALES"}] : {dno:int, name:string} list + +(*) We can iterate over a finite datatype +from i +where Option.getOpt i; +> val it = [(NONE,true),(SOME true,false),(SOME true,true)] +> : (bool option * bool) list (*) If the expression is 'elem set' we can deduce the extent. -from e suchthat (e elem scott.emp) +from e + where (e elem scott.emp) where e.deptno = 20 yield e.ename; > val it = ["SMITH","JONES","SCOTT","ADAMS","FORD"] : string list @@ -50,7 +133,8 @@ let fun isEmp e = e elem scott.emp in - from e suchthat isEmp e + from e + where isEmp e where e.deptno = 20 yield e.ename end; @@ -61,7 +145,8 @@ let fun isEmp e = e elem scott.emp in - from e suchthat isEmp e andalso e.deptno = 20 + from e + where isEmp e andalso e.deptno = 20 yield e.ename end; > val it = ["SMITH","JONES","SCOTT","ADAMS","FORD"] : string list @@ -75,7 +160,8 @@ fun isEmp e = > {comm:real, deptno:int, empno:int, ename:string, hiredate:string, > job:string, mgr:int, sal:real} -> bool (* -from e suchthat isEmp e andalso e.deptno = 20 +from e + where isEmp e andalso e.deptno = 20 yield e.ename; *) @@ -84,7 +170,8 @@ let fun isClerk e = e elem scott.emp andalso e.job = "CLERK" in - from e suchthat isClerk e andalso e.deptno = 20 + from e + where isClerk e andalso e.deptno = 20 yield e.ename end; > val it = ["SMITH","ADAMS"] : string list @@ -96,7 +183,8 @@ let fun isEmp50 e = e elem scott.emp orelse e.deptno = 50 in - from e suchthat isEmp50 e + from e + where isEmp50 e yield e.ename end; *) @@ -114,7 +202,8 @@ let e.job = job in from e in scott.emp, - j suchthat hasJob (e, j) + j + where hasJob (e, j) yield j end; val it = @@ -129,7 +218,7 @@ let fun hasJob (e, job) = e.job = job in - from e suchthat hasJob (e, "CLERK") + from e where hasJob (e, "CLERK") end; *) @@ -140,7 +229,7 @@ fun isPrefix (s1, s2) = > val isPrefix = fn : string * string -> bool (*) This is invalid, but it could be valid (* -from s suchthat isPrefix (s, "abcd"); +from s where isPrefix (s, "abcd"); > val it = ["", "a", "ab", "abc", "abcd"] : string list *) @@ -150,7 +239,7 @@ fun isBetween (i, j, k) = i <= j andalso j <= k; > val isBetween = fn : 'a * 'a * 'a -> bool (* -from i suchthat isBetween (i, 5, 8); +from i where isBetween (i, 5, 8); > val it = [5, 6, 7, 8] : int list *) @@ -158,7 +247,7 @@ from i suchthat isBetween (i, 5, 8); (*) Convenience function that converts a predicate to a relation (* fun enumerate predicate = - from r suchthat predicate r; + from r where predicate r; *) (* TODO should print val enumerate = fn : ('a -> bool) -> 'a list @@ -212,21 +301,17 @@ val edges = [ > val edges = [{x=1,y=2},{x=2,y=3}] : {x:int, y:int} list fun edge (x, y) = {x, y} elem edges; > val edge = fn : int * int -> bool -(* +(* TODO fun path (x, y) = edge (x, y) orelse exists ( - from z suchthat path (x, z) andalso edge (z, y)); + from z where path (x, z) andalso edge (z, y)); +> val path = fn : int * int -> bool *) -(* TODO should return -val path = fn : int * int -> bool - *) -(* -from p suchthat path p; +(* TODO +from p where path p; +> val it = [(1,2),(2,3),(1,3)] : (int * int) list *) -(* TODO should return -val it = [(1,2),(2,3),(1,3)] : (int * int) list - *) (* More edges *) (* @@ -242,28 +327,30 @@ fun edge (x, y) = (x, y) elem edges; (*) Return points that are 2 hops apart. (* -from (x, y, z) suchthat edge (x, y) andalso edge (y, z) andalso x <> z +from x, y, z where edge (x, y) andalso edge (y, z) andalso x <> z group x, z; *) (* Previous is equivalent to following. (Which implies a theorem connecting 'exists' with 'group' and variable elimination.) *) (* -from (x, z) suchthat - exists (from y suchthat edge (x, y) andalso edge (y, z)) +from x, z + where exists (from y where edge (x, y) andalso edge (y, z)) andalso x <> z; *) (*) Also equivalent. (* -from (x, z) suchthat exists ( - from y suchthat edge (x, y) andalso edge (y, z) andalso x <> z); +from x, z where exists ( + from y where edge (x, y) andalso edge (y, z) andalso x <> z); *) (*) Also equivalent. (* -from (x, y) suchthat edge (x, y), - (y2, z) suchthat y2 = y andalso edge (y, z) andalso x <> z +from x, y + where edge (x, y) + join y2, z + where y2 = y andalso edge (y, z) andalso x <> z group x, y; *) @@ -318,62 +405,56 @@ fun sells (bar, beer, price) = * Datalog: * Happy(p) <- Frequents(p, bar) AND Likes(p, beer) AND Sells(bar, beer) *) -(* +(* TODO fun happy patron = exists ( - from (bar, beer, price) suchthat frequents (patron, bar) + from bar, beer, price + where frequents (patron, bar) andalso likes (patron, beer) andalso sells (bar, beer, price)); +> val happy = fn : string -> bool *) -(* TODO should return -val happy = fn : string -> bool - *) (* Find happy patrons. Shaggy is happy because the Squirrel and Cask sell Amber; Velma is happy because Cask sells Stout. Fred and Scooby are not happy. *) -(* -from p suchthat happy p; +(* TODO +from p where happy p; +> val it = ["shaggy", "velma"] : string list *) -(* TODO should return -val it = ["shaggy", "velma"] : string list - *) (* A beer is considered cheap if there are at least two bars that sell it for * under $3. - + * * Datalog: * Cheap(beer) <- Sells(bar1, beer, p1) AND Sells(bar2, beer, p2) * AND p1 < 3 AND p2 < 3 AND bar1 <> bar2 *) -(* +(* TODO fun cheap beer = exists ( - from (bar1, price1, bar2, price2) - suchthat sells (bar1, beer, price1) + from bar1, price1, bar2, price2 + where sells (bar1, beer, price1) andalso sells (bar2, beer, price2) andalso price1 < 3 andalso price2 < 3 andalso bar1 <> bar2); +> val cheap = fn : string -> bool *) -(* TODO should return -val cheap = fn : string -> bool - *) (*) Pale is cheap -(* -from b suchthat cheap b; +(* TODO +from b where cheap b; +> val it = ["pale"] : string list *) -(* TODO should return -val it = ["pale"] : string list - *) (* A rule is safe if: * 1. Each distinguished variable, * 2. Each variable in an arithmetic subgoal, and * 3. Each variable in a negated subgoal, * - * also appears in a non-negated, relational sub-goal. + * also appears in a non-negated, relational sub-goal. + * * Each of the following is unsafe and not allowed: * * 1. S(x) <- R(y) @@ -391,38 +472,36 @@ val it = ["pale"] : string list *) fun isR y = true; > val isR = fn : 'a -> bool -(* -fun isS1 x = exists (from y suchthat isR y); +(* TODO +fun isS1 x = exists (from y where isR y); +> val isS1 = fn : 'a -> bool *) -(* TODO should return -val isS1 = fn : 'a -> bool - *) (*) isS1 is unsafe -(* -from x suchthat isS1 x; +(* TODO should throw unsafe +from x where isS1 x; +> Unsafe *) -(*) TODO should throw unsafe (* -fun isS2 x = exists (from y suchthat isR y andalso not (isR x)); +fun isS2 x = exists (from y where isR y andalso not (isR x)); *) (*) isS2 is unsafe -(* -from x suchthat isS2 x; +(* TODO should throw unsafe +from x where isS2 x; +> Unsafe *) -(*) TODO should throw unsafe (* -fun isS3 x = exists (from y suchthat isR y andalso x < y); +fun isS3 x = exists (from y where isR y andalso x < y); *) (*) isS3 is unsafe -(* -from x suchthat isS3 x; +(* TODO should throw unsafe +from x where isS3 x; +> Unsafe *) -(*) TODO should throw unsafe (* Example Datalog Program. Using EDB Sells (bar, beer, price) and * Likes (patron, beer), find the patrons who like beers Joe doesn't sell. @@ -432,20 +511,17 @@ from x suchthat isS3 x; * Answer(p) <- Likes(p, b) * AND NOT JoeSells(b) *) -(* +(* TODO fun caskSells b = - exists (from (beer, price) suchthat sells ("cask", beer, price)); + exists (from beer, price where sells ("cask", beer, price)); +> val caskSells = fn : 'a -> bool *) -(* TODO should return -val caskSells = fn : 'a -> bool - *) -(* -from p suchthat exists ( - from b suchthat likes (p, b) andalso not (caskSells b)); + +(* TODO +from p where exists ( + from b where likes (p, b) andalso not (caskSells b)); +> val it = ["foo"] : string list *) -(* TODO should return something like -val it = ["foo"] : string list - *) (* Cousin * @@ -468,24 +544,21 @@ fun par (x, p) = ("g", "k"), ("h", "i")]; > val par = fn : string * string -> bool -(* +(* TODO fun sib (x, y) = exists ( - from p suchthat par (x, p) andalso par (y, p) andalso x <> y); + from p where par (x, p) andalso par (y, p) andalso x <> y); +> val sib = fn : string * string -> bool *) -(* TODO should return -val sib = fn : string * string -> bool - *) -(* +(* TODO fun cousin (x, y) = sib (x, y) orelse exists ( - from (xp, yp) suchthat par (x, xp) + from xp, yp + where par (x, xp) andalso par (y, yp) andalso cousin (xp, yp)); +> val cousin = fn : string * string -> bool *) -(* TODO should return -val cousin = fn : string * string -> bool - *) (* Round 1: (b, c), (c, e), (g, h), (j, k) @@ -493,37 +566,34 @@ val cousin = fn : string * string -> bool Round 3: add (f, g), (f, h), (g, i), (i, k) Round 4: add (i, j), (k, k) *) -(* +(* TODO enumerate sib; +> val it = [("b","c")] : (string * string) list *) -(* TODO return something like -val it = [("b","c")] : (string * string) list - *) + (* enumerate cousin; +> val it = [("b","c"), ("c","e"),("g","h"),("j","k"),("f","g"),("f","h"),("g","i"),("i","k"),("i","j"),("k","k")] : (string * string) list *) -(* TODO return something like -val it = [("b","c"), ("c","e"),("g","h"),("j","k"),("f","g"),("f","h"),("g","i"),("i","k"),("i","j"),("k","k")] : (string * string) list - *) (* Nonmonotone relation. * 'cousin2' is as 'cousin', but 'orelse' has become 'and not'. * The graph is not stratified: there is a path with an infinite number * of 'not' as we traverse the cycle cousin - s2 - cousin, * where s2 is the expression 'notExists (...)'. *) -(* +(* TODO fun cousin2 (x, y) = sib (x, y) andalso notExists ( - from (xp, yp) suchthat par (x, xp) + from (xp, yp) + where par (x, xp) andalso par (y, yp) andalso cousin2 (xp, yp)); +> val cousin2 = fn : string * string -> bool *) -(* TODO should return -val cousin2 = fn : string * string -> bool - *) -(* + +(* TODO enumerate cousin2; +> Error: non-stratified *) -(*) TODO: maybe give error: non-stratified (*) End suchThat.smli