From 864bd999190a3c402c5af1a7692db15d53e713c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Immanuel=20Brachth=C3=A4user?= Date: Wed, 22 Jan 2025 13:47:36 +0100 Subject: [PATCH] Add more VM tests (#788) I added very basic support for regexes in the VM so that we can run the case studies. I also had to move `regex.effekt` to common to do that, even though it is not supported by LLVM. --- .../effekt/context/VirtualModuleDB.scala | 8 +- .../scala/effekt/context/IOModuleDB.scala | 8 +- .../src/test/scala/effekt/core/VMTests.scala | 119 +++++++++++++++++- .../main/scala/effekt/core/vm/Builtin.scala | 86 ++++++++++++- .../src/main/scala/effekt/core/vm/VM.scala | 4 +- examples/benchmarks/other/emit.check | 1 + examples/benchmarks/other/emit.effekt | 40 ++++++ examples/casestudies/lexer.effekt.md | 2 +- examples/casestudies/parser.effekt.md | 4 +- examples/chez/libraries.effekt | 2 +- examples/neg/unbound_type.effekt | 2 +- examples/pos/simpleparser.effekt | 2 +- libraries/chez/common/text/regex.effekt | 31 ----- libraries/common/effekt.effekt | 5 +- libraries/common/regex.effekt | 64 ++++++++++ libraries/common/string.effekt | 7 ++ libraries/js/text/regex.effekt | 33 ----- 17 files changed, 330 insertions(+), 88 deletions(-) create mode 100644 examples/benchmarks/other/emit.check create mode 100644 examples/benchmarks/other/emit.effekt delete mode 100644 libraries/chez/common/text/regex.effekt create mode 100644 libraries/common/regex.effekt delete mode 100644 libraries/js/text/regex.effekt diff --git a/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala b/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala index c154f20c1..4dbf27700 100644 --- a/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala +++ b/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala @@ -17,11 +17,9 @@ trait VirtualModuleDB extends ModuleDB { self: Context => * used by Namer to resolve FFI includes */ override def contentsOf(path: String): Option[String] = { - val f = file(module.source.name).parent / path - if (!f.exists) { - None - } else { - Some(f.read) + val parent = file(module.source.name).parent + (parent :: config.includes().map(file)).collectFirst { + case base if (base / path).exists => (base / path).read } } diff --git a/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala b/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala index bc8781998..4d3ac96ea 100644 --- a/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala +++ b/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala @@ -13,11 +13,9 @@ trait IOModuleDB extends ModuleDB { self: Context => * used by Namer to resolve FFI includes */ override def contentsOf(path: String): Option[String] = { - val includeFile = file(module.source.name).parent / path - if (!includeFile.exists) { - None - } else { - Some(FileSource(includeFile.toString).content) + val parent = file(module.source.name).parent + (parent :: config.includes().map(file)).collectFirst { + case base if (base / path).exists => FileSource((base / path).toString).content } } diff --git a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala index 3fb2fd110..8d9a1f1b2 100644 --- a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala @@ -674,6 +674,54 @@ class VMTests extends munit.FunSuite { resumes = 7 )), + examplesDir / "casestudies" / "lexer.effekt.md" -> Some(Summary( + staticDispatches = 245, + dynamicDispatches = 18, + patternMatches = 298, + branches = 405, + pushedFrames = 703, + poppedFrames = 703, + allocations = 202, + closures = 27, + variableReads = 164, + variableWrites = 51, + resets = 31, + shifts = 11, + resumes = 11 + )), + + examplesDir / "casestudies" / "parser.effekt.md" -> Some(Summary( + staticDispatches = 8845, + dynamicDispatches = 783, + patternMatches = 13502, + branches = 14892, + pushedFrames = 28523, + poppedFrames = 28499, + allocations = 7923, + closures = 521, + variableReads = 6742, + variableWrites = 1901, + resets = 806, + shifts = 855, + resumes = 839 + )), + + examplesDir / "casestudies" / "anf.effekt.md" -> Some(Summary( + staticDispatches = 4775, + dynamicDispatches = 443, + patternMatches = 7272, + branches = 8110, + pushedFrames = 16275, + poppedFrames = 16260, + allocations = 4317, + closures = 358, + variableReads = 4080, + variableWrites = 1343, + resets = 481, + shifts = 660, + resumes = 644 + )), + examplesDir / "casestudies" / "inference.effekt.md" -> Some(Summary( staticDispatches = 1457444, dynamicDispatches = 3201452, @@ -689,13 +737,80 @@ class VMTests extends munit.FunSuite { shifts = 297723, resumes = 9275 )), + + examplesDir / "pos" / "raytracer.effekt" -> Some(Summary( + staticDispatches = 79696, + dynamicDispatches = 0, + patternMatches = 1014772, + branches = 71995, + pushedFrames = 223269, + poppedFrames = 223269, + allocations = 127533, + closures = 0, + variableReads = 77886, + variableWrites = 26904, + resets = 0, + shifts = 0, + resumes = 0 + )), + ) + + val other: Seq[(File, Option[Summary])] = Seq( + examplesDir / "benchmarks" / "other" / "emit.effekt" -> Some(Summary( + staticDispatches = 11, + dynamicDispatches = 0, + patternMatches = 0, + branches = 11, + pushedFrames = 102, + poppedFrames = 102, + allocations = 0, + closures = 0, + variableReads = 61, + variableWrites = 30, + resets = 1, + shifts = 10, + resumes = 10 + )), + + examplesDir / "benchmarks" / "other" / "church_exponentiation.effekt" -> Some(Summary( + staticDispatches = 7, + dynamicDispatches = 1062912, + patternMatches = 0, + branches = 5, + pushedFrames = 531467, + poppedFrames = 531467, + allocations = 0, + closures = 265750, + variableReads = 0, + variableWrites = 0, + resets = 0, + shifts = 0, + resumes = 0 + )), + + examplesDir / "benchmarks" / "other" / "variadic_combinators.effekt" -> Some(Summary( + staticDispatches = 27057, + dynamicDispatches = 9009, + patternMatches = 30052, + branches = 3003, + pushedFrames = 54105, + poppedFrames = 54105, + allocations = 24060, + closures = 12030, + variableReads = 24048, + variableWrites = 18036, + resets = 0, + shifts = 0, + resumes = 0 + )), ) val testFiles: Seq[(File, Option[Summary])] = are_we_fast_yet ++ duality_of_compilation ++ effect_handlers_bench ++ - casestudies + casestudies ++ + other def runTest(f: File, expectedSummary: Option[Summary]): Unit = val path = f.getPath @@ -711,6 +826,4 @@ class VMTests extends munit.FunSuite { } testFiles.foreach(runTest) - - } diff --git a/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala b/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala index 0953259ce..1b3209fa3 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala @@ -3,6 +3,8 @@ package core package vm import java.io.PrintStream +import scala.util.matching as regex +import scala.util.matching.Regex trait Runtime { def out: PrintStream @@ -193,12 +195,56 @@ lazy val strings: Builtins = Map( }, builtin("effekt::inspect(Any)") { - case any :: Nil => Value.String(inspect(any)) + case any :: Nil => + Runtime.out.println(inspect(any)) + Value.Unit() }, builtin("effekt::infixEq(String, String)") { case As.String(x) :: As.String(y) :: Nil => Value.Bool(x == y) }, + + builtin("effekt::length(String)") { + case As.String(x) :: Nil => Value.Int(x.length) + }, + + builtin("effekt::substring(String, Int, Int)") { + case As.String(x) :: As.Int(from) :: As.Int(to) :: Nil => Value.String(x.substring(from.toInt, to.toInt)) + }, + + builtin("string::unsafeCharAt(String, Int)") { + case As.String(x) :: As.Int(at) :: Nil => Value.Int(x.charAt(at.toInt).toLong) + }, + + builtin("string::toInt(Char)") { + case As.Int(n) :: Nil => Value.Int(n) + }, + + builtin("string::toChar(Int)") { + case As.Int(n) :: Nil => Value.Int(n) + }, + + builtin("string::infixLte(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => Value.Bool(x <= y) + }, + + builtin("string::infixLt(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => Value.Bool(x < y) + }, + + builtin("string::infixGt(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => Value.Bool(x > y) + }, + + builtin("string::infixGte(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => Value.Bool(x >= y) + }, +) + +lazy val chars: Builtins = Map( + builtin("effekt::infixEq(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => Value.Bool(x == y) + }, ) lazy val arrays: Builtins = Map( @@ -216,6 +262,12 @@ lazy val arrays: Builtins = Map( }, ) +lazy val undefined: Builtins = Map( + builtin("effekt::isUndefined[A](A)") { + case Value.Literal(m) :: Nil => Value.Bool(m == null) + }, +) + lazy val refs: Builtins = Map( builtin("ref::ref[T](T)") { case init :: Nil => Value.Ref(Reference(init)) @@ -228,7 +280,22 @@ lazy val refs: Builtins = Map( }, ) -lazy val builtins: Builtins = printing ++ integers ++ doubles ++ booleans ++ strings ++ arrays ++ refs +lazy val regexes: Builtins = Map( + builtin("regex::regex(String)") { + case As.String(str) :: Nil => Value.Literal(new Regex(str)) + }, + builtin("regex::exec(Regex, String)") { + case As.Regex(r) :: As.String(str) :: Nil => Value.Literal(r.findFirstMatchIn(str).orNull) + }, + builtin("regex::matched(RegexMatch)") { + case As.RegexMatch(m) :: Nil => Value.String(m.matched) + }, + builtin("regex::index(RegexMatch)") { + case As.RegexMatch(m) :: Nil => Value.Int(m.start) + }, +) + +lazy val builtins: Builtins = printing ++ integers ++ doubles ++ booleans ++ strings ++ arrays ++ refs ++ chars ++ regexes ++ undefined protected object As { object String { @@ -240,6 +307,8 @@ protected object As { object Int { def unapply(v: Value): Option[scala.Long] = v match { case Value.Literal(value: scala.Long) => Some(value) + case Value.Literal(value: scala.Int) => Some(value.toLong) + case Value.Literal(value: java.lang.Integer) => Some(value.toLong) case _ => None } } @@ -267,4 +336,17 @@ protected object As { case _ => None } } + object Regex { + def unapply(v: Value): Option[regex.Regex] = v match { + case Value.Literal(v: regex.Regex) => Some(v) + case _ => None + } + } + object RegexMatch { + def unapply(v: Value): Option[regex.Regex.Match | Null] = v match { + case Value.Literal(null) => Some(null) + case Value.Literal(v: regex.Regex.Match) => Some(v) + case _ => None + } + } } diff --git a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala index 82f7799ae..98bfbd9ec 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -491,7 +491,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { val arguments = vargs.map(a => eval(a, env)) instrumentation.builtin(name) try { impl(runtime)(arguments) } catch { case e => sys error s"Cannot call ${b} with arguments ${arguments.map { - case Value.Literal(l) => s"${l}: ${l.getClass.getName}" + case Value.Literal(l) => s"${l}: ${l.getClass.getName}\n${e.getMessage}" case other => other.toString }.mkString(", ")}" } } @@ -503,7 +503,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { val arguments = vargs.map(a => eval(a, env)) instrumentation.builtin(name) try { impl(runtime)(arguments) } catch { case e => sys error s"Cannot call ${x} with arguments ${arguments.map { - case Value.Literal(l) => s"${l}: ${l.getClass.getName}" + case Value.Literal(l) => s"${l}: ${l.getClass.getName}\n${e.getMessage}" case other => other.toString }.mkString(", ")}" } } diff --git a/examples/benchmarks/other/emit.check b/examples/benchmarks/other/emit.check new file mode 100644 index 000000000..2bd5a0a98 --- /dev/null +++ b/examples/benchmarks/other/emit.check @@ -0,0 +1 @@ +22 diff --git a/examples/benchmarks/other/emit.effekt b/examples/benchmarks/other/emit.effekt new file mode 100644 index 000000000..9fc6fc823 --- /dev/null +++ b/examples/benchmarks/other/emit.effekt @@ -0,0 +1,40 @@ +import examples/benchmarks/runner + +effect emit(value: Double): Unit + +def printDoubles { p: () => Unit / emit } = + try { p() } with emit { d => + println(d) + resume(()) + } + +def sumDoubles { p: () => Unit / emit } = { + var sum = 0.0 + try { p() } with emit { d => + sum = sum + d + resume(()) + } + sum +} + +def runningMean { stream: => Unit / emit }: Unit / emit = { + var n = 0 + var mean = 0.0 + try { stream() } + with emit { x => + n = n + 1 + mean = mean + ((x - mean) / n.toDouble) + do emit(mean) + resume(()) + } +} + +def generate(N: Int) = { + each(0, N) { n => + do emit(n.toDouble) + } +} + +def run(N: Int) = sumDoubles { runningMean { generate(N) } }.toInt + +def main() = benchmark(10){run} diff --git a/examples/casestudies/lexer.effekt.md b/examples/casestudies/lexer.effekt.md index 2b67cc624..49a171582 100644 --- a/examples/casestudies/lexer.effekt.md +++ b/examples/casestudies/lexer.effekt.md @@ -15,7 +15,7 @@ Before we get started, we require a few imports to deal with strings and regular module examples/casestudies/lexer import string -import text/regex +import regex ``` ## Tokens and Positions diff --git a/examples/casestudies/parser.effekt.md b/examples/casestudies/parser.effekt.md index 08d857ae4..62b65f49c 100644 --- a/examples/casestudies/parser.effekt.md +++ b/examples/casestudies/parser.effekt.md @@ -23,7 +23,7 @@ Parsers can be expressed by using the lexer effect and process the token stream. ``` interface Nondet { def alt(): Bool - def fail[A](msg: String): A + def fail(msg: String): Nothing } effect Parser = { Nondet, Lexer } @@ -205,7 +205,7 @@ def parse[R](input: String) { p: => R / Parser }: ParseResult[R] = try { case Failure(msg) => resume(false) case Success(res) => Success(res) } - def fail[A](msg) = Failure(msg) + def fail(msg) = Failure(msg) } with LexerError { (msg, pos) => Failure(msg) } diff --git a/examples/chez/libraries.effekt b/examples/chez/libraries.effekt index 8b1d2b532..15b26cd91 100644 --- a/examples/chez/libraries.effekt +++ b/examples/chez/libraries.effekt @@ -12,7 +12,7 @@ import array import mutable/dict import ref -import text/regex +import regex import string def main() = () diff --git a/examples/neg/unbound_type.effekt b/examples/neg/unbound_type.effekt index 160767fb5..48c63cb50 100644 --- a/examples/neg/unbound_type.effekt +++ b/examples/neg/unbound_type.effekt @@ -1,7 +1,7 @@ module examples/pos/lexer import string -import text/regex +import regex effect EOS(): Nothing effect LexerError(msg: String, pos: Position): Nothing diff --git a/examples/pos/simpleparser.effekt b/examples/pos/simpleparser.effekt index 09d7ef5c7..1cd69464d 100644 --- a/examples/pos/simpleparser.effekt +++ b/examples/pos/simpleparser.effekt @@ -1,7 +1,7 @@ module examples/pos/simpleparser import string -import text/regex +import regex effect fail(msg: String): Nothing diff --git a/libraries/chez/common/text/regex.effekt b/libraries/chez/common/text/regex.effekt deleted file mode 100644 index f9bea3ce4..000000000 --- a/libraries/chez/common/text/regex.effekt +++ /dev/null @@ -1,31 +0,0 @@ -module text/regex - -import immutable/cslist - -extern include chez "pregexp.scm" - -extern type Regex - -record Match(matched: String, index: Int) - -extern pure def regex(str: String): Regex = - chez "(pregexp ${str})" - -def exec(reg: Regex, str: String): Option[Match] = { - val matched = reg.unsafeMatchString(str) - if (matched.isUndefined) { None() } - else { Some(Match(matched, reg.unsafeMatchIndex(str))) } -} - -extern pure def unsafeMatchIndex(reg: Regex, str: String): Int = - chez "(let ([m (pregexp-match-positions ${reg} ${str})]) (if m (car (car m)) #f))" - -// we ignore the captures for now and only return the whole match -extern pure def unsafeMatchString(reg: Regex, str: String): String = - chez "(let ([m (pregexp-match ${reg} ${str})]) (if m (car m) #f))" - -extern pure def split(reg: Regex, str: String): CSList[String] = - chez "(pregexp-split ${reg} ${str})" - -// def split(str: String, sep: String): Array[String] = -// toArray(split(sep.regex, str)) \ No newline at end of file diff --git a/libraries/common/effekt.effekt b/libraries/common/effekt.effekt index 6159d3b12..96fbfac18 100644 --- a/libraries/common/effekt.effekt +++ b/libraries/common/effekt.effekt @@ -390,7 +390,7 @@ extern pure def log(x: Double): Double = extern pure def log1p(x: Double): Double = js "Math.log1p(${x})" chez "(log (+ ${x} 1))" - llvm "%z = call %Double @log1p(double ${x}) ret %Double %z" + llvm "%z = call %Double @log1p(double ${x}) ret %Double %z" extern pure def exp(x: Double): Double = js "Math.exp(${x})" @@ -651,6 +651,7 @@ extern pure def toByte(n: Int): Byte = extern pure def toInt(n: Byte): Int = js "${n}" llvm "%z = zext %Byte ${n} to %Int ret %Int %z" + vm "effekt::toInt(Byte)" // Undefined and Null @@ -666,11 +667,13 @@ extern pure def toInt(n: Byte): Int = extern pure def undefined[A](): A = js "undefined" chez "#f" + vm "effekt::undefined()" /// Is an FFI value undefined? extern pure def isUndefined[A](value: A): Bool = js "(${value} === undefined || ${value} === null)" chez "(eq? ${value} #f)" + vm "effekt::isUndefined[A](A)" // Tuples diff --git a/libraries/common/regex.effekt b/libraries/common/regex.effekt new file mode 100644 index 000000000..cafa1f49f --- /dev/null +++ b/libraries/common/regex.effekt @@ -0,0 +1,64 @@ +module regex + +import string + +extern include chez "text/pregexp.scm" + +extern type Regex + +record Match(matched: String, index: Int) + +extern pure def regex(str: String): Regex = + js "new RegExp(${str})" + chez "(pregexp ${str})" + vm "regex::regex(String)" + +def exec(reg: Regex, str: String): Option[Match] = { + val v = internal::exec(reg, str) + if (v.isUndefined) + None() + else + Some(Match(internal::matched(v), internal::index(v))) +} + +namespace internal { + + /// The type RegexMatch is used internally to represent platform + /// dependent results of matching a regex. + extern type RegexMatch + // js: { matched: String, index: Int } | undefined + // vm: scala.util.matching.Regex.Match | null + + extern pure def matched(r: RegexMatch): String = + js "${r}.matched" + chez "(vector-ref ${r} 0)" + vm "regex::matched(RegexMatch)" + + extern pure def index(r: RegexMatch): Int = + js "${r}.index" + chez "(vector-ref ${r} 1)" + vm "regex::index(RegexMatch)" + + extern js """ + function regex$exec(reg, str) { + var res = reg.exec(str); + if (res === null) { return undefined } + else { return { matched: res[0], index: res.index } } + } + """ + + extern chez """ + (define regex-exec + (lambda (regex str) + (let* ([positions (pregexp-match-positions regex str)] + [match (pregexp-match regex str)]) + (if (and positions match) + (vector (car match) (caar positions)) + #f)))) + """ + + extern io def exec(reg: Regex, str: String): RegexMatch = + js "regex$exec(${reg}, ${str})" + chez "(regex-exec ${reg} ${str})" + vm "regex::exec(Regex, String)" +} \ No newline at end of file diff --git a/libraries/common/string.effekt b/libraries/common/string.effekt index 578848b02..4ecd4dece 100644 --- a/libraries/common/string.effekt +++ b/libraries/common/string.effekt @@ -243,11 +243,13 @@ extern pure def toInt(ch: Char): Int = js "${ch}" chez "${ch}" llvm "ret %Int ${ch}" + vm "string::toInt(Char)" extern pure def toChar(codepoint: Int): Char = js "${codepoint}" chez "${codepoint}" llvm "ret %Int ${codepoint}" + vm "string::toChar(Int)" extern pure def infixLt(x: Char, y: Char): Bool = @@ -259,6 +261,7 @@ extern pure def infixLt(x: Char, y: Char): Bool = %adt_boolean = insertvalue %Pos zeroinitializer, i64 %fat_z, 0 ret %Pos %adt_boolean """ + vm "string::infixLt(Char, Char)" extern pure def infixLte(x: Char, y: Char): Bool = js "(${x} <= ${y})" @@ -269,6 +272,7 @@ extern pure def infixLte(x: Char, y: Char): Bool = %adt_boolean = insertvalue %Pos zeroinitializer, i64 %fat_z, 0 ret %Pos %adt_boolean """ + vm "string::infixLte(Char, Char)" extern pure def infixGt(x: Char, y: Char): Bool = js "(${x} > ${y})" @@ -279,6 +283,7 @@ extern pure def infixGt(x: Char, y: Char): Bool = %adt_boolean = insertvalue %Pos zeroinitializer, i64 %fat_z, 0 ret %Pos %adt_boolean """ + vm "string::infixGt(Char, Char)" extern pure def infixGte(x: Char, y: Char): Bool = js "(${x} >= ${y})" @@ -289,6 +294,7 @@ extern pure def infixGte(x: Char, y: Char): Bool = %adt_boolean = insertvalue %Pos zeroinitializer, i64 %fat_z, 0 ret %Pos %adt_boolean """ + vm "string::infixGte(Char, Char)" /** @@ -327,3 +333,4 @@ extern pure def unsafeCharAt(str: String, n: Int): Char = %x = call %Int @c_bytearray_character_at(%Pos ${str}, %Int ${n}) ret %Int %x """ + vm "string::unsafeCharAt(String, Int)" diff --git a/libraries/js/text/regex.effekt b/libraries/js/text/regex.effekt deleted file mode 100644 index b42e67a30..000000000 --- a/libraries/js/text/regex.effekt +++ /dev/null @@ -1,33 +0,0 @@ -module text/regex - -import string - -extern type Regex - -record Match(matched: String, index: Int) - -extern pure def regex(str: String): Regex = - js "new RegExp(${str})" - -def exec(reg: Regex, str: String): Option[Match] = - undefinedToOption(reg.unsafeExec(str)) match { - case None() => None() - case Some(v) => Some(Match(v.matched, v.index)) - } - -// internal representation { matched: String, index: Int } -extern type RegexMatch -extern pure def matched(r: RegexMatch): String = js "${r}.matched" -extern pure def index(r: RegexMatch): Int = js "${r}.index" - -extern js """ -function regex$exec(reg, str) { - var res = reg.exec(str); - if (res === null) { return undefined } - else { return { matched: res[0], index: res.index } } -} -""" - -// internals -extern io def unsafeExec(reg: Regex, str: String): RegexMatch = - js "regex$exec(${reg}, ${str})"