diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 66068027d..b89c79103 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,18 +25,31 @@ jobs: strategy: matrix: - java-version: [ 8, 11, 17 ] + java-version: [ "11" ] + guava-version: [ "" ] + checkstyle-version: [ "" ] javadoc: [ false ] include: - - java-version: 17 + - java-version: "8" + guava-version: "19.0" + checkstyle-version: "9.3" + javadoc: false + - java-version: "17" + guava-version: "23.0" javadoc: true + - java-version: "17" + guava-version: "31.1-jre" + javadoc: false + checkstyle-version: "" + - java-version: "18" + javadoc: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v1 with: fetch-depth: 0 - name: Set up JDK - uses: actions/setup-java@v2 + uses: actions/setup-java@v1 with: java-version: ${{ matrix.java-version }} distribution: 'adopt' @@ -47,6 +60,15 @@ jobs: then GOALS="$GOALS javadoc:javadoc javadoc:test-javadoc" fi - mvn -Dmorel.ci --batch-mode --update-snapshots $GOALS + DEFS="-Dmorel.ci" + if [ "${{ matrix.checkstyle-version }}" ] + then + DEFS="$DEFS -Dcheckstyle.version=${{ matrix.checkstyle-version }}" + fi + if [ "${{ matrix.guava-version }}" ] + then + DEFS="$DEFS -Dguava.version=${{ matrix.guava-version }}" + fi + mvn $DEFS --batch-mode --update-snapshots $GOALS # End main.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ab9d43a0d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed to Julian Hyde under one or more contributor license -# agreements. See the NOTICE file distributed with this work -# for additional information regarding copyright ownership. -# Julian Hyde licenses this file to you under the Apache -# License, Version 2.0 (the "License"); you may not use this -# file except in compliance with the License. You may obtain a -# copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -# either express or implied. See the License for the specific -# language governing permissions and limitations under the -# License. -# -# Configuration for Travis CI -language: java -matrix: - fast_finish: true - include: - - env: IMAGE=maven:3-jdk-13 - - env: IMAGE=maven:3-jdk-12 JDOC=Y - - env: IMAGE=maven:3-jdk-11 - - env: IMAGE=maven:3-jdk-10 SITE=Y - - env: IMAGE=maven:3-jdk-9 - - env: IMAGE=maven:3-jdk-8 JDOC=Y -env: - global: - - DOCKERRUN="docker run -it --rm -v $PWD:/src -v $HOME/.m2:/root/.m2 -w /src" -services: - - docker -before_install: - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker pull $IMAGE -install: skip -script: - - if [ "$JDOC" = "Y" ]; then export JDOC=javadoc:javadoc; fi - - if [ "$SITE" = "Y" ]; then export SITE="site"; fi - - $DOCKERRUN $IMAGE ./mvnw -Dcheckstyle.skip -Dsurefire.useFile=false -Dsurefire.threadCount=1 -Dsurefire.perCoreThreadCount=false -Djavax.net.ssl.trustStorePassword=changeit test $JDOC $SITE -cache: - directories: - - $HOME/.m2 -git: - depth: 1000 -# End .travis.yml diff --git a/README.md b/README.md index abe257d19..e223ab4ea 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ until version 0.2.) ## Requirements -Java version 8 or higher. +Java version 11 or higher. ## Get Morel @@ -61,6 +61,8 @@ On Windows, the last line is > mvnw install ``` +If you are using Java 8, you should add parameters `-Dcheckstyle.version=9.3`. + ### Run the shell ```bash diff --git a/docs/reference.md b/docs/reference.md index f71431e9c..66906760d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -45,7 +45,6 @@ In Morel but not Standard ML: In Standard ML but not in Morel: * `word` constant * `longid` identifier -* type annotations ("`:` *typ*") (appears in expressions, patterns, and *funmatch*) * references (`ref` and operators `!` and `:=`) * exceptions (`raise`, `handle`, `exception`) * `while` loop @@ -131,6 +130,7 @@ In Standard ML but not in Morel: | '(' exp1 ; ... ; expn ')' sequence (n ≥ 2) | let dec in exp1 ; ... ; expn end local declaration (n ≥ 1) + | exp : type type annotation | exp1 andalso exp2 conjunction | exp1 orelse exp2 disjunction | if exp1 then exp2 else exp3 @@ -173,6 +173,7 @@ In Standard ML but not in Morel: | '(' pat1 , ... , patn ')' tuple (n ≠ 1) | { [ patrow ] } record | '[' pat1 , ... , patn ']' list (n ≥ 0) + | pat : type type annotation | id as pat layered patrow → '...' wildcard | lab = pat [, patrow] pattern @@ -206,10 +207,10 @@ In Standard ML but not in Morel: funbindfunmatch [ and funmatch ]* clausal function funmatchfunmatchItem [ '|' funmatchItem ]* -funmatchItem → [ op ] id pat1 ... patn = exp +funmatchItem → [ op ] id pat1 ... patn [ : type ] = exp nonfix (n ≥ 1) - | pat1 id pat2 = exp infix - | '(' pat1 id pat2 ')' pat'1 ... pat'n = exp + | pat1 id pat2 [ : type ] = exp infix + | '(' pat1 id pat2 ')' pat'1 ... pat'n [ : type ] = exp infix (n ≥ 0) datbinddatbindItem [ and datbindItem ]* data type @@ -470,11 +471,12 @@ Each property is set using the function `Sys.set (name, value)`, displayed using `Sys.show name`, and unset using `Sys.unset name`. -| Name | Type | Default | Description | -| ---------------- | ---- | ------- | ----------- | -| hybrid | bool | false | Whether to try to create a hybrid execution plan that uses Apache Calcite relational algebra. | -| inlinePassCount | int | 5 | Maximum number of inlining passes. | -| lineWidth | int | 79 | When printing, the length at which lines are wrapped. | -| printDepth | int | 5 | When printing, the depth of nesting of recursive data structure at which ellipsis begins. | -| printLength | int | 12 | When printing, the length of lists at which ellipsis begins. | -| stringDepth | int | 70 | When printing, the length of strings at which ellipsis begins. | +| Name | Type | Default | Description | +| -------------------- | ---- | ------- | ----------- | +| hybrid | bool | false | Whether to try to create a hybrid execution plan that uses Apache Calcite relational algebra. | +| inlinePassCount | int | 5 | Maximum number of inlining passes. | +| lineWidth | int | 79 | When printing, the length at which lines are wrapped. | +| matchCoverageEnabled | bool | true | Whether to check whether patterns are exhaustive and/or redundant. | +| printDepth | int | 5 | When printing, the depth of nesting of recursive data structure at which ellipsis begins. | +| printLength | int | 12 | When printing, the length of lists at which ellipsis begins. | +| stringDepth | int | 70 | When printing, the length of strings at which ellipsis begins. | diff --git a/pom.xml b/pom.xml index ac421ecdc..76d07840b 100644 --- a/pom.xml +++ b/pom.xml @@ -76,28 +76,34 @@ License. ${project.basedir} - 1.29.0 - 7.8.2 - 1.3.9 - 0.4 - - 21.0 + 3.3.0 + 1.30.0 + + 10.2 + 3.0.2 + 0.5 + 4.9.10 + + 31.1-jre 2.2 - 2.3.1 - 0.3 + 2.5.1 1.1.2 - 3.0.0 - 7.0.5 - 3.16.0 - 5.7.2 - 3.0.0 - 3.0.0-M1 - 3.0.1 - 2.9 - 3.7.1 - 3.0.0-M3 - 0.1 - 1.7.25 + 3.0.3 + 7.0.11 + 3.21.0 + 5.8.2 + 3.1.2 + 3.10.1 + 3.0.0 + 3.4.0 + 3.3.0 + 3.12.0 + 3.2.1 + 3.0.0-M6 + 0.2 + 1.7.36 -html5 net.hydromatic.morel.parse @@ -234,6 +240,7 @@ License. org.apache.maven.plugins maven-source-plugin + ${maven-source-plugin.version} attach-sources @@ -248,6 +255,7 @@ License. org.apache.maven.plugins maven-compiler-plugin + ${maven-compiler-plugin.version} 8 8 @@ -280,17 +288,6 @@ License. checkstyle ${checkstyle.version} - - net.hydromatic - toolbox - ${hydromatic-toolbox.version} - - - com.google.guava - guava - - - @@ -335,6 +332,7 @@ License. org.codehaus.mojo build-helper-maven-plugin + ${build-helper-maven-plugin.version} @@ -397,6 +395,7 @@ License. pl.project13.maven git-commit-id-plugin + ${git-commit-id-plugin.version} false diff --git a/src/main/config/checkstyle/checker.xml b/src/main/config/checkstyle/checker.xml index 90d3e8b76..931b97024 100644 --- a/src/main/config/checkstyle/checker.xml +++ b/src/main/config/checkstyle/checker.xml @@ -57,9 +57,18 @@ License. - - + + + + + + + + + @@ -126,8 +135,10 @@ License. + + @@ -150,9 +161,6 @@ License. - @@ -220,15 +228,6 @@ License. - - - - - - - - @@ -256,28 +255,23 @@ License. - - - - - - - - - - + + + + + + - - - - - + + + + + - - - - + + + + + - - diff --git a/src/main/java/net/hydromatic/morel/Main.java b/src/main/java/net/hydromatic/morel/Main.java index 58b4a3bfb..8aa70dadd 100644 --- a/src/main/java/net/hydromatic/morel/Main.java +++ b/src/main/java/net/hydromatic/morel/Main.java @@ -19,7 +19,7 @@ package net.hydromatic.morel; import net.hydromatic.morel.ast.AstNode; -import net.hydromatic.morel.compile.CompileException; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.CompiledStatement; import net.hydromatic.morel.compile.Compiles; import net.hydromatic.morel.compile.Environment; @@ -31,6 +31,7 @@ import net.hydromatic.morel.parse.ParseException; import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.MorelException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -150,6 +151,7 @@ void run(Session session, BufferingReader in2) { new SubShell(main, outLines, bindingMap, env0); for (;;) { try { + parser.zero("stdIn"); final AstNode statement = parser.statementSemicolonOrEof(); String code = in2.flush(); if (statement == null && code.endsWith("\n")) { @@ -182,15 +184,17 @@ void run(Session session, BufferingReader in2) { } } - @Override public void use(String fileName) { + @Override public void use(String fileName, Pos pos) { throw new UnsupportedOperationException(); } @Override public void handle(RuntimeException e, StringBuilder buf) { - if (e instanceof Codes.MorelRuntimeException) { - ((Codes.MorelRuntimeException) e).describeTo(buf); - } else if (e instanceof CompileException) { - buf.append(e.getMessage()); + if (e instanceof MorelException) { + final MorelException me = (MorelException) e; + me.describeTo(buf) + .append("\n") + .append(" raised at: "); + me.pos().describeTo(buf); } else { buf.append(e); } @@ -209,7 +213,7 @@ static class SubShell extends Shell { super(main, env0, outLines, outBindings); } - @Override public void use(String fileName) { + @Override public void use(String fileName, Pos pos) { outLines.accept("[opening " + fileName + "]"); File file = new File(fileName); if (!file.isAbsolute()) { @@ -219,7 +223,7 @@ static class SubShell extends Shell { outLines.accept("[use failed: Io: openIn failed on " + fileName + ", No such file or directory]"); - throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR); + throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR, pos); } try (FileReader fileReader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(fileReader)) { @@ -234,16 +238,20 @@ void command(AstNode statement, Consumer outLines) { final Environment env = env0.bindAll(bindingMap.values()); final CompiledStatement compiled = Compiles.prepareStatement(main.typeSystem, main.session, env, - statement, null); + statement, null, e -> appendToOutput(e, outLines)); final List bindings = new ArrayList<>(); compiled.eval(main.session, env, outLines, bindings::add); bindings.forEach(b -> this.bindingMap.put(b.id.name, b)); } catch (Codes.MorelRuntimeException e) { - final StringBuilder buf = new StringBuilder(); - main.session.handle(e, buf); - outLines.accept(buf.toString()); + appendToOutput(e, outLines); } } + + private void appendToOutput(MorelException e, Consumer outLines) { + final StringBuilder buf = new StringBuilder(); + main.session.handle(e, buf); + outLines.accept(buf.toString()); + } } /** Reader that snoops which characters have been read and saves diff --git a/src/main/java/net/hydromatic/morel/ProgrammaticShell.java b/src/main/java/net/hydromatic/morel/ProgrammaticShell.java new file mode 100644 index 000000000..02ee31085 --- /dev/null +++ b/src/main/java/net/hydromatic/morel/ProgrammaticShell.java @@ -0,0 +1,156 @@ +package net.hydromatic.morel; + +import com.google.common.collect.ImmutableMap; +import net.hydromatic.morel.ast.AstNode; +import net.hydromatic.morel.ast.Pos; +import net.hydromatic.morel.compile.CompiledStatement; +import net.hydromatic.morel.compile.Compiles; +import net.hydromatic.morel.compile.Environment; +import net.hydromatic.morel.compile.Environments; +import net.hydromatic.morel.eval.Codes; +import net.hydromatic.morel.eval.Session; +import net.hydromatic.morel.foreign.ForeignValue; +import net.hydromatic.morel.parse.MorelParserImpl; +import net.hydromatic.morel.parse.ParseException; +import net.hydromatic.morel.type.Binding; +import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.MorelException; + +import java.io.PrintWriter; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + + +/** + * Programmatic, non-interactive shell for Morel. + *

+ * Meant to be used for programmatic evaluation of expressions. + * Useful for embedding Morel in other programs, and for testing. + *

+ * The shell contains a persistent environment/foreign value map, + * along with a cache of compiled statements and their results, + * which reduces the cost of repeated invocations. + */ +public class ProgrammaticShell implements Session.Shell { + private Environment env0; + private Map foreignValueMap; + + private final Session session = new Session(); + private TypeSystem typeSystem = new TypeSystem(); + + ProgrammaticShell(Map foreignValueMap) { + this.foreignValueMap = ImmutableMap.copyOf(foreignValueMap); + this.env0 = makeEnv(typeSystem, foreignValueMap); + } + + public Map getForeignValueMap() { + return foreignValueMap; + } + + public void setForeignValueMap(Map foreignValueMap) { + this.foreignValueMap = ImmutableMap.copyOf(foreignValueMap); + this.env0 = makeEnv(typeSystem, foreignValueMap); + } + + public void setTypeSystem(TypeSystem typeSystem) { + this.typeSystem = typeSystem; + this.env0 = makeEnv(typeSystem, foreignValueMap); + } + + private static Environment makeEnv(TypeSystem typeSystem, Map foreignValueMap) { + return Environments.env(typeSystem, foreignValueMap); + } + + /**************************************************************/ + + public void run(String code, PrintWriter out, boolean echo) { + final MorelParserImpl parser = new MorelParserImpl(new StringReader(code)); + final Consumer outLines = out::println; + + while (true) { + try { + parser.zero("stdIn"); + final AstNode statement = parser.statementSemicolonOrEof(); + + if (statement == null && code.endsWith("\n")) { + code = code.substring(0, code.length() - 1); + } + + if (echo) { + outLines.accept(code); + } + + if (statement == null) { + break; + } + + session.withShell(this, outLines, session1 -> + command(statement, outLines)); + } catch (ParseException e) { + final String message = e.getMessage(); + + if (message.startsWith("Encountered \"\" ")) { + break; + } + + if (echo) { + outLines.accept(code); + } + + outLines.accept(message); + if (code.length() == 0) { + // If we consumed no input, we're not making progress, so we'll + // never finish. Abort. + break; + } + } + } + } + + private void command(AstNode statement, Consumer outLines) { + try { + final Map outBindings = new LinkedHashMap<>(); + final Environment env = env0.bindAll(outBindings.values()); + + final CompiledStatement compiled = + Compiles.prepareStatement(typeSystem, session, env, + statement, null, e -> appendToOutput(e, outLines)); + + final List bindings = new ArrayList<>(); + compiled.eval(session, env, outLines, bindings::add); + bindings.forEach(b -> outBindings.put(b.id.name, b)); + } catch (Codes.MorelRuntimeException e) { + appendToOutput(e, outLines); + } + } + + private void appendToOutput(MorelException e, Consumer outLines) { + final StringBuilder buf = new StringBuilder(); + session.handle(e, buf); + outLines.accept(buf.toString()); + } + + /**************************************************************/ + + @Override + public void use(String fileName, Pos pos) { + throw new UnsupportedOperationException(); + } + + @Override + public void handle(RuntimeException e, StringBuilder buf) { + if (e instanceof MorelException) { + final MorelException me = (MorelException) e; + me.describeTo(buf) + .append("\n") + .append(" raised at: "); + me.pos().describeTo(buf); + } else { + buf.append(e); + } + } +} diff --git a/src/main/java/net/hydromatic/morel/Shell.java b/src/main/java/net/hydromatic/morel/Shell.java index 7ac30679e..6fa3b9784 100644 --- a/src/main/java/net/hydromatic/morel/Shell.java +++ b/src/main/java/net/hydromatic/morel/Shell.java @@ -19,6 +19,7 @@ package net.hydromatic.morel; import net.hydromatic.morel.ast.AstNode; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.CompileException; import net.hydromatic.morel.compile.CompiledStatement; import net.hydromatic.morel.compile.Compiles; @@ -33,6 +34,7 @@ import net.hydromatic.morel.parse.ParseException; import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.MorelException; import net.hydromatic.morel.util.Pair; import com.google.common.collect.ImmutableList; @@ -474,11 +476,13 @@ void extracted(@Nullable Map outBindings) { new MorelParserImpl(new StringReader(code)); final AstNode statement; try { + smlParser.zero("stdIn"); statement = smlParser.statementSemicolon(); final Environment env0 = env1; + final List warningList = new ArrayList<>(); final CompiledStatement compiled = Compiles.prepareStatement(typeSystem, session, env0, - statement, null); + statement, null, warningList::add); final Use shell = new Use(env0, bindingMap); session.withShell(shell, outLines, session1 -> compiled.eval(session1, env0, outLines, bindings::add)); @@ -515,7 +519,7 @@ private class Use implements Session.Shell { this.bindings = bindings; } - @Override public void use(String fileName) { + @Override public void use(String fileName, Pos pos) { outLines.accept("[opening " + fileName + "]"); File file = new File(fileName); if (!file.isAbsolute()) { @@ -525,13 +529,13 @@ private class Use implements Session.Shell { outLines.accept("[use failed: Io: openIn failed on " + fileName + ", No such file or directory]"); - throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR); + throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR, pos); } if (depth > maxDepth && maxDepth >= 0) { outLines.accept("[use failed: Io: openIn failed on " + fileName + ", Too many open files]"); - throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR); + throw new Codes.MorelRuntimeException(Codes.BuiltInExn.ERROR, pos); } try (FileReader fileReader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(fileReader)) { @@ -549,10 +553,12 @@ private class Use implements Session.Shell { if (depth != 1) { throw e; } - if (e instanceof Codes.MorelRuntimeException) { - ((Codes.MorelRuntimeException) e).describeTo(buf); - } else if (e instanceof CompileException) { - buf.append(e.getMessage()); + if (e instanceof MorelException) { + final MorelException me = (MorelException) e; + me.describeTo(buf) + .append("\n") + .append(" raised at: "); + me.pos().describeTo(buf); } else { buf.append(e); } diff --git a/src/main/java/net/hydromatic/morel/ast/Ast.java b/src/main/java/net/hydromatic/morel/ast/Ast.java index 808f0202d..da5490d59 100644 --- a/src/main/java/net/hydromatic/morel/ast/Ast.java +++ b/src/main/java/net/hydromatic/morel/ast/Ast.java @@ -496,7 +496,7 @@ public static class AnnotatedExp extends Exp { public final Exp exp; /** Creates a type annotation. */ - AnnotatedExp(Pos pos, Type type, Exp exp) { + AnnotatedExp(Pos pos, Exp exp, Type type) { super(pos, Op.ANNOTATED_EXP); this.type = requireNonNull(type); this.exp = requireNonNull(exp); @@ -1087,12 +1087,15 @@ AstWriter unparse(AstWriter w, int left, int right) { public static class FunMatch extends AstNode { public final String name; public final List patList; + @Nullable public final Type returnType; public final Exp exp; - FunMatch(Pos pos, String name, ImmutableList patList, Exp exp) { + FunMatch(Pos pos, String name, ImmutableList patList, + @Nullable Type returnType, Exp exp) { super(pos, Op.FUN_MATCH); this.name = name; this.patList = patList; + this.returnType = returnType; this.exp = exp; } diff --git a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java index a9a13cf38..56a604d5d 100644 --- a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java @@ -362,8 +362,10 @@ public Ast.FunBind funBind(Pos pos, } public Ast.FunMatch funMatch(Pos pos, String name, - Iterable patList, Ast.Exp exp) { - return new Ast.FunMatch(pos, name, ImmutableList.copyOf(patList), exp); + Iterable patList, @Nullable Ast.Type returnType, + Ast.Exp exp) { + return new Ast.FunMatch(pos, name, ImmutableList.copyOf(patList), + returnType, exp); } public Ast.Apply apply(Ast.Exp fn, Ast.Exp arg) { @@ -379,8 +381,8 @@ public Ast.InfixPat infixPat(Pos pos, Op op, Ast.Pat p0, Ast.Pat p1) { return new Ast.InfixPat(pos, op, p0, p1); } - public Ast.Exp annotatedExp(Pos pos, Ast.Type type, Ast.Exp expression) { - return new Ast.AnnotatedExp(pos, type, expression); + public Ast.Exp annotatedExp(Pos pos, Ast.Exp expression, Ast.Type type) { + return new Ast.AnnotatedExp(pos, expression, type); } public Ast.Exp infixCall(Pos pos, Op op, Ast.Exp a0, Ast.Exp a1) { diff --git a/src/main/java/net/hydromatic/morel/ast/Core.java b/src/main/java/net/hydromatic/morel/ast/Core.java index 067dd957a..b291811ec 100644 --- a/src/main/java/net/hydromatic/morel/ast/Core.java +++ b/src/main/java/net/hydromatic/morel/ast/Core.java @@ -41,12 +41,10 @@ import java.util.Objects; import java.util.Set; import java.util.SortedMap; -import java.util.function.BiConsumer; import java.util.function.ObjIntConsumer; import javax.annotation.Nullable; import static net.hydromatic.morel.ast.CoreBuilder.core; -import static net.hydromatic.morel.ast.Pos.ZERO; import static com.google.common.base.Preconditions.checkArgument; @@ -59,13 +57,14 @@ * class names short. */ // TODO: remove 'parse tree for...' from all the comments below +@SuppressWarnings("StaticPseudoFunctionalStyleMethod") public class Core { private Core() {} /** Abstract base class of Core nodes. */ abstract static class BaseNode extends AstNode { - BaseNode(Op op) { - super(ZERO, op); + BaseNode(Pos pos, Op op) { + super(pos, op); } @Override public AstNode accept(Shuttle shuttle) { @@ -87,7 +86,7 @@ public abstract static class Pat extends BaseNode { public final Type type; Pat(Op op, Type type) { - super(op); + super(Pos.ZERO, op); this.type = requireNonNull(type); } @@ -459,8 +458,8 @@ public Pat copy(TypeSystem typeSystem, Set argNames, public abstract static class Exp extends BaseNode { public final Type type; - Exp(Op op, Type type) { - super(op); + Exp(Pos pos, Op op, Type type) { + super(pos, op); this.type = requireNonNull(type); } @@ -488,7 +487,7 @@ public static class Id extends Exp { /** Creates an Id. */ Id(NamedPat idPat) { - super(Op.ID, idPat.type); + super(Pos.ZERO, Op.ID, idPat.type); this.idPat = requireNonNull(idPat); } @@ -523,7 +522,7 @@ public static class RecordSelector extends Exp { /** Creates a record selector. */ RecordSelector(FnType fnType, int slot) { - super(Op.RECORD_SELECTOR, fnType); + super(Pos.ZERO, Op.RECORD_SELECTOR, fnType); this.slot = slot; } @@ -567,7 +566,7 @@ public static class Literal extends Exp { /** Creates a Literal. */ Literal(Op op, Type type, Comparable value) { - super(op, type); + super(Pos.ZERO, op, type); this.value = requireNonNull(value); } @@ -608,8 +607,8 @@ public Object unwrap() { /** Base class for declarations. */ public abstract static class Decl extends BaseNode { - Decl(Op op) { - super(op); + Decl(Pos pos, Op op) { + super(pos, op); } @Override public abstract Decl accept(Shuttle shuttle); @@ -620,7 +619,7 @@ public static class DatatypeDecl extends Decl { public final List dataTypes; DatatypeDecl(ImmutableList dataTypes) { - super(Op.DATATYPE_DECL); + super(Pos.ZERO, Op.DATATYPE_DECL); this.dataTypes = requireNonNull(dataTypes); checkArgument(!this.dataTypes.isEmpty()); } @@ -652,13 +651,19 @@ public static class DatatypeDecl extends Decl { /** Abstract (recursive or non-recursive) value declaration. */ public abstract static class ValDecl extends Decl { - ValDecl(Op op) { - super(op); + ValDecl(Pos pos, Op op) { + super(pos, op); } @Override public abstract ValDecl accept(Shuttle shuttle); - public abstract void forEachBinding(BiConsumer consumer); + public abstract void forEachBinding(BindingConsumer consumer); + } + + /** Consumer of bindings. */ + @FunctionalInterface + public interface BindingConsumer { + void accept(NamedPat namedPat, Exp exp, Pos pos); } /** Non-recursive value declaration. @@ -668,8 +673,8 @@ public static class NonRecValDecl extends ValDecl { public final NamedPat pat; public final Exp exp; - NonRecValDecl(NamedPat pat, Exp exp) { - super(Op.VAL_DECL); + NonRecValDecl(NamedPat pat, Exp exp, Pos pos) { + super(pos, Op.VAL_DECL); this.pat = pat; this.exp = exp; } @@ -700,11 +705,11 @@ public static class NonRecValDecl extends ValDecl { public NonRecValDecl copy(NamedPat pat, Exp exp) { return pat == this.pat && exp == this.exp ? this - : core.nonRecValDecl(pat, exp); + : core.nonRecValDecl(pat, exp, pos); } - @Override public void forEachBinding(BiConsumer consumer) { - consumer.accept(pat, exp); + @Override public void forEachBinding(BindingConsumer consumer) { + consumer.accept(pat, exp, pos); } } @@ -713,7 +718,7 @@ public static class RecValDecl extends ValDecl { public final ImmutableList list; RecValDecl(ImmutableList list) { - super(Op.REC_VAL_DECL); + super(Pos.ZERO, Op.REC_VAL_DECL); this.list = requireNonNull(list); } @@ -743,7 +748,7 @@ public static class RecValDecl extends ValDecl { visitor.visit(this); } - @Override public void forEachBinding(BiConsumer consumer) { + @Override public void forEachBinding(BindingConsumer consumer) { list.forEach(b -> b.forEachBinding(consumer)); } @@ -759,7 +764,7 @@ public static class Tuple extends Exp { public final List args; Tuple(RecordLikeType type, ImmutableList args) { - super(Op.TUPLE, type); + super(Pos.ZERO, Op.TUPLE, type); this.args = ImmutableList.copyOf(args); } @@ -806,7 +811,7 @@ public static class Let extends Exp { public final Exp exp; Let(ValDecl decl, Exp exp) { - super(Op.LET, exp.type); + super(Pos.ZERO, Op.LET, exp.type); this.decl = requireNonNull(decl); this.exp = requireNonNull(exp); } @@ -837,7 +842,7 @@ public static class Local extends Exp { public final Exp exp; Local(DataType dataType, Exp exp) { - super(Op.LOCAL, exp.type); + super(Pos.ZERO, Op.LOCAL, exp.type); this.dataType = requireNonNull(dataType); this.exp = requireNonNull(exp); } @@ -873,8 +878,8 @@ public static class Match extends BaseNode { public final Pat pat; public final Exp exp; - Match(Pat pat, Exp exp) { - super(Op.MATCH); + Match(Pos pos, Pat pat, Exp exp) { + super(pos, Op.MATCH); this.pat = pat; this.exp = exp; } @@ -893,7 +898,7 @@ public static class Match extends BaseNode { public Match copy(Pat pat, Exp exp) { return pat == this.pat && exp == this.exp ? this - : core.match(pat, exp); + : core.match(pat, exp, pos); } } @@ -903,7 +908,7 @@ public static class Fn extends Exp { public final Exp exp; Fn(FnType type, IdPat idPat, Exp exp) { - super(Op.FN, type); + super(Pos.ZERO, Op.FN, type); this.idPat = requireNonNull(idPat); this.exp = requireNonNull(exp); } @@ -938,8 +943,8 @@ public static class Case extends Exp { public final Exp exp; public final List matchList; - Case(Type type, Exp exp, ImmutableList matchList) { - super(Op.CASE, type); + Case(Pos pos, Type type, Exp exp, ImmutableList matchList) { + super(pos, Op.CASE, type); this.exp = exp; this.matchList = matchList; } @@ -959,7 +964,7 @@ public static class Case extends Exp { public Case copy(Exp exp, List matchList) { return exp == this.exp && matchList.equals(this.matchList) ? this - : core.caseOf(type, exp, matchList); + : core.caseOf(type, exp, matchList, pos); } } @@ -968,7 +973,7 @@ public static class From extends Exp { public final ImmutableList steps; From(ListType type, ImmutableList steps) { - super(Op.FROM, type); + super(Pos.ZERO, Op.FROM, type); this.steps = requireNonNull(steps); } @@ -1007,7 +1012,7 @@ public abstract static class FromStep extends BaseNode { public final ImmutableList bindings; FromStep(Op op, ImmutableList bindings) { - super(op); + super(Pos.ZERO, op); this.bindings = bindings; } @@ -1150,7 +1155,7 @@ public static class OrderItem extends BaseNode { public final Ast.Direction direction; OrderItem(Exp exp, Ast.Direction direction) { - super(Op.ORDER_ITEM); + super(Pos.ZERO, Op.ORDER_ITEM); this.exp = requireNonNull(exp); this.direction = requireNonNull(direction); } @@ -1251,8 +1256,8 @@ public static class Apply extends Exp { public final Exp fn; public final Exp arg; - Apply(Type type, Exp fn, Exp arg) { - super(Op.APPLY, type); + Apply(Pos pos, Type type, Exp fn, Exp arg) { + super(pos, Op.APPLY, type); this.fn = fn; this.arg = arg; } @@ -1292,7 +1297,7 @@ public static class Apply extends Exp { public Apply copy(Exp fn, Exp arg) { return fn == this.fn && arg == this.arg ? this - : core.apply(type, fn, arg); + : core.apply(pos, type, fn, arg); } } @@ -1306,7 +1311,7 @@ public static class Aggregate extends BaseNode { public final @Nullable Exp argument; Aggregate(Type type, Exp aggregate, @Nullable Exp argument) { - super(Op.AGGREGATE); + super(Pos.ZERO, Op.AGGREGATE); this.type = type; this.aggregate = requireNonNull(aggregate); this.argument = argument; diff --git a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java index bb6e2e2c4..efcbf7ea6 100644 --- a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java @@ -292,8 +292,9 @@ public Core.Local local(DataType dataType, Core.Exp exp) { return new Core.Local(dataType, exp); } - public Core.NonRecValDecl nonRecValDecl(Core.NamedPat pat, Core.Exp exp) { - return new Core.NonRecValDecl(pat, exp); + public Core.NonRecValDecl nonRecValDecl(Core.NamedPat pat, Core.Exp exp, + Pos pos) { + return new Core.NonRecValDecl(pat, exp, pos); } public Core.RecValDecl recValDecl( @@ -301,13 +302,13 @@ public Core.RecValDecl recValDecl( return new Core.RecValDecl(ImmutableList.copyOf(list)); } - public Core.Match match(Core.Pat pat, Core.Exp exp) { - return new Core.Match(pat, exp); + public Core.Match match(Core.Pat pat, Core.Exp exp, Pos pos) { + return new Core.Match(pos, pat, exp); } public Core.Case caseOf(Type type, Core.Exp exp, - Iterable matchList) { - return new Core.Case(type, exp, ImmutableList.copyOf(matchList)); + Iterable matchList, Pos pos) { + return new Core.Case(pos, type, exp, ImmutableList.copyOf(matchList)); } public Core.From from(ListType type, List steps) { @@ -371,17 +372,19 @@ public Core.Fn fn(FnType type, Core.IdPat idPat, Core.Exp exp) { return new Core.Fn(type, idPat, exp); } - public Core.Apply apply(Type type, Core.Exp fn, Core.Exp arg) { - return new Core.Apply(type, fn, arg); + public Core.Apply apply(Pos pos, Type type, Core.Exp fn, Core.Exp arg) { + return new Core.Apply(pos, type, fn, arg); } public Core.Case ifThenElse(Core.Exp condition, Core.Exp ifTrue, Core.Exp ifFalse) { // Translate "if c then a else b" - // as if user had written "case c of true => a | _ => b" - return new Core.Case(ifTrue.type, condition, - ImmutableList.of(match(truePat, ifTrue), - match(boolWildcardPat, ifFalse))); + // as if user had written "case c of true => a | _ => b". + // Pos.ZERO is ok because match failure is impossible. + final Pos pos = Pos.ZERO; + return new Core.Case(pos, ifTrue.type, condition, + ImmutableList.of(match(truePat, ifTrue, pos), + match(boolWildcardPat, ifFalse, pos))); } public Core.DatatypeDecl datatypeDecl(Iterable dataTypes) { diff --git a/src/main/java/net/hydromatic/morel/ast/Op.java b/src/main/java/net/hydromatic/morel/ast/Op.java index 5605fd28b..0d01623ba 100644 --- a/src/main/java/net/hydromatic/morel/ast/Op.java +++ b/src/main/java/net/hydromatic/morel/ast/Op.java @@ -91,7 +91,7 @@ public enum Op { FORALL_TYPE, // annotated expression "e: t" - ANNOTATED_EXP(" : "), + ANNOTATED_EXP(" : ", 0), TIMES(" * ", 7), DIVIDE(" / ", 7), diff --git a/src/main/java/net/hydromatic/morel/ast/Pos.java b/src/main/java/net/hydromatic/morel/ast/Pos.java index 932d8f9bf..b45686c10 100644 --- a/src/main/java/net/hydromatic/morel/ast/Pos.java +++ b/src/main/java/net/hydromatic/morel/ast/Pos.java @@ -20,6 +20,9 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import org.apache.calcite.util.Pair; +import org.apache.calcite.util.mapping.IntPair; +import org.checkerframework.checker.nullness.qual.NonNull; import java.util.AbstractList; import java.util.List; @@ -29,21 +32,49 @@ /** Position of a parse-tree node. */ public class Pos { - public static final Pos ZERO = new Pos(0, 0, 0, 0); + public static final Pos ZERO = new Pos("", 0, 0, 0, 0); + public final String file; public final int startLine; public final int startColumn; public final int endLine; public final int endColumn; /** Creates a Pos. */ - public Pos(int startLine, int startColumn, int endLine, int endColumn) { + public Pos(String file, int startLine, int startColumn, + int endLine, int endColumn) { + this.file = file; this.startLine = startLine; this.startColumn = startColumn; this.endLine = endLine; this.endColumn = endColumn; } + /** Creates a Pos from two offsets. */ + public static Pos of(String ml, String file, int startOffset, int endOffset) { + IntPair start = lineCol(ml, startOffset); + IntPair end = lineCol(ml, endOffset); + return new Pos(file, start.source, start.target, end.source, end.target); + } + + /** Creates a Pos from a filename and a string with a delimiter character. + * The delimiter must occur exactly twice in the string. */ + public static Pair<@NonNull String, @NonNull Pos> split(String s, + char delimiter, String file) { + final int i = s.indexOf(delimiter); + final int j = s.indexOf(delimiter, i + 1); + final int k = s.indexOf(delimiter, j + 1); + if (i < 0 || j <= i || k >= 0) { + throw new IllegalArgumentException("expected exactly two occurrences " + + "of delimiter, '" + delimiter + "'"); + } + final String s2 = s.substring(0, i) + + s.substring(i + 1, j) + + s.substring(j + 1); + final Pos pos = of(s2, file, i, j - 1); + return Pair.of(s2, pos); + } + @Override public int hashCode() { return Objects.hash(startLine, startColumn, endLine, endColumn); } @@ -58,7 +89,22 @@ public Pos(int startLine, int startColumn, int endLine, int endColumn) { } @Override public String toString() { - return "line " + startLine + ", column " + startColumn; + return describeTo(new StringBuilder()).toString(); + } + + public StringBuilder describeTo(StringBuilder buf) { + buf.append(file) + .append(file.isEmpty() ? "" : ":") + .append(startLine) + .append('.') + .append(startColumn); + if (endColumn != startColumn + 1 || endLine != startLine) { + buf.append('-') + .append(endLine) + .append('.') + .append(endColumn); + } + return buf; } /** @@ -127,10 +173,12 @@ private static Pos sum( int endColumn) { int testLine; int testColumn; + String file = Pos.ZERO.file; for (Pos pos : poses) { if (pos == null || pos.equals(Pos.ZERO)) { continue; } + file = pos.file; testLine = pos.startLine; testColumn = pos.startColumn; if (testLine < line || testLine == line && testColumn < column) { @@ -145,11 +193,27 @@ private static Pos sum( endColumn = testColumn; } } - return new Pos(line, column, endLine, endColumn); + return new Pos(file, line, column, endLine, endColumn); } public Pos plus(Pos pos) { - return new Pos(startLine, startColumn, pos.endLine, pos.endColumn); + int startLine = this.startLine; + int startColumn = this.startColumn; + if (pos.startLine < startLine + || pos.startLine == startLine + && pos.startColumn < startColumn) { + startLine = pos.startLine; + startColumn = pos.startColumn; + } + int endLine = pos.endLine; + int endColumn = pos.endColumn; + if (this.endLine > endLine + || this.endLine == endLine + && this.endColumn > endColumn) { + endLine = this.endLine; + endColumn = this.endColumn; + } + return new Pos(file, startLine, startColumn, endLine, endColumn); } public Pos plusAll(Iterable poses) { @@ -160,6 +224,25 @@ public Pos plusAll(@Nonnull List nodes) { //noinspection StaticPseudoFunctionalStyleMethod,ConstantConditions return plusAll(Lists.transform(nodes, (AstNode node) -> node.pos)); } + + /** Returns the 1-based line. */ + private static IntPair lineCol(String s, int offset) { + int line = 1; + int lineStart = 0; + int i; + final int n = Math.min(s.length(), offset); + for (i = 0; i < n; i++) { + if (s.charAt(i) == '\n') { + ++line; + lineStart = i + 1; + } + } + if (i == offset) { + return IntPair.of(line, offset - lineStart + 1); + } else { + throw new IllegalArgumentException("not found"); + } + } } // End Pos.java diff --git a/src/main/java/net/hydromatic/morel/ast/Shuttle.java b/src/main/java/net/hydromatic/morel/ast/Shuttle.java index dac5e06c6..62187246d 100644 --- a/src/main/java/net/hydromatic/morel/ast/Shuttle.java +++ b/src/main/java/net/hydromatic/morel/ast/Shuttle.java @@ -73,9 +73,8 @@ protected Ast.Id visit(Ast.Id id) { } protected Ast.Exp visit(Ast.AnnotatedExp annotatedExp) { - return ast.annotatedExp(annotatedExp.pos, - annotatedExp.type.accept(this), - annotatedExp.exp.accept(this)); + return ast.annotatedExp(annotatedExp.pos, annotatedExp.exp.accept(this), + annotatedExp.type.accept(this)); } protected Ast.Exp visit(Ast.If ifThenElse) { @@ -206,7 +205,9 @@ protected Ast.FunBind visit(Ast.FunBind funBind) { protected Ast.FunMatch visit(Ast.FunMatch funMatch) { return ast.funMatch(funMatch.pos, funMatch.name, - visitList(funMatch.patList), funMatch.exp.accept(this)); + visitList(funMatch.patList), + funMatch.returnType == null ? null : funMatch.returnType.accept(this), + funMatch.exp.accept(this)); } protected Ast.ValDecl visit(Ast.ValDecl valDecl) { diff --git a/src/main/java/net/hydromatic/morel/compile/BuiltIn.java b/src/main/java/net/hydromatic/morel/compile/BuiltIn.java index 9c76d7479..83afab927 100644 --- a/src/main/java/net/hydromatic/morel/compile/BuiltIn.java +++ b/src/main/java/net/hydromatic/morel/compile/BuiltIn.java @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.List; @@ -42,7 +43,6 @@ import java.util.function.Function; import java.util.function.UnaryOperator; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import static net.hydromatic.morel.type.PrimitiveType.BOOL; import static net.hydromatic.morel.type.PrimitiveType.CHAR; @@ -1059,7 +1059,8 @@ public enum BuiltIn { * *

Returns true if and only if {@code signBit r1} equals * {@code signBit r2}. */ - REAL_SAME_SIGN("Real", "sameSign", ts -> ts.fnType(ts.tupleType(REAL, REAL), BOOL)), + REAL_SAME_SIGN("Real", "sameSign", ts -> + ts.fnType(ts.tupleType(REAL, REAL), BOOL)), /** Function "Real.sign", of type "real → int". * @@ -1662,19 +1663,30 @@ public static void forEachStructure(TypeSystem typeSystem, /** Defines built-in {@code datatype} and {@code eqtype} instances, e.g. * {@code option}, {@code vector}. */ public static void dataTypes(TypeSystem typeSystem, List bindings) { - defineDataType(typeSystem, bindings, "order", 0, h -> + defineDataType(typeSystem, bindings, "order", false, 0, h -> h.tyCon("LESS").tyCon("EQUAL").tyCon("GREATER")); - defineDataType(typeSystem, bindings, "option", 1, h -> + defineDataType(typeSystem, bindings, "option", false, 1, h -> h.tyCon("NONE").tyCon("SOME", h.get(0))); defineEqType(typeSystem, "vector", 1); + + // Define two internal datatypes: + // datatype 'a list = NIL | CONS of ('a * 'a list); + // datatype bool = FALSE | TRUE; + // These are not available from within programs but are used by the + // match coverage checker. + defineDataType(typeSystem, bindings, "$list", true, 1, h -> + h.tyCon("NIL").tyCon("CONS", h.get(0))); + defineDataType(typeSystem, bindings, "$bool", true, 0, h -> + h.tyCon("FALSE").tyCon("TRUE")); } private static void defineEqType(TypeSystem ts, String name, int varCount) { - defineDataType(ts, new ArrayList<>(), name, varCount, h -> h); + defineDataType(ts, new ArrayList<>(), name, false, varCount, h -> h); } private static void defineDataType(TypeSystem ts, List bindings, - String name, int varCount, UnaryOperator transform) { + String name, boolean internal, int varCount, + UnaryOperator transform) { final List tyVars = new ArrayList<>(); for (int i = 0; i < varCount; i++) { tyVars.add(ts.typeVariable(i)); @@ -1697,8 +1709,12 @@ public TypeVar get(int i) { final Type type = ts.dataTypeScheme(name, tyVars, tyCons); final DataType dataType = (DataType) (type instanceof DataType ? type : ((ForallType) type).type); - tyCons.keySet().forEach(tyConName -> - bindings.add(ts.bindTyCon(dataType, tyConName))); + if (internal) { + ts.setInternal(name); + } else { + tyCons.keySet().forEach(tyConName -> + bindings.add(ts.bindTyCon(dataType, tyConName))); + } } /** Callback used when defining a datatype. */ diff --git a/src/main/java/net/hydromatic/morel/compile/CalciteCompiler.java b/src/main/java/net/hydromatic/morel/compile/CalciteCompiler.java index 4e5397647..d58e72ee4 100644 --- a/src/main/java/net/hydromatic/morel/compile/CalciteCompiler.java +++ b/src/main/java/net/hydromatic/morel/compile/CalciteCompiler.java @@ -22,6 +22,7 @@ import net.hydromatic.morel.ast.AstNode; import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.ast.Visitor; import net.hydromatic.morel.eval.Applicable; import net.hydromatic.morel.eval.Code; @@ -635,7 +636,7 @@ private Core.Tuple toRecord(RelContext cx, Core.Id id) { final List args = new ArrayList<>(); recordType.argNameTypes.forEach((field, fieldType) -> args.add( - core.apply(fieldType, + core.apply(Pos.ZERO, fieldType, core.recordSelector(typeSystem, recordType, field), id))); return core.tuple(recordType, args); diff --git a/src/main/java/net/hydromatic/morel/compile/CompileException.java b/src/main/java/net/hydromatic/morel/compile/CompileException.java index ef697d0b5..ae992d2be 100644 --- a/src/main/java/net/hydromatic/morel/compile/CompileException.java +++ b/src/main/java/net/hydromatic/morel/compile/CompileException.java @@ -18,10 +18,33 @@ */ package net.hydromatic.morel.compile; +import net.hydromatic.morel.ast.Pos; +import net.hydromatic.morel.util.MorelException; + /** An error occurred during compilation. */ -public class CompileException extends RuntimeException { - public CompileException(String message) { +public class CompileException extends RuntimeException + implements MorelException { + private final boolean warning; + private final Pos pos; + + public CompileException(String message, boolean warning, Pos pos) { super(message); + this.warning = warning; + this.pos = pos; + } + + @Override public String toString() { + return super.toString() + " at " + pos; + } + + @Override public Pos pos() { + return pos; + } + + public StringBuilder describeTo(StringBuilder buf) { + return pos.describeTo(buf) + .append(warning ? " Warning: " : " Error: ") + .append(getMessage()); } } diff --git a/src/main/java/net/hydromatic/morel/compile/Compiler.java b/src/main/java/net/hydromatic/morel/compile/Compiler.java index 518b2d64f..c734aa8fb 100644 --- a/src/main/java/net/hydromatic/morel/compile/Compiler.java +++ b/src/main/java/net/hydromatic/morel/compile/Compiler.java @@ -20,6 +20,7 @@ import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.eval.Applicable; import net.hydromatic.morel.eval.Applicable2; import net.hydromatic.morel.eval.Applicable3; @@ -221,7 +222,7 @@ public Code compile(Context cx, Core.Exp expression) { case FN: final Core.Fn fn = (Core.Fn) expression; return compileMatchList(cx, - ImmutableList.of(core.match(fn.idPat, fn.exp))); + ImmutableList.of(core.match(fn.idPat, fn.exp, fn.pos))); case CASE: final Core.Case case_ = (Core.Case) expression; @@ -266,11 +267,12 @@ protected Code compileApply(Context cx, Core.Apply apply) { case FN_LITERAL: final Core.Literal literal = (Core.Literal) apply.fn; final BuiltIn builtIn = (BuiltIn) literal.value; - return compileCall(cx, builtIn, apply.arg); + return compileCall(cx, builtIn, apply.arg, apply.pos); } final Code argCode = compileArg(cx, apply.arg); final Type argType = apply.arg.type; - final Applicable fnValue = compileApplicable(cx, apply.fn, argType); + final Applicable fnValue = + compileApplicable(cx, apply.fn, argType, apply.pos); if (fnValue != null) { return finishCompileApply(cx, fnValue, argCode, argType); } @@ -379,7 +381,7 @@ && getOnlyElement(bindings).id.type.equals(elementType)) { } final Applicable aggregateApplicable = compileApplicable(cx, aggregate.aggregate, - typeSystem.listType(argumentType)); + typeSystem.listType(argumentType), aggregate.pos); final Code aggregateCode; if (aggregateApplicable == null) { aggregateCode = compile(cx, aggregate.aggregate); @@ -414,16 +416,17 @@ private ImmutableList bindingNames(List bindings) { /** Compiles a function value to an {@link Applicable}, if possible, or * returns null. */ - private Applicable compileApplicable(Context cx, Core.Exp fn, Type argType) { + private Applicable compileApplicable(Context cx, Core.Exp fn, Type argType, + Pos pos) { switch (fn.op) { case FN_LITERAL: final BuiltIn builtIn = (BuiltIn) ((Core.Literal) fn).value; final Object o = Codes.BUILT_IN_VALUES.get(builtIn); - return toApplicable(cx, o, argType); + return toApplicable(cx, o, argType, pos); case VALUE_LITERAL: final Core.Literal literal = (Core.Literal) fn; - return toApplicable(cx, literal.unwrap(), argType); + return toApplicable(cx, literal.unwrap(), argType, pos); case ID: final Binding binding = cx.env.getOpt(((Core.Id) fn).idPat.name); @@ -432,7 +435,7 @@ private Applicable compileApplicable(Context cx, Core.Exp fn, Type argType) { || binding.value == Unit.INSTANCE) { return null; } - return toApplicable(cx, binding.value, argType); + return toApplicable(cx, binding.value, argType, pos); case RECORD_SELECTOR: final Core.RecordSelector recordSelector = (Core.RecordSelector) fn; @@ -444,9 +447,13 @@ private Applicable compileApplicable(Context cx, Core.Exp fn, Type argType) { } private @Nullable Applicable toApplicable(Context cx, Object o, - Type argType) { + Type argType, Pos pos) { if (o instanceof Applicable) { - return (Applicable) o; + final Applicable applicable = (Applicable) o; + if (applicable instanceof Codes.Positioned) { + return ((Codes.Positioned) applicable).withPos(pos); + } + return applicable; } if (o instanceof Macro) { final Macro value = (Macro) o; @@ -532,7 +539,7 @@ private void compileDatatypeDecl(List dataTypes, } } - private Code compileCall(Context cx, BuiltIn builtIn, Core.Exp arg) { + private Code compileCall(Context cx, BuiltIn builtIn, Core.Exp arg, Pos pos) { final List argCodes; switch (builtIn) { case Z_ANDALSO: @@ -548,7 +555,13 @@ private Code compileCall(Context cx, BuiltIn builtIn, Core.Exp arg) { argCodes = compileArgs(cx, ((Core.Tuple) arg).args); return Codes.list(argCodes); default: - final Object o = Codes.BUILT_IN_VALUES.get(builtIn); + final Object o0 = Codes.BUILT_IN_VALUES.get(builtIn); + final Object o; + if (o0 instanceof Codes.Positioned) { + o = ((Codes.Positioned) o0).withPos(pos); + } else { + o = o0; + } if (o instanceof Applicable) { final Code argCode = compile(cx, arg); if (argCode instanceof Codes.TupleCode) { @@ -587,7 +600,7 @@ private Code compileMatchList(Context cx, matchList.stream() .map(match -> compileMatch(cx, match)) .collect(toImmutableList()); - return new MatchCode(patCodes); + return new MatchCode(patCodes, Util.last(matchList).pos); } private Pair compileMatch(Context cx, Core.Match match) { @@ -603,7 +616,7 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, boolean isDecl, final List newBindings = new TailList<>(bindings); final Map linkCodes = new HashMap<>(); if (valDecl.op == Op.REC_VAL_DECL) { - valDecl.forEachBinding((pat, exp) -> { + valDecl.forEachBinding((pat, exp, pos) -> { final LinkCode linkCode = new LinkCode(); linkCodes.put(pat, linkCode); bindings.add(Binding.of(pat, linkCode)); @@ -611,8 +624,7 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, boolean isDecl, } final Context cx1 = cx.bindAll(newBindings); - valDecl.forEachBinding((pat, exp) -> { - + valDecl.forEachBinding((pat, exp, pos) -> { // Using 'compileArg' rather than 'compile' encourages CalciteCompiler // to use a pure Calcite implementation if possible, and has no effect // in the basic Compiler. @@ -620,7 +632,7 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, boolean isDecl, if (!linkCodes.isEmpty()) { link(linkCodes, pat, code); } - matchCodes.add(new MatchCode(ImmutableList.of(Pair.of(pat, code)))); + matchCodes.add(new MatchCode(ImmutableList.of(Pair.of(pat, code)), pos)); if (actions != null) { final Type type0 = exp.type; @@ -633,7 +645,7 @@ private void compileValDecl(Context cx, Core.ValDecl valDecl, boolean isDecl, final Object o = code.eval(evalEnv); final Map pairs = new LinkedHashMap<>(); if (!Closure.bindRecurse(pat.withType(type), o, pairs::put)) { - throw new Codes.MorelRuntimeException(Codes.BuiltInExn.BIND); + throw new Codes.MorelRuntimeException(Codes.BuiltInExn.BIND, pos); } pairs.forEach((pat2, o2) -> { outBindings.accept(Binding.of(pat2, o2)); @@ -748,9 +760,11 @@ public Object eval(EvalEnv env) { /** Code that implements {@link Compiler#compileMatchList(Context, List)}. */ private static class MatchCode implements Code { private final ImmutableList> patCodes; + private final Pos pos; - MatchCode(ImmutableList> patCodes) { + MatchCode(ImmutableList> patCodes, Pos pos) { this.patCodes = patCodes; + this.pos = pos; } @Override public Describer describe(Describer describer) { @@ -760,7 +774,7 @@ private static class MatchCode implements Code { } @Override public Object eval(EvalEnv evalEnv) { - return new Closure(evalEnv, patCodes); + return new Closure(evalEnv, patCodes, pos); } } } diff --git a/src/main/java/net/hydromatic/morel/compile/Compiles.java b/src/main/java/net/hydromatic/morel/compile/Compiles.java index 23e6e7844..82ed2d385 100644 --- a/src/main/java/net/hydromatic/morel/compile/Compiles.java +++ b/src/main/java/net/hydromatic/morel/compile/Compiles.java @@ -34,8 +34,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import javax.annotation.Nullable; import static net.hydromatic.morel.ast.AstBuilder.ast; @@ -59,7 +61,7 @@ public static TypeResolver.Resolved validateExpression(AstNode statement, */ public static CompiledStatement prepareStatement(TypeSystem typeSystem, Session session, Environment env, AstNode statement, - @Nullable Calcite calcite) { + @Nullable Calcite calcite, Consumer warningConsumer) { Ast.Decl decl; if (statement instanceof Ast.Exp) { decl = toValDecl((Ast.Exp) statement); @@ -67,7 +69,7 @@ public static CompiledStatement prepareStatement(TypeSystem typeSystem, decl = (Ast.Decl) statement; } return prepareDecl(typeSystem, session, env, calcite, decl, - decl == statement); + decl == statement, warningConsumer); } /** @@ -76,7 +78,8 @@ public static CompiledStatement prepareStatement(TypeSystem typeSystem, */ private static CompiledStatement prepareDecl(TypeSystem typeSystem, Session session, Environment env, @Nullable Calcite calcite, - Ast.Decl decl, boolean isDecl) { + Ast.Decl decl, boolean isDecl, + Consumer warningConsumer) { final TypeResolver.Resolved resolved = TypeResolver.deduceType(env, decl, typeSystem); final boolean hybrid = Prop.HYBRID.booleanValue(session.map); @@ -85,6 +88,14 @@ private static CompiledStatement prepareDecl(TypeSystem typeSystem, final Resolver resolver = Resolver.of(resolved.typeMap, env); final Core.Decl coreDecl0 = resolver.toCore(resolved.node); + // Check for exhaustive and redundant patterns, and throw errors or + // warnings. + final boolean matchCoverageEnabled = + Prop.MATCH_COVERAGE_ENABLED.booleanValue(session.map); + if (matchCoverageEnabled) { + checkPatternCoverage(typeSystem, coreDecl0, warningConsumer); + } + Core.Decl coreDecl; if (inlinePassCount == 0) { // Inlining is disabled. Use the Inliner in a limited mode. @@ -116,6 +127,50 @@ private static CompiledStatement prepareDecl(TypeSystem typeSystem, return compiler.compileStatement(env, coreDecl, isDecl); } + /** Checks for exhaustive and redundant patterns, and throws if there are + * errors/warnings. */ + private static void checkPatternCoverage(TypeSystem typeSystem, + Core.Decl decl, final Consumer warningConsumer) { + final List errorList = new ArrayList<>(); + decl.accept(new Visitor() { + @Override protected void visit(Core.Case kase) { + super.visit(kase); + checkPatternCoverage(typeSystem, kase, errorList::add, + warningConsumer); + } + }); + if (!errorList.isEmpty()) { + throw errorList.get(0); + } + } + + private static void checkPatternCoverage(TypeSystem typeSystem, + Core.Case kase, Consumer errorConsumer, + Consumer warningConsumer) { + final List prevPatList = new ArrayList<>(); + final List redundantMatchList = new ArrayList<>(); + for (Core.Match match : kase.matchList) { + if (PatternCoverageChecker.isCoveredBy(typeSystem, prevPatList, + match.pat)) { + redundantMatchList.add(match); + } + prevPatList.add(match.pat); + } + final boolean exhaustive = + PatternCoverageChecker.isExhaustive(typeSystem, prevPatList); + if (!redundantMatchList.isEmpty()) { + final String message = exhaustive + ? "match redundant" + : "match redundant and nonexhaustive"; + errorConsumer.accept( + new CompileException(message, false, + redundantMatchList.get(0).pos)); + } else if (!exhaustive) { + warningConsumer.accept( + new CompileException("match nonexhaustive", true, kase.pos)); + } + } + /** Converts {@code e} to {@code val = e}. */ public static Ast.ValDecl toValDecl(Ast.Exp statement) { final Pos pos = statement.pos; @@ -150,9 +205,9 @@ static void bindPattern(TypeSystem typeSystem, List bindings, * we have the expression. */ static void bindPattern(TypeSystem typeSystem, List bindings, Core.ValDecl valDecl) { - valDecl.forEachBinding((pat, exp) -> { + valDecl.forEachBinding((pat, exp, pos) -> { if (pat instanceof Core.IdPat) { - bindings.add(Binding.of((Core.IdPat) pat, exp)); + bindings.add(Binding.of(pat, exp)); } }); } diff --git a/src/main/java/net/hydromatic/morel/compile/EnvShuttle.java b/src/main/java/net/hydromatic/morel/compile/EnvShuttle.java index 919e40970..8b1baee20 100644 --- a/src/main/java/net/hydromatic/morel/compile/EnvShuttle.java +++ b/src/main/java/net/hydromatic/morel/compile/EnvShuttle.java @@ -56,7 +56,7 @@ protected EnvShuttle(TypeSystem typeSystem, Environment env) { final List bindings = new ArrayList<>(); final Core.Pat pat2 = match.pat.accept(this); Compiles.bindPattern(typeSystem, bindings, pat2); - return core.match(pat2, match.exp.accept(bind(bindings))); + return core.match(pat2, match.exp.accept(bind(bindings)), match.pos); } @Override public Core.Exp visit(Core.Let let) { diff --git a/src/main/java/net/hydromatic/morel/compile/Inliner.java b/src/main/java/net/hydromatic/morel/compile/Inliner.java index 8aa72f175..d29e06a3f 100644 --- a/src/main/java/net/hydromatic/morel/compile/Inliner.java +++ b/src/main/java/net/hydromatic/morel/compile/Inliner.java @@ -144,7 +144,7 @@ public static Inliner of(TypeSystem typeSystem, Environment env, // let x = A in E end final Core.Fn fn = (Core.Fn) apply2.fn; return core.let( - core.nonRecValDecl(fn.idPat, apply2.arg), fn.exp); + core.nonRecValDecl(fn.idPat, apply2.arg, apply2.pos), fn.exp); } return apply2; } diff --git a/src/main/java/net/hydromatic/morel/compile/PatternCoverageChecker.java b/src/main/java/net/hydromatic/morel/compile/PatternCoverageChecker.java new file mode 100644 index 000000000..ca008ddff --- /dev/null +++ b/src/main/java/net/hydromatic/morel/compile/PatternCoverageChecker.java @@ -0,0 +1,352 @@ +/* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package net.hydromatic.morel.compile; + +import net.hydromatic.morel.ast.Core; +import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.type.DataType; +import net.hydromatic.morel.type.ForallType; +import net.hydromatic.morel.type.Type; +import net.hydromatic.morel.type.TypeSystem; +import net.hydromatic.morel.util.Ord; +import net.hydromatic.morel.util.Pair; +import net.hydromatic.morel.util.Sat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import org.apache.calcite.util.Util; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static net.hydromatic.morel.ast.CoreBuilder.core; + +import static java.util.Objects.requireNonNull; + +/** Checks whether patterns are exhaustive and/or redundant. + * + *

The algorithm converts a list of patterns into a boolean formula + * with several variables, then checks whether the formula is satisfiable + * (that is, whether there is a combination of assignments of boolean values + * to the variables such that the formula evaluates to true). */ +class PatternCoverageChecker { + final TypeSystem typeSystem; + final Sat sat = new Sat(); + final Map pathSlots = new HashMap<>(); + + /** Creates a PatternCoverageChecker. */ + private PatternCoverageChecker(TypeSystem typeSystem) { + this.typeSystem = requireNonNull(typeSystem, "typeSystem"); + } + + /** Returns whether every possible value that could be matched by + * pattern {@code pat} would already have been matched by one or more of + * {@code prevPatList}. + * + *

For example, the pattern "(1, b: bool)" is covered by "[(1, true), + * (_, false)]" but not by "[(1, true)]" or "[(_, false)]". */ + static boolean isCoveredBy(TypeSystem typeSystem, List prevPatList, + Core.Pat pat) { + if (prevPatList.isEmpty()) { + return false; // shortcut + } + // p isCoveredBy [p0 ... pN ] + // iff + // (f ^ ~f0 ^ ... ^ ~fN) is not satisfiable + // where f is the formula for p + // and f0 is the formula for p0, etc. + return new PatternCoverageChecker(typeSystem).isCoveredBy(pat, prevPatList); + } + + /** Returns whether a list of patterns covers every possible value. + * If so, any pattern added to this list would be redundant. */ + @SuppressWarnings("StaticPseudoFunctionalStyleMethod") + static boolean isExhaustive(TypeSystem typeSystem, List patList) { + if (patList.isEmpty()) { + return false; // shortcut + } + if (Iterables.any(patList, p -> + p.op == Op.WILDCARD_PAT || p.op == Op.ID_PAT)) { + return true; // shortcut + } + final Core.WildcardPat wildcardPat = + core.wildcardPat(patList.get(0).type); + return isCoveredBy(typeSystem, patList, wildcardPat); + } + + /** Converts a pattern to a logical term. */ + private Sat.Term toTerm(Core.Pat pat) { + final List terms = new ArrayList<>(); + toTerm(pat, Path.ROOT, terms); + return terms.size() == 1 ? terms.get(0) : sat.and(terms); + } + + private void toTerm(Core.Pat pat, Path path, List terms) { + switch (pat.op) { + case WILDCARD_PAT: + case ID_PAT: + return; // no constraints to add + + case AS_PAT: + toTerm(((Core.AsPat) pat).pat, path, terms); + return; + + case BOOL_LITERAL_PAT: + // Transform false to FALSE and true to TRUE, constructor of the + // internal $bool datatype: + // datatype $bool = FALSE | TRUE + // Knowing there are only two values allows us to + final DataType boolDataType = + (DataType) typeSystem.lookupInternal("$bool"); + final Core.LiteralPat literalPat0 = (Core.LiteralPat) pat; + final Boolean value = (Boolean) literalPat0.value; + toTerm(core.con0Pat(boolDataType, value ? "TRUE" : "FALSE"), path, terms); + return; + + case CHAR_LITERAL_PAT: + case INT_LITERAL_PAT: + case REAL_LITERAL_PAT: + case STRING_LITERAL_PAT: + final Core.LiteralPat literalPat = (Core.LiteralPat) pat; + terms.add(sat.variable(path.toVar(literalPat.value.toString()))); + return; + + case CON0_PAT: + final Core.Con0Pat con0Pat = (Core.Con0Pat) pat; + terms.add(typeConstructorTerm(path, con0Pat.tyCon)); + return; + + case CON_PAT: + final Core.ConPat conPat = (Core.ConPat) pat; + terms.add(typeConstructorTerm(path, conPat.tyCon)); + toTerm(conPat.pat, path, terms); + return; + + case CONS_PAT: + final Core.ConPat consPat = (Core.ConPat) pat; + addConsTerms(path, terms, (Core.TuplePat) consPat.pat); + return; + + case TUPLE_PAT: + final Core.TuplePat tuplePat = (Core.TuplePat) pat; + Ord.forEach(tuplePat.args, (pat2, i) -> + toTerm(pat2, path.sub(i), terms)); + return; + + case RECORD_PAT: + final Core.RecordPat recordPat = (Core.RecordPat) pat; + Ord.forEach(recordPat.args, (pat2, i) -> + toTerm(pat2, path.sub(i), terms)); + return; + + case LIST_PAT: + // For list + // [a, b, c] + // built terms as if they had written + // CONS (a, CONS (b, CONS (c, NIL)) + // namely + // var(tag.0=CONS) + // ^ var(tag.0.1=CONS) + // ^ var(tag.0.1.1=CONS) + // ^ var(tag.0.1.1.1=NIL + toTerm(listToCons((Core.ListPat) pat), path, terms); + return; + + default: + throw new AssertionError(pat.op); + } + } + + /** Converts a list pattern into a pattern made up of the {@code CONS} and + * {@code NIL} constructors of the built-in {@code datatype list}. + * + *

For example, converts: + * "[]" to "NIL", + * "[x]" to "CONS (x, NIL)", + * "[x, y]" to "CONS (x, CONS (y, NIL))", + * etc. */ + private Core.Pat listToCons(Core.ListPat listPat) { + final Type listType = typeSystem.lookupInternal("$list"); + final DataType listDataType = (DataType) ((ForallType) listType).type; + return listToConsRecurse(listDataType, listPat.args); + } + + private Core.Pat listToConsRecurse(DataType listDataType, + List args) { + if (args.isEmpty()) { + return core.con0Pat(listDataType, "NIL"); + } else { + return core.consPat(listDataType, "CONS", + core.tuplePat(typeSystem, + ImmutableList.of(args.get(0), + listToConsRecurse(listDataType, Util.skip(args))))); + } + } + + private void addConsTerms(Path path, List terms, + Core.TuplePat tuplePat) { + terms.add(typeConstructorTerm(path, "CONS")); + toTerm(tuplePat, path, terms); + } + + private Sat.Variable typeConstructorTerm(Path path, String con) { + final Pair pair = typeSystem.lookupTyCon(con); + final DataType dataType = pair.left; + DataTypeSlot slot = + pathSlots.computeIfAbsent(path, + p -> new DataTypeSlot(dataType, p, sat)); + return slot.constructorMap.get(con); + } + + /** Returns whether a pattern is covered by a list of patterns. + * + *

A pattern {@code pat} is said to be covered by a list of + * patterns {@code patList} if any possible value would be caught by one of + * the patterns in {@code patList} before reaching {@code pat}. Thus + * {@code pat} is said to be redundant in that context, and could + * be removed without affecting behavior. */ + public boolean isCoveredBy(Core.Pat pat, List patList) { + final List terms = new ArrayList<>(); + patList.forEach(p -> terms.add(toTerm(p))); + final Sat.Term term = toTerm(pat); + + final List terms1 = new ArrayList<>(); + terms1.add(term); + terms.forEach(t -> terms1.add(sat.not(t))); + + // Add constraints for tags, which are mutually exclusive. + // For example, for a type with constructors A, B, C + // (tag=A or tag=B or tag=C) + // because at least one tag must be present, and + // (not (tag=A or tag=B) + // or not (tag=B or tag=C) + // or not (tag=C or tag=A)) + // because at most one tag must be present. + pathSlots.values().forEach(slot -> { + final List terms2 = + new ArrayList<>(slot.constructorMap.values()); + terms1.add(sat.or(terms2)); + + final List terms3 = new ArrayList<>(); + for (int i = 0; i < terms2.size(); i++) { + terms3.add(sat.not(sat.or(new ElideList<>(terms2, i)))); + } + terms1.add(sat.or(terms3)); + }); + final Sat.Term formula = sat.and(terms1); + final Map solve = sat.solve(formula); + return solve == null; + } + + /** List that removes one particular element from a backing list. + * + * @param element type */ + private static class ElideList extends AbstractList { + private final List list; + private final int elide; + + ElideList(List list, int elide) { + this.list = requireNonNull(list, "list"); + this.elide = elide; + } + + @Override public E get(int index) { + return list.get(index < elide ? index : index + 1); + } + + @Override public int size() { + return list.size() - 1; + } + } + + /** Identifies a point in a nested pattern. + * + *

Paths are basically immutable lists of integers, built by appending + * one element at a time. */ + private abstract static class Path { + /** Root path. */ + static final Path ROOT = new Path() { + @Override protected void path(StringBuilder b) { + } + }; + + @Override public String toString() { + return toVar(""); + } + + /** Creates a sub-path. */ + Path sub(int i) { + return new SubPath(this, i); + } + + /** Converts this to a variable. + * + *

{@code ROOT.sub(2).sub(1).toVar("x")} + * will return "2.1.x". */ + String toVar(String name) { + final StringBuilder builder = new StringBuilder(); + path(builder); + builder.append(name); + return builder.toString(); + } + + protected abstract void path(StringBuilder b); + } + + /** Path that is a child of a given parent path. + * The {@code ordinal} makes it unique within its parent. + * For tuple and record patterns, {@code ordinal} is the field ordinal. */ + private static class SubPath extends Path { + final Path parent; + final int ordinal; + + SubPath(Path parent, int ordinal) { + this.parent = parent; + this.ordinal = ordinal; + } + + @Override protected void path(StringBuilder b) { + parent.path(b); + b.append(ordinal).append('.'); + } + } + + /** Payload of a {@code Sat.Variable} that is an algebraic type. + * There are sub-variables representing whether the tag holds + * each of its allowed values (each of which is a constructor). */ + private static class DataTypeSlot { + final DataType dataType; + final ImmutableMap constructorMap; + + DataTypeSlot(DataType dataType, Path path, Sat sat) { + this.dataType = dataType; + final ImmutableMap.Builder b = + ImmutableMap.builder(); + dataType.typeConstructors.forEach((name, type) -> + b.put(name, sat.variable(path.toVar(name)))); + this.constructorMap = b.build(); + } + } +} + +// End PatternCoverageChecker.java diff --git a/src/main/java/net/hydromatic/morel/compile/Relationalizer.java b/src/main/java/net/hydromatic/morel/compile/Relationalizer.java index 798b9744c..72006d1ac 100644 --- a/src/main/java/net/hydromatic/morel/compile/Relationalizer.java +++ b/src/main/java/net/hydromatic/morel/compile/Relationalizer.java @@ -87,7 +87,7 @@ public static Relationalizer of(TypeSystem typeSystem, Environment env) { // "defaultYieldExp", and therefore we cannot add another yield // step. We will have to inline the yield expression as a let. final Core.Yield yieldStep = core.yield_(typeSystem, - core.apply(fnType.resultType, f, + core.apply(apply.pos, fnType.resultType, f, core.implicitYieldExp(typeSystem, from.steps))); return core.from(typeSystem, append(from.steps, yieldStep)); } @@ -100,7 +100,7 @@ public static Relationalizer of(TypeSystem typeSystem, Environment env) { final Core.From from = toFrom(apply.arg); final Core.Where whereStep = core.where(core.lastBindings(from.steps), - core.apply(fnType.resultType, f, + core.apply(apply.pos, fnType.resultType, f, core.implicitYieldExp(typeSystem, from.steps))); return core.from(typeSystem, append(from.steps, whereStep)); } diff --git a/src/main/java/net/hydromatic/morel/compile/Resolver.java b/src/main/java/net/hydromatic/morel/compile/Resolver.java index ab6160d49..e0c162303 100644 --- a/src/main/java/net/hydromatic/morel/compile/Resolver.java +++ b/src/main/java/net/hydromatic/morel/compile/Resolver.java @@ -21,6 +21,7 @@ import net.hydromatic.morel.ast.Ast; import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.ast.Visitor; import net.hydromatic.morel.type.Binding; import net.hydromatic.morel.type.DataType; @@ -44,6 +45,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -141,7 +143,8 @@ public Core.ValDecl toCore(Ast.ValDecl valDecl) { final List bindings = new ArrayList<>(); // discard final ResolvedValDecl resolvedValDecl = resolveValDecl(valDecl, bindings); Core.NonRecValDecl nonRecValDecl = - core.nonRecValDecl(resolvedValDecl.pat, resolvedValDecl.exp); + core.nonRecValDecl(resolvedValDecl.pat, resolvedValDecl.exp, + resolvedValDecl.patExps.get(0).pos); return resolvedValDecl.rec ? core.recValDecl(ImmutableList.of(nonRecValDecl)) : nonRecValDecl; @@ -181,19 +184,23 @@ private ResolvedValDecl resolveValDecl(Ast.ValDecl valDecl, valDecl.valBinds.forEach(valBind -> flatten(matches, composite, valBind.pat, valBind.exp)); - final List pats = new ArrayList<>(); - final List exps = new ArrayList<>(); + final List patExps = new ArrayList<>(); if (valDecl.rec) { + final List pats = new ArrayList<>(); matches.forEach((pat, exp) -> pats.add(toCore(pat))); pats.forEach(p -> Compiles.acceptBinding(typeMap.typeSystem, p, bindings)); final Resolver r = withEnv(bindings); - matches.forEach((pat, exp) -> exps.add(r.toCore(exp))); + final Iterator patIter = pats.iterator(); + matches.forEach((pat, exp) -> + patExps.add( + new PatExp(patIter.next(), r.toCore(exp), + pat.pos.plus(exp.pos)))); } else { - matches.forEach((pat, exp) -> { - pats.add(toCore(pat)); - exps.add(toCore(exp)); - }); - pats.forEach(p -> Compiles.acceptBinding(typeMap.typeSystem, p, bindings)); + matches.forEach((pat, exp) -> + patExps.add( + new PatExp(toCore(pat), toCore(exp), pat.pos.plus(exp.pos)))); + patExps.forEach(x -> + Compiles.acceptBinding(typeMap.typeSystem, x.pat, bindings)); } // Convert recursive to non-recursive if the bound variable is not @@ -203,17 +210,20 @@ private ResolvedValDecl resolveValDecl(Ast.ValDecl valDecl, // val inc = fn i => i + 1 // because "i + 1" does not reference "inc". boolean rec = valDecl.rec - && references(exps, pats); + && references(patExps); // Transform "let val v1 = E1 and v2 = E2 in E end" // to "let val v = (v1, v2) in case v of (E1, E2) => E end" final Core.Pat pat0; final Core.Exp exp; if (composite) { + final List pats = Util.transform(patExps, x -> x.pat); + final List exps = Util.transform(patExps, x -> x.exp); pat0 = core.tuplePat(typeMap.typeSystem, pats); exp = core.tuple((RecordLikeType) pat0.type, exps); } else { - pat0 = pats.get(0); - exp = exps.get(0); + final PatExp patExp = patExps.get(0); + pat0 = patExp.pat; + exp = patExp.exp; } final Core.NamedPat pat; if (pat0 instanceof Core.NamedPat) { @@ -222,8 +232,7 @@ private ResolvedValDecl resolveValDecl(Ast.ValDecl valDecl, pat = core.asPat(exp.type, "it", nameGenerator, pat0); } - return new ResolvedValDecl(rec, ImmutableList.copyOf(pats), - ImmutableList.copyOf(exps), pat, exp); + return new ResolvedValDecl(rec, ImmutableList.copyOf(patExps), pat, exp); } /** Returns whether any of the expressions in {@code exps} references @@ -231,11 +240,11 @@ private ResolvedValDecl resolveValDecl(Ast.ValDecl valDecl, * *

This method is used to decide whether it is safe to convert a recursive * declaration into a non-recursive one. */ - private boolean references(List exps, List pats) { + private boolean references(List patExps) { final Set refSet = new HashSet<>(); final ReferenceFinder finder = new ReferenceFinder(typeMap.typeSystem, Environments.empty(), refSet); - exps.forEach(e -> e.accept(finder)); + patExps.forEach(x -> x.exp.accept(finder)); final Set defSet = new HashSet<>(); final Visitor v = new Visitor() { @@ -243,7 +252,7 @@ private boolean references(List exps, List pats) { defSet.add(idPat); } }; - pats.forEach(p -> p.accept(v)); + patExps.forEach(x -> x.pat.accept(v)); return Util.intersects(refSet, defSet); } @@ -303,6 +312,8 @@ private Core.Exp toCore(Ast.Exp exp) { return core.stringLiteral((String) ((Ast.Literal) exp).value); case UNIT_LITERAL: return core.unitLiteral(); + case ANNOTATED_EXP: + return toCore(((Ast.AnnotatedExp) exp).exp); case ID: return toCore((Ast.Id) exp); case ANDALSO: @@ -374,7 +385,7 @@ private Core.Tuple toCore(Ast.Record record) { private Core.Exp toCore(Ast.ListExp list) { final ListType type = (ListType) typeMap.getType(list); - return core.apply(type, + return core.apply(list.pos, type, core.functionLiteral(typeMap.typeSystem, BuiltIn.Z_LIST), core.tuple(typeMap.typeSystem, null, transform(list.args, this::toCore))); @@ -385,7 +396,7 @@ private Core.Exp toCore(Ast.ListExp list) { private Core.Exp toCoreFromEq(Ast.Exp exp) { final Type type = typeMap.getType(exp); final ListType listType = typeMap.typeSystem.listType(type); - return core.apply(listType, + return core.apply(exp.pos, listType, core.functionLiteral(typeMap.typeSystem, BuiltIn.Z_LIST), core.tuple(typeMap.typeSystem, null, ImmutableList.of(toCore(exp)))); } @@ -401,7 +412,7 @@ private Core.Apply toCore(Ast.Apply apply) { } else { coreFn = toCore(apply.fn); } - return core.apply(type, coreFn, coreArg); + return core.apply(apply.pos, type, coreFn, coreArg); } private Core.RecordSelector toCore(Ast.RecordSelector recordSelector) { @@ -414,7 +425,7 @@ private Core.Apply toCore(Ast.InfixCall call) { Core.Exp core0 = toCore(call.a0); Core.Exp core1 = toCore(call.a1); final BuiltIn builtIn = toBuiltIn(call.op); - return core.apply(typeMap.getType(call), + return core.apply(call.pos, typeMap.getType(call), core.functionLiteral(typeMap.typeSystem, builtIn), core.tuple(typeMap.typeSystem, null, ImmutableList.of(core0, core1))); } @@ -445,7 +456,8 @@ private Core.Fn toCore(Ast.Fn fn) { // needs intermediate variable and case, "fn v => case v of (x, y) => exp" final Core.IdPat idPat = core.idPat(type.paramType, nameGenerator); final Core.Id id = core.id(idPat); - return core.fn(type, idPat, core.caseOf(type.resultType, id, matchList)); + return core.fn(type, idPat, + core.caseOf(type.resultType, id, matchList, fn.pos)); } private Core.Case toCore(Ast.If if_) { @@ -455,7 +467,7 @@ private Core.Case toCore(Ast.If if_) { private Core.Case toCore(Ast.Case case_) { return core.caseOf(typeMap.getType(case_), toCore(case_.exp), - transform(case_.matchList, this::toCore)); + transform(case_.matchList, this::toCore), case_.pos); } private Core.Exp toCore(Ast.Let let) { @@ -525,6 +537,11 @@ private Core.Pat toCore(Ast.Pat pat, Type type, Type targetType) { final Ast.AsPat asPat = (Ast.AsPat) pat; return core.asPat(type, asPat.id.name, nameGenerator, toCore(asPat.pat)); + case ANNOTATED_PAT: + // There is no annotated pat in core, because all patterns have types. + final Ast.AnnotatedPat annotatedPat = (Ast.AnnotatedPat) pat; + return toCore(annotatedPat.pat); + case CON_PAT: final Ast.ConPat conPat = (Ast.ConPat) pat; return core.conPat(type, conPat.tyCon.name, toCore(conPat.pat)); @@ -574,7 +591,7 @@ private Core.Match toCore(Ast.Match match) { final List bindings = new ArrayList<>(); Compiles.acceptBinding(typeMap.typeSystem, pat, bindings); final Core.Exp exp = withEnv(bindings).toCore(match.exp); - return core.match(pat, exp); + return core.match(pat, exp, match.pos); } Core.Exp toCore(Ast.From from) { @@ -585,7 +602,7 @@ Core.Exp toCore(Ast.From from) { final Core.From coreFrom = fromStepToCore(bindings, listType, from.steps, ImmutableList.of()); - return core.apply(type, + return core.apply(from.pos, type, core.functionLiteral(typeMap.typeSystem, BuiltIn.RELATIONAL_ONLY), coreFrom); } else { @@ -731,20 +748,16 @@ public abstract static class ResolvedDecl { class ResolvedValDecl extends ResolvedDecl { final boolean rec; final boolean composite; - final ImmutableList pats; - final ImmutableList exps; + final ImmutableList patExps; final Core.NamedPat pat; final Core.Exp exp; ResolvedValDecl(boolean rec, - ImmutableList pats, - ImmutableList exps, + ImmutableList patExps, Core.NamedPat pat, Core.Exp exp) { this.rec = rec; - this.composite = pats.size() > 1; - this.pats = pats; - this.exps = exps; - assert pats.size() == exps.size(); + this.composite = patExps.size() > 1; + this.patExps = patExps; this.pat = pat; this.exp = exp; } @@ -752,12 +765,14 @@ class ResolvedValDecl extends ResolvedDecl { @Override Core.Let toExp(Core.Exp resultExp) { if (rec) { final List valDecls = new ArrayList<>(); - Pair.forEach(pats, exps, (pat, exp) -> - valDecls.add(core.nonRecValDecl((Core.IdPat) pat, exp))); + patExps.forEach(x -> + valDecls.add(core.nonRecValDecl((Core.IdPat) x.pat, x.exp, x.pos))); return core.let(core.recValDecl(valDecls), resultExp); } - if (!composite && pats.get(0) instanceof Core.IdPat) { - Core.NonRecValDecl valDecl = core.nonRecValDecl((Core.IdPat) pats.get(0), exps.get(0)); + if (!composite && patExps.get(0).pat instanceof Core.IdPat) { + final PatExp x = patExps.get(0); + Core.NonRecValDecl valDecl = + core.nonRecValDecl((Core.IdPat) x.pat, x.exp, x.pos); return rec ? core.let(core.recValDecl(ImmutableList.of(valDecl)), resultExp) : core.let(valDecl, resultExp); @@ -769,13 +784,31 @@ class ResolvedValDecl extends ResolvedDecl { final String name = nameGenerator.get(); final Core.IdPat idPat = core.idPat(pat.type, name, nameGenerator); final Core.Id id = core.id(idPat); - return core.let(core.nonRecValDecl(idPat, exp), + final Pos pos = patExps.get(0).pos; + return core.let(core.nonRecValDecl(idPat, exp, pos), core.caseOf(resultExp.type, id, - ImmutableList.of(core.match(pat, resultExp)))); + ImmutableList.of(core.match(pat, resultExp, pos)), pos)); } } } + /** Pattern and expression. */ + static class PatExp { + final Core.Pat pat; + final Core.Exp exp; + final Pos pos; + + PatExp(Core.Pat pat, Core.Exp exp, Pos pos) { + this.pat = pat; + this.exp = exp; + this.pos = pos; + } + + @Override public String toString() { + return "[pat: " + pat + ", exp: " + exp + ", pos: " + pos + "]"; + } + } + /** Resolved datatype declaration. */ static class ResolvedDatatypeDecl extends ResolvedDecl { private final ImmutableList dataTypes; diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index 73730204c..14c2e62d7 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -48,6 +48,7 @@ import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.apache.calcite.util.Holder; @@ -71,6 +72,7 @@ import java.util.stream.Collectors; import static net.hydromatic.morel.ast.AstBuilder.ast; +import static net.hydromatic.morel.type.RecordType.ORDERING; import static net.hydromatic.morel.util.Static.skip; import static net.hydromatic.morel.util.Static.toImmutableList; @@ -194,6 +196,12 @@ private Ast.Exp deduceType(TypeEnv env, Ast.Exp node, Unifier.Variable v) { case UNIT_LITERAL: return reg(node, v, toTerm(PrimitiveType.UNIT)); + case ANNOTATED_EXP: + final Ast.AnnotatedExp annotatedExp = (Ast.AnnotatedExp) node; + final Type type = toType(annotatedExp.type, typeSystem); + deduceType(env, annotatedExp.exp, v); + return reg(node, v, toTerm(type, Subst.EMPTY)); + case ANDALSO: case ORELSE: return infix(env, (Ast.InfixCall) node, v, PrimitiveType.BOOL); @@ -311,7 +319,9 @@ private Ast.Exp deduceType(TypeEnv env, Ast.Exp node, Unifier.Variable v) { case ID: final Ast.Id id = (Ast.Id) node; - final Unifier.Term term = env.get(typeSystem, id.name); + final Unifier.Term term = env.get(typeSystem, id.name, name -> + new CompileException("unbound variable or constructor: " + name, + false, id.pos)); return reg(id, v, term); case FN: @@ -744,8 +754,9 @@ private Ast.Decl deduceValDeclType(TypeEnv env, Ast.ValDecl valDecl, final Holder envHolder = Holder.of(env); final Map> map0 = new LinkedHashMap<>(); + //noinspection FunctionalExpressionCanBeFolded valDecl.valBinds.forEach(b -> - map0.put(b, Suppliers.memoize(unifier::variable))); + map0.put(b, Suppliers.memoize(unifier::variable)::get)); map0.forEach((valBind, vPatSupplier) -> { // If recursive, bind each value (presumably a function) to its type // in the environment before we try to deduce the type of the expression. @@ -792,14 +803,31 @@ private Type toType(Ast.Type type) { final Ast.TupleType tupleType = (Ast.TupleType) type; return typeSystem.tupleType(toTypes(tupleType.types)); + case RECORD_TYPE: + final Ast.RecordType recordType = (Ast.RecordType) type; + final ImmutableSortedMap.Builder argNameTypes = + ImmutableSortedMap.orderedBy(ORDERING); + recordType.fieldTypes.forEach((name, t) -> + argNameTypes.put(name, toType(t))); + return typeSystem.recordType(argNameTypes.build()); + + case FUNCTION_TYPE: + final Ast.FunctionType functionType = (Ast.FunctionType) type; + final Type paramType = toType(functionType.paramType, typeSystem); + final Type resultType = toType(functionType.resultType, typeSystem); + return typeSystem.fnType(paramType, resultType); + case NAMED_TYPE: final Ast.NamedType namedType = (Ast.NamedType) type; + final List typeList = toTypes(namedType.types); + if (namedType.name.equals(LIST_TY_CON) && typeList.size() == 1) { + // TODO: make 'list' a regular generic type + return typeSystem.listType(typeList.get(0)); + } final Type genericType = typeSystem.lookup(namedType.name); if (namedType.types.isEmpty()) { return genericType; } - final List typeList = namedType.types.stream().map(this::toType) - .collect(toImmutableList()); return typeSystem.apply(genericType, typeList); case TY_VAR: @@ -808,7 +836,7 @@ private Type toType(Ast.Type type) { name -> typeSystem.typeVariable(tyVarMap.size())); default: - throw new AssertionError("cannot convert type " + type); + throw new AssertionError("cannot convert type " + type + " " + type.op); } } @@ -847,22 +875,39 @@ private Ast.ValDecl toValDecl(TypeEnv env, Ast.FunDecl funDecl) { private Ast.ValBind toValBind(TypeEnv env, Ast.FunBind funBind) { final List vars; Ast.Exp exp; + Ast.Type returnType = null; if (funBind.matchList.size() == 1) { - exp = funBind.matchList.get(0).exp; - vars = funBind.matchList.get(0).patList; + final Ast.FunMatch funMatch = funBind.matchList.get(0); + exp = funMatch.exp; + vars = funMatch.patList; + returnType = funMatch.returnType; } else { final List varNames = MapList.of(funBind.matchList.get(0).patList.size(), index -> "v" + index); vars = Lists.transform(varNames, v -> ast.idPat(Pos.ZERO, v)); final List matchList = new ArrayList<>(); + Pos prevReturnTypePos = null; for (Ast.FunMatch funMatch : funBind.matchList) { matchList.add( ast.match(funMatch.pos, patTuple(env, funMatch.patList), funMatch.exp)); + if (funMatch.returnType != null) { + if (returnType != null + && !returnType.equals(funMatch.returnType)) { + throw new CompileException("parameter or result constraints of " + + "clauses don't agree [tycon mismatch]", false, + prevReturnTypePos.plus(funMatch.pos)); + } + returnType = funMatch.returnType; + prevReturnTypePos = funMatch.pos; + } } exp = ast.caseOf(Pos.ZERO, idTuple(varNames), matchList); } + if (returnType != null) { + exp = ast.annotatedExp(exp.pos, exp, returnType); + } final Pos pos = funBind.pos; for (Ast.Pat var : Lists.reverse(vars)) { exp = ast.fn(pos, ast.match(pos, var, exp)); @@ -894,7 +939,8 @@ private Ast.Pat patTuple(TypeEnv env, List patList) { case ID_PAT: final Ast.IdPat idPat = (Ast.IdPat) pat; if (env.has(idPat.name)) { - final Unifier.Term term = env.get(typeSystem, idPat.name); + final Unifier.Term term = env.get(typeSystem, idPat.name, name -> + new RuntimeException("oops, should have " + idPat.name)); if (term instanceof Unifier.Sequence && ((Unifier.Sequence) term).operator.equals(FN_TY_CON)) { list2.add( @@ -959,6 +1005,12 @@ private Ast.Pat deducePatType(TypeEnv env, Ast.Pat pat, deducePatType(env, asPat.pat, termMap, null, v); return reg(pat, null, v); + case ANNOTATED_PAT: + final Ast.AnnotatedPat annotatedPat = (Ast.AnnotatedPat) pat; + final Type type = toType(annotatedPat.type, typeSystem); + deducePatType(env, annotatedPat.pat, termMap, null, v); + return reg(pat, v, toTerm(type, Subst.EMPTY)); + case TUPLE_PAT: final List typeTerms = new ArrayList<>(); final Ast.TuplePat tuple = (Ast.TuplePat) pat; @@ -1186,8 +1238,9 @@ private Unifier.Term toTerm(Type type, Subst subst) { enum EmptyTypeEnv implements TypeEnv { INSTANCE; - @Override public Unifier.Term get(TypeSystem typeSystem, String name) { - throw new CompileException("unbound variable or constructor: " + name); + @Override public Unifier.Term get(TypeSystem typeSystem, String name, + Function exceptionFactory) { + throw exceptionFactory.apply(name); } @Override public boolean has(String name) { @@ -1206,7 +1259,8 @@ enum EmptyTypeEnv implements TypeEnv { /** Type environment. */ interface TypeEnv { - Unifier.Term get(TypeSystem typeSystem, String name); + Unifier.Term get(TypeSystem typeSystem, String name, + Function exceptionFactory); boolean has(String name); TypeEnv bind(String name, Function termFactory); @@ -1252,13 +1306,14 @@ private static class BindTypeEnv implements TypeEnv { this.parent = Objects.requireNonNull(parent); } - @Override public Unifier.Term get(TypeSystem typeSystem, String name) { + @Override public Unifier.Term get(TypeSystem typeSystem, String name, + Function exceptionFactory) { for (BindTypeEnv e = this;; e = (BindTypeEnv) e.parent) { if (e.definedName.equals(name)) { return e.termFactory.apply(typeSystem); } if (!(e.parent instanceof BindTypeEnv)) { - return e.parent.get(typeSystem, name); + return e.parent.get(typeSystem, name, exceptionFactory); } } } diff --git a/src/main/java/net/hydromatic/morel/eval/Applicable2.java b/src/main/java/net/hydromatic/morel/eval/Applicable2.java index 203f5655e..829817d1a 100644 --- a/src/main/java/net/hydromatic/morel/eval/Applicable2.java +++ b/src/main/java/net/hydromatic/morel/eval/Applicable2.java @@ -18,6 +18,7 @@ */ package net.hydromatic.morel.eval; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.BuiltIn; import java.util.List; @@ -63,8 +64,12 @@ */ @SuppressWarnings({"rawtypes", "unchecked"}) public abstract class Applicable2 extends ApplicableImpl { + protected Applicable2(BuiltIn builtIn, Pos pos) { + super(builtIn, pos); + } + protected Applicable2(BuiltIn builtIn) { - super(builtIn); + this(builtIn, Pos.ZERO); } @Override public Object apply(EvalEnv env, Object argValue) { diff --git a/src/main/java/net/hydromatic/morel/eval/Applicable3.java b/src/main/java/net/hydromatic/morel/eval/Applicable3.java index 5e80f1dcd..9b5052375 100644 --- a/src/main/java/net/hydromatic/morel/eval/Applicable3.java +++ b/src/main/java/net/hydromatic/morel/eval/Applicable3.java @@ -18,6 +18,7 @@ */ package net.hydromatic.morel.eval; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.BuiltIn; import java.util.List; @@ -48,8 +49,12 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public abstract class Applicable3 extends ApplicableImpl { + protected Applicable3(BuiltIn builtIn, Pos pos) { + super(builtIn, pos); + } + protected Applicable3(BuiltIn builtIn) { - super(builtIn); + this(builtIn, Pos.ZERO); } @Override public Object apply(EvalEnv env, Object argValue) { diff --git a/src/main/java/net/hydromatic/morel/eval/ApplicableImpl.java b/src/main/java/net/hydromatic/morel/eval/ApplicableImpl.java index e6f68454f..e8208a513 100644 --- a/src/main/java/net/hydromatic/morel/eval/ApplicableImpl.java +++ b/src/main/java/net/hydromatic/morel/eval/ApplicableImpl.java @@ -18,23 +18,37 @@ */ package net.hydromatic.morel.eval; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.BuiltIn; /** Abstract implementation of {@link Applicable} that describes itself * with a constant name. */ abstract class ApplicableImpl implements Applicable { private final String name; + final Pos pos; - protected ApplicableImpl(String name) { + protected ApplicableImpl(String name, Pos pos) { this.name = name; + this.pos = pos; + } + + protected ApplicableImpl(String name) { + this(name, Pos.ZERO); } /** Creates an ApplicableImpl that directly implements a BuiltIn. * The parameter is currently only for provenance purposes. */ - protected ApplicableImpl(BuiltIn builtIn) { + protected ApplicableImpl(BuiltIn builtIn, Pos pos) { this(builtIn.mlName.startsWith("op ") ? builtIn.mlName.substring("op ".length()) - : builtIn.structure + "." + builtIn.mlName); + : builtIn.structure + "." + builtIn.mlName, + pos); + } + + /** Creates an ApplicableImpl that directly implements a BuiltIn. + * The parameter is currently only for provenance purposes. */ + protected ApplicableImpl(BuiltIn builtIn) { + this(builtIn, Pos.ZERO); } @Override public String toString() { diff --git a/src/main/java/net/hydromatic/morel/eval/Closure.java b/src/main/java/net/hydromatic/morel/eval/Closure.java index 0a8c1bfb3..0531a1b17 100644 --- a/src/main/java/net/hydromatic/morel/eval/Closure.java +++ b/src/main/java/net/hydromatic/morel/eval/Closure.java @@ -19,6 +19,7 @@ package net.hydromatic.morel.eval; import net.hydromatic.morel.ast.Core; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.util.Pair; import com.google.common.collect.ImmutableList; @@ -48,12 +49,14 @@ public class Closure implements Comparable, Applicable { * pattern ({@code _}) succeeds, and therefore we evaluate the second * code {@code "no"}. */ private final ImmutableList> patCodes; + private final Pos pos; /** Not a public API. */ public Closure(EvalEnv evalEnv, - ImmutableList> patCodes) { + ImmutableList> patCodes, Pos pos) { this.evalEnv = requireNonNull(evalEnv).fix(); this.patCodes = requireNonNull(patCodes); + this.pos = pos; } @Override public String toString() { @@ -106,7 +109,7 @@ Object bindEval(Object argValue) { return code.eval(envRef.env); } } - throw new Codes.MorelRuntimeException(Codes.BuiltInExn.BIND); + throw new Codes.MorelRuntimeException(Codes.BuiltInExn.BIND, pos); } @Override public Object apply(EvalEnv env, Object argValue) { diff --git a/src/main/java/net/hydromatic/morel/eval/Codes.java b/src/main/java/net/hydromatic/morel/eval/Codes.java index cc12fc996..ff516ad33 100644 --- a/src/main/java/net/hydromatic/morel/eval/Codes.java +++ b/src/main/java/net/hydromatic/morel/eval/Codes.java @@ -20,6 +20,7 @@ import net.hydromatic.morel.ast.Core; import net.hydromatic.morel.ast.Op; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.BuiltIn; import net.hydromatic.morel.compile.Environment; import net.hydromatic.morel.compile.Macro; @@ -30,6 +31,7 @@ import net.hydromatic.morel.type.Type; import net.hydromatic.morel.type.TypeSystem; import net.hydromatic.morel.util.MapList; +import net.hydromatic.morel.util.MorelException; import net.hydromatic.morel.util.Ord; import net.hydromatic.morel.util.Pair; import net.hydromatic.morel.util.Static; @@ -307,15 +309,26 @@ public static Code orElse(Code code0, Code code1) { }; /** @see BuiltIn#INTERACT_USE */ - private static final Applicable INTERACT_USE = - new ApplicableImpl(BuiltIn.INTERACT_USE) { - @Override public Object apply(EvalEnv env, Object arg) { - final String f = (String) arg; - final Session session = (Session) env.getOpt(EvalEnv.SESSION); - session.use(f); - return Unit.INSTANCE; - } - }; + private static final Applicable INTERACT_USE = new InteractUse(Pos.ZERO); + + /** Implements {@link BuiltIn#INTERACT_USE}. */ + private static class InteractUse extends ApplicableImpl + implements Positioned { + InteractUse(Pos pos) { + super(BuiltIn.INTERACT_USE, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new InteractUse(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final String f = (String) arg; + final Session session = (Session) env.getOpt(EvalEnv.SESSION); + session.use(f, pos); + return Unit.INSTANCE; + } + } /** @see BuiltIn#OP_CARET */ private static final Applicable OP_CARET = @@ -588,81 +601,140 @@ public static Applicable nth(int slot) { }; /** @see BuiltIn#STRING_SUB */ - private static final Applicable STRING_SUB = - new Applicable2(BuiltIn.STRING_SUB) { - @Override public Character apply(String s, Integer i) { - if (i < 0 || i >= s.length()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return s.charAt(i); - } - }; + private static final Applicable STRING_SUB = new StringSub(Pos.ZERO); + + /** Implements {@link BuiltIn#STRING_SUB}. */ + private static class StringSub extends Applicable2 + implements Positioned { + StringSub(Pos pos) { + super(BuiltIn.STRING_SUB, pos); + } + + @Override public Character apply(String s, Integer i) { + if (i < 0 || i >= s.length()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + return s.charAt(i); + } + + public StringSub withPos(Pos pos) { + return new StringSub(pos); + } + } /** @see BuiltIn#STRING_EXTRACT */ private static final Applicable STRING_EXTRACT = - new Applicable3(BuiltIn.STRING_EXTRACT) { - @Override public String apply(String s, Integer i, List jOpt) { - if (i < 0) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - if (jOpt.size() == 2) { - final int j = (Integer) jOpt.get(1); - if (j < 0 || i + j > s.length()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return s.substring(i, i + j); - } else { - if (i > s.length()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return s.substring(i); - } + new StringExtract(Pos.ZERO); + + /** Implements {@link BuiltIn#STRING_SUB}. */ + private static class StringExtract + extends Applicable3 implements Positioned { + StringExtract(Pos pos) { + super(BuiltIn.STRING_EXTRACT, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new StringExtract(pos); + } + + @Override public String apply(String s, Integer i, List jOpt) { + if (i < 0) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + if (jOpt.size() == 2) { + final int j = (Integer) jOpt.get(1); + if (j < 0 || i + j > s.length()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); } - }; + return s.substring(i, i + j); + } else { + if (i > s.length()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + return s.substring(i); + } + } + } /** @see BuiltIn#STRING_SUBSTRING */ private static final Applicable STRING_SUBSTRING = - new Applicable3( - BuiltIn.STRING_SUBSTRING) { - @Override public String apply(String s, Integer i, Integer j) { - if (i < 0 || j < 0 || i + j > s.length()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return s.substring(i, i + j); - } - }; + new StringSubstring(Pos.ZERO); + + /** Implements {@link BuiltIn#STRING_SUBSTRING}. */ + private static class StringSubstring + extends Applicable3 + implements Positioned { + StringSubstring(Pos pos) { + super(BuiltIn.STRING_SUBSTRING, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new StringSubstring(pos); + } + + @Override public String apply(String s, Integer i, Integer j) { + if (i < 0 || j < 0 || i + j > s.length()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + return s.substring(i, i + j); + } + } /** @see BuiltIn#STRING_CONCAT */ - private static final Applicable STRING_CONCAT = - new ApplicableImpl(BuiltIn.STRING_CONCAT) { - @SuppressWarnings("unchecked") - @Override public Object apply(EvalEnv env, Object arg) { - return stringConcat("", (List) arg); - } - }; + private static final Applicable STRING_CONCAT = new StringConcat(Pos.ZERO); + + /** Implements {@link BuiltIn#STRING_CONCAT}. */ + private static class StringConcat extends ApplicableImpl + implements Positioned { + StringConcat(Pos pos) { + super(BuiltIn.STRING_CONCAT, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new StringConcat(pos); + } + + @SuppressWarnings("unchecked") + @Override public Object apply(EvalEnv env, Object arg) { + return stringConcat(pos, "", (List) arg); + } + } /** @see BuiltIn#STRING_CONCAT_WITH */ private static final Applicable STRING_CONCAT_WITH = - new ApplicableImpl(BuiltIn.STRING_CONCAT_WITH) { - @Override public Object apply(EvalEnv env, Object argValue) { - final String separator = (String) argValue; - return new ApplicableImpl("String.concatWith$separator") { - @SuppressWarnings("unchecked") - @Override public Object apply(EvalEnv env, Object arg) { - return stringConcat(separator, (List) arg); - } - }; + new StringConcatWith(Pos.ZERO); + + /** Implements {@link BuiltIn#STRING_CONCAT_WITH}. */ + private static class StringConcatWith extends ApplicableImpl + implements Positioned { + StringConcatWith(Pos pos) { + super(BuiltIn.STRING_CONCAT_WITH, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new StringConcatWith(pos); + } + + @Override public Object apply(EvalEnv env, Object argValue) { + final String separator = (String) argValue; + return new ApplicableImpl("String.concatWith$separator") { + @SuppressWarnings("unchecked") + @Override public Object apply(EvalEnv env, Object arg) { + return stringConcat(pos, separator, (List) arg); } }; + } + } - private static String stringConcat(String separator, List list) { + private static String stringConcat(Pos pos, String separator, + List list) { long n = 0; for (String s : list) { n += s.length(); n += separator.length(); } if (n > STRING_MAX_SIZE) { - throw new MorelRuntimeException(BuiltInExn.SIZE); + throw new MorelRuntimeException(BuiltInExn.SIZE, pos); } return String.join(separator, list); } @@ -825,41 +897,72 @@ private static ApplicableImpl union(final BuiltIn builtIn) { /** @see BuiltIn#LIST_HD */ private static final Applicable LIST_HD = - new ApplicableImpl(BuiltIn.LIST_HD) { - @Override public Object apply(EvalEnv env, Object arg) { - final List list = (List) arg; - if (list.isEmpty()) { - throw new MorelRuntimeException(BuiltInExn.EMPTY); - } - return list.get(0); - } - }; + new ListHd(Pos.ZERO); + + /** Implements {@link BuiltIn#LIST_HD}. */ + private static class ListHd extends ApplicableImpl implements Positioned { + ListHd(Pos pos) { + super(BuiltIn.LIST_HD, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new ListHd(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List list = (List) arg; + if (list.isEmpty()) { + throw new MorelRuntimeException(BuiltInExn.EMPTY, pos); + } + return list.get(0); + } + } /** @see BuiltIn#LIST_TL */ - private static final Applicable LIST_TL = - new ApplicableImpl(BuiltIn.LIST_TL) { - @Override public Object apply(EvalEnv env, Object arg) { - final List list = (List) arg; - final int size = list.size(); - if (size == 0) { - throw new MorelRuntimeException(BuiltInExn.EMPTY); - } - return list.subList(1, size); - } - }; + private static final Applicable LIST_TL = new ListTl(Pos.ZERO); + + /** Implements {@link BuiltIn#LIST_TL}. */ + private static class ListTl extends ApplicableImpl implements Positioned { + ListTl(Pos pos) { + super(BuiltIn.LIST_TL, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new ListTl(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List list = (List) arg; + final int size = list.size(); + if (size == 0) { + throw new MorelRuntimeException(BuiltInExn.EMPTY, pos); + } + return list.subList(1, size); + } + } /** @see BuiltIn#LIST_LAST */ - private static final Applicable LIST_LAST = - new ApplicableImpl(BuiltIn.LIST_LAST) { - @Override public Object apply(EvalEnv env, Object arg) { - final List list = (List) arg; - final int size = list.size(); - if (size == 0) { - throw new MorelRuntimeException(BuiltInExn.EMPTY); - } - return list.get(size - 1); - } - }; + private static final Applicable LIST_LAST = new ListLast(Pos.ZERO); + + /** Implements {@link BuiltIn#LIST_LAST}. */ + private static class ListLast extends ApplicableImpl implements Positioned { + ListLast(Pos pos) { + super(BuiltIn.LIST_LAST, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new ListLast(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List list = (List) arg; + final int size = list.size(); + if (size == 0) { + throw new MorelRuntimeException(BuiltInExn.EMPTY, pos); + } + return list.get(size - 1); + } + } /** @see BuiltIn#LIST_GET_ITEM */ private static final Applicable LIST_GET_ITEM = @@ -876,29 +979,53 @@ private static ApplicableImpl union(final BuiltIn builtIn) { }; /** @see BuiltIn#LIST_NTH */ - private static final Applicable LIST_NTH = nth(BuiltIn.LIST_NTH); + private static final Applicable LIST_NTH = + new ListNth(BuiltIn.LIST_NTH, Pos.ZERO); - private static ApplicableImpl nth(BuiltIn builtIn) { - return new Applicable2(builtIn) { - @Override public Object apply(List list, Integer i) { - if (i < 0 || i >= list.size()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return list.get(i); + /** Implements {@link BuiltIn#LIST_NTH} + * and {@link BuiltIn#VECTOR_SUB}. */ + private static class ListNth extends Applicable2 + implements Positioned { + private final BuiltIn builtIn; + + ListNth(BuiltIn builtIn, Pos pos) { + super(builtIn, pos); + this.builtIn = builtIn; + } + + @Override public Applicable withPos(Pos pos) { + return new ListNth(builtIn, pos); + } + + @Override public Object apply(List list, Integer i) { + if (i < 0 || i >= list.size()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); } - }; + return list.get(i); + } } /** @see BuiltIn#LIST_TAKE */ - private static final Applicable LIST_TAKE = - new Applicable2(BuiltIn.LIST_TAKE) { - @Override public List apply(List list, Integer i) { - if (i < 0 || i > list.size()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - return list.subList(0, i); - } - }; + private static final Applicable LIST_TAKE = new ListTake(Pos.ZERO); + + /** Implements {@link BuiltIn#LIST_TAKE}. */ + private static class ListTake extends Applicable2 + implements Positioned { + ListTake(Pos pos) { + super(BuiltIn.LIST_TAKE, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new ListTake(pos); + } + + @Override public List apply(List list, Integer i) { + if (i < 0 || i > list.size()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + return list.subList(0, i); + } + } /** @see BuiltIn#LIST_DROP */ private static final Applicable LIST_DROP = @@ -1165,24 +1292,35 @@ private static Applicable listAll(Applicable f) { /** @see BuiltIn#LIST_TABULATE */ private static final Applicable LIST_TABULATE = - tabulate(BuiltIn.LIST_TABULATE); + new ListTabulate(BuiltIn.LIST_TABULATE, Pos.ZERO); - private static ApplicableImpl tabulate(final BuiltIn builtIn) { - return new ApplicableImpl(builtIn) { - @Override public Object apply(EvalEnv env, Object arg) { - final List tuple = (List) arg; - final int count = (Integer) tuple.get(0); - if (count < 0) { - throw new MorelRuntimeException(BuiltInExn.SIZE); - } - final Applicable fn = (Applicable) tuple.get(1); - final ImmutableList.Builder builder = ImmutableList.builder(); - for (int i = 0; i < count; i++) { - builder.add(fn.apply(env, i)); - } - return builder.build(); + /** Implements {@link BuiltIn#LIST_TABULATE}. */ + private static class ListTabulate extends ApplicableImpl + implements Positioned { + private final BuiltIn builtIn; + + ListTabulate(BuiltIn builtIn, Pos pos) { + super(builtIn, pos); + this.builtIn = builtIn; + } + + @Override public Applicable withPos(Pos pos) { + return new ListTabulate(builtIn, pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List tuple = (List) arg; + final int count = (Integer) tuple.get(0); + if (count < 0) { + throw new MorelRuntimeException(BuiltInExn.SIZE, pos); } - }; + final Applicable fn = (Applicable) tuple.get(1); + final ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < count; i++) { + builder.add(fn.apply(env, i)); + } + return builder.build(); + } } /** @see BuiltIn#LIST_COLLATE */ @@ -1393,16 +1531,28 @@ private static Applicable optionApp(Applicable f) { /** @see BuiltIn#OPTION_VAL_OF */ private static final Applicable OPTION_VAL_OF = - new ApplicableImpl(BuiltIn.OPTION_VAL_OF) { - @Override public Object apply(EvalEnv env, Object arg) { - final List opt = (List) arg; - if (opt.size() == 2) { // SOME has 2 elements, NONE has 1 - return opt.get(1); - } else { - throw new MorelRuntimeException(BuiltInExn.OPTION); - } - } - }; + new OptionValOf(Pos.ZERO); + + /** Implements {@link BuiltIn#OPTION_VAL_OF}. */ + private static class OptionValOf extends ApplicableImpl + implements Positioned { + OptionValOf(Pos pos) { + super(BuiltIn.OPTION_VAL_OF, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new OptionValOf(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List opt = (List) arg; + if (opt.size() == 2) { // SOME has 2 elements, NONE has 1 + return opt.get(1); + } else { + throw new MorelRuntimeException(BuiltInExn.OPTION, pos); + } + } + } /** @see BuiltIn#OPTION_FILTER */ private static final Applicable OPTION_FILTER = @@ -1557,37 +1707,61 @@ private static Applicable optionComposePartial(Applicable f, Applicable g) { /** @see BuiltIn#REAL_CHECK_FLOAT */ private static final Applicable REAL_CHECK_FLOAT = - new ApplicableImpl(BuiltIn.REAL_CHECK_FLOAT) { - @Override public Float apply(EvalEnv env, Object arg) { - final Float f = (Float) arg; - if (Float.isFinite(f)) { - return f; - } - if (Float.isNaN(f)) { - throw new MorelRuntimeException(BuiltInExn.DIV); - } else { - throw new MorelRuntimeException(BuiltInExn.OVERFLOW); - } - } - }; + new RealCheckFloat(Pos.ZERO); + + /** Implements {@link BuiltIn#REAL_CHECK_FLOAT}. */ + private static class RealCheckFloat extends ApplicableImpl + implements Positioned { + RealCheckFloat(Pos pos) { + super(BuiltIn.REAL_CHECK_FLOAT, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new RealCheckFloat(pos); + } + + @Override public Float apply(EvalEnv env, Object arg) { + final Float f = (Float) arg; + if (Float.isFinite(f)) { + return f; + } + if (Float.isNaN(f)) { + throw new MorelRuntimeException(BuiltInExn.DIV, pos); + } else { + throw new MorelRuntimeException(BuiltInExn.OVERFLOW, pos); + } + } + } /** @see BuiltIn#REAL_COMPARE */ private static final Applicable REAL_COMPARE = - new Applicable2(BuiltIn.REAL_COMPARE) { - @Override public List apply(Float f0, Float f1) { - if (Float.isNaN(f0) || Float.isNaN(f1)) { - throw new MorelRuntimeException(BuiltInExn.UNORDERED); - } - if (f0 < f1) { - return ORDER_LESS; - } - if (f0 > f1) { - return ORDER_GREATER; - } - // In particular, compare (~0.0, 0) returns ORDER_EQUAL - return ORDER_EQUAL; - } - }; + new RealCompare(Pos.ZERO); + + /** Implements {@link BuiltIn#REAL_COMPARE}. */ + private static class RealCompare extends Applicable2 + implements Positioned { + RealCompare(Pos pos) { + super(BuiltIn.REAL_COMPARE, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new RealCompare(pos); + } + + @Override public List apply(Float f0, Float f1) { + if (Float.isNaN(f0) || Float.isNaN(f1)) { + throw new MorelRuntimeException(BuiltInExn.UNORDERED, pos); + } + if (f0 < f1) { + return ORDER_LESS; + } + if (f0 > f1) { + return ORDER_GREATER; + } + // In particular, compare (~0.0, 0) returns ORDER_EQUAL + return ORDER_EQUAL; + } + } /** @see BuiltIn#REAL_COPY_SIGN */ private static final Applicable REAL_COPY_SIGN = @@ -1809,18 +1983,29 @@ private static Applicable optionComposePartial(Applicable f, Applicable g) { }; /** @see BuiltIn#REAL_SIGN */ - private static final Applicable REAL_SIGN = - new ApplicableImpl(BuiltIn.REAL_SIGN) { - @Override public Object apply(EvalEnv env, Object arg) { - final float f = (Float) arg; - if (Float.isNaN(f)) { - throw new MorelRuntimeException(BuiltInExn.DOMAIN); - } - return f == 0f ? 0 // positive or negative zero - : (f > 0f) ? 1 // positive number or positive infinity - : -1; // negative number or negative infinity - } - }; + private static final Applicable REAL_SIGN = new RealSign(Pos.ZERO); + + /** Implements {@link BuiltIn#REAL_COMPARE}. */ + private static class RealSign extends ApplicableImpl + implements Positioned { + RealSign(Pos pos) { + super(BuiltIn.REAL_SIGN, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new RealSign(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final float f = (Float) arg; + if (Float.isNaN(f)) { + throw new MorelRuntimeException(BuiltInExn.DOMAIN, pos); + } + return f == 0f ? 0 // positive or negative zero + : (f > 0f) ? 1 // positive number or positive infinity + : -1; // negative number or negative infinity + } + } /** @see BuiltIn#REAL_SIGN_BIT */ private static final Applicable REAL_SIGN_BIT = @@ -1969,18 +2154,30 @@ private static ApplicableImpl isNotEmpty(BuiltIn builtIn) { /** @see BuiltIn#RELATIONAL_ONLY */ private static final Applicable RELATIONAL_ONLY = - new ApplicableImpl(BuiltIn.RELATIONAL_ONLY) { - @Override public Object apply(EvalEnv env, Object arg) { - final List list = (List) arg; - if (list.isEmpty()) { - throw new MorelRuntimeException(BuiltInExn.EMPTY); - } - if (list.size() > 1) { - throw new MorelRuntimeException(BuiltInExn.SIZE); - } - return list.get(0); - } - }; + new RelationalOnly(Pos.ZERO); + + /** Implements {@link BuiltIn#RELATIONAL_ONLY}. */ + private static class RelationalOnly extends ApplicableImpl + implements Positioned { + RelationalOnly(Pos pos) { + super(BuiltIn.RELATIONAL_ONLY, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new RelationalOnly(pos); + } + + @Override public Object apply(EvalEnv env, Object arg) { + final List list = (List) arg; + if (list.isEmpty()) { + throw new MorelRuntimeException(BuiltInExn.EMPTY, pos); + } + if (list.size() > 1) { + throw new MorelRuntimeException(BuiltInExn.SIZE, pos); + } + return list.get(0); + } + } /** Implements {@link #RELATIONAL_SUM} for type {@code int list}. */ private static final Applicable Z_SUM_INT = @@ -2055,7 +2252,7 @@ private static Core.Exp sysEnv(TypeSystem typeSystem, Environment env, core.stringLiteral(entry.getKey()), core.stringLiteral(entry.getValue().id.type.moniker()))) .collect(Collectors.toList()); - return core.apply(typeSystem.listType(argType), + return core.apply(Pos.ZERO, typeSystem.listType(argType), core.functionLiteral(typeSystem, BuiltIn.Z_LIST), core.tuple(typeSystem, null, args)); } @@ -2123,26 +2320,39 @@ private static Core.Exp sysEnv(TypeSystem typeSystem, Environment env, /** @see BuiltIn#VECTOR_TABULATE */ private static final Applicable VECTOR_TABULATE = - tabulate(BuiltIn.VECTOR_TABULATE); + new ListTabulate(BuiltIn.VECTOR_TABULATE, Pos.ZERO); /** @see BuiltIn#VECTOR_LENGTH */ private static final Applicable VECTOR_LENGTH = length(BuiltIn.VECTOR_LENGTH); /** @see BuiltIn#VECTOR_SUB */ - private static final Applicable VECTOR_SUB = nth(BuiltIn.VECTOR_SUB); + private static final Applicable VECTOR_SUB = + new ListNth(BuiltIn.VECTOR_SUB, Pos.ZERO); /** @see BuiltIn#VECTOR_UPDATE */ private static final Applicable VECTOR_UPDATE = - new Applicable3(BuiltIn.VECTOR_UPDATE) { - @Override public List apply(List vec, Integer i, Object x) { - if (i < 0 || i >= vec.size()) { - throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT); - } - final Object[] elements = vec.toArray(); - elements[i] = x; - return ImmutableList.copyOf(elements); - } - }; + new VectorUpdate(Pos.ZERO); + + /** Implements {@link BuiltIn#VECTOR_UPDATE}. */ + private static class VectorUpdate + extends Applicable3 implements Positioned { + VectorUpdate(Pos pos) { + super(BuiltIn.VECTOR_UPDATE, pos); + } + + @Override public Applicable withPos(Pos pos) { + return new VectorUpdate(pos); + } + + @Override public List apply(List vec, Integer i, Object x) { + if (i < 0 || i >= vec.size()) { + throw new MorelRuntimeException(BuiltInExn.SUBSCRIPT, pos); + } + final Object[] elements = vec.toArray(); + elements[i] = x; + return ImmutableList.copyOf(elements); + } + } /** @see BuiltIn#VECTOR_CONCAT */ private static final Applicable VECTOR_CONCAT = @@ -3078,18 +3288,29 @@ private static class GetTupleCode implements Code { } /** Java exception that wraps an exception thrown by the Morel runtime. */ - public static class MorelRuntimeException extends RuntimeException { + public static class MorelRuntimeException extends RuntimeException + implements MorelException { private final BuiltInExn e; + private final Pos pos; /** Creates a MorelRuntimeException. */ - public MorelRuntimeException(BuiltInExn e) { + public MorelRuntimeException(BuiltInExn e, Pos pos) { this.e = requireNonNull(e); + this.pos = requireNonNull(pos); } - public StringBuilder describeTo(StringBuilder buf) { + @Override public String toString() { + return e.mlName + " at " + pos; + } + + @Override public StringBuilder describeTo(StringBuilder buf) { return buf.append("uncaught exception ") .append(e.mlName); } + + @Override public Pos pos() { + return pos; + } } /** Definitions of Morel built-in exceptions. */ @@ -3328,6 +3549,18 @@ static class ApplyCodeCode implements Code { } } + /** An {@link Applicable} whose position can be changed. + * + *

Operations that may throw exceptions should implement this interface. + * Then the exceptions can be tied to the correct position in the source code. + * + *

If you don't implement this interface, the applicable will use the + * default position, which is {@link Pos#ZERO}. If the exception has position + * "0.0-0.0", that is an indication you need to use this interface, and make + * sure that the position is propagated through the translation process. */ + public interface Positioned extends Applicable { + Applicable withPos(Pos pos); + } } // End Codes.java diff --git a/src/main/java/net/hydromatic/morel/eval/Prop.java b/src/main/java/net/hydromatic/morel/eval/Prop.java index f6ec3b051..74c0ef0e7 100644 --- a/src/main/java/net/hydromatic/morel/eval/Prop.java +++ b/src/main/java/net/hydromatic/morel/eval/Prop.java @@ -37,6 +37,13 @@ public enum Prop { /** Maximum number of inlining passes. */ INLINE_PASS_COUNT("inlinePassCount", Integer.class, 5), + /** Boolean property "matchCoverageEnabled" controls whether to check the + * coverage of patterns. If true (the default), Morel warns if patterns are + * redundant and gives errors if patterns are not exhaustive. If false, + * Morel does not analyze pattern coverage, and therefore will not give + * warnings or errors. */ + MATCH_COVERAGE_ENABLED("matchCoverageEnabled", Boolean.class, true), + /** Integer property "optionalInt" is for testing. Default is null. */ OPTIONAL_INT("optionalInt", Integer.class, null), diff --git a/src/main/java/net/hydromatic/morel/eval/Session.java b/src/main/java/net/hydromatic/morel/eval/Session.java index 5d1a7bcbd..7f0d84bfb 100644 --- a/src/main/java/net/hydromatic/morel/eval/Session.java +++ b/src/main/java/net/hydromatic/morel/eval/Session.java @@ -18,7 +18,9 @@ */ package net.hydromatic.morel.eval; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.CompileException; +import net.hydromatic.morel.util.MorelException; import java.util.LinkedHashMap; import java.util.List; @@ -40,7 +42,7 @@ public class Session { public final Map map = new LinkedHashMap<>(); /** Implementation of "use". */ - private Shell shell = DefaultShell.INSTANCE; + private Shell shell = Shells.INSTANCE; /** Calls some code with a new value of {@link Shell}. */ public void withShell(Shell shell, Consumer outLines, @@ -58,41 +60,60 @@ public void withShell(Shell shell, Consumer outLines, } } - public void use(String fileName) { - shell.use(fileName); + /** Calls some code with a {@link Shell} that does not handle errors. */ + public void withoutHandlingExceptions(Consumer consumer) { + final Shell prevShell = this.shell; + try { + this.shell = Shells.BARF; + consumer.accept(this); + } finally { + this.shell = prevShell; + } + } + + public void use(String fileName, Pos pos) { + shell.use(fileName, pos); } - public void handle(Codes.MorelRuntimeException e, StringBuilder buf) { - shell.handle(e, buf); + public void handle(MorelException e, StringBuilder buf) { + shell.handle((RuntimeException) e, buf); } /** Callback to implement "use" command. */ public interface Shell { - void use(String fileName); + void use(String fileName, Pos pos); /** Handles an exception. Particular implementations may re-throw the * exception, or may format the exception to a buffer that will be added to - * the output. Typically the a root shell will handle the exception, and + * the output. Typically, a root shell will handle the exception, and * sub-shells will re-throw. */ void handle(RuntimeException e, StringBuilder buf); } - /** Default implementation of {@link Shell}. */ - private enum DefaultShell implements Shell { - INSTANCE; - - @Override public void use(String fileName) { - throw new UnsupportedOperationException(); - } + /** Various implementations of {@link Shell}. */ + private enum Shells implements Shell { + /** Default instance of Shell. */ + INSTANCE { + @Override public void handle(RuntimeException e, StringBuilder buf) { + if (e instanceof Codes.MorelRuntimeException) { + ((Codes.MorelRuntimeException) e).describeTo(buf); + } else if (e instanceof CompileException) { + buf.append(e.getMessage()); + } else { + buf.append(e); + } + } + }, - @Override public void handle(RuntimeException e, StringBuilder buf) { - if (e instanceof Codes.MorelRuntimeException) { - ((Codes.MorelRuntimeException) e).describeTo(buf); - } else if (e instanceof CompileException) { - buf.append(e.getMessage()); - } else { - buf.append(e); + /** Instance of Shell that does not handle exceptions. */ + BARF { + @Override public void handle(RuntimeException e, StringBuilder buf) { + throw e; } + }; + + @Override public void use(String fileName, Pos pos) { + throw new UnsupportedOperationException(); } } } diff --git a/src/main/java/net/hydromatic/morel/foreign/RelList.java b/src/main/java/net/hydromatic/morel/foreign/RelList.java index 342b7195b..44f60136e 100644 --- a/src/main/java/net/hydromatic/morel/foreign/RelList.java +++ b/src/main/java/net/hydromatic/morel/foreign/RelList.java @@ -38,10 +38,11 @@ public class RelList extends AbstractList { RelList(RelNode rel, DataContext dataContext, Function converter) { this.rel = rel; + //noinspection FunctionalExpressionCanBeFolded supplier = Suppliers.memoize(() -> new Interpreter(dataContext, rel) .select(converter::apply) - .toList()); + .toList())::get; } public Object get(int index) { diff --git a/src/main/java/net/hydromatic/morel/parse/MorelParser.java b/src/main/java/net/hydromatic/morel/parse/MorelParser.java index 0f958011d..8a8e7c22e 100644 --- a/src/main/java/net/hydromatic/morel/parse/MorelParser.java +++ b/src/main/java/net/hydromatic/morel/parse/MorelParser.java @@ -23,7 +23,10 @@ /** Parser for Morel, a variant of Standard ML. */ public interface MorelParser { /** Returns the position of the last token returned by the parser. */ - Pos getPos(); + Pos pos(); + + /** Sets the current file, and sets the current line to zero. */ + void zero(String file); } // End MorelParser.java diff --git a/src/main/java/net/hydromatic/morel/parse/Span.java b/src/main/java/net/hydromatic/morel/parse/Span.java index 69e7266c9..09d0e9280 100644 --- a/src/main/java/net/hydromatic/morel/parse/Span.java +++ b/src/main/java/net/hydromatic/morel/parse/Span.java @@ -113,7 +113,7 @@ public Span addAll(Iterable nodes) { * and returns this Span. */ public Span add(MorelParser parser) { try { - final Pos pos = parser.getPos(); + final Pos pos = parser.pos(); return add(pos); } catch (Exception e) { // getPos does not really throw an exception diff --git a/src/main/java/net/hydromatic/morel/type/TypeSystem.java b/src/main/java/net/hydromatic/morel/type/TypeSystem.java index a066f9b7e..1a6408354 100644 --- a/src/main/java/net/hydromatic/morel/type/TypeSystem.java +++ b/src/main/java/net/hydromatic/morel/type/TypeSystem.java @@ -54,6 +54,7 @@ * "{@code int -> int}"). */ public class TypeSystem { final Map typeByName = new HashMap<>(); + final Map internalTypeByName = new HashMap<>(); final Map typeByKey = new HashMap<>(); private final Map> typeConstructorByName = @@ -98,6 +99,15 @@ private Type wrap(DataType dataType, Type type) { return typeVars.isEmpty() ? type : forallType(typeVars, type); } + /** Looks up an internal type by name. */ + public Type lookupInternal(String name) { + final Type type = internalTypeByName.get(name); + if (type == null) { + throw new AssertionError("unknown type: " + name); + } + return type; + } + /** Looks up a type by name. */ public Type lookup(String name) { final Type type = typeByName.get(name); @@ -253,6 +263,13 @@ DataType dataType(String name, Key key, List types, return dataType; } + /** Converts a regular type to an internal type. Throws if the type is not + * known. */ + public void setInternal(String name) { + final Type type = typeByName.remove(name); + internalTypeByName.put(name, type); + } + /** Replaces temporary data types with real data types, using the supplied * map. */ @FunctionalInterface diff --git a/src/main/java/net/hydromatic/morel/util/MorelException.java b/src/main/java/net/hydromatic/morel/util/MorelException.java new file mode 100644 index 000000000..f4279e8aa --- /dev/null +++ b/src/main/java/net/hydromatic/morel/util/MorelException.java @@ -0,0 +1,32 @@ +/* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package net.hydromatic.morel.util; + +import net.hydromatic.morel.ast.Pos; + +/** Interface implemented by all exceptions in Morel. */ +public interface MorelException { + /** Returns the position. */ + Pos pos(); + + /** Writes a description of this exception. */ + StringBuilder describeTo(StringBuilder buf); +} + +// End MorelException.java diff --git a/src/main/java/net/hydromatic/morel/util/Pair.java b/src/main/java/net/hydromatic/morel/util/Pair.java index 963e17c8d..00dff57be 100644 --- a/src/main/java/net/hydromatic/morel/util/Pair.java +++ b/src/main/java/net/hydromatic/morel/util/Pair.java @@ -83,7 +83,7 @@ public static Pair of(Map.Entry entry) { public boolean equals(Object obj) { return this == obj - || (obj instanceof Pair) + || obj instanceof Pair && Objects.equals(this.left, ((Pair) obj).left) && Objects.equals(this.right, ((Pair) obj).right); } diff --git a/src/main/java/net/hydromatic/morel/util/Sat.java b/src/main/java/net/hydromatic/morel/util/Sat.java new file mode 100644 index 000000000..e38f5afac --- /dev/null +++ b/src/main/java/net/hydromatic/morel/util/Sat.java @@ -0,0 +1,256 @@ +/* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package net.hydromatic.morel.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * Boolean satisfiability. + */ +public class Sat { + private final Map variablesById = new HashMap<>(); + private final Map variablesByName = new HashMap<>(); + private int nextVariable = 0; + + /** Finds an assignment of variables such that a term evaluates to true, + * or null if there is no solution. */ + public @Nullable Map solve(Term term) { + final List> allAssignments = new ArrayList<>(); + for (Variable variable : variablesById.values()) { + allAssignments.add( + ImmutableList.of(new Assignment(variable, false), + new Assignment(variable, true))); + } + + final boolean[] env = new boolean[nextVariable]; + for (List assignments + : Lists.cartesianProduct(allAssignments)) { + assignments.forEach(a -> env[a.variable.id] = a.value); + if (term.evaluate(env)) { + final ImmutableMap.Builder builder = + ImmutableMap.builder(); + assignments.forEach(a -> builder.put(a.variable, a.value)); + return builder.build(); + } + } + return null; + } + + public Variable variable(String name) { + Variable variable = variablesByName.get(name); + if (variable != null) { + return variable; + } + int id = nextVariable++; + variable = new Variable(id, name); + variablesById.put(id, variable); + variablesByName.put(name, variable); + return variable; + } + + public Term not(Term term) { + return new Not(term); + } + + public Term and(Term... terms) { + return new And(ImmutableList.copyOf(terms)); + } + + public Term and(Iterable terms) { + return new And(ImmutableList.copyOf(terms)); + } + + public Term or(Term... terms) { + return new Or(ImmutableList.copyOf(terms)); + } + + public Term or(Iterable terms) { + return new Or(ImmutableList.copyOf(terms)); + } + + /** Base class for all terms (variables, and, or, not). */ + public abstract static class Term { + final Op op; + + Term(Op op) { + this.op = requireNonNull(op, "op"); + } + + @Override public String toString() { + return unparse(new StringBuilder(), 0, 0).toString(); + } + + protected abstract StringBuilder unparse(StringBuilder buf, int left, + int right); + + public abstract boolean evaluate(boolean[] env); + } + + /** Variable. Its value can be true or false. */ + public static class Variable extends Term { + public final int id; + public final String name; + + Variable(int id, String name) { + super(Op.VARIABLE); + this.id = id; + this.name = requireNonNull(name, "name"); + } + + @Override protected StringBuilder unparse(StringBuilder buf, int left, + int right) { + return buf.append(name); + } + + @Override public boolean evaluate(boolean[] env) { + return env[id]; + } + } + + /** Term that has a variable number of arguments ("and" or "or"). */ + abstract static class Node extends Term { + public final ImmutableList terms; + + Node(Op op, ImmutableList terms) { + super(op); + this.terms = requireNonNull(terms); + } + + @Override protected StringBuilder unparse(StringBuilder buf, int left, + int right) { + switch (terms.size()) { + case 0: + // empty "and" prints as "true"; + // empty "or" prints as "false" + return buf.append(op.emptyName); + case 1: + // singleton "and" and "or" print as the sole term + return terms.get(0).unparse(buf, left, right); + } + if (left > op.left || right > op.right) { + return unparse(buf.append('('), 0, 0).append(')'); + } + for (int i = 0; i < terms.size(); i++) { + final Term term = terms.get(i); + if (i > 0) { + buf.append(op.str); + } + term.unparse(buf, + i == 0 ? left : op.right, + i == terms.size() - 1 ? right : op.left); + } + return buf; + } + } + + /** "And" term. */ + static class And extends Node { + And(ImmutableList terms) { + super(Op.AND, terms); + } + + @Override public boolean evaluate(boolean[] env) { + for (Term term : terms) { + if (!term.evaluate(env)) { + return false; + } + } + return true; + } + } + + /** "Or" term. */ + static class Or extends Node { + Or(ImmutableList terms) { + super(Op.OR, terms); + } + + @Override public boolean evaluate(boolean[] env) { + for (Term term : terms) { + if (term.evaluate(env)) { + return true; + } + } + return false; + } + } + + /** "Not" term. */ + static class Not extends Term { + public final Term term; + + Not(Term term) { + super(Op.NOT); + this.term = requireNonNull(term, "term"); + } + + @Override protected StringBuilder unparse(StringBuilder buf, int left, + int right) { + return term.unparse(buf.append(op.str), op.right, right); + } + + @Override public boolean evaluate(boolean[] env) { + return !term.evaluate(env); + } + } + + /** Operator (or type of term), with its left and right precedence and print + * name. */ + private enum Op { + AND(3, 4, " ∧ ", "true"), + OR(1, 2, " ∨ ", "false"), + NOT(5, 5, "¬", ""), + VARIABLE(0, 0, "", ""); + + final int left; + final int right; + final String str; + final String emptyName; + + Op(int left, int right, String str, String emptyName) { + this.left = left; + this.right = right; + this.str = str; + this.emptyName = emptyName; + } + } + + /** Assignment of a variable to a value. */ + private static class Assignment { + final Variable variable; + final boolean value; + + Assignment(Variable variable, boolean value) { + this.variable = requireNonNull(variable, "variable"); + this.value = value; + } + } +} + +// End Sat.java diff --git a/src/main/java/net/hydromatic/morel/util/Tracers.java b/src/main/java/net/hydromatic/morel/util/Tracers.java index c23fa61af..90c65b350 100644 --- a/src/main/java/net/hydromatic/morel/util/Tracers.java +++ b/src/main/java/net/hydromatic/morel/util/Tracers.java @@ -102,8 +102,8 @@ public void onVariable(Unifier.Variable variable, Unifier.Term term) { flush(); } - public void onSubstitute( - Unifier.Term left, Unifier.Term right, Unifier.Term left2, Unifier.Term right2) { + public void onSubstitute(Unifier.Term left, Unifier.Term right, + Unifier.Term left2, Unifier.Term right2) { b.append("substitute ").append(left).append(' ').append(right); if (left2 != left) { b.append("; ").append(left).append(" -> ").append(left2); diff --git a/src/main/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 07f4e4b04..caf7ddc12 100644 --- a/src/main/javacc/MorelParser.jj +++ b/src/main/javacc/MorelParser.jj @@ -56,13 +56,24 @@ public class MorelParserImpl implements MorelParser private static final Logger LOGGER = LoggerFactory.getLogger("net.hydromatic.morel.parse"); + private int lineOffset; + private String file = ""; + public void setTabSize(int tabSize) { jj_input_stream.setTabSize(tabSize); } - public Pos getPos() { - return new Pos(token.beginLine, token.beginColumn, - token.endLine, token.endColumn); + public Pos pos() { + return new Pos(file, + token.beginLine - lineOffset, token.beginColumn, + token.endLine - lineOffset, token.endColumn + 1); + } + + public void zero(String file) { + this.file = file; + if (jj_input_stream.bufpos >= 0) { + this.lineOffset = jj_input_stream.bufline[jj_input_stream.bufpos]; + } } } @@ -75,11 +86,6 @@ void debug_message1() LOGGER.info("{} , {}", getToken(0).image, getToken(1).image); } -JAVACODE Pos pos() { - return new Pos(token.beginLine, token.beginColumn, - token.endLine, token.endColumn); -} - /** Parses a literal expression. */ Literal literal() : { @@ -248,9 +254,9 @@ RecordType recordType() : } { { - span = Span.of(getPos()); - final Map map = new LinkedHashMap<>(); - } + span = Span.of(pos()); + final Map map = new LinkedHashMap<>(); +} [ fieldType(map) ( @@ -282,7 +288,7 @@ Exp ifThenElse() : final Exp ifFalse; } { - { span = Span.of(getPos()); } condition = expression() + { span = Span.of(pos()); } condition = expression() ifTrue = expression() ifFalse = expression() { return ast.ifThenElse(span.end(this), condition, ifTrue, ifFalse); @@ -298,7 +304,7 @@ Exp let() : final List declList = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } ( decl = decl() [ ] { declList.add(decl); } )+ e = expression() { return ast.let(span.end(this), declList, e); @@ -313,7 +319,7 @@ Exp caseOf() : final List matchList; } { - { span = Span.of(getPos()); } exp = expression() + { span = Span.of(pos()); } exp = expression() matchList = matchList() { return ast.caseOf(span.end(this), exp, matchList); } @@ -327,7 +333,7 @@ Exp from() : final List steps = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } [ fromFirstStep(steps) ( fromFirstStep(steps) )* ] @@ -362,9 +368,9 @@ void fromStep(List steps) : } { { - span = Span.of(getPos()); - op = Op.INNER_JOIN; - } + span = Span.of(pos()); + op = Op.INNER_JOIN; +} patExp = fromSource() ( LOOKAHEAD(2) @@ -378,11 +384,11 @@ void fromStep(List steps) : condition)); } | - { span = Span.of(getPos()); } filterExp = expression() { + { span = Span.of(pos()); } filterExp = expression() { steps.add(ast.where(span.end(this), filterExp)); } | - { span = Span.of(getPos()); } + { span = Span.of(pos()); } ( groupExps = namedExpressionCommaList() | @@ -397,15 +403,15 @@ void fromStep(List steps) : steps.add(ast.group(span.end(this), groupExps, aggregates)); } | - { span = Span.of(getPos()); } aggregates = aggregateCommaList() { + { span = Span.of(pos()); } aggregates = aggregateCommaList() { steps.add(ast.compute(span.end(this), aggregates)); } | - { span = Span.of(getPos()); } orderItems = orderItemCommaList() { + { span = Span.of(pos()); } orderItems = orderItemCommaList() { steps.add(ast.order(span.end(this), orderItems)); } | - { span = Span.of(getPos()); } yieldExp = expression() { + { span = Span.of(pos()); } yieldExp = expression() { steps.add(ast.yield(span.end(this), yieldExp)); } } @@ -456,7 +462,7 @@ Aggregate aggregate() : { final Id id = aggregateId.left; final Exp aggregate = aggregateId.right; - return ast.aggregate(id.pos.plus(getPos()), aggregate, argument, id); + return ast.aggregate(id.pos.plus(pos()), aggregate, argument, id); } } @@ -482,7 +488,7 @@ OrderItem orderItem() : final Exp exp; } { - exp = expression() { span = Span.of(getPos()); } + exp = expression() { span = Span.of(pos()); } ( { return ast.orderItem(span.end(this), exp, Ast.Direction.DESC); } | @@ -497,7 +503,7 @@ Exp fn() : final Match match; } { - { span = Span.of(getPos()); } match = match() { + { span = Span.of(pos()); } match = match() { return ast.fn(span.end(this), match); } } @@ -583,7 +589,7 @@ Exp expression7() : Exp e2; } { - e = expression7() { return ast.negate(getPos(), e); } + e = expression7() { return ast.negate(pos(), e); } | e = expression8() ( @@ -755,9 +761,16 @@ Exp expression1() : Exp expression() : { Exp e; + Type t; } { - e = expression1() { return e; } + e = expression1() + ( + t = type() { + e = ast.annotatedExp(e.pos.plus(t.pos), e, t); + } + )* + { return e; } } /** List of expressions "e1 as id1, e2 as id2, e3 as id3". */ @@ -819,7 +832,7 @@ Exp atom() : e = from() { return e; } | { - span = Span.of(getPos()); + span = Span.of(pos()); } ( { return ast.unitLiteral(span.end(this)); } @@ -843,7 +856,7 @@ Exp atom() : ) | { - span = Span.of(getPos()); + span = Span.of(pos()); final List list = new ArrayList<>(); Exp e2; } @@ -858,7 +871,7 @@ Exp atom() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); final Map map = new LinkedHashMap<>(); } [ @@ -912,7 +925,7 @@ ValDecl valDecl() : final List valBinds = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } [ { rec = true; } ] valBind(valBinds) ( @@ -963,7 +976,7 @@ Ast.DatatypeDecl datatypeDecl() : final List binds = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } datatypeBind(binds) ( datatypeBind(binds) @@ -1035,7 +1048,7 @@ Ast.FunDecl funDecl() : final List funBindList = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } funBind(funBindList) ( funBind(funBindList) @@ -1071,13 +1084,16 @@ void funMatch(List list) : Ast.Pat pat; final List patList = new ArrayList<>(); final Ast.Exp expression; + Ast.Type returnType = null; } { id = identifier() ( pat = atomPat() { patList.add(pat); } )+ + [ returnType = type() ] expression = expression() { list.add( - ast.funMatch(id.pos.plus(expression.pos), id.name, patList, expression)); + ast.funMatch(id.pos.plus(expression.pos), id.name, patList, returnType, + expression)); } } @@ -1163,7 +1179,7 @@ Pat atomPat() : return ast.wildcardPat(pos()); } | - { span = Span.of(getPos()); } + { span = Span.of(pos()); } [ p = pat() { list.add(p); } ( @@ -1179,7 +1195,7 @@ Pat atomPat() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); } [ p = pat() { list.add(p); } @@ -1192,7 +1208,7 @@ Pat atomPat() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); final Map map = new LinkedHashMap<>(); boolean ellipsis = false; } @@ -1222,7 +1238,7 @@ void recordPat(Map map) : ( pat = pat() { map.put(id, pat); } | - { map.put(id, ast.idPat(getPos(), id)); } + { map.put(id, ast.idPat(pos(), id)); } ) } @@ -1240,7 +1256,7 @@ Ast.Type atomicType() : type = recordType() { return type; } | { - span = Span.of(getPos()); + span = Span.of(pos()); } type = type() ( @@ -1545,17 +1561,15 @@ MORE : ("$" (||"_")+)? > | - < IDENTIFIER: (|)* > + < IDENTIFIER: (||"_"|"'")* > | - < TY_VAR: "'" (|)* > + < TY_VAR: "'" (||"_"|"'")* > | - < LABEL: "#" (|)+ > + < LABEL: "#" (||"_"|"'")+ > | < #LETTER: [ - "\u0024", "\u0041"-"\u005a", - "\u005f", "\u0061"-"\u007a", "\u00c0"-"\u00d6", "\u00d8"-"\u00f6", diff --git a/src/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index 18d92aaa0..b0af24987 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -19,6 +19,9 @@ package net.hydromatic.morel; import net.hydromatic.morel.ast.Ast; +import net.hydromatic.morel.compile.CompileException; +import net.hydromatic.morel.eval.Codes; +import net.hydromatic.morel.eval.Prop; import net.hydromatic.morel.parse.ParseException; import net.hydromatic.morel.type.DataType; import net.hydromatic.morel.type.TypeVar; @@ -26,6 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.apache.calcite.util.Util; +import org.hamcrest.CustomTypeSafeMatcher; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -50,6 +54,9 @@ import static net.hydromatic.morel.Matchers.map; import static net.hydromatic.morel.Matchers.throwsA; import static net.hydromatic.morel.Matchers.whenAppliedTo; +import static net.hydromatic.morel.Ml.MatchCoverage.NON_EXHAUSTIVE; +import static net.hydromatic.morel.Ml.MatchCoverage.OK; +import static net.hydromatic.morel.Ml.MatchCoverage.REDUNDANT; import static net.hydromatic.morel.Ml.assertError; import static net.hydromatic.morel.Ml.ml; @@ -119,6 +126,14 @@ public class MainTest { ml("val x : int = 5") .assertParseDecl(Ast.ValDecl.class, "val x : int = 5"); + // other valid identifiers + ml("val x' = 5").assertParseDecl(Ast.ValDecl.class, "val x' = 5"); + ml("val x'' = 5").assertParseDecl(Ast.ValDecl.class, "val x'' = 5"); + ml("val x'y = 5").assertParseDecl(Ast.ValDecl.class, "val x'y = 5"); + ml("val ABC123 = 5").assertParseDecl(Ast.ValDecl.class, "val ABC123 = 5"); + ml("val Abc_123 = 6").assertParseDecl(Ast.ValDecl.class, "val Abc_123 = 6"); + ml("val Abc_ = 7").assertParseDecl(Ast.ValDecl.class, "val Abc_ = 7"); + ml("val succ = fn x => x + 1") .assertParseDecl(Ast.ValDecl.class, "val succ = fn x => x + 1"); @@ -233,6 +248,13 @@ public class MainTest { containsString( "Encountered \" \"rec\" \"rec \"\" at line 1, column 19.")); + // : is right-associative and low precedence + ml("1 : int : int").assertParseSame(); + ml("(2 : int) + 1 : int").assertParseSame(); + ml("(2 : int) + (1 : int) : int").assertParseSame(); + ml("((2 : int) + (1 : int)) : int") + .assertParse("(2 : int) + (1 : int) : int"); + // pattern ml("let val (x, y) = (1, 2) in x + y end").assertParseSame(); ml("let val w as (x, y) = (1, 2) in #1 w + #2 w + x + y end") @@ -364,6 +386,21 @@ public class MainTest { .assertParse("fn {a = a, b = 2, ...} => a + b"); } + @Test void testParseErrorPosition() { + ml("let val x = 1 and y = $x$ + 2 in x + y end", '$') + .assertEvalError(pos -> + throwsA("unbound variable or constructor: x", pos)); + } + + @Test void testRuntimeErrorPosition() { + ml("\"x\" ^\n" + + " $String.substring(\"hello\",\n" + + " 1, 15)$ ^\n" + + " \"y\"\n", '$') + .assertEvalError(pos -> + throwsA(Codes.BuiltInExn.SUBSCRIPT.mlName, pos)); + } + /** Tests the name of {@link TypeVar}. */ @Test void testTypeVarName() { assertError(() -> new TypeVar(-1).key(), @@ -426,6 +463,10 @@ public class MainTest { ml("fn x => case x of 0 => 1 | _ => 2").assertType("int -> int"); ml("fn x => case x of 0 => \"zero\" | _ => \"nonzero\"") .assertType("int -> string"); + ml("fn x: int => true").assertType("int -> bool"); + ml("fn x: int * int => true").assertType("int * int -> bool"); + ml("fn x: int * string => (false, #2 x)") + .assertType("int * string -> bool * string"); } @Test void testTypeFnTuple() { @@ -733,8 +774,8 @@ public class MainTest { // 'and' is executed in parallel, therefore 'x + 1' evaluates to 2, not 4 ml("let val x = 1; val x = 3 and y = x + 1 in x + y end").assertEval(is(5)); - ml("let val x = 1 and y = x + 2 in x + y end") - .assertEvalError(throwsA("unbound variable or constructor: x")); + ml("let val x = 1 and y = $x$ + 2 in x + y end", '$') + .assertEvalError(pos -> throwsA("unbound variable or constructor: x")); // let with val and fun ml("let fun f x = 1 + x; val x = 2 in f x end").assertEval(is(3)); @@ -785,7 +826,9 @@ public class MainTest { + "in\n" + " y + x1 + x2 + 3\n" + "end"; - ml(ml).assertEval(is(11)); + ml(ml) + .with(Prop.MATCH_COVERAGE_ENABLED, false) + .assertEval(is(11)); } /** Tests that 'and' assignments occur simultaneously. */ @@ -1191,6 +1234,137 @@ public class MainTest { ml(ml).assertEval(is(7)); } + /** + *

The algorithm is described in + * Stack overflow and in Lennart Augustsson's 1985 paper "Compiling + * Pattern Matching". + */ + @Test void testMatchRedundant() { + final String ml = "fun f x = case x > 0 of\n" + + " true => \"positive\"\n" + + " | false => \"non-positive\"\n" + + " | $true => \"oops\"$"; + ml(ml, '$') + .assertMatchCoverage(REDUNDANT) + .assertEvalThrows(pos -> throwsA("match redundant", pos)); + + // similar, but 'fun' rather than 'case' + final String ml2 = "" + + "fun f true = \"positive\"\n" + + " | f false = \"non-positive\"\n" + + " | $f true = \"oops\"$"; + ml(ml2, '$') + .assertMatchCoverage(REDUNDANT) + .assertEvalThrows(pos -> throwsA("match redundant", pos)); + } + + @Test void testMatchCoverage1() { + final String ml = "fun f (x, y) = x + y + 1"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage2() { + final String ml = "" + + "fun f (1, y) = y\n" + + " | f (x, 2) = x\n" + + " | f (x, y) = x + y + 1"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage3() { + final String ml = "fun f 1 = 2 | f x = x + 3"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage4() { + final String ml = "" + + "fun f 1 = 2\n" + + " | f _ = 1"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage5() { + final String ml = "" + + "fun f [] = 0\n" + + " | f (h :: t) = 1 + (f t)"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage6() { + final String ml = "" + + "fun f (0, y) = y\n" + + " | f (x, y) = x + y + 1"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage7() { + final String ml = "" + + "fun f (x, y, 0) = y\n" + + " | f (x, y, z) = x + z"; + ml(ml).assertMatchCoverage(OK); + } + + @Test void testMatchCoverage8() { + // The last case is redundant because we know that bool has two values. + final String ml = "" + + "fun f (true, y, z) = y\n" + + " | f (false, y, z) = z\n" + + " | $f _ = 0$"; + ml(ml, '$') + .assertMatchCoverage(REDUNDANT) + .assertEvalError(pos -> throwsA("match redundant", pos)); + } + + @Test void testMatchCoverage9() { + // The last case is redundant because we know that unit has only one value. + final String ml = "" + + "fun f () = 1\n" + + " | $f _ = 0$"; + ml(ml, '$').assertMatchCoverage(REDUNDANT); + } + + @Test void testMatchCoverage10() { + final String ml = "fun maskToString m =\n" + + " let\n" + + " fun maskToString2 (m, s, 0) = s\n" + + " | maskToString2 (m, s, k) =\n" + + " maskToString2 (m div 3,\n" + + " ($case (m mod 3) of\n" + + " 0 => \"b\"\n" + + " | 1 => \"y\"\n" + + " | 2 => \"g\"$) ^ s,\n" + + " k - 1)\n" + + " in\n" + + " maskToString2 (m, \"\", 5)\n" + + " end"; + ml(ml, '$') + .assertMatchCoverage(NON_EXHAUSTIVE); + } + + @Test void testMatchCoverage12() { + // two "match nonexhaustive" warnings + final String ml = "fun f x =\n" + + " let\n" + + " fun g 1 = 1\n" + + " and h 2 = 2\n" + + " in\n" + + " (g x) + (h 2)\n" + + " end"; + ml(ml) + .assertMatchCoverage(NON_EXHAUSTIVE) + .assertEvalWarnings( + new CustomTypeSafeMatcher>( + "two warnings") { + @Override protected boolean matchesSafely(List list) { + return list.size() == 2 + && list.get(0) instanceof CompileException + && list.get(0).getMessage().equals("match nonexhaustive") + && list.get(1) instanceof CompileException + && list.get(1).getMessage().equals("match nonexhaustive"); + } + }); + } + /** Function declaration. */ @Test void testFun() { final String ml = "let\n" diff --git a/src/test/java/net/hydromatic/morel/Matchers.java b/src/test/java/net/hydromatic/morel/Matchers.java index 7b4df92e4..1871533a3 100644 --- a/src/test/java/net/hydromatic/morel/Matchers.java +++ b/src/test/java/net/hydromatic/morel/Matchers.java @@ -21,11 +21,13 @@ import net.hydromatic.morel.ast.Ast; import net.hydromatic.morel.ast.AstNode; import net.hydromatic.morel.ast.AstWriter; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.eval.Applicable; import net.hydromatic.morel.eval.Code; import net.hydromatic.morel.eval.Codes; import net.hydromatic.morel.type.DataType; import net.hydromatic.morel.type.Type; +import net.hydromatic.morel.util.MorelException; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.Lists; @@ -44,7 +46,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import javax.annotation.Nullable; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -97,7 +101,7 @@ private String stringValue(T t) { }; } - /** Matches an Code node by its string representation. */ + /** Matches a Code node by its string representation. */ static Matcher isCode(String expected) { return new CustomTypeSafeMatcher("code " + expected) { @Override protected boolean matchesSafely(Code code) { @@ -178,7 +182,7 @@ static Matcher isUnordered(E expected) { @Override protected boolean matchesSafely(E actual) { final E actualMultiset = expectedMultiset instanceof Multiset - && (actual instanceof Iterable) + && actual instanceof Iterable && !(actual instanceof Multiset) ? (E) ImmutableMultiset.copyOf((Iterable) actual) : actual; @@ -195,6 +199,24 @@ static Matcher throwsA(String message) { }; } + static Matcher throwsA(String message, Pos position) { + return new CustomTypeSafeMatcher("throwable [" + message + + "] at position [" + position + "]") { + @Override protected boolean matchesSafely(Throwable item) { + return item.toString().contains(message) + && Objects.equals(positionString(item), position.toString()); + } + + @Nullable String positionString(Throwable e) { + if (e instanceof MorelException) { + return ((MorelException) e).pos() + .describeTo(new StringBuilder()).toString(); + } + return null; + } + }; + } + static Matcher throwsA(Class clazz, Matcher messageMatcher) { return new CustomTypeSafeMatcher(clazz + " with message " diff --git a/src/test/java/net/hydromatic/morel/Ml.java b/src/test/java/net/hydromatic/morel/Ml.java index 37197ccfa..90a688d76 100644 --- a/src/test/java/net/hydromatic/morel/Ml.java +++ b/src/test/java/net/hydromatic/morel/Ml.java @@ -21,8 +21,10 @@ import net.hydromatic.morel.ast.Ast; import net.hydromatic.morel.ast.AstNode; import net.hydromatic.morel.ast.Core; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.Analyzer; import net.hydromatic.morel.compile.CalciteCompiler; +import net.hydromatic.morel.compile.CompileException; import net.hydromatic.morel.compile.CompiledStatement; import net.hydromatic.morel.compile.Compiles; import net.hydromatic.morel.compile.Environment; @@ -48,6 +50,9 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.apache.calcite.plan.RelOptUtil; import org.apache.calcite.rel.RelNode; +import org.apache.calcite.util.Pair; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Matcher; import java.io.StringReader; @@ -57,7 +62,7 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; -import javax.annotation.Nullable; +import java.util.function.Function; import static net.hydromatic.morel.Matchers.hasMoniker; import static net.hydromatic.morel.Matchers.isAst; @@ -74,19 +79,27 @@ /** Fluent test helper. */ class Ml { private final String ml; + @Nullable private final Pos pos; private final Map dataSetMap; private final Map propMap; - Ml(String ml, Map dataSetMap, + Ml(String ml, @Nullable Pos pos, Map dataSetMap, Map propMap) { this.ml = ml; + this.pos = pos; this.dataSetMap = ImmutableMap.copyOf(dataSetMap); this.propMap = ImmutableMap.copyOf(propMap); } /** Creates an {@code Ml}. */ static Ml ml(String ml) { - return new Ml(ml, ImmutableMap.of(), ImmutableMap.of()); + return new Ml(ml, null, ImmutableMap.of(), ImmutableMap.of()); + } + + /** Creates an {@code Ml} with an error position in it. */ + static Ml ml(String ml, char delimiter) { + Pair pair = Pos.split(ml, delimiter, "stdIn"); + return new Ml(pair.left, pair.right, ImmutableMap.of(), ImmutableMap.of()); } /** Runs a task and checks that it throws an exception. @@ -193,6 +206,7 @@ Ml assertParseThrows(Matcher matcher) { private Ml withValidate(BiConsumer action) { return withParser(parser -> { try { + parser.zero("stdIn"); final AstNode statement = parser.statementEof(); final Calcite calcite = Calcite.withDataSets(dataSetMap); final TypeResolver.Resolved resolved = @@ -213,6 +227,10 @@ Ml assertType(String expected) { return assertType(hasMoniker(expected)); } + Ml assertTypeThrows(Function> matcherSupplier) { + return assertTypeThrows(matcherSupplier.apply(pos)); + } + Ml assertTypeThrows(Matcher matcher) { assertError(() -> withValidate((resolved, calcite) -> @@ -228,9 +246,10 @@ Ml withPrepare(Consumer action) { final AstNode statement = parser.statementEof(); final Environment env = Environments.empty(); final Session session = new Session(); + final List warningList = new ArrayList<>(); final CompiledStatement compiled = Compiles.prepareStatement(typeSystem, session, env, statement, - null); + null, warningList::add); action.accept(compiled); } catch (ParseException e) { throw new RuntimeException(e); @@ -351,8 +370,54 @@ Ml assertAnalyze(Matcher matcher) { return this; } + Ml assertMatchCoverage(MatchCoverage expectedCoverage) { + final Function> exceptionMatcherFactory; + final Matcher> warningsMatcher; + switch (expectedCoverage) { + case OK: + // Expect no errors or warnings + exceptionMatcherFactory = null; + warningsMatcher = isEmptyList(); + break; + case REDUNDANT: + exceptionMatcherFactory = pos -> throwsA("match redundant", pos); + warningsMatcher = isEmptyList(); + break; + case NON_EXHAUSTIVE_AND_REDUNDANT: + exceptionMatcherFactory = pos -> + throwsA("match nonexhaustive and redundant", pos); + warningsMatcher = isEmptyList(); + break; + case NON_EXHAUSTIVE: + exceptionMatcherFactory = null; + warningsMatcher = + new CustomTypeSafeMatcher>("non-empty list") { + @Override protected boolean matchesSafely(List list) { + return list.stream() + .anyMatch(e -> + e instanceof CompileException + && e.getMessage().equals("match nonexhaustive")); + } + }; + break; + default: + // Java doesn't know the switch is exhaustive; how ironic + throw new AssertionError(expectedCoverage); + } + return assertEval(notNullValue(), null, exceptionMatcherFactory, + warningsMatcher); + } + + private static Matcher> isEmptyList() { + return new CustomTypeSafeMatcher>("empty list") { + @Override protected boolean matchesSafely(List list) { + return list.isEmpty(); + } + }; + } + Ml assertPlan(Matcher planMatcher) { - return assertEval(null, planMatcher); + return assertEval(null, planMatcher, null, null); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -361,27 +426,58 @@ Ml assertEvalIter(Matcher> matcher) { } Ml assertEval(Matcher resultMatcher) { - return assertEval(resultMatcher, null); + return assertEval(resultMatcher, null, null, null); } - Ml assertEval(Matcher resultMatcher, Matcher planMatcher) { + Ml assertEval(@Nullable Matcher resultMatcher, + @Nullable Matcher planMatcher, + @Nullable Function> exceptionMatcherFactory, + @Nullable Matcher> warningsMatcher) { + final Matcher exceptionMatcher = + exceptionMatcherFactory == null + ? null + : exceptionMatcherFactory.apply(pos); return withValidate((resolved, calcite) -> { final Session session = new Session(); session.map.putAll(propMap); eval(session, resolved.env, resolved.typeMap.typeSystem, resolved.node, - calcite, resultMatcher, planMatcher); + calcite, resultMatcher, planMatcher, exceptionMatcher, + warningsMatcher); }); } + Ml assertEvalThrows( + Function> exceptionMatcherFactory) { + return assertEval(null, null, exceptionMatcherFactory, null); + } + @CanIgnoreReturnValue - private Object eval(Session session, Environment env, + private Object eval(Session session, Environment env, TypeSystem typeSystem, AstNode statement, Calcite calcite, @Nullable Matcher resultMatcher, - @Nullable Matcher planMatcher) { - CompiledStatement compiledStatement = - Compiles.prepareStatement(typeSystem, session, env, statement, calcite); + @Nullable Matcher planMatcher, + @Nullable Matcher exceptionMatcher, + @Nullable Matcher> warningsMatcher) { final List bindings = new ArrayList<>(); - compiledStatement.eval(session, env, line -> {}, bindings::add); + final List warningList = new ArrayList<>(); + try { + CompiledStatement compiledStatement = + Compiles.prepareStatement(typeSystem, session, env, statement, + calcite, warningList::add); + session.withoutHandlingExceptions(session1 -> + compiledStatement.eval(session1, env, line -> {}, bindings::add)); + if (exceptionMatcher != null) { + fail("expected exception, but none was thrown"); + } + } catch (Throwable e) { + if (exceptionMatcher == null) { + throw e; + } + assertThat(e, exceptionMatcher); + } + if (warningsMatcher != null) { + assertThat(warningList, warningsMatcher); + } final Object result; if (statement instanceof Ast.Exp) { result = bindingValue(bindings, "it"); @@ -415,7 +511,9 @@ private Object bindingValue(List bindings, String name) { return null; } - Ml assertEvalError(Matcher matcher) { + Ml assertEvalError(Function> matcherSupplier) { + assertThat(pos, notNullValue()); + final Matcher matcher = matcherSupplier.apply(pos); try { assertEval(notNullValue()); fail("expected error"); @@ -425,6 +523,10 @@ Ml assertEvalError(Matcher matcher) { return this; } + Ml assertEvalWarnings(Matcher> warningsMatcher) { + return assertEval(notNullValue(), null, null, warningsMatcher); + } + Ml assertEvalSame() { final Matchers.LearningMatcher resultMatcher = Matchers.learning(Object.class); @@ -444,11 +546,11 @@ Ml assertError(String expected) { } Ml withBinding(String name, DataSet dataSet) { - return new Ml(ml, plus(dataSetMap, name, dataSet), propMap); + return new Ml(ml, pos, plus(dataSetMap, name, dataSet), propMap); } Ml with(Prop prop, Object value) { - return new Ml(ml, dataSetMap, plus(propMap, prop, value)); + return new Ml(ml, pos, dataSetMap, plus(propMap, prop, value)); } /** Returns a map plus (adding or overwriting) one (key, value) entry. */ @@ -466,6 +568,16 @@ private static Map plus(Map map, K k, V v) { builder.put(k, v); return builder.build(); } + + /** Whether a list of patterns is exhaustive (covers all possible input + * values), redundant (covers some input values more than once), both or + * neither. */ + enum MatchCoverage { + NON_EXHAUSTIVE, + REDUNDANT, + NON_EXHAUSTIVE_AND_REDUNDANT, + OK + } } // End Ml.java diff --git a/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java b/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java new file mode 100644 index 000000000..4bf5da78f --- /dev/null +++ b/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java @@ -0,0 +1,37 @@ +package net.hydromatic.morel; + +import net.hydromatic.morel.foreign.Calcite; +import net.hydromatic.morel.foreign.ForeignValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +class ProgrammaticShellTest { + + private final Map foreignValueMap = + Calcite.withDataSets(BuiltInDataSet.DICTIONARY).foreignValues(); + + @Test + void run() { + ProgrammaticShell shell = new ProgrammaticShell(foreignValueMap); + + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + + String expected = "val it = {bonus=,dept=,emp=,salgrade=}\n" + + " : {bonus:{comm:real, ename:string, job:string, sal:real} list, dept:{deptno:int, dname:string, loc:string} list, emp:{comm:real, deptno:int, empno:int, ename:string, hiredate:string, job:string, mgr:int, sal:real} list, salgrade:{grade:int, hisal:real, losal:real} list}\n"; + + shell.run("scott;", writer, false); + writer.flush(); + + System.out.println("Out: " + out); + + // Handle CRLF vs LF differences so that test passes on Windows. + String expectedNormalized = expected.replaceAll("\\s+", " ").trim(); + String outNormalized = out.toString().replaceAll("\\s+", " ").trim(); + Assertions.assertEquals(expectedNormalized, outNormalized); + } +} diff --git a/src/test/java/net/hydromatic/morel/SatTest.java b/src/test/java/net/hydromatic/morel/SatTest.java new file mode 100644 index 000000000..a16a9e968 --- /dev/null +++ b/src/test/java/net/hydromatic/morel/SatTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package net.hydromatic.morel; + +import net.hydromatic.morel.util.Sat; +import net.hydromatic.morel.util.Sat.Term; +import net.hydromatic.morel.util.Sat.Variable; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +/** Tests satisfiability. */ +public class SatTest { + /** Tests a formula with three clauses, three terms each. + * It is in "3SAT" form, and has a solution (i.e. is satisfiable). */ + @Test void testBuild() { + final Sat sat = new Sat(); + final Variable x = sat.variable("x"); + final Variable y = sat.variable("y"); + + // (x ∨ x ∨ y) ∧ (¬x ∨ ¬y ∨ ¬y) ∧ (¬x ∨ y ∨ y) + final Term clause0 = sat.or(x, x, y); + final Term clause1 = sat.or(sat.not(x), sat.not(y), sat.not(y)); + final Term clause2 = sat.or(sat.not(x), y, y); + final Term formula = sat.and(clause0, clause1, clause2); + assertThat(formula.toString(), + is("(x ∨ x ∨ y) ∧ (¬x ∨ ¬y ∨ ¬y) ∧ (¬x ∨ y ∨ y)")); + + final Map solution = sat.solve(formula); + assertThat(solution, notNullValue()); + assertThat(solution.toString(), is("{x=false, y=true}")); + } + + /** Tests true ("and" with zero arguments). */ + @Test void testTrue() { + final Sat sat = new Sat(); + final Term trueTerm = sat.and(); + assertThat(trueTerm.toString(), is("true")); + + final Map solve = sat.solve(trueTerm); + assertThat("satisfiable", solve, notNullValue()); + assertThat(solve.isEmpty(), is(true)); + } + + /** Tests false ("or" with zero arguments). */ + @Test void testFalse() { + final Sat sat = new Sat(); + final Term falseTerm = sat.or(); + assertThat(falseTerm.toString(), is("false")); + + final Map solve = sat.solve(falseTerm); + assertThat("not satisfiable", solve, nullValue()); + } + +} + +// End SatTest.java diff --git a/src/test/java/net/hydromatic/morel/ShellTest.java b/src/test/java/net/hydromatic/morel/ShellTest.java index 8f07fc5e0..699ceb98b 100644 --- a/src/test/java/net/hydromatic/morel/ShellTest.java +++ b/src/test/java/net/hydromatic/morel/ShellTest.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; @@ -104,11 +105,12 @@ static File getUseDirectory() { + "\u001B[?2004lval it = 3 : int\r\n" + "- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); } /** Tests {@link Shell} with a continued line. */ @Test void testTwoLines() { + assumeNotInCi(); final String in = "1 +\n" + "2;\n"; final String expected = "1 +\r\n" @@ -118,7 +120,7 @@ static File getUseDirectory() { + "\u001B[?2004lval it = 3 : int\r\n" + "- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); } /** Tests {@link Shell} printing some tricky real values. */ @@ -145,7 +147,12 @@ static File getUseDirectory() { + "\u001B[?2004l- ;\r\r\n" + "\u001B[?2004l- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); + } + + private Matcher is2(String expected) { + return CoreMatchers.anyOf(is(expected), + is(expected.replace("\u001B[?2004l", ""))); } /** Tests {@link Shell} with a single-line comment. */ @@ -160,7 +167,7 @@ static File getUseDirectory() { + "\u001B[?2004lval it = 3 : int\r\n" + "- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); } /** Tests {@link Shell} with a single-line comment that contains a quote. */ @@ -175,7 +182,7 @@ static File getUseDirectory() { + "\u001B[?2004lval it = 5 : int\r\n" + "- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); } /** Tests {@link Shell} with {@code let} statement spread over multiple @@ -200,7 +207,7 @@ static File getUseDirectory() { + "\u001B[?2004lval it = 3 : int\r\n" + "- \r\r\n" + "\u001B[?2004l"; - fixture().withInputString(in).assertOutput(is(expected)); + fixture().withInputString(in).assertOutput(is2(expected)); } /** Tests the {@code use} function. */ @@ -254,7 +261,7 @@ static File getUseDirectory() { fixture() .withArgListPlusDirectory() .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); final String expectedRaw = "[opening x.sml]\n" + "val x = 2 : int\n" @@ -274,6 +281,20 @@ static File getUseDirectory() { .assertOutput(is(expectedRaw)); } + /** Tests a warning. */ + @Test void testMatchWarning() { + final String in = "fun f 1 = 1;\n" + + "f 1;\n"; + final String expected = "stdIn:1.5-1.12 Warning: match nonexhaustive\n" + + " raised at: stdIn:1.5-1.12\n" + + "val f = fn : int -> int\n" + + "val it = 1 : int\n"; + fixture() + .withRaw(true) + .withInputString(in) + .assertOutput(is(expected)); + } + /** Tests the {@code use} function on an empty file. */ @Test void testUseEmpty() { assumeNotInCi(); @@ -287,7 +308,7 @@ static File getUseDirectory() { fixture() .withArgListPlusDirectory() .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); } /** Tests the {@code use} function on a missing file. */ @@ -307,12 +328,13 @@ static File getUseDirectory() { + "[use failed: Io: openIn failed on missing.sml," + " No such file or directory]\r\n" + "uncaught exception Error\r\n" + + " raised at: stdIn:1.1-1.18\r\n" + "- \r\r\n" + "\u001B[?2004l"; fixture() .withArgListPlusDirectory() .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); } /** Tests the {@code use} function on a file that uses itself. */ @@ -335,6 +357,7 @@ static File getUseDirectory() { + "[use failed: Io: openIn failed on self-referential.sml," + " Too many open files]\r\n" + "uncaught exception Error\r\n" + + " raised at: stdIn:1.1-1.27\r\n" + "val it = () : unit\r\n" + "- \r\r\n" + "\u001B[?2004l"; @@ -342,7 +365,7 @@ static File getUseDirectory() { .withArgListPlusDirectory() .withArgList(list -> plus(list, "--maxUseDepth=3")) .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); } /** Tests a script running in raw mode. diff --git a/src/test/java/net/hydromatic/morel/UtilTest.java b/src/test/java/net/hydromatic/morel/UtilTest.java index 00fb013f2..b424648aa 100644 --- a/src/test/java/net/hydromatic/morel/UtilTest.java +++ b/src/test/java/net/hydromatic/morel/UtilTest.java @@ -27,6 +27,7 @@ import net.hydromatic.morel.util.TailList; import org.apache.calcite.util.ImmutableIntList; +import org.apache.calcite.util.Pair; import org.apache.calcite.util.Util; import org.junit.jupiter.api.Test; @@ -37,13 +38,17 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import static net.hydromatic.morel.ast.AstBuilder.ast; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.fail; /** Tests for various utility classes. */ public class UtilTest { @@ -168,6 +173,60 @@ private void checkShorterThan(Iterable iterable, int size) { assertThat(Static.shorterThan(iterable, 4), is(size < 4)); assertThat(Static.shorterThan(iterable, 1_000_000), is(size < 1_000_000)); } + + /** Unit tests for {@link Pos}. */ + @Test void testPos() { + final BiConsumer check = (s, posString) -> { + final Pair pos = Pos.split(s, '$', "stdIn"); + assertThat(pos.left, is("abcdefgh")); + assertThat(pos.right, notNullValue()); + assertThat(pos.right.toString(), is(posString)); + }; + // starts and ends in middle + check.accept("abc$def$gh", "stdIn:1.4-1.7"); + // ends at end + check.accept("abc$defgh$", "stdIn:1.4-1.9"); + // starts at start + check.accept("$abc$defgh", "stdIn:1.1-1.4"); + // one character long + check.accept("abc$d$efgh", "stdIn:1.4"); + + final BiConsumer check2 = (s, posString) -> { + final Pair pos = Pos.split(s, '$', "stdIn"); + assertThat(pos.left, + is("abc\n" + + "de\n" + + "\n" + + "fgh")); + assertThat(pos.right, notNullValue()); + assertThat(pos.right.toString(), is(posString)); + }; + // start of line + check2.accept("abc\n" + + "$de$\n" + + "\n" + + "fgh", "stdIn:2.1-2.3"); + // spans multiple lines + check2.accept("abc\n" + + "d$e\n" + + "\n" + + "fg$h", "stdIn:2.2-4.3"); + + // too many, too few + Consumer checkTooFew = s -> { + try { + final Pair pos4 = Pos.split(s, '$', "stdIn"); + fail("expected error, got " + pos4); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), + is("expected exactly two occurrences of delimiter, '$'")); + } + }; + checkTooFew.accept("$abc$de$f"); + checkTooFew.accept("abc$def"); + checkTooFew.accept("abcdef"); + } + } // End UtilTest.java diff --git a/src/test/resources/script/blog.sml b/src/test/resources/script/blog.sml index e4532f939..3b6238358 100644 --- a/src/test/resources/script/blog.sml +++ b/src/test/resources/script/blog.sml @@ -460,9 +460,10 @@ wc_reducer ("hello", [1, 4, 2]); (*) Bind them to mapReduce, and run fun wordCount lines = mapReduce wc_mapper wc_reducer lines; -wordCount ["a skunk sat on a stump", +from p in wordCount ["a skunk sat on a stump", "and thunk the stump stunk", - "but the stump thunk the skunk stunk"]; + "but the stump thunk the skunk stunk"] +order p.word; (*) WordCount in Morel val lines = ["a skunk sat on a stump", @@ -478,7 +479,8 @@ fun split s = end; from line in lines, word in split line - group word compute count; + group word compute count + order word; (*) A more complete solution fun wordCount lines = @@ -492,7 +494,7 @@ fun wordCount lines = word in split line group word compute count end; -wordCount lines; +from p in wordCount lines order p.word; (*) === Aggregate functions ========================================= @@ -500,14 +502,16 @@ val emps = scott.emp; val depts = scott.dept; from e in emps - group e.deptno compute sumSal = sum of e.sal; + group e.deptno compute sumSal = sum of e.sal + order deptno; from e in emps, d in depts where e.deptno = d.deptno group e.deptno, d.dname, e.job compute sumSal = sum of e.sal, - minRemuneration = min of e.sal + e.comm; + minRemuneration = min of e.sal + e.comm + order deptno, job; (*) In this example, we define our own version of the `sum` function: let @@ -517,12 +521,14 @@ in from e in emps group e.deptno compute sumEmpno = my_sum of e.empno + order deptno end; (*) The equivalent of SQL's COLLECT aggregate function is trivial from e in emps group e.deptno - compute names = (fn x => x) of e.ename; + compute names = (fn x => x) of e.ename + order deptno; (*) === StrangeLoop 2021 talk ======================================= (*) Standard ML: values @@ -625,10 +631,12 @@ in from line in lines, word in split line group word compute c = count + order word end; -wordCount ["a skunk sat on a stump", +from p in wordCount ["a skunk sat on a stump", "and thunk the stump stunk", - "but the stump thunk the skunk stunk"]; + "but the stump thunk the skunk stunk"] +order p.word; (*) Functions as views, functions as values fun emps2 () = @@ -710,10 +718,11 @@ fun wc_mapper line = fun wc_reducer (key, values) = List.foldl (fn (x, y) => x + y) 0 values; val wordCount = mapReduce wc_mapper wc_reducer; -wordCount lines; +from p in wordCount lines order p.k; from line in lines, word in split line - group word compute c = count; + group word compute c = count + order word; (*) === Coda ======================================================== from message in ["the end"]; diff --git a/src/test/resources/script/blog.sml.out b/src/test/resources/script/blog.sml.out index b03574d07..a6e0ac108 100644 --- a/src/test/resources/script/blog.sml.out +++ b/src/test/resources/script/blog.sml.out @@ -724,12 +724,11 @@ val it = 7 : int fun wordCount lines = mapReduce wc_mapper wc_reducer lines; val wordCount = fn : string list -> (string * int) list -wordCount ["a skunk sat on a stump", +from p in wordCount ["a skunk sat on a stump", "and thunk the stump stunk", - "but the stump thunk the skunk stunk"]; -val it = - [("but",1),("the",3),("stump",3),("thunk",2),("skunk",2),("stunk",2), - ("and",1),("a",2),("sat",1),("on",1)] : (string * int) list + "but the stump thunk the skunk stunk"] +order p.word; +java.lang.IllegalArgumentException: no field 'word' in type 'string * int' (*) WordCount in Morel @@ -752,12 +751,13 @@ val split = fn : string -> string list from line in lines, word in split line - group word compute count; + group word compute count + order word; val it = - [{count=2,word="a"},{count=3,word="the"},{count=1,word="but"}, - {count=1,word="sat"},{count=1,word="and"},{count=2,word="stunk"}, - {count=3,word="stump"},{count=1,word="on"},{count=2,word="thunk"}, - {count=2,word="skunk"}] : {count:int, word:string} list + [{count=2,word="a"},{count=1,word="and"},{count=1,word="but"}, + {count=1,word="on"},{count=1,word="sat"},{count=2,word="skunk"}, + {count=3,word="stump"},{count=2,word="stunk"},{count=3,word="the"}, + {count=2,word="thunk"}] : {count:int, word:string} list (*) A more complete solution @@ -774,12 +774,12 @@ fun wordCount lines = end; val wordCount = fn : string list -> {count:int, word:string} list -wordCount lines; +from p in wordCount lines order p.word; val it = - [{count=2,word="a"},{count=3,word="the"},{count=1,word="but"}, - {count=1,word="sat"},{count=1,word="and"},{count=2,word="stunk"}, - {count=3,word="stump"},{count=1,word="on"},{count=2,word="thunk"}, - {count=2,word="skunk"}] : {count:int, word:string} list + [{count=2,word="a"},{count=1,word="and"},{count=1,word="but"}, + {count=1,word="on"},{count=1,word="sat"},{count=2,word="skunk"}, + {count=3,word="stump"},{count=2,word="stunk"},{count=3,word="the"}, + {count=2,word="thunk"}] : {count:int, word:string} list (*) === Aggregate functions ========================================= @@ -793,9 +793,10 @@ val depts = : {deptno:int, dname:string, loc:string} list from e in emps - group e.deptno compute sumSal = sum of e.sal; + group e.deptno compute sumSal = sum of e.sal + order deptno; val it = - [{deptno=20,sumSal=10875.0},{deptno=10,sumSal=8750.0}, + [{deptno=10,sumSal=8750.0},{deptno=20,sumSal=10875.0}, {deptno=30,sumSal=9400.0}] : {deptno:int, sumSal:real} list @@ -804,22 +805,23 @@ from e in emps, where e.deptno = d.deptno group e.deptno, d.dname, e.job compute sumSal = sum of e.sal, - minRemuneration = min of e.sal + e.comm; + minRemuneration = min of e.sal + e.comm + order deptno, job; val it = [ - {deptno=30,dname="SALES",job="MANAGER",minRemuneration=2850.0,sumSal=2850.0}, - {deptno=20,dname="RESEARCH",job="CLERK",minRemuneration=800.0,sumSal=1900.0}, - {deptno=10,dname="ACCOUNTING",job="PRESIDENT",minRemuneration=5000.0, - sumSal=5000.0}, + {deptno=10,dname="ACCOUNTING",job="CLERK",minRemuneration=1300.0, + sumSal=1300.0}, {deptno=10,dname="ACCOUNTING",job="MANAGER",minRemuneration=2450.0, sumSal=2450.0}, + {deptno=10,dname="ACCOUNTING",job="PRESIDENT",minRemuneration=5000.0, + sumSal=5000.0}, {deptno=20,dname="RESEARCH",job="ANALYST",minRemuneration=3000.0, sumSal=6000.0}, - {deptno=30,dname="SALES",job="CLERK",minRemuneration=950.0,sumSal=950.0}, - {deptno=10,dname="ACCOUNTING",job="CLERK",minRemuneration=1300.0, - sumSal=1300.0}, + {deptno=20,dname="RESEARCH",job="CLERK",minRemuneration=800.0,sumSal=1900.0}, {deptno=20,dname="RESEARCH",job="MANAGER",minRemuneration=2975.0, sumSal=2975.0}, + {deptno=30,dname="SALES",job="CLERK",minRemuneration=950.0,sumSal=950.0}, + {deptno=30,dname="SALES",job="MANAGER",minRemuneration=2850.0,sumSal=2850.0}, {deptno=30,dname="SALES",job="SALESMAN",minRemuneration=1500.0, sumSal=5600.0}] : {deptno:int, dname:string, job:string, minRemuneration:real, sumSal:real} list @@ -833,19 +835,21 @@ in from e in emps group e.deptno compute sumEmpno = my_sum of e.empno + order deptno end; val it = - [{deptno=20,sumEmpno=38501},{deptno=10,sumEmpno=23555}, + [{deptno=10,sumEmpno=23555},{deptno=20,sumEmpno=38501}, {deptno=30,sumEmpno=46116}] : {deptno:int, sumEmpno:int} list (*) The equivalent of SQL's COLLECT aggregate function is trivial from e in emps group e.deptno - compute names = (fn x => x) of e.ename; + compute names = (fn x => x) of e.ename + order deptno; val it = - [{deptno=20,names=["SMITH","JONES","SCOTT","ADAMS","FORD"]}, - {deptno=10,names=["CLARK","KING","MILLER"]}, + [{deptno=10,names=["CLARK","KING","MILLER"]}, + {deptno=20,names=["SMITH","JONES","SCOTT","ADAMS","FORD"]}, {deptno=30,names=["ALLEN","WARD","MARTIN","BLAKE","TURNER","JAMES"]}] : {deptno:int, names:string list} list @@ -1037,20 +1041,22 @@ in from line in lines, word in split line group word compute c = count + order word end; val it = - [{c=2,word="a"},{c=3,word="the"},{c=1,word="but"},{c=1,word="sat"}, - {c=1,word="and"},{c=2,word="stunk"},{c=3,word="stump"},{c=1,word="on"}, - {c=2,word="thunk"},{c=2,word="skunk"}] : {c:int, word:string} list + [{c=2,word="a"},{c=1,word="and"},{c=1,word="but"},{c=1,word="on"}, + {c=1,word="sat"},{c=2,word="skunk"},{c=3,word="stump"},{c=2,word="stunk"}, + {c=3,word="the"},{c=2,word="thunk"}] : {c:int, word:string} list -wordCount ["a skunk sat on a stump", +from p in wordCount ["a skunk sat on a stump", "and thunk the stump stunk", - "but the stump thunk the skunk stunk"]; + "but the stump thunk the skunk stunk"] +order p.word; val it = - [{count=2,word="a"},{count=3,word="the"},{count=1,word="but"}, - {count=1,word="sat"},{count=1,word="and"},{count=2,word="stunk"}, - {count=3,word="stump"},{count=1,word="on"},{count=2,word="thunk"}, - {count=2,word="skunk"}] : {count:int, word:string} list + [{count=2,word="a"},{count=1,word="and"},{count=1,word="but"}, + {count=1,word="on"},{count=1,word="sat"},{count=2,word="skunk"}, + {count=3,word="stump"},{count=2,word="stunk"},{count=3,word="the"}, + {count=2,word="thunk"}] : {count:int, word:string} list (*) Functions as views, functions as values @@ -1240,19 +1246,20 @@ val wc_reducer = fn : 'a * int list -> int val wordCount = mapReduce wc_mapper wc_reducer; val wordCount = fn : string list -> {c:int, k:string} list -wordCount lines; +from p in wordCount lines order p.k; val it = - [{c=2,k="a"},{c=3,k="the"},{c=1,k="but"},{c=1,k="sat"},{c=1,k="and"}, - {c=2,k="stunk"},{c=3,k="stump"},{c=1,k="on"},{c=2,k="thunk"}, - {c=2,k="skunk"}] : {c:int, k:string} list + [{c=2,k="a"},{c=1,k="and"},{c=1,k="but"},{c=1,k="on"},{c=1,k="sat"}, + {c=2,k="skunk"},{c=3,k="stump"},{c=2,k="stunk"},{c=3,k="the"}, + {c=2,k="thunk"}] : {c:int, k:string} list from line in lines, word in split line - group word compute c = count; + group word compute c = count + order word; val it = - [{c=2,word="a"},{c=3,word="the"},{c=1,word="but"},{c=1,word="sat"}, - {c=1,word="and"},{c=2,word="stunk"},{c=3,word="stump"},{c=1,word="on"}, - {c=2,word="thunk"},{c=2,word="skunk"}] : {c:int, word:string} list + [{c=2,word="a"},{c=1,word="and"},{c=1,word="but"},{c=1,word="on"}, + {c=1,word="sat"},{c=2,word="skunk"},{c=3,word="stump"},{c=2,word="stunk"}, + {c=3,word="the"},{c=2,word="thunk"}] : {c:int, word:string} list (*) === Coda ======================================================== diff --git a/src/test/resources/script/builtIn.sml.out b/src/test/resources/script/builtIn.sml.out index d21c83faa..bf8827ca9 100644 --- a/src/test/resources/script/builtIn.sml.out +++ b/src/test/resources/script/builtIn.sml.out @@ -368,12 +368,15 @@ val it = #"c" : char String.sub("abc", 20); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.22 String.sub("abc", 3); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.21 String.sub("abc", ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.22 Sys.plan (); val it = "apply2(fnValue String.sub, constant(abc), constant(-1))" : string @@ -397,21 +400,27 @@ val it = "" : string String.extract("abc", 4, NONE); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.31 String.extract("abc", ~1, NONE); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.32 String.extract("abc", 4, SOME 2); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.33 String.extract("abc", ~1, SOME 2); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.34 String.extract("abc", 1, SOME ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.34 String.extract("abc", 1, SOME 99); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.34 Sys.plan (); val it = @@ -440,12 +449,15 @@ val it = "" : string String.substring("hello", ~1, 0); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.33 String.substring("hello", 1, ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.33 String.substring("hello", 1, 5); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.32 Sys.plan (); val it = @@ -764,6 +776,7 @@ val it = 1 : int List.hd []; uncaught exception Empty + raised at: stdIn:1.1-1.11 Sys.plan (); val it = "apply(fnValue List.hd, argCode tuple)" : string @@ -778,6 +791,7 @@ val it = [2,3] : int list List.tl []; uncaught exception Empty + raised at: stdIn:1.1-1.11 Sys.plan (); val it = "apply(fnValue List.tl, argCode tuple)" : string @@ -792,6 +806,7 @@ val it = 3 : int List.last []; uncaught exception Empty + raised at: stdIn:1.1-1.13 Sys.plan (); val it = "apply(fnValue List.last, argCode tuple)" : string @@ -823,9 +838,11 @@ val it = 1 : int List.nth ([1,2,3], 3); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.22 List.nth ([1,2,3], ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.23 Sys.plan (); val it = @@ -848,9 +865,11 @@ val it = [1,2,3] : int list List.take ([1,2,3], 4); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.23 List.take ([1,2,3], ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.24 Sys.plan (); val it = @@ -1158,6 +1177,7 @@ val it = [] : int list List.tabulate (~1, let fun fact n = if n = 0 then 1 else n * fact (n - 1) in fact end); uncaught exception Size + raised at: stdIn:1.1-1.87 Sys.plan (); val it = @@ -1643,6 +1663,7 @@ val it = 1 : int *) Option.valOf NONE; uncaught exception Option + raised at: stdIn:5.1-5.18 val noneInt = if true then NONE else SOME 0; val noneInt = NONE : int option @@ -1654,6 +1675,7 @@ val it = Option.valOf noneInt; uncaught exception Option + raised at: stdIn:1.1-1.21 Sys.plan (); val it = "apply(fnValue Option.valOf, argCode constant([NONE]))" : string @@ -2066,6 +2088,7 @@ val it = ~1 : int Real.sign nan; uncaught exception Domain + raised at: stdIn:1.1-1.14 Sys.plan (); val it = "apply(fnValue Real.sign, argCode constant(NaN))" : string @@ -2184,9 +2207,11 @@ val it = EQUAL : order Real.compare (Real.negInf, nan); uncaught exception Unordered + raised at: stdIn:1.1-1.32 Real.compare (nan, nan); uncaught exception Unordered + raised at: stdIn:1.1-1.24 Sys.plan (); val it = "apply2(fnValue Real.compare, constant(NaN), constant(NaN))" : string @@ -2658,9 +2683,11 @@ val it = 1.5 : real Real.checkFloat Real.posInf; uncaught exception Overflow + raised at: stdIn:1.1-1.28 Real.checkFloat Real.negInf; uncaught exception Overflow + raised at: stdIn:1.1-1.28 Real.checkFloat Real.minNormalPos; val it = 1.17549435E~38 : real @@ -2670,6 +2697,7 @@ val it = 1.4E~45 : real Real.checkFloat nan; uncaught exception Div + raised at: stdIn:1.1-1.20 (* "realFloor r", "realCeil r", "realTrunc r", "realRound r" convert real values @@ -3271,9 +3299,11 @@ val it = 2 : int Relational.only [1, 2, 3]; uncaught exception Size + raised at: stdIn:1.1-1.26 Relational.only []; uncaught exception Empty + raised at: stdIn:1.1-1.19 Sys.plan (); val it = "apply(fnValue Relational.only, argCode tuple)" : string @@ -3671,9 +3701,11 @@ val it = 9 : int Vector.sub (Vector.fromList [3,6,9], ~1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.41 Vector.sub (Vector.fromList [3,6,9], 3); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.40 Sys.plan (); val it = @@ -3690,9 +3722,11 @@ val it = #["a","baz","c"] : string vector Vector.update (Vector.fromList ["a","b","c"], ~1, "baz"); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.57 Vector.update (Vector.fromList ["a","b","c"], 3, "baz"); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.56 Sys.plan (); val it = diff --git a/src/test/resources/script/fixedPoint.sml b/src/test/resources/script/fixedPoint.sml index 1b86f8924..92498a83e 100644 --- a/src/test/resources/script/fixedPoint.sml +++ b/src/test/resources/script/fixedPoint.sml @@ -118,21 +118,21 @@ fun states_within x 0 = [x] group a); states_within "CA" 0; -states_within "CA" 1; -states_within "CA" 2; +from s in states_within "CA" 1 order s; +from s in states_within "CA" 2 order s; from s in states_within "CA" 2 group compute count; from s in states_within "CA" 3 group compute count; (* It takes 11 steps to reach to all 48 contiguous states plus DC. But it takes 2 minutes, so the following expression is disabled. See later, the same expression computed efficiently using semi-naive. *) if true then [49] else from s in states_within "CA" 11 group compute count; -states_within "HI" 0; -states_within "HI" 1; -states_within "HI" 100; -states_within "ME" 0; -states_within "ME" 1; -states_within "ME" 2; -states_within "ME" 3; (*) maine is not 3 steps from itself +from s in states_within "HI" 0 order s; +from s in states_within "HI" 1 order s; +from s in states_within "HI" 100 order s; +from s in states_within "ME" 0 order s; +from s in states_within "ME" 1 order s; +from s in states_within "ME" 2 order s; +from s in states_within "ME" 3 order s; (*) maine is not 3 steps from itself (*) Finding a square root using the Babylonian method (*) (An example of a scalar fixed-point query.) @@ -184,7 +184,7 @@ fun fixu_naive f a = else fixu_naive f a3 end; -fixu_naive prefixes ["cat", "dog", "", "car", "cart"]; +from p in fixu_naive prefixes ["cat", "dog", "", "car", "cart"] order p; (*) Fixed-point over union, with an iteration limit 'n'. (*) A semi-naive algorithm applies the function only to @@ -219,8 +219,8 @@ fun states_within2 s n = p in pairs where p.state = s group p.adjacent), [s], n); -states_within2 "CA" 1; -states_within2 "CA" 2; +from s in states_within2 "CA" 1 order s; +from s in states_within2 "CA" 2 order s; from s in states_within2 "CA" 8 group compute count; from s in states_within2 "CA" 9 group compute count; from s in states_within2 "CA" 10 group compute count; diff --git a/src/test/resources/script/fixedPoint.sml.out b/src/test/resources/script/fixedPoint.sml.out index 3910acc68..e0c9ddc97 100644 --- a/src/test/resources/script/fixedPoint.sml.out +++ b/src/test/resources/script/fixedPoint.sml.out @@ -220,11 +220,11 @@ val states_within = fn : string -> int -> string list states_within "CA" 0; val it = ["CA"] : string list -states_within "CA" 1; -val it = ["OR","NV","AZ"] : string list +from s in states_within "CA" 1 order s; +val it = ["AZ","NV","OR"] : string list -states_within "CA" 2; -val it = ["OR","NV","AZ","WA","ID","CO","CA","UT","NM"] : string list +from s in states_within "CA" 2 order s; +val it = ["AZ","CA","CO","ID","NM","NV","OR","UT","WA"] : string list from s in states_within "CA" 2 group compute count; val it = [9] : int list @@ -238,26 +238,26 @@ val it = [15] : int list if true then [49] else from s in states_within "CA" 11 group compute count; val it = [49] : int list -states_within "HI" 0; +from s in states_within "HI" 0 order s; val it = ["HI"] : string list -states_within "HI" 1; +from s in states_within "HI" 1 order s; val it = [] : string list -states_within "HI" 100; +from s in states_within "HI" 100 order s; val it = [] : string list -states_within "ME" 0; +from s in states_within "ME" 0 order s; val it = ["ME"] : string list -states_within "ME" 1; +from s in states_within "ME" 1 order s; val it = ["NH"] : string list -states_within "ME" 2; +from s in states_within "ME" 2 order s; val it = ["MA","ME","VT"] : string list -states_within "ME" 3; -val it = ["CT","MA","RI","NH","NY","VT"] : string list +from s in states_within "ME" 3 order s; +val it = ["CT","MA","NH","NY","RI","VT"] : string list (*) maine is not 3 steps from itself (*) Finding a square root using the Babylonian method @@ -340,8 +340,8 @@ fun fixu_naive f a = end; val fixu_naive = fn : ('a list -> 'a list) -> 'a list -> 'a list -fixu_naive prefixes ["cat", "dog", "", "car", "cart"]; -val it = ["cart","car","c","d","cat","dog","do","ca",""] : string list +from p in fixu_naive prefixes ["cat", "dog", "", "car", "cart"] order p; +val it = ["","c","ca","car","cart","cat","d","do","dog"] : string list (*) Fixed-point over union, with an iteration limit 'n'. @@ -383,11 +383,11 @@ fun states_within2 s n = group p.adjacent), [s], n); val states_within2 = fn : string -> int -> string list -states_within2 "CA" 1; -val it = ["OR","NV","AZ"] : string list +from s in states_within2 "CA" 1 order s; +val it = ["AZ","NV","OR"] : string list -states_within2 "CA" 2; -val it = ["OR","NV","AZ","WA","ID","CO","CA","UT","NM"] : string list +from s in states_within2 "CA" 2 order s; +val it = ["AZ","CA","CO","ID","NM","NV","OR","UT","WA"] : string list from s in states_within2 "CA" 8 group compute count; val it = [43] : int list diff --git a/src/test/resources/script/match.sml b/src/test/resources/script/match.sml new file mode 100644 index 000000000..f4f648c3d --- /dev/null +++ b/src/test/resources/script/match.sml @@ -0,0 +1,66 @@ +(* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + * + * Pattern matching + *) + +(*) Warning: match nonexhaustive +fun f 1 = 0; +f 1; +f 2; + +(*) Warning: match nonexhaustive, twice +fun f x = + let + fun g 1 = 1 + and h 2 = 2 + in + (g x) + (h 2) + end; +f 1; +f 2; + +(*) Error: match redundant and nonexhaustive +fun f 1 = 0 + | f 1 = 0; + +(*) OK +fun f 1 = 0 + | f _ = 1; +f 1; +f 2; + +(*) Error: match redundant +fun f (1, _) = 1 + | f (_, 2) = 2 + | f (1, 2) = 3 + | f (_, _) = 4; + +(*) The Ackermann-Péter function +(*) See "Recursion Equations as a Programming Language", D A Turner 1982 +fun ack 0 n = n + 1 + | ack m 0 = ack (m - 1) 1 + | ack m n = ack (m - 1) (ack m (n - 1)); +ack 0 0; +ack 0 1; +ack 1 0; +ack 1 2; +ack 2 3; +ack 3 3; + +(*) End match.sml diff --git a/src/test/resources/script/match.sml.out b/src/test/resources/script/match.sml.out new file mode 100644 index 000000000..f8330757a --- /dev/null +++ b/src/test/resources/script/match.sml.out @@ -0,0 +1,112 @@ +(* + * Licensed to Julian Hyde under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Julian Hyde licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + * + * Pattern matching + *) + +(*) Warning: match nonexhaustive +fun f 1 = 0; +stdIn:23.5-23.12 Warning: match nonexhaustive + raised at: stdIn:23.5-23.12 +val f = fn : int -> int + +f 1; +val it = 0 : int + +f 2; +uncaught exception Bind + raised at: stdIn:23.5-23.12 + + +(*) Warning: match nonexhaustive, twice +fun f x = + let + fun g 1 = 1 + and h 2 = 2 + in + (g x) + (h 2) + end; +stdIn:5.9-5.16 Warning: match nonexhaustive + raised at: stdIn:5.9-5.16 +stdIn:6.9-6.16 Warning: match nonexhaustive + raised at: stdIn:6.9-6.16 +val f = fn : int -> int + +f 1; +val it = 3 : int + +f 2; +uncaught exception Bind + raised at: stdIn:5.9-5.16 + + +(*) Error: match redundant and nonexhaustive +fun f 1 = 0 + | f 1 = 0; +stdIn:4.5-4.12 Error: match redundant and nonexhaustive + raised at: stdIn:4.5-4.12 + + +(*) OK +fun f 1 = 0 + | f _ = 1; +val f = fn : int -> int + +f 1; +val it = 0 : int + +f 2; +val it = 1 : int + + +(*) Error: match redundant +fun f (1, _) = 1 + | f (_, 2) = 2 + | f (1, 2) = 3 + | f (_, _) = 4; +stdIn:5.5-5.17 Error: match redundant + raised at: stdIn:5.5-5.17 + + +(*) The Ackermann-Péter function +(*) See "Recursion Equations as a Programming Language", D A Turner 1982 +fun ack 0 n = n + 1 + | ack m 0 = ack (m - 1) 1 + | ack m n = ack (m - 1) (ack m (n - 1)); +val ack = fn : int -> int -> int + +ack 0 0; +val it = 1 : int + +ack 0 1; +val it = 2 : int + +ack 1 0; +val it = 2 : int + +ack 1 2; +val it = 4 : int + +ack 2 3; +val it = 9 : int + +ack 3 3; +val it = 61 : int + + +(*) End match.sml diff --git a/src/test/resources/script/relational.sml b/src/test/resources/script/relational.sml index d9b37d1ef..6d2f22ac1 100644 --- a/src/test/resources/script/relational.sml +++ b/src/test/resources/script/relational.sml @@ -126,12 +126,14 @@ from e in emps (*) singleton record 'yield' followed by singleton 'group' from e in emps yield {d = e.deptno} - group d; + group d + order d; (*) singleton record 'yield' followed by 'group' from e in emps yield {d = e.deptno} - group d compute c = count; + group d compute c = count + order d; (*) singleton record 'yield' followed by 'order' from e in emps @@ -253,7 +255,8 @@ from e in emps, d in depts (*) join group where right variable is not referenced from e in emps, d in depts - group e.deptno compute count = sum of 1; + group e.deptno compute count = sum of 1 + order deptno; (*) join with intervening 'where' (*) we can't write ', d in depts' after 'where' @@ -343,7 +346,8 @@ from deptno in ( (from e in emps yield e.deptno) union (from d in depts yield d.deptno)) -group deptno; +group deptno +order deptno; (*) except (from d in depts yield d.deptno) @@ -386,8 +390,9 @@ intersect fun intersectDistinct l1 l2 = from v in l1 intersect l2 group v; -intersectDistinct (from e in emps yield e.deptno) - (from d in depts yield d.deptno); +from d in intersectDistinct (from e in emps yield e.deptno) + (from d in depts yield d.deptno) + order d; (*) simulate SQL's INTERSECT ALL fun intersectAll l1 l2 = @@ -407,8 +412,9 @@ fun intersectAll l1 l2 = units e.c end) yield e.v; -intersectAll (from e in emps yield e.deptno) - (from d in depts yield d.deptno); +from d in intersectAll (from e in emps yield e.deptno) + (from d in depts yield d.deptno) + order d; (*) union followed by group from x in (from e in emps yield e.deptno) @@ -457,25 +463,30 @@ end; from e in emps group deptno = e.deptno compute sum = sum of e.id, - count = count; + count = count +order deptno; (*) As previous, without the implied "deptno =" in "group", (*) and "sum =" and "count =" in "compute". from e in emps group e.deptno compute sum of e.id, - count; + count +order deptno; (*) 'group' with no aggregates from e in emps -group deptno = e.deptno; +group deptno = e.deptno +order deptno; from e in emps -group e.deptno; +group e.deptno +order deptno; (*) composite 'group' with no aggregates from e in emps -group e.deptno, idMod2 = e.id mod 2; +group e.deptno, idMod2 = e.id mod 2 +order deptno; (*) 'group' with empty key produces one output row from e in emps @@ -496,20 +507,23 @@ from e in emps where e.deptno < 30 group deptno = e.deptno compute sumId = sum of e.id, - sumIdPlusDeptno = sum of e.id + e.deptno; + sumIdPlusDeptno = sum of e.id + e.deptno +order deptno; (*) 'group' with 'exists' as an aggregate function from e in emps group e.deptno compute sumId = sum of e.id, existsId = exists of e.id, - existsStar = exists; + existsStar = exists +order deptno; (*) 'group' with record key (*) (useful if we want to refer to 'e' later in the pipeline) from e in emps group e = {e.deptno, odd = e.id mod 2 = 1} compute c = count -yield {e.deptno, c1 = c + 1}; +yield {e.deptno, c1 = c + 1} +order deptno; (*) 'group' with join from e in emps, d in depts @@ -547,8 +561,8 @@ let | siz (ht :: tl) = 1 + (siz tl) in from e in emps - group deptno = e.deptno - compute size = siz of e.id + group deptno = e.deptno compute size = siz of e.id + order deptno end; (*) as previous, but 'e' rather than 'e.id' @@ -557,8 +571,8 @@ let | siz (ht :: tl) = 1 + (siz tl) in from e in emps - group deptno = e.deptno - compute size = siz of e + group deptno = e.deptno compute size = siz of e + order deptno end; (*) user-defined aggregate function #3 @@ -567,8 +581,8 @@ let | my_sum (head :: tail) = head + (my_sum tail) in from e in emps - group e.deptno - compute my_sum of e.id + group e.deptno compute my_sum of e.id + order deptno end; (*) Identity aggregate function (equivalent to SQL's COLLECT) @@ -577,6 +591,7 @@ let in from e in emps group e.deptno compute rows = id of e + order deptno end; (*) Identity aggregate function, without 'of' @@ -585,23 +600,27 @@ let in from e in emps group e.deptno compute rows = id + order deptno end; (*) Identity aggregate function, using lambda from e in emps -group e.deptno compute rows = (fn x => x); +group e.deptno compute rows = (fn x => x) +order deptno; (*) Identity aggregate function with multiple input variables from e in emps, d in depts where e.deptno = d.deptno -group e.deptno compute rows = (fn x => x); +group e.deptno compute rows = (fn x => x) +order deptno; (*) Group followed by yield from e in emps group e.deptno compute sumId = sum of e.id, count = count of e -yield {deptno, avgId = sumId / count}; +yield {deptno, avgId = sumId / count} +order deptno; (*) Similar, using a sub-from: from g in ( @@ -609,7 +628,8 @@ from g in ( group e.deptno compute sumId = sum of e.id, count = count of e) -yield {g.deptno, avgId = g.sumId / g.count}; +yield {g.deptno, avgId = g.sumId / g.count} +order deptno; (*) Group followed by order and yield from e in emps @@ -647,13 +667,15 @@ from e in emps, from e in emps, d in depts where e.deptno = d.deptno - group d.deptno; + group d.deptno + order deptno; (*) Join followed by single group (from left input) from e in emps, d in depts where e.deptno = d.deptno - group e.deptno; + group e.deptno + order deptno; (*) Join followed by single group and order from e in emps, diff --git a/src/test/resources/script/relational.sml.out b/src/test/resources/script/relational.sml.out index f0713265e..23c41b6dd 100644 --- a/src/test/resources/script/relational.sml.out +++ b/src/test/resources/script/relational.sml.out @@ -229,15 +229,17 @@ val it = [{d=20},{d=30},{d=30}] : {d:int} list (*) singleton record 'yield' followed by singleton 'group' from e in emps yield {d = e.deptno} - group d; -val it = [20,10,30] : int list + group d + order d; +val it = [10,20,30] : int list (*) singleton record 'yield' followed by 'group' from e in emps yield {d = e.deptno} - group d compute c = count; -val it = [{c=1,d=20},{c=1,d=10},{c=2,d=30}] : {c:int, d:int} list + group d compute c = count + order d; +val it = [{c=1,d=10},{c=1,d=20},{c=2,d=30}] : {c:int, d:int} list (*) singleton record 'yield' followed by 'order' @@ -472,8 +474,9 @@ val it = [16] : int list (*) join group where right variable is not referenced from e in emps, d in depts - group e.deptno compute count = sum of 1; -val it = [{count=4,deptno=20},{count=4,deptno=10},{count=8,deptno=30}] + group e.deptno compute count = sum of 1 + order deptno; +val it = [{count=4,deptno=10},{count=4,deptno=20},{count=8,deptno=30}] : {count:int, deptno:int} list @@ -586,8 +589,9 @@ from deptno in ( (from e in emps yield e.deptno) union (from d in depts yield d.deptno)) -group deptno; -val it = [20,40,10,30] : int list +group deptno +order deptno; +val it = [10,20,30,40] : int list (*) except @@ -645,9 +649,10 @@ fun intersectDistinct l1 l2 = group v; val intersectDistinct = fn : 'a list -> 'a list -> 'a list -intersectDistinct (from e in emps yield e.deptno) - (from d in depts yield d.deptno); -val it = [20,10,30] : int list +from d in intersectDistinct (from e in emps yield e.deptno) + (from d in depts yield d.deptno) + order d; +val it = [10,20,30] : int list (*) simulate SQL's INTERSECT ALL @@ -670,9 +675,10 @@ fun intersectAll l1 l2 = yield e.v; val intersectAll = fn : 'a list -> 'a list -> 'a list -intersectAll (from e in emps yield e.deptno) - (from d in depts yield d.deptno); -val it = [20,10,30] : int list +from d in intersectAll (from e in emps yield e.deptno) + (from d in depts yield d.deptno) + order d; +val it = [10,20,30] : int list (*) union followed by group @@ -730,9 +736,10 @@ end; from e in emps group deptno = e.deptno compute sum = sum of e.id, - count = count; + count = count +order deptno; val it = - [{count=1,deptno=20,sum=101},{count=1,deptno=10,sum=100}, + [{count=1,deptno=10,sum=100},{count=1,deptno=20,sum=101}, {count=2,deptno=30,sum=205}] : {count:int, deptno:int, sum:int} list @@ -741,29 +748,33 @@ val it = from e in emps group e.deptno compute sum of e.id, - count; + count +order deptno; val it = - [{count=1,deptno=20,sum=101},{count=1,deptno=10,sum=100}, + [{count=1,deptno=10,sum=100},{count=1,deptno=20,sum=101}, {count=2,deptno=30,sum=205}] : {count:int, deptno:int, sum:int} list (*) 'group' with no aggregates from e in emps -group deptno = e.deptno; -val it = [20,10,30] : int list +group deptno = e.deptno +order deptno; +val it = [10,20,30] : int list from e in emps -group e.deptno; -val it = [20,10,30] : int list +group e.deptno +order deptno; +val it = [10,20,30] : int list (*) composite 'group' with no aggregates from e in emps -group e.deptno, idMod2 = e.id mod 2; +group e.deptno, idMod2 = e.id mod 2 +order deptno; val it = - [{deptno=30,idMod2=0},{deptno=30,idMod2=1},{deptno=10,idMod2=0}, - {deptno=20,idMod2=1}] : {deptno:int, idMod2:int} list + [{deptno=10,idMod2=0},{deptno=20,idMod2=1},{deptno=30,idMod2=0}, + {deptno=30,idMod2=1}] : {deptno:int, idMod2:int} list (*) 'group' with empty key produces one output row @@ -791,10 +802,11 @@ from e in emps where e.deptno < 30 group deptno = e.deptno compute sumId = sum of e.id, - sumIdPlusDeptno = sum of e.id + e.deptno; + sumIdPlusDeptno = sum of e.id + e.deptno +order deptno; val it = - [{deptno=20,sumId=101,sumIdPlusDeptno=121}, - {deptno=10,sumId=100,sumIdPlusDeptno=110}] + [{deptno=10,sumId=100,sumIdPlusDeptno=110}, + {deptno=20,sumId=101,sumIdPlusDeptno=121}] : {deptno:int, sumId:int, sumIdPlusDeptno:int} list @@ -803,10 +815,11 @@ from e in emps group e.deptno compute sumId = sum of e.id, existsId = exists of e.id, - existsStar = exists; + existsStar = exists +order deptno; val it = - [{deptno=20,existsId=true,existsStar=true,sumId=101}, - {deptno=10,existsId=true,existsStar=true,sumId=100}, + [{deptno=10,existsId=true,existsStar=true,sumId=100}, + {deptno=20,existsId=true,existsStar=true,sumId=101}, {deptno=30,existsId=true,existsStar=true,sumId=205}] : {deptno:int, existsId:bool, existsStar:bool, sumId:int} list @@ -815,8 +828,9 @@ val it = (*) (useful if we want to refer to 'e' later in the pipeline) from e in emps group e = {e.deptno, odd = e.id mod 2 = 1} compute c = count -yield {e.deptno, c1 = c + 1}; -val it = [{c1=2,deptno=30},{c1=2,deptno=30},{c1=2,deptno=10},{c1=2,deptno=20}] +yield {e.deptno, c1 = c + 1} +order deptno; +val it = [{c1=2,deptno=10},{c1=2,deptno=20},{c1=2,deptno=30},{c1=2,deptno=30}] : {c1:int, deptno:int} list @@ -876,10 +890,10 @@ let | siz (ht :: tl) = 1 + (siz tl) in from e in emps - group deptno = e.deptno - compute size = siz of e.id + group deptno = e.deptno compute size = siz of e.id + order deptno end; -val it = [{deptno=20,size=1},{deptno=10,size=1},{deptno=30,size=2}] +val it = [{deptno=10,size=1},{deptno=20,size=1},{deptno=30,size=2}] : {deptno:int, size:int} list @@ -889,10 +903,10 @@ let | siz (ht :: tl) = 1 + (siz tl) in from e in emps - group deptno = e.deptno - compute size = siz of e + group deptno = e.deptno compute size = siz of e + order deptno end; -val it = [{deptno=20,size=1},{deptno=10,size=1},{deptno=30,size=2}] +val it = [{deptno=10,size=1},{deptno=20,size=1},{deptno=30,size=2}] : {deptno:int, size:int} list @@ -902,11 +916,11 @@ let | my_sum (head :: tail) = head + (my_sum tail) in from e in emps - group e.deptno - compute my_sum of e.id + group e.deptno compute my_sum of e.id + order deptno end; val it = - [{deptno=20,my_sum=101},{deptno=10,my_sum=100},{deptno=30,my_sum=205}] + [{deptno=10,my_sum=100},{deptno=20,my_sum=101},{deptno=30,my_sum=205}] : {deptno:int, my_sum:int} list @@ -916,10 +930,11 @@ let in from e in emps group e.deptno compute rows = id of e + order deptno end; val it = - [{deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, - {deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + [{deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + {deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, {deptno=30, rows=[{deptno=30,id=102,name="Shaggy"},{deptno=30,id=103,name="Scooby"}]}] : {deptno:int, rows:{deptno:int, id:int, name:string} list} list @@ -931,10 +946,11 @@ let in from e in emps group e.deptno compute rows = id + order deptno end; val it = - [{deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, - {deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + [{deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + {deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, {deptno=30, rows=[{deptno=30,id=102,name="Shaggy"},{deptno=30,id=103,name="Scooby"}]}] : {deptno:int, rows:{deptno:int, id:int, name:string} list} list @@ -942,10 +958,11 @@ val it = (*) Identity aggregate function, using lambda from e in emps -group e.deptno compute rows = (fn x => x); +group e.deptno compute rows = (fn x => x) +order deptno; val it = - [{deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, - {deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + [{deptno=10,rows=[{deptno=10,id=100,name="Fred"}]}, + {deptno=20,rows=[{deptno=20,id=101,name="Velma"}]}, {deptno=30, rows=[{deptno=30,id=102,name="Shaggy"},{deptno=30,id=103,name="Scooby"}]}] : {deptno:int, rows:{deptno:int, id:int, name:string} list} list @@ -954,13 +971,14 @@ val it = (*) Identity aggregate function with multiple input variables from e in emps, d in depts where e.deptno = d.deptno -group e.deptno compute rows = (fn x => x); +group e.deptno compute rows = (fn x => x) +order deptno; val it = [ - {deptno=20, - rows=[{d={deptno=20,name="HR"},e={deptno=20,id=101,name="Velma"}}]}, {deptno=10, rows=[{d={deptno=10,name="Sales"},e={deptno=10,id=100,name="Fred"}}]}, + {deptno=20, + rows=[{d={deptno=20,name="HR"},e={deptno=20,id=101,name="Velma"}}]}, {deptno=30, rows= [{d={deptno=30,name="Engineering"},e={deptno=30,id=102,name="Shaggy"}}, @@ -973,8 +991,9 @@ from e in emps group e.deptno compute sumId = sum of e.id, count = count of e -yield {deptno, avgId = sumId / count}; -val it = [{avgId=101,deptno=20},{avgId=100,deptno=10},{avgId=102,deptno=30}] +yield {deptno, avgId = sumId / count} +order deptno; +val it = [{avgId=100,deptno=10},{avgId=101,deptno=20},{avgId=102,deptno=30}] : {avgId:int, deptno:int} list @@ -984,8 +1003,9 @@ from g in ( group e.deptno compute sumId = sum of e.id, count = count of e) -yield {g.deptno, avgId = g.sumId / g.count}; -val it = [{avgId=101,deptno=20},{avgId=100,deptno=10},{avgId=102,deptno=30}] +yield {g.deptno, avgId = g.sumId / g.count} +order deptno; +val it = [{avgId=100,deptno=10},{avgId=101,deptno=20},{avgId=102,deptno=30}] : {avgId:int, deptno:int} list @@ -1038,16 +1058,18 @@ val it = from e in emps, d in depts where e.deptno = d.deptno - group d.deptno; -val it = [20,10,30] : int list + group d.deptno + order deptno; +val it = [10,20,30] : int list (*) Join followed by single group (from left input) from e in emps, d in depts where e.deptno = d.deptno - group e.deptno; -val it = [20,10,30] : int list + group e.deptno + order deptno; +val it = [10,20,30] : int list (*) Join followed by single group and order diff --git a/src/test/resources/script/simple.sml b/src/test/resources/script/simple.sml index 9f8c8a8d3..ccfb91a46 100644 --- a/src/test/resources/script/simple.sml +++ b/src/test/resources/script/simple.sml @@ -47,6 +47,31 @@ true; {0=0,1=1,2=2,3=3,4=4,5=5,6=6,7=7,8=8,9=9,10=10,11=11}; {1=1,2=2,3=3,5=5,6=6,7=7,8=8,9=9,10=10,11=11}; +(*) Identifiers +val x = 1 +and x' = 2 +and x'' = 3 +and x'y = 4 +and ABC123 = 5 +and Abc_123 = 6 +and Abc_ = 7; + +fun foo x + x' + x'' + x'y + ABC123 + Abc_123 + Abc_ = 1; + +{x = 1, + x' = 2, + x'' = 3, + x'y = 4, + ABC123 = 5, + Abc_123 = 6, + Abc_ = 7}; + (*) Simple commands val x = 1; x + 2; @@ -236,6 +261,11 @@ in (isZeroMod3 17, isOneMod3 17, isTwoMod3 17) end; +(*) An example where identifiers with primes (') are more readable +fun distance (x, y) (x', y') = + Math.sqrt ((x - x') * (x - x') + (y - y') * (y - y')); +distance (0.0, 1.0) (4.0, 4.0); + (*) Composite declarations let val (x, y) = (1, 2) @@ -264,6 +294,7 @@ val i = 2 : 'a val SOME (i, NONE) = SOME (1, SOME false); *) +Sys.set ("matchCoverageEnabled", false); val (i, true) = (1, true); val (i, false) = (1, true); let @@ -271,6 +302,7 @@ let in i + 1 end; +Sys.unset "matchCoverageEnabled"; (*) Patterns @@ -278,10 +310,12 @@ end; val x = (( "foo", true ), 17 ); val ( l as (ll,lr), r ) = x; +Sys.set ("matchCoverageEnabled", false); fun f (true, []) = ~1 | f (true, l as (hd :: tl)) = length l | f (false, list) = 0; f (true, ["a","b","c"]); +Sys.unset "matchCoverageEnabled"; let val w as (x, y) = (1, 2) @@ -301,4 +335,16 @@ val d = 2 : 'b *) val x as y as z = 3; +(*) Errors +(* The first is printed as '1.9', the second as '1.9-1.11', and the third as '1.9-1.12'. *) +"a string literal to reset the line counter after the comment"; +val a = p + 1; +val a = pq + 1; +val a = pqr + 1; + +fun f n = String.substring ("hello", 1, n); +f 2; +f 6; +f ~1; + (*) End simple.sml diff --git a/src/test/resources/script/simple.sml.out b/src/test/resources/script/simple.sml.out index c7a5d0b59..a51d57319 100644 --- a/src/test/resources/script/simple.sml.out +++ b/src/test/resources/script/simple.sml.out @@ -109,6 +109,44 @@ val it = {1=1,2=2,3=3,5=5,6=6,7=7,8=8,9=9,10=10,11=11} : {1:int, 2:int, 3:int, 5:int, 6:int, 7:int, 8:int, 9:int, 10:int, 11:int} +(*) Identifiers +val x = 1 +and x' = 2 +and x'' = 3 +and x'y = 4 +and ABC123 = 5 +and Abc_123 = 6 +and Abc_ = 7; +val x = 1 : int +val x' = 2 : int +val x'' = 3 : int +val x'y = 4 : int +val ABC123 = 5 : int +val Abc_123 = 6 : int +val Abc_ = 7 : int + + +fun foo x + x' + x'' + x'y + ABC123 + Abc_123 + Abc_ = 1; +val foo = fn : 'a -> 'b -> 'c -> 'd -> 'e -> 'f -> 'g -> int + + +{x = 1, + x' = 2, + x'' = 3, + x'y = 4, + ABC123 = 5, + Abc_123 = 6, + Abc_ = 7}; +val it = {ABC123=5,Abc_=7,Abc_123=6,x=1,x'=2,x''=3,x'y=4} + : {ABC123:int, Abc_:int, Abc_123:int, x:int, x':int, x'':int, x'y:int} + + (*) Simple commands val x = 1; val x = 1 : int @@ -392,6 +430,15 @@ end; val it = (false,false,true) : bool * bool * bool +(*) An example where identifiers with primes (') are more readable +fun distance (x, y) (x', y') = + Math.sqrt ((x - x') * (x - x') + (y - y') * (y - y')); +val distance = fn : real * real -> real * real -> real + +distance (0.0, 1.0) (4.0, 4.0); +val it = 5.0 : real + + (*) Composite declarations let val (x, y) = (1, 2) @@ -429,6 +476,7 @@ val i = 1 : 'a *) val SOME i = NONE; uncaught exception Bind + raised at: stdIn:6.5-6.18 (* TODO fix in [MOREL-43] val SOME (p as (1, i), SOME true) = SOME ((1, 2), SOME true); @@ -439,11 +487,15 @@ val i = 2 : 'a val SOME (i, NONE) = SOME (1, SOME false); *) +Sys.set ("matchCoverageEnabled", false); +val it = () : unit + val (i, true) = (1, true); val i = 1 : int val (i, false) = (1, true); uncaught exception Bind + raised at: stdIn:1.5-1.27 let val (i, false) = (1, true) @@ -451,6 +503,10 @@ in i + 1 end; uncaught exception Bind + raised at: stdIn:2.7-2.29 + +Sys.unset "matchCoverageEnabled"; +val it = () : unit (*) Patterns @@ -466,6 +522,9 @@ val lr = true : bool val r = 17 : int +Sys.set ("matchCoverageEnabled", false); +val it = () : unit + fun f (true, []) = ~1 | f (true, l as (hd :: tl)) = length l | f (false, list) = 0; @@ -474,6 +533,9 @@ val f = fn : bool * 'a list -> int f (true, ["a","b","c"]); val it = 3 : int +Sys.unset "matchCoverageEnabled"; +val it = () : unit + let val w as (x, y) = (1, 2) @@ -513,4 +575,38 @@ val y = 3 : int val z = 3 : int +(*) Errors +(* The first is printed as '1.9', the second as '1.9-1.11', and the third as '1.9-1.12'. *) +"a string literal to reset the line counter after the comment"; +val it = "a string literal to reset the line counter after the comment" + : string + +val a = p + 1; +stdIn:1.9 Error: unbound variable or constructor: p + raised at: stdIn:1.9 + +val a = pq + 1; +stdIn:1.9-1.11 Error: unbound variable or constructor: pq + raised at: stdIn:1.9-1.11 + +val a = pqr + 1; +stdIn:1.9-1.12 Error: unbound variable or constructor: pqr + raised at: stdIn:1.9-1.12 + + +fun f n = String.substring ("hello", 1, n); +val f = fn : int -> string + +f 2; +val it = "el" : string + +f 6; +uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:2.11-2.43 + +f ~1; +uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:2.11-2.43 + + (*) End simple.sml diff --git a/src/test/resources/script/type.sml b/src/test/resources/script/type.sml index 1d4afc853..8a14a79f8 100644 --- a/src/test/resources/script/type.sml +++ b/src/test/resources/script/type.sml @@ -32,6 +32,52 @@ {} = (); () = {}; +(*) Expressions with type annotations +1: int; +(2, true): int * bool; +[]: int list; +(1: int) + (2: int); +1 + (2: int); +(1: int) + 2; +String.size "abc": int; +String.size ("abc": string); +String.size ("abc": string): int; + +(*) Patterns with type annotations +val x: int = 1; +val y: bool = true; +val p: int * bool = (1, true); +val empty: int list = []; + +(*) Function declarations with type annotations +fun f (x: int, y) = x + y; +fun f (x, y: int) = x + y; +fun f3 (e: {name: string, deptno:int}) = e.deptno; +fun hello (name: string, code: int): string = "hello!"; +fun hello2 (name: string) (code : int): string = "hello!"; +val hello3: string * int -> string = + fn (name, code) => "hello!"; +fun l1 [] = 0 | l1 ((h: string) :: t) = 1 + (l1 t); +fun l2 [] = 0 | l2 (h :: (t: bool list)) = 1 + (l2 t); +fun countOption (NONE: string option) = 0 + | countOption (SOME x) = 1; +fun countOption2 NONE: int = 0 + | countOption2 (SOME x) = 1; +fun firstOrSecond (e1 :: e2 :: rest): int = e2 + | firstOrSecond (e1 :: rest) = e1; + +(* +sml-nj gives the following error: +stdIn:1.6-2.32 Error: parameter or result constraints of clauses don't agree [tycon mismatch] + this clause: 'Z option -> string list + previous clauses: 'Z option -> int list + in declaration: + f = (fn NONE => nil: int list + | SOME x => nil: string list) +*) +fun f NONE:int list = [] + | f (SOME x):string list = []; + (*) Function with unit arg fun one () = 1; one (); @@ -66,7 +112,9 @@ fun f2 {a = 1, b} = b * 2 f2 {a = 1, b = 6}; f2 {a = 2, b = 6}; +Sys.set ("matchCoverageEnabled", false); fun f3 {a = 1, b} = b * 2; +Sys.unset "matchCoverageEnabled"; f3 {a = 1, b = 6}; (*) The following correctly throws diff --git a/src/test/resources/script/type.sml.out b/src/test/resources/script/type.sml.out index 545b1c12c..7ad0a3d2b 100644 --- a/src/test/resources/script/type.sml.out +++ b/src/test/resources/script/type.sml.out @@ -54,6 +54,105 @@ val it = true : bool val it = true : bool +(*) Expressions with type annotations +1: int; +val it = 1 : int + +(2, true): int * bool; +val it = (2,true) : int * bool + +[]: int list; +val it = [] : int list + +(1: int) + (2: int); +val it = 3 : int + +1 + (2: int); +val it = 3 : int + +(1: int) + 2; +val it = 3 : int + +String.size "abc": int; +val it = 3 : int + +String.size ("abc": string); +val it = 3 : int + +String.size ("abc": string): int; +val it = 3 : int + + +(*) Patterns with type annotations +val x: int = 1; +val x = 1 : int + +val y: bool = true; +val y = true : bool + +val p: int * bool = (1, true); +val p = (1,true) : int * bool + +val empty: int list = []; +val empty = [] : int list + + +(*) Function declarations with type annotations +fun f (x: int, y) = x + y; +val f = fn : int * int -> int + +fun f (x, y: int) = x + y; +val f = fn : int * int -> int + +fun f3 (e: {name: string, deptno:int}) = e.deptno; +val f3 = fn : {deptno:int, name:string} -> int + +fun hello (name: string, code: int): string = "hello!"; +val hello = fn : string * int -> string + +fun hello2 (name: string) (code : int): string = "hello!"; +val hello2 = fn : string -> int -> string + +val hello3: string * int -> string = + fn (name, code) => "hello!"; +val hello3 = fn : string * int -> string + +fun l1 [] = 0 | l1 ((h: string) :: t) = 1 + (l1 t); +val l1 = fn : string list -> int + +fun l2 [] = 0 | l2 (h :: (t: bool list)) = 1 + (l2 t); +val l2 = fn : bool list -> int + +fun countOption (NONE: string option) = 0 + | countOption (SOME x) = 1; +val countOption = fn : string option -> int + +fun countOption2 NONE: int = 0 + | countOption2 (SOME x) = 1; +val countOption2 = fn : (int * 'a) option -> int + +fun firstOrSecond (e1 :: e2 :: rest): int = e2 + | firstOrSecond (e1 :: rest) = e1; +0.0-0.0 Warning: match nonexhaustive + raised at: 0.0-0.0 +val firstOrSecond = fn : int list -> int + + +(* +sml-nj gives the following error: +stdIn:1.6-2.32 Error: parameter or result constraints of clauses don't agree [tycon mismatch] + this clause: 'Z option -> string list + previous clauses: 'Z option -> int list + in declaration: + f = (fn NONE => nil: int list + | SOME x => nil: string list) +*) +fun f NONE:int list = [] + | f (SOME x):string list = []; +stdIn:11.5-12.32 Error: parameter or result constraints of clauses don't agree [tycon mismatch] + raised at: stdIn:11.5-12.32 + + (*) Function with unit arg fun one () = 1; val one = fn : unit -> int @@ -112,9 +211,15 @@ f2 {a = 2, b = 6}; val it = 18 : int +Sys.set ("matchCoverageEnabled", false); +val it = () : unit + fun f3 {a = 1, b} = b * 2; val f3 = fn : {a:int, b:int} -> int +Sys.unset "matchCoverageEnabled"; +val it = () : unit + f3 {a = 1, b = 6}; val it = 12 : int diff --git a/src/test/resources/script/use-1.sml.out b/src/test/resources/script/use-1.sml.out index f10da43ab..08fb22567 100644 --- a/src/test/resources/script/use-1.sml.out +++ b/src/test/resources/script/use-1.sml.out @@ -25,10 +25,12 @@ val it = "entering use-1.sml" : string val y = x ^ ", "; -unbound variable or constructor: x +stdIn:1.9 Error: unbound variable or constructor: x + raised at: stdIn:1.9 val x = y ^ "step 2"; -unbound variable or constructor: y +stdIn:1.9 Error: unbound variable or constructor: y + raised at: stdIn:1.9 fun plus3 n = n + 3; val plus3 = fn : int -> int diff --git a/src/test/resources/script/use.sml.out b/src/test/resources/script/use.sml.out index 15af4a01c..77c077146 100644 --- a/src/test/resources/script/use.sml.out +++ b/src/test/resources/script/use.sml.out @@ -37,6 +37,7 @@ val b = 20 : int (*) throws Subscript: val a = String.sub("abc", b); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:2.9-2.29 (*) plan is for the unsuccessful statement: Sys.plan(); @@ -44,6 +45,7 @@ val it = "apply2(fnValue String.sub, constant(abc), constant(20))" : string String.sub("xyz", b - 1); uncaught exception Subscript [subscript out of bounds] + raised at: stdIn:1.1-1.25 (*) plan is for the unsuccessful expression: Sys.plan(); diff --git a/src/test/resources/script/wordle.sml b/src/test/resources/script/wordle.sml index 992f27711..b47b6cd05 100644 --- a/src/test/resources/script/wordle.sml +++ b/src/test/resources/script/wordle.sml @@ -1868,6 +1868,7 @@ mask ("abcde", "spree"); (*) should be 6 (second to last letter is correct) mask ("abcde", "spuds"); +Sys.set ("matchCoverageEnabled", false); fun maskToString m = let fun maskToString2 (m, s, 0) = s @@ -1881,6 +1882,7 @@ fun maskToString m = in maskToString2 (m, "", 5) end; +Sys.unset "matchCoverageEnabled"; maskToString 0; maskToString 1; diff --git a/src/test/resources/script/wordle.sml.out b/src/test/resources/script/wordle.sml.out index 32b43f4ca..844b4520b 100644 --- a/src/test/resources/script/wordle.sml.out +++ b/src/test/resources/script/wordle.sml.out @@ -1903,6 +1903,9 @@ mask ("abcde", "spuds"); val it = 6 : int +Sys.set ("matchCoverageEnabled", false); +val it = () : unit + fun maskToString m = let fun maskToString2 (m, s, 0) = s @@ -1918,6 +1921,9 @@ fun maskToString m = end; val maskToString = fn : int -> string +Sys.unset "matchCoverageEnabled"; +val it = () : unit + maskToString 0; val it = "bbbbb" : string