Skip to content

Commit

Permalink
Add more VM tests (#788)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
b-studios authored Jan 22, 2025
1 parent 719ae83 commit 864bd99
Show file tree
Hide file tree
Showing 17 changed files with 330 additions and 88 deletions.
8 changes: 3 additions & 5 deletions effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
8 changes: 3 additions & 5 deletions effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
119 changes: 116 additions & 3 deletions effekt/jvm/src/test/scala/effekt/core/VMTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -711,6 +826,4 @@ class VMTests extends munit.FunSuite {
}

testFiles.foreach(runTest)


}
86 changes: 84 additions & 2 deletions effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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))
Expand All @@ -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 {
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
}
4 changes: 2 additions & 2 deletions effekt/shared/src/main/scala/effekt/core/vm/VM.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")}" }
}
Expand All @@ -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(", ")}" }
}
Expand Down
1 change: 1 addition & 0 deletions examples/benchmarks/other/emit.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
40 changes: 40 additions & 0 deletions examples/benchmarks/other/emit.effekt
Original file line number Diff line number Diff line change
@@ -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}
2 changes: 1 addition & 1 deletion examples/casestudies/lexer.effekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/casestudies/parser.effekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit 864bd99

Please sign in to comment.