From 6f03a5974c02446b0a199a66dc5c59d0124ab4a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:46:06 +0000 Subject: [PATCH 01/36] Bump javacc from 7.0.5 to 7.0.10 Bumps [javacc](https://github.com/javacc/javacc) from 7.0.5 to 7.0.10. - [Release notes](https://github.com/javacc/javacc/releases) - [Changelog](https://github.com/javacc/javacc/blob/master/docs/release-notes.md) - [Commits](https://github.com/javacc/javacc/compare/7.0.5...javacc-7.0.10) --- updated-dependencies: - dependency-name: net.java.dev.javacc:javacc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac421ecdc..581927d78 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,7 @@ License. 0.3 1.1.2 3.0.0 - 7.0.5 + 7.0.10 3.16.0 5.7.2 3.0.0 From afe1a32a94872330e14e692ca2b718e68906fe2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:46:17 +0000 Subject: [PATCH 02/36] Bump junit-jupiter.version from 5.7.2 to 5.8.2 Bumps `junit-jupiter.version` from 5.7.2 to 5.8.2. Updates `junit-jupiter-api` from 5.7.2 to 5.8.2 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.2...r5.8.2) Updates `junit-jupiter-engine` from 5.7.2 to 5.8.2 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.2...r5.8.2) Updates `junit-jupiter-params` from 5.7.2 to 5.8.2 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.2...r5.8.2) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 581927d78..5998a4c0b 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ License. 3.0.0 7.0.10 3.16.0 - 5.7.2 + 5.8.2 3.0.0 3.0.0-M1 3.0.1 From 7a70a1d2c01a69d34206d930736eae7df2fad8fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:46:25 +0000 Subject: [PATCH 03/36] Bump slf4j-api from 1.7.25 to 1.7.36 Bumps [slf4j-api](https://github.com/qos-ch/slf4j) from 1.7.25 to 1.7.36. - [Release notes](https://github.com/qos-ch/slf4j/releases) - [Commits](https://github.com/qos-ch/slf4j/compare/v_1.7.25...v_1.7.36) --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5998a4c0b..2a0298db1 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ License. 3.7.1 3.0.0-M3 0.1 - 1.7.25 + 1.7.36 -html5 net.hydromatic.morel.parse From f4d744f3914b213410936a1e42ad8c9e1de4e218 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Mon, 28 Feb 2022 22:34:07 -0800 Subject: [PATCH 04/36] [MOREL-105] Allow identifiers to contain prime characters (') Disable one more method in ShellTest. Fixes #105 --- src/main/javacc/MorelParser.jj | 8 ++-- .../java/net/hydromatic/morel/MainTest.java | 8 ++++ .../java/net/hydromatic/morel/ShellTest.java | 1 + src/test/resources/script/simple.sml | 30 ++++++++++++ src/test/resources/script/simple.sml.out | 47 +++++++++++++++++++ 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/main/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 07f4e4b04..7f15b666d 100644 --- a/src/main/javacc/MorelParser.jj +++ b/src/main/javacc/MorelParser.jj @@ -1545,17 +1545,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..921d9b856 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -119,6 +119,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"); diff --git a/src/test/java/net/hydromatic/morel/ShellTest.java b/src/test/java/net/hydromatic/morel/ShellTest.java index 8f07fc5e0..083adfb56 100644 --- a/src/test/java/net/hydromatic/morel/ShellTest.java +++ b/src/test/java/net/hydromatic/morel/ShellTest.java @@ -109,6 +109,7 @@ static File getUseDirectory() { /** Tests {@link Shell} with a continued line. */ @Test void testTwoLines() { + assumeNotInCi(); final String in = "1 +\n" + "2;\n"; final String expected = "1 +\r\n" diff --git a/src/test/resources/script/simple.sml b/src/test/resources/script/simple.sml index 9f8c8a8d3..ebf56778b 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) diff --git a/src/test/resources/script/simple.sml.out b/src/test/resources/script/simple.sml.out index c7a5d0b59..6810f1c36 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) From da15b9fe8eaaaa243cec4a94943c6344f31c4011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 04:38:05 +0000 Subject: [PATCH 05/36] Bump jsr305 from 1.3.9 to 3.0.2 Bumps jsr305 from 1.3.9 to 3.0.2. --- updated-dependencies: - dependency-name: com.google.code.findbugs:jsr305 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2a0298db1..e1de7bf65 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ License. 1.29.0 7.8.2 - 1.3.9 + 3.0.2 0.4 21.0 From e5c91b406fd0f069c795aba05ed06f9eb4984041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 02:46:08 +0000 Subject: [PATCH 06/36] Bump maven-surefire-plugin from 3.0.0-M3 to 3.0.0-M5 Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M3 to 3.0.0-M5. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M3...surefire-3.0.0-M5) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e1de7bf65..94e76fe71 100644 --- a/pom.xml +++ b/pom.xml @@ -95,7 +95,7 @@ License. 3.0.1 2.9 3.7.1 - 3.0.0-M3 + 3.0.0-M5 0.1 1.7.36 From 588a23cfccd4c7d559caba744bc624e8d3a90367 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 06:53:18 +0000 Subject: [PATCH 07/36] Bump maven-site-plugin from 3.7.1 to 3.11.0 Bumps [maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.7.1 to 3.11.0. - [Release notes](https://github.com/apache/maven-site-plugin/releases) - [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.7.1...maven-site-plugin-3.11.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-site-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 94e76fe71..5e3f59f83 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ License. 3.0.0-M1 3.0.1 2.9 - 3.7.1 + 3.11.0 3.0.0-M5 0.1 1.7.36 From b535bf657b983cf7cc9458fb00d3c3ae609c70f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 07:07:09 +0000 Subject: [PATCH 08/36] Bump jline from 3.16.0 to 3.21.0 Bumps [jline](https://github.com/jline/jline3) from 3.16.0 to 3.21.0. - [Release notes](https://github.com/jline/jline3/releases) - [Changelog](https://github.com/jline/jline3/blob/master/changelog.md) - [Commits](https://github.com/jline/jline3/compare/jline-parent-3.16.0...jline-parent-3.21.0) --- updated-dependencies: - dependency-name: org.jline:jline dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e3f59f83..c3a4695fd 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ License. 1.1.2 3.0.0 7.0.10 - 3.16.0 + 3.21.0 5.8.2 3.0.0 3.0.0-M1 From 8073a6daa3aeeb378c0d8d4cec573c9742e83224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 06:45:07 +0000 Subject: [PATCH 09/36] Bump guava from 21.0 to 23.0 Bumps [guava](https://github.com/google/guava) from 21.0 to 23.0. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/compare/v21.0...v23.0) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c3a4695fd..9d1db8d37 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ License. 3.0.2 0.4 - 21.0 + 23.0 2.2 2.3.1 0.3 From 47249daa7ac9383a0ed98f9394fbd7506d023012 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Tue, 1 Mar 2022 00:18:43 -0800 Subject: [PATCH 10/36] Test Guava versions 19.0 to 31.1-jre in CI Add 'order' clauses in tests where necessary to achieve deterministic results; the order of iteration over collections seems to have changed in recent versions of Guava. --- .github/workflows/main.yml | 19 ++- pom.xml | 2 +- .../morel/compile/TypeResolver.java | 3 +- .../net/hydromatic/morel/foreign/RelList.java | 3 +- src/test/resources/script/blog.sml | 31 ++-- src/test/resources/script/blog.sml.out | 99 ++++++------ src/test/resources/script/fixedPoint.sml | 24 +-- src/test/resources/script/fixedPoint.sml.out | 36 ++--- src/test/resources/script/relational.sml | 78 ++++++---- src/test/resources/script/relational.sml.out | 142 ++++++++++-------- 10 files changed, 256 insertions(+), 181 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 66068027d..4f2db9f86 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,11 +25,19 @@ jobs: strategy: matrix: - java-version: [ 8, 11, 17 ] + java-version: [ "8" ] + guava-version: [ "21.0" ] javadoc: [ false ] include: - - java-version: 17 + - java-version: "11" + guava-version: "19.0" + javadoc: false + - java-version: "17" + guava-version: "23.0" javadoc: true + - java-version: "17" + guava-version: "31.1-jre" + javadoc: false steps: - uses: actions/checkout@v2 @@ -47,6 +55,11 @@ jobs: then GOALS="$GOALS javadoc:javadoc javadoc:test-javadoc" fi - mvn -Dmorel.ci --batch-mode --update-snapshots $GOALS + DEFS="-Dmorel.ci" + 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/pom.xml b/pom.xml index 9d1db8d37..b75722eb5 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ License. 7.8.2 3.0.2 0.4 - + 23.0 2.2 2.3.1 diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index 73730204c..643fd9bc5 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -744,8 +744,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. 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/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/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/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 From 51804d4299acab9136a5f96e19cab034fbf2acda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 04:47:22 +0000 Subject: [PATCH 11/36] Bump javacc-maven-plugin from 3.0.0 to 3.0.3 Bumps [javacc-maven-plugin](https://github.com/javacc/javacc-maven-plugin) from 3.0.0 to 3.0.3. - [Release notes](https://github.com/javacc/javacc-maven-plugin/releases) - [Commits](https://github.com/javacc/javacc-maven-plugin/compare/javacc-maven-plugin-3.0.0...javacc-maven-plugin-3.0.3) --- updated-dependencies: - dependency-name: org.javacc.plugin:javacc-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b75722eb5..ca4af2789 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ License. 2.3.1 0.3 1.1.2 - 3.0.0 + 3.0.3 7.0.10 3.21.0 5.8.2 From 6453be4cd987b52c1aa9ade4cb1070de0f21cebe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 04:47:15 +0000 Subject: [PATCH 12/36] Bump maven-javadoc-plugin from 3.0.1 to 3.3.2 Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.0.1 to 3.3.2. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.0.1...maven-javadoc-plugin-3.3.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ca4af2789..136e9584f 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ License. 5.8.2 3.0.0 3.0.0-M1 - 3.0.1 + 3.3.2 2.9 3.11.0 3.0.0-M5 From af5036b097f843bb4b55ffbe22024d1c828915cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 04:31:31 +0000 Subject: [PATCH 13/36] Bump maven-enforcer-plugin from 3.0.0-M1 to 3.0.0 Bumps [maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.0.0-M1 to 3.0.0. - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.0.0-M1...enforcer-3.0.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 136e9584f..3f5877c8e 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ License. 3.21.0 5.8.2 3.0.0 - 3.0.0-M1 + 3.0.0 3.3.2 2.9 3.11.0 From 7f4ea968cd77e71109d25c2d3b21a31211a32375 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 04:31:34 +0000 Subject: [PATCH 14/36] Bump maven-project-info-reports-plugin from 2.9 to 3.2.2 Bumps [maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 2.9 to 3.2.2. - [Release notes](https://github.com/apache/maven-project-info-reports-plugin/releases) - [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-2.9...maven-project-info-reports-plugin-3.2.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-project-info-reports-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3f5877c8e..47bad5964 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ License. 3.0.0 3.0.0 3.3.2 - 2.9 + 3.2.2 3.11.0 3.0.0-M5 0.1 From 2a34f2032ef3ec98e35654f38301c4e146538f90 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Wed, 9 Mar 2022 00:37:58 -0800 Subject: [PATCH 15/36] Bump hsqldb from 2.3.1 to 2.5.1, foodmart-data-hsqldb from 0.4 to 0.5, scott-data-hsqldb from 0.1 to 0.2 The foodmart-data-hsqldb and scott-data-hsqldb are necessary because their previous versions used HSQLDB 1.8 file format, which could not be read by HSQLDB higher than 2.5.1 or higher. If you are running Java 11 or higher, you can use HSQLDB 2.6.1. --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 47bad5964..23f82fa5f 100644 --- a/pom.xml +++ b/pom.xml @@ -79,11 +79,11 @@ License. 1.29.0 7.8.2 3.0.2 - 0.4 + 0.5 23.0 2.2 - 2.3.1 + 2.5.1 0.3 1.1.2 3.0.3 @@ -96,7 +96,7 @@ License. 3.2.2 3.11.0 3.0.0-M5 - 0.1 + 0.2 1.7.36 -html5 From 37b038759b03a3c160b768eaed96788b484ae8b8 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Sun, 13 Mar 2022 11:55:58 -0700 Subject: [PATCH 16/36] Turn off Travis CI --- .travis.yml | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 .travis.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 From 2d7b347de65f021363a0b894472e87249e853dec Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Sun, 13 Mar 2022 11:32:37 -0700 Subject: [PATCH 17/36] Make ShellTest more robust --- .../java/net/hydromatic/morel/ShellTest.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/test/java/net/hydromatic/morel/ShellTest.java b/src/test/java/net/hydromatic/morel/ShellTest.java index 083adfb56..e3ac29833 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,7 +105,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 continued line. */ @@ -119,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. */ @@ -146,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. */ @@ -161,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. */ @@ -176,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 @@ -201,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. */ @@ -255,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" @@ -288,7 +294,7 @@ static File getUseDirectory() { fixture() .withArgListPlusDirectory() .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); } /** Tests the {@code use} function on a missing file. */ @@ -313,7 +319,7 @@ static File getUseDirectory() { fixture() .withArgListPlusDirectory() .withInputString(in) - .assertOutput(is(expected)); + .assertOutput(is2(expected)); } /** Tests the {@code use} function on a file that uses itself. */ @@ -343,7 +349,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. From 21c6fef0bfd2c0aa7f8ff83e83a025ab5b675876 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Wed, 2 Mar 2022 10:16:01 -0800 Subject: [PATCH 18/36] [MOREL-118] Report positions in error messages and exceptions Make it easier to write tests that check for positions in error messages, and write a unit test for Pos. Fixes #118 --- src/main/java/net/hydromatic/morel/Main.java | 20 +- src/main/java/net/hydromatic/morel/Shell.java | 19 +- .../java/net/hydromatic/morel/ast/Core.java | 80 +-- .../net/hydromatic/morel/ast/CoreBuilder.java | 20 +- .../java/net/hydromatic/morel/ast/Pos.java | 92 ++- .../morel/compile/CalciteCompiler.java | 3 +- .../morel/compile/CompileException.java | 25 +- .../hydromatic/morel/compile/Compiler.java | 54 +- .../hydromatic/morel/compile/Compiles.java | 4 +- .../hydromatic/morel/compile/EnvShuttle.java | 2 +- .../net/hydromatic/morel/compile/Inliner.java | 2 +- .../morel/compile/Relationalizer.java | 4 +- .../hydromatic/morel/compile/Resolver.java | 99 ++- .../morel/compile/TypeResolver.java | 20 +- .../hydromatic/morel/eval/Applicable2.java | 7 +- .../hydromatic/morel/eval/Applicable3.java | 7 +- .../hydromatic/morel/eval/ApplicableImpl.java | 20 +- .../net/hydromatic/morel/eval/Closure.java | 7 +- .../java/net/hydromatic/morel/eval/Codes.java | 645 ++++++++++++------ .../net/hydromatic/morel/eval/Session.java | 58 +- .../hydromatic/morel/parse/MorelParser.java | 5 +- .../java/net/hydromatic/morel/parse/Span.java | 2 +- .../hydromatic/morel/util/MorelException.java | 32 + src/main/javacc/MorelParser.jj | 82 +-- .../java/net/hydromatic/morel/MainTest.java | 20 +- .../java/net/hydromatic/morel/Matchers.java | 24 +- src/test/java/net/hydromatic/morel/Ml.java | 27 +- .../java/net/hydromatic/morel/ShellTest.java | 2 + .../java/net/hydromatic/morel/UtilTest.java | 59 ++ src/test/resources/script/builtIn.sml.out | 34 + src/test/resources/script/simple.sml | 12 + src/test/resources/script/simple.sml.out | 37 + src/test/resources/script/use-1.sml.out | 6 +- src/test/resources/script/use.sml.out | 2 + 34 files changed, 1109 insertions(+), 423 deletions(-) create mode 100644 src/main/java/net/hydromatic/morel/util/MorelException.java diff --git a/src/main/java/net/hydromatic/morel/Main.java b/src/main/java/net/hydromatic/morel/Main.java index 58b4a3bfb..415661dc4 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)) { diff --git a/src/main/java/net/hydromatic/morel/Shell.java b/src/main/java/net/hydromatic/morel/Shell.java index 7ac30679e..5d479920e 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,6 +476,7 @@ 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 CompiledStatement compiled = @@ -515,7 +518,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 +528,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 +552,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/Core.java b/src/main/java/net/hydromatic/morel/ast/Core.java index 067dd957a..c2347df0d 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; @@ -64,8 +62,8 @@ 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 +85,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 +457,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 +486,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 +521,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 +565,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 +606,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 +618,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 +650,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 +672,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 +704,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 +717,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 +747,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 +763,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 +810,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 +841,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 +877,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 +897,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 +907,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); } @@ -939,7 +943,7 @@ public static class Case extends Exp { public final List matchList; Case(Type type, Exp exp, ImmutableList matchList) { - super(Op.CASE, type); + super(Pos.ZERO, Op.CASE, type); this.exp = exp; this.matchList = matchList; } @@ -968,7 +972,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 +1011,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 +1154,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 +1255,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 +1296,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 +1310,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..56a20bdb8 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,8 +302,8 @@ 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, @@ -371,17 +372,18 @@ 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" + // as if user had written "case c of true => a | _ => b". + // Pos.ZERO is ok becuase match failure is impossible. return new Core.Case(ifTrue.type, condition, - ImmutableList.of(match(truePat, ifTrue), - match(boolWildcardPat, ifFalse))); + ImmutableList.of(match(truePat, ifTrue, Pos.ZERO), + match(boolWildcardPat, ifFalse, Pos.ZERO))); } public Core.DatatypeDecl datatypeDecl(Iterable dataTypes) { diff --git a/src/main/java/net/hydromatic/morel/ast/Pos.java b/src/main/java/net/hydromatic/morel/ast/Pos.java index 932d8f9bf..ba65beaa9 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,48 @@ /** 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 +88,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 +172,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 +192,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 +223,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/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..db63d8884 100644 --- a/src/main/java/net/hydromatic/morel/compile/CompileException.java +++ b/src/main/java/net/hydromatic/morel/compile/CompileException.java @@ -18,10 +18,31 @@ */ 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 Pos pos; + + public CompileException(String message, Pos pos) { super(message); + 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(" 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..fd92bd211 100644 --- a/src/main/java/net/hydromatic/morel/compile/Compiles.java +++ b/src/main/java/net/hydromatic/morel/compile/Compiles.java @@ -150,9 +150,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/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..7ef2814cc 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); } @@ -374,7 +383,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 +394,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 +410,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 +423,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))); } @@ -574,7 +583,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 +594,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 +740,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 +757,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 +776,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)))); } } } + /** 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 643fd9bc5..dfd30093a 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -311,7 +311,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, + id.pos)); return reg(id, v, term); case FN: @@ -895,7 +897,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( @@ -1187,8 +1190,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) { @@ -1207,7 +1211,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); @@ -1253,13 +1258,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/Session.java b/src/main/java/net/hydromatic/morel/eval/Session.java index 5d1a7bcbd..abd82d04a 100644 --- a/src/main/java/net/hydromatic/morel/eval/Session.java +++ b/src/main/java/net/hydromatic/morel/eval/Session.java @@ -18,6 +18,7 @@ */ package net.hydromatic.morel.eval; +import net.hydromatic.morel.ast.Pos; import net.hydromatic.morel.compile.CompileException; import java.util.LinkedHashMap; @@ -40,7 +41,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,8 +59,19 @@ 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) { @@ -68,31 +80,39 @@ public void handle(Codes.MorelRuntimeException e, StringBuilder 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/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/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/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 7f15b666d..90fa7dbf4 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() ( @@ -819,7 +825,7 @@ Exp atom() : e = from() { return e; } | { - span = Span.of(getPos()); + span = Span.of(pos()); } ( { return ast.unitLiteral(span.end(this)); } @@ -843,7 +849,7 @@ Exp atom() : ) | { - span = Span.of(getPos()); + span = Span.of(pos()); final List list = new ArrayList<>(); Exp e2; } @@ -858,7 +864,7 @@ Exp atom() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); final Map map = new LinkedHashMap<>(); } [ @@ -912,7 +918,7 @@ ValDecl valDecl() : final List valBinds = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } [ { rec = true; } ] valBind(valBinds) ( @@ -963,7 +969,7 @@ Ast.DatatypeDecl datatypeDecl() : final List binds = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } datatypeBind(binds) ( datatypeBind(binds) @@ -1035,7 +1041,7 @@ Ast.FunDecl funDecl() : final List funBindList = new ArrayList<>(); } { - { span = Span.of(getPos()); } + { span = Span.of(pos()); } funBind(funBindList) ( funBind(funBindList) @@ -1163,7 +1169,7 @@ Pat atomPat() : return ast.wildcardPat(pos()); } | - { span = Span.of(getPos()); } + { span = Span.of(pos()); } [ p = pat() { list.add(p); } ( @@ -1179,7 +1185,7 @@ Pat atomPat() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); } [ p = pat() { list.add(p); } @@ -1192,7 +1198,7 @@ Pat atomPat() : } | { - span = Span.of(getPos()); + span = Span.of(pos()); final Map map = new LinkedHashMap<>(); boolean ellipsis = false; } @@ -1222,7 +1228,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 +1246,7 @@ Ast.Type atomicType() : type = recordType() { return type; } | { - span = Span.of(getPos()); + span = Span.of(pos()); } type = type() ( diff --git a/src/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index 921d9b856..d5dd803b4 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -19,6 +19,7 @@ package net.hydromatic.morel; import net.hydromatic.morel.ast.Ast; +import net.hydromatic.morel.eval.Codes; import net.hydromatic.morel.parse.ParseException; import net.hydromatic.morel.type.DataType; import net.hydromatic.morel.type.TypeVar; @@ -372,6 +373,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(), @@ -741,8 +757,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)); diff --git a/src/test/java/net/hydromatic/morel/Matchers.java b/src/test/java/net/hydromatic/morel/Matchers.java index 7b4df92e4..8050ae48b 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) { @@ -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..19950015e 100644 --- a/src/test/java/net/hydromatic/morel/Ml.java +++ b/src/test/java/net/hydromatic/morel/Ml.java @@ -21,6 +21,7 @@ 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.CompiledStatement; @@ -48,6 +49,7 @@ 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.hamcrest.Matcher; import java.io.StringReader; @@ -57,6 +59,7 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import javax.annotation.Nullable; import static net.hydromatic.morel.Matchers.hasMoniker; @@ -74,19 +77,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 +204,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 = @@ -381,7 +393,8 @@ private Object eval(Session session, Environment env, CompiledStatement compiledStatement = Compiles.prepareStatement(typeSystem, session, env, statement, calcite); final List bindings = new ArrayList<>(); - compiledStatement.eval(session, env, line -> {}, bindings::add); + session.withoutHandlingExceptions(session1 -> + compiledStatement.eval(session1, env, line -> {}, bindings::add)); final Object result; if (statement instanceof Ast.Exp) { result = bindingValue(bindings, "it"); @@ -415,7 +428,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"); @@ -444,11 +459,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. */ diff --git a/src/test/java/net/hydromatic/morel/ShellTest.java b/src/test/java/net/hydromatic/morel/ShellTest.java index e3ac29833..021594711 100644 --- a/src/test/java/net/hydromatic/morel/ShellTest.java +++ b/src/test/java/net/hydromatic/morel/ShellTest.java @@ -314,6 +314,7 @@ private Matcher is2(String expected) { + "[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() @@ -342,6 +343,7 @@ private Matcher is2(String expected) { + "[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"; 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/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/simple.sml b/src/test/resources/script/simple.sml index ebf56778b..47ca00ee7 100644 --- a/src/test/resources/script/simple.sml +++ b/src/test/resources/script/simple.sml @@ -331,4 +331,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 6810f1c36..db416ba2f 100644 --- a/src/test/resources/script/simple.sml.out +++ b/src/test/resources/script/simple.sml.out @@ -476,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); @@ -491,6 +492,7 @@ 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) @@ -498,6 +500,7 @@ in i + 1 end; uncaught exception Bind + raised at: stdIn:2.7-2.29 (*) Patterns @@ -560,4 +563,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/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(); From e102684f44816054afcd34d13f28141b939b3d21 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Thu, 17 Mar 2022 17:15:17 -0700 Subject: [PATCH 19/36] Satisfiability prover Implementation is brute force. If there are N variables, the algorithm will try each combination of assigments of {false, true} to variables, and stop when it finds one that works. We can improve the implementation later, if necessary. For example, if there is a group of variables where exactly one is always true, then we can eliminate one of the variables. (If exactly one of A, B and C is true, replace A with "not (B or C)".) For the environment, use a boolean[] rather than a Map, exploiting the fact that variables have contiguous integer ids. --- .../java/net/hydromatic/morel/util/Sat.java | 256 ++++++++++++++++++ .../java/net/hydromatic/morel/SatTest.java | 79 ++++++ 2 files changed, 335 insertions(+) create mode 100644 src/main/java/net/hydromatic/morel/util/Sat.java create mode 100644 src/test/java/net/hydromatic/morel/SatTest.java 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/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 From 5a7d2489b4c2799fd89a4ab11c0ef0814f2ee473 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Sat, 19 Mar 2022 12:38:14 -0700 Subject: [PATCH 20/36] [MOREL-55] Analyze match coverage, detecting redundant and exhaustive matches Allow the shell to print warnings (even multiple warnings). Add property 'matchCoverageEnabled', to control whether coverage is analyzed. If it is false, an expression will never be rejected for being redundant. Add $bool and $list internal data types. This allows true, false, NIL and CONS to be analyzed in matches the same way as other constructors. Unlike values of other types, we know that constructors are a finite set. So we can deduce that fun f true = ... | f false = ... is exhaustive. Because $bool and $list are internal, they can't be used in programs and don't appear in metadata. Fixes #55 --- docs/reference.md | 17 +- src/main/java/net/hydromatic/morel/Main.java | 12 +- src/main/java/net/hydromatic/morel/Shell.java | 3 +- .../java/net/hydromatic/morel/ast/Core.java | 7 +- .../net/hydromatic/morel/ast/CoreBuilder.java | 13 +- .../net/hydromatic/morel/compile/BuiltIn.java | 29 +- .../morel/compile/CompileException.java | 6 +- .../hydromatic/morel/compile/Compiles.java | 61 ++- .../morel/compile/PatternCoverageChecker.java | 352 ++++++++++++++++++ .../hydromatic/morel/compile/Resolver.java | 7 +- .../morel/compile/TypeResolver.java | 2 +- .../java/net/hydromatic/morel/eval/Prop.java | 7 + .../net/hydromatic/morel/eval/Session.java | 5 +- .../net/hydromatic/morel/type/TypeSystem.java | 17 + .../java/net/hydromatic/morel/MainTest.java | 141 ++++++- src/test/java/net/hydromatic/morel/Ml.java | 121 +++++- .../java/net/hydromatic/morel/ShellTest.java | 14 + src/test/resources/script/match.sml | 66 ++++ src/test/resources/script/match.sml.out | 112 ++++++ src/test/resources/script/simple.sml | 4 + src/test/resources/script/simple.sml.out | 12 + src/test/resources/script/type.sml | 2 + src/test/resources/script/type.sml.out | 6 + src/test/resources/script/wordle.sml | 2 + src/test/resources/script/wordle.sml.out | 6 + 25 files changed, 971 insertions(+), 53 deletions(-) create mode 100644 src/main/java/net/hydromatic/morel/compile/PatternCoverageChecker.java create mode 100644 src/test/resources/script/match.sml create mode 100644 src/test/resources/script/match.sml.out diff --git a/docs/reference.md b/docs/reference.md index f71431e9c..8895d60cc 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -470,11 +470,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/src/main/java/net/hydromatic/morel/Main.java b/src/main/java/net/hydromatic/morel/Main.java index 415661dc4..8aa70dadd 100644 --- a/src/main/java/net/hydromatic/morel/Main.java +++ b/src/main/java/net/hydromatic/morel/Main.java @@ -238,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/Shell.java b/src/main/java/net/hydromatic/morel/Shell.java index 5d479920e..6fa3b9784 100644 --- a/src/main/java/net/hydromatic/morel/Shell.java +++ b/src/main/java/net/hydromatic/morel/Shell.java @@ -479,9 +479,10 @@ void extracted(@Nullable Map outBindings) { 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)); diff --git a/src/main/java/net/hydromatic/morel/ast/Core.java b/src/main/java/net/hydromatic/morel/ast/Core.java index c2347df0d..b291811ec 100644 --- a/src/main/java/net/hydromatic/morel/ast/Core.java +++ b/src/main/java/net/hydromatic/morel/ast/Core.java @@ -57,6 +57,7 @@ * class names short. */ // TODO: remove 'parse tree for...' from all the comments below +@SuppressWarnings("StaticPseudoFunctionalStyleMethod") public class Core { private Core() {} @@ -942,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(Pos.ZERO, Op.CASE, type); + Case(Pos pos, Type type, Exp exp, ImmutableList matchList) { + super(pos, Op.CASE, type); this.exp = exp; this.matchList = matchList; } @@ -963,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); } } diff --git a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java index 56a20bdb8..efcbf7ea6 100644 --- a/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/CoreBuilder.java @@ -307,8 +307,8 @@ public Core.Match match(Core.Pat pat, Core.Exp exp, Pos pos) { } 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) { @@ -380,10 +380,11 @@ 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". - // Pos.ZERO is ok becuase match failure is impossible. - return new Core.Case(ifTrue.type, condition, - ImmutableList.of(match(truePat, ifTrue, Pos.ZERO), - match(boolWildcardPat, ifFalse, Pos.ZERO))); + // 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/compile/BuiltIn.java b/src/main/java/net/hydromatic/morel/compile/BuiltIn.java index 9c76d7479..c926a6292 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; @@ -1662,19 +1662,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 +1708,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/CompileException.java b/src/main/java/net/hydromatic/morel/compile/CompileException.java index db63d8884..ae992d2be 100644 --- a/src/main/java/net/hydromatic/morel/compile/CompileException.java +++ b/src/main/java/net/hydromatic/morel/compile/CompileException.java @@ -24,10 +24,12 @@ /** An error occurred during compilation. */ public class CompileException extends RuntimeException implements MorelException { + private final boolean warning; private final Pos pos; - public CompileException(String message, Pos pos) { + public CompileException(String message, boolean warning, Pos pos) { super(message); + this.warning = warning; this.pos = pos; } @@ -41,7 +43,7 @@ public CompileException(String message, Pos pos) { public StringBuilder describeTo(StringBuilder buf) { return pos.describeTo(buf) - .append(" Error: ") + .append(warning ? " Warning: " : " Error: ") .append(getMessage()); } } diff --git a/src/main/java/net/hydromatic/morel/compile/Compiles.java b/src/main/java/net/hydromatic/morel/compile/Compiles.java index fd92bd211..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; 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/Resolver.java b/src/main/java/net/hydromatic/morel/compile/Resolver.java index 7ef2814cc..4d028a417 100644 --- a/src/main/java/net/hydromatic/morel/compile/Resolver.java +++ b/src/main/java/net/hydromatic/morel/compile/Resolver.java @@ -454,7 +454,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_) { @@ -464,7 +465,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) { @@ -779,7 +780,7 @@ class ResolvedValDecl extends ResolvedDecl { 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, pos)))); + ImmutableList.of(core.match(pat, resultExp, pos)), pos)); } } } diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index dfd30093a..b7e1362bf 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -313,7 +313,7 @@ private Ast.Exp deduceType(TypeEnv env, Ast.Exp node, Unifier.Variable v) { final Ast.Id id = (Ast.Id) node; final Unifier.Term term = env.get(typeSystem, id.name, name -> new CompileException("unbound variable or constructor: " + name, - id.pos)); + false, id.pos)); return reg(id, v, term); case FN: 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 abd82d04a..7f0d84bfb 100644 --- a/src/main/java/net/hydromatic/morel/eval/Session.java +++ b/src/main/java/net/hydromatic/morel/eval/Session.java @@ -20,6 +20,7 @@ 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; @@ -74,8 +75,8 @@ 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. */ 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/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index d5dd803b4..1076be8e3 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -19,7 +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; @@ -27,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; @@ -51,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; @@ -809,7 +815,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. */ @@ -1215,6 +1223,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/Ml.java b/src/test/java/net/hydromatic/morel/Ml.java index 19950015e..709ddc595 100644 --- a/src/test/java/net/hydromatic/morel/Ml.java +++ b/src/test/java/net/hydromatic/morel/Ml.java @@ -24,6 +24,7 @@ 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; @@ -50,6 +51,8 @@ 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; @@ -60,7 +63,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import javax.annotation.Nullable; import static net.hydromatic.morel.Matchers.hasMoniker; import static net.hydromatic.morel.Matchers.isAst; @@ -225,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) -> @@ -240,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); @@ -363,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"}) @@ -373,28 +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<>(); - session.withoutHandlingExceptions(session1 -> - compiledStatement.eval(session1, 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"); @@ -440,6 +523,10 @@ Ml assertEvalError(Function> matcherSupplier) { return this; } + Ml assertEvalWarnings(Matcher> warningsMatcher) { + return assertEval(notNullValue(), null, null, warningsMatcher); + } + Ml assertEvalSame() { final Matchers.LearningMatcher resultMatcher = Matchers.learning(Object.class); @@ -481,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/ShellTest.java b/src/test/java/net/hydromatic/morel/ShellTest.java index 021594711..699ceb98b 100644 --- a/src/test/java/net/hydromatic/morel/ShellTest.java +++ b/src/test/java/net/hydromatic/morel/ShellTest.java @@ -281,6 +281,20 @@ private Matcher is2(String expected) { .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(); 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/simple.sml b/src/test/resources/script/simple.sml index 47ca00ee7..ccfb91a46 100644 --- a/src/test/resources/script/simple.sml +++ b/src/test/resources/script/simple.sml @@ -294,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 @@ -301,6 +302,7 @@ let in i + 1 end; +Sys.unset "matchCoverageEnabled"; (*) Patterns @@ -308,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) diff --git a/src/test/resources/script/simple.sml.out b/src/test/resources/script/simple.sml.out index db416ba2f..a51d57319 100644 --- a/src/test/resources/script/simple.sml.out +++ b/src/test/resources/script/simple.sml.out @@ -487,6 +487,9 @@ 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 @@ -502,6 +505,9 @@ end; uncaught exception Bind raised at: stdIn:2.7-2.29 +Sys.unset "matchCoverageEnabled"; +val it = () : unit + (*) Patterns @@ -516,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; @@ -524,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) diff --git a/src/test/resources/script/type.sml b/src/test/resources/script/type.sml index 1d4afc853..2f3ff2d98 100644 --- a/src/test/resources/script/type.sml +++ b/src/test/resources/script/type.sml @@ -66,7 +66,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..3acfa1b03 100644 --- a/src/test/resources/script/type.sml.out +++ b/src/test/resources/script/type.sml.out @@ -112,9 +112,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/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 From 49bc3c3f3d5a21af0fbf363eee597062278ed10c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 04:34:33 +0000 Subject: [PATCH 21/36] Bump calcite-core from 1.29.0 to 1.30.0 Bumps [calcite-core](https://github.com/apache/calcite) from 1.29.0 to 1.30.0. - [Release notes](https://github.com/apache/calcite/releases) - [Commits](https://github.com/apache/calcite/compare/calcite-1.29.0...calcite-1.30.0) --- updated-dependencies: - dependency-name: org.apache.calcite:calcite-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 23f82fa5f..c7d9e71fa 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ License. ${project.basedir} - 1.29.0 + 1.30.0 7.8.2 3.0.2 0.5 From b7e38814f686a749544b107a831fdf383c3853a8 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Thu, 24 Mar 2022 18:10:37 -0700 Subject: [PATCH 22/36] Bump checkstyle from 7.8.2 to 9.3 and maven-checkstyle-plugin from 3.0.0 to 3.1.2 Bumps [checkstyle](https://github.com/checkstyle/checkstyle) from 7.8.2 to 10.0. - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-7.8.2...checkstyle-10.0) Bumps [maven-checkstyle-plugin](https://github.com/apache/maven-checkstyle-plugin) from 3.0.0 to 3.1.2. - [Release notes](https://github.com/apache/maven-checkstyle-plugin/releases) - [Commits](https://github.com/apache/maven-checkstyle-plugin/compare/maven-checkstyle-plugin-3.0.0...maven-checkstyle-plugin-3.1.2) We now support and test checkstyle 9.3 and 10, but we use checkstyle 9.3 by default because checkstyle 10.0 requires JDK 11 or higher. Update a few Checkstyle rules, and tighten the line length a bit. Only check line length of Java files. Fix a few violations. Stop using the net.hydromatic.toolbox.checkstyle Checkstyle plugin. It apparently uses an obsolete Checkstyle SPI. Maybe we'll re-enable it one day. In POM, make plugin versions explicit. --- .github/workflows/main.yml | 6 ++ pom.xml | 25 ++++---- src/main/config/checkstyle/checker.xml | 64 +++++++++---------- .../java/net/hydromatic/morel/ast/Pos.java | 3 +- .../net/hydromatic/morel/compile/BuiltIn.java | 3 +- .../java/net/hydromatic/morel/util/Pair.java | 2 +- .../net/hydromatic/morel/util/Tracers.java | 4 +- .../java/net/hydromatic/morel/Matchers.java | 2 +- src/test/java/net/hydromatic/morel/Ml.java | 14 ++-- 9 files changed, 61 insertions(+), 62 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f2db9f86..8d3ad24eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: matrix: java-version: [ "8" ] guava-version: [ "21.0" ] + checkstyle-version: [ "" ] javadoc: [ false ] include: - java-version: "11" @@ -38,6 +39,7 @@ jobs: - java-version: "17" guava-version: "31.1-jre" javadoc: false + checkstyle-version: "10.0" steps: - uses: actions/checkout@v2 @@ -56,6 +58,10 @@ jobs: GOALS="$GOALS javadoc:javadoc javadoc:test-javadoc" fi 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 }}" diff --git a/pom.xml b/pom.xml index c7d9e71fa..98bdb007e 100644 --- a/pom.xml +++ b/pom.xml @@ -76,25 +76,29 @@ License. ${project.basedir} + 1.9 1.30.0 - 7.8.2 + + 9.3 3.0.2 0.5 + 2.1.9 23.0 2.2 2.5.1 - 0.3 1.1.2 3.0.3 7.0.10 3.21.0 5.8.2 - 3.0.0 + 3.1.2 + 2.3.2 3.0.0 3.3.2 3.2.2 3.11.0 + 2.2.1 3.0.0-M5 0.2 1.7.36 @@ -234,6 +238,7 @@ License. org.apache.maven.plugins maven-source-plugin + ${maven-source-plugin.version} attach-sources @@ -248,6 +253,7 @@ License. org.apache.maven.plugins maven-compiler-plugin + ${maven-compiler-plugin.version} 8 8 @@ -280,17 +286,6 @@ License. checkstyle ${checkstyle.version} - - net.hydromatic - toolbox - ${hydromatic-toolbox.version} - - - com.google.guava - guava - - - @@ -335,6 +330,7 @@ License. org.codehaus.mojo build-helper-maven-plugin + ${build-helper-maven-plugin.version} @@ -397,6 +393,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/ast/Pos.java b/src/main/java/net/hydromatic/morel/ast/Pos.java index ba65beaa9..b45686c10 100644 --- a/src/main/java/net/hydromatic/morel/ast/Pos.java +++ b/src/main/java/net/hydromatic/morel/ast/Pos.java @@ -41,7 +41,8 @@ public class Pos { public final int endColumn; /** Creates a Pos. */ - public Pos(String file, 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; diff --git a/src/main/java/net/hydromatic/morel/compile/BuiltIn.java b/src/main/java/net/hydromatic/morel/compile/BuiltIn.java index c926a6292..83afab927 100644 --- a/src/main/java/net/hydromatic/morel/compile/BuiltIn.java +++ b/src/main/java/net/hydromatic/morel/compile/BuiltIn.java @@ -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". * 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/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/test/java/net/hydromatic/morel/Matchers.java b/src/test/java/net/hydromatic/morel/Matchers.java index 8050ae48b..1871533a3 100644 --- a/src/test/java/net/hydromatic/morel/Matchers.java +++ b/src/test/java/net/hydromatic/morel/Matchers.java @@ -182,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; diff --git a/src/test/java/net/hydromatic/morel/Ml.java b/src/test/java/net/hydromatic/morel/Ml.java index 709ddc595..90a688d76 100644 --- a/src/test/java/net/hydromatic/morel/Ml.java +++ b/src/test/java/net/hydromatic/morel/Ml.java @@ -392,13 +392,13 @@ Ml assertMatchCoverage(MatchCoverage expectedCoverage) { 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")); - } - }; + @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 From 9aecee4c42f946b4eb71ea1506eaeb120d4a4f11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 04:38:34 +0000 Subject: [PATCH 23/36] Bump maven-source-plugin from 2.2.1 to 3.2.1 Bumps [maven-source-plugin](https://github.com/apache/maven-source-plugin) from 2.2.1 to 3.2.1. - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-2.2.1...maven-source-plugin-3.2.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 98bdb007e..c322243bd 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ License. 3.3.2 3.2.2 3.11.0 - 2.2.1 + 3.2.1 3.0.0-M5 0.2 1.7.36 From 91173750e9774c4ca1f92aa2a2656e0a3a28677d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 04:38:38 +0000 Subject: [PATCH 24/36] Bump maven-compiler-plugin from 2.3.2 to 3.10.1 Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 2.3.2 to 3.10.1. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-2.3.2...maven-compiler-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c322243bd..13662510a 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ License. 3.21.0 5.8.2 3.1.2 - 2.3.2 + 3.10.1 3.0.0 3.3.2 3.2.2 From a56205c919f400f73ded1ee1e9d7ce22c196c6f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 04:38:45 +0000 Subject: [PATCH 25/36] Bump build-helper-maven-plugin from 1.9 to 3.3.0 Bumps [build-helper-maven-plugin](https://github.com/mojohaus/build-helper-maven-plugin) from 1.9 to 3.3.0. - [Release notes](https://github.com/mojohaus/build-helper-maven-plugin/releases) - [Commits](https://github.com/mojohaus/build-helper-maven-plugin/compare/build-helper-maven-plugin-1.9...build-helper-maven-plugin-3.3.0) --- updated-dependencies: - dependency-name: org.codehaus.mojo:build-helper-maven-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13662510a..23c948149 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ License. ${project.basedir} - 1.9 + 3.3.0 1.30.0 9.3 From 8f772e435baa2566ed509da937c54c5508b49784 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 04:38:40 +0000 Subject: [PATCH 26/36] Bump git-commit-id-plugin from 2.1.9 to 4.9.10 Bumps git-commit-id-plugin from 2.1.9 to 4.9.10. --- updated-dependencies: - dependency-name: pl.project13.maven:git-commit-id-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 23c948149..af82e716f 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ License. 9.3 3.0.2 0.5 - 2.1.9 + 4.9.10 23.0 2.2 From aeb10508de8f44cef635dd1360b3de2ef5d2172e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 04:35:55 +0000 Subject: [PATCH 27/36] Bump maven-surefire-plugin from 3.0.0-M5 to 3.0.0-M6 Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M5 to 3.0.0-M6. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M5...surefire-3.0.0-M6) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af82e716f..0ea7bafbe 100644 --- a/pom.xml +++ b/pom.xml @@ -99,7 +99,7 @@ License. 3.2.2 3.11.0 3.2.1 - 3.0.0-M5 + 3.0.0-M6 0.2 1.7.36 From 95e81ed428500a44dd3dde7e554b22e5b442f5fd Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Wed, 6 Apr 2022 12:46:49 +0200 Subject: [PATCH 28/36] [MOREL-138] Type annotations in patterns, function declarations and expressions Fixes #138 --- docs/reference.md | 9 +- .../java/net/hydromatic/morel/ast/Ast.java | 7 +- .../net/hydromatic/morel/ast/AstBuilder.java | 10 +- .../java/net/hydromatic/morel/ast/Op.java | 2 +- .../net/hydromatic/morel/ast/Shuttle.java | 9 +- .../hydromatic/morel/compile/Resolver.java | 7 ++ .../morel/compile/TypeResolver.java | 58 ++++++++++- src/main/javacc/MorelParser.jj | 14 ++- .../java/net/hydromatic/morel/MainTest.java | 11 +++ src/test/resources/script/type.sml | 46 +++++++++ src/test/resources/script/type.sml.out | 99 +++++++++++++++++++ 11 files changed, 250 insertions(+), 22 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 8895d60cc..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 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/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/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/Resolver.java b/src/main/java/net/hydromatic/morel/compile/Resolver.java index 4d028a417..e0c162303 100644 --- a/src/main/java/net/hydromatic/morel/compile/Resolver.java +++ b/src/main/java/net/hydromatic/morel/compile/Resolver.java @@ -312,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: @@ -535,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)); diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index b7e1362bf..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); @@ -795,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: @@ -811,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); } } @@ -850,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)); @@ -963,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; diff --git a/src/main/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 90fa7dbf4..caf7ddc12 100644 --- a/src/main/javacc/MorelParser.jj +++ b/src/main/javacc/MorelParser.jj @@ -761,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". */ @@ -1077,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)); } } diff --git a/src/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index 1076be8e3..b0af24987 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -248,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") @@ -456,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() { diff --git a/src/test/resources/script/type.sml b/src/test/resources/script/type.sml index 2f3ff2d98..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 (); diff --git a/src/test/resources/script/type.sml.out b/src/test/resources/script/type.sml.out index 3acfa1b03..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 From 2ea5380172418debefbdc446485bb15b6ac1b7a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Apr 2022 04:39:40 +0000 Subject: [PATCH 29/36] Bump javacc from 7.0.10 to 7.0.11 Bumps [javacc](https://github.com/javacc/javacc) from 7.0.10 to 7.0.11. - [Release notes](https://github.com/javacc/javacc/releases) - [Changelog](https://github.com/javacc/javacc/blob/master/docs/release-notes.md) - [Commits](https://github.com/javacc/javacc/compare/javacc-7.0.10...javacc-7.0.11) --- updated-dependencies: - dependency-name: net.java.dev.javacc:javacc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0ea7bafbe..6b101adb4 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ License. 2.5.1 1.1.2 3.0.3 - 7.0.10 + 7.0.11 3.21.0 5.8.2 3.1.2 From b10a6137da4715d81b3acced0f5e66013582fa04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 04:42:04 +0000 Subject: [PATCH 30/36] Bump maven-site-plugin from 3.11.0 to 3.12.0 Bumps [maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.11.0 to 3.12.0. - [Release notes](https://github.com/apache/maven-site-plugin/releases) - [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.11.0...maven-site-plugin-3.12.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-site-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b101adb4..bfa47a48b 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ License. 3.0.0 3.3.2 3.2.2 - 3.11.0 + 3.12.0 3.2.1 3.0.0-M6 0.2 From 339f7d53f2252034843aa0895b7ffda4b9000d9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 04:42:11 +0000 Subject: [PATCH 31/36] Bump maven-javadoc-plugin from 3.3.2 to 3.4.0 Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.3.2 to 3.4.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.3.2...maven-javadoc-plugin-3.4.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bfa47a48b..5ccff9df0 100644 --- a/pom.xml +++ b/pom.xml @@ -95,7 +95,7 @@ License. 3.1.2 3.10.1 3.0.0 - 3.3.2 + 3.4.0 3.2.2 3.12.0 3.2.1 From 21031ef5776c97de65d3f6e065c32df36af373ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 04:36:50 +0000 Subject: [PATCH 32/36] Bump checkstyle from 9.3 to 10.2 Bumps [checkstyle](https://github.com/checkstyle/checkstyle) from 9.3 to 10.2. - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-9.3...checkstyle-10.2) --- updated-dependencies: - dependency-name: com.puppycrawl.tools:checkstyle dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5ccff9df0..153c8c33f 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,7 @@ License. 3.3.0 1.30.0 - 9.3 + 10.2 3.0.2 0.5 4.9.10 From 44a8f3e85fa78e2f71686a3dd323f3f6a8488e4c Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Mon, 25 Apr 2022 10:11:14 -0700 Subject: [PATCH 33/36] Default to Checkstyle version 10.2 and Guava 31.1-jre If you are building under JDK 8, you must override Checkstyle by adding `-Dcheckstyle.version=9.3` to the command line. In GitHub actions, downgrade to setup-java from v2 to v1, because apparently only v1 supports Java 18. --- .github/workflows/main.yml | 15 +++++++++------ README.md | 4 +++- pom.xml | 8 +++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8d3ad24eb..b89c79103 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,13 +25,14 @@ jobs: strategy: matrix: - java-version: [ "8" ] - guava-version: [ "21.0" ] + java-version: [ "11" ] + guava-version: [ "" ] checkstyle-version: [ "" ] javadoc: [ false ] include: - - java-version: "11" + - java-version: "8" guava-version: "19.0" + checkstyle-version: "9.3" javadoc: false - java-version: "17" guava-version: "23.0" @@ -39,14 +40,16 @@ jobs: - java-version: "17" guava-version: "31.1-jre" javadoc: false - checkstyle-version: "10.0" + 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' 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/pom.xml b/pom.xml index 153c8c33f..f38f8c8cf 100644 --- a/pom.xml +++ b/pom.xml @@ -78,13 +78,15 @@ License. 3.3.0 1.30.0 - + 10.2 3.0.2 0.5 4.9.10 - - 23.0 + + 31.1-jre 2.2 2.5.1 1.1.2 From a328fbe55c06894f6752269928e8d99f8e72f263 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Apr 2022 04:29:25 +0000 Subject: [PATCH 34/36] Bump maven-project-info-reports-plugin from 3.2.2 to 3.3.0 Bumps [maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.2.2 to 3.3.0. - [Release notes](https://github.com/apache/maven-project-info-reports-plugin/releases) - [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-3.2.2...maven-project-info-reports-plugin-3.3.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-project-info-reports-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f38f8c8cf..76d07840b 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ License. 3.10.1 3.0.0 3.4.0 - 3.2.2 + 3.3.0 3.12.0 3.2.1 3.0.0-M6 From 6040e55dd209e0451d79c6f43ee37fc240409474 Mon Sep 17 00:00:00 2001 From: Gavin Ray Date: Sat, 14 May 2022 16:36:21 -0400 Subject: [PATCH 35/36] Initial programmatic shell buildout --- .../hydromatic/morel/ProgrammaticShell.java | 222 ++++++++++++++++++ .../morel/ProgrammaticShellTest.java | 29 +++ 2 files changed, 251 insertions(+) create mode 100644 src/main/java/net/hydromatic/morel/ProgrammaticShell.java create mode 100644 src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java 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..338b5ecf3 --- /dev/null +++ b/src/main/java/net/hydromatic/morel/ProgrammaticShell.java @@ -0,0 +1,222 @@ +package net.hydromatic.morel; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +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.Objects; +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 boolean cacheEnabled = true; + private final Cache statementResultCache = + CacheBuilder.newBuilder().maximumSize(1000).recordStats().build(); + + 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); + } + + public boolean isCacheEnabled() { + return cacheEnabled; + } + + public void setCacheEnabled(boolean cacheEnabled) { + this.cacheEnabled = cacheEnabled; + } + + 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 (cacheEnabled && statement != null) { + boolean wasCached = writeCachedResultIfExists(outLines, statement); + if (wasCached) continue; + } + + 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 boolean writeCachedResultIfExists(Consumer outLines, AstNode statement) { + String statementAsString = statement.toString(); + EvaluationContext context = new EvaluationContext(statementAsString, env0); + String result = statementResultCache.getIfPresent(context); + if (result != null) { + outLines.accept(result); + return true; + } + return false; + } + + 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)); + + if (cacheEnabled) { + String statementAsString = statement.toString(); + EvaluationContext context = new EvaluationContext(statementAsString, env); + statementResultCache.put(context, outLines.toString()); + } + } 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); + } + } + + /**************************************************************/ + + /** + * Class used for caching compiled statements. + * Can simply compare instances to see if they are the same. + */ + static class EvaluationContext { + final String code; + final Environment env; + + EvaluationContext(String code, Environment env) { + this.code = code; + this.env = env; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EvaluationContext that = (EvaluationContext) o; + return code.equals(that.code) && env.equals(that.env); + } + + @Override + public int hashCode() { + return Objects.hash(code, env); + } + } +} 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..b70b14882 --- /dev/null +++ b/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java @@ -0,0 +1,29 @@ +package net.hydromatic.morel; + +import net.hydromatic.morel.foreign.Calcite; +import net.hydromatic.morel.foreign.ForeignValue; +import org.junit.jupiter.api.Test; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +class ProgrammaticShellTest { + + private static Map foreignValueMap = Calcite + .withDataSets(BuiltInDataSet.DICTIONARY) + .foreignValues(); + + @Test + void run() { + ProgrammaticShell shell = new ProgrammaticShell(foreignValueMap); + + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + + shell.run("scott;", writer, true); + writer.flush(); + + System.out.println(out); + } +} From 7722b2f289ed1ba856f5c9f52a25cfea979032e8 Mon Sep 17 00:00:00 2001 From: Gavin Ray Date: Sat, 14 May 2022 17:40:32 -0400 Subject: [PATCH 36/36] Initial working implementation --- .../hydromatic/morel/ProgrammaticShell.java | 66 ------------------- .../morel/ProgrammaticShellTest.java | 18 +++-- 2 files changed, 13 insertions(+), 71 deletions(-) diff --git a/src/main/java/net/hydromatic/morel/ProgrammaticShell.java b/src/main/java/net/hydromatic/morel/ProgrammaticShell.java index 338b5ecf3..02ee31085 100644 --- a/src/main/java/net/hydromatic/morel/ProgrammaticShell.java +++ b/src/main/java/net/hydromatic/morel/ProgrammaticShell.java @@ -1,7 +1,5 @@ package net.hydromatic.morel; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; import net.hydromatic.morel.ast.AstNode; import net.hydromatic.morel.ast.Pos; @@ -24,7 +22,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Consumer; @@ -42,10 +39,6 @@ public class ProgrammaticShell implements Session.Shell { private Environment env0; private Map foreignValueMap; - private boolean cacheEnabled = true; - private final Cache statementResultCache = - CacheBuilder.newBuilder().maximumSize(1000).recordStats().build(); - private final Session session = new Session(); private TypeSystem typeSystem = new TypeSystem(); @@ -68,14 +61,6 @@ public void setTypeSystem(TypeSystem typeSystem) { this.env0 = makeEnv(typeSystem, foreignValueMap); } - public boolean isCacheEnabled() { - return cacheEnabled; - } - - public void setCacheEnabled(boolean cacheEnabled) { - this.cacheEnabled = cacheEnabled; - } - private static Environment makeEnv(TypeSystem typeSystem, Map foreignValueMap) { return Environments.env(typeSystem, foreignValueMap); } @@ -91,11 +76,6 @@ public void run(String code, PrintWriter out, boolean echo) { parser.zero("stdIn"); final AstNode statement = parser.statementSemicolonOrEof(); - if (cacheEnabled && statement != null) { - boolean wasCached = writeCachedResultIfExists(outLines, statement); - if (wasCached) continue; - } - if (statement == null && code.endsWith("\n")) { code = code.substring(0, code.length() - 1); } @@ -131,17 +111,6 @@ public void run(String code, PrintWriter out, boolean echo) { } } - private boolean writeCachedResultIfExists(Consumer outLines, AstNode statement) { - String statementAsString = statement.toString(); - EvaluationContext context = new EvaluationContext(statementAsString, env0); - String result = statementResultCache.getIfPresent(context); - if (result != null) { - outLines.accept(result); - return true; - } - return false; - } - private void command(AstNode statement, Consumer outLines) { try { final Map outBindings = new LinkedHashMap<>(); @@ -154,12 +123,6 @@ private void command(AstNode statement, Consumer outLines) { final List bindings = new ArrayList<>(); compiled.eval(session, env, outLines, bindings::add); bindings.forEach(b -> outBindings.put(b.id.name, b)); - - if (cacheEnabled) { - String statementAsString = statement.toString(); - EvaluationContext context = new EvaluationContext(statementAsString, env); - statementResultCache.put(context, outLines.toString()); - } } catch (Codes.MorelRuntimeException e) { appendToOutput(e, outLines); } @@ -190,33 +153,4 @@ public void handle(RuntimeException e, StringBuilder buf) { buf.append(e); } } - - /**************************************************************/ - - /** - * Class used for caching compiled statements. - * Can simply compare instances to see if they are the same. - */ - static class EvaluationContext { - final String code; - final Environment env; - - EvaluationContext(String code, Environment env) { - this.code = code; - this.env = env; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - EvaluationContext that = (EvaluationContext) o; - return code.equals(that.code) && env.equals(that.env); - } - - @Override - public int hashCode() { - return Objects.hash(code, env); - } - } } diff --git a/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java b/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java index b70b14882..4bf5da78f 100644 --- a/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java +++ b/src/test/java/net/hydromatic/morel/ProgrammaticShellTest.java @@ -2,6 +2,7 @@ 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; @@ -10,9 +11,8 @@ class ProgrammaticShellTest { - private static Map foreignValueMap = Calcite - .withDataSets(BuiltInDataSet.DICTIONARY) - .foreignValues(); + private final Map foreignValueMap = + Calcite.withDataSets(BuiltInDataSet.DICTIONARY).foreignValues(); @Test void run() { @@ -21,9 +21,17 @@ void run() { StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out); - shell.run("scott;", writer, true); + 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); + 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); } }