diff --git a/bench/src/main/scala/sjsonnet/ProfilingEvaluator.scala b/bench/src/main/scala/sjsonnet/ProfilingEvaluator.scala index 01672038..9a2b85f6 100644 --- a/bench/src/main/scala/sjsonnet/ProfilingEvaluator.scala +++ b/bench/src/main/scala/sjsonnet/ProfilingEvaluator.scala @@ -154,7 +154,7 @@ class ProfilingEvaluator(resolver: CachedResolver, def builtins(): Seq[BuiltinBox] = { val names = new util.IdentityHashMap[Val.Func, String]() - new Std().functions.foreachEntry((n, f) => names.put(f, n)) + new Std(settings).functions.foreachEntry((n, f) => names.put(f, n)) val m = new mutable.HashMap[String, BuiltinBox] def add(b: ExprBox, func: Val.Builtin): Unit = { val n = names.getOrDefault(func, func.getClass.getName) diff --git a/sjsonnet/src-jvm-native/sjsonnet/Config.scala b/sjsonnet/src-jvm-native/sjsonnet/Config.scala index ff077f96..f5e73fb0 100644 --- a/sjsonnet/src-jvm-native/sjsonnet/Config.scala +++ b/sjsonnet/src-jvm-native/sjsonnet/Config.scala @@ -138,4 +138,9 @@ case class Config( doc = """Properly handle assertions defined in a Jsonnet dictionary that is extended more than once""" ) strictInheritedAssertions: Flag = Flag(), + @arg( + name = "strict-set-operations", + doc = """Strict set operations""" + ) + strictSetOperations: Flag = Flag(), ) diff --git a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala index 1ea9989c..a9b08757 100644 --- a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala +++ b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala @@ -50,8 +50,7 @@ object SjsonnetMain { stderr: PrintStream, wd: os.Path, allowedInputs: Option[Set[os.Path]] = None, - importer: Option[(Path, String) => Option[os.Path]] = None, - std: Val.Obj = new Std().Std): Int = { + importer: Option[(Path, String) => Option[os.Path]] = None): Int = { var hasWarnings = false def warn(msg: String): Unit = { @@ -73,7 +72,7 @@ object SjsonnetMain { Left("error: -i/--interactive must be passed in as the first argument") }else Right(config.file) } - outputStr <- mainConfigured(file, config, parseCache, wd, allowedInputs, importer, warn, std) + outputStr <- mainConfigured(file, config, parseCache, wd, allowedInputs, importer, warn) res <- { if(hasWarnings && config.fatalWarnings.value) Left("") else Right(outputStr) @@ -175,8 +174,7 @@ object SjsonnetMain { wd: os.Path, allowedInputs: Option[Set[os.Path]] = None, importer: Option[(Path, String) => Option[os.Path]] = None, - warnLogger: String => Unit = null, - std: Val.Obj = new Std().Std): Either[String, String] = { + warnLogger: String => Unit = null): Either[String, String] = { val (jsonnetCode, path) = if (config.exec.value) (file, wd / "\uFE64exec\uFE65") @@ -198,6 +196,15 @@ object SjsonnetMain { ) var currentPos: Position = null + val settings = new Settings( + preserveOrder = config.preserveOrder.value, + strict = config.strict.value, + noStaticErrors = config.noStaticErrors.value, + noDuplicateKeysInComprehension = config.noDuplicateKeysInComprehension.value, + strictImportSyntax = config.strictImportSyntax.value, + strictInheritedAssertions = config.strictInheritedAssertions.value, + strictSetOperations = config.strictSetOperations.value + ) val interp = new Interpreter( extBinding, tlaBinding, @@ -213,17 +220,9 @@ object SjsonnetMain { case None => resolveImport(config.jpaths.map(os.Path(_, wd)).map(OsPath(_)), allowedInputs) }, parseCache, - settings = new Settings( - preserveOrder = config.preserveOrder.value, - strict = config.strict.value, - noStaticErrors = config.noStaticErrors.value, - noDuplicateKeysInComprehension = config.noDuplicateKeysInComprehension.value, - strictImportSyntax = config.strictImportSyntax.value, - strictInheritedAssertions = config.strictInheritedAssertions.value - ), + settings = settings, storePos = if (config.yamlDebug.value) currentPos = _ else null, warnLogger = warnLogger, - std = std ) (config.multi, config.yamlStream.value) match { diff --git a/sjsonnet/src/sjsonnet/Interpreter.scala b/sjsonnet/src/sjsonnet/Interpreter.scala index b82ed5dc..be90c6d1 100644 --- a/sjsonnet/src/sjsonnet/Interpreter.scala +++ b/sjsonnet/src/sjsonnet/Interpreter.scala @@ -20,12 +20,12 @@ class Interpreter(extVars: Map[String, String], settings: Settings = Settings.default, storePos: Position => Unit = null, warnLogger: (String => Unit) = null, - std: Val.Obj = new Std().Std ) { self => private val internedStrings = new mutable.HashMap[String, String] private val internedStaticFieldSets = new mutable.HashMap[Val.StaticObjectFieldSet, java.util.LinkedHashMap[String, java.lang.Boolean]] + private val std = new Std(settings).Std val resolver = new CachedResolver(importer, parseCache, settings.strictImportSyntax, internedStrings, internedStaticFieldSets) { override def process(expr: Expr, fs: FileScope): Either[Error, (Expr, FileScope)] = diff --git a/sjsonnet/src/sjsonnet/Settings.scala b/sjsonnet/src/sjsonnet/Settings.scala index 8ba6202f..37638e3b 100644 --- a/sjsonnet/src/sjsonnet/Settings.scala +++ b/sjsonnet/src/sjsonnet/Settings.scala @@ -9,6 +9,7 @@ class Settings( val noDuplicateKeysInComprehension: Boolean = false, val strictImportSyntax: Boolean = false, val strictInheritedAssertions: Boolean = false, + val strictSetOperations: Boolean = false, ) object Settings { diff --git a/sjsonnet/src/sjsonnet/Std.scala b/sjsonnet/src/sjsonnet/Std.scala index b2526318..e754b1b9 100644 --- a/sjsonnet/src/sjsonnet/Std.scala +++ b/sjsonnet/src/sjsonnet/Std.scala @@ -8,6 +8,7 @@ import java.util.regex.Pattern import sjsonnet.Expr.Member.Visibility import sjsonnet.Expr.BinaryOp +import scala.collection.Searching.Found import scala.collection.mutable import scala.util.matching.Regex @@ -16,7 +17,7 @@ import scala.util.matching.Regex * in Scala code. Uses `builtin` and other helpers to handle the common wrapper * logic automatically */ -class Std { +class Std(settings: Settings) { private val dummyPos: Position = new Position(null, 0) private val emptyLazyArray = new Array[Lazy](0) @@ -637,92 +638,6 @@ class Std { } } - private object SetInter extends Val.Builtin3("a", "b", "keyF", Array(null, null, Val.False(dummyPos))) { - private def isStr(a: Val.Arr) = a.forall(_.isInstanceOf[Val.Str]) - - override def specialize(args: Array[Expr]): (Val.Builtin, Array[Expr]) = args match { - case Array(a: Val.Arr, b) if isStr(a) => (new Spec1Str(a), Array(b)) - case Array(a, b: Val.Arr) if isStr(b) => (new Spec1Str(b), Array(a)) - case args if args.length == 2 => (Spec2, args) - case _ => null - } - - def asArray(a: Val): Array[Lazy] = a match { - case arr: Val.Arr => arr.asLazyArray - case str: Val.Str => stringChars(pos, str.value).asLazyArray - case _ => Error.fail("Arguments must be either arrays or strings") - } - - def evalRhs(_a: Val, _b: Val, _keyF: Val, ev: EvalScope, pos: Position): Val = { - if(_keyF.isInstanceOf[Val.False]) Spec2.evalRhs(_a, _b, ev, pos) - else { - val a = asArray(_a) - val b = asArray(_b) - val keyFFunc = _keyF.asInstanceOf[Val.Func] - val out = new mutable.ArrayBuffer[Lazy] - for (v <- a) { - val appliedX = keyFFunc.apply1(v, pos.noOffset)(ev) - if (b.exists(value => { - val appliedValue = keyFFunc.apply1(value, pos.noOffset)(ev) - ev.equal(appliedValue, appliedX) - }) && !out.exists(value => { - val mValue = keyFFunc.apply1(value, pos.noOffset)(ev) - ev.equal(mValue, appliedX) - })) { - out.append(v) - } - } - sortArr(pos, ev, new Val.Arr(pos, out.toArray), keyFFunc) - } - } - - private object Spec2 extends Val.Builtin2("a", "b") { - def evalRhs(_a: Val, _b: Val, ev: EvalScope, pos: Position): Val = { - val a = asArray(_a) - val b = asArray(_b) - val out = new mutable.ArrayBuffer[Lazy](a.length) - for (v <- a) { - val vf = v.force - if (b.exists(value => { - ev.equal(value.force, vf) - }) && !out.exists(value => { - ev.equal(value.force, vf) - })) { - out.append(v) - } - } - sortArr(pos, ev, new Val.Arr(pos, out.toArray), null) - } - } - - private class Spec1Str(_a: Val.Arr) extends Val.Builtin1("b") { - private[this] val a = - ArrayOps.sortInPlaceBy(ArrayOps.distinctBy(_a.asLazyArray)(_.asInstanceOf[Val.Str].value))(_.asInstanceOf[Val.Str].value) - // 2.13+: _a.asLazyArray.distinctBy(_.asInstanceOf[Val.Str].value).sortInPlaceBy(_.asInstanceOf[Val.Str].value) - - def evalRhs(_b: Val, ev: EvalScope, pos: Position): Val = { - val b = asArray(_b) - val bs = new mutable.HashSet[String] - var i = 0 - while(i < b.length) { - b(i).force match { - case s: Val.Str => bs.add(s.value) - case _ => - } - i += 1 - } - val out = new mutable.ArrayBuilder.ofRef[Lazy] - i = 0 - while(i < a.length) { - val s = a(i).asInstanceOf[Val.Str] - if(bs.contains(s.value)) out.+=(s) - i += 1 - } - new Val.Arr(pos, out.result()) - } - } - } - val functions: Map[String, Val.Func] = Map( "assertEqual" -> AssertEqual, "toString" -> ToString, @@ -1141,17 +1056,15 @@ class Std { val concat = new Val.Arr(pos, a ++ b) uniqArr(pos, ev, sortArr(pos, ev, concat, args(2)), args(2)) }, - "setInter" -> SetInter, - builtinWithDefaults("setDiff", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => - + builtinWithDefaults("setInter", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => val a = args(0) match { case arr: Val.Arr => arr.asLazyArray - case str: Val.Str => stringChars(pos, str.value).asLazyArray + case str: Val.Str if !settings.strictSetOperations => stringChars(pos, str.value).asLazyArray case _ => Error.fail("Arguments must be either arrays or strings") } val b = args(1) match { case arr: Val.Arr => arr.asLazyArray - case str: Val.Str => stringChars(pos, str.value).asLazyArray + case str: Val.Str if !settings.strictSetOperations => stringChars(pos, str.value).asLazyArray case _ => Error.fail("Arguments must be either arrays or strings") } @@ -1159,50 +1072,48 @@ class Std { val out = new mutable.ArrayBuffer[Lazy] for (v <- a) { - if (keyF.isInstanceOf[Val.False]) { - val vf = v.force - if (!b.exists(value => { - ev.equal(value.force, vf) - }) && !out.exists(value => { - ev.equal(value.force, vf) - })) { - out.append(v) - } - } else { - val keyFFunc = keyF.asInstanceOf[Val.Func] - val appliedX = keyFFunc.apply1(v, pos.noOffset)(ev) - - if (!b.exists(value => { - val appliedValue = keyFFunc.apply1(value, pos.noOffset)(ev) - ev.equal(appliedValue, appliedX) - }) && !out.exists(value => { - val mValue = keyFFunc.apply1(value, pos.noOffset)(ev) - ev.equal(mValue, appliedX) - })) { - out.append(v) - } + if (existsInSet(ev, pos, keyF, b, v.force) && !existsInSet(ev, pos, keyF, out.toArray, v.force)) { + out.append(v) } } - - sortArr(pos, ev, new Val.Arr(pos, out.toArray), keyF) + if (settings.strictSetOperations) { + new Val.Arr(pos, out.toArray) + } else { + sortArr(pos, ev, new Val.Arr(pos, out.toArray), keyF) + } }, - builtinWithDefaults("setMember", "x" -> null, "arr" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => + builtinWithDefaults("setDiff", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => + val a = args(0) match { + case arr: Val.Arr => arr.asLazyArray + case str: Val.Str if !settings.strictSetOperations => stringChars(pos, str.value).asLazyArray + case _ => Error.fail("Arguments must be either arrays or strings") + } + val b = args(1) match { + case arr: Val.Arr => arr.asLazyArray + case str: Val.Str if !settings.strictSetOperations => stringChars(pos, str.value).asLazyArray + case _ => Error.fail("Arguments must be either arrays or strings") + } + val keyF = args(2) + val out = new mutable.ArrayBuffer[Lazy] + + for (v <- a) { + if (!existsInSet(ev, pos, keyF, b, v.force) && !existsInSet(ev, pos, keyF, out.toArray, v.force)) { + out.append(v) + } + } - if (keyF.isInstanceOf[Val.False]) { - val ujson.Arr(mArr) = Materializer(args(1))(ev) - val mx = Materializer(args(0))(ev) - mArr.contains(mx) + if (settings.strictSetOperations) { + new Val.Arr(pos, out.toArray) } else { - val arr = args(1).asInstanceOf[Val.Arr].asLazyArray - val keyFFunc = keyF.asInstanceOf[Val.Func] - val appliedX = keyFFunc.apply1(args(0), pos.noOffset)(ev) - arr.exists(value => { - val appliedValue = keyFFunc.apply1(value, pos.noOffset)(ev) - ev.equal(appliedValue, appliedX) - }) + sortArr(pos, ev, new Val.Arr(pos, out.toArray), keyF) } }, + builtinWithDefaults("setMember", "x" -> null, "arr" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => + val keyF = args(2) + val arr = args(1).asInstanceOf[Val.Arr].asLazyArray + existsInSet(ev, pos, keyF, arr, args(0)) + }, "split" -> Split, "splitLimit" -> SplitLimit, @@ -1281,6 +1192,37 @@ class Std { Platform.sha3(str) }, ) + + private def existsInSet(ev: EvalScope, pos: Position, keyF: Val, arr: Array[Lazy], toFind: Val): Boolean = { + val appliedX = keyF match { + case keyFFunc: Val.Func => keyFFunc.apply1(toFind, pos.noOffset)(ev) + case _ => toFind + } + System.out.println(appliedX.force) + if (settings.strictSetOperations) { + arr.search(appliedX.force)((toFind: Lazy, value: Lazy) => { + val appliedValue = keyF match { + case keyFFunc: Val.Func => keyFFunc.apply1(value, pos.noOffset)(ev).force + case _ => value.force + } + toFind.force match { + case s: Val.Str if appliedValue.isInstanceOf[Val.Str] => Ordering.String.compare(s.asString, appliedValue.force.asString) + case n: Val.Num if appliedValue.isInstanceOf[Val.Num] => Ordering.Double.TotalOrdering.compare(n.asDouble, appliedValue.force.asDouble) + case t: Val.Bool if appliedValue.isInstanceOf[Val.Bool] => Ordering.Boolean.compare(t.asBoolean, appliedValue.force.asBoolean) + case _ => Error.fail("Cannot call setMember on " + toFind.force.prettyName + " and " + appliedValue.force.prettyName) + } + }).isInstanceOf[Found] + } else { + arr.exists(value => { + val appliedValue = keyF match { + case keyFFunc: Val.Func => keyFFunc.apply1(value, pos.noOffset)(ev) + case _ => value + } + ev.equal(appliedValue.force, appliedX.force) + }) + } + } + val Std: Val.Obj = Val.Obj.mk( null, functions.toSeq diff --git a/sjsonnet/test/src-jvm/sjsonnet/Example.java b/sjsonnet/test/src-jvm/sjsonnet/Example.java index 44499f68..88ff5a29 100644 --- a/sjsonnet/test/src-jvm/sjsonnet/Example.java +++ b/sjsonnet/test/src-jvm/sjsonnet/Example.java @@ -10,8 +10,7 @@ public void example(){ System.err, os.package$.MODULE$.pwd(), scala.None$.empty(), - scala.None$.empty(), - new sjsonnet.Std().Std() + scala.None$.empty() ); } } diff --git a/sjsonnet/test/src/sjsonnet/PreserveOrderTests.scala b/sjsonnet/test/src/sjsonnet/PreserveOrderTests.scala index 40d9aadd..4439adf4 100644 --- a/sjsonnet/test/src/sjsonnet/PreserveOrderTests.scala +++ b/sjsonnet/test/src/sjsonnet/PreserveOrderTests.scala @@ -309,9 +309,7 @@ object PreserveOrderTests extends TestSuite { } test("preserveOrderPreservesSetMembership") { - eval("""std.setMember({a: 1, b: 2}, [{b: 2, a: 1}])""", true).toString ==> "true" - - eval("""std.setMember({q: {a: 1, b: 2}}, [{q: {b: 2, a: 1}}], keyF=function(v) v.q)""", true).toString ==> "true" + eval("""std.setMember({q: {a: 1, b: 2}}, [{q: {b: 2, a: 1}}], keyF=function(v) v.q.a)""", true).toString ==> "true" } test("preserveOrderSetIntersection") { diff --git a/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala b/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala index 1070915b..eeb144b9 100644 --- a/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala +++ b/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala @@ -6,12 +6,12 @@ object StdWithKeyFTests extends TestSuite { def tests = Tests { test("stdSetMemberWithKeyF") { - eval("std.setMember(\"a\", [\"a\", \"b\", \"c\"], function(x) x)") ==> ujson.True - eval("std.setMember(\"a\", [\"a\", \"b\", \"c\"])") ==> ujson.True - eval("std.setMember(\"d\", [\"a\", \"b\", \"c\"], function(x) x)") ==> ujson.False + eval("std.setMember(\"a\", std.set([\"a\", \"b\", \"c\"], function(x) x), function(x) x)") ==> ujson.True + eval("std.setMember(\"a\", std.set([\"a\", \"b\", \"c\"]))") ==> ujson.True + eval("std.setMember(\"d\", std.set([\"a\", \"b\", \"c\"], function(x) x), function(x) x)") ==> ujson.False eval( - """local arr = [ + """local arr = std.set([ { "name": "Foo", "language": { @@ -33,7 +33,7 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; + ], function(x) x.language.name); local testObj = { "name": "TestObj", @@ -154,10 +154,11 @@ object StdWithKeyFTests extends TestSuite { """[{"language":{"name":"C++","version":"n/a"},"name":"FooBar"},{"language":{"name":"Java","version":"1.8"},"name":"Foo"}]""" } test("stdSetUnionWithKeyF") { - eval("std.setUnion([\"c\", \"c\", \"b\"], [\"b\", \"b\", \"a\", \"b\", \"a\"])").toString() ==> """["a","b","c"]""" + eval("std.setUnion(std.set([\"c\", \"c\", \"b\"]), std.set([\"b\", \"b\", \"a\", \"b\", \"a\"]))").toString() ==> + """["a","b","c"]""" eval( - """local arr1 = [ + """local arr1 = std.set([ { "name": "Foo", "language": { @@ -179,8 +180,8 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; - local arr2 = [ + ], function(x) x.language.name); + local arr2 = std.set([ { "name": "Foo", "language": { @@ -202,7 +203,7 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; + ], function(x) x.language.name); std.setUnion(arr1, arr2, function(x) x.language.name)""").toString() ==> """[{"language":{"name":"C++","version":"n/a"},"name":"FooBar"},{"language":{"name":"Java","version":"1.8"},"name":"Foo"},{"language":{"name":"Scala","version":"2.13"},"name":"Bar"}]""" @@ -211,7 +212,7 @@ object StdWithKeyFTests extends TestSuite { eval("std.setInter([\"c\", \"c\", \"b\"], [\"b\", \"b\", \"a\", \"b\", \"a\"])").toString() ==> """["b"]""" eval( - """local arr1 = [ + """local arr1 = std.set([ { "name": "Foo", "language": { @@ -233,8 +234,8 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; - local arr2 = [ + ], function(x) x.language.name); + local arr2 = std.set([ { "name": "Foo", "language": { @@ -256,7 +257,7 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; + ], function(x) x.language.name); std.setInter(arr1, arr2, function(x) x.language.name)""").toString() ==> """[{"language":{"name":"C++","version":"n/a"},"name":"FooBar"},{"language":{"name":"Java","version":"1.8"},"name":"Foo"}]""" @@ -265,7 +266,7 @@ object StdWithKeyFTests extends TestSuite { eval("std.setDiff([\"c\", \"c\", \"b\"], [\"b\", \"b\", \"a\", \"b\", \"a\"])").toString() ==> """["c"]""" eval( - """local arr1 = [ + """local arr1 = std.set([ { "name": "Foo", "language": { @@ -287,8 +288,8 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; - local arr2 = [ + ], function(x) x.language.name); + local arr2 = std.set([ { "name": "Foo", "language": { @@ -310,7 +311,7 @@ object StdWithKeyFTests extends TestSuite { "version": "n/a" } } - ]; + ], function(x) x.language.name); std.setDiff(arr1, arr2, function(x) x.language.name)""").toString() ==> """[{"language":{"name":"Scala","version":"2.13"},"name":"Bar"}]""" diff --git a/sjsonnet/test/src/sjsonnet/TestUtils.scala b/sjsonnet/test/src/sjsonnet/TestUtils.scala index 511ef0f7..72adb221 100644 --- a/sjsonnet/test/src/sjsonnet/TestUtils.scala +++ b/sjsonnet/test/src/sjsonnet/TestUtils.scala @@ -5,7 +5,8 @@ object TestUtils { preserveOrder: Boolean = false, strict: Boolean = false, noDuplicateKeysInComprehension: Boolean = false, - strictInheritedAssertions: Boolean = false) = { + strictInheritedAssertions: Boolean = false, + strictSetOperations: Boolean = true) = { new Interpreter( Map(), Map(), @@ -16,7 +17,8 @@ object TestUtils { preserveOrder = preserveOrder, strict = strict, noDuplicateKeysInComprehension = noDuplicateKeysInComprehension, - strictInheritedAssertions = strictInheritedAssertions + strictInheritedAssertions = strictInheritedAssertions, + strictSetOperations = strictSetOperations ) ).interpret(s, DummyPath("(memory)")) } @@ -25,8 +27,9 @@ object TestUtils { preserveOrder: Boolean = false, strict: Boolean = false, noDuplicateKeysInComprehension: Boolean = false, - strictInheritedAssertions: Boolean = false) = { - eval0(s, preserveOrder, strict, noDuplicateKeysInComprehension, strictInheritedAssertions) match { + strictInheritedAssertions: Boolean = false, + strictSetOperations: Boolean = true) = { + eval0(s, preserveOrder, strict, noDuplicateKeysInComprehension, strictInheritedAssertions, strictSetOperations) match { case Right(x) => x case Left(e) => throw new Exception(e) } @@ -36,8 +39,9 @@ object TestUtils { preserveOrder: Boolean = false, strict: Boolean = false, noDuplicateKeysInComprehension: Boolean = false, - strictInheritedAssertions: Boolean = false) = { - eval0(s, preserveOrder, strict, noDuplicateKeysInComprehension, strictInheritedAssertions) match{ + strictInheritedAssertions: Boolean = false, + strictSetOperations: Boolean = true) = { + eval0(s, preserveOrder, strict, noDuplicateKeysInComprehension, strictInheritedAssertions, strictSetOperations) match{ case Left(err) => err.split('\n').map(_.trim).mkString("\n") // normalize inconsistent indenation on JVM vs JS case Right(r) => throw new Exception(s"Expected exception, got result: $r") }