diff --git a/build.sbt b/build.sbt index 1228add8f..2ab9eaaf0 100644 --- a/build.sbt +++ b/build.sbt @@ -67,7 +67,7 @@ lazy val kiama: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("ki ) lazy val root = project.in(file("effekt")) - .aggregate(effekt.js, effekt.jvm) + .aggregate(effekt.jvm) .settings(noPublishSettings) .settings(Seq( Compile / run := (effekt.jvm / Compile / run).evaluated diff --git a/effekt/js/src/main/scala/effekt/Backend.scala b/effekt/js/src/main/scala/effekt/Backend.scala deleted file mode 100644 index ad34093be..000000000 --- a/effekt/js/src/main/scala/effekt/Backend.scala +++ /dev/null @@ -1,7 +0,0 @@ -package effekt - -class Backend { - val compiler = generator.js.JavaScriptWeb() - val runner = () - val name = "js-web" -} diff --git a/effekt/js/src/main/scala/effekt/EffektConfig.scala b/effekt/js/src/main/scala/effekt/EffektConfig.scala deleted file mode 100644 index bc24d9a28..000000000 --- a/effekt/js/src/main/scala/effekt/EffektConfig.scala +++ /dev/null @@ -1,38 +0,0 @@ -package effekt - -import java.io.File - -import kiama.util.{ Emitter, OutputEmitter } - -trait EffektConfig { - - def output(): Emitter = new OutputEmitter - - def includes(): List[String] = List( - "common", - "js", - ".") - - def backend(): Backend = Backend() - - def outputPath(): String = "out" - - def prelude(): List[String] = List( - "effekt", - "option", - "list", - "result", - "exception", - "array", - "string", - "ref" - ) - - def optimize() = true - - def maxInlineSize() = 50L - - def timed() = false - - def debug() = false -} diff --git a/effekt/js/src/main/scala/effekt/LanguageServer.scala b/effekt/js/src/main/scala/effekt/LanguageServer.scala deleted file mode 100644 index 6c62143f2..000000000 --- a/effekt/js/src/main/scala/effekt/LanguageServer.scala +++ /dev/null @@ -1,173 +0,0 @@ -package effekt - -import effekt.context.{ Context, VirtualFileSource, VirtualModuleDB } - -import effekt.util.{ PlainMessaging, getOrElseAborting } -import effekt.util.messages.{ BufferedMessaging, EffektError, EffektMessaging, FatalPhaseError } -import effekt.util.paths.* -import effekt.generator.js.JavaScriptWeb -import kiama.util.{ Messaging, Position, Positions, Severities, Source, StringSource } - -import scala.scalajs.js -import scala.scalajs.js.JSConverters.* -import scala.scalajs.js.annotation.* - -// the LSP types -// https://github.com/microsoft/vscode-languageserver-node/blob/master/types/src/main.ts -object lsp { - - // General Messages - // ----------- - - class RequestMessage[T](val id: Int, val method: String, val params: T) extends js.Object - class NotificationMessage[T](val method: String, val params: T) extends js.Object - class ResponseError(val code: Int, val message: String, val data: Any) extends js.Object - class ResponseMessage[T](val id: Int, val method: String, val params: T, val error: ResponseError = null) extends js.Object - - // Positions - // --------- - - // for now we do not stick to the protocol and use simple filenames - type DocumentUri = String - - // LSP positions are zero indexed (for both line and character) - class Position(val line: Int, val character: Int) extends js.Object - class Range(val start: Position, val end: Position) extends js.Object - class Location(val uri: DocumentUri, val range: Range) extends js.Object - - // Domain Specific Messages - // ------------------------ - class CaptureInfo(val pos: Position, val capture: String) extends js.Object - - // Diagnostics - // ----------- - class Diagnostic(val range: Range, val severity: DiagnosticSeverity, val message: String) extends js.Object { - val source = "effekt" - } - - type DiagnosticSeverity = Int - object DiagnosticSeverity { - val Error = 1 - val Warning = 2 - val Information = 3 - val Hint = 4 - } -} - -/** - * A language server to be run in a webworker - */ -class LanguageServer extends Intelligence { - - val positions: Positions = new Positions - - object config extends EffektConfig - - implicit object context extends Context(positions) with VirtualModuleDB { - object messaging extends PlainMessaging - } - - context.setup(config) - - @JSExport - def infoAt(path: String, pos: lsp.Position): String = { - val p = fromLSPPosition(pos, VirtualFileSource(path)) - for { - (tree, sym) <- getSymbolAt(p) - info <- getInfoOf(sym) - } yield info.fullDescription - }.orNull - - @JSExport - def typecheck(path: String): js.Array[lsp.Diagnostic] = { - context.messaging.clear() - context.compiler.runFrontend(VirtualFileSource(path)) - context.messaging.buffer.distinct.map(messageToDiagnostic).toJSArray - } - - @JSExport - def writeFile(path: String, contents: String): Unit = - file(path).write(contents) - - @JSExport - def readFile(path: String): String = - file(path).read - - @JSExport - def lastModified(path: String): Long = - file(path).lastModified - - @JSExport - def compileFile(path: String): String = - compileCached(VirtualFileSource(path)).getOrElseAborting { - // report all collected error messages - val formattedErrors = context.messaging.formatMessages(context.messaging.get) - throw js.JavaScriptException(formattedErrors) - } - - @JSExport - def showCore(path: String): String = { - val doc = context.compiler.prettyIR(VirtualFileSource(path), Stage.Core).getOrElseAborting { - return null - } - doc.layout - } - - @JSExport - def inferredCaptures(path: String): js.Array[lsp.CaptureInfo] = { - typecheck(path) - val source = VirtualFileSource(path) - val range = kiama.util.Range(source.offsetToPosition(0), source.offsetToPosition(source.charCount)) - getInferredCaptures(range).map { - case (p, c) => new lsp.CaptureInfo(toLSPPosition(p), c.toString) - }.toJSArray - } - - /** - * Has the side effect of saving the generated output to a file - */ - object compileWhole extends Phase[Source, String] { - val phaseName = "compile" - def run(src: Source)(using Context) = - context.compiler.compileWeb(src).map { - case (files, mainPath) => - files.foreach { case (path, content) => writeFile(path, content) } - mainPath - } - } - - // Here we cache the full pipeline for a single file, including writing the result - // to an output file. - private val compileCached = Phase.cached("compile-cached") { compileWhole } - - private def messageToDiagnostic(m: EffektError) = { - val from = m.startPosition.map(toLSPPosition).orNull - val to = m.finishPosition.map(toLSPPosition).orNull - val text = context.messaging.formatContent(m) - new lsp.Diagnostic(new lsp.Range(from, to), convertSeverity(m.severity), text) - } - - private def toLSPPosition(p: Position): lsp.Position = - new lsp.Position(p.line - 1, p.column - 1) - - private def fromLSPPosition(p: lsp.Position, source: Source): Position = - Position(p.line + 1, p.character + 1, source) - - private def toLSPLocation(from: Position, to: Position): Option[lsp.Location] = from.source match { - case VirtualFileSource(path) => Some(new lsp.Location(from.source.name, new lsp.Range(toLSPPosition(from), toLSPPosition(to)))) - case _ => None - } - - private def convertSeverity(s: Severities.Severity): lsp.DiagnosticSeverity = s match { - case Severities.Error => lsp.DiagnosticSeverity.Error - case Severities.Warning => lsp.DiagnosticSeverity.Warning - case Severities.Information => lsp.DiagnosticSeverity.Information - case Severities.Hint => lsp.DiagnosticSeverity.Hint - } -} - -@JSExportTopLevel("effekt") -object Effekt { - @JSExport - def LanguageServer(): LanguageServer = new LanguageServer() -} diff --git a/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala b/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala deleted file mode 100644 index 4dbf27700..000000000 --- a/effekt/js/src/main/scala/effekt/context/VirtualModuleDB.scala +++ /dev/null @@ -1,41 +0,0 @@ -package effekt -package context - -import effekt.util.Resources -import effekt.util.paths._ - -import kiama.util.Source - -trait VirtualModuleDB extends ModuleDB { self: Context => - - // initialize resources, which are copied by sbt - Resources.load() - - /** - * tries to find a file in the workspace, that matches the import path - * - * used by Namer to resolve FFI includes - */ - override def contentsOf(path: String): Option[String] = { - val parent = file(module.source.name).parent - (parent :: config.includes().map(file)).collectFirst { - case base if (base / path).exists => (base / path).read - } - } - - override def findSource(path: String): Option[Source] = { - val filename = path + ".effekt" - val f = file(filename) - - def effektFile(include: File) = include / (path + ".effekt") - - config.includes().collectFirst { - case p if effektFile(p).exists => - VirtualFileSource(effektFile(p).toString) - } - } -} - -case class VirtualFileSource(name: String) extends Source { - lazy val content = file(name).read -} diff --git a/effekt/js/src/main/scala/effekt/util/PathUtils.scala b/effekt/js/src/main/scala/effekt/util/PathUtils.scala deleted file mode 100644 index 087c4eb87..000000000 --- a/effekt/js/src/main/scala/effekt/util/PathUtils.scala +++ /dev/null @@ -1,61 +0,0 @@ -package effekt.util - -import effekt.context.VirtualFileSource - -import scala.collection.mutable -import kiama.util.Source - -class FileNotFound(filename: String) extends Exception(s"File not found: ${filename}") - -/** - * A very primitive virtual in-memory file system to be used from JavaScript - * - * TODO Extend and implement util.PathUtil - */ -object paths extends PathUtils { - - private type Path = Seq[String] - - case class VirtualFile(timestamp: Long, contents: String) - - private lazy val files: mutable.Map[Path, VirtualFile] = mutable.Map.empty - - val separatorChar = '/' - - class File(path: Path) extends FileOps { - - val normalized: Path = { - var res: Path = Seq.empty - path.foreach { - case "." => () - case ".." if res.nonEmpty => res = res.init - case seg => res = res :+ seg.trim.stripSuffix("/") - } - res - } - - def /(other: String): File = new File(path ++ other.split(separatorChar).toSeq) - - def unixPath = normalized.mkString(separatorChar.toString) - def canonicalPath = unixPath - - def parent = file(normalized.init) - - def lastModified: Long = files.getOrElse(normalized, throw new FileNotFound(unixPath)).timestamp - - def read: String = files.getOrElse(normalized, throw new FileNotFound(unixPath)).contents - def tryRead: Option[String] = files.get(normalized).map(_.contents) - def write(content: String) = files.put(normalized, VirtualFile(System.currentTimeMillis, content)) - def exists: Boolean = files.isDefinedAt(normalized) - - override def toString = unixPath - } - - override def lastModified(src: Source): Long = src match { - case VirtualFileSource(name) => file(name).lastModified - case _ => super.lastModified(src) - } - - implicit def file(path: String): File = file(path.split(separatorChar).toSeq) - def file(path: Seq[String]): File = new File(path) -} diff --git a/effekt/js/src/test/scala/effekt/WebTests.scala b/effekt/js/src/test/scala/effekt/WebTests.scala deleted file mode 100644 index c67eab2c1..000000000 --- a/effekt/js/src/test/scala/effekt/WebTests.scala +++ /dev/null @@ -1,109 +0,0 @@ -package effekt - -import kiama.util.{ FileSource, StringSource } - -import utest._ -import scala.scalajs.js -import scala.scalajs.js.annotation.* -import scala.collection.mutable - -object WebTests extends TestSuite { - - class Loaded(var module: js.Dynamic, var timestamp: Long) - - def tests = Tests { - - val server = effekt.Effekt.LanguageServer() - - val loadedModules = mutable.Map.empty[String, Loaded] - - def load(path: String): js.Dynamic = - val mod = loadedModules.getOrElse(path, Loaded(null, 0)) - val fullpath = path - val lastModified = server.lastModified(fullpath) - if (lastModified > mod.timestamp) { - val contents = server.readFile(fullpath) - mod.timestamp = lastModified - - js.Dynamic.global.globalThis.load = load - mod.module = js.eval( - s"""|(function() { - | module = { exports: {} }; - | ${contents} - | return module.exports - | }).apply(this)""".stripMargin).asInstanceOf[js.Dynamic] - mod.module - } else { - mod.module - } - - /** - * Since main needs to return `Unit`, we define the effekt function `withResult` which side-channels the result - * using the global object `module.export`. - */ - def evaluate[A](includes: List[String], content: String): A = - run(includes, - s"""|extern io def withResult[A](value: A): Unit = js"(function() { module.exports.result = $${value} })()" - |def main() = { withResult(${content}) } - |""".stripMargin) - js.Dynamic.global.globalThis.module.`exports`.result.asInstanceOf[A] - - def run(includes: List[String], content: String) = { - server.writeFile("interactive.effekt", includes.map(i => s"import $i").mkString("\n") + s"\n\n${content}") - val mainFile = server.compileFile("interactive.effekt") - load(mainFile).main() - } - - test("Can read files from the stdlib") { - val contents = server.readFile("common/effekt.effekt") - assert(contents.length > 100) - } - - test("Evaluate simple expressions in REPL") { - val result = evaluate[Int](List(), "1 + 2") - assert(result == 3) - } - - test("Evaluate expressions that uses a builtin which relies on an extern literal") { - evaluate(List(), "println(42)") - } - - test("Evaluate expressions using stdlib in REPL") { - val result = evaluate[Int](List("list"), "Cons(1, Cons(2, Nil())).size") - assert(result == 2) - } - - test("Show core on website") { - server.writeFile("test.effekt", - """ def main() = println(1 + 2) - |""".stripMargin) - val result = server.showCore("test.effekt") - - assert(result.contains("infixAdd")) - } - - test("Load file with multiline extern strings") { - val result = evaluate[Int](List("list", "ref"), "Cons(1, Cons(2, Nil())).size") - assert(result == 2) - } - - test("Exporting functions from a namespace") { - server.writeFile("foo.effekt", - """ namespace foo { def bar() = 42 } - |""".stripMargin) - - val result = evaluate[Int](List("foo"), "foo::bar()") - assert(result == 42) - } - - test("Extern resources on website") { - run(Nil, - """interface Greet { } - | - |extern resource my_resource : Greet - | - |def main() = () - |""".stripMargin) - } - } -} diff --git a/effekt/jvm/src/main/scala/effekt/Backend.scala b/effekt/jvm/src/main/scala/effekt/Backend.scala deleted file mode 100644 index 1c80a68e6..000000000 --- a/effekt/jvm/src/main/scala/effekt/Backend.scala +++ /dev/null @@ -1,36 +0,0 @@ -package effekt - -import effekt.generator.* - -/** - * A Backend is composed of a compiler implementation and a runner. - * - * The compiler defines the respective pipeline for the backend. It inherits from [[Compiler]] - * and can make use of common phases, such as [[Frontend]]. - * - * The runner decides how to run an executable created by the compiler. It inherits from [[Runner]] - * and can also configure the standard library of the particular backend. - * - * Compiler and Runner can communicate via a self-chosen type [[E]] -- the executable. Most - * of the time, [[E]] will be String and correspond to the name of the generated file. - * - * The lifetime of a backend is tightly coupled to a corresponding instance of [[EffektConfig]]. - * This is important for the right caching / recomputation behavior. - * - * @param name the name of the backend (should be identical to the --backend flag) - * @param compiler the compiler for this backend - * @param runner the runner for this backend - * @tparam E the type of executables, mostly String - */ -case class Backend[E](name: String, compiler: Compiler[E], runner: Runner[E]) - -object Backend { - - def backend(name: String): Backend[_] = name match { - case "js" => Backend("js", js.JavaScriptNode(), JSNodeRunner) - case "js-web" => Backend("js-web", js.JavaScriptWeb(), JSWebRunner) - case "chez-monadic" => Backend("chez-monadic", chez.ChezSchemeMonadic(), ChezMonadicRunner) - case "chez-callcc" => Backend("chez-callcc", chez.ChezSchemeCallCC(), ChezCallCCRunner) - case "llvm" => Backend("llvm", llvm.LLVM(), LLVMRunner) - } -} diff --git a/effekt/jvm/src/main/scala/effekt/Driver.scala b/effekt/jvm/src/main/scala/effekt/Driver.scala deleted file mode 100644 index 65ed95114..000000000 --- a/effekt/jvm/src/main/scala/effekt/Driver.scala +++ /dev/null @@ -1,140 +0,0 @@ -package effekt - -// Adapted from -// https://github.com/inkytonik/kiama/blob/master/extras/src/test/scala/org/bitbucket/inkytonik/kiama/example/oberon0/base/Driver.scala - -import effekt.source.{ ModuleDecl, Tree } -import effekt.symbols.Module -import effekt.context.{ Context, IOModuleDB } -import kiama.output.PrettyPrinterTypes.Document -import kiama.parsing.ParseResult -import kiama.util.{ IO, Source } -import effekt.util.messages.{ BufferedMessaging, CompilerPanic, EffektError, EffektMessaging, FatalPhaseError } -import effekt.util.paths.file -import effekt.util.{ AnsiColoredMessaging, MarkdownSource, getOrElseAborting } - -import scala.sys.process.Process - -/** - * effekt.Compiler <----- compiles code with ------ Driver ------ implements UI with -----> kiama.util.Compiler - */ -trait Driver extends kiama.util.Compiler[EffektConfig, EffektError] { outer => - - object messaging extends AnsiColoredMessaging - - // Compiler context - // ================ - // We always only have one global instance of the compiler - object context extends Context(positions) with IOModuleDB { val messaging = outer.messaging } - - override def createConfig(args: Seq[String]) = - new EffektConfig(args) - - /** - * Main entry to the compiler, invoked by Kiama after creating the config - * - * In LSP mode: invoked for each file opened in an editor - */ - override def compileSource(source: Source, config: EffektConfig): Unit = try { - val src = if (source.name.endsWith(".md")) { MarkdownSource(source) } else { source } - - // remember that we have seen this source, this is used by LSP (kiama.util.Server) - sources(source.name) = src - - implicit val C = context - C.setup(config) - - def saveOutput(path: String, doc: String): Unit = - if (C.config.requiresCompilation()) { - val out = C.config.outputPath() - out.mkdirs - IO.createFile((out / path).unixPath, doc) - } - - C.backend match { - - case Backend(name, compiler, runner) => - // measure the total compilation time here - def compile() = C.timed("total", source.name) { - compiler.compile(src) map { - case (outputFiles, exec) => - outputFiles.foreach { - case (filename, doc) => - saveOutput(filename, doc) - } - exec - } - } - - // we are in one of three exclusive modes: LSPServer, Compile, Run - if (config.server()) { compiler.runFrontend(src) } - else if (config.interpret()) { compile() foreach runner.eval } - else if (config.build()) { compile() foreach runner.build } - else if (config.compile()) { compile() } - } - } catch { - case FatalPhaseError(msg) => context.report(msg) - case e @ CompilerPanic(msg) => - context.report(msg) - e.getStackTrace.foreach { line => - context.info(" at " + line) - } - // when in server-mode, do not crash but report the error to avoid - // restarting the server. - case e if config.server() => - context.info("Effekt Compiler Crash: " + e.getMessage) - e.getStackTrace.foreach { line => - context.info(" at " + line) - } - } finally { - outputTimes(source, config)(context) - showIR(source, config)(context) - writeIRs(source, config)(context) - // This reports error messages - afterCompilation(source, config)(context) - } - - /** - * Outputs the timing information captured in [[effekt.util.Timers]] by [[effekt.context.Context]]. Either a JSON file - * is written to disk or a plain text message is written to stdout. - */ - def outputTimes(source: Source, config: EffektConfig)(implicit C: Context): Unit = { - if (C.timersActive) config.time.toOption foreach { - case "json" => - // extract source filename and write to given output path - val out = config.outputPath().getAbsolutePath - val name = s"${source.name.split("/").last.stripSuffix(".effekt")}.json" - IO.createFile((out / name).unixPath, C.timesToJSON()) - case "text" => - C.info(C.timesToString()) - } - } - - def showIR(source: Source, config: EffektConfig)(implicit C: Context): Unit = - config.showIR().map { stage => - C.compiler.prettyIR(source, stage).map { case (Document(s, _)) => println(s) } - } - - def writeIRs(source: Source, config: EffektConfig)(implicit C: Context): Unit = { - if (!config.writeIRs()) return - val out = config.outputPath().getAbsolutePath - for (stage <- Stage.values) - C.compiler.prettyIR(source, stage).map { case (Document(s, _)) => - val extension = if (stage == Stage.Target) C.runner.extension else "ir" - val name = source.name.split("/").last + "-" + stage.toString.toLowerCase + "." + extension - IO.createFile((out / name).unixPath, s) - } - } - - /** - * Overridden in [[Server]] to also publish core and js compilation results to VSCode - */ - def afterCompilation(source: Source, config: EffektConfig)(implicit C: Context): Unit = { - // report messages - report(source, C.messaging.buffer, config) - - // exit with non-zero code if not in repl/server mode and messaging buffer contains errors - if (config.exitOnError() && C.messaging.hasErrors) - sys.exit(1) - } -} diff --git a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala deleted file mode 100644 index 4f68a3547..000000000 --- a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala +++ /dev/null @@ -1,234 +0,0 @@ -package effekt - -import java.io.File -import effekt.util.paths.file -import kiama.util.REPLConfig -import org.rogach.scallop.{ ScallopOption, fileConverter, fileListConverter, longConverter, stringConverter, stringListConverter } - -class EffektConfig(args: Seq[String]) extends REPLConfig(args.takeWhile(_ != "--")) { - // Print version information on `--version` and `--help` - version(s"Effekt ${effekt.util.Version.effektVersion}") - - // Common - // ------ - private val common = group("Common Options") - - val compile: ScallopOption[Boolean] = toggle( - "compile", - descrYes = "Compile the Effekt program to the backend specific representation", - default = Some(false), - prefix = "no-", - group = common - ) - - val build: ScallopOption[Boolean] = toggle( - "build", - descrYes = "Compile the Effekt program and build a backend specific executable", - default = Some(false), - group = common - ) - - val outputPath: ScallopOption[File] = opt[File]( - "out", - descr = "Path to write generated files to (defaults to ./out)", - default = Some(new File("./out")), - required = false, - group = common - ) - - val includePath: ScallopOption[List[File]] = opt[List[File]]( - "includes", - descr = "Path to consider for includes (can be set multiple times)", - default = Some(List(new File("."))), - noshort = true, - group = common - ) - - val stdlibPath: ScallopOption[File] = opt[File]( - "lib", - descr = "Path to the standard library to be used", - required = false, - group = common - ) - - val backend: ScallopOption[Backend[_]] = choice( - choices = List("js", "js-web", "chez-callcc", "chez-monadic", "llvm"), - name = "backend", - descr = "The backend that should be used", - default = Some("js"), - noshort = true, - group = common - ).map(Backend.backend) - - def runArgs(): Seq[String] = args.dropWhile(_ != "--").drop(1) - - // Advanced - // -------- - private val advanced = group("Advanced Options") - - val llvmVersion: ScallopOption[String] = opt[String]( - "llvm-version", - descr = "the llvm version that should be used to compile the generated programs (only necessary if backend is llvm, defaults to 18)", - default = Some(sys.env.getOrElse("EFFEKT_LLVM_VERSION", "18")), - noshort = true, - group = advanced - ) - - val gccIncludes: ScallopOption[File] = opt[File]( - "gcc-includes", - descr = "Additional include path for gcc (necessary for libuv on the llvm backend)", - noshort = true, - group = advanced - ) - - val gccLibraries: ScallopOption[File] = opt[File]( - "gcc-libraries", - descr = "Additional library path for gcc (necessary for libuv on the llvm backend)", - noshort = true, - group = advanced - ) - - val preludePath: ScallopOption[List[String]] = opt[List[String]]( - "prelude", - descr = "Modules to be automatically imported in every file", - default = None, - noshort = true, - group = advanced - ) - - val exitOnError: ScallopOption[Boolean] = toggle( - "exit-on-error", - descrYes = "Exit with non-zero exit code on error", - default = Some(!repl() && !server()), - noshort = true, - prefix = "no-", - group = advanced - ) - - val optimize: ScallopOption[Boolean] = toggle( - "optimize", - descrYes = "Run optimizations (in particular the inliner) when compiling programs", - default = Some(true), - short = 'O', - prefix = "no-", - group = advanced - ) - - val maxInlineSize: ScallopOption[Long] = opt( - "max-inline-size", - descr = "Maximum size (number of core tree-nodes) of a function considered by the inliner", - default = Some(50L), - noshort = true, - group = advanced - ) - advanced.append(server) - - - // Debugging - // --------- - private val debugging = group("Compiler Development") - - val showIR: ScallopOption[Option[Stage]] = choice( - choices = List("none", "core", "machine", "target"), - name = "ir-show", - descr = "The intermediate presentation that should be printed.", - default = Some("none"), - noshort = true, - group = debugging - ).map(s => Stage.values.find(_.toString.toLowerCase == s)) - - debugging.append(showIR) - - val writeIRs: ScallopOption[Boolean] = toggle( - "ir-write-all", - descrYes = "Write all IRs to files in the output directory", - default = Some(false), - noshort = true, - prefix = "no-", - group = debugging - ) - - val time: ScallopOption[String] = choice( - choices = Seq("text", "json"), - name = "time", - descr = "Measure the time spent in each compilation phase", - required = false, - group = debugging - ) - - lazy val valgrind = toggle( - "valgrind", - descrYes = "Execute files using valgrind", - descrNo = "Don't execute files using valgrind", - default = Some(false), - noshort = true, - prefix = "no-", - group = debugging - ) - - /** - * Tries to find the path to the standard library. Proceeds in the following - * order: - * 1) specified as part of the settings arg `lib`? - * 2) specified in an environment variable `EFFEKT_LIB` - * 3) relative to the current working directory - * 4) relative to to the executed JAR file (effekt.jar) - */ - def findStdLib: util.paths.File = { - - def backendStdLibPath(path: util.paths.File) = - backend().runner.standardLibraryPath(path) - - // 1) in config? - if (stdlibPath.isDefined) { - return stdlibPath() - } - - // 2) in PATH - if (System.getenv.containsKey("EFFEKT_LIB")) { - return System.getenv("EFFEKT_LIB") - } - - // 3) in PWD - val pwd = file(".") - val localLib = backendStdLibPath(pwd) - - if ((localLib / "effekt.effekt").exists) { - return localLib - } - - // 4) next to Jar - val jarPath = try { - file(getClass.getProtectionDomain.getCodeSource.getLocation.toURI).parent - } catch { - case e: Throwable => - sys.error("Cannot find path to standard library") - } - - val jarLib = backendStdLibPath(jarPath / "..") - if ((jarLib / "effekt.effekt").exists) { - return jarLib - } - - sys.error("Cannot find path to standard library") - } - - lazy val libPath: File = findStdLib.canonicalPath.toFile - - def includes(): List[File] = libPath :: backend().runner.includes(libPath).map(_.toFile) ++ includePath() - - def prelude(): List[String] = preludePath.getOrElse(backend().runner.prelude) - - def requiresCompilation(): Boolean = !server() - - def interpret(): Boolean = !server() && !compile() && !build() - - def repl(): Boolean = filenames().isEmpty && !server() && !compile() - - def timed(): Boolean = time.isSupplied && !server() - - validateFilesIsDirectory(includePath) - - // force some other configs manually to initialize them when compiling with native-image - server; output; filenames -} diff --git a/effekt/jvm/src/main/scala/effekt/Main.scala b/effekt/jvm/src/main/scala/effekt/Main.scala index d600889d9..c2c31f7bd 100644 --- a/effekt/jvm/src/main/scala/effekt/Main.scala +++ b/effekt/jvm/src/main/scala/effekt/Main.scala @@ -1,56 +1,8 @@ package effekt -import org.rogach.scallop.exceptions.ScallopException - object Main { - /** - * Main entry point for the Effekt compiler. - * - * Depending on the command line arguments, we run in one of the following modes: - * - * - Launch the Effekt language server (e.g. `effekt --server`) - * - Launch the REPL (e.g. `effekt`) - * - Build the provided files (e.g. `effekt --build hello.effekt`) - */ - def main(args: Array[String]): Unit = { - val config = try { - parseArgs(args) - } catch { - case e: ScallopException => - System.err.println(e.getMessage) - return - } - - if (config.server()) { - val serverConfig = ServerConfig( - debug = config.debug(), - debugPort = config.debugPort() - ) - val server = new Server(config) - server.launch(serverConfig) - } else if (config.repl()) { - new Repl(new Driver {}).run(config) - } else { - compileFiles(config) - } - } - /** - * Parse command line arguments into an EffektConfig. - */ - private def parseArgs(args: Array[String]): EffektConfig = { - val config = new EffektConfig(args.toIndexedSeq) - config.verify() - config - } - - /** - * Compile files specified in the configuration. - */ - private def compileFiles(config: EffektConfig): Unit = { - val driver = new Driver {} - for (filename <- config.filenames()) { - driver.compileFile(filename, config) - } + def main(args: Array[String]): Unit = { + ??? } } diff --git a/effekt/jvm/src/main/scala/effekt/Repl.scala b/effekt/jvm/src/main/scala/effekt/Repl.scala deleted file mode 100644 index d3f08efe8..000000000 --- a/effekt/jvm/src/main/scala/effekt/Repl.scala +++ /dev/null @@ -1,325 +0,0 @@ -package effekt - -import effekt.lexer.Token -import effekt.source.* -import effekt.context.{Context, IOModuleDB} -import effekt.symbols.{BlockSymbol, DeclPrinter, ErrorMessageInterpolator, Module, ValueSymbol, isSynthetic} -import effekt.util.{AnsiColoredMessaging, AnsiHighlight, VirtualSource, getOrElseAborting} -import effekt.util.messages.EffektError -import effekt.util.Version.effektVersion -import kiama.util.{Console, REPL, Range, Source, StringSource} -import kiama.parsing.{NoSuccess, ParseResult, Success, Input} - -class Repl(driver: Driver) extends REPL[Tree, EffektConfig, EffektError] { - - private implicit lazy val context: Context with IOModuleDB = driver.context - - val messaging = new AnsiColoredMessaging - - val logo = - """| _____ ______ __ __ _ _ - | (_____) | ____|/ _|/ _| | | | | - | ___ | |__ | |_| |_ ___| | _| |_ - | (___) | __| | _| _/ _ \ |/ / __| - | _____ | |____| | | || __/ <| |_ - | (_____) |______|_| |_| \___|_|\_\\__| - |""".stripMargin - - override val banner = logo + - s"""| - | Welcome to the Effekt interpreter (v$effektVersion). Enter a top-level definition, or - | an expression to evaluate. - | - | To print the available commands, enter :help - |""".stripMargin - - override def createConfig(args: Seq[String]) = driver.createConfig(args) - - /** - * Adapting Kiama REPL's driver to work with an already processed config. - * - * Called by the Driver if no arguments are provided to the Effekt binary - */ - def run(config: EffektConfig): Unit = { - val out = config.output() - out.emitln(banner) - usingCommandHistory(config) { - processlines(config) - out.emitln() - } - } - - /** - * Use the special `repl` nonterminal to process the input. It recognizes expressions, statements - * and everything else that can occur on the top-level. - */ - override def parse(source: Source): ParseResult[Tree] = { - val lexer = effekt.lexer.Lexer(source) - val tokens = lexer.lex() - val parser = RecursiveDescent(context.positions, tokens, source) - parser.parseRepl(Input(source, 0)) - } - - /** - * Adapted from the kiama/lambda2/Lambda example - * - * Process a user input line by intercepting meta-level commands to - * update the evaluation mechanisms. By default we just parse what - * they type into an expression. - */ - override def processline(source: Source, console: Console, config: EffektConfig): Option[EffektConfig] = { - - // Shorthand access to the output emitter - val output = config.output() - - /** - * Command `help`: Prints help about the available commands. - */ - def help(): Unit = { - output.emitln( - """| print the result of evaluating exp - | add a definition to the REPL context - |import add an import to the REPL context - | - |:status show the current REPL environment - |:type (:t) show the type of an expression - |:imports list all current imports - |:reset reset the REPL state - |:help (:h) print this help message - |:quit (:q) quit this REPL""".stripMargin - ) - } - - /** - * Command `:reset` -- Reset the virtual module the Repl operates on - */ - def reset() = { - module = emptyModule - } - - /** - * Command `:status` -- Prints help about the available commands - */ - def status(config: EffektConfig): Unit = { - import symbols._ - - module.includes.foreach { im => - outputCode(s"import ${im.path}", config) - } - output.emitln("") - - runFrontend(StringSource(""), module.make(UnitLit()), config) { cu => - module.definitions.foreach { - case u: Def => - outputCode(DeclPrinter(context.symbolOf(u)), config) - } - } - } - - /** - * Command `:type` -- runs the frontend (and typechecker) on the given expression - */ - def typecheck(source: Source, config: EffektConfig): Unit = - parse(source) match { - case Success(e: Term, _) => - runFrontend(source, module.make(e), config) { mod => - // TODO this is a bit ad-hoc - val mainSym = mod.exports.terms("main").head - val mainTpe = context.functionTypeOf(mainSym) - output.emitln(pp"${mainTpe.result}") - } - - case Success(other, _) => - output.emitln("Can only show type of expressions") - - // this is usually encapsulated in REPL.processline - case res: NoSuccess => - val pos = res.next.position - val messages = Vector(messaging.message(Range(pos, pos), res.message)) - report(source, messages, config) - } - - source.content match { - case Command(":status", _) => - status(config) - Some(config) - - case Command(":reset", _) => - reset() - Some(config) - - case Command(":l", path) => - val src = StringSource(s"import $path", source.name) - super.processline(src, console, config) - Some(config) - - case Command(":imports", _) => - output.emitln(module.includes.map { i => i.path }.mkString("\n")) - Some(config) - - case Command(":help", _) | Command(":h", _) => - help() - Some(config) - - case Command(":quit", _) | Command(":q", _) => - None - - case Command(cmd, expr) if cmd == ":type" || cmd == ":t" => - typecheck(StringSource(expr, source.name), config) - Some(config) - - case Command(cmd, _) => - output.emitln(s"Unknown command ${cmd}, enter :help for a list of available commands") - Some(config) - - // Otherwise it's an expression for evaluation - case _ => - super.processline(source, console, config) - } - } - - /** - * Processes the given tree, but only commits changes to definitions and - * imports, if they typecheck. - */ - def process(source: Source, tree: Tree, config: EffektConfig): Unit = tree match { - case e: Term => - runCompiler(source, module.makeEval(e), config) - - case i: Include => - val extendedIncludes = module + i - val output = config.output() - - context.setup(config) - - val src = context.findSource(i.path).getOrElseAborting { - output.emitln(s"Cannot find source for import ${i.path}") - return - } - - runParsingFrontend(src, config) { cu => - output.emitln(s"Successfully imported ${i.path}\n") - output.emitln(s"Imported Types\n==============") - cu.exports.types.toList.sortBy { case (n, _) => n }.collect { - case (name, sym) if !sym.isSynthetic => - outputCode(DeclPrinter(sym), config) - } - output.emitln(s"\nImported Functions\n==================") - cu.exports.terms.toList.sortBy { case (n, _) => n }.foreach { - case (name, syms) => - syms.collect { - case sym if !sym.isSynthetic => - outputCode(DeclPrinter(sym), config) - } - } - module = extendedIncludes - } - - case d: Def => - val extendedDefs = module + d - val decl = extendedDefs.make(UnitLit()) - - runFrontend(source, decl, config) { cu => - module = extendedDefs - - // try to find the symbol for the def to print the type - (context.symbolOption(d.id) match { - case Some(v: ValueSymbol) => - Some(context.valueTypeOf(v)) - case Some(b: BlockSymbol) => - Some(context.blockTypeOf(b)) - case t => - None - }) map { tpe => - outputCode(pp"${d.id}: ${tpe}", config) - } - } - - case _ => () - } - - private def runCompiler(source: Source, ast: ModuleDecl, config: EffektConfig): Unit = - driver.compileSource(VirtualSource(ast, source), config) - - private def runFrontend(source: Source, ast: ModuleDecl, config: EffektConfig)(f: Module => Unit): Unit = { - context.setup(config) - val src = VirtualSource(ast, source) - context.compiler.runFrontend(src) map { f } getOrElse { - report(source, context.messaging.buffer, context.config) - } - } - - private def runParsingFrontend(source: Source, config: EffektConfig)(f: Module => Unit): Unit = { - context.setup(config) - context.compiler.runFrontend(source) map { f } getOrElse { - report(source, context.messaging.buffer, context.config) - } - } - - object Command { - import scala.util.matching.Regex - - val command = ":[\\w]+".r - def unapply(str: String): Option[(String, String)] = command.findPrefixMatchOf(str) map { - m => (m.matched, str.substring(m.end).trim) - } - } - - def outputCode(code: String, config: EffektConfig): Unit = - config.output().emitln(AnsiHighlight(code)) - - /** - * Enables persistent command history on JLine - */ - def usingCommandHistory[T](config: EffektConfig)(block: => T): T = { - import kiama.util.JLineConsole - import effekt.util.paths._ - import jline.console.history.FileHistory - - config.console() match { - case c: JLineConsole.type => - val historyFile = file(System.getProperty("user.home")) / ".effekt_history" - val history = new FileHistory(historyFile.toFile) - c.reader.setHistory(history) - c.reader.setHistoryEnabled(true) - - try { block } finally { history.flush() } - case _ => block - } - } - - private var module: ReplModule = emptyModule - - /** - * A virtual module to which we add by using the REPL - */ - case class ReplModule( - definitions: List[Def], - includes: List[Include] - ) { - def +(d: Def) = { - // drop all equally named definitions for now. - val otherDefs = definitions.filterNot { other => other.id.name == d.id.name } - copy(definitions = otherDefs :+ d) - } - def +(i: Include) = copy(includes = includes.filterNot { _.path == i.path } :+ i) - - def contains(im: Include) = includes.exists { other => im.path == other.path } - - /** - * Create a module declaration using the given expression as body of main - */ - def make(expr: Term): ModuleDecl = { - - val body = Return(expr) - - ModuleDecl("interactive", includes, - definitions :+ FunDef(IdDef("main"), Nil, Nil, Nil, None, - body)) - } - - def makeEval(expr: Term): ModuleDecl = - make(Call(IdTarget(IdRef(List(), "println")), Nil, List(expr), Nil)) - } - lazy val emptyModule = ReplModule(Nil, Nil) -} diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala deleted file mode 100644 index 994214b48..000000000 --- a/effekt/jvm/src/main/scala/effekt/Runner.scala +++ /dev/null @@ -1,340 +0,0 @@ -package effekt - -import effekt.context.Context -import effekt.util.messages.FatalPhaseError -import effekt.util.paths.{File, file} -import effekt.util.{getOrElseAborting, escape, OS, os} -import kiama.util.IO - -/** - * Interface used by [[Driver]] and [[EffektTests]] to run a compiled program. - * - * @tparam Executable the executable, as produced by [[BackendCompiler.compile]]. - */ -trait Runner[Executable] { - - import scala.sys.process.* - - /** - * Path to the standard library. - * - * @param root is the path of the Effekt compiler installation - */ - def standardLibraryPath(root: File): File - - /** - * File extension of generated files (e.g. "js", or "sml") - */ - def extension: String - - /** - * Additional includes the specific backend requires. - * - * @param stdlibPath is the path to the standard library - */ - def includes(stdlibPath: File): List[File] = Nil - - /** - * Modules this backend loads by default. - * - * Invariants: - * - All imports of prelude modules have to be in the prelude as well. - * - The order matters and should correspond to the topological ordering with respect to the imports, that is, - * if module A depends on module B, then B should come before A. - * - Furthermore, each module mentioned here must import the `effekt` module as its first import. - */ - def prelude: List[String] = List("effekt", "option", "list", "result", "exception", "array", "string", "ref") - - /** - * Creates a OS-specific script file that will execute the command when executed, - * forwarding command line arguments. - * `$SCRIPT_DIR` refers to the directory the script is in. - * - * @return the actual name of the generated script (might be `!= name`) - */ - def createScript(name: String, command: String*): String = os match { - case OS.POSIX => - val computeScriptDir = - """# Determine the directory of the script - |SCRIPT_DIR=$(dirname "$(realpath "$0")") - |""".stripMargin - IO.createFile(name, s"#!/bin/sh\n${computeScriptDir}\n${command.mkString(" ")} \"$$@\"", true) - name - case OS.Windows => - val computeScriptDir = - """setlocal enabledelayedexpansion - | - |:: Get the directory of the batch file - |set "SCRIPT_DIR=%~dp0" - | - |:: Remove trailing backslash - |set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" - |""".stripMargin - val batName = name + ".bat" - // replace UNIX-style variables in command: $SCRIPT_DIR --> %SCRIPT_DIR% - val cmd = command.mkString(" ").replaceAll("\\$([A-Za-z_][A-Za-z0-9_]*)", "%$1%") - IO.createFile(batName, s"@echo off\r\n${computeScriptDir}\r\n${cmd} %*") - batName - } - - /** - * Should check whether everything is installed for this backend - * to run. Should return Right(()) if everything is ok and - * Left(explanation) if something is missing. - */ - def checkSetup(): Either[String, Unit] - - /** - * Builds a given executable and returns the resulting path to the executable. - */ - def build(executable: Executable)(using Context): Option[String] - - /** - * Runs the executable (e.g. the main file) by calling the build function. - */ - def eval(executable: Executable)(using C: Context): Unit = build(executable).foreach { execFile => - val valgrindArgs = Seq("--leak-check=full", "--undef-value-errors=no", "--quiet", "--log-file=valgrind.log", "--error-exitcode=1") - val process = if (C.config.valgrind()) - Process("valgrind", valgrindArgs ++ (execFile +: Context.config.runArgs())) - else - Process(execFile, Context.config.runArgs()) - - val exitCode = process.run(new ProcessLogger { - - override def out(s: => String): Unit = { - C.config.output().emitln(s) - } - - override def err(s: => String): Unit = System.err.println(s) - - override def buffer[T](f: => T): T = f - - }, connectInput = true).exitValue() - - if (exitCode != 0) { - C.error(s"Process exited with non-zero exit code ${exitCode}.") - if (C.config.valgrind()) C.error(s"Valgrind log:\n" ++ scala.io.Source.fromFile("valgrind.log").mkString) - } - } - - def canRunExecutable(command: String*): Boolean = - try { - Process(command).run(ProcessIO(out => (), in => (), err => ())).exitValue() == 0 - } catch { case _ => false } - - /** - * Helper function to run an executable - */ - def exec(command: String*)(using C: Context): Unit = try { - val p = Process(command) - C.config.output().emit(p.!!) - } catch { - case FatalPhaseError(e) => C.report(e) - } - - /** - * Try running a handful of names for a system executable; returns the first successful name, - * if any. - */ - def discoverExecutable(progs0: List[String], args: Seq[String]): Option[String] = { - def go(progs: List[String]): Option[String] = progs match { - case prog :: progs => - try { Process(prog +: args).!!; Some(prog) } - catch case ioe => go(progs) - case _ => None - } - go(progs0) - } -} - -object JSNodeRunner extends Runner[String] { - import scala.sys.process.Process - - val extension = "js" - - def standardLibraryPath(root: File): File = root / "libraries" / "common" - - override def includes(path: File): List[File] = List(path / ".." / "js") - - def checkSetup(): Either[String, Unit] = - if canRunExecutable("node", "--version") then Right(()) - else Left("Cannot find nodejs. This is required to use the JavaScript backend.") - - /** - * Creates an executable `.js` file besides the given `.js` file ([[path]]) - * and then returns the absolute path of the created executable. - */ - def build(path: String)(using C: Context): Option[String] = - val out = C.config.outputPath().getAbsolutePath - val jsFilePath = (out / path).canonicalPath.escape - val jsFileName = path.unixPath.split("/").last - // create "executable" using shebang besides the .js file - val jsScript = s"require('./${jsFileName}').main()" - - os match { - case OS.POSIX => - val shebang = "#!/usr/bin/env node" - val jsScriptFilePath = jsFilePath.stripSuffix(s".$extension") - IO.createFile(jsScriptFilePath, s"$shebang\n$jsScript", true) - Some(jsScriptFilePath) - - case OS.Windows => - val jsMainFilePath = jsFilePath.stripSuffix(s".$extension") + "__main.js" - val jsMainFileName = jsFileName.stripSuffix(s".$extension") + "__main.js" - val exePath = jsFilePath.stripSuffix(s".$extension") - IO.createFile(jsMainFilePath, jsScript) - Some(createScript(exePath, "node", "$SCRIPT_DIR/" + jsMainFileName)) - } -} -object JSWebRunner extends Runner[String] { - - val extension = "js" - - def standardLibraryPath(root: File): File = root / "libraries" / "common" - - override def includes(path: File): List[File] = List(path / ".." / "js") - - def checkSetup(): Either[String, Unit] = - Left("Running js-web code directly is not supported. Use `--compile` to generate a js file / `--build` to generate a html file.") - - /** - * Creates an openable `.html` file besides the given `.js` file ([[path]]) - * and then errors out, printing it's path. - */ - def build(path: String)(using C: Context): Option[String] = - val out = C.config.outputPath().getAbsolutePath - val jsFilePath = (out / path).unixPath - val jsFileName = path.unixPath.split("/").last - val htmlFilePath = jsFilePath.stripSuffix(s".$extension") + ".html" - val mainName = "$" + jsFileName.stripSuffix(".js") + ".main" - val htmlContent = - s""" - | - | - | - | - |
- | - | - |""".stripMargin - IO.createFile(htmlFilePath, htmlContent, false) - - C.info(s"Open file://${htmlFilePath} in your browser or include ${jsFilePath}.") - None -} - -trait ChezRunner extends Runner[String] { - val extension = "ss" - - def standardLibraryPath(root: File): File = root / "libraries" / "common" - - def checkSetup(): Either[String, Unit] = - if canRunExecutable("scheme", "--help") then Right(()) - else Left("Cannot find scheme. This is required to use the ChezScheme backend.") - - /** - * Creates an executable bash script besides the given `.ss` file ([[path]]) - * and returns the resulting absolute path. - */ - def build(path: String)(using C: Context): Option[String] = - val out = C.config.outputPath().getAbsolutePath - val schemeFilePath = (out / path).canonicalPath.escape - val exeScriptPath = schemeFilePath.stripSuffix(s".$extension") - val schemeFileName = ("./" + (path.unixPath.split('/').last)).escape - Some(createScript(exeScriptPath, "scheme", "--script", "$SCRIPT_DIR/" + schemeFileName)) -} - -object ChezMonadicRunner extends ChezRunner { - override def includes(path: File): List[File] = List( - path / ".." / "chez" / "common", - path / ".." / "chez" / "monadic") -} - -object ChezCallCCRunner extends ChezRunner { - override def includes(path: File): List[File] = List( - path / ".." / "chez" / "common", - path / ".." / "chez" / "callcc") -} - -object LLVMRunner extends Runner[String] { - - val extension = "ll" - - def standardLibraryPath(root: File): File = root / "libraries" / "common" - - override def includes(path: File): List[File] = List(path / ".." / "llvm") - - lazy val gccCmd = discoverExecutable(List("cc", "clang", "gcc"), List("--version")) - lazy val llcCmd = discoverExecutable(List("llc", "llc-18"), List("--version")) - lazy val optCmd = discoverExecutable(List("opt", "opt-18"), List("--version")) - - def checkSetup(): Either[String, Unit] = - gccCmd.getOrElseAborting { return Left("Cannot find gcc. This is required to use the LLVM backend.") } - llcCmd.getOrElseAborting { return Left("Cannot find llc. This is required to use the LLVM backend.") } - optCmd.getOrElseAborting { return Left("Cannot find opt. This is required to use the LLVM backend.") } - Right(()) - - def libuvArgs(using C: Context): Seq[String] = - val OS = System.getProperty("os.name").toLowerCase - val libraries = C.config.gccLibraries.toOption.map(file).orElse { - OS match { - case os if os.contains("mac") => Some(file("/opt/homebrew/lib")) - case os if os.contains("win") => None - case os if os.contains("linux") => Some(file("/usr/local/lib")) - case os => None - } - } - val includes = C.config.gccIncludes.toOption.map(file).orElse { - OS match { - case os if os.contains("mac") => Some(file("/opt/homebrew/include")) - case os if os.contains("win") => None - case os if os.contains("linux") => Some(file("/usr/local/include")) - case os => None - } - } - (libraries, includes) match { - case (Some(lib), Some(include)) => Seq(s"-L${lib.unixPath}", "-luv", s"-I${include.unixPath}") - case _ => - C.warning(s"Cannot find libuv on ${OS}; please use --gcc-libraries and --gcc-includes to configure the paths for the libuv dylib and header files, respectively.") - Seq() - } - - /** - * Compile the LLVM source file (`<...>.ll`) to an executable - * - * Requires LLVM and GCC to be installed on the machine. - * Assumes [[path]] has the format "SOMEPATH.ll". - */ - override def build(path: String)(using C: Context): Option[String] = - - val out = C.config.outputPath() - val basePath = (out / path.stripSuffix(".ll")).unixPath - val llPath = basePath + ".ll" - val optPath = basePath + ".opt.ll" - val objPath = basePath + ".o" - val linkedLibraries = Seq( - "-lm", // Math library - ) ++ libuvArgs - - def missing(cmd: String) = C.abort(s"Cannot find ${cmd}. This is required to use the LLVM backend.") - val gcc = gccCmd.getOrElse(missing("gcc")) - val llc = llcCmd.getOrElse(missing("llc")) - val opt = optCmd.getOrElse(missing("opt")) - - exec(opt, llPath, "-S", "-O2", "-o", optPath) - exec(llc, "--relocation-model=pic", optPath, "-filetype=obj", "-o", objPath) - - val gccMainFile = (C.config.libPath / ".." / "llvm" / "main.c").unixPath - val executableFile = basePath - var gccArgs = Seq(gcc, gccMainFile, "-o", executableFile, objPath) ++ linkedLibraries - - if (C.config.debug()) gccArgs ++= Seq("-g", "-Wall", "-Wextra", "-Werror") - if (C.config.valgrind()) gccArgs ++= Seq("-O0", "-g") - else if (C.config.debug()) gccArgs ++= Seq("-fsanitize=address,undefined", "-fstack-protector-all") - - exec(gccArgs: _*) - - Some(executableFile) -} diff --git a/effekt/jvm/src/main/scala/effekt/Server.scala b/effekt/jvm/src/main/scala/effekt/Server.scala deleted file mode 100644 index 17705f7be..000000000 --- a/effekt/jvm/src/main/scala/effekt/Server.scala +++ /dev/null @@ -1,593 +0,0 @@ -package effekt - -import com.google.gson.JsonElement -import kiama.util.Convert.* -import effekt.context.Context -import effekt.source.Def.FunDef -import effekt.source.Term.Hole -import effekt.source.Tree -import effekt.symbols.Binder.{ValBinder, VarBinder} -import effekt.symbols.BlockTypeConstructor.{ExternInterface, Interface} -import effekt.symbols.TypeConstructor.{DataType, ExternType} -import effekt.symbols.{Anon, Binder, Callable, Effects, Module, Param, Symbol, TypeAlias, TypePrinter, UserFunction, ValueType, isSynthetic} -import effekt.util.{PlainMessaging, PrettyPrinter} -import effekt.util.messages.EffektError -import kiama.util.Collections.{mapToJavaMap, seqToJavaList} -import kiama.util.{Collections, Convert, Position, Source} -import org.eclipse.lsp4j.jsonrpc.services.JsonNotification -import org.eclipse.lsp4j.jsonrpc.{Launcher, messages} -import org.eclipse.lsp4j.launch.LSPLauncher -import org.eclipse.lsp4j.services.* -import org.eclipse.lsp4j.{CodeAction, CodeActionKind, CodeActionParams, Command, DefinitionParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InitializeParams, InitializeResult, InlayHint, InlayHintKind, InlayHintParams, Location, LocationLink, MarkupContent, MessageParams, MessageType, PublishDiagnosticsParams, ReferenceParams, SaveOptions, ServerCapabilities, SetTraceParams, SymbolInformation, SymbolKind, TextDocumentSaveRegistrationOptions, TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkspaceEdit, Range as LSPRange} - -import java.io.{InputStream, OutputStream, PrintWriter} -import java.net.ServerSocket -import java.nio.file.Paths -import java.util -import java.util.concurrent.{CompletableFuture, ExecutorService, Executors} - -/** - * Effekt Language Server - * - * @param compileOnChange Whether to compile on `didChange` events - * Currently disabled because references are erased when there are any compiler errors. - * Therefore, we currently only update on `didSave` until we have working caching for references. - */ -class Server(config: EffektConfig, compileOnChange: Boolean=false) extends LanguageServer with Driver with Intelligence with TextDocumentService with WorkspaceService { - private var client: EffektLanguageClient = _ - private val textDocumentService = this - private val workspaceService = this - - // Track whether shutdown has been requested - private var shutdownRequested: Boolean = false - // Configuration sent by the language client - var settings: JsonElement = null - - val getDriver: Driver = this - val getConfig: EffektConfig = config - - object lspMessaging extends PlainMessaging - - // LSP Lifecycle - // - // - - override def initialize(params: InitializeParams): CompletableFuture[InitializeResult] = { - val capabilities = new ServerCapabilities() - capabilities.setTextDocumentSync(TextDocumentSyncKind.Full) - capabilities.setHoverProvider(true) - capabilities.setDefinitionProvider(true) - capabilities.setReferencesProvider(true) - capabilities.setDocumentSymbolProvider(true) - capabilities.setCodeActionProvider(true) - capabilities.setInlayHintProvider(true) - - // We need to explicitly ask the client to include the text on save events. - // Otherwise, when not listening to `didChange`, we have no way to get the text of the file, - // when the client decides not to include the text in the `didSave` event. - val saveOptions = new SaveOptions() - saveOptions.setIncludeText(true) - - val syncOptions = new TextDocumentSyncOptions(); - syncOptions.setOpenClose(true); - syncOptions.setChange(TextDocumentSyncKind.Full); - syncOptions.setSave(saveOptions); - capabilities.setTextDocumentSync(syncOptions); - - // Load the initial settings from client-sent `initializationOptions` (if any) - // This is not part of the LSP standard, but seems to be the most reliable way to have the correct initial settings - // on first compile. - // There is a `workspace/didChangeConfiguration` notification and a `workspace/configuration` request, but we cannot - // guarantee that the client will send these before the file is first compiled, leading to either duplicate work - // or a bad user experience. - if (params.getInitializationOptions != null) - workspaceService.didChangeConfiguration(new DidChangeConfigurationParams(params.getInitializationOptions)) - - val result = new InitializeResult(capabilities) - CompletableFuture.completedFuture(result) - } - - override def shutdown(): CompletableFuture[Object] = { - shutdownRequested = true - CompletableFuture.completedFuture(null) - } - - override def exit(): Unit = { - System.exit(if (shutdownRequested) 0 else 1) - } - - override def setTrace(params: SetTraceParams): Unit = { - // Do nothing - } - - // The LSP services are also implemented by the Server class as they are strongly coupled anyway. - override def getTextDocumentService(): TextDocumentService = this - override def getWorkspaceService(): WorkspaceService = this - - // LSP Diagnostics - // - // - - def clearDiagnostics(name: String): Unit = { - publishDiagnostics(name, Vector()) - } - - def publishDiagnostics(name: String, diagnostics: Vector[Diagnostic]): Unit = { - val params = new PublishDiagnosticsParams(Convert.toURI(name), Collections.seqToJavaList(diagnostics)) - client.publishDiagnostics(params) - } - - // Custom Effekt extensions - // - // - - /** - * Publish Effekt IR for a given source file - * - * @param source The Kiama source file - * @param config The Effekt configuration - * @param C The compiler context - */ - def publishIR(source: Source, config: EffektConfig)(implicit C: Context): Unit = { - // Publish Effekt IR - val showIR = workspaceService.settingString("showIR").getOrElse("none") - val showTree = workspaceService.settingBool("showTree") - - if (showIR == "none") { - return; - } - - if (showIR == "source") { - val tree = C.compiler.getAST(source) - if (tree.isEmpty) return - client.publishIR(EffektPublishIRParams( - basename(source.name) + ".scala", - PrettyPrinter.format(tree.get).layout - )) - return; - } - - def unsupported = - throw new RuntimeException(s"Combination of '${showIR}' and showTree=${showTree} not supported by backend ${C.config.backend().name}") - - val stage = showIR match { - case "core" => Stage.Core - case "machine" => Stage.Machine - case "target" => Stage.Target - case _ => unsupported - } - - if (showTree) { - client.publishIR(EffektPublishIRParams( - basename(source.name) + ".scala", - PrettyPrinter.format(C.compiler.treeIR(source, stage).getOrElse(unsupported)).layout - )) - } else if (showIR == "target") { - client.publishIR(EffektPublishIRParams( - basename(source.name) + "." + C.runner.extension, - C.compiler.prettyIR(source, Stage.Target).getOrElse(unsupported).layout - )) - } else { - client.publishIR(EffektPublishIRParams( - basename(source.name) + ".ir", - C.compiler.prettyIR(source, stage).getOrElse(unsupported).layout - )) - } - } - - // Driver methods - // - // - - override def afterCompilation(source: Source, config: EffektConfig)(implicit C: Context): Unit = { - // Publish LSP diagnostics - val messages = C.messaging.buffer - val groups = messages.groupBy(msg => msg.sourceName.getOrElse("")) - for ((name, msgs) <- groups) { - publishDiagnostics(name, msgs.distinct.map(Convert.messageToDiagnostic(lspMessaging))) - } - try { - publishIR(source, config) - } catch { - case e => client.logMessage(new MessageParams(MessageType.Error, e.toString + ":" + e.getMessage)) - } - } - - // Other methods - // - // - - def basename(filename: String): String = { - val name = Paths.get(filename).getFileName.toString - val dotIndex = name.lastIndexOf('.') - if (dotIndex > 0) name.substring(0, dotIndex) else name - } - - def connect(client: EffektLanguageClient): Unit = { - this.client = client - } - - /** - * Launch a language server using provided input/output streams. - * This allows tests to connect via in-memory pipes. - */ - def launch(client: EffektLanguageClient, in: InputStream, out: OutputStream): Launcher[EffektLanguageClient] = { - val executor = Executors.newSingleThreadExecutor() - val launcher = - new LSPLauncher.Builder() - .setLocalService(this) - .setRemoteInterface(classOf[EffektLanguageClient]) - .setInput(in) - .setOutput(out) - .setExecutorService(executor) - .create() - this.connect(client) - launcher.startListening() - launcher - } - - // LSP Document Lifecycle - // - // - - def didChange(params: DidChangeTextDocumentParams): Unit = { - if (!compileOnChange) return - val document = params.getTextDocument - clearDiagnostics(document.getUri) - getDriver.compileString(document.getUri, params.getContentChanges.get(0).getText, getConfig) - } - - def didClose(params: DidCloseTextDocumentParams): Unit = { - clearDiagnostics(params.getTextDocument.getUri) - } - - def didOpen(params: DidOpenTextDocumentParams): Unit = { - val document = params.getTextDocument - clearDiagnostics(document.getUri) - getDriver.compileString(document.getUri, document.getText, getConfig) - } - - def didSave(params: DidSaveTextDocumentParams): Unit = { - val document = params.getTextDocument - val text = Option(params.getText) match { - case Some(t) => t - case None => - return - } - clearDiagnostics(document.getUri) - getDriver.compileString(document.getUri, text, getConfig) - } - - // LSP Hover - // - // - - override def hover(params: HoverParams): CompletableFuture[Hover] = { - val position = sources.get(params.getTextDocument.getUri).map { source => - Convert.fromLSPPosition(params.getPosition, source) - } - position match - case Some(position) => { - val hover = getSymbolHover(position) orElse getHoleHover(position) - val markup = new MarkupContent("markdown", hover.getOrElse("")) - val result = new Hover(markup, new LSPRange(params.getPosition, params.getPosition)) - CompletableFuture.completedFuture(result) - } - case None => CompletableFuture.completedFuture(new Hover()) - } - - def getSymbolHover(position: Position): Option[String] = for { - (tree, sym) <- getSymbolAt(position)(using context) - info <- getInfoOf(sym)(using context) - } yield if (settingBool("showExplanations")) info.fullDescription else info.shortDescription - - def getHoleHover(position: Position): Option[String] = for { - trees <- getTreesAt(position)(using context) - tree <- trees.collectFirst { case h: source.Hole => h } - info <- getHoleInfo(tree)(using context) - } yield info - - // LSP Document Symbols - // - // - - override def documentSymbol(params: DocumentSymbolParams): CompletableFuture[util.List[messages.Either[SymbolInformation, DocumentSymbol]]] = { - val source = sources.get(params.getTextDocument.getUri) - if (source.isEmpty) return CompletableFuture.completedFuture(Collections.seqToJavaList(Vector())) - - context.compiler.runFrontend(source.get)(using context) - - val documentSymbols = for { - sym <- context.sourceSymbolsFor(source.get).toVector - if !sym.isSynthetic - id <- context.definitionTreeOption(sym) - decl <- getSourceTreeFor(sym) - kind <- getSymbolKind(sym) - detail <- getInfoOf(sym)(using context) - declRange = convertRange(positions.getStart(decl), positions.getFinish(decl)) - idRange = convertRange(positions.getStart(id), positions.getFinish(id)) - } yield new DocumentSymbol(sym.name.name, kind, declRange, idRange, detail.header) - - val result = Collections.seqToJavaList( - documentSymbols.map(sym => messages.Either.forRight[SymbolInformation, DocumentSymbol](sym)) - ) - CompletableFuture.completedFuture(result) - } - - def getSourceTreeFor(sym: effekt.symbols.Symbol): Option[Tree] = sym match { - case a: Anon => Some(a.decl) - case f: UserFunction => Some(f.decl) - case b: Binder => Some(b.decl) - case _ => context.definitionTreeOption(sym) - } - - def getSymbolKind(sym: Symbol): Option[SymbolKind] = - sym match { - case _: Module => - Some(SymbolKind.Module) - case _: Interface | _: ExternInterface => - Some(SymbolKind.Interface) - case _: DataType | _: ExternType | _: TypeAlias => - Some(SymbolKind.Enum) - case _: Callable => - Some(SymbolKind.Method) - case _: Param | _: ValBinder | _: VarBinder => - Some(SymbolKind.Variable) - case _ => - None - } - - // LSP Go To Definition - // - // - - override def definition(params: DefinitionParams): CompletableFuture[messages.Either[util.List[_ <: Location], util.List[_ <: LocationLink]]] = { - val location = for { - position <- sources.get(params.getTextDocument.getUri).map { source => - fromLSPPosition(params.getPosition, source) - }; - definition <- getDefinitionAt(position)(using context); - location = locationOfNode(positions, definition) - } yield location - - val result = location.map(l => messages.Either.forLeft[util.List[_ <: Location], util.List[_ <: LocationLink]](Collections.seqToJavaList(List(l)))) - .getOrElse(messages.Either.forLeft(Collections.seqToJavaList(List()))) - - CompletableFuture.completedFuture(result) - } - - // LSP References - // - // - - override def references(params: ReferenceParams): CompletableFuture[util.List[_ <: Location]] = { - val position = sources.get(params.getTextDocument.getUri).map { source => - fromLSPPosition(params.getPosition, source) - } - if (position.isEmpty) - return CompletableFuture.completedFuture(Collections.seqToJavaList(Vector())) - - val locations = for { - (tree, sym) <- getSymbolAt(position.get)(using context) - refs = context.distinctReferencesTo(sym) - // getContext may be null! - includeDeclaration = Option(params.getContext).exists(_.isIncludeDeclaration) - allRefs = if (includeDeclaration) tree :: refs else refs - locations = allRefs.map(ref => locationOfNode(positions, ref)) - } yield locations - - CompletableFuture.completedFuture(Collections.seqToJavaList(locations.getOrElse(Seq[Location]()))) - } - - // LSP Inlay Hints - // - // - - override def inlayHint(params: InlayHintParams): CompletableFuture[util.List[InlayHint]] = { - val hints = for { - source <- sources.get(params.getTextDocument.getUri) - hints = { - val range = fromLSPRange(params.getRange, source) - getInferredCaptures(range)(using context).map { - case (p, c) => - val prettyCaptures = TypePrinter.show(c) - val inlayHint = new InlayHint(convertPosition(p), messages.Either.forLeft(prettyCaptures)) - inlayHint.setKind(InlayHintKind.Type) - val markup = new MarkupContent() - markup.setValue(s"captures: `${prettyCaptures}`") - markup.setKind("markdown") - inlayHint.setTooltip(markup) - inlayHint.setPaddingRight(true) - inlayHint.setData("capture") - inlayHint - }.toVector - } - } yield hints - - CompletableFuture.completedFuture(Collections.seqToJavaList(hints.getOrElse(Vector()))) - } - - // LSP Code Actions - // - // - - // FIXME: This is the code actions code from the previous language server implementation. - // It doesn't even work in the previous implementation. - override def codeAction(params: CodeActionParams): CompletableFuture[util.List[messages.Either[Command, CodeAction]]] = { - val codeActions = for { - position <- sources.get(params.getTextDocument.getUri).map { source => - fromLSPPosition(params.getRange.getStart, source) - }; - codeActions = for { - trees <- getTreesAt(position)(using context).toVector - actions <- trees.flatMap { t => action(t)(using context) } - } yield actions - } yield codeActions.toList - - val result = codeActions.getOrElse(List[CodeAction]()).map(messages.Either.forRight[Command, CodeAction]) - CompletableFuture.completedFuture(Collections.seqToJavaList(result)) - } - - def action(tree: Tree)(using C: Context): Option[CodeAction] = tree match { - case f: FunDef => inferEffectsAction(f) - case h: Hole => closeHoleAction(h) - case _ => None - } - - def EffektCodeAction(description: String, oldNode: Any, newText: String): Option[CodeAction] = { - for { - posFrom <- positions.getStart(oldNode) - posTo <- positions.getFinish(oldNode) - } yield { - val textEdit = new TextEdit(convertRange(Some(posFrom), Some(posTo)), newText) - val changes = Map(posFrom.source.name -> seqToJavaList(List(textEdit))) - val workspaceEdit = new WorkspaceEdit(mapToJavaMap(changes)) - val action = new CodeAction(description) - action.setKind(CodeActionKind.Refactor) - action.setEdit(workspaceEdit) - action - } - } - - /** - * FIXME: The following comment was left on the previous Kiama-based implementation and can now be addressed: - * - * TODO it would be great, if Kiama would allow setting the position of the code action separately - * from the node to replace. Here, we replace the annotated return type, but would need the - * action on the function (since the return type might not exist in the original program). - * - * Also, it is necessary to be able to manually set the code action kind (and register them on startup). - * This way, we can use custom kinds like `refactor.closehole` that can be mapped to keys. - */ - def inferEffectsAction(fun: FunDef)(using C: Context): Option[CodeAction] = for { - // the inferred type - (tpe, eff) <- C.inferredTypeAndEffectOption(fun) - // the annotated type - ann = for { - result <- fun.symbol.annotatedResult - effects <- fun.symbol.annotatedEffects - } yield (result, effects) - if ann.map { - needsUpdate(_, (tpe, eff)) - }.getOrElse(true) - res <- EffektCodeAction("Update return type with inferred effects", fun.ret, s": $tpe / $eff") - } yield res - - def closeHoleAction(hole: Hole)(using C: Context): Option[CodeAction] = for { - holeTpe <- C.inferredTypeOption(hole) - contentTpe <- C.inferredTypeOption(hole.stmts) - if holeTpe == contentTpe - res <- hole match { - case Hole(source.Return(exp)) => for { - text <- positions.textOf(exp) - res <- EffektCodeAction("Close hole", hole, text) - } yield res - - // <{ s1 ; s2; ... }> - case Hole(stmts) => for { - text <- positions.textOf(stmts) - res <- EffektCodeAction("Close hole", hole, s"locally { ${text} }") - } yield res - } - } yield res - - def needsUpdate(annotated: (ValueType, Effects), inferred: (ValueType, Effects))(using Context): Boolean = { - val (tpe1, effs1) = annotated - val (tpe2, effs2) = inferred - tpe1 != tpe2 || effs1 != effs2 - } - - // LSP methods - // - // - - def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = { - this.settings = params.getSettings.asInstanceOf[JsonElement].getAsJsonObject - } - - def didChangeWatchedFiles(params: DidChangeWatchedFilesParams): Unit = {} - - // Settings - // - // - - def settingBool(name: String): Boolean = { - if (settings == null) return false - val obj = settings.getAsJsonObject - if (obj == null) return false - val value = obj.get(name) - if (value == null) return false - value.getAsBoolean - } - - def settingString(name: String): Option[String] = { - if (settings == null) return None - val obj = settings.getAsJsonObject - if (obj == null) return None - val value = obj.get(name) - if (value == null) return None - Some(value.getAsString) - } - - /** - * Launch a language server with a given `ServerConfig` - */ - def launch(config: ServerConfig): Unit = { - // Create a single-threaded executor to serialize all requests. - val executor: ExecutorService = Executors.newSingleThreadExecutor() - - if (config.debug) { - val serverSocket = new ServerSocket(config.debugPort) - System.err.println(s"Starting language server in debug mode on port ${config.debugPort}") - val socket = serverSocket.accept() - - val launcher = - new LSPLauncher.Builder() - .setLocalService(this) - .setRemoteInterface(classOf[EffektLanguageClient]) - .setInput(socket.getInputStream) - .setOutput(socket.getOutputStream) - .setExecutorService(executor) - .traceMessages(new PrintWriter(System.err, true)) - .create() - val client = launcher.getRemoteProxy - this.connect(client) - launcher.startListening() - } else { - val launcher = - new LSPLauncher.Builder() - .setLocalService(this) - .setRemoteInterface(classOf[EffektLanguageClient]) - .setInput(System.in) - .setOutput(System.out) - .setExecutorService(executor) - .create() - - val client = launcher.getRemoteProxy - this.connect(client) - launcher.startListening() - } - } -} - -case class ServerConfig(debug: Boolean = false, debugPort: Int = 5000) - -trait EffektLanguageClient extends LanguageClient { - /** - * Custom LSP notification to publish Effekt IR - * - * @param params The parameters for the notification - */ - @JsonNotification("$/effekt/publishIR") - def publishIR(params: EffektPublishIRParams): Unit -} - -/** - * Custom LSP notification to publish Effekt IR - * - * @param filename The filename of the resulting IR file - * @param content The IR content - */ -case class EffektPublishIRParams(filename: String, - content: String -) diff --git a/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala b/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala deleted file mode 100644 index 4d3ac96ea..000000000 --- a/effekt/jvm/src/main/scala/effekt/context/IOModuleDB.scala +++ /dev/null @@ -1,36 +0,0 @@ -package effekt -package context - -import effekt.util.paths._ -import effekt.util.MarkdownSource -import kiama.util.{ FileSource, Filenames, IO, Source } - -trait IOModuleDB extends ModuleDB { self: Context => - - /** - * Tries to find a file in the workspace, that matches the import path - * - * used by Namer to resolve FFI includes - */ - override def contentsOf(path: String): Option[String] = { - val parent = file(module.source.name).parent - (parent :: config.includes().map(file)).collectFirst { - case base if (base / path).exists => FileSource((base / path).toString).content - } - } - - /** - * First try to find it in the includes paths, then in the bundled resources - */ - override def findSource(modulePath: String): Option[Source] = { - // ATTENTION modulePath can contain non-platform dependent path separators - - def effektFile(include: File) = include / (modulePath + ".effekt") - def mdFile(include: File) = include / (modulePath + ".effekt.md") - - config.includes().collectFirst { - case p if effektFile(p).exists => FileSource(effektFile(p).toString) - case p if mdFile(p).exists => MarkdownSource(FileSource(mdFile(p).toString)) - } - } -} diff --git a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala b/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala deleted file mode 100644 index d0db787fa..000000000 --- a/effekt/jvm/src/test/scala/effekt/ChezSchemeTests.scala +++ /dev/null @@ -1,78 +0,0 @@ -package effekt - -import java.io.File - -import sbt.io._ -import sbt.io.syntax._ - -import scala.language.implicitConversions - -abstract class ChezSchemeTests extends EffektTests { - - override def positives: List[File] = List( - examplesDir / "pos", - examplesDir / "casestudies", - examplesDir / "chez", - examplesDir / "benchmarks" - ) - - // Test files which are to be ignored (since features are missing or known bugs exist) - override def ignored: List[File] = List( - - examplesDir / "llvm", - - // bidirectional effects are not yet supported in our Chez backend - examplesDir / "pos" / "maps.effekt", - examplesDir / "pos" / "bidirectional", - examplesDir / "pos" / "object", - examplesDir / "pos" / "type_omission_op.effekt", - - // filesystem operations and bytearrays are not yet supported in our Chez backend - examplesDir / "benchmarks" / "input_output" / "word_count_ascii.effekt", - examplesDir / "benchmarks" / "input_output" / "word_count_utf8.effekt", - examplesDir / "benchmarks" / "input_output" / "dyck_one.effekt", - examplesDir / "benchmarks" / "input_output" / "number_matrix.effekt", - examplesDir / "benchmarks" / "input_output" / "large_file.effekt", - examplesDir / "benchmarks" / "input_output" / "small_files.effekt", - examplesDir / "benchmarks" / "input_output" / "interleave_promises.effekt", - examplesDir / "benchmarks" / "input_output" / "financial_format.effekt", - - // unsafe continuations are not yet supported in our Chez backend - examplesDir / "pos" / "unsafe_cont.effekt", - examplesDir / "pos" / "propagators.effekt", - - // the number representations differ in JS and Chez - examplesDir / "casestudies" / "ad.effekt.md", - examplesDir / "casestudies" / "inference.effekt.md", - - // in the CallCC variant, we cannot have toplevel vals at the moment (their bindings need to be wrapped in `(run (thunk ...))` - // see comment on commit 61492d9 - examplesDir / "casestudies" / "anf.effekt.md", - - // we do not need to run the negative tests for the other backends - examplesDir / "neg", - - examplesDir / "pos" / "infer", - - examplesDir / "pos" / "lambdas", - - examplesDir / "pos" / "multiline_extern_definition.effekt", // the test is specific to JS - - examplesDir / "pos" / "io", // async io is only implemented for monadic JS - - - examplesDir / "pos" / "issue429.effekt", - - // Generic comparison - examplesDir / "pos" / "genericcompare.effekt", - examplesDir / "pos" / "issue733.effekt", - ) -} - -class ChezSchemeMonadicTests extends ChezSchemeTests { - def backendName = "chez-monadic" -} - -class ChezSchemeCallCCTests extends ChezSchemeTests { - def backendName = "chez-callcc" -} diff --git a/effekt/jvm/src/test/scala/effekt/ConstraintTests.scala b/effekt/jvm/src/test/scala/effekt/ConstraintTests.scala deleted file mode 100644 index 366508705..000000000 --- a/effekt/jvm/src/test/scala/effekt/ConstraintTests.scala +++ /dev/null @@ -1,281 +0,0 @@ -package effekt -package typer -package constraints - -import effekt.source.NoSource -import effekt.symbols.* -import effekt.util.messages.{ DebugMessaging, ErrorReporter, FatalPhaseError } -import kiama.util.Positions - -import scala.language.implicitConversions - -abstract class ConstraintTests extends munit.FunSuite { - - object messages extends DebugMessaging - - given ErrorReporter with { var focus = NoSource; val messaging = messages; val positions = new Positions } - - lazy val scope = { val s = new Unification; s.enterScope(); s } - - lazy val R = freshTypeVar("R") - lazy val S = freshTypeVar("S") - lazy val T = freshTypeVar("T") - lazy val U = freshTypeVar("U") - - lazy val A = freshCaptVar("A") - lazy val B = freshCaptVar("B") - lazy val C = freshCaptVar("C") - lazy val D = freshCaptVar("D") - - lazy val x = CaptureParam(Name.local("x")) - lazy val y = CaptureParam(Name.local("y")) - lazy val z = CaptureParam(Name.local("z")) - - - def freshTypeVar(name: String) = - scope.freshTypeVar(TypeParam(Name.local(name)), NoSource) - - def freshCaptVar(name: String) = - scope.freshCaptVar(CaptUnificationVar.VariableInstantiation(CaptureParam(Name.local(name)), NoSource)) - - def freshGraph() = { - messages.clear() - new Constraints - } -} - -class TestSimpleFlow extends ConstraintTests { - - test("should propagate captures through lower bounds") { - val graph = freshGraph() - import graph.* - - connect(A, B) - requireLower(Set(x), A) - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set(x))) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - } - - test("should transitively propagate captures through lower bounds") { - val graph = freshGraph() - import graph.* - - connect(A, B) - connect(B, C) - requireLower(Set(x), A) - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set(x))) - assert(C.lower == Some(Set(x))) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - assert(C.upper == None) - } - - test("should propagate captures after connecting") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - connect(A, B) - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set(x))) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - } - - test("should transitively propagate captures through lower bounds after connecting") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - - connect(A, B) - connect(B, C) - - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set(x))) - assert(C.lower == Some(Set(x))) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - assert(C.upper == None) - } -} - -class TestErrors extends ConstraintTests { - test("should report an error when conflicting bounds flow") { - val graph = freshGraph() - import graph.* - - connect(A, B) - requireUpper(Set(y), B) - - intercept[FatalPhaseError] { - requireLower(Set(x), A) - } - } -} - -class TestSubstitutions extends ConstraintTests { - test("should add a substitution for a capture variable, when leaving the scope") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - leave(Nil, List(A)) - - assert(subst.isDefinedAt(A)) - assert(subst.get(A) == Some(CaptureSet(x))) - } - - test("should check consistency with already substituted variables") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - leave(Nil, List(A)) - - connect(A, B) - assert(B.lower == Some(Set(x))) - - requireUpper(Set(y), C) - intercept[FatalPhaseError] { - connect(A, C) - } - } -} - -class TestSubtracting extends ConstraintTests { - test ("should not propagate filtered captures into bounds") { - val graph = freshGraph() - import graph.* - - connect(A, B, Set(x)) - requireLower(Set(x), A) - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set())) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - } - test ("should not propagate filtered existing captures into bounds") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - connect(A, B, Set(x)) - - // should propagate - assert(A.lower == Some(Set(x))) - assert(B.lower == Some(Set())) - - // should not affect upper bounds - assert(A.upper == None) - assert(B.upper == None) - } - test ("should not conflict with solved substitutions") { - val graph = freshGraph() - import graph.* - - requireLower(Set(x), A) - leave(Nil, List(A)) - - connect(A, B, Set(x)) - - // should propagate - assert(B.lower == Some(Set())) - - // should not affect upper bounds - assert(B.upper == None) - } - test ("if bounded from both sides it should still filter, appropriately") { - val graph = freshGraph() - import graph.* - - // before: - // ({} B {*}) <: ({x, y} A {*}) - requireLower(Set(x, y), A) - connect(B, A) - - // after: - // ({x, y} A {*}) [x] <: ({y} B {*}) <: ({x, y} A {*}) - connect(A, B, Set(x)) - - // should propagate - assert(B.lower == Some(Set(y))) - - // should not affect upper bounds - assert(B.upper == None) - } - test ("filtering from above should admit *more* capabilities, not less") { - val graph = freshGraph() - import graph.* - - // before: - // {y} A {*} - // {} B {x} - requireUpper(Set(x), B) - requireLower(Set(y), A) - - // connecting: - // ({y} A {*}) <[y]< ({} B {x}) - connect(A, B, Set(y)) - - // should result in: - // ({y} A {x, y}) <[y]< ({} B {x}) - - // Bounds are unchanged - assert(A.lower == Some(Set(y))) - assert(A.upper == Some(Set(x, y))) - assert(B.lower == Some(Set())) - assert(B.upper == Some(Set(x))) - } - test ("filtering from above transitively should admit *more* capabilities, not less") { - val graph = freshGraph() - import graph.* - - // before: - // {y,z} A {*} - // {} B {*} - // {} C {x} - requireUpper(Set(x), C) - requireLower(Set(y, z), A) - - connect(A, B, Set(y)) - connect(B, C, Set(z)) - - // lower bound of A stays unchanged - assert(A.lower == Some(Set(y, z))) - // z flows from A to B (but y is filtered) - assert(B.lower == Some(Set(z))) - // everything is filtered before arriving at C - assert(C.lower == Some(Set())) - - // upper bound of C is unchanged - assert(C.upper == Some(Set(x))) - // upper bound x on C flows to B, but also adds z - assert(B.upper == Some(Set(x, z))) - // all three flow as upper bound to A - assert(A.upper == Some(Set(x, y, z))) - } -} diff --git a/effekt/jvm/src/test/scala/effekt/EffektTests.scala b/effekt/jvm/src/test/scala/effekt/EffektTests.scala deleted file mode 100644 index aaaa014d8..000000000 --- a/effekt/jvm/src/test/scala/effekt/EffektTests.scala +++ /dev/null @@ -1,195 +0,0 @@ -package effekt - -import effekt.util.PlainMessaging -import effekt.util.messages.EffektError -import kiama.util.Severities -import kiama.util.Severities.Severity - -import java.io.File -import sbt.io.* -import sbt.io.syntax.* - -import scala.sys.process.* -import scala.language.implicitConversions - -trait EffektTests extends munit.FunSuite { - - // The name of the backend as it is passed to the --backend flag. - def backendName: String - - // Whether to execute using valgrind - def valgrind = false - - // Whether to execute using debug mode - def debug = false - - def output: File = new File(".") / "out" / "tests" / getClass.getName.toLowerCase - - // The sources of all testfiles are stored here: - def examplesDir = new File("examples") - - // Test files which are to be ignored (since features are missing or known bugs exist) - def ignored: List[File] = List() - - // Folders to discover and run tests in - def positives: List[File] = List() - - def negatives: List[File] = List() - - // Test files that should be run with optimizations disabled - def withoutOptimizations: List[File] = List() - - def runTestFor(input: File, expected: String): Unit = - test(input.getPath + s" (${backendName})") { - assertNoDiff(run(input, true), expected) - } - - // one shared driver for all tests in this test runner - object driver extends effekt.Driver - - lazy val state: driver.context.State = warmup(file("empty.effekt")) - - def warmup(input: File): driver.context.State = - val compiler = driver - val configs = compiler.createConfig(Seq( - "--Koutput", "string", - "--compile", - "--no-exit-on-error", - "--backend", backendName, - "--out", output.getPath - )) - configs.verify() - compiler.compileFile(input.getPath, configs) - compiler.context.backup - - def run(input: File, optimizations: Boolean): String = - val compiler = driver - var options = Seq( - "--Koutput", "string", - "--backend", backendName, - "--out", output.getPath, - ) - if (valgrind) options = options :+ "--valgrind" - if (debug) options = options :+ "--debug" - if (!optimizations) options = options :+ "--no-optimize" - val configs = compiler.createConfig(options) - configs.verify() - - // reuse state after compiling a trivial file - driver.context.restore(state) - compiler.compileFile(input.getPath, configs) - configs.stringEmitter.result() - - def compile(input: File): List[EffektError] = - val compiler = new effekt.Driver {} - val configs = compiler.createConfig(Seq( - "--Koutput", "string", - "--compile", - "--no-exit-on-error", - "--backend", backendName, - "--out", output.getPath - )) - configs.verify() - compiler.compileFile(input.getPath, configs) - compiler.messaging.get.toList - - - def runTests() = - Backend.backend(backendName).runner.checkSetup() match { - case Left(msg) => test(s"${this.getClass.getName}: ${msg}".ignore) { () } - case Right(value) => - negatives.foreach(runNegativeTestsIn) - positives.foreach(runPositiveTestsIn) - withoutOptimizations.foreach(runWithoutOptimizations) - } - - def runWithoutOptimizations(dir: File): Unit = - foreachFileIn(dir) { - case (f, None) => sys error s"Missing checkfile for ${f.getPath}" - case (f, Some(expected)) => - test(s"${f.getPath} (${backendName})") { - assertNoDiff(run(f, false), expected) - } - } - - def runPositiveTestsIn(dir: File): Unit = - foreachFileIn(dir) { - case (f, None) => sys error s"Missing checkfile for ${f.getPath}" - case (f, Some(expected)) => - test(s"${f.getPath} (${backendName})") { - assertNoDiff(run(f, true), expected) - } - } - - def runNegativeTestsIn(dir: File): Unit = - foreachFileIn(dir) { - case (f, Some(expected)) => - test(s"${f.getPath} (${backendName})") { - assertNoDiff(run(f, true), expected) - } - - case (f, None) => - test(s"${f.getPath} (${backendName})") { - validateErrors(f, compile(f)) - } - } - - def validateErrors(f: File, messages: List[EffektError]): Unit = - import scala.util.matching.Regex - val messaging = new PlainMessaging - val messageWithFormat = messages.map { msg => (msg, messaging.formatContent(msg)) } - - assert(messages.nonEmpty, s"File ${f} is supposed to report at least one error.") - - val rx = """//\s*(ERROR|WARN)\s*(.*)$""".r - - def findError(severity: Severity, lineNo: Int, contents: String): Unit = - val couldFind = messageWithFormat.exists { - case (msg, formatted) => - def rightSeverity = msg.severity == severity - def rightLine = msg.startPosition.exists(p => p.line == lineNo) - def containsText = formatted.contains(contents.strip) - rightSeverity && rightLine && containsText - } - assert(couldFind, - s"""Could not find an message [${Severities.severityToWord(severity)}] line ${lineNo}: ${contents} - | - |Compilation resulted in the following messages - |---------------------------------------------- - |${ messages.map(m => messaging.formatMessage(m)).distinct.mkString("\n\n") }""".stripMargin) - - // for each line with a comment, check whether a corresponding message exists - IO.readLines(f).zipWithIndex.foreach { - case (line, no) => - rx.findFirstMatchIn(line).foreach { res => - val severity = res.group(1) match { - case "ERROR" => Severities.Error - case "WARN" => Severities.Warning - } - findError(severity, no + 1, res.group(2)) - } - } - - assert(messages.nonEmpty) - - def foreachFileIn(file: File)(test: (File, Option[String]) => Unit): Unit = - file match { - case f if f.isDirectory && !ignored.contains(f) => - f.listFiles.foreach(foreachFileIn(_)(test)) - case f if f.getName.endsWith(".effekt") || f.getName.endsWith(".effekt.md") => - if (!ignored.contains(f)) { - test(f, expectedResultFor(f)) - } - case _ => () - } - - runTests() -} - -def expectedResultFor(f: File): Option[String] = { - val path = f.getParentFile - val baseName = f.getName.stripSuffix(".md").stripSuffix(".effekt") - val checkfile = path / (baseName + ".check") - if checkfile.exists() then Some(IO.read(checkfile)) else None -} - diff --git a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala deleted file mode 100644 index de90df3b4..000000000 --- a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala +++ /dev/null @@ -1,86 +0,0 @@ -package effekt - -import java.io.File - -import sbt.io._ -import sbt.io.syntax._ - -import scala.util.matching._ - -import scala.language.implicitConversions - - -class JavaScriptTests extends EffektTests { - - def backendName = "js" - - override def positives: List[File] = List( - examplesDir / "pos", - examplesDir / "casestudies", - examplesDir / "benchmarks", - ) - - override def negatives: List[File] = List( - examplesDir / "neg" - ) - - override lazy val withoutOptimizations: List[File] = List( - // contifying under reset - examplesDir / "pos" / "issue842.effekt", - examplesDir / "pos" / "issue861.effekt", - - // syntax error (multiple declaration) - examplesDir / "pos" / "parser.effekt", - examplesDir / "pos" / "probabilistic.effekt", - ) - - override def ignored: List[File] = List( - // unsafe cont - examplesDir / "pos" / "propagators.effekt" - ) -} - -object TestUtils { - - object jsTests extends JavaScriptTests - import jsTests.* - - /** - * Generates the check files from the actual outputs. - * - * Call from sbt with: - * > project effektJVM - * > test:console - * scala> effekt.TestUtils.generateCheckFiles() - * - * Check afterwards with: - * git diff - */ - def generateCheckFilesIn(dir: File, regenerateAll: Boolean): Unit = { - dir.listFiles.foreach { - case f if f.isDirectory && !ignored.contains(f) => generateCheckFilesIn(f, regenerateAll) - case f if f.getName.endsWith(".effekt") || f.getName.endsWith(".effekt.md") => - val path = f.getParentFile - val baseName = f.getName.stripSuffix(".md").stripSuffix(".effekt") - val checkfile = path / (baseName + ".check") - - val isIgnored = ignored.contains(f) - val shouldGenerate = regenerateAll || f.lastModified() > checkfile.lastModified() - if (!isIgnored && shouldGenerate) { - println(s"Writing checkfile for ${f}") - val out = run(f, true) - - // Save checkfile in source folder (e.g. examples/) - // We remove ansi colors to make check files human-readable. - IO.write(checkfile, removeAnsiColors(out)) - } - case _ => () - } - } - - def generateCheckFiles(regenerateAll: Boolean = false): Unit = { - generateCheckFilesIn(examplesDir, regenerateAll) - } - - def removeAnsiColors(text: String): String = text.replaceAll("\u001B\\[[;\\d]*m", "") -} diff --git a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala deleted file mode 100644 index 6a1c55b2d..000000000 --- a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala +++ /dev/null @@ -1,95 +0,0 @@ -package effekt - -import java.io.File -import sbt.io._ -import sbt.io.syntax._ -import scala.language.implicitConversions -import scala.sys.process.Process - -class LLVMTests extends EffektTests { - - def backendName = "llvm" - - override def valgrind = sys.env.get("EFFEKT_VALGRIND").nonEmpty - override def debug = sys.env.get("EFFEKT_DEBUG").nonEmpty - - override lazy val positives: List[File] = List( - examplesDir / "llvm", - examplesDir / "pos", - examplesDir / "benchmarks", - ) - - /** - * Documentation of currently failing tests because of missing features - * and their reason - */ - lazy val missingFeatures: List[File] = List( - // Regex - examplesDir / "pos" / "simpleparser.effekt", - - // toplevel def and let bindings - examplesDir / "pos" / "capture" / "mbed.effekt", - - // unsafe cont - examplesDir / "pos" / "propagators.effekt", - - // Only JS (tests should be moved to a JS folder) - examplesDir / "pos" / "genericcompare.effekt", - examplesDir / "pos" / "multiline_extern_definition.effekt", - examplesDir / "pos" / "maps.effekt", - examplesDir / "pos" / "capture" / "resources.effekt", - examplesDir / "pos" / "io", - - // higher order foreign functions are not supported - examplesDir / "pos" / "capture" / "ffi_blocks.effekt", - - // See PR #355 - examplesDir / "llvm" / "string_toint.effekt", - - // Generic equality - examplesDir / "pos" / "issue429.effekt", - - // Generic comparison - examplesDir / "pos" / "issue733.effekt", - ) - - override lazy val withoutOptimizations: List[File] = List( - // contifying under reset - examplesDir / "pos" / "issue842.effekt", - examplesDir / "pos" / "issue861.effekt", - - examplesDir / "pos" / "capture" / "regions.effekt", - examplesDir / "pos" / "capture" / "selfregion.effekt", - examplesDir / "benchmarks" / "other" / "generator.effekt", - examplesDir / "pos" / "bidirectional" / "typeparametric.effekt", - examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt", - examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt", - - // top-level object definition - examplesDir / "pos" / "object" / "if_control_effect.effekt", - examplesDir / "pos" / "lambdas" / "toplevel_objects.effekt", - examplesDir / "pos" / "type_omission_op.effekt", - examplesDir / "pos" / "bidirectional" / "higherorderobject.effekt", - examplesDir / "pos" / "bidirectional" / "res_obj_boxed.effekt", - examplesDir / "pos" / "bidirectional" / "effectfulobject.effekt", - ) - - override lazy val ignored: List[File] = missingFeatures ++ noValgrind(examplesDir) -} - -/** - * Documentation of tests that succeed in running, but fail valgrind - * and their reason - */ -def noValgrind(examplesDir: File): List[File] = List( - examplesDir / "llvm" / "prompt-duplication.effekt", -) - -class LLVMNoValgrindTests extends EffektTests { - def backendName = "llvm" - - override def valgrind = false - override def debug = false - - override lazy val positives: List[File] = noValgrind(examplesDir) -} diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala deleted file mode 100644 index 9e995c452..000000000 --- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala +++ /dev/null @@ -1,921 +0,0 @@ -package effekt - -import com.google.gson.{JsonElement, JsonParser} -import munit.FunSuite -import org.eclipse.lsp4j.{DefinitionParams, Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InitializeParams, InitializeResult, InlayHint, InlayHintKind, InlayHintParams, MarkupContent, MessageActionItem, MessageParams, Position, PublishDiagnosticsParams, Range, ReferenceContext, ReferenceParams, SaveOptions, ServerCapabilities, SetTraceParams, ShowMessageRequestParams, SymbolInformation, SymbolKind, TextDocumentContentChangeEvent, TextDocumentItem, TextDocumentSyncKind, TextDocumentSyncOptions, VersionedTextDocumentIdentifier} -import org.eclipse.lsp4j.jsonrpc.messages - -import java.io.{PipedInputStream, PipedOutputStream} -import java.util -import java.util.concurrent.CompletableFuture -import scala.collection.mutable -import scala.collection.mutable.Queue -import scala.jdk.CollectionConverters.* - -class LSPTests extends FunSuite { - // Import the extension method for String - import TextDocumentSyntax.* - - // Test helpers - // - // - - - /** - * @param compileOnChange The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing - * because we would like to switch to `didChange` events once we have working caching for references. - */ - def withClientAndServer(compileOnChange: Boolean)(testBlock: (MockLanguageClient, Server) => Unit): Unit = { - val driver = new Driver {} - val config = EffektConfig(Seq("--server")) - config.verify() - - val clientIn = new PipedInputStream() - val clientOut = new PipedOutputStream() - val serverIn = new PipedInputStream(clientOut) - val serverOut = new PipedOutputStream(clientIn) - - val server = new Server(config, compileOnChange) - - val mockClient = new MockLanguageClient() - server.connect(mockClient) - - val launcher = server.launch(mockClient, serverIn, serverOut) - - testBlock(mockClient, server) - } - - def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = { - withClientAndServer(true)(testBlock) - } - - // Fixtures - // - // - - val helloWorld = raw""" - |def main() = { println("Hello, world!") } - |""".textDocument - - val helloEffekt = raw""" - |def main() = { println("Hello, Effekt!") } - |""" - - // LSP: lifecycle events - // - // - - test("Initialization works") { - withClientAndServer { (client, server) => - val initializeResult = server.initialize(new InitializeParams()).get() - val expectedCapabilities = new ServerCapabilities() - expectedCapabilities.setHoverProvider(true) - expectedCapabilities.setDefinitionProvider(true) - expectedCapabilities.setReferencesProvider(true) - expectedCapabilities.setDocumentSymbolProvider(true) - expectedCapabilities.setCodeActionProvider(true) - expectedCapabilities.setInlayHintProvider(true) - - val saveOptions = new SaveOptions() - saveOptions.setIncludeText(true) - - val syncOptions = new TextDocumentSyncOptions(); - syncOptions.setOpenClose(true); - syncOptions.setChange(TextDocumentSyncKind.Full); - syncOptions.setSave(saveOptions); - expectedCapabilities.setTextDocumentSync(syncOptions); - - assertEquals(initializeResult, new InitializeResult(expectedCapabilities)) - } - } - - test("didOpen yields empty diagnostics") { - withClientAndServer { (client, server) => - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - - val diagnostics = client.receivedDiagnostics() - assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(helloWorld.getUri, new util.ArrayList()))) - } - } - - test("setTrace is implemented") { - withClientAndServer { (client, server) => - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = SetTraceParams("off") - server.setTrace(params) - } - } - - // LSP: Changes to text documents - // - // - - test("didOpen yields error diagnostics") { - withClientAndServer { (client, server) => - val (textDoc, range) = raw""" - |val x: Int = "String" - | ↑ ↑ - |""".textDocumentAndRange - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val diagnostic = new Diagnostic() - diagnostic.setRange(range) - diagnostic.setSeverity(DiagnosticSeverity.Error) - diagnostic.setSource("effekt") - diagnostic.setMessage("Expected Int but got String.") - - val diagnosticsWithError = new util.ArrayList[Diagnostic]() - diagnosticsWithError.add(diagnostic) - - val expected = List( - new PublishDiagnosticsParams("file://test.effekt", new util.ArrayList[Diagnostic]()), - new PublishDiagnosticsParams("file://test.effekt", diagnosticsWithError) - ) - - val diagnostics = client.receivedDiagnostics() - assertEquals(diagnostics, expected) - } - } - - test("didChange yields empty diagnostics") { - withClientAndServer { (client, server) => - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - // Pop the diagnostics from the queue before changing the document - val _ = client.receivedDiagnostics() - - val (textDoc, changeEvent) = helloWorld.changeTo(helloEffekt) - - val didChangeParams = new DidChangeTextDocumentParams() - didChangeParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) - didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) - server.getTextDocumentService().didChange(didChangeParams) - - val diagnostics = client.receivedDiagnostics() - assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) - } - } - - test("didSave yields empty diagnostics") { - withClientAndServer { (client, server) => - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - // Pop the diagnostics from the queue before changing the document - val _ = client.receivedDiagnostics() - - val (textDoc, changeEvent) = helloWorld.changeTo(helloEffekt) - - val didSaveParams = new DidSaveTextDocumentParams() - didSaveParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) - didSaveParams.setText(textDoc.getText) - server.getTextDocumentService().didSave(didSaveParams) - - val diagnostics = client.receivedDiagnostics() - assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) - } - } - - - test("didClose yields empty diagnostics") { - withClientAndServer { (client, server) => - // We use an erroneous example to show that closing the document clears the diagnostics. - val textDoc = raw"""val x: Int = "String"""".textDocument - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - // Pop the diagnostics from the queue before closing the document - val _ = client.receivedDiagnostics() - - val didCloseParams = new DidCloseTextDocumentParams() - didCloseParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) - server.getTextDocumentService().didClose(didCloseParams) - - val diagnostics = client.receivedDiagnostics() - assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) - } - } - - test("didSave doesn't throw a NullPointerException when text is null") { - withClientAndServer { (client, server) => - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - // Clear any initial diagnostics. - val _ = client.receivedDiagnostics() - - val didSaveParams = new DidSaveTextDocumentParams() - didSaveParams.setTextDocument(helloWorld.versionedTextDocumentIdentifier) - // The text is set to null - didSaveParams.setText(null) - - server.getTextDocumentService().didSave(didSaveParams) - } - } - - // LSP: Hovering - // - // - - test("Hovering over symbol shows type information") { - withClientAndServer { (client, server) => - val (textDoc, cursor) = raw""" - |val x: Int = 42 - | ↑ - |""".textDocumentAndPosition - val hoverContents = - raw"""|#### Value binder - |```effekt - |test::x: Int - |``` - |""".stripMargin - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) - val hover = server.getTextDocumentService().hover(hoverParams).get() - - val expectedHover = new Hover() - expectedHover.setRange(new Range(cursor, cursor)) - expectedHover.setContents(new MarkupContent("markdown", hoverContents)) - assertEquals(hover, expectedHover) - } - } - - // FIXME: Hovering over holes does not work at the moment. - // https://github.com/effekt-lang/effekt/issues/549 - test("Hovering over hole shows nothing") { - withClientAndServer { (client, server) => - val (textDoc, cursor) = raw""" - |def foo(x: Int) = <> - | ↑ - |""".textDocumentAndPosition - val hoverContents = "" - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) - val hover = server.getTextDocumentService().hover(hoverParams).get() - - val expectedHover = new Hover() - expectedHover.setRange(new Range(cursor, cursor)) - expectedHover.setContents(new MarkupContent("markdown", hoverContents)) - assertEquals(hover, expectedHover) - } - } - - test("Hovering over mutable binder without extended description") { - withClientAndServer { (client, server) => - val (textDoc, cursor) = raw""" - |def main() = { - | var foo = 1 - | ↑ - | <> - |} - |""".textDocumentAndPosition - val hoverContents = - raw"""#### Mutable variable binder - |```effekt - |foo: Int - |``` - |""".stripMargin - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) - val hover = server.getTextDocumentService().hover(hoverParams).get() - - val expectedHover = new Hover() - expectedHover.setRange(new Range(cursor, cursor)) - expectedHover.setContents(new MarkupContent("markdown", hoverContents)) - assertEquals(hover, expectedHover) - } - } - - test("Hovering over mutable binder with extended description") { - withClientAndServer { (client, server) => - val (textDoc, cursor) = raw""" - |def main() = { - | var foo = 1 - | ↑ - | <> - |} - |""".textDocumentAndPosition - val hoverContents = - raw"""#### Mutable variable binder - |```effekt - |foo: Int - |``` - |Like in other languages, mutable variable binders like `foo` - |can be modified (e.g., `foo = VALUE`) by code that has `foo` - |in its lexical scope. - | - |However, as opposed to other languages, variable binders in Effekt - |are stack allocated and show the right backtracking behavior in - |combination with effect handlers. - |""".stripMargin + " \n" - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val configParams = new DidChangeConfigurationParams() - val settings: JsonElement = JsonParser.parseString("""{"showExplanations": true}""") - configParams.setSettings(settings) - server.getWorkspaceService().didChangeConfiguration(configParams) - - val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) - val hover = server.getTextDocumentService().hover(hoverParams).get() - - val expectedHover = new Hover() - expectedHover.setRange(new Range(cursor, cursor)) - expectedHover.setContents(new MarkupContent("markdown", hoverContents)) - assertEquals(hover, expectedHover) - } - } - - test("Hovering works after editing") { - withClientAndServer { (client, server) => - // Initial code - // - // - - val (textDoc, firstPos) = raw""" - |val x: Int = 42 - | ↑ - |""".textDocumentAndPosition - val hoverContents = - raw"""|#### Value binder - |```effekt - |test::x: Int - |``` - |""".stripMargin - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, firstPos) - val hover = server.getTextDocumentService().hover(hoverParams).get() - - val expectedHover = (pos: Position) => { - val expectedHover = new Hover() - expectedHover.setRange(new Range(pos, pos)) - expectedHover.setContents(new MarkupContent("markdown", hoverContents)) - expectedHover - } - assertEquals(hover, expectedHover(firstPos)) - - // First edit: now we add a blank line in front - // - // - - val (newTextDoc, changeEvent) = textDoc.changeTo( - raw""" - | - |val x: Int = 42 - |""".stripMargin - ) - val secondPos = new Position(firstPos.getLine + 1, firstPos.getCharacter) - - val didChangeParams = new DidChangeTextDocumentParams() - didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) - didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) - server.getTextDocumentService().didChange(didChangeParams) - - val hoverParamsAfterChange = new HoverParams(newTextDoc.versionedTextDocumentIdentifier, secondPos) - val hoverAfterChange = server.getTextDocumentService().hover(hoverParamsAfterChange).get() - - assertEquals(hoverAfterChange, expectedHover(secondPos)) - - // Second edit: we revert the change - // - // - - val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText) - - val didChangeParamsReverted = new DidChangeTextDocumentParams() - didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) - didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent)) - server.getTextDocumentService().didChange(didChangeParamsReverted) - - val hoverParamsAfterRevert = new HoverParams(revertedTextDoc.versionedTextDocumentIdentifier, firstPos) - val hoverAfterRevert = server.getTextDocumentService().hover(hoverParamsAfterRevert).get() - - assertEquals(hoverAfterRevert, expectedHover(firstPos)) - } - } - - // LSP: Document symbols - // - // - - test("documentSymbols returns expected symbols") { - withClientAndServer { (client, server) => - val (textDoc, positions) = - raw""" - |def mySymbol() = <> - |↑ ↑ ↑ ↑ - |""".textDocumentAndPositions - - val expectedSymbols: List[messages.Either[SymbolInformation, DocumentSymbol]] = List( - messages.Either.forRight(new DocumentSymbol( - "mySymbol", - SymbolKind.Method, - new Range(positions(0), positions(3)), - new Range(positions(1), positions(2)), - "Function", - )) - ) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new DocumentSymbolParams() - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - - val documentSymbols = server.getTextDocumentService().documentSymbol(params).get() - // FIXME: The server currently returns spurious symbols at position (0, 0) that we need to filter out. - val filtered = server.getTextDocumentService().documentSymbol(params).get().asScala.filter { - symbol => symbol.getRight.getRange.getStart != new Position(0, 0) && symbol.getRight.getRange.getEnd != new Position(0, 0) - }.asJava - - assertEquals(filtered, expectedSymbols.asJava) - } - } - - // LSP Go to definition - // - // - - test("definition returns expected range") { - withClientAndServer { (client, server) => - val (textDoc, positions) = - raw""" - |def foo() = <> - |↑ ↑ - |def bar() = foo() - | ↑ - """.textDocumentAndPositions - - val expectedRange = new Range(positions(0), positions(1)) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new DefinitionParams() - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - params.setPosition(positions(2)) - - val definition = server.getTextDocumentService().definition(params).get().getLeft.get(0) - assertEquals(definition.getRange, expectedRange) - } - } - - // LSP References - // - // - - // FIXME: the server doesn't actually return the reference to `foo` in `bar` in this example - // It only returns the declaration site. - test("references with setIncludeDeclaration returns declaration site") { - withClientAndServer { (client, server) => - val (textDoc, positions) = - raw""" - |def foo() = <> - | ↑ ↑ - |def bar() = foo() - """.textDocumentAndPositions - - val expectedReferences: List[Range] = List( - new Range(positions(0), positions(1)), - ) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new ReferenceParams() - params.setPosition(positions(0)) - val context = new ReferenceContext() - context.setIncludeDeclaration(true) - params.setContext(context) - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - - val references = server.getTextDocumentService().references(params).get() - assertEquals(references.asScala.map(_.getRange).toList, expectedReferences) - } - } - - // LSP: Inlay hints - // - // - - test("inlayHints should show the io effect") { - withClientAndServer { (client, server) => - val (textDoc, positions) = raw""" - |↑ - |def main() = { - |↑ - | println("Hello, world!") - |} - |↑ - |""".textDocumentAndPositions - - val inlayHint = new InlayHint() - inlayHint.setKind(InlayHintKind.Type) - inlayHint.setPosition(positions(1)) - inlayHint.setLabel("{io}") - val markup = new MarkupContent() - markup.setKind("markdown") - markup.setValue("captures: `{io}`") - inlayHint.setTooltip(markup) - inlayHint.setPaddingRight(true) - inlayHint.setData("capture") - - val expectedInlayHints = List(inlayHint) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new InlayHintParams() - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - params.setRange(new Range(positions(0), positions(2))) - - val inlayHints = server.getTextDocumentService().inlayHint(params).get() - assertEquals(inlayHints, expectedInlayHints.asJava) - } - } - - test("inlayHints work after editing") { - withClientAndServer { (client, server) => - val (textDoc, positions) = - raw""" - |↑ - |def main() = { - |↑ - | println("Hello, world!") - |} - |↑ - |""".textDocumentAndPositions - - val inlayHint = new InlayHint() - inlayHint.setKind(InlayHintKind.Type) - inlayHint.setPosition(positions(1)) - inlayHint.setLabel("{io}") - val markup = new MarkupContent() - markup.setKind("markdown") - markup.setValue("captures: `{io}`") - inlayHint.setTooltip(markup) - inlayHint.setPaddingRight(true) - inlayHint.setData("capture") - - val expectedInlayHints = List(inlayHint) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new InlayHintParams() - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - params.setRange(new Range(positions(0), positions(2))) - - val inlayHints = server.getTextDocumentService().inlayHint(params).get() - assertEquals(inlayHints, expectedInlayHints.asJava) - - // First edit: now we add a blank line in front - // - // - - val (newTextDoc, changeEvent) = textDoc.changeTo( - raw""" - | - |def main() = { - | println("Hello, world!") - |} - |""".stripMargin - ) - val newPos = new Position(positions(1).getLine + 1, positions(1).getCharacter) - - val didChangeParams = new DidChangeTextDocumentParams() - didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) - didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) - server.getTextDocumentService().didChange(didChangeParams) - - val paramsAfterChange = new InlayHintParams() - paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) - paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter))) - - inlayHint.setPosition(newPos) - val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get() - assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava) - - // Second edit: we revert the change - // - // - - val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText) - inlayHint.setPosition(positions(1)) - - val didChangeParamsReverted = new DidChangeTextDocumentParams() - didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) - didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent)) - server.getTextDocumentService().didChange(didChangeParamsReverted) - - val paramsAfterRevert = new InlayHintParams() - paramsAfterRevert.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) - paramsAfterRevert.setRange(new Range(positions(0), positions(2))) - - val inlayHintsAfterRevert = server.getTextDocumentService().inlayHint(paramsAfterRevert).get() - assertEquals(inlayHintsAfterRevert, expectedInlayHints.asJava) - } - - } - - test("inlayHints work after invalid edits") { - withClientAndServer(false) { (client, server) => - val (textDoc, positions) = - raw""" - |↑ - |def main() = { - |↑ - | println("Hello, world!") - |} - |↑ - |""".textDocumentAndPositions - - val inlayHint = new InlayHint() - inlayHint.setKind(InlayHintKind.Type) - inlayHint.setPosition(positions(1)) - inlayHint.setLabel("{io}") - val markup = new MarkupContent() - markup.setKind("markdown") - markup.setValue("captures: `{io}`") - inlayHint.setTooltip(markup) - inlayHint.setPaddingRight(true) - inlayHint.setData("capture") - - val expectedInlayHints = List(inlayHint) - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(textDoc) - server.getTextDocumentService().didOpen(didOpenParams) - - val params = new InlayHintParams() - params.setTextDocument(textDoc.versionedTextDocumentIdentifier) - params.setRange(new Range(positions(0), positions(2))) - - val inlayHints = server.getTextDocumentService().inlayHint(params).get() - assertEquals(inlayHints, expectedInlayHints.asJava) - - // Edit: now we add some invalid syntax to the end - // - // - - val (newTextDoc, changeEvent) = textDoc.changeTo( - raw""" - |def main() = { - | println("Hello, world!") - |} - |invalid syntax - |""".stripMargin - ) - - val didChangeParams = new DidChangeTextDocumentParams() - didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) - didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) - server.getTextDocumentService().didChange(didChangeParams) - - val paramsAfterChange = new InlayHintParams() - paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) - // The client may send a range that is outside of the text the server currently has - // We use somewhat arbitrary values here. - paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter + 5))) - - val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get() - assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava) - } - } - - // Effekt: Publish IR - // - // - - test("When showIR=source, server should provide source AST") { - withClientAndServer { (client, server) => - val source = - raw""" - |def main() = { println("Hello, world!") } - |""" - val textDoc = new TextDocumentItem("file://path/to/test.effekt", "effekt", 0, source.stripMargin) - val initializeParams = new InitializeParams() - val initializationOptions = """{"showIR": "source"}""" - initializeParams.setInitializationOptions(JsonParser.parseString(initializationOptions)) - server.initialize(initializeParams).get() - - val didOpenParams = new DidOpenTextDocumentParams() - didOpenParams.setTextDocument(helloWorld) - server.getTextDocumentService().didOpen(didOpenParams) - - val expectedIRContents = - raw"""ModuleDecl( - | test, - | Nil, - | List( - | FunDef( - | IdDef(main), - | Nil, - | Nil, - | Nil, - | None(), - | BlockStmt( - | Return( - | Call( - | IdTarget(IdRef(Nil, println)), - | Nil, - | List(Literal(Hello, world!, ValueTypeApp(String_whatever, Nil))), - | Nil - | ) - | ) - | ) - | ) - | ) - |)""".stripMargin - - val receivedIRContent = client.receivedIR() - assertEquals(receivedIRContent.length, 1) - val fixedReceivedIR = receivedIRContent.head.content.replaceAll("String_\\d+", "String_whatever") - assertEquals(fixedReceivedIR, expectedIRContents) - } - } - - // Text document DSL - // - // - - test("Correct cursor position") { - val (textDoc, cursor) = raw""" - |def main() = { println("Hello, world!") } - | ↑ - |""".textDocumentAndPosition - - assertEquals(cursor, new org.eclipse.lsp4j.Position(1, 4)) - } - - test("Missing cursor throws exception") { - intercept[IllegalArgumentException] { - raw""" - |def main() = { println("Hello, world!") } - |""".textDocumentAndPosition - } - } - - test("Correct multiline range") { - val (textDoc, range) = raw""" - | There is some content here. - | ↑ - | And here. - | ↑ - |""".textDocumentAndRange - - val textWithoutRanges = raw""" - | There is some content here. - | And here.""".stripMargin - - assertEquals(range.getStart, new org.eclipse.lsp4j.Position(1, 5)) - assertEquals(range.getEnd, new org.eclipse.lsp4j.Position(2, 6)) - assertEquals(textDoc.getText, textWithoutRanges) - } -} - -class MockLanguageClient extends EffektLanguageClient { - private val diagnosticQueue: mutable.Queue[PublishDiagnosticsParams] = mutable.Queue.empty - private val publishIRQueue: mutable.Queue[EffektPublishIRParams] = mutable.Queue.empty - - /** - * Pops all diagnostics received since the last call to this method. - */ - def receivedDiagnostics(): Seq[PublishDiagnosticsParams] = { - val diagnostics = diagnosticQueue.toSeq - diagnosticQueue.clear() - diagnostics - } - - /** - * Pops all publishIR events received since the last call to this method. - */ - def receivedIR(): Seq[EffektPublishIRParams] = { - val irs = publishIRQueue.toSeq - publishIRQueue.clear() - irs - } - - override def telemetryEvent(`object`: Any): Unit = { - // Not implemented for testing. - } - - override def publishDiagnostics(diagnostics: PublishDiagnosticsParams): Unit = { - diagnosticQueue.enqueue(diagnostics) - } - - override def showMessage(messageParams: MessageParams): Unit = { - // Not implemented for testing. - } - - override def showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture[MessageActionItem] = { - // Not implemented for testing. - CompletableFuture.completedFuture(null) - } - - override def logMessage(message: MessageParams): Unit = { - // Not implemented for testing. - } - - override def publishIR(params: EffektPublishIRParams): Unit = { - publishIRQueue.enqueue(params) - } -} - -// DSL for creating text documents using extension methods for String -object TextDocumentSyntax { - implicit class StringOps(val content: String) extends AnyVal { - def textDocument(version: Int): TextDocumentItem = - new TextDocumentItem("file://test.effekt", "effekt", version, content.stripMargin) - - def textDocument: TextDocumentItem = - new TextDocumentItem("file://test.effekt", "effekt", 0, content.stripMargin) - - def textDocumentAndPosition: (TextDocumentItem, Position) = { - val (textDocument, positions) = content.textDocumentAndPositions - - if (positions.length != 1) - throw new IllegalArgumentException("Exactly one marker line (with '" + "↑" + "') is required.") - - (textDocument, positions.head) - } - - def textDocumentAndRange: (TextDocumentItem, Range) = { - val (textDocument, positions) = content.textDocumentAndPositions - if (positions.length != 2) - throw new IllegalArgumentException("Exactly two marker lines (with '" + "↑" + "') are required.") - val start = positions(0) - val end = positions(1) - // The end of the range is exclusive, so we need to increment the character position. - val range = new Range(start, new Position(end.getLine, end.getCharacter + 1)) - (textDocument, range) - } - - def textDocumentAndPositions: (TextDocumentItem, Seq[Position]) = { - val lines = content.stripMargin.split("\n").toBuffer - val positions = scala.collection.mutable.ArrayBuffer[Position]() - var lineIdx = 0 - while (lineIdx < lines.length) { - val line = lines(lineIdx) - if (line.contains("↑")) { - if (lineIdx == 0) - throw new IllegalArgumentException("Marker on first line cannot refer to a previous line.") - // There may be multiple markers on the same line, so we need to record all of them. - for (i <- line.indices if line(i) == '↑') { - positions += new Position(lineIdx - 1, i) - } - lines.remove(lineIdx) - // adjust index because of removal - lineIdx -= 1 - } - lineIdx += 1 - } - val newContent = lines.mkString("\n") - (newContent.textDocument, positions.toList) - } - } - - implicit class TextDocumentOps(val textDocument: TextDocumentItem) extends AnyVal { - def changeTo(newContent: String): (TextDocumentItem, TextDocumentContentChangeEvent) = { - val newDoc = new TextDocumentItem(textDocument.getUri, textDocument.getLanguageId, textDocument.getVersion + 1, newContent.stripMargin) - val changeEvent = new TextDocumentContentChangeEvent(newDoc.getText) - (newDoc, changeEvent) - } - - def versionedTextDocumentIdentifier: VersionedTextDocumentIdentifier = - new VersionedTextDocumentIdentifier(textDocument.getUri, textDocument.getVersion) - } -} diff --git a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala deleted file mode 100644 index 44731886a..000000000 --- a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala +++ /dev/null @@ -1,653 +0,0 @@ -package effekt - -import effekt.source.* - -import effekt.lexer.{ Lexer, Token, TokenKind } -import effekt.lexer.TokenKind.* - -import kiama.util.{ Positions, StringSource } - -import munit.Location - -class RecursiveDescentTests extends munit.FunSuite { - - def parser(input: String, positions: Positions)(using munit.Location): RecursiveDescent = { - val source = StringSource(input, "") - val lexer = effekt.lexer.Lexer(source) - val tokens = lexer.run() - // TODO catch LexerError exception? - new RecursiveDescent(positions, tokens, source) - } - - def parse[R](input: String, f: RecursiveDescent => R, positions: Positions = new Positions())(using munit.Location): R = - try { - val p = parser(input, positions) - val result = f(p) - assert(p.peek(TokenKind.EOF), s"Did not consume everything: ${p.peek}") - result - } catch { - case Fail(msg, pos) => - fail(s"Unexpected parse error (token index ${pos}): ${msg}") - } - - def parseExpr(input: String, positions: Positions = new Positions())(using munit.Location): Term = - parse(input, _.expr()) - - def parseStmt(input: String, positions: Positions = new Positions())(using munit.Location): Stmt = - parse(input, _.stmt()) - - def parseStmts(input: String, positions: Positions = new Positions())(using munit.Location): Stmt = - parse(input, _.stmts()) - - def parseMatchPattern(input: String, positions: Positions = new Positions())(using munit.Location): MatchPattern = - parse(input, _.matchPattern()) - - def parseMatchClause(input: String, positions: Positions = new Positions())(using munit.Location): MatchClause = - parse(input, _.matchClause()) - - def parseValueType(input: String, positions: Positions = new Positions())(using munit.Location): ValueType = - parse(input, _.valueType()) - - def parseBlockType(input: String, positions: Positions = new Positions())(using munit.Location): BlockType = - parse(input, _.blockType()) - - def parseOpClause(input: String, positions: Positions = new Positions())(using munit.Location): OpClause = - parse(input, _.opClause()) - - def parseImplementation(input: String, positions: Positions = new Positions())(using munit.Location): Implementation = - parse(input, _.implementation()) - - def parseTry(input: String, positions: Positions = new Positions())(using munit.Location): Term = - parse(input, _.tryExpr()) - - def parseParams(input: String, positions: Positions = new Positions())(using munit.Location): (List[Id], List[ValueParam], List[BlockParam]) = - parse(input, _.params()) - - def parseLambdaParams(input: String, positions: Positions = new Positions())(using munit.Location): (List[Id], List[ValueParam], List[BlockParam]) = - parse(input, _.lambdaParams()) - - def parseDefinition(input: String, positions: Positions = new Positions())(using munit.Location): Def = - parse(input, _.definition()) - - def parseDefinitions(input: String, positions: Positions = new Positions())(using munit.Location): List[Def] = - parse(input, _.definitions()) - - def parseToplevel(input: String, positions: Positions = new Positions())(using munit.Location): Def = - parse(input, _.toplevel()) - - def parseProgram(input: String, positions: Positions = new Positions())(using munit.Location): ModuleDecl = - parse(input, _.program()) - - def parseExternDef(input: String, positions: Positions = new Positions())(using munit.Location): Def = - parse(input, _.externDef()) - - test("Peeking") { - implicit def toToken(t: TokenKind): Token = Token(0, 0, t) - def peek(tokens: Seq[Token], offset: Int): Token = - new RecursiveDescent(new Positions, tokens, StringSource("", "test")).peek(offset) - - val tokens = List[Token](`(`, Space, Newline, `)`, Space, `=>`, EOF) - assertEquals(peek(tokens, 0).kind, `(`) - assertEquals(peek(tokens, 1).kind, `)`) - assertEquals(peek(tokens, 2).kind, `=>`) - assertEquals(peek(tokens, 3).kind, EOF) - } - - test("Simple expressions") { - parseExpr("42") - parseExpr("f") - parseExpr("f(a)") - parseExpr("f(a, 42)") - parseExpr("\\u0000") - - parseExpr("l.foreach { _ => 42 }") - - assertEquals( - parseExpr("loop { f }"), - Call(IdTarget(IdRef(Nil, "loop")), Nil, Nil, List(Var(IdRef(Nil, "f"))))) - - assertNotEquals( - parseExpr("f.m(a, 42)"), - parseExpr("(f.m)(a, 42)")) - - assertEquals( - parseExpr("f(a, 42)()"), - parseExpr("(f(a, 42))()")) - - assertEquals( - parseExpr("foo.bar"), - // At the moment uniform function call syntax is always a method call - MethodCall(Var(IdRef(Nil, "foo")), IdRef(Nil, "bar"),Nil, Nil, Nil)) - - parseExpr("resume(42)") - parseExpr("in(42)") - - parseExpr("fun() { foo(()) }") - - parseExpr("10.seconds") - - parseExpr("[1,2,3]") - parseExpr("[3,2,1,]") - parseExpr("[]") - parseExpr("[,]") - intercept[Throwable] { parseExpr("[,1]") } - } - - test("Boxing") { - parseExpr("box f") - parseExpr("unbox f") - assertEquals( - parseExpr("unbox box f"), - parseExpr("unbox (box f)") - ) - assertNotEquals( - parseExpr("box { 42 }"), - parseExpr("box {} { 42 }") - ) - parseExpr("box { (x: Int) => x }") - parseExpr("box new Fresh { def fresh() = \"42\" }") - parseExpr("box foo()") - parseExpr("box bar(1)") - parseExpr("box baz(quux)") - - // { f } is parsed as a capture set and not backtracked. - intercept[Throwable] { parseExpr("box { f }") } - } - - test("Pattern matching") { - parseExpr( - """do raise(RuntimeError(), msg) match {} - |""".stripMargin) - - parseExpr( - """x match { - | case 0 => 42 - | case _ => <{ "Test" }> - |} - |""".stripMargin) - - parseExpr( - """x match { - | case 0 => { 42 } - | case 1 => { 1; 2 } - | case _ => 42 - |} - |""".stripMargin) - - parseExpr( - """x match { - | case Some(b) => acc = Cons(b, acc) - |} - |""".stripMargin) - } - - test("Qualified names") { - assertEquals(parseExpr("map"), Var(IdRef(List(), "map"))) - assertEquals(parseExpr("list::map"), Var(IdRef(List("list"), "map"))) - assertEquals(parseExpr("list::internal::test"), Var(IdRef(List("list", "internal"), "test"))) - } - - test("Operator precedence") { - parseExpr("1 + 1") - - assertEquals( - parseExpr("1 + 2 + 3"), - parseExpr("(1 + 2) + 3")) - - assertEquals( - parseExpr("1 + 2 * 3"), - parseExpr("1 + (2 * 3)")) - - assertEquals( - parseExpr("1 + 2 * 3 == 4 + 5"), - parseExpr("(1 + (2 * 3)) == (4 + 5)")) - - parseExpr("i = i + 1") - - parseExpr("compare(x, y) && go(next)") - - parseExpr("foo || bar") - } - - test("Dangling else") { - assertEquals( - parseExpr("if (42) if (42) x else y"), - parseExpr("if (42) (if (42) x else y) else ()")) - } - - test("Simple statements") { - parseStmt("42") - parseStmt("return 42") - parseStmt("{ f(); return 43 }") - parseStmt("{ f(); 43 }") - } - - test("Compound statements") { - parseStmts( - """ val x = { 42; 43 }; - | val y = f(x); - | y - |""".stripMargin) - - parseStmts( - """with foo().baz; - |bar() - |""".stripMargin) - - parseStmts( - """var x = baz; - |return x - |""".stripMargin) - - parseStmts( - """var x in r = baz; - |return x - |""".stripMargin) - - // trailing semicolon - parseStmts( - """return x; - |""".stripMargin) - - parseStmts("fun() { x = x + 1; x }") - } - - test("Definition statements") { - parseStmts( - """val x = 42 - |type T = Int - |() - |""".stripMargin) - - parseStmts("val (left, right) = list; return left") - } - - test("Semicolon insertion") { - parseStmts("f(); return x") - parseStmts( - """var x = { baz } - |return x - |""".stripMargin) - - assertEquals( - parseStmts( - """f() - |g() - |""".stripMargin), - parseStmts( - """f(); - |return g() - |""".stripMargin)) - - parseStmts( - """ val next = f() // Comment - | g() - |""".stripMargin) - - assertEquals( - parseStmts( - """f() - | - |() - |""".stripMargin), - parseStmts( - """f(); - | - |() - |""".stripMargin)) - } - - test("Simple patterns") { - parseMatchPattern("x") - parseMatchPattern("Cons(x, y)") - assertEquals( - parseMatchPattern("_"), - IgnorePattern()) - parseMatchPattern("Cons(x, Cons(x, Nil()))") - - assertEquals( - parseMatchPattern("(left, Cons(x, right))"), - TagPattern(IdRef(List("effekt"), "Tuple2"), - List(AnyPattern(IdDef("left")), - TagPattern(IdRef(List(), "Cons"), List(AnyPattern(IdDef("x")), AnyPattern(IdDef("right"))))))) - } - - test("Block arguments") { - parseExpr("map {f}") - parseExpr("map {list::f}") - parseExpr("map {f} {g}") - parseExpr("map { f } { g }") - parseExpr("map(x) { f } { g }") - parseExpr("map(x) { return 42 }") - parseExpr("map(x) { map(x) { return 42 } }") - } - - test("Value types") { - assertEquals( - parseValueType("Int"), - ValueTypeRef(IdRef(Nil, "Int"), Nil)) - - parseValueType("List[Int]") - parseValueType("list::List[Int]") - parseValueType("list::List[effekt::Int]") - } - - test("Block types") { - parseBlockType("Exc") - parseBlockType("State[S]") - parseBlockType("State[Int]") - parseBlockType("() => Int") - parseBlockType("(Int) => Int") - parseBlockType("Int => Int") - - assertEquals( - parseBlockType("(Int, String) => Int"), - FunctionType(Nil, List(ValueTypeRef(IdRef(Nil,"Int"), Nil), - ValueTypeRef(IdRef(Nil,"String"), Nil)), Nil, ValueTypeRef(IdRef(Nil, "Int"), Nil), Effects(Nil))) - - parseBlockType("(Int, String) => Int / Exc") - parseBlockType("[T](Int, String) => Int / { Exc, State[T] }") - parseBlockType("=> Int") - parseBlockType("{Exc} => Int") - parseBlockType("{Exc} {Amb} => Int") - parseBlockType("({Exc} {Amb} => Int)") - parseBlockType("{Exc} {amb : Amb} => Int") - parseBlockType("{exc:Exc} => Int") - parseBlockType("[T] => T") // Not sure we want this... - - parseValueType("Exc at { a, b, c }") - parseValueType("() => (Exc at {}) / {} at { a, b, c }") - - assertEquals( - parseValueType("() => Int at { a, b, c }"), - parseValueType("(() => Int) at { a, b, c }")) - - // we purposefully cannot parse this: - intercept[Throwable] { parseValueType("() => Int at { a, b, c } at {}") } - - parseValueType("() => (Int at { a, b, c }) at {}") - parseValueType("(() => Int) at { a, b, c }") - parseValueType("(() => Int at {}) => Int at { a, b, c }") - } - - test("Params") { - parseParams("()") - intercept[Throwable] { parseParams("(x, y)") } - parseParams("[A, B](x: A, y: B)") - intercept[Throwable] { parseParams("[]") } - // is this desirable? - parseParams("[A]") - - - parseLambdaParams("a") - parseLambdaParams("(a)") - parseLambdaParams("(a: Int)") - parseLambdaParams("[T](a: Int)") - parseLambdaParams("[T](a)") - parseLambdaParams("[T]") - parseLambdaParams("{f: Exc}") - parseLambdaParams("[T]{f: Exc}") - } - - test("Match clauses") { - parseMatchClause("case x => 42") - parseMatchClause("case Foo(x, y) => 42") - parseMatchClause("case Foo(x, y) and x == 43 => 42") - parseMatchClause("case Foo(Bar(42, true), _) and x == 43 => 42") - parseMatchClause("case _ => 42") - parseMatchClause("case a and a is Foo(bar) => 42") - parseMatchClause("case outer::inner::Constructor(x) => x + 1") - parseMatchClause("case () => 2") - } - - test("Op clauses") { - parseOpClause("def foo() = 42") - parseOpClause("def foo[T]() = 42") - parseOpClause("def foo[T](a) = 42") - parseOpClause("def foo[T](a: Int) = 42") - parseOpClause("def foo[T](a: Int, b) = 42") - // TODO rebase! - // parseOpClause("def foo[T](){f} = 42") - } - - test("Implementations") { - assertEquals( - parseImplementation("Foo {}"), - Implementation(BlockTypeRef(IdRef(Nil, "Foo"), Nil), Nil)) - - parseImplementation("Foo[T] {}") - parseImplementation("Foo[T] { def bar() = 42 }") - parseImplementation( - """Foo[T] { - | def bar() = 42 - | def baz() = 42 - |}""".stripMargin) - - parseImplementation("Foo[T] { case Get(x) => 43 }") - - // Doesn't work yet - parseImplementation("Foo { x => 43 }".stripMargin) - - assertEquals( - parseImplementation("Foo { 43 }"), - Implementation( - BlockTypeRef(IdRef(Nil, "Foo"), Nil), - List(OpClause(IdRef(Nil, "Foo"), Nil, Nil, Nil, None, - Return(Literal(43, symbols.builtins.TInt)), IdDef("resume"))))) - } - - test("Try expressions") { - parseTry("try 42 with Eff { 42 }") - parseTry("try { 42 } with Eff { 42 }") - parseTry("try { 42 } with Empty {}") - parseTry("try { 42 } with Eff { def op(x) = x + 42 }") - parseTry( - """try { - | val x = 42 - | do op(x + 1) - |} with Eff[A] { - | def op(x: A) = x - |} - """.stripMargin - ) - parseTry( - """try { do op(42) } - with Eff1 {} - with Eff2 { case Get(x) => x + 1 } - with Eff3 { def op(x, y) = { x } def op2() = { () }} - """ - ) - } - - test("Type definition") { - parseDefinition("type A = Int") - parseDefinition("type A[X] = Int") - parseDefinition("type A[X] { Foo() }") - parseDefinition( - """type A[X] { - | Foo(); - | Bar() - |} - |""".stripMargin) - - parseDefinition( - """type DATATYPE[X] { - | Foo() - | Bar() - |} - |""".stripMargin) - } - - test("Namespaces") { - parseDefinitions( - """val x = 4 - |val y = 5 - |""".stripMargin) - - val nested = parseDefinitions( - """namespace list { - | val x = 4 - | val y = 5 - |} - |""".stripMargin) - - val semi = parseDefinitions( - """namespace list; - |val x = 4 - |val y = 5 - |""".stripMargin) - - assertEquals(nested, semi) - - val nested2 = parseDefinitions( - """namespace list { - | namespace internal { - | - | val x = 4 - | val y = 5 - | } - |} - |""".stripMargin) - - val semi2 = parseDefinitions( - """namespace list; - |namespace internal; - | - |val x = 4 - |val y = 5 - |""".stripMargin) - - val semiInsertion = parseDefinitions( - """namespace list - |namespace internal - | - |val x = 4 - |val y = 5 - |""".stripMargin) - - assertEquals(nested2, semi2) - assertEquals(nested2, semiInsertion) - - parseDefinitions( - """val x = { - | namespace foo; - | val y = 4; - | foo::y - |} - |""".stripMargin) - } - - test("Function definitions") { - assertEquals( - parseDefinition( - """def foo = f - |""".stripMargin), - DefDef(IdDef("foo"), None, Var(IdRef(Nil, "f")))) - - parseDefinition( - """def foo: Exc = f - |""".stripMargin) - - parseDefinition( - """def foo() = e - |""".stripMargin) - - parseDefinition( - """def foo[T] = e - |""".stripMargin) - - parseDefinition( - """def foo[T](x: Int) = e - |""".stripMargin) - - parseDefinition( - """def foo[T](x: Int): String / {} = e - |""".stripMargin) - } - - test("Toplevel definitions") { - parseToplevel("def foo() = ()") - parseToplevel("effect Foo = {}") - parseToplevel("effect Foo(): Int") - parseToplevel("interface Foo {}") - - parseToplevel( - """interface State { - | def get(): Int - | def set(n: Int): Unit - |} - |""".stripMargin) - - parseToplevel( - """interface State[T] { - | def get(): T - | def set(n: T): Unit - |} - |""".stripMargin) - - parseToplevel( - """extern include "foo/text.txt" - |""".stripMargin) - - parseToplevel("extern type Foo[S]") - parseToplevel("extern resource foo: Foo") - - parseToplevel( - """extern "function() { console.log('hey!'); }" - |""".stripMargin) - - parseToplevel( - """extern def foo(): Int = "bar ${test} baz ${bam}" - |""".stripMargin) - } - - test("Programs") { - // this is from examples/pos/builtins - parseProgram( - """module examples/pos/builtins - | - |type Color { Red(); Green(); Blue() } - | - |def main() = { - | println(1); - | println("foo"); - | println(true); - | println(1 == 2); - | inspect(Red()) - |} - |""".stripMargin) - - parseProgram( - """// test comment - |def foo() = 42 - |""".stripMargin) - - parseProgram( - """module trailing_comment - | - |def main() = () - | - |// foo""".stripMargin) - } - - test("Extern definition") { - parseExternDef("extern {io} def read(s: String): Int = default { 42 } js { 1 + 1 } chez { 42 }") - parseExternDef("extern \"console.log(42)\"") - parseExternDef("extern \"\"\"console.log(42)\"\"\"") - parseExternDef("extern type Complex") - parseExternDef("extern type State[A]") - parseExternDef("extern interface State[A]") - parseExternDef("extern resource withFile: [A](String) { () => A } => A") - parseExternDef("extern include \"path/to/file\"") - parseExternDef("extern js \"\"\"console.log(42)\"\"\"") - parseExternDef("extern pure def read(s: String): String = default { s }") - parseExternDef("extern def read(s: String): String = \"${s}\"") - parseProgram( - "extern def println(value: String): Unit =" + - "js \"$effekt.println(${value})\"" + - "chez \"(println_impl ${value})\"" + - "llvm \"\"\"call void @c_io_println_String(%Pos %value); ret %Pos zeroinitializer ; Unit\"\"\"" + "\n" + - "extern js \"\"\" function \"\"\"" - ) - } -} diff --git a/effekt/jvm/src/test/scala/effekt/ReplTests.scala b/effekt/jvm/src/test/scala/effekt/ReplTests.scala deleted file mode 100644 index 0f58b70b1..000000000 --- a/effekt/jvm/src/test/scala/effekt/ReplTests.scala +++ /dev/null @@ -1,100 +0,0 @@ -package effekt - -import kiama.util.StringConsole - -class ReplTests extends munit.FunSuite { - - val inputs = List("1 + 1") - - def runRepl(input: String): String = { - val compiler = new effekt.Driver {} - - val configs = compiler.createConfig(Seq("--Koutput", "string")) - - configs.verify() - - val repl = Repl(compiler) - - repl.processconsole(StringConsole(input), "", configs) - - configs.stringEmitter.result() - } - - test("Evaluating small examples") { - assertNoDiff(runRepl("1 + 1\n"), "2\n") - assertNoDiff(runRepl(":t 1 + 1\n"), "Int\n") - assertNoDiff(runRepl(":t 1 + 1\n1 + 1\n"), "Int\n2\n") - } - - test("Function definitions") { - val in = - """|def f() = 1 - |def g() = 2 - | - |f() + g() - |""".stripMargin - - val out = - """|f: () => Int - |g: () => Int - |3 - |""".stripMargin - - assertNoDiff(runRepl(in), out) - } - - test("Regression (#168)") { - // defining the same function twice lead to a cache inconsistency. - val in = - """|def f() = 1 - |def f() = 1 - | - |f() + f() - |""".stripMargin - - val out = - """|f: () => Int - |f: () => Int - |2 - |""".stripMargin - - assertNoDiff(runRepl(in), out) - } - - test("Regression: toplevel block definitions") { - val in = - """|interface Foo {} - |def f = new Foo {} - |""".stripMargin - - val out = - """|f: Foo - |""".stripMargin - - assertNoDiff(runRepl(in), out) - } - - test("Imports") { - - val expected = - """|Successfully imported examples/pos/builtins - | - |Imported Types - |============== - |type Color { - | def Red(): Color / {} - | def Green(): Color / {} - | def Blue(): Color / {} - |} - | - |Imported Functions - |================== - |def main(): Unit / {} - |def show(c: Color): String / {} - |""".stripMargin - - assertNoDiff(runRepl("import examples/pos/builtins"), expected) - } - - -} diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala deleted file mode 100644 index b053c633f..000000000 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ /dev/null @@ -1,71 +0,0 @@ -package effekt - -import java.io.File -import sbt.io.* -import sbt.io.syntax.* - -import scala.language.implicitConversions - -abstract class StdlibTests extends EffektTests { - override def positives: List[File] = List( - examplesDir / "stdlib", - ) - - override def ignored: List[File] = List() - - override def withoutOptimizations: List[File] = List() -} - -class StdlibJavaScriptTests extends StdlibTests { - def backendName: String = "js" - - override def withoutOptimizations: List[File] = List( - examplesDir / "stdlib" / "acme.effekt", - examplesDir / "stdlib" / "json.effekt", - examplesDir / "stdlib" / "exception" / "combinators.effekt", - examplesDir / "stdlib" / "stream" / "fibonacci.effekt", - examplesDir / "stdlib" / "list" / "flatmap.effekt", - examplesDir / "stdlib" / "list" / "sortBy.effekt", - examplesDir / "stdlib" / "stream" / "zip.effekt", - examplesDir / "stdlib" / "stream" / "characters.effekt", - examplesDir / "stdlib" / "list" / "deleteat.effekt", - examplesDir / "stdlib" / "char" / "ascii_isalphanumeric.effekt", - examplesDir / "stdlib" / "char" / "ascii_iswhitespace.effekt", - ) - - override def ignored: List[File] = List() -} - -abstract class StdlibChezTests extends StdlibTests { - override def ignored: List[File] = List( - // Not implemented yet - examplesDir / "stdlib" / "io", - examplesDir / "stdlib" / "stream" / "characters.effekt", - examplesDir / "stdlib" / "stream" / "fuse_newlines.effekt" - ) -} -class StdlibChezSchemeMonadicTests extends StdlibChezTests { - def backendName: String = "chez-monadic" -} -class StdlibChezSchemeCallCCTests extends StdlibChezTests { - def backendName: String = "chez-callcc" -} -class StdlibLLVMTests extends StdlibTests { - def backendName: String = "llvm" - - override def valgrind = sys.env.get("EFFEKT_VALGRIND").nonEmpty - override def debug = sys.env.get("EFFEKT_DEBUG").nonEmpty - - override def withoutOptimizations: List[File] = List( - examplesDir / "stdlib" / "acme.effekt", - ) - - override def ignored: List[File] = List( - // String comparison using `<`, `<=`, `>`, `>=` is not implemented yet on LLVM - examplesDir / "stdlib" / "string" / "compare.effekt", - - // Wrong codegen for negative types, see #801 - examplesDir / "stdlib" / "json.effekt", - examplesDir / "stdlib" / "buffer.effekt", - ) -} diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index 7d8f89167..7da46dc8f 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -1,8 +1,7 @@ -package effekt.core -import effekt.{symbols, Phase} +package effekt +package core import munit.Location -import kiama.parsing.{NoSuccess, Success} -import effekt.PhaseResult.CoreTransformed +import kiama.parsing.{ NoSuccess, Success } /** Base class for tests of [[core]]-related stuff. * Provides helpers to parse inputs and test for alpha-equivalence(*), @@ -81,8 +80,6 @@ trait CoreTests extends munit.FunSuite { fail(s"Parsing ${nickname} failed\n[${pos.line}:${pos.column}] ${err.message}") } } - - protected given testContext: TestContext = new TestContext } trait CoreTransformationTests extends CoreTests { @@ -97,26 +94,3 @@ trait CoreTransformationTests extends CoreTests { assertAlphaEquivalent(obtained, pExpected, clue, names = names) } } -/** [[CoreTransformationTests]] for the common case of testing a specific [[Phase]]. */ -trait CorePhaseTests[P <: Phase[CoreTransformed, CoreTransformed]](phase: P) extends CoreTransformationTests { - - protected val theSource: kiama.util.Source = new kiama.util.Source { - override def name: String = "(core test)" - override def content: String = - throw NotImplementedError("The original Effekt source is not available in core tests.") - } - protected val theSourceModuleDecl: effekt.source.ModuleDecl = effekt.source.ModuleDecl("(core test)", Nil, Nil) // FIXME sentinel value - - protected val theSourceModule: effekt.symbols.Module = effekt.symbols.Module(theSourceModuleDecl, theSource) - - override def transform(input: ModuleDecl): ModuleDecl = { - testContext.in { - testContext.module = theSourceModule - // (source: Source, tree: ModuleDecl, mod: symbols.Module, core: effekt.core.ModuleDecl) - phase.run(CoreTransformed(theSource, theSourceModuleDecl, theSourceModule, input))(using testContext) match { - case Some(CoreTransformed(source, tree, mod, core)) => core - case None => fail(s"Phase ${phase.phaseName} failed on test input") - } - } - } -} diff --git a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala b/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala deleted file mode 100644 index 521c00940..000000000 --- a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala +++ /dev/null @@ -1,149 +0,0 @@ -package effekt -package core -import effekt.symbols -import kiama.util.StringSource - -import PatternMatchingCompiler.* -import core.Type.{TBoolean, TInt, TUnit} - -class PatternMatchingTests extends CoreTests { - - val SomeC = Id("Some") - val NoneC = Id("None") - val OptionId = Id("Option") - def OptionT(tpe: core.ValueType): core.ValueType = - core.ValueType.Data(OptionId, List(tpe)) - - val trivalPredicate = core.Return(core.Literal(true, TBoolean)) - - def block(name: String, args: ValueType*): core.BlockVar = - core.BlockVar(Id(name), core.BlockType.Function(Nil, Nil, args.toList, Nil, TUnit), Set.empty) - - def jump(label: BlockVar, args: Pure*) = - App(label, Nil, args.toList, Nil) - - def variable(name: String, tpe: ValueType): core.ValueVar = - core.ValueVar(Id(name), tpe) - - - test("Simple normalization with renamings") { - - val x = variable("x", TInt) - val y = variable("y", TInt) - val z = variable("z", TInt) - val f = block("f", TInt) - - // case x is y; y is z => f(z) - val normalized = normalize(Clause( - List( - Condition.Patterns(Map(x -> Pattern.Any(y.id))), - Condition.Patterns(Map(y -> Pattern.Any(z.id)))), - f, Nil, List(z))) - - // case => f(z) - val expected = Clause(Nil, f, Nil, List(x)) - - assertEquals(normalized, expected) - } - - test("Sanity check: compiling empty list of clauses") { - assertEquals(compile(Nil), core.Hole()) - } - - test("Simple guard") { - // sc match { - // case x and x > 0 => "hello" - // case _ => "world" - // } - // ~= - // match { - // case sc is x; val p = return x > 0; p? => b1(v) - // case sc is _ => b2() - // } - - val sc = variable("sc", TInt) - val x = variable("x", TInt) - val p = variable("p", TBoolean) - val b1 = block("b1", TInt) - val b2 = block("b2") - - val result = compile(List( - Clause( - List( - Condition.Patterns(Map(sc -> Pattern.Any(x.id))), - Condition.Val(p.id, TBoolean, trivalPredicate), - Condition.Predicate(p)), - b1, Nil, List(x)), - Clause( - List( - Condition.Patterns(Map(sc -> Pattern.Ignore()))), - b2, Nil, List()))) - - val expected = - Val(p.id, TBoolean, trivalPredicate, - If(p, - jump(b1, sc), - jump(b2))) - - assertAlphaEquivalentStatements(result, expected) - - - //val names = Names(defaultNames ++ Map("sc" -> sc.id, "b1" -> b1.id, "b2" -> b2.id)) - // assertAlphaEquivalentStatements(result, parseStatement( - // """ - // |val p3 = return true; - // |if (p3: Boolean) - // | (b1 : (Int) => Unit @ {})(sc: Int) - // |else - // | (b2 : () => Unit @ {})() - // | - // |""".stripMargin, - // names = names - // ), names) - } - - test("Match then guard") { - - // opt match { - // case Some(v) and v > 0 => b1(v) - // case _ => b2() - // } - // ~= - // match { - // case opt is Some(v); val p = return v > 0; p? => b1(v) - // case opt is _ => b2() - // } - val opt = variable("opt", OptionT(TInt)) - val v = variable("v", TInt) - val p = variable("p", TBoolean) - val b1 = block("b1", TInt) - val b2 = block("b2") - - val tmp = variable("tmp", TInt) - - val result = compile(List( - Clause( - List( - Condition.Patterns(Map(opt -> Pattern.Tag(SomeC, List(), List(Pattern.Any(v.id) -> TInt)))), - Condition.Val(p.id, TBoolean, trivalPredicate), - Condition.Predicate(p)), - b1, Nil, List(v)), - Clause( - List( - Condition.Patterns(Map(opt -> Pattern.Ignore()))), - b2, Nil, List()))) - - // opt match { - // case Some(tmp) => val p = return v > 0; if (p) { b1(tmp) } else { b2() } - // case _ => b2() - // } - val expected = Match(opt, - List((SomeC, BlockLit(Nil, Nil, List(ValueParam(tmp.id, tmp.tpe)), Nil, - Val(p.id, TBoolean, trivalPredicate, If(p, - App(b1, Nil, List(tmp), Nil), - App(b2, Nil, Nil, Nil)))))), - Some(App(b2, Nil, Nil, Nil))) - - assertAlphaEquivalentStatements(result, expected) - } -} diff --git a/effekt/jvm/src/test/scala/effekt/core/PolymorphismBoxingTests.scala b/effekt/jvm/src/test/scala/effekt/core/PolymorphismBoxingTests.scala deleted file mode 100644 index 6894395a6..000000000 --- a/effekt/jvm/src/test/scala/effekt/core/PolymorphismBoxingTests.scala +++ /dev/null @@ -1,193 +0,0 @@ -package effekt -package core - -import effekt.{core, source, symbols} -import effekt.context.Context -import effekt.core.{Block, DirectApp, PolymorphismBoxing, Pure, Stmt} -import effekt.source.{IdDef, Include} -import effekt.util.messages -import effekt.util.messages.DebugMessaging -import kiama.parsing.{Failure, NoSuccess, Success} - -abstract class AbstractPolymorphismBoxingTests extends CorePhaseTests(PolymorphismBoxing) { - - override protected val defaultNames: Map[String, _root_.effekt.symbols.Symbol] = super.defaultNames ++ Map( - "BoxedInt" -> Id("BoxedInt"), - "BoxedString" -> Id("BoxedString"), - "MkBoxedString" -> Id("MkBoxedString"), - "boxInt" -> Id("boxInt"), - "unboxInt" -> Id("unboxInt"), - "unboxString" -> Id("unboxInt"), - ) - val boxDecls = List( - Declaration.Data(defaultNames("BoxedString"), List(), - List(Constructor(defaultNames("MkBoxedString"), - List(Field(defaultNames("unboxString"), ValueType.Data(defaultNames("String"), Nil)))))) - ) - val boxExterns = List( - Extern.Def(defaultNames("boxInt"), Nil, Nil, List(ValueParam(Id("i"), ValueType.Data(defaultNames("Int"), Nil))), Nil, - ValueType.Data(defaultNames("BoxedInt"), Nil), Set.empty, - ExternBody.StringExternBody(source.FeatureFlag.Default, Template(List(""), Nil))), - Extern.Def(defaultNames("unboxInt"), Nil, Nil, List(ValueParam(Id("i"), ValueType.Data(defaultNames("BoxedInt"), Nil))), Nil, - ValueType.Data(defaultNames("Int"), Nil), Set.empty, - ExternBody.StringExternBody(source.FeatureFlag.Default, Template(List(""), Nil))) - ) - - override def transform(input: ModuleDecl): ModuleDecl = input match { - case ModuleDecl(path, includes, declarations, externs, definitions, exports) => - super.transform(ModuleDecl(path, includes, boxDecls ++ declarations, boxExterns ++ externs, definitions, exports)) match { - case ModuleDecl(path, includes, declarations, externs, definitions, exports) => - ModuleDecl(path, includes, declarations.filterNot(boxDecls.contains), externs.filterNot(boxExterns.contains), definitions, exports) - } - } - -} -class PolymorphismBoxingTests extends AbstractPolymorphismBoxingTests { - test("simple non-polymorphic code should stay the same"){ - val code = - """module main - | - |extern {} def bar() {f@f: MyInterface}: Foo at {f} = default "my decl" - |extern {} def bar() {g: MyInterface}: Foo at {g} = default "my decl" - | - |def prim = { () => return 42 } - |def union = { () => if (true) (f: () => Int @ {f})() else (g: () => Unit @ {g})() } - |def main = { () => return () } - | - |export prim - |""".stripMargin - assertTransformsTo(code, code) - } - - test("if only data types are used, code should stay the same"){ - val code = - """module main - | - |type Foo { X() B() } - | - |def baz = { ['b](){ f@f: MyInterface } => (f: () => Unit @ {})() } - |def bar = { ['b](){ f@f: MyInterface } => return box {f} (f : MyInterface @ {f}) } - |def id = { ['A](a: 'A) => return a: 'A } - | - |def returnA = { () => return (id: ['A]('A) => 'A @ {})[Foo]((X: () => Foo @ {})()) } - |def pReturnA = { ['B](b: 'B) => return (id: ['A]('A) => 'A @ {})['A](b: 'B) } - | - |""".stripMargin - assertTransformsTo(code,code) - } - - test("simple PureApp with int gets wrapped"){ - val from = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => return (id: ['A]('A) => 'A @ {})[Int](x: Int) } - |""".stripMargin - val to = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => return (unboxInt: (BoxedInt) => Int @ {})((id: ['A]('A) => 'A @ {})[BoxedInt]((boxInt: (Int) => BoxedInt @ {})(x: Int))) } - |""".stripMargin - assertTransformsTo(from, to) - } - - test("simple App with int gets wrapped"){ - val from = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => (id: ['A]('A) => 'A @ {})[Int](x: Int) } - |""".stripMargin - val to = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => val tmp = (id: ['A]('A) => 'A @ {})[BoxedInt]((boxInt: (Int) => BoxedInt @ {})(x: Int)) ; return (unboxInt: (BoxedInt) => Int @ {})(tmp:BoxedInt) } - |""".stripMargin - assertTransformsTo(from, to) - } - - test("DirectApp with [Int] gets wrapped correctly"){ - val from = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => - | { - | let res = !(id: ['A]('A) => 'A @ {})[Int](x: Int) - | return res: Int - | } - |} - |""".stripMargin - val to = - """module main - | - |def id = { ['A](a: 'A) => return a: 'A } - |def idInt = { (x: Int) => - | let boxed = !(id: ['A]('A) => 'A @ {})[BoxedInt]((boxInt: (Int) => BoxedInt @ {})(x: Int)) - | let unboxed = (unboxInt: (BoxedInt) => Int @ {})(boxed:BoxedInt) - | return unboxed: Int - |} - |""".stripMargin - assertTransformsTo(from,to) - } - - test("block parameters get wrapped \"inversely\""){ - val from = - """module main - |def test = { () => - | (hof: ['A](){ b : ('A) => 'A } => 'A @ {} )[Int](){ (x: Int) => return x: Int } - |} - |""".stripMargin - val to = - """module main - |def test = { () => - | val r = (hof: ['A](){ b : ('A) => 'A } => 'A @ {} )[BoxedInt](){ (boxedX: BoxedInt) => - | { - | def originalFn = { (x: Int) => return x: Int } - | val result = (originalFn: (Int) => Int @ {})((unboxInt: (BoxedInt) => Int @ {})(boxedX: BoxedInt)); - | return (boxInt: (Int) => BoxedInt @ {})(result: Int) - | } - | }; - | return (unboxInt: (BoxedInt) => Int @ {})(r:BoxedInt) - |} - |""".stripMargin - assertTransformsTo(from, to) - } - - test("higher higher order functions get wrapped correctly"){ - val from = - """module main - | - |def hhof_caller = { () => - | (hhof: ['A](){ b: (){ hhofarg: ('A) => 'A } => 'A } => 'A @ {})[Int](){ - | (){ hhofarg: (Int) => Int } => (hhofarg: (Int) => Int @ {})(5) - | } - |} - |""".stripMargin - val to = - """module main - | - |def hhof_caller = { () => - | val result = (hhof: ['A](){ b: (){ hhofarg: ('A) => 'A } => 'A } => 'A @ {})[BoxedInt](){ - | (){ hhofargB: ('A) => 'A } => - | { - | def originalFn = { (){ hhofarg: (Int) => Int } => (hhofarg: (Int) => Int @ {})(5) } - | val res = (originalFn: (){ hhofarg: (Int) => Int } => Int @ {})(){ - | (hhofargarg: Int) => - | { - | def tmp = hhofargB: ('A) => 'A @ {} - | val rres:BoxedInt = (tmp: ('A) => 'A @ {})((boxInt: (Int) => BoxedInt @ {})(hhofargarg: Int)); - | return (unboxInt: (BoxedInt) => Int @ {})(rres:BoxedInt) - | } - | }; - | return (boxInt: (Int) => BoxedInt @ {})(res:Int) - | } - | }; - | return (unboxInt: (BoxedInt) => Int @ {})(result:BoxedInt) - |} - |""".stripMargin - assertTransformsTo(from, to) - } -} diff --git a/effekt/jvm/src/test/scala/effekt/core/TestContext.scala b/effekt/jvm/src/test/scala/effekt/core/TestContext.scala deleted file mode 100644 index bd4fdffbc..000000000 --- a/effekt/jvm/src/test/scala/effekt/core/TestContext.scala +++ /dev/null @@ -1,51 +0,0 @@ -package effekt.core - -import effekt.context.{Context, ModuleDB} -import kiama.util.Positions -import kiama.util.Source -import effekt.util.messages.DebugMessaging -import scala.collection.immutable.Map - -trait TestModuleDB extends ModuleDB { self: Context => - - private var fileContents: Map[String, String] = Map.empty - private var sources: Map[String, Source] = Map.empty - - /** Ensures that [[contentsOf()]] returns [[contents]] when called with [[path]] in the future. */ - def provideFileContents(path: String, contents: String): Unit = { - fileContents = fileContents.updated(path, contents) - } - /** Ensures that [[findSource()]] returns [[source]] when called with [[path]] in the future. */ - def provideSource(path: String, source: Source): Unit = { - sources = sources.updated(path, source) - } - - /** Ensures that [[contentsOf()]] returns [[contents]] when called with [[path]] within [[body]]. */ - def withFileContents[R](path: String, contents: String)(body: => R): R = { - val oldFileContents = fileContents - fileContents = fileContents.updated(path, contents) - try { - body - } finally { - fileContents = oldFileContents - } - } - /** Ensures that [[findSource()]] returns [[source]] when called with [[path]] within [[body]]. */ - def withSource[R](path: String, source: Source)(body: => R): R = { - val oldSources = sources - sources = sources.updated(path, source) - try { - body - } finally { - sources = oldSources - } - } - - override def contentsOf(path: String): Option[String] = fileContents.get(path) - override def findSource(modulePath: String): Option[Source] = sources.get(modulePath) -} - -class TestContext extends Context(new Positions) with TestModuleDB { - - object messaging extends DebugMessaging -} \ No newline at end of file diff --git a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala index 0b6a26a9b..317ff12b2 100644 --- a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala @@ -1,845 +1,845 @@ -package effekt -package core - -import core.vm.* -import effekt.util.MarkdownSource - -class VMTests extends munit.FunSuite { - - import effekt.context.{ Context, IOModuleDB } - import effekt.util.PlainMessaging - import effekt.generator.vm.VM - import kiama.util.{ Positions, StringSource, FileSource } - - - val positions = new Positions - object plainMessaging extends PlainMessaging - object context extends Context(positions) with IOModuleDB { - val messaging = plainMessaging - object frontend extends generator.vm.VM - override lazy val compiler = frontend.asInstanceOf - } - - def compileString(content: String): (Id, symbols.Module, ModuleDecl) = - val config = new EffektConfig(Seq("--Koutput", "string")) - config.verify() - context.setup(config) - context.frontend.compile(StringSource(content, "input.effekt"))(using context).map { - case (_, decl) => decl - }.getOrElse { - val errors = plainMessaging.formatMessages(context.messaging.buffer) - sys error errors - } - - def compileFile(path: String): (Id, symbols.Module, ModuleDecl) = - val config = new EffektConfig(Seq("--Koutput", "string")) - config.verify() - context.setup(config) - - val source = if path.endsWith(".effekt.md") then - MarkdownSource(FileSource(path)) - else - FileSource(path) - - context.frontend.compile(source)(using context).map { - case (_, decl) => decl - }.getOrElse { - val errors = plainMessaging.formatMessages(context.messaging.buffer) - sys error s"Cannot compile ${path}\n\n${errors}" - } - - class OutputCapturingRuntime extends Runtime { - - import java.io.{ByteArrayOutputStream, PrintStream} - - private val outputStream = new ByteArrayOutputStream() - val out = new PrintStream(outputStream) - - def output(): String = { - out.flush() - outputStream.toString - } - } - - def runCounting(main: Id, decl: ModuleDecl): (String, Summary) = - object runtime extends OutputCapturingRuntime - object counting extends Counting - Interpreter(counting, runtime).run(main, decl) - (runtime.output(), Summary( - staticDispatches = counting.staticDispatches, - dynamicDispatches = counting.dynamicDispatches, - patternMatches = counting.patternMatches, - branches = counting.branches, - pushedFrames = counting.pushedFrames, - poppedFrames = counting.poppedFrames, - allocations = counting.allocations, - closures = counting.closures, - variableReads = counting.variableReads, - variableWrites = counting.variableWrites, - resets = counting.resets, - shifts = counting.shifts, - resumes = counting.resumes - )) - - def runString(contents: String): (String, Summary) = - val (main, mod, decl) = compileString(contents) - runCounting(main, decl) - - def runFile(file: String): (String, Summary) = - val (main, mod, decl) = compileFile(file) - runCounting(main, decl) - - - case class Summary( - staticDispatches: Int, - dynamicDispatches: Int, - patternMatches: Int, - branches: Int, - pushedFrames: Int, - poppedFrames: Int, - allocations: Int, - closures: Int, - variableReads: Int, - variableWrites: Int, - resets: Int, - shifts: Int, - resumes: Int - ) - - val recursion = - """def fib(n: Int): Int = - | if (n == 0) 1 - | else if (n == 1) 1 - | else fib(n - 2) + fib(n - 1) - | - |def main() = { - | println(fib(10).show) - |} - |""".stripMargin - - - test ("simple recursion") { - assertEquals(runString(recursion)._1, "89\n") - } - - val dynamicDispatch = - """def size[T](l: List[T]): Int = - | l match { - | case Nil() => 0 - | case Cons(hd, tl) => 1 + size(tl) - | } - | - |def map[A, B](l: List[A]) { f: A => B }: List[B] = - | l match { - | case Nil() => Nil() - | case Cons(hd, tl) => Cons(f(hd), map(tl){f}) - | } - | - |def main() = { - | println(size([1, 2, 3].map { x => x + 1 })) - |} - |""".stripMargin - - test ("dynamic dispatch") { - assertEquals(runString(dynamicDispatch)._1, "3\n") - } - - val simpleObject = - """interface Counter { - | def inc(): Unit - | def get(): Int - |} - | - |def main() = { - | def c = new Counter { - | def inc() = println("tick") - | def get() = 0 - | }; - | c.inc() - | c.inc() - | c.inc() - |} - | - |""".stripMargin - - test ("simple object") { - assertEquals(runString(simpleObject)._1, "tick\ntick\ntick\n") - } - - val mutableState = - """def main() = { - | var x = 42; - | x = x + 1; - | println(x.show) - | - | region r { - | var x in r = 10; - | x = x + 1 - | println(x.show) - | } - |} - |""".stripMargin - - test ("mutable state") { - assertEquals(runString(mutableState)._1, "43\n11\n") - } - - val simpleException = - """effect raise(): Unit - | - |def main() = { - | try { - | println("before"); - | do raise() - | println("after") - | } with raise { println("caught") } - |} - | - |""".stripMargin - - test ("simple exception") { - assertEquals(runString(simpleException)._1, "before\ncaught\n") - } - - val sorting = - """import list - | - |def main() = { - | // synchronized with doctest in `sortBy` - | println([1, 3, -1, 5].sortBy { (a, b) => a <= b }) - |} - |""".stripMargin - - test ("sorting (integration)") { - assertEquals(runString(sorting)._1, "Cons(-1, Cons(1, Cons(3, Cons(5, Nil()))))\n") - } - - val toplevelVal = - """ - |val top: Int = 43 - | - |def main() = { - | val x = top + top - | println(x.show) - |} - |""".stripMargin - - test ("toplevel val") { - assertEquals(runString(toplevelVal)._1, "86\n") - } - - - import java.io.File - import sbt.io.* - import sbt.io.syntax.* - - def examplesDir = new File("examples") - - val are_we_fast_yet: Seq[(File, Option[Summary])] = Seq( - examplesDir / "benchmarks" / "are_we_fast_yet" / "bounce.effekt" -> Some(Summary( - staticDispatches = 20202, - dynamicDispatches = 5000, - patternMatches = 0, - branches = 31628, - pushedFrames = 5052, - poppedFrames = 5052, - allocations = 0, - closures = 100, - variableReads = 7132, - variableWrites = 3157, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "list_tail.effekt" -> Some(Summary( - staticDispatches = 23713, - dynamicDispatches = 0, - patternMatches = 41728, - branches = 2843, - pushedFrames = 4961, - poppedFrames = 4961, - allocations = 34, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "mandelbrot.effekt" -> Some(Summary( - staticDispatches = 18, - dynamicDispatches = 0, - patternMatches = 0, - branches = 26, - pushedFrames = 3, - poppedFrames = 3, - allocations = 0, - closures = 0, - variableReads = 70, - variableWrites = 32, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "nbody.effekt" -> Some(Summary( - staticDispatches = 56, - dynamicDispatches = 0, - patternMatches = 455, - branches = 56, - pushedFrames = 15, - poppedFrames = 15, - allocations = 31, - closures = 0, - variableReads = 34, - variableWrites = 30, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "queens.effekt" -> Some(Summary( - staticDispatches = 2898, - dynamicDispatches = 0, - patternMatches = 0, - branches = 3887, - pushedFrames = 455, - poppedFrames = 439, - allocations = 0, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 113, - shifts = 8, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "sieve.effekt" -> Some(Summary( - staticDispatches = 21738, - dynamicDispatches = 0, - patternMatches = 0, - branches = 26736, - pushedFrames = 5000, - poppedFrames = 5000, - allocations = 0, - closures = 0, - variableReads = 34546, - variableWrites = 11738, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "towers.effekt" -> Some(Summary( - staticDispatches = 24606, - dynamicDispatches = 0, - patternMatches = 16396, - branches = 16287, - pushedFrames = 8193, - poppedFrames = 8193, - allocations = 8206, - closures = 0, - variableReads = 41027, - variableWrites = 8205, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt" -> Some(Summary( - staticDispatches = 17326, - dynamicDispatches = 0, - patternMatches = 0, - branches = 17326, - pushedFrames = 8661, - poppedFrames = 8661, - allocations = 0, - closures = 0, - variableReads = 23776, - variableWrites = 5039, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt" -> Some(Summary( - staticDispatches = 5463, - dynamicDispatches = 0, - patternMatches = 0, - branches = 5463, - pushedFrames = 5462, - poppedFrames = 5462, - allocations = 5461, - closures = 0, - variableReads = 8192, - variableWrites = 4096, - resets = 0, - shifts = 0, - resumes = 0 - )), - ) - - val duality_of_compilation: Seq[(File, Option[Summary])] = Seq( - examplesDir / "benchmarks" / "duality_of_compilation" / "erase_unused.effekt" -> Some(Summary( - staticDispatches = 21, - dynamicDispatches = 0, - patternMatches = 0, - branches = 21, - pushedFrames = 6, - poppedFrames = 6, - allocations = 16, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "factorial_accumulator.effekt" -> Some(Summary( - staticDispatches = 6, - dynamicDispatches = 0, - patternMatches = 0, - branches = 6, - pushedFrames = 1, - poppedFrames = 1, - allocations = 0, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "fibonacci_recursive.effekt" -> Some(Summary( - staticDispatches = 15, - dynamicDispatches = 0, - patternMatches = 0, - branches = 27, - pushedFrames = 15, - poppedFrames = 15, - allocations = 0, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "iterate_increment.effekt" -> Some(Summary( - staticDispatches = 6, - dynamicDispatches = 0, - patternMatches = 0, - branches = 6, - pushedFrames = 1, - poppedFrames = 1, - allocations = 0, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "lookup_tree.effekt" -> Some(Summary( - staticDispatches = 12, - dynamicDispatches = 0, - patternMatches = 6, - branches = 6, - pushedFrames = 7, - poppedFrames = 7, - allocations = 6, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "match_options.effekt" -> Some(Summary( - staticDispatches = 6, - dynamicDispatches = 0, - patternMatches = 6, - branches = 6, - pushedFrames = 6, - poppedFrames = 6, - allocations = 6, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "duality_of_compilation" / "sum_range.effekt" -> Some(Summary( - staticDispatches = 12, - dynamicDispatches = 0, - patternMatches = 6, - branches = 6, - pushedFrames = 12, - poppedFrames = 12, - allocations = 6, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 0, - shifts = 0, - resumes = 0 - )), - ) - - val effect_handlers_bench: Seq[(File, Option[Summary])] = Seq( - examplesDir / "benchmarks" / "effect_handlers_bench" / "countdown.effekt" -> Some(Summary( - staticDispatches = 6, - dynamicDispatches = 0, - patternMatches = 0, - branches = 6, - pushedFrames = 1, - poppedFrames = 1, - allocations = 0, - closures = 0, - variableReads = 6, - variableWrites = 5, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "iterator.effekt" -> Some(Summary( - staticDispatches = 7, - dynamicDispatches = 0, - patternMatches = 0, - branches = 7, - pushedFrames = 1, - poppedFrames = 1, - allocations = 0, - closures = 0, - variableReads = 7, - variableWrites = 6, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "nqueens.effekt" -> Some(Summary( - staticDispatches = 626, - dynamicDispatches = 0, - patternMatches = 400, - branches = 1282, - pushedFrames = 491, - poppedFrames = 715, - allocations = 54, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 1, - shifts = 211, - resumes = 220 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "parsing_dollars.effekt" -> Some(Summary( - staticDispatches = 67, - dynamicDispatches = 0, - patternMatches = 0, - branches = 210, - pushedFrames = 68, - poppedFrames = 67, - allocations = 0, - closures = 0, - variableReads = 222, - variableWrites = 88, - resets = 1, - shifts = 1, - resumes = 0 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "product_early.effekt" -> Some(Summary( - staticDispatches = 6013, - dynamicDispatches = 0, - patternMatches = 5005, - branches = 6013, - pushedFrames = 6008, - poppedFrames = 1008, - allocations = 1002, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 5, - shifts = 5, - resumes = 0 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "resume_nontail.effekt" -> Some(Summary( - staticDispatches = 7001, - dynamicDispatches = 0, - patternMatches = 0, - branches = 12001, - pushedFrames = 11001, - poppedFrames = 11001, - allocations = 0, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 1000, - shifts = 5000, - resumes = 5000 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "tree_explore.effekt" -> Some(Summary( - staticDispatches = 3317, - dynamicDispatches = 0, - patternMatches = 2380, - branches = 3167, - pushedFrames = 2697, - poppedFrames = 4297, - allocations = 1446, - closures = 0, - variableReads = 941, - variableWrites = 630, - resets = 10, - shifts = 310, - resumes = 620 - )), - - examplesDir / "benchmarks" / "effect_handlers_bench" / "triples.effekt" -> Some(Summary( - staticDispatches = 231, - dynamicDispatches = 0, - patternMatches = 4, - branches = 701, - pushedFrames = 702, - poppedFrames = 880, - allocations = 4, - closures = 0, - variableReads = 0, - variableWrites = 0, - resets = 1, - shifts = 347, - resumes = 350 - )), - ) - - val casestudies: Seq[(File, Option[Summary])] = Seq( - examplesDir / "casestudies" / "ad.effekt.md" -> Some(Summary( - staticDispatches = 19, - dynamicDispatches = 335, - patternMatches = 706, - branches = 247, - pushedFrames = 372, - poppedFrames = 372, - allocations = 174, - closures = 39, - variableReads = 0, - variableWrites = 0, - resets = 8, - shifts = 29, - resumes = 29 - )), - - examplesDir / "casestudies" / "buildsystem.effekt.md" -> Some(Summary( - staticDispatches = 43, - dynamicDispatches = 57, - patternMatches = 31, - branches = 40, - pushedFrames = 23, - poppedFrames = 20, - allocations = 15, - closures = 36, - variableReads = 7, - variableWrites = 3, - resets = 9, - shifts = 7, - resumes = 4 - )), - - examplesDir / "casestudies" / "scheduler.effekt.md" -> Some(Summary( - staticDispatches = 60, - dynamicDispatches = 7, - patternMatches = 80, - branches = 41, - pushedFrames = 56, - poppedFrames = 57, - allocations = 58, - closures = 7, - variableReads = 29, - variableWrites = 18, - resets = 1, - shifts = 7, - resumes = 7 - )), - - examplesDir / "casestudies" / "lexer.effekt.md" -> Some(Summary( - staticDispatches = 343, - dynamicDispatches = 18, - patternMatches = 205, - branches = 405, - pushedFrames = 194, - poppedFrames = 194, - allocations = 109, - closures = 27, - variableReads = 164, - variableWrites = 51, - resets = 31, - shifts = 11, - resumes = 11 - )), - - examplesDir / "casestudies" / "parser.effekt.md" -> Some(Summary( - staticDispatches = 12887, - dynamicDispatches = 783, - patternMatches = 9464, - branches = 14696, - pushedFrames = 7598, - poppedFrames = 7574, - allocations = 3885, - closures = 521, - variableReads = 6742, - variableWrites = 1901, - resets = 778, - shifts = 229, - resumes = 213 - )), - - examplesDir / "casestudies" / "anf.effekt.md" -> Some(Summary( - staticDispatches = 6925, - dynamicDispatches = 443, - patternMatches = 5128, - branches = 8018, - pushedFrames = 4310, - poppedFrames = 4297, - allocations = 2173, - closures = 358, - variableReads = 4080, - variableWrites = 1343, - resets = 458, - shifts = 322, - resumes = 306 - )), - - examplesDir / "casestudies" / "inference.effekt.md" -> Some(Summary( - staticDispatches = 1457454, - dynamicDispatches = 3201452, - patternMatches = 1474254, - branches = 303298, - pushedFrames = 2914407, - poppedFrames = 2049112, - allocations = 4625971, - closures = 865541, - variableReads = 2908620, - variableWrites = 1453663, - resets = 288559, - shifts = 297723, - resumes = 9275 - )), - - examplesDir / "pos" / "raytracer.effekt" -> Some(Summary( - staticDispatches = 91784, - dynamicDispatches = 0, - patternMatches = 771652, - branches = 65951, - pushedFrames = 51235, - poppedFrames = 51235, - allocations = 78909, - 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 = 1, - poppedFrames = 1, - allocations = 0, - closures = 0, - variableReads = 61, - variableWrites = 30, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "benchmarks" / "other" / "church_exponentiation.effekt" -> Some(Summary( - staticDispatches = 7, - dynamicDispatches = 797188, - patternMatches = 0, - branches = 5, - pushedFrames = 531467, - poppedFrames = 531467, - allocations = 0, - closures = 26, - 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 = 12024, - poppedFrames = 12024, - allocations = 24060, - closures = 12030, - variableReads = 24048, - variableWrites = 18036, - resets = 0, - shifts = 0, - resumes = 0 - )), - - examplesDir / "stdlib" / "stream" / "fix.effekt" -> Some(Summary( - staticDispatches = 38, - dynamicDispatches = 52, - patternMatches = 37, - branches = 0, - pushedFrames = 34, - poppedFrames = 34, - allocations = 37, - closures = 37, - variableReads = 10, - variableWrites = 9, - resets = 0, - shifts = 0, - resumes = 0 - )), - ) - - val testFiles: Seq[(File, Option[Summary])] = - are_we_fast_yet ++ - duality_of_compilation ++ - effect_handlers_bench ++ - casestudies ++ - other - - def runTest(f: File, expectedSummary: Option[Summary]): Unit = - val path = f.getPath - test(path) { - try { - val (result, summary) = runFile(path) - val expected = expectedResultFor(f).getOrElse { s"Missing checkfile for ${path}"} - assertNoDiff(result, expected) - expectedSummary.foreach { expected => assertEquals(summary, expected) } - } catch { - case i: VMError => fail(i.getMessage, i) - } - } - - testFiles.foreach(runTest) -} +//package effekt +//package core +// +//import core.vm.* +//import effekt.util.MarkdownSource +// +//class VMTests extends munit.FunSuite { +// +// import effekt.context.{ Context, IOModuleDB } +// import effekt.util.PlainMessaging +// import effekt.generator.vm.VM +// import kiama.util.{ Positions, StringSource, FileSource } +// +// +// val positions = new Positions +// object plainMessaging extends PlainMessaging +// object context extends Context(positions) with IOModuleDB { +// val messaging = plainMessaging +// object frontend extends generator.vm.VM +// override lazy val compiler = frontend.asInstanceOf +// } +// +// def compileString(content: String): (Id, symbols.Module, ModuleDecl) = +// val config = new EffektConfig(Seq("--Koutput", "string")) +// config.verify() +// context.setup(config) +// context.frontend.compile(StringSource(content, "input.effekt"))(using context).map { +// case (_, decl) => decl +// }.getOrElse { +// val errors = plainMessaging.formatMessages(context.messaging.buffer) +// sys error errors +// } +// +// def compileFile(path: String): (Id, symbols.Module, ModuleDecl) = +// val config = new EffektConfig(Seq("--Koutput", "string")) +// config.verify() +// context.setup(config) +// +// val source = if path.endsWith(".effekt.md") then +// MarkdownSource(FileSource(path)) +// else +// FileSource(path) +// +// context.frontend.compile(source)(using context).map { +// case (_, decl) => decl +// }.getOrElse { +// val errors = plainMessaging.formatMessages(context.messaging.buffer) +// sys error s"Cannot compile ${path}\n\n${errors}" +// } +// +// class OutputCapturingRuntime extends Runtime { +// +// import java.io.{ByteArrayOutputStream, PrintStream} +// +// private val outputStream = new ByteArrayOutputStream() +// val out = new PrintStream(outputStream) +// +// def output(): String = { +// out.flush() +// outputStream.toString +// } +// } +// +// def runCounting(main: Id, decl: ModuleDecl): (String, Summary) = +// object runtime extends OutputCapturingRuntime +// object counting extends Counting +// Interpreter(counting, runtime).run(main, decl) +// (runtime.output(), Summary( +// staticDispatches = counting.staticDispatches, +// dynamicDispatches = counting.dynamicDispatches, +// patternMatches = counting.patternMatches, +// branches = counting.branches, +// pushedFrames = counting.pushedFrames, +// poppedFrames = counting.poppedFrames, +// allocations = counting.allocations, +// closures = counting.closures, +// variableReads = counting.variableReads, +// variableWrites = counting.variableWrites, +// resets = counting.resets, +// shifts = counting.shifts, +// resumes = counting.resumes +// )) +// +// def runString(contents: String): (String, Summary) = +// val (main, mod, decl) = compileString(contents) +// runCounting(main, decl) +// +// def runFile(file: String): (String, Summary) = +// val (main, mod, decl) = compileFile(file) +// runCounting(main, decl) +// +// +// case class Summary( +// staticDispatches: Int, +// dynamicDispatches: Int, +// patternMatches: Int, +// branches: Int, +// pushedFrames: Int, +// poppedFrames: Int, +// allocations: Int, +// closures: Int, +// variableReads: Int, +// variableWrites: Int, +// resets: Int, +// shifts: Int, +// resumes: Int +// ) +// +// val recursion = +// """def fib(n: Int): Int = +// | if (n == 0) 1 +// | else if (n == 1) 1 +// | else fib(n - 2) + fib(n - 1) +// | +// |def main() = { +// | println(fib(10).show) +// |} +// |""".stripMargin +// +// +// test ("simple recursion") { +// assertEquals(runString(recursion)._1, "89\n") +// } +// +// val dynamicDispatch = +// """def size[T](l: List[T]): Int = +// | l match { +// | case Nil() => 0 +// | case Cons(hd, tl) => 1 + size(tl) +// | } +// | +// |def map[A, B](l: List[A]) { f: A => B }: List[B] = +// | l match { +// | case Nil() => Nil() +// | case Cons(hd, tl) => Cons(f(hd), map(tl){f}) +// | } +// | +// |def main() = { +// | println(size([1, 2, 3].map { x => x + 1 })) +// |} +// |""".stripMargin +// +// test ("dynamic dispatch") { +// assertEquals(runString(dynamicDispatch)._1, "3\n") +// } +// +// val simpleObject = +// """interface Counter { +// | def inc(): Unit +// | def get(): Int +// |} +// | +// |def main() = { +// | def c = new Counter { +// | def inc() = println("tick") +// | def get() = 0 +// | }; +// | c.inc() +// | c.inc() +// | c.inc() +// |} +// | +// |""".stripMargin +// +// test ("simple object") { +// assertEquals(runString(simpleObject)._1, "tick\ntick\ntick\n") +// } +// +// val mutableState = +// """def main() = { +// | var x = 42; +// | x = x + 1; +// | println(x.show) +// | +// | region r { +// | var x in r = 10; +// | x = x + 1 +// | println(x.show) +// | } +// |} +// |""".stripMargin +// +// test ("mutable state") { +// assertEquals(runString(mutableState)._1, "43\n11\n") +// } +// +// val simpleException = +// """effect raise(): Unit +// | +// |def main() = { +// | try { +// | println("before"); +// | do raise() +// | println("after") +// | } with raise { println("caught") } +// |} +// | +// |""".stripMargin +// +// test ("simple exception") { +// assertEquals(runString(simpleException)._1, "before\ncaught\n") +// } +// +// val sorting = +// """import list +// | +// |def main() = { +// | // synchronized with doctest in `sortBy` +// | println([1, 3, -1, 5].sortBy { (a, b) => a <= b }) +// |} +// |""".stripMargin +// +// test ("sorting (integration)") { +// assertEquals(runString(sorting)._1, "Cons(-1, Cons(1, Cons(3, Cons(5, Nil()))))\n") +// } +// +// val toplevelVal = +// """ +// |val top: Int = 43 +// | +// |def main() = { +// | val x = top + top +// | println(x.show) +// |} +// |""".stripMargin +// +// test ("toplevel val") { +// assertEquals(runString(toplevelVal)._1, "86\n") +// } +// +// +// import java.io.File +// import sbt.io.* +// import sbt.io.syntax.* +// +// def examplesDir = new File("examples") +// +// val are_we_fast_yet: Seq[(File, Option[Summary])] = Seq( +// examplesDir / "benchmarks" / "are_we_fast_yet" / "bounce.effekt" -> Some(Summary( +// staticDispatches = 20202, +// dynamicDispatches = 5000, +// patternMatches = 0, +// branches = 31628, +// pushedFrames = 5052, +// poppedFrames = 5052, +// allocations = 0, +// closures = 100, +// variableReads = 7132, +// variableWrites = 3157, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "list_tail.effekt" -> Some(Summary( +// staticDispatches = 23713, +// dynamicDispatches = 0, +// patternMatches = 41728, +// branches = 2843, +// pushedFrames = 4961, +// poppedFrames = 4961, +// allocations = 34, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "mandelbrot.effekt" -> Some(Summary( +// staticDispatches = 18, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 26, +// pushedFrames = 3, +// poppedFrames = 3, +// allocations = 0, +// closures = 0, +// variableReads = 70, +// variableWrites = 32, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "nbody.effekt" -> Some(Summary( +// staticDispatches = 56, +// dynamicDispatches = 0, +// patternMatches = 455, +// branches = 56, +// pushedFrames = 15, +// poppedFrames = 15, +// allocations = 31, +// closures = 0, +// variableReads = 34, +// variableWrites = 30, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "queens.effekt" -> Some(Summary( +// staticDispatches = 2898, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 3887, +// pushedFrames = 455, +// poppedFrames = 439, +// allocations = 0, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 113, +// shifts = 8, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "sieve.effekt" -> Some(Summary( +// staticDispatches = 21738, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 26736, +// pushedFrames = 5000, +// poppedFrames = 5000, +// allocations = 0, +// closures = 0, +// variableReads = 34546, +// variableWrites = 11738, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "towers.effekt" -> Some(Summary( +// staticDispatches = 24606, +// dynamicDispatches = 0, +// patternMatches = 16396, +// branches = 16287, +// pushedFrames = 8193, +// poppedFrames = 8193, +// allocations = 8206, +// closures = 0, +// variableReads = 41027, +// variableWrites = 8205, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt" -> Some(Summary( +// staticDispatches = 17326, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 17326, +// pushedFrames = 8661, +// poppedFrames = 8661, +// allocations = 0, +// closures = 0, +// variableReads = 23776, +// variableWrites = 5039, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt" -> Some(Summary( +// staticDispatches = 5463, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 5463, +// pushedFrames = 5462, +// poppedFrames = 5462, +// allocations = 5461, +// closures = 0, +// variableReads = 8192, +// variableWrites = 4096, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// ) +// +// val duality_of_compilation: Seq[(File, Option[Summary])] = Seq( +// examplesDir / "benchmarks" / "duality_of_compilation" / "erase_unused.effekt" -> Some(Summary( +// staticDispatches = 21, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 21, +// pushedFrames = 6, +// poppedFrames = 6, +// allocations = 16, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "factorial_accumulator.effekt" -> Some(Summary( +// staticDispatches = 6, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 6, +// pushedFrames = 1, +// poppedFrames = 1, +// allocations = 0, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "fibonacci_recursive.effekt" -> Some(Summary( +// staticDispatches = 15, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 27, +// pushedFrames = 15, +// poppedFrames = 15, +// allocations = 0, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "iterate_increment.effekt" -> Some(Summary( +// staticDispatches = 6, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 6, +// pushedFrames = 1, +// poppedFrames = 1, +// allocations = 0, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "lookup_tree.effekt" -> Some(Summary( +// staticDispatches = 12, +// dynamicDispatches = 0, +// patternMatches = 6, +// branches = 6, +// pushedFrames = 7, +// poppedFrames = 7, +// allocations = 6, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "match_options.effekt" -> Some(Summary( +// staticDispatches = 6, +// dynamicDispatches = 0, +// patternMatches = 6, +// branches = 6, +// pushedFrames = 6, +// poppedFrames = 6, +// allocations = 6, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "duality_of_compilation" / "sum_range.effekt" -> Some(Summary( +// staticDispatches = 12, +// dynamicDispatches = 0, +// patternMatches = 6, +// branches = 6, +// pushedFrames = 12, +// poppedFrames = 12, +// allocations = 6, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// ) +// +// val effect_handlers_bench: Seq[(File, Option[Summary])] = Seq( +// examplesDir / "benchmarks" / "effect_handlers_bench" / "countdown.effekt" -> Some(Summary( +// staticDispatches = 6, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 6, +// pushedFrames = 1, +// poppedFrames = 1, +// allocations = 0, +// closures = 0, +// variableReads = 6, +// variableWrites = 5, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "iterator.effekt" -> Some(Summary( +// staticDispatches = 7, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 7, +// pushedFrames = 1, +// poppedFrames = 1, +// allocations = 0, +// closures = 0, +// variableReads = 7, +// variableWrites = 6, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "nqueens.effekt" -> Some(Summary( +// staticDispatches = 626, +// dynamicDispatches = 0, +// patternMatches = 400, +// branches = 1282, +// pushedFrames = 491, +// poppedFrames = 715, +// allocations = 54, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 1, +// shifts = 211, +// resumes = 220 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "parsing_dollars.effekt" -> Some(Summary( +// staticDispatches = 67, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 210, +// pushedFrames = 68, +// poppedFrames = 67, +// allocations = 0, +// closures = 0, +// variableReads = 222, +// variableWrites = 88, +// resets = 1, +// shifts = 1, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "product_early.effekt" -> Some(Summary( +// staticDispatches = 6013, +// dynamicDispatches = 0, +// patternMatches = 5005, +// branches = 6013, +// pushedFrames = 6008, +// poppedFrames = 1008, +// allocations = 1002, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 5, +// shifts = 5, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "resume_nontail.effekt" -> Some(Summary( +// staticDispatches = 7001, +// dynamicDispatches = 0, +// patternMatches = 0, +// branches = 12001, +// pushedFrames = 11001, +// poppedFrames = 11001, +// allocations = 0, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 1000, +// shifts = 5000, +// resumes = 5000 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "tree_explore.effekt" -> Some(Summary( +// staticDispatches = 3317, +// dynamicDispatches = 0, +// patternMatches = 2380, +// branches = 3167, +// pushedFrames = 2697, +// poppedFrames = 4297, +// allocations = 1446, +// closures = 0, +// variableReads = 941, +// variableWrites = 630, +// resets = 10, +// shifts = 310, +// resumes = 620 +// )), +// +// examplesDir / "benchmarks" / "effect_handlers_bench" / "triples.effekt" -> Some(Summary( +// staticDispatches = 231, +// dynamicDispatches = 0, +// patternMatches = 4, +// branches = 701, +// pushedFrames = 702, +// poppedFrames = 880, +// allocations = 4, +// closures = 0, +// variableReads = 0, +// variableWrites = 0, +// resets = 1, +// shifts = 347, +// resumes = 350 +// )), +// ) +// +// val casestudies: Seq[(File, Option[Summary])] = Seq( +// examplesDir / "casestudies" / "ad.effekt.md" -> Some(Summary( +// staticDispatches = 19, +// dynamicDispatches = 335, +// patternMatches = 706, +// branches = 247, +// pushedFrames = 372, +// poppedFrames = 372, +// allocations = 174, +// closures = 39, +// variableReads = 0, +// variableWrites = 0, +// resets = 8, +// shifts = 29, +// resumes = 29 +// )), +// +// examplesDir / "casestudies" / "buildsystem.effekt.md" -> Some(Summary( +// staticDispatches = 43, +// dynamicDispatches = 57, +// patternMatches = 31, +// branches = 40, +// pushedFrames = 23, +// poppedFrames = 20, +// allocations = 15, +// closures = 36, +// variableReads = 7, +// variableWrites = 3, +// resets = 9, +// shifts = 7, +// resumes = 4 +// )), +// +// examplesDir / "casestudies" / "scheduler.effekt.md" -> Some(Summary( +// staticDispatches = 60, +// dynamicDispatches = 7, +// patternMatches = 80, +// branches = 41, +// pushedFrames = 56, +// poppedFrames = 57, +// allocations = 58, +// closures = 7, +// variableReads = 29, +// variableWrites = 18, +// resets = 1, +// shifts = 7, +// resumes = 7 +// )), +// +// examplesDir / "casestudies" / "lexer.effekt.md" -> Some(Summary( +// staticDispatches = 343, +// dynamicDispatches = 18, +// patternMatches = 205, +// branches = 405, +// pushedFrames = 194, +// poppedFrames = 194, +// allocations = 109, +// closures = 27, +// variableReads = 164, +// variableWrites = 51, +// resets = 31, +// shifts = 11, +// resumes = 11 +// )), +// +// examplesDir / "casestudies" / "parser.effekt.md" -> Some(Summary( +// staticDispatches = 12887, +// dynamicDispatches = 783, +// patternMatches = 9464, +// branches = 14696, +// pushedFrames = 7598, +// poppedFrames = 7574, +// allocations = 3885, +// closures = 521, +// variableReads = 6742, +// variableWrites = 1901, +// resets = 778, +// shifts = 229, +// resumes = 213 +// )), +// +// examplesDir / "casestudies" / "anf.effekt.md" -> Some(Summary( +// staticDispatches = 6925, +// dynamicDispatches = 443, +// patternMatches = 5128, +// branches = 8018, +// pushedFrames = 4310, +// poppedFrames = 4297, +// allocations = 2173, +// closures = 358, +// variableReads = 4080, +// variableWrites = 1343, +// resets = 458, +// shifts = 322, +// resumes = 306 +// )), +// +// examplesDir / "casestudies" / "inference.effekt.md" -> Some(Summary( +// staticDispatches = 1457454, +// dynamicDispatches = 3201452, +// patternMatches = 1474254, +// branches = 303298, +// pushedFrames = 2914407, +// poppedFrames = 2049112, +// allocations = 4625971, +// closures = 865541, +// variableReads = 2908620, +// variableWrites = 1453663, +// resets = 288559, +// shifts = 297723, +// resumes = 9275 +// )), +// +// examplesDir / "pos" / "raytracer.effekt" -> Some(Summary( +// staticDispatches = 91784, +// dynamicDispatches = 0, +// patternMatches = 771652, +// branches = 65951, +// pushedFrames = 51235, +// poppedFrames = 51235, +// allocations = 78909, +// 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 = 1, +// poppedFrames = 1, +// allocations = 0, +// closures = 0, +// variableReads = 61, +// variableWrites = 30, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "benchmarks" / "other" / "church_exponentiation.effekt" -> Some(Summary( +// staticDispatches = 7, +// dynamicDispatches = 797188, +// patternMatches = 0, +// branches = 5, +// pushedFrames = 531467, +// poppedFrames = 531467, +// allocations = 0, +// closures = 26, +// 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 = 12024, +// poppedFrames = 12024, +// allocations = 24060, +// closures = 12030, +// variableReads = 24048, +// variableWrites = 18036, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// +// examplesDir / "stdlib" / "stream" / "fix.effekt" -> Some(Summary( +// staticDispatches = 38, +// dynamicDispatches = 52, +// patternMatches = 37, +// branches = 0, +// pushedFrames = 34, +// poppedFrames = 34, +// allocations = 37, +// closures = 37, +// variableReads = 10, +// variableWrites = 9, +// resets = 0, +// shifts = 0, +// resumes = 0 +// )), +// ) +// +// val testFiles: Seq[(File, Option[Summary])] = +// are_we_fast_yet ++ +// duality_of_compilation ++ +// effect_handlers_bench ++ +// casestudies ++ +// other +// +// def runTest(f: File, expectedSummary: Option[Summary]): Unit = +// val path = f.getPath +// test(path) { +// try { +// val (result, summary) = runFile(path) +// val expected = expectedResultFor(f).getOrElse { s"Missing checkfile for ${path}"} +// assertNoDiff(result, expected) +// expectedSummary.foreach { expected => assertEquals(summary, expected) } +// } catch { +// case i: VMError => fail(i.getMessage, i) +// } +// } +// +// testFiles.foreach(runTest) +//} diff --git a/effekt/shared/src/main/scala/effekt/Compiler.scala b/effekt/shared/src/main/scala/effekt/Compiler.scala deleted file mode 100644 index 93c1f2466..000000000 --- a/effekt/shared/src/main/scala/effekt/Compiler.scala +++ /dev/null @@ -1,320 +0,0 @@ -package effekt - -import effekt.PhaseResult.{ AllTransformed, CoreTransformed } -import effekt.context.Context -import effekt.core.Transformer -import effekt.namer.Namer -import effekt.source.{ AnnotateCaptures, ExplicitCapabilities, ResolveExternDefs, ModuleDecl } -import effekt.symbols.Module -import effekt.typer.{ BoxUnboxInference, Typer, Wellformedness } -import effekt.util.messages.{ FatalPhaseError, CompilerPanic } -import effekt.util.{ SourceTask, Task, VirtualSource, paths } -import kiama.output.PrettyPrinterTypes.Document -import kiama.util.{ Positions, Source } - -/** - * Intermediate results produced by the various phases. - * - * All phases have a source field, which is mostly used to invalidate caches based on the timestamp. - */ -enum PhaseResult { - - val source: Source - - /** - * The result of [[Parser]] parsing a single file into a [[effekt.source.Tree]]. - */ - case Parsed(source: Source, tree: ModuleDecl) - - /** - * The result of [[Namer]] resolving all names in a given syntax tree. The resolved symbols are - * annotated in the [[Context]] using [[effekt.context.Annotations]]. - */ - case NameResolved(source: Source, tree: ModuleDecl, mod: symbols.Module) - - /** - * The result of [[Typer]] type checking a given syntax tree. - * - * We can notice that [[NameResolved]] and [[Typechecked]] haave the same fields. - * Like, [[Namer]], [[Typer]] writes to the types of each tree into the DB, using [[effekt.context.Annotations]]. - * This might change in the future, when we switch to elaboration. - */ - case Typechecked(source: Source, tree: ModuleDecl, mod: symbols.Module) - - /** - * The result of [[Transformer]] ANF transforming [[source.Tree]] into the core representation [[core.Tree]]. - */ - case CoreTransformed(source: Source, tree: ModuleDecl, mod: symbols.Module, core: effekt.core.ModuleDecl) - - /** - * The result of running the [[Compiler.Middleend]] on all dependencies. - */ - case AllTransformed(source: Source, main: PhaseResult.CoreTransformed, dependencies: List[PhaseResult.CoreTransformed]) - - /** - * The result of [[effekt.generator.Backend]], consisting of a mapping from filename to output to be written. - */ - case Compiled(source: Source, mainFile: String, outputFiles: Map[String, Document]) -} -export PhaseResult.* - -enum Stage { case Core; case Machine; case Target; } - -/** - * The compiler for the Effekt language. - * - * The compiler is set up in the following large phases that consist itself of potentially multiple phases - * - * 1. Parser (Source -> source.Tree) Load file and parse it into an AST - * - * 2. Frontend (source.Tree -> source.Tree) Perform name analysis, typechecking, region inference, - * and other rewriting of the source AST - * - * 3. Middleend (source.Tree -> core.Tree) Perform an ANF transformation into core, and - * other rewritings on the core AST - * - * 4. Backend (core.Tree -> Document) Generate code in a target language - * - * The compiler itself does not read from or write to files. This is important since, we need to - * virtualize the file system to also run the compiler in the browser. - * - * - Reading files is performed by the mixin [[effekt.context.ModuleDB]], which is implemented - * differently for the JS and JVM versions. - * - Writing to files is performed by the hook [[Compiler.saveOutput]], which is implemented - * differently for the JS and JVM versions. - * - * Backends that implement this interface: - * - * - [[generator.js.JavaScript]] - * - [[generator.chez.ChezScheme]] (in three variants) - * - [[generator.llvm.LLVM]] - * - * @tparam Executable information of this compilation run, which is passed to - * the corresponding backend runner (e.g. the name of the main file) - */ -trait Compiler[Executable] { - - // The Compiler Interface: - // ----------------------- - // Used by Driver, Server, Repl and Web - - def extension: String - - def supportedFeatureFlags: List[String] - - /** - * Used by LSP server (Intelligence) to map positions to source trees - */ - def getAST(source: Source)(using Context): Option[ModuleDecl] = - CachedParser(source).map { res => res.tree } - - /** - * This is the second-most important entry-point besides [[Driver.compileSource]]. - * It is used by - * - Namer to resolve dependencies - * - Server / Driver to typecheck and report type errors in VSCode - */ - def runFrontend(source: Source)(using C: Context): Option[Module] = - def getStackTrace(e: Throwable): String = - val stringWriter = new java.io.StringWriter() - e.printStackTrace(new java.io.PrintWriter(stringWriter)) - stringWriter.toString - - try { - val res = Frontend(source).map { res => - val mod = res.mod - validate(source, mod) - mod - } - if C.messaging.hasErrors then None else res - } catch { - case FatalPhaseError(msg) => - C.report(msg) - None - case e @ CompilerPanic(msg) => - C.report(msg) - C.info(getStackTrace(e)) - None - case e => - C.info("Effekt Compiler Crash: " + e.toString) - C.info(getStackTrace(e)) - None - } - - /** - * Called after running the frontend from editor services. - * - * Can be overridden to implement backend specific checks (exclude certain - * syntactic constructs, etc.) - */ - def validate(source: Source, mod: Module)(using Context): Unit = () - - /** - * Show the IR of this backend for [[source]] after [[stage]]. - * Backends can return [[None]] if they do not support this. - * - * Used to show the IR in VSCode. For the JS Backend, also used for - * separate compilation in the web. - */ - def prettyIR(source: Source, stage: Stage)(using Context): Option[Document] - - /** - * Return the backend specific AST for [[source]] after [[stage]]. - * Backends can return [[None]] if they do not support this. - * - * The AST will be pretty printed with a generic [[effekt.util.PrettyPrinter]]. - */ - def treeIR(source: Source, stage: Stage)(using Context): Option[Any] - - /** - * Should compile [[source]] with this backend. Each backend can - * choose the representation of the executable. - */ - def compile(source: Source)(using Context): Option[(Map[String, String], Executable)] - - - // The Compiler Compiler Phases: - // ----------------------------- - // Components that can be used by individual (backend) implementations to structure - // the (individual) full compiler pipeline. - - /** - * @note The result of parsing needs to be cached. - * - * [[Intelligence]] uses both the results of [[getAST]] and [[runFrontend]]. - * Since we associate trees and symbols by the *object identity* of the tree, - * running parser multiple times on the same input results in different trees. - * In consequence, the symbols can't be found anymore. To avoid this, we - * use a separate task for parsing. - * - * Having access to the parse trees separately is also helpful for programs - * that fail in later phases (for instance type checking). This way some - * editor services can be provided, even in presence of errors. - */ - val CachedParser = Phase.cached("cached-parser") { Parser } - - /** - * Frontend - */ - val Frontend = Phase.cached("frontend") { - /** - * Parses a file to a syntax tree - * [[Source]] --> [[Parsed]] - */ - CachedParser andThen - /** - * Performs name analysis and associates Id-trees with symbols - * [[Parsed]] --> [[NameResolved]] - */ - Namer andThen - /** - * Explicit box transformation - * [[NameResolved]] --> [[NameResolved]] - */ - BoxUnboxInference andThen - /** - * Type checks and annotates trees with inferred types and effects - * [[NameResolved]] --> [[Typechecked]] - */ - Typer andThen - /** - * Wellformedness checks (exhaustivity, non-escape) - * [[Typechecked]] --> [[Typechecked]] - */ - Wellformedness andThen - /** - * Resolves `extern`s for the current backend - * [[Typechecked]] --> [[Typechecked]] - */ - ResolveExternDefs andThen - /** - * Uses annotated effects to translate to explicit capability passing - * [[Typechecked]] --> [[Typechecked]] - */ - ExplicitCapabilities andThen - /** - * Computes and annotates the capture of each subexpression - * [[Typechecked]] --> [[Typechecked]] - */ - AnnotateCaptures - } - - /** - * Middleend - */ - val Middleend = Phase.cached("middleend", cacheBy = (in: Typechecked) => paths.lastModified(in.source)) { - /** - * Translates a source program to a core program - * [[Typechecked]] --> [[CoreTransformed]] - */ - Transformer - } - - /** - * Maps the phase over all core modules (dependencies and the main module) - */ - def all(phase: Phase[CoreTransformed, CoreTransformed]): Phase[AllTransformed, AllTransformed] = - new Phase[AllTransformed, AllTransformed] { - val phaseName = s"all-${phase.phaseName}" - - def run(input: AllTransformed)(using Context) = for { - main <- phase(input.main) - dependencies <- input.dependencies.foldRight[Option[List[CoreTransformed]]](Some(Nil)) { - case (dep, Some(deps)) => phase(dep).map(_ :: deps) - case (_, _) => None - } - } yield AllTransformed(input.source, main, dependencies) - } - - def allToCore(phase: Phase[Source, CoreTransformed]): Phase[Source, AllTransformed] = new Phase[Source, AllTransformed] { - val phaseName = "core-dependencies" - - def run(input: Source)(using Context) = for { - main @ CoreTransformed(_, _, mod, _) <- phase(input) - dependencies <- mod.dependencies.foldRight[Option[List[CoreTransformed]]](Some(Nil)) { - case (dep, Some(deps)) => phase(dep.source).map(_ :: deps) - case (_, _) => None - } - } yield AllTransformed(input, main, dependencies) - } - - lazy val Aggregate = Phase[AllTransformed, CoreTransformed]("aggregate") { - case AllTransformed(_, CoreTransformed(src, tree, mod, main), deps) => - val dependencies = deps.map(d => d.core) - - // collect all information - var declarations: List[core.Declaration] = Nil - var externs: List[core.Extern] = Nil - var definitions: List[core.Toplevel] = Nil - var exports: List[symbols.Symbol] = Nil - - (dependencies :+ main).foreach { module => - externs ++= module.externs - declarations ++= module.declarations - definitions ++= module.definitions - exports ++= module.exports - } - - val aggregated = core.ModuleDecl(main.path, Nil, declarations, externs, definitions, exports) - - // TODO in the future check for duplicate exports - CoreTransformed(src, tree, mod, aggregated) - } - - lazy val Machine = Phase("machine") { - case CoreTransformed(source, tree, mod, core) => - val main = Context.checkMain(mod) - val program = machine.Transformer.transform(main, core) - (mod, main, program) - } - - // Helpers - // ------- - import effekt.util.paths.file - - /** - * Path relative to the output folder - */ - def path(m: symbols.Module)(using C: Context): String = - (m.path.split('/').last.replace('-', '_')) + extension -} diff --git a/effekt/shared/src/main/scala/effekt/Intelligence.scala b/effekt/shared/src/main/scala/effekt/Intelligence.scala deleted file mode 100644 index ed918338b..000000000 --- a/effekt/shared/src/main/scala/effekt/Intelligence.scala +++ /dev/null @@ -1,270 +0,0 @@ -package effekt - -import effekt.context.{ Annotations, Context } -import effekt.source.{ FunDef, ModuleDecl, Tree, Include } -import kiama.util.{ Position, Source } - -trait Intelligence { - - import effekt.symbols._ - import builtins.TState - - type EffektTree = kiama.relation.Tree[AnyRef & Product, ModuleDecl] - - case class SymbolInfo( - symbol: Symbol, - header: String, - signature: Option[String], - description: Option[String] - ) { - def fullDescription: String = { - val sig = signature.map(sig => s"```effekt\n$sig\n```").getOrElse { "" } - val desc = description.getOrElse("") - - s"""|#### $header - |$sig - |$desc - |""".stripMargin - } - - def shortDescription: String = { - val sig = signature.map(sig => s"```effekt\n$sig\n```").getOrElse { "" } - - s"""|#### $header - |$sig - |""".stripMargin - } - } - - private def sortByPosition(trees: Vector[Tree])(using C: Context): Vector[Tree] = - val pos = C.positions - trees.sortWith { - (t1, t2) => - val p1s = pos.getStart(t1).get - val p2s = pos.getStart(t2).get - - if (p2s == p1s) { - val p1e = pos.getFinish(t1).get - val p2e = pos.getFinish(t2).get - p1e < p2e - } else { - p2s < p1s - } - } - - def getTreesAt(position: Position)(using C: Context): Option[Vector[Tree]] = for { - decl <- C.compiler.getAST(position.source) - tree = new EffektTree(decl) - allTrees = tree.nodes.collect { case t: Tree => t } - trees = C.positions.findNodesContaining(allTrees, position) - nodes = sortByPosition(trees) - } yield nodes - - def getTreesInRange(range: kiama.util.Range)(using C: Context): Option[Vector[Tree]] = for { - decl <- C.compiler.getAST(range.from.source) - tree = new EffektTree(decl) - allTrees = tree.nodes.collect { case t: Tree => t } - trees = C.positions.findNodesInRange(allTrees, range) - nodes = sortByPosition(trees) - } yield nodes - - def getIdTreeAt(position: Position)(using C: Context): Option[source.Id] = for { - trees <- getTreesAt(position) - id <- trees.collectFirst { case id: source.Id => id } - } yield id - - def getSymbolAt(position: Position)(using C: Context): Option[(Tree, Symbol)] = - def identifiers = for { - id <- getIdTreeAt(position) - sym <- C.symbolOption(id) - } yield (id, resolveCallTarget(sym)) - - def others = for { - trees <- getTreesAt(position).toVector - tree <- trees - sym <- getSymbolOf(tree) - } yield (tree, sym) - - identifiers.orElse(others.headOption) - - def getSymbolOf(tree: Tree)(using C: Context): Option[Symbol] = - tree match { - case i @ Include(path) => C.annotationOption(Annotations.IncludedSymbols, i) - case _ => None - } - - def getDefinitionAt(position: Position)(using C: Context): Option[Tree] = for { - (_, sym) <- getSymbolAt(position) - decl <- getDefinitionOf(resolveCallTarget(sym)) - } yield decl - - def getDefinitionOf(s: Symbol)(using C: Context): Option[Tree] = s match { - case u: UserFunction => Some(u.decl) - case u: Binder => Some(u.decl) - case d: Operation => C.definitionTreeOption(d.interface) - case a: Anon => Some(a.decl) - case m: Module => Some(m.decl) - case u => C.definitionTreeOption(u) - } - - // For now, only show the first call target - def resolveCallTarget(sym: Symbol): Symbol = sym match { - case t: CallTarget => t.symbols.flatten.headOption.getOrElse(sym) - case s => s - } - - def getHoleInfo(hole: source.Hole)(using C: Context): Option[String] = for { - (outerTpe, outerEff) <- C.inferredTypeAndEffectOption(hole) - (innerTpe, innerEff) <- C.inferredTypeAndEffectOption(hole.stmts) - } yield pp"""| | Outside | Inside | - | |:------------- |:------------- | - | | `${outerTpe}` | `${innerTpe}` | - |""".stripMargin - - def allCaptures(src: Source)(using C: Context): List[(Tree, CaptureSet)] = - C.annotationOption(Annotations.CaptureForFile, src).getOrElse(Nil) - - // For now, we only show captures of function definitions and calls to box - def getInferredCaptures(range: kiama.util.Range)(using C: Context): List[(Position, CaptureSet)] = - val src = range.from.source - allCaptures(src).filter { - // keep only captures in the current range - case (t, c) => C.positions.getStart(t) match - case Some(p) => p.between(range.from, range.to) - case None => false - }.collect { - case (t: source.FunDef, c) => for { - pos <- C.positions.getStart(t) - } yield (pos, c) - case (t: source.DefDef, c) => for { - pos <- C.positions.getStart(t) - } yield (pos, c) - case (source.Box(None, block), _) if C.inferredCaptureOption(block).isDefined => for { - pos <- C.positions.getStart(block) - capt <- C.inferredCaptureOption(block) - } yield (pos, capt) - }.flatten - - def getInfoOf(sym: Symbol)(using C: Context): Option[SymbolInfo] = PartialFunction.condOpt(resolveCallTarget(sym)) { - - case b: ExternFunction => - SymbolInfo(b, "External function definition", Some(DeclPrinter(b)), None) - - case f: UserFunction if C.functionTypeOption(f).isDefined => - SymbolInfo(f, "Function", Some(DeclPrinter(f)), None) - - case f: Operation => - val ex = - pp"""|Effect operations, like `${f.name}` allow to express non-local control flow. - | - |Other than blocks, the implementation of an effect operation is provided by - |the closest - |```effekt - |try { EXPR } with ${f.interface.name} { def ${f.name}(...) => ... } - |``` - |that _dynamically_ surrounds the call-site `do ${f.name}(...)`. - | - |However, note that opposed to languages like Java, effect operations - |cannot be _captured_ in Effekt. That is, if the type of a function or block - |```effekt - |def f(): Int / {} - |``` - |does not mention the effect `${f.interface.name}`, then this effect will not be - |handled by the handler. This is important when considering higher-order functions. - |""".stripMargin - - SymbolInfo(f, "Effect operation", Some(DeclPrinter(f)), Some(ex)) - - case f: EffectAlias => - SymbolInfo(f, "Effect alias", Some(DeclPrinter(f)), None) - - case t: TypeAlias => - SymbolInfo(t, "Type alias", Some(DeclPrinter(t)), None) - - case t: ExternType => - SymbolInfo(t, "External type definition", Some(DeclPrinter(t)), None) - - case t: ExternInterface => - SymbolInfo(t, "External interface definition", Some(DeclPrinter(t)), None) - - case t: ExternResource => - SymbolInfo(t, "External resource definition", Some(DeclPrinter(t)), None) - - case c: Constructor => - val ex = pp"""|Instances of data types like `${c.tpe}` can only store - |_values_, not _blocks_. Hence, constructors like `${c.name}` only have - |value parameter lists, not block parameters. - |""".stripMargin - - SymbolInfo(c, s"Constructor of data type `${c.tpe}`", Some(DeclPrinter(c)), Some(ex)) - - case c: BlockParam => - val signature = C.functionTypeOption(c).map { tpe => pp"{ ${c.name}: ${tpe} }" } - - val ex = - s"""|Blocks, like `${c.name}`, are similar to functions in other languages. - | - |However, there is an important difference to functions (like in JavaScript): - |Blocks cannot be returned or stored in data types. - | - |They are thus more like blocks (hence the name) in Ruby that can only be - |yielded to. - |""".stripMargin - - SymbolInfo(c, "Block parameter", signature, Some(ex)) - - case c: ResumeParam => - val tpe = C.functionTypeOption(c) - val signature = tpe.map { tpe => pp"{ ${c.name}: ${tpe} }" } - val hint = tpe.map { tpe => pp"(i.e., `${tpe.result}`)" }.getOrElse { " " } - - val ex = - s"""|Resumptions are block parameters, implicitly bound - |when handling effect operations. - | - |The following three types have to be the same$hint: - |- the return type of the operation clause - |- the type of the handled expression enclosed by `try { EXPR } with EFFECT { ... }`, and - |- the return type of the resumption. - |""".stripMargin - - SymbolInfo(c, "Resumption", signature, Some(ex)) - - case c: VarBinder => - val signature = C.blockTypeOption(c).map(TState.extractType).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" } - - val ex = - s"""|Like in other languages, mutable variable binders like `${c.name}` - |can be modified (e.g., `${c.name} = VALUE`) by code that has `${c.name}` - |in its lexical scope. - | - |However, as opposed to other languages, variable binders in Effekt - |are stack allocated and show the right backtracking behavior in - |combination with effect handlers. - """.stripMargin - - SymbolInfo(c, "Mutable variable binder", signature, Some(ex)) - - case s: RegBinder => - - val ex = - pp"""|The region a variable is allocated into (${s.region}) not only affects its lifetime, but - |also its backtracking behavior in combination with continuation capture and - |resumption. - |""".stripMargin - - SymbolInfo(s, "Variable in region", None, Some(ex)) - - case c: ValueParam => - val signature = C.valueTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" } - SymbolInfo(c, "Value parameter", signature, None) - - case c: ValBinder => - val signature = C.valueTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" } - SymbolInfo(c, "Value binder", signature, None) - - case c: DefBinder => - val signature = C.blockTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" } - SymbolInfo(c, "Block binder", signature, None) - } -} diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala deleted file mode 100644 index 40420a814..000000000 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ /dev/null @@ -1,1038 +0,0 @@ -package effekt -package namer - -/** - * In this file we fully qualify source types, but use symbols directly - */ -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.context.assertions.* -import effekt.typer.Substitutions -import effekt.source.{ Def, Id, IdDef, IdRef, MatchGuard, ModuleDecl, Tree } -import effekt.symbols.* -import effekt.util.messages.ErrorMessageReifier -import effekt.symbols.scopes.* -import effekt.source.FeatureFlag.supportedByFeatureFlags - -import scala.annotation.tailrec -import scala.util.DynamicVariable - -/** - * The output of this phase: a mapping from source identifier to symbol - * - * It contains both, TermSymbols and TypeSymbols - * - * There is an important distinction between: - * - resolving: that is looking up symbols (might include storing the result into the symbolTable) - * - binding: that is adding a binding to the environment (lexical.Scope) - * - * - * TODO we could split resolution in three phases - * 1. only look at the declaration head of definitions in one scope - * 2. look at the bodies of effect declarations and type definitions - * 3. look into the bodies of functions - */ -object Namer extends Phase[Parsed, NameResolved] { - - val phaseName = "namer" - - def run(input: Parsed)(using Context): Option[NameResolved] = { - val Parsed(source, tree) = input - val mod = Module(tree, source) - Context.using(module = mod, focus = tree) { resolve(mod) } - Some(NameResolved(source, tree, mod)) - } - - /** Shadow stack of modules currently named, for detecction of cyclic imports */ - private val currentlyNaming: DynamicVariable[List[ModuleDecl]] = DynamicVariable(List()) - /** - * Run body in a context where we are currently naming `mod`. - * Produces a cyclic import error when this is already the case - */ - private def recursiveProtect[R](mod: ModuleDecl)(body: => R)(using Context): R = { - if (currentlyNaming.value.contains(mod)) { - val cycle = mod :: currentlyNaming.value.takeWhile(_ != mod).reverse - Context.abort( - pretty"""Cyclic import: ${mod.path} depends on itself, via:\n\t${cycle.map(_.path).mkString(" -> ")} -> ${mod.path}""") - } else { - currentlyNaming.withValue(mod :: currentlyNaming.value) { - body - } - } - } - - def resolve(mod: Module)(using Context): ModuleDecl = { - val Module(decl, src) = mod - val scope = scopes.toplevel(Context.module.namespace, builtins.rootBindings) - - Context.initNamerstate(scope) - - def importDependency(filePath: String) = - val included = Context.moduleOf(filePath) - - // Fully qualified: - // include "foo/bar/baz.effekt" as foo::bar::baz - scope.importAs(included.exports, included.namespace) - - // Bind the module itself: - // include "foo/bar/baz.effekt" as baz - scope.importAs(included.exports, List(included.name.name)) - - // Open it: - // import baz::* - scope.importAll(included.exports) - included - - // process the prelude (but don't if we are processing the prelude already) - val preludes = Context.config.prelude() - val isPrelude = preludes.contains(decl.path) - - val processedPreludes = if (!isPrelude) { - preludes.map(importDependency) - } else { Nil } - - // process all includes, updating the terms and types in scope - val includes = decl.includes collect { - case im @ source.Include(path) => - // [[recursiveProtect]] is called here so the source position is the recursive import - val mod = Context.at(im) { recursiveProtect(decl){ importDependency(path) } } - Context.annotate(Annotations.IncludedSymbols, im, mod) - mod - } - - Context.timed(phaseName, src.name) { resolveGeneric(decl) } - - // We only want to import each dependency once. - val allIncludes = (processedPreludes ++ includes).distinct - - Context.module.exports(allIncludes, scope.exports) - decl - } - - /** - * To allow mutually recursive definitions, here we only resolve the declarations, - * not the bodies of functions. - */ - def preresolve(d: Def)(using Context): Unit = Context.focusing(d) { - - case d @ source.ValDef(id, annot, binding) => - () - - case d @ source.VarDef(id, annot, binding) => - () - - case d @ source.RegDef(id, annot, region, binding) => - () - - case source.NamespaceDef(id, definitions) => - Context.namespace(id.name) { - definitions.foreach(preresolve) - } - - // allow recursive definitions of objects - case d @ source.DefDef(id, annot, source.New(source.Implementation(interface, clauses))) => - val tpe = Context.at(interface) { resolve(interface) } - val sym = Binder.DefBinder(Context.nameFor(id), Some(tpe), d) - Context.define(id, sym) - - case d @ source.DefDef(id, annot, block) => - () - - case f @ source.FunDef(id, tparams, vparams, bparams, annot, body) => - val uniqueId = Context.nameFor(id) - - // we create a new scope, since resolving type params introduces them in this scope - val sym = Context scoped { - val tps = tparams map resolve - // TODO resolve(ParamSection) loses structural information about value params and block params. - // we should refactor! - val vps = vparams map resolve - val bps = bparams map resolve - val ret = Context scoped { - Context.bindValues(vps) - Context.bindBlocks(bps) - annot map resolve - } - UserFunction(uniqueId, tps, vps, bps, ret.map { _._1 }, ret.map { _._2 }, f) - } - Context.define(id, sym) - - case source.InterfaceDef(id, tparams, ops) => - val effectName = Context.nameFor(id) - // we use the localName for effects, since they will be bound as capabilities - val effectSym = Context scoped { - val tps = tparams map resolve - // we do not resolve the effect operations here to allow them to refer to types that are defined - // later in the file - Interface(effectName, tps) - } - Context.define(id, effectSym) - - case source.TypeDef(id, tparams, tpe) => - val tps = Context scoped { tparams map resolve } - val alias = Context scoped { - tps.foreach { t => Context.bind(t) } - TypeAlias(Context.nameFor(id), tps, resolve(tpe)) - } - Context.define(id, alias) - - case source.EffectDef(id, tparams, effs) => - val tps = Context scoped { tparams map resolve } - val alias = Context scoped { - tps.foreach { t => Context.bind(t) } - EffectAlias(Context.nameFor(id), tps, resolve(effs)) - } - Context.define(id, alias) - - case source.DataDef(id, tparams, ctors) => - val typ = Context scoped { - val tps = tparams map resolve - // we do not resolve the constructors here to allow them to refer to types that are defined - // later in the file - DataType(Context.nameFor(id), tps) - } - Context.define(id, typ) - - case source.RecordDef(id, tparams, fields) => - lazy val sym: Record = { - val tps = Context scoped { tparams map resolve } - // we do not resolve the fields here to allow them to refer to types that are defined - // later in the file - Record(Context.nameFor(id), tps, null) - } - Context.define(id, sym) - - case source.ExternType(id, tparams) => - Context.define(id, Context scoped { - val tps = tparams map resolve - ExternType(Context.nameFor(id), tps) - }) - - case source.ExternInterface(id, tparams) => - Context.define(id, Context scoped { - val tps = tparams map resolve - ExternInterface(Context.nameFor(id), tps) - }) - - case source.ExternDef(capture, id, tparams, vparams, bparams, ret, bodies) => { - val name = Context.nameFor(id) - val capt = resolve(capture) - Context.define(id, Context scoped { - val tps = tparams map resolve - val vps = vparams map resolve - val bps = bparams map resolve - - val (tpe, eff) = Context scoped { - Context.bindBlocks(bps) - resolve(ret) - } - - ExternFunction(name, tps, vps, bps, tpe, eff, capt, bodies) - }) - } - - case source.ExternResource(id, tpe) => - val name = Context.nameFor(id) - val btpe = resolve(tpe) - val sym = ExternResource(name, btpe) - Context.define(id, sym) - Context.bindBlock(sym) - - case d @ source.ExternInclude(ff, path, Some(contents), _) => - () - - case d @ source.ExternInclude(ff, path, None, _) => - // only load include if it is required by the backend. - if (ff matches Context.compiler.supportedFeatureFlags) { - d.contents = Some(Context.contentsOf(path).getOrElse { - Context.abort(s"Missing include: ${path}") - }) - } else { - d.contents = None - } - } - - /** - * An effectful traversal with two side effects: - * 1) the passed environment is enriched with definitions - * 2) names are resolved using the environment and written to the table - */ - def resolveGeneric(tree: Tree)(using Context): Unit = Context.focusing(tree) { - - // (1) === Binding Occurrences === - case source.ModuleDecl(path, includes, definitions) => - definitions foreach { preresolve } - resolveAll(definitions) - - case source.DefStmt(d, rest) => - // resolve declarations but do not resolve bodies - preresolve(d) - // resolve bodies - resolveGeneric(d) - resolveGeneric(rest) - - case source.ValueParam(id, tpe) => - Context.define(id, ValueParam(Name.local(id), tpe.map(resolve))) - - case source.BlockParam(id, tpe) => - val p = BlockParam(Name.local(id), tpe.map { resolve }) - Context.define(id, p) - Context.bind(p.capture) - - case d @ source.ValDef(id, annot, binding) => - val tpe = annot.map(resolve) - resolveGeneric(binding) - Context.define(id, ValBinder(Context.nameFor(id), tpe, d)) - - - // Local mutable state - case d @ source.VarDef(id, annot, binding) => - val tpe = annot.map(resolve) - - resolveGeneric(binding) - val sym = VarBinder(Context.nameFor(id), tpe, d) - Context.define(id, sym) - Context.bind(sym.capture) - - // allocation into a region - case d @ source.RegDef(id, annot, region, binding) => - val tpe = annot.map(resolve) - val reg = Context.resolveTerm(region) match { - case t: BlockSymbol => t - case _ => Context.abort("Region needs to be a block.") - } - - resolveGeneric(binding) - val sym = RegBinder(Context.nameFor(id), tpe, reg, d) - - Context.define(id, sym) - - // already has been preresolved (to enable recursive definitions) - case d @ source.DefDef(id, annot, source.New(impl)) => - resolveGeneric(impl) - - case d @ source.DefDef(id, annot, binding) => - val tpe = annot.map(resolve) - resolveGeneric(binding) - Context.define(id, DefBinder(Context.nameFor(id), tpe, d)) - - // FunDef and InterfaceDef have already been resolved as part of the module declaration - case f @ source.FunDef(id, tparams, vparams, bparams, ret, body) => - val sym = f.symbol - Context scoped { - sym.tparams.foreach { p => Context.bind(p) } - Context.bindValues(sym.vparams) - Context.bindBlocks(sym.bparams) - - resolveGeneric(body) - } - - case f @ source.ExternDef(capture, id, tparams, vparams, bparams, ret, bodies) => - val sym = f.symbol - Context scoped { - sym.tparams.foreach { p => Context.bind(p) } - Context.bindValues(sym.vparams) - Context.bindBlocks(sym.bparams) - bodies.foreach { - case source.ExternBody.StringExternBody(ff, body) => body.args.foreach(resolveGeneric) - case source.ExternBody.EffektExternBody(ff, body) => resolveGeneric(body) - case u: source.ExternBody.Unsupported => u - } - } - - case source.InterfaceDef(id, tparams, operations) => - // symbol has already been introduced by the previous traversal - val interface = Context.symbolOf(id).asInterface - interface.operations = operations.map { - case op @ source.Operation(id, tparams, vparams, bparams, ret) => Context.at(op) { - val name = Context.nameFor(id) - - Context scoped { - // the parameters of the interface are in scope - interface.tparams.foreach { p => Context.bind(p) } - - val tps = tparams map resolve - - val resVparams = vparams map resolve - val resBparams = bparams map resolve - - // bring capture names in scope that are introduced by blockparameters - resBparams.map { b => Context.bind(b.capture) } - - // The type parameters of an effect op are: - // 1) all type parameters on the effect, followed by - // 2) the annotated type parameters on the concrete operation - val (result, effects) = resolve(ret) - - val op = Operation(name, interface.tparams ++ tps, resVparams, resBparams, result, effects, interface) - Context.define(id, op) - op - } - } - } - - case source.NamespaceDef(id, definitions) => - Context.namespace(id.name) { - definitions.foreach(resolveGeneric) - } - - case source.TypeDef(id, tparams, tpe) => () - case source.EffectDef(id, tparams, effs) => () - - // The type itself has already been resolved, now resolve constructors - case d @ source.DataDef(id, tparams, ctors) => - val data = d.symbol - data.constructors = ctors map { - case source.Constructor(id, tparams, ps) => - val constructor = Context scoped { - val name = Context.nameFor(id) - val tps = tparams map resolve - Constructor(name, data.tparams ++ tps, null, data) - } - Context.define(id, constructor) - constructor.fields = resolveFields(ps, constructor) - constructor - } - - // The record has been resolved as part of the preresolution step - case d @ source.RecordDef(id, tparams, fs) => - val record = d.symbol - val name = Context.nameFor(id) - val constructor = Constructor(name, record.tparams, null, record) - // we define the constructor on a copy to avoid confusion with symbols - Context.define(id.clone, constructor) - record.constructor = constructor - constructor.fields = resolveFields(fs, constructor) - - case source.ExternType(id, tparams) => () - case source.ExternInterface(id, tparams) => () - case source.ExternResource(id, tpe) => () - case source.ExternInclude(ff, path, _, _) => () - - case source.If(guards, thn, els) => - Context scoped { guards.foreach(resolve); resolveGeneric(thn) } - Context scoped { resolveGeneric(els) } - - case source.While(guards, block, default) => - Context scoped { guards.foreach(resolve); resolveGeneric(block) } - Context scoped { default foreach resolveGeneric } - - case source.BlockStmt(block) => - Context scoped { resolveGeneric(block) } - - case tree @ source.TryHandle(body, handlers) => - resolveAll(handlers) - - Context scoped { - - // bind all annotated capabilities - handlers.foreach { handler => - handler.capability.foreach { p => - Context.bindBlock(resolve(p)) - } - } - - resolveGeneric(body) - } - - case tree @ source.Region(name, body) => - val reg = BlockParam(Name.local(name.name), Some(builtins.TRegion)) - Context.define(name, reg) - Context scoped { - Context.bindBlock(reg) - resolveGeneric(body) - } - - case source.Implementation(interface, clauses) => - val eff: Interface = Context.at(interface) { resolve(interface).typeConstructor.asInterface } - - clauses.foreach { - case clause @ source.OpClause(op, tparams, vparams, bparams, ret, body, resumeId) => Context.at(clause) { - - // try to find the operation in the handled effect: - eff.operations.find { o => o.name.toString == op.name } map { opSym => - Context.assignSymbol(op, opSym) - } getOrElse { - Context.abort(pretty"Operation ${op} is not part of interface ${eff}.") - } - Context scoped { - val tps = tparams.map(resolve) - val vps = vparams.map(resolve) - val bps = bparams.map(resolve) - Context.bindValues(vps) - Context.bindBlocks(bps) - Context.define(resumeId, ResumeParam(Context.module)) - resolveGeneric(body) - } - } - } - - case source.MatchClause(pattern, guards, body) => - val ps = resolve(pattern) - Context scoped { - // variables bound by patterns are available in the guards. - ps.foreach { Context.bind } - guards.foreach { resolve } - - // wellformedness: only linear patterns - var names: Set[Name] = Set.empty - ps foreach { p => - if (names contains p.name) - Context.error(pp"Patterns have to be linear: names can only occur once, but ${p.name} shows up multiple times.") - - val cs = Context.allConstructorsFor(p.name) - if (cs.nonEmpty) { - Context.warning(pp"Pattern binds variable ${p.name}. Maybe you meant to match on equally named constructor of type ${cs.head.tpe}?") - } - names = names + p.name - } - - Context scoped { - resolveGeneric(body) - } - } - - case f @ source.BlockLiteral(tparams, vparams, bparams, stmt) => - Context scoped { - val tps = tparams map resolve - val vps = vparams map resolve - val bps = bparams map resolve - - Context.bindValues(vps) - Context.bindBlocks(bps) - - resolveGeneric(stmt) - } - - case source.Box(capt, block) => - capt foreach resolve - resolveGeneric(block) - - // (2) === Bound Occurrences === - - case source.Select(receiver, target) => - Context.panic("Cannot happen since Select is introduced later") - - case source.MethodCall(receiver, target, targs, vargs, bargs) => - resolveGeneric(receiver) - - // We are a bit context sensitive in resolving the method - Context.focusing(target) { _ => - receiver match { - case source.Var(id) => Context.resolveTerm(id) match { - // (foo: ValueType).bar(args) = Call(bar, foo :: args) - case symbol: ValueSymbol => - if !Context.resolveOverloadedFunction(target) - then Context.abort(pp"Cannot resolve function ${target}, called on a value receiver.") - - case symbol: RefBinder => - if !Context.resolveOverloadedFunction(target) - then Context.abort(pp"Cannot resolve function ${target}, called on a receiver that is a reference.") - - // (foo: BlockType).bar(args) = Invoke(foo, bar, args) - case symbol: BlockSymbol => - if !Context.resolveOverloadedOperation(target) - then Context.abort(pp"Cannot resolve operation ${target}, called on a receiver that is a computation.") - } - // (unbox term).bar(args) = Invoke(Unbox(term), bar, args) - case source.Unbox(term) => - if !Context.resolveOverloadedOperation(target) - then Context.abort(pp"Cannot resolve operation ${target}, called on an unboxed computation.") - - // expr.bar(args) = Call(bar, expr :: args) - case term => - if !Context.resolveOverloadedFunction(target) - then Context.abort(pp"Cannot resolve function ${target}, called on an expression.") - } - } - targs foreach resolve - resolveAll(vargs) - resolveAll(bargs) - - case source.Do(effect, target, targs, vargs, bargs) => - Context.resolveEffectCall(effect map resolve, target) - targs foreach resolve - resolveAll(vargs) - resolveAll(bargs) - - case source.Call(target, targs, vargs, bargs) => - Context.focusing(target) { - case source.IdTarget(id) => Context.resolveFunctionCalltarget(id) - case source.ExprTarget(expr) => resolveGeneric(expr) - } - targs foreach resolve - resolveAll(vargs) - resolveAll(bargs) - - case source.Var(id) => Context.resolveVar(id) - - case source.Assign(id, expr) => Context.resolveVar(id) match { - case _: VarBinder | _: RegBinder => resolveGeneric(expr) - case _: ValBinder | _: ValueParam => Context.abort(pretty"Can only assign to mutable variables, but ${id.name} is a constant.") - case y: Wildcard => Context.abort(s"Trying to assign to a wildcard, which is not allowed.") - case _ => Context.abort(s"Can only assign to mutable variables.") - } - - case tpe: source.ValueType => resolve(tpe) - case tpe: source.FunctionType => resolve(tpe) - - // THIS COULD ALSO BE A TYPE! - case id: IdRef => Context.resolveTerm(id) - - case other => resolveAll(other) - } - - // TODO move away - def resolveFields(params: List[source.ValueParam], constructor: Constructor)(using Context): List[Field] = { - val vps = Context scoped { - // Bind the type parameters - constructor.tparams.foreach { t => Context.bind(t) } - params map resolve - } - - (vps zip params) map { - case (paramSym, paramTree) => - val fieldId = paramTree.id.clone - val name = Context.nameFor(fieldId) - val fieldSym = Field(name, paramSym, constructor) - Context.define(fieldId, fieldSym) - fieldSym - } - } - - def resolveAll(obj: Any)(using Context): Unit = obj match { - case p: Product => p.productIterator.foreach { - case t: Tree => resolveGeneric(t) - case other => resolveAll(other) - } - case t: Iterable[t] => t.foreach { t => resolveAll(t) } - case leaf => () - } - - /** - * Resolve Parameters as part of resolving function signatures - * - * Since we annotate functions and effect declarations with resolved types, we need to - * resolve the parameters. - * - * Importantly, resolving them will *not* add the parameters as binding occurence in the current scope. - * This is done separately by means of `bind` - */ - def resolve(params: List[source.Param])(using Context): List[Param] = - params map resolve - - def resolve(param: source.Param)(using Context): Param = param match { - case p: source.ValueParam => resolve(p) - case p: source.BlockParam => resolve(p) - } - def resolve(p: source.ValueParam)(using Context): ValueParam = { - val sym = ValueParam(Name.local(p.id), p.tpe.map(resolve)) - Context.assignSymbol(p.id, sym) - sym - } - def resolve(p: source.BlockParam)(using Context): BlockParam = { - val sym: BlockParam = BlockParam(Name.local(p.id), p.tpe.map { resolve }) - Context.assignSymbol(p.id, sym) - sym - } - - /** - * Resolve pattern matching - * - * Returns the value params it binds - */ - def resolve(p: source.MatchPattern)(using Context): List[ValueParam] = p match { - case source.IgnorePattern() => Nil - case source.LiteralPattern(lit) => Nil - case source.AnyPattern(id) => - val p = ValueParam(Name.local(id), None) - Context.assignSymbol(id, p) - List(p) - case source.TagPattern(id, patterns) => - Context.resolveTerm(id) match { - case symbol: Constructor => () - case _ => Context.at(id) { - Context.error("Can only pattern match on constructors of data types.") - } - } - patterns.flatMap { resolve } - case source.MultiPattern(patterns) => - patterns.flatMap { resolve } - } - - def resolve(p: source.MatchGuard)(using Context): Unit = p match { - case MatchGuard.BooleanGuard(condition) => resolveGeneric(condition) - case MatchGuard.PatternGuard(scrutinee, pattern) => - resolveGeneric(scrutinee) - val ps = resolve(pattern) - ps.foreach { Context.bind } - } - - /** - * Resolve Types - * - * resolving a type means reconstructing the composite type (e.g. Effectful, ...) from - * symbols, instead of trees. - * - * We de-alias on-the-fly in Namer so that aliases can never show up again in the remaining compiler. - * This way error messages might suffer; however it simplifies the compiler a lot. - */ - def resolve(tpe: source.ValueType)(using Context): ValueType = resolvingType(tpe) { - case source.ValueTypeRef(id, args) => Context.resolveType(id) match { - case constructor: TypeConstructor => ValueTypeApp(constructor, args.map(resolve)) - case id: TypeVar => - if (args.nonEmpty) { - Context.abort(pretty"Type variables cannot be applied, but receieved ${args.size} arguments.") - } - ValueTypeRef(id) - case TypeAlias(name, tparams, tpe) => - val targs = args.map(resolve) - if (tparams.size != targs.size) { - Context.abort(pretty"Type alias ${name} expects ${tparams.size} type arguments, but got ${targs.size}.") - } - Substitutions.types(tparams, targs).substitute(tpe) - case other => Context.abort(pretty"Expected a value type, but got ${other}") - } - case source.ValueTypeTree(tpe) => - tpe - // TODO reconsider reusing the same set for terms and types... - case source.BoxedType(tpe, capt) => - BoxedType(resolve(tpe), resolve(capt)) - } - - def resolve(tpe: source.BlockType)(using Context): BlockType = resolvingType(tpe) { - case t: source.FunctionType => resolve(t) - case t: source.BlockTypeTree => t.eff - case t: source.BlockTypeRef => resolve(t) - } - - def resolve(funTpe: source.FunctionType)(using Context): FunctionType = resolvingType(funTpe) { - /** - * TODO share code with [[typer.Typer.makeFunctionType]] - */ - case source.FunctionType(tparams, vparams, bparams, ret, effects) => Context scoped { - val tps = tparams.map(resolve) - val vps = vparams.map(resolve) - - var cps: List[Capture] = Nil - val bps = bparams.map { - case (id, tpe) => - val name = id.map(Name.local).getOrElse(NoName) - val cap = CaptureParam(name) - cps = cps :+ cap - resolve(tpe) - } - - val effs = resolve(effects).distinct - CanonicalOrdering(effs.toList) foreach { eff => - val cap = CaptureParam(eff.name) - cps = cps :+ cap - } - - cps foreach Context.bind - - val res = resolve(ret) - - FunctionType(tps, cps, vps, bps, res, effs) - } - } - - def resolve(tpe: source.BlockTypeRef)(using Context): InterfaceType = resolvingType(tpe) { tpe => - resolveWithAliases(tpe) match { - case Nil => Context.abort("Expected a single interface type, not an empty effect set.") - case resolved :: Nil => resolved - case _ => Context.abort("Expected a single interface type, arbitrary effect aliases are not allowed.") - } - } - - /** - * Resolves an interface type, potentially with effect aliases on the top level - */ - def resolveWithAliases(tpe: source.BlockTypeRef)(using Context): List[InterfaceType] = Context.at(tpe) { - val resolved: List[InterfaceType] = tpe match { - case source.BlockTypeRef(id, args) => Context.resolveType(id) match { - case EffectAlias(name, tparams, effs) => - if (tparams.size != args.size) { - Context.abort(pretty"Effect alias ${name} expects ${tparams.size} type arguments, but got ${args.size}.") - } - val targs = args.map(resolve) - val subst = Substitutions.types(tparams, targs) - effs.toList.map(subst.substitute) - case i: BlockTypeConstructor => List(InterfaceType(i, args.map(resolve))) - case _ => Context.abort("Expected an interface type.") - } - } - resolved.foreach(kinds.wellformed) - resolved - } - - def resolve(tpe: source.Effects)(using Context): Effects = - Effects(tpe.effs.flatMap(resolveWithAliases).toSeq: _*) // TODO this otherwise is calling the wrong apply - - def resolve(e: source.Effectful)(using Context): (ValueType, Effects) = - (resolve(e.tpe), resolve(e.eff)) - - def resolve(capt: source.CaptureSet)(using Context): CaptureSet = Context.at(capt) { - val captResolved = CaptureSet(capt.captures.map { Context.resolveCapture }.toSet) - Context.annotateResolvedCapture(capt)(captResolved) - captResolved - } - - /** - * Resolves type variables, term vars are resolved as part of resolve(tree: Tree) - */ - def resolve(id: Id)(using Context): TypeParam = Context.at(id) { - val sym: TypeParam = TypeParam(Name.local(id)) - Context.define(id, sym) - sym - } - - def resolvingType[T <: source.Type, R <: symbols.Type](tpe: T)(f: T => R)(using Context): R = Context.at(tpe) { - val res = f(tpe) - Context.annotateResolvedType(tpe)(res) - kinds.wellformed(res) - res - } -} - -/** - * Environment Utils -- we use a mutable cell to express adding definitions more easily - */ -trait NamerOps extends ContextOps { Context: Context => - - /** - * The state of the namer phase - */ - private var scope: Scoping = _ - - private[namer] def initNamerstate(s: Scoping): Unit = scope = s - - /** - * Override the dynamically scoped `in` to also reset namer state. - * This is important since dependencies are resolved in a stack-like manner. - */ - override def in[T](block: => T): T = { - val before = scope - val result = super.in(block) - scope = before - result - } - - private[namer] def nameFor(id: IdDef): Name = scope.path match { - case Some(path) => QualifiedName(path, id.name) - case None => LocalName(id.name) - } - - // Name Binding and Resolution - // =========================== - private[namer] def define(id: Id, s: TermSymbol): Unit = { - assignSymbol(id, s) - scope.define(id.name, s) - } - - private[namer] def define(id: Id, s: TypeSymbol): Unit = { - assignSymbol(id, s) - scope.define(id.name, s) - } - - private[namer] def bind(s: Capture): Unit = bind(s.name.name, s) - - private[namer] def bind(name: String, s: Capture): Unit = scope.define(name, s) - - private[namer] def bind(s: TermSymbol): Unit = scope.define(s.name.name, s) - - private[namer] def bind(s: TypeSymbol): Unit = scope.define(s.name.name, s) - - private[namer] def bindParams(params: List[Param]) = - params.foreach { p => bind(p) } - - private[namer] def bindValues(params: List[ValueParam]) = - params.foreach { p => bind(p) } - - private[namer] def bindBlocks(params: List[BlockParam]) = - // bind the block parameter as a term - params.foreach { bindBlock } - - private[namer] def bindBlock(p: TrackedParam) = { - // bind the block parameter as a term - bind(p) - bind(p.capture) - } - private[namer] def bindBlock(name: String, p: TrackedParam) = { - scope.define(name, p) - scope.define(name, p.capture) - } - - /** - * Tries to find a _unique_ term symbol in the current scope under name id. - * Stores a binding in the symbol table - */ - private[namer] def resolveTerm(id: IdRef): TermSymbol = at(id) { - val sym = scope.lookupFirstTerm(id) - assignSymbol(id, sym) - sym - } - - private[namer] def allConstructorsFor(name: Name): Set[Constructor] = name match { - case NoName => panic("Constructor needs to be named") - case LocalName(name) => scope.allTermsFor(Nil, name).collect { - case c: Constructor => c - } - case QualifiedName(prefix, name) => scope.allTermsFor(prefix, name).collect { - case c: Constructor => c - } - } - - private[namer] def resolveAny(id: IdRef): Symbol = at(id) { - val sym = scope.lookupFirst(id.path, id.name) - assignSymbol(id, sym) - sym - } - - /** - * Resolves a potentially overloaded method target - */ - private[namer] def resolveOverloadedOperation(id: IdRef): Boolean = at(id) { - val syms = scope.lookupOperation(id.path, id.name) - - val syms2 = if (syms.isEmpty) scope.lookupFunction(id.path, id.name) else syms - - if (syms2.nonEmpty) { assignSymbol(id, CallTarget(syms2.asInstanceOf)); true } else { false } - } - - private[namer] def resolveOverloadedFunction(id: IdRef): Boolean = at(id) { - val syms = scope.lookupFunction(id.path, id.name) - - val syms2 = if (syms.isEmpty) scope.lookupOperation(id.path, id.name) else syms - - // lookup first block param and do not collect multiple since we do not (yet?) permit overloading on block parameters - val syms3 = if (syms2.isEmpty) List(scope.lookupFirstBlockParam(id.path, id.name)) else syms2 - - if (syms3.nonEmpty) { assignSymbol(id, CallTarget(syms3.asInstanceOf)); true } else { false } - } - - /** - * Resolves a potentially overloaded function call - */ - private[namer] def resolveFunctionCalltarget(id: IdRef): Unit = at(id) { - val candidates = scope.lookupOverloaded(id, term => !term.isInstanceOf[Operation]) - - resolveFunctionCalltarget(id, candidates) match { - case Left(value) => - assignSymbol(id, value) - case Right(blocks) => - if (blocks.isEmpty) { - val ops = scope.lookupOperation(id.path, id.name).flatten - - // Provide specific info messages for operations - ops.foreach { op => - info(pretty"There is an equally named effect operation ${op} of interface ${op.interface}. Use syntax `do ${id}()` to call it.") - } - - // Always abort with the generic message - abort(pretty"Cannot find a function named `${id}`.") - } - assignSymbol(id, CallTarget(blocks)) - } - } - - /** - * This function implements the scoping rules for blocks and values. - * - * 1) A single value in the tightest scope shadows blocks. - * i.e. { def foo() = ...; { val foo = ...; >>>foo<<< }} - * refers to the value foo - * - * 2) If the tighest scope contains blocks, then we will ignore all values - * and resolve to an overloaded target. - */ - @tailrec - private def resolveFunctionCalltarget(id: IdRef, candidates: List[Set[TermSymbol]]): Either[TermSymbol, List[Set[BlockSymbol]]] = - - // Mutable variables are treated as values, not as blocks. Maybe we should change the representation. - def isValue(t: TermSymbol): Boolean = t.isInstanceOf[ValueSymbol] || t.isInstanceOf[RefBinder] - def isBlock(t: TermSymbol): Boolean = t.isInstanceOf[BlockSymbol] && !t.isInstanceOf[RefBinder] - - candidates match { - case Nil => Right(Nil) - - // should not occur by construction - case terms :: rest if terms.isEmpty => resolveFunctionCalltarget(id, rest) - - case terms :: rest if terms.forall(isBlock) => - Right(candidates.map { scope => scope.collect { case b: BlockSymbol => b }}.filterNot(_.isEmpty)) - - case terms :: rest if terms.exists(isValue) => - if (terms.exists(isBlock)) { - panic("Should not happen by construction.") - } - // it is only a SINGLE value in the current scope => take it. It shadows blocks. - if (terms.size == 1) { - Left(terms.head) - } else { - abort(pretty"Multiple values with the same name $id in one scope. Values cannot be overloaded.") - } - - case _ => panic("Should not happen") - } - - /** - * Resolves a potentially overloaded field access - */ - private[namer] def resolveSelect(id: IdRef): Unit = at(id) { - val syms = scope.lookupOverloaded(id, term => term.isInstanceOf[Field]) - - if (syms.isEmpty) { - abort(pretty"Cannot resolve field access ${id}") - } - - assignSymbol(id, CallTarget(syms.asInstanceOf)) - } - - /** - * Resolves a potentially overloaded call to an effect - */ - private[namer] def resolveEffectCall(eff: Option[InterfaceType], id: IdRef): Unit = at(id) { - - val syms = eff match { - case Some(tpe) => - val interface = tpe.typeConstructor.asInterface - val operations = interface.operations.filter { op => op.name.name == id.name } - if (operations.isEmpty) Nil else List(operations.toSet) - case None => scope.lookupOperation(id.path, id.name) - } - - if (syms.isEmpty) { - abort(pretty"Cannot resolve effect operation ${id}") - } - - assignSymbol(id, CallTarget(syms.asInstanceOf)) - } - - /** - * Variables have to be resolved uniquely - */ - private[namer] def resolveVar(id: IdRef): TermSymbol = resolveTerm(id) match { - case b: BlockParam => b // abort("Blocks have to be fully applied and can't be used as values.") - case other => other - } - - private[namer] def resolveType(id: IdRef): TypeSymbol = at(id) { - val sym = scope.lookupType(id) - assignSymbol(id, sym) - sym - } - - private[namer] def resolveCapture(id: IdRef): Capture = at(id) { - val sym = scope.lookupCapture(id) - assignSymbol(id, sym) - sym - } - - private[namer] def scoped[R](block: => R): R = Context in { - scope.scoped { block } - } - - private[namer] def namespace[R](name: String)(block: => R): R = Context in { - scope.namespace(name) { block } - } -} diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 500a015d8..e7f760eb2 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -1,8 +1,6 @@ package effekt import effekt.context.Context -import effekt.source.* -import effekt.util.{ VirtualSource } import effekt.util.messages.ParseError import kiama.parsing.{ Failure, Input, NoSuccess, ParseResult, Parsers, Success } import kiama.util.{ Position, Positions, Range, Source, StringSource } @@ -16,25 +14,6 @@ case class Template[+T](strings: List[String], args: List[T]) { def map[R](f: T => R): Template[R] = Template(strings, args.map(f)) } -object Parser extends Phase[Source, Parsed] { - - val phaseName = "parser" - - def run(source: Source)(implicit C: Context): Option[PhaseResult.Parsed] = source match { - case VirtualSource(decl, _) => Some(decl) - case source => - //println(s"parsing ${source.name}") - Context.timed(phaseName, source.name) { - val lexer = effekt.lexer.Lexer(source) - val tokens = lexer.lex() - val parser = RecursiveDescent(C.positions, tokens, source) - parser.parse(Input(source, 0)) - } - } map { tree => - Parsed(source, tree) - } -} - class EffektLexers(positions: Positions) extends Parsers(positions) { @@ -179,26 +158,6 @@ class EffektLexers(positions: Positions) extends Parsers(positions) { p.flatMap(_ => error(errorMessage)) } - implicit class PositionOps[T](val self: T) { - def withPositionOf(other: Any): self.type = { dupAll(other, self); self } - - private def dupIfEmpty(from: Any, to: Any): Unit = - if (positions.getStart(to).isEmpty) { positions.dupPos(from, to) } - - private def dupAll(from: Any, to: Any): Unit = to match { - case t: Tree => - dupIfEmpty(from, t) - t.productIterator.foreach { dupAll(from, _) } - case t: Iterable[t] => t.foreach { dupAll(from, _) } - case _ => () - } - - def range: Option[Range] = for { - from <- positions.getStart(self) - to <- positions.getFinish(self) - } yield SourceRange(from, to) - } - trait Range { def ++(other: Range): Range } @@ -216,30 +175,8 @@ class EffektLexers(positions: Positions) extends Parsers(positions) { } } - /** - * Check positions of all subtrees, stopping at trees that already have positions - */ - def checkPosition(t: Tree): Range = t.range.getOrElse { - t.productIterator.map(checkPositions).fold(EmptyRange)(_ ++ _) match { - case EmptyRange => sys error s"Missing position for ${ t }. Cannot guess the source position from its children." - case rng @ SourceRange(from, to) => - positions.setStart(t, from) - positions.setFinish(t, to) - rng - } - } - - def checkPositions(t: Any): Range = t match { - case t: Tree => checkPosition(t) - case t: Iterable[t] => t.map(checkPositions).fold(EmptyRange)(_ ++ _) - case _ => EmptyRange - } - override implicit def memo[T](parser: => Parser[T]): PackratParser[T] = - new PackratParser[T](parser.map { t => - checkPositions(t) - t - }) + new PackratParser[T](parser) def parseAll[T](p: Parser[T], input: String): ParseResult[T] = parseAll(p, StringSource(input, "input-string")) diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala deleted file mode 100644 index c02e8ff35..000000000 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ /dev/null @@ -1,1515 +0,0 @@ -package effekt - -import effekt.lexer.* -import effekt.lexer.TokenKind.{ `::` as PathSep, * } -import effekt.source.* -import effekt.context.Context -import kiama.parsing.{ ParseResult, Input } -import kiama.util.{ Positions, Source, StringSource, Range, Position } - -import scala.annotation.{ tailrec, targetName } -import scala.util.matching.Regex -import scala.language.implicitConversions -import scala.util.boundary -import scala.util.boundary.break - - -case class Fail(message: String, position: Int) extends Throwable(null, null, false, false) -case class SoftFail(message: String, positionStart: Int, positionEnd: Int) - -class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) { - - import scala.collection.mutable.ListBuffer - - val softFails: ListBuffer[SoftFail] = ListBuffer[SoftFail]() - - def parse(input: Input)(using C: Context): Option[ModuleDecl] = - - try { - //println(input.tokens) - //val before = System.currentTimeMillis() - val res = Some(program()) - //val after = System.currentTimeMillis() - //println(s"${input.source.name}: ${after - before}ms") - - // Report soft fails - softFails.foreach { - case SoftFail(msg, from, to) => - val source = input.source - val fromPos = source.offsetToPosition(tokens(from).start) - val toPos = source.offsetToPosition(tokens(to).end) - val range = Range(fromPos, toPos) - C.report(effekt.util.messages.ParseError(msg, Some(range))) - } - - if (softFails.isEmpty) { res } else { None } - } catch { - case Fail(msg, pos) => - val source = input.source - val range = tokens.lift(pos) match { - case Some(value) => - val from = source.offsetToPosition(value.start) - val to = source.offsetToPosition(value.end + 1) - Some(Range(from, to)) - case None => - val pos = Position(0, 0, source) - Some(Range(pos, pos)) - } - - C.report(effekt.util.messages.ParseError(msg, range)) // fix error reporting - None - } - - // here we need to convert to kiamas error format since REPL is defined in kiama - def parseRepl(input: Input)(using C: Context): ParseResult[Tree] = - try { kiama.parsing.Success(repl(), input) } catch { - case Fail(msg, pos) => kiama.parsing.Error(msg, input.copy(offset = pos)) - } - - - // Interfacing with the token stream - // --------------------------------- - - // always points to the latest non-space position - var position: Int = 0 - - extension(token: Token) def failOnErrorToken: Token = token.kind match { - case TokenKind.Error(err) => fail(err.msg) - case _ => token - } - - def peek: Token = tokens(position).failOnErrorToken - - /** - * Negative lookahead - */ - def lookbehind(offset: Int): Token = - tokens(position - offset) - - /** - * Peeks n tokens ahead of the current one. - */ - def peek(offset: Int): Token = - @tailrec - def go(position: Int, offset: Int): Token = - if position >= tokens.length then fail("Unexpected end of file") - - tokens(position).failOnErrorToken match { - case token if isSpace(token.kind) => go(position + 1, offset) - case token if offset <= 0 => token - case _ => go(position + 1, offset - 1) - } - - go(position, offset) - - // the previously consumed token - var previous = tokens(position) - - def peek(kind: TokenKind): Boolean = - peek.kind == kind - def peek(offset: Int, kind: TokenKind): Boolean = - peek(offset).kind == kind - - def hasNext(): Boolean = position < tokens.length - def next(): Token = - val t = tokens(position).failOnErrorToken - skip() - t - - /** - * Skips the current token and then all subsequent whitespace - */ - def skip(): Unit = - previous = tokens(position) - position += 1; - spaces() - - def isSpace(kind: TokenKind): Boolean = - kind match { - case TokenKind.Space | TokenKind.Comment(_) | TokenKind.Newline => true - case _ => false - } - - @tailrec - final def spaces(): Unit = if hasNext() then peek.kind match { - case kind if isSpace(kind) => position += 1; spaces() - case _ => () - } - - def consume(kind: TokenKind): Unit = - // if !hasNext() then fail(s"Expected ${kind}, but reached end of file") - val positionBefore = position - val t = next() - - if (t.kind != kind) { - // we need to fail at the position before consuming - position = positionBefore - fail(s"Expected ${explain(kind)} but got ${explain(t.kind)}") - } - - - /* The actual parser itself - * ------------------------ - * We use the following naming conventions for nonterminals: - * - * - maybe[...]s: zero or more times - * - [...]s: one or more times - * - [...]opt: optional type annotation - * - * Furthermore, we try to adhere to the following conventions: - * - Use `backtrack` with caution and as a last resort. Try to disambiguate the grammar by using `peek` and also try to - * only use `backtrack` on "shallow" non-terminals, that is, not on a non-terminal containing an expression, - * a statement or a type. - * - For the same reason, there is no function `manyWhile[T](p: => T): List[T]` but only - * `manyWhile[T](p: => T, predicate: => Boolean): List[T]` as this one does not use backtracking. - * - Use `fail` for reporting errors. - * - Functions consuming tokens have an empty parameter list `()`, functions that do not, have no parameter list (e.g. peek) - * - All non-terminals are to use the `nonterminal` function for keeping track of positions. - */ - - // tokens that delimit a statement - def returnPosition: Boolean = peek(`}`) || peek(`case`) || peek(`}>`) || peek(EOF) - - /** - * Statements - */ - def stmts(): Stmt = - nonterminal: - peek.kind match { - case `val` => valStmt() - case _ if isDefinition => DefStmt(definition(), semi() ~> stmts()) - case `with` => withStmt() - case `var` => DefStmt(varDef(), semi() ~> stmts()) - case `return` => - val result = `return` ~> Return(expr()) - maybeSemi() - result - case _ => - val e = expr() - semi() - if returnPosition then Return(e) - else ExprStmt(e, stmts()) - } - - // ATTENTION: here the grammar changed (we added `with val` to disambiguate) - // with val (: )? = ; - // with val ( (: )?...) = - // with ; - def withStmt(): Stmt = `with` ~> peek.kind match { - case `val` => - val params = (`val` ~> peek.kind match { - case `(` => valueParamsOpt() - case _ => List(valueParamOpt()) // TODO copy position - }) - desugarWith(params, Nil, `=` ~> expr(), semi() ~> stmts()) - - case `def` => - val params = (`def` ~> peek.kind match { - case `{` => blockParamsOpt() - case _ => List(blockParamOpt()) // TODO copy position - }) - desugarWith(Nil, params, `=` ~> expr(), semi() ~> stmts()) - - case _ => desugarWith(Nil, Nil, expr(), semi() ~> stmts()) - } - - def desugarWith(vparams: List[ValueParam], bparams: List[BlockParam], call: Term, body: Stmt): Stmt = call match { - case m@MethodCall(receiver, id, tps, vargs, bargs) => - Return(MethodCall(receiver, id, tps, vargs, bargs :+ (BlockLiteral(Nil, vparams, bparams, body)))) - case c@Call(callee, tps, vargs, bargs) => - Return(Call(callee, tps, vargs, bargs :+ (BlockLiteral(Nil, vparams, bparams, body)))) - case Var(id) => - val tgt = IdTarget(id) - Return(Call(tgt, Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body)) :: Nil)) - case Do(effect, id, targs, vargs, bargs) => - Return(Do(effect, id, targs, vargs, bargs :+ BlockLiteral(Nil, vparams, bparams, body))) - case term => - Return(Call(ExprTarget(term), Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body)) :: Nil)) - } - - def maybeSemi(): Unit = if isSemi then semi() - def isSemi: Boolean = peek.kind match { - // \n ; while - // - case `;` => true - // foo } - // ^ - case t if returnPosition => true - - // \n while - // ^ - case _ => lookbehind(1).kind == Newline - } - def semi(): Unit = peek.kind match { - // \n ; while - // - case `;` => consume(`;`) - // foo } - // ^ - case t if returnPosition => () - - // \n while - // ^ - case _ if lookbehind(1).kind == Newline => () - - case _ => fail("Expected ;") - } - - def stmt(): Stmt = - nonterminal: - if peek(`{`) then braces { BlockStmt(stmts()) } - else when(`return`) { Return(expr()) } { Return(expr()) } - - - /** - * Main entry point for the repl. - */ - def repl(): Tree = - nonterminal: - // skip spaces at the start - spaces() - val res = peek.kind match { - case t if isToplevel => toplevel() - case `import` => includeDecl() - case _ => expr() - } - if peek(`EOF`) then res else fail("Unexpected end of input") - - /** - * Main entry point - */ - def program(): ModuleDecl = - nonterminal: - // skip spaces at the start - spaces() - val res = ModuleDecl(moduleDecl(), manyWhile(includeDecl(), `import`), toplevelDefs()) - if peek(`EOF`) then res else fail("Unexpected end of input") - // failure("Required at least one top-level function or effect definition") - - def moduleDecl(): String = - when(`module`) { moduleName() } { defaultModulePath } - - // we are purposefully not using File here since the parser needs to work both - // on the JVM and in JavaScript - def defaultModulePath: String = - val baseWithExt = source.name.split("[\\\\/]").last - baseWithExt.split('.').head - - - def includeDecl(): Include = - nonterminal: - Include(`import` ~> moduleName()) - - def moduleName(): String = - some(ident, `/`).mkString("/") - - def isToplevel: Boolean = peek.kind match { - case `val` | `fun` | `def` | `type` | `effect` | `namespace` | - `extern` | `effect` | `interface` | `type` | `record` => true - case _ => false - } - - def toplevel(): Def = - nonterminal: - peek.kind match { - case `val` => valDef() - case `def` => defDef() - case `interface` => interfaceDef() - case `type` => typeOrAliasDef() - case `record` => recordDef() - case `extern` => externDef() - case `effect` => effectOrOperationDef() - case `namespace` => namespaceDef() - case `var` => fail("Mutable variable declarations are currently not supported on the toplevel.") - case _ => fail("Expected a top-level definition") - } - - def toplevelDefs(): List[Def] = - peek.kind match { - case `namespace` => - consume(`namespace`) - val id = idDef() - peek.kind match { - case `{` => - val defs = braces(toplevelDefs()) - val df = toplevelDefs() - NamespaceDef(id, defs) :: df - case _ => - val defs = toplevelDefs() - List(NamespaceDef(id, defs)) - } - case _ => - if (isToplevel) toplevel() :: toplevelDefs() - else Nil - } - - def toplevels(): List[Def] = - nonterminal: - manyWhile(toplevel(), isToplevel) - - def isDefinition: Boolean = peek.kind match { - case `val` | `def` | `type` | `effect` | `namespace` => true - case `extern` | `effect` | `interface` | `type` | `record` => - val kw = peek.kind - fail(s"Only supported on the toplevel: ${kw.toString} declaration.") - case _ => false - } - - def definition(): Def = - nonterminal: - peek.kind match { - case `val` => valDef() - case `def` => defDef() - case `type` => typeOrAliasDef() - case `effect` => effectDef() - case `namespace` => namespaceDef() - // TODO - // (`extern` | `effect` | `interface` | `type` | `record`).into { (kw: String) => - // failure(s"Only supported on the toplevel: ${kw} declaration.") - // } - case _ => fail("Expected definition") - } - - def definitions(): List[Def] = - nonterminal: - manyWhile(definition(), isDefinition) - - def functionBody: Stmt = stmt() // TODO error context: "the body of a function definition" - - def valDef(): Def = - nonterminal: - ValDef(`val` ~> idDef(), maybeValueTypeAnnotation(), `=` ~> stmt()) - - /** - * In statement position, val-definitions can also be destructing: - * i.e. val (l, r) = point(); ... - */ - def valStmt(): Stmt = - nonterminal: - val startMarker = nonterminal { new {} } - def simpleLhs() = backtrack { - `val` ~> idDef() ~ maybeValueTypeAnnotation() <~ `=` - } map { - case id ~ tpe => - val binding = stmt() - val valDef = ValDef(id, tpe, binding).withRangeOf(startMarker, binding) - DefStmt(valDef, { semi(); stmts() }) - } - def matchLhs() = - `val` ~> matchPattern() ~ manyWhile(`and` ~> matchGuard(), `and`) <~ `=` match { - case AnyPattern(id) ~ Nil => - val binding = stmt() - val valDef = ValDef(id, None, binding).withRangeOf(startMarker, binding) - DefStmt(valDef, { semi(); stmts() }) - case p ~ guards => - val sc = expr() - val default = when(`else`) { Some(stmt()) } { None } - val body = semi() ~> stmts() - val clause = MatchClause(p, guards, body).withRangeOf(p, sc) - val matching = Match(List(sc), List(clause), default).withRangeOf(startMarker, sc) - Return(matching) - } - - simpleLhs() getOrElse matchLhs() - - - def varDef(): Def = - nonterminal: - (`var` ~> idDef()) ~ maybeValueTypeAnnotation() ~ when(`in`) { Some(idRef()) } { None } ~ (`=` ~> stmt()) match { - case id ~ tpe ~ Some(reg) ~ expr => RegDef(id, tpe, reg, expr) - case id ~ tpe ~ None ~ expr => VarDef(id, tpe, expr) - } - - def defDef(): Def = - nonterminal: - val id = consume(`def`) ~> idDef() - - def isBlockDef: Boolean = peek(`:`) || peek(`=`) - - if isBlockDef then - // (: )? `=` - DefDef(id, maybeBlockTypeAnnotation(), `=` ~> expr()) - else - // [...](...) {...} `=` > - val (tps, vps, bps) = params() - FunDef(id, tps, vps, bps, maybeReturnAnnotation(), `=` ~> stmt()) - - - // right now: data type definitions (should be renamed to `data`) and type aliases - def typeOrAliasDef(): Def = - nonterminal: - val id ~ tps = (`type` ~> idDef()) ~ maybeTypeParams() - - peek.kind match { - case `=` => `=` ~> TypeDef(id, tps, valueType()) - case _ => braces { DataDef(id, tps, manyWhile({ constructor() <~ semi() }, !peek(`}`))) } - } - - def recordDef(): Def = - nonterminal: - RecordDef(`record` ~> idDef(), maybeTypeParams(), valueParams()) - - def constructor(): Constructor = - nonterminal: - Constructor(idDef(), maybeTypeParams(), valueParams()) - - // On the top-level both - // effect Foo = {} - // and - // effect Foo(): Int - // are allowed. Here we simply backtrack, since effect definitions shouldn't be - // very long and cannot be nested. - def effectOrOperationDef(): Def = - nonterminal: - backtrack { effectDef() } getOrElse { operationDef() } - - def effectDef(): Def = - nonterminal: - // effect = - EffectDef(`effect` ~> idDef(), maybeTypeParams(), `=` ~> effects()) - - // effect [...](...): ... - def operationDef(): Def = - nonterminal: - `effect` ~> operation() match { - case op @ Operation(id, tps, vps, bps, ret) => - InterfaceDef(IdDef(id.name) withPositionOf op, tps, List(Operation(id, Nil, vps, bps, ret) withPositionOf op)) - } - - def operation(): Operation = - nonterminal: - idDef() ~ params() ~ returnAnnotation() match { - case id ~ (tps, vps, bps) ~ ret => Operation(id, tps, vps, bps, ret) - } - - def interfaceDef(): InterfaceDef = - nonterminal: - InterfaceDef(`interface` ~> idDef(), maybeTypeParams(), `{` ~> manyWhile(`def` ~> operation(), `def`) <~ `}`) - - def namespaceDef(): Def = - nonterminal: - consume(`namespace`) - val id = idDef() - // namespace foo { * } - if peek(`{`) then braces { NamespaceDef(id, definitions()) } - // namespace foo - // * - else { semi(); NamespaceDef(id, definitions()) } - - - def externDef(): Def = - nonterminal: - { peek(`extern`); peek(1).kind } match { - case `type` => externType() - case `interface` => externInterface() - case `resource` => externResource() - case `include` => externInclude() - // extern """...""" - case s: Str => externString() - case Ident(_) | `pure` => - // extern IDENT def ... - if (peek(2, `def`)) externFun() - // extern IDENT """...""" - else externString() - // extern {...} def ... - case _ => externFun() - } - - def featureFlag(): FeatureFlag = - next().kind match { - case Ident("default") => FeatureFlag.Default - case Ident(flag) => FeatureFlag.NamedFeatureFlag(flag) - case _ => fail("Expected identifier") - } - - def maybeFeatureFlag(): FeatureFlag = - nonterminal: - backtrack(featureFlag()).getOrElse(FeatureFlag.Default) - - def externType(): Def = - nonterminal: - ExternType(`extern` ~> `type` ~> idDef(), maybeTypeParams()) - def externInterface(): Def = - nonterminal: - ExternInterface(`extern` ~> `interface` ~> idDef(), maybeTypeParams()) - def externResource(): Def = - nonterminal: - ExternResource(`extern` ~> `resource` ~> idDef(), blockTypeAnnotation()) - def externInclude(): Def = - nonterminal: - `extern` ~> `include` ~> ExternInclude(maybeFeatureFlag(), path().stripPrefix("\"").stripSuffix("\""), None) - - def externString(): Def = - nonterminal: - consume(`extern`) - val ff = maybeFeatureFlag() - next().kind match { - case Str(contents, _) => ExternInclude(ff, "", Some(contents)) - case _ => fail("Expected string literal.") - } - - def externFun(): Def = - nonterminal: - ((`extern` ~> maybeExternCapture()) ~ (`def` ~> idDef()) ~ params() ~ (returnAnnotation() <~ `=`)) match { - case capt ~ id ~ (tps, vps, bps) ~ ret => - val bodies = manyWhile(externBody(), isExternBodyStart) - ExternDef(capt, id, tps, vps, bps, ret, bodies) - } - - def externBody(): ExternBody = - nonterminal: - peek.kind match { - case _: Ident => peek(1).kind match { - case `{` => ExternBody.EffektExternBody(featureFlag(), `{` ~> stmts() <~ `}`) - case _ => ExternBody.StringExternBody(maybeFeatureFlag(), template()) - } - case _ => ExternBody.StringExternBody(maybeFeatureFlag(), template()) - } - - private def isExternBodyStart: Boolean = - peek.kind match { - case Str(_, _) | Ident(_) | `{` => true - case _ => false - } - - def template(): Template[Term] = - nonterminal: - // TODO handle case where the body is not a string, e.g. - // Expected an extern definition, which can either be a single-line string (e.g., "x + y") or a multi-line string (e.g., """...""") - val first = string() - val (exprs, strs) = manyWhile((`${` ~> expr() <~ `}`, string()), `${`).unzip - Template(first :: strs, exprs) - - def maybeExternCapture(): CaptureSet = - nonterminal: - if peek(`{`) || peek(`pure`) || isVariable then externCapture() - else CaptureSet(List(IdRef(List("effekt"), "io"))) - - def externCapture(): CaptureSet = - nonterminal: - if peek(`{`) then captureSet() - else if peek(`pure`) then `pure` ~> CaptureSet(Nil) - else CaptureSet(List(idRef())) - - def path(): String = - nonterminal: - next().kind match { - case Str(s, false) => s - case _ => fail("Expected path as string literal.") - } - - def string(): String = - nonterminal: - next().kind match { - case Str(s, _) => s - case _ => fail("Expected string literal.") - } - - - def maybeValueTypeAnnotation(): Option[ValueType] = - nonterminal: - if peek(`:`) then Some(valueTypeAnnotation()) else None - - def maybeBlockTypeAnnotation(): Option[BlockType] = - nonterminal: - if peek(`:`) then Some(blockTypeAnnotation()) else None - - def maybeReturnAnnotation(): Option[Effectful] = - nonterminal: - when(`:`) { Some(effectful()) } { None } - - def returnAnnotation(): Effectful = - if peek(`:`) then `:` ~> effectful() - else fail("Expected return type annotation") - - def valueTypeAnnotation(): ValueType = - if peek(`:`) then `:` ~> valueType() - else fail("Expected a type annotation") - - def blockTypeAnnotation(): BlockType = - if peek(`:`) then `:` ~> blockType() - else fail("Expected a type annotation") - - def expr(): Term = peek.kind match { - case _ => matchExpr() - } - - def ifExpr(): Term = - nonterminal: - If(`if` ~> parens { matchGuards() }, - stmt(), - when(`else`) { stmt() } { Return(UnitLit()) }) - - def whileExpr(): Term = - nonterminal: - While(`while` ~> parens { matchGuards() }, - stmt(), - when(`else`) { Some(stmt()) } { None }) - - def doExpr(): Term = - nonterminal: - (`do` ~> idRef()) ~ arguments() match { - case id ~ (targs, vargs, bargs) => Do(None, id, targs, vargs, bargs) - } - - /* - ::= try { } + - ::= with ( :)? - { + } - */ - def tryExpr(): Term = - nonterminal: - `try` ~> stmt() ~ someWhile(handler(), `with`) match { - case s ~ hs => TryHandle(s, hs) - } - - def regionExpr(): Term = - nonterminal: - Region(`region` ~> idDef(), stmt()) - - def boxExpr(): Term = - nonterminal: - val captures = `box` ~> backtrack(captureSet()) - val expr = if (peek(`{`)) functionArg() - else if (peek(`new`)) newExpr() - else callExpr() - Box(captures, expr) - - - // TODO deprecate - def funExpr(): Term = - nonterminal: - `fun` ~> Box(None, BlockLiteral(Nil, valueParams(), Nil, braces { stmts() })) - // TODO positions - - def unboxExpr(): Term = - nonterminal: - Unbox(`unbox` ~> expr()) - - def newExpr(): Term = - nonterminal: - New(`new` ~> implementation()) - - def handler(): Handler = - nonterminal: - `with` ~> backtrack(idDef() <~ `:`) ~ implementation() match { - case capabilityName ~ impl => - val capability = capabilityName map { name => BlockParam(name, Some(impl.interface)): BlockParam } - Handler(capability, impl) - } - - // This nonterminal uses limited backtracking: It parses the interface type multiple times. - def implementation(): Implementation = - nonterminal: - // Interface[...] {} - def emptyImplementation() = backtrack { Implementation(interfaceType(), `{` ~> Nil <~ `}`) } - - // Interface[...] { def = ... } - def interfaceImplementation() = backtrack { - val tpe = interfaceType() - consume(`{`) - if !peek(`def`) then fail("Expected at least one operation definition to implement this interface.") - tpe - } map { tpe => - Implementation(tpe, manyWhile(opClause(), `def`)) <~ `}` - } - - // Interface[...] { () => ... } - // Interface[...] { case ... => ... } - def operationImplementation() = idRef() ~ maybeTypeArgs() ~ implicitResume ~ functionArg() match { - case (id ~ tps ~ k ~ BlockLiteral(_, vps, bps, body)) => - val synthesizedId = IdRef(Nil, id.name).withPositionOf(id) - val interface = BlockTypeRef(id, tps).withPositionOf(id): BlockTypeRef - val operation = OpClause(synthesizedId, Nil, vps, bps, None, body, k).withRangeOf(id, body) - Implementation(interface, List(operation)) - } - - emptyImplementation() orElse interfaceImplementation() getOrElse operationImplementation() - - def opClause(): OpClause = - nonterminal: - (`def` ~> idRef()) ~ paramsOpt() ~ maybeReturnAnnotation() ~ (`=` ~> stmt()) match { - case id ~ (tps, vps, bps) ~ ret ~ body => - if (isSemi) { - semi() - - val startPosition = position - - if (!peek(`}`) && !peek(`def`) && !peek(EOF)) { - // consume until the next `def` or `}` or EOF - while (!peek(`}`) && !peek(`def`) && !peek(EOF)) { - next() - } - - val endPosition = position - val msg = "Unexpected tokens after operation definition. Expected either a new operation definition or the end of the implementation." - softFail(msg, startPosition, endPosition) - } - } - - // TODO the implicitResume needs to have the correct position assigned (maybe move it up again...) - OpClause(id, tps, vps, bps, ret, body, implicitResume) - } - - def implicitResume: IdDef = - nonterminal: - IdDef("resume") - - def matchClause(): MatchClause = - nonterminal: - val patterns = `case` ~> some(matchPattern, `,`) - val pattern = patterns match { - case List(pat) => pat - case pats => MultiPattern(pats) - } - MatchClause( - pattern, - manyWhile(`and` ~> matchGuard(), `and`), - // allow a statement enclosed in braces or without braces - // both is allowed since match clauses are already delimited by `case` - `=>` ~> (if (peek(`{`)) { stmt() } else { stmts() }) - ) - - def matchGuards() = - nonterminal: - some(matchGuard, `and`) - - def matchGuard(): MatchGuard = - nonterminal: - expr() ~ when(`is`) { Some(matchPattern()) } { None } match { - case e ~ Some(p) => MatchGuard.PatternGuard(e, p) - case e ~ None => MatchGuard.BooleanGuard(e) - } - - def matchPattern(): MatchPattern = - nonterminal: - peek.kind match { - case `__` => skip(); IgnorePattern() - case _ if isVariable => - idRef() match { - case id if peek(`(`) => TagPattern(id, many(matchPattern, `(`, `,`, `)`)) - case IdRef(Nil, name) => AnyPattern(IdDef(name)) // TODO positions - case IdRef(_, name) => fail("Cannot use qualified names to bind a pattern variable") - } - case _ if isVariable => - AnyPattern(idDef()) - case _ if isLiteral => LiteralPattern(literal()) - case `(` => some(matchPattern, `(`, `,`, `)`) match { - case p :: Nil => fail("Pattern matching on tuples requires more than one element") - case ps => TagPattern(IdRef(List("effekt"), s"Tuple${ps.size}"), ps) - } - case _ => fail("Expected pattern") - } - - def matchExpr(): Term = - nonterminal: - var sc = assignExpr() - while (peek(`match`)) { - val clauses = `match` ~> braces { manyWhile(matchClause(), `case`) } - val default = when(`else`) { Some(stmt()) } { None } - sc = Match(List(sc), clauses, default) - } - sc - - def assignExpr(): Term = - nonterminal: - orExpr() match { - case x @ Term.Var(id) => when(`=`) { Assign(id, expr()) } { x } - case other => other - } - - def orExpr(): Term = infix(andExpr, `||`) - def andExpr(): Term = infix(eqExpr, `&&`) - def eqExpr(): Term = infix(relExpr, `===`, `!==`) - def relExpr(): Term = infix(addExpr, `<=`, `>=`, `<`, `>`) - def addExpr(): Term = infix(mulExpr, `++`, `+`, `-`) - def mulExpr(): Term = infix(callExpr, `*`, `/`) - - inline def infix(nonTerminal: () => Term, ops: TokenKind*): Term = - nonterminal: - var left = nonTerminal() - while (ops.contains(peek.kind)) { - val op = next().kind - val right = nonTerminal() - left = binaryOp(left, op, right).withRangeOf(left, right) - } - left - - // === AST Helpers === - - private def binaryOp(lhs: Term, op: TokenKind, rhs: Term): Term = - nonterminal: - if isThunkedOp(op) then - Call(IdTarget(IdRef(Nil, opName(op))), Nil, Nil, List(BlockLiteral(Nil, Nil, Nil, Return(lhs)), BlockLiteral(Nil, Nil, Nil, Return(rhs)))) - else - Call(IdTarget(IdRef(Nil, opName(op))), Nil, List(lhs, rhs), Nil) - - private def isThunkedOp(op: TokenKind): Boolean = op match { - case `||` | `&&` => true - case _ => false - } - - private def opName(op: TokenKind): String = op match { - case `||` => "infixOr" - case `&&` => "infixAnd" - case `===` => "infixEq" - case `!==` => "infixNeq" - case `<` => "infixLt" - case `>` => "infixGt" - case `<=` => "infixLte" - case `>=` => "infixGte" - case `+` => "infixAdd" - case `-` => "infixSub" - case `*` => "infixMul" - case `/` => "infixDiv" - case `++` => "infixConcat" - case _ => sys.error(s"Internal compiler error: not a valid operator ${op}") - } - - private def TupleTypeTree(tps: List[ValueType]): ValueType = - ValueTypeRef(IdRef(List("effekt"), s"Tuple${tps.size}"), tps) - // TODO positions! - - /** - * This is a compound production for - * - member selection . - * - method calls .(...) - * - function calls (...) - * - * This way expressions like `foo.bar.baz()(x).bam.boo()` are - * parsed with the correct left-associativity. - */ - def callExpr(): Term = nonterminal { - var e = primExpr() - - while (peek(`.`) || isArguments) - peek.kind match { - // member selection (or method call) - // . - // | .( ... ) - case `.` => - consume(`.`) - val member = idRef() - // method call - if (isArguments) { - val (targs, vargs, bargs) = arguments() - e = Term.MethodCall(e, member, targs, vargs, bargs) - } else { - e = Term.MethodCall(e, member, Nil, Nil, Nil) - } - - // function call - case _ if isArguments => - val callee = e match { - case Term.Var(id) => IdTarget(id) - case other => ExprTarget(other) - } - val (targs, vargs, bargs) = arguments() - e = Term.Call(callee, targs, vargs, bargs) - - // nothing to do - case _ => () - } - - e - } - - // argument lists cannot follow a linebreak: - // foo == foo; - // () () - def isArguments: Boolean = lookbehind(1).kind != Newline && (peek(`(`) || peek(`[`) || peek(`{`)) - def arguments(): (List[ValueType], List[Term], List[Term]) = - if (!isArguments) fail("Expected at least one argument section (types, values, or blocks)") - (maybeTypeArgs(), maybeValueArgs(), maybeBlockArgs()) - - def maybeTypeArgs(): List[ValueType] = if peek(`[`) then typeArgs() else Nil - def maybeValueArgs(): List[Term] = if peek(`(`) then valueArgs() else Nil - def maybeBlockArgs(): List[Term] = if peek(`{`) then blockArgs() else Nil - - def typeArgs(): List[ValueType] = - nonterminal: - some(valueType, `[`, `,`, `]`) - def valueArgs(): List[Term] = - nonterminal: - many(expr, `(`, `,`, `)`) - def blockArgs(): List[Term] = - nonterminal: - someWhile(blockArg(), `{`) - - /** - * Note: for this nonterminal, we need some backtracking. - */ - def blockArg(): Term = - nonterminal: - backtrack { `{` ~> Var(idRef()) <~ `}` } getOrElse { functionArg() } - - def functionArg(): BlockLiteral = - nonterminal: - braces { - peek.kind match { - // { case ... => ... } - case `case` => someWhile(matchClause(), `case`) match { case cs => - val arity = cs match { - case MatchClause(MultiPattern(ps), _, _) :: _ => ps.length - case _ => 1 - } - // TODO positions should be improved here and fresh names should be generated for the scrutinee - // also mark the temp name as synthesized to prevent it from being listed in VSCode - val names = List.tabulate(arity){ n => s"__arg${n}" } - BlockLiteral( - Nil, - names.map{ name => ValueParam(IdDef(name), None) }, - Nil, - Return(Match(names.map{ name => Var(IdRef(Nil, name)) }, cs, None))) : BlockLiteral - } - case _ => - // { (x: Int) => ... } - backtrack { lambdaParams() <~ `=>` } map { - case (tps, vps, bps) => BlockLiteral(tps, vps, bps, stmts()) : BlockLiteral - } getOrElse { - // { } - BlockLiteral(Nil, Nil, Nil, stmts()) : BlockLiteral - } - } - } - - def primExpr(): Term = peek.kind match { - case `if` => ifExpr() - case `while` => whileExpr() - case `try` => tryExpr() - case `region` => regionExpr() - case `box` => boxExpr() - case `unbox` => unboxExpr() - case `fun` => funExpr() - case `new` => newExpr() - case `do` => doExpr() - case _ if isString => templateString() - case _ if isLiteral => literal() - case _ if isVariable => - peek(1).kind match { - case _: Str => templateString() - case _ => variable() - } - case _ if isHole => hole() - case _ if isTupleOrGroup => tupleOrGroup() - case _ if isListLiteral => listLiteral() - case _ => fail(s"Expected variables, literals, tuples, lists, holes or group") - } - - def isListLiteral: Boolean = peek.kind match { - case `[` => true - case _ => false - } - def listLiteral(): Term = - nonterminal: - manyTrailing(expr, `[`, `,`, `]`).foldRight(NilTree) { ConsTree } - - private def NilTree: Term = - Call(IdTarget(IdRef(List(), "Nil")), Nil, Nil, Nil) - - private def ConsTree(el: Term, rest: Term): Term = - Call(IdTarget(IdRef(List(), "Cons")), Nil, List(el, rest), Nil) - - def isTupleOrGroup: Boolean = peek(`(`) - def tupleOrGroup(): Term = - some(expr, `(`, `,`, `)`) match { - case e :: Nil => e - case xs => Call(IdTarget(IdRef(List("effekt"), s"Tuple${xs.size}")), Nil, xs.toList, Nil) - } - - def isHole: Boolean = peek(`<>`) || peek(`<{`) - def hole(): Term = peek.kind match { - case `<>` => `<>` ~> Hole(Return(UnitLit())) - case `<{` => `<{` ~> Hole(stmts()) <~ `}>` - case _ => fail("Expected hole") - } - - def isLiteral: Boolean = peek.kind match { - case _: (Integer | Float | Str | Chr) => true - case `true` => true - case `false` => true - case _ => isUnitLiteral - } - - def isString: Boolean = peek.kind match { - case _: Str => true - case _ => false - } - - def templateString(): Term = - nonterminal: - backtrack(idRef()) ~ template() match { - // We do not need to apply any transformation if there are no splices _and_ no custom handler id is given - case None ~ Template(str :: Nil, Nil) => StringLit(str) - // s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y); return () } - case id ~ Template(strs, args) => - val target = id.getOrElse(IdRef(Nil, "s")) - val doLits = strs.map { s => - Do(None, IdRef(Nil, "literal"), Nil, List(StringLit(s)), Nil) - } - val doSplices = args.map { arg => - Do(None, IdRef(Nil, "splice"), Nil, List(arg), Nil) - } - val body = interleave(doLits, doSplices) - .foldRight(Return(UnitLit())) { (term, acc) => ExprStmt(term, acc) } - val blk = BlockLiteral(Nil, Nil, Nil, body) - Call(IdTarget(target), Nil, Nil, List(blk)) - } - - def literal(): Literal = - nonterminal: - peek.kind match { - case Integer(v) => skip(); IntLit(v) - case Float(v) => skip(); DoubleLit(v) - case Str(s, multiline) => skip(); StringLit(s) - case Chr(c) => skip(); CharLit(c) - case `true` => skip(); BooleanLit(true) - case `false` => skip(); BooleanLit(false) - case t if isUnitLiteral => skip(); skip(); UnitLit() - case t => fail("Expected a literal") - } - - // Will also recognize ( ) as unit if we do not emit space in the lexer... - private def isUnitLiteral: Boolean = peek(`(`) && peek(1, `)`) - - def isVariable: Boolean = isIdRef - def variable(): Term = - nonterminal: - Var(idRef()) - - def isIdRef: Boolean = isIdent - - def idRef(): IdRef = - nonterminal: - some(ident, PathSep) match { - case ids => IdRef(ids.init, ids.last) - } - - def idDef(): IdDef = - nonterminal: - IdDef(ident()) - - def isIdent: Boolean = peek.kind match { - case Ident(id) => true - case _ => false - } - def ident(): String = - nonterminal: - next().kind match { - case Ident(id) => id - case _ => fail(s"Expected identifier") - } - - - /** - * Types - */ - - def valueType(): ValueType = valueType2(true) - - /** - * Uses backtracking! - * - * This is not very efficient. To parse a value type, we first parse a block type, - * just to see that it either is no blocktype or it is not followed by an `at` - * and just "looked" like a block type... - * - * The parameter [[boxedAllowed]] controls whether on the right a dangling `at` - * can occur. This way we prevent parsing `() => S at {} at {}` and force users - * to manually parenthesize. - */ - private def valueType2(boxedAllowed: Boolean): ValueType = nonterminal { - def boxedBlock = backtrack { - BoxedType(blockType2(false), `at` ~> captureSet()) - } - if (boxedAllowed) { boxedBlock getOrElse atomicValueType() } - else atomicValueType() - } - - def atomicValueType(): ValueType = - nonterminal: - peek.kind match { - case `(` => some(valueType, `(`, `,`, `)`) match { - case tpe :: Nil => tpe - case tpes => TupleTypeTree(tpes) - } - case _ => ValueTypeRef(idRef(), maybeTypeArgs()) - } - - - /** - * Uses backtracking! - * - * TODO improve errors - * i.e. fail("Expected either a function type (e.g., (A) => B / {E} or => B) or an interface type (e.g., State[T]).") - */ - def blockType(): BlockType = blockType2(true) - private def blockType2(boxedAllowed: Boolean): BlockType = - nonterminal: - - def simpleFunType = backtrack { - ValueTypeRef(idRef(), maybeTypeArgs()) <~ `=>` - } map { tpe => - FunctionType(Nil, List(tpe), Nil, valueType2(boxedAllowed), maybeEffects()) - } - - def funType = backtrack { - maybeTypeParams() ~ maybeValueTypes() ~ (maybeBlockTypeParams() <~ `=>`) ~ valueType2(boxedAllowed) ~ maybeEffects() match { - case tparams ~ vparams ~ bparams ~ t ~ effs => FunctionType(tparams, vparams, bparams, t, effs) - } - } - def parenthesized = backtrack { parens { blockType() } } - - def interface() = - val res = interfaceType() - if peek(`/`) then - fail("Effects not allowed here. Maybe you mean to use a function type `() => T / E`?") - else res - - simpleFunType orElse funType orElse parenthesized getOrElse interface() - - def interfaceType(): BlockTypeRef = - nonterminal: - BlockTypeRef(idRef(), maybeTypeArgs()): BlockTypeRef - // TODO error "Expected an interface type" - - def maybeTypeParams(): List[Id] = - nonterminal: - if peek(`[`) then typeParams() else Nil - - def typeParams(): List[Id] = - nonterminal: - some(idDef, `[`, `,`, `]`) - - def maybeBlockTypeParams(): List[(Option[IdDef], BlockType)] = - nonterminal: - if peek(`{`) then blockTypeParams() else Nil - - def blockTypeParams(): List[(Option[IdDef], BlockType)] = - nonterminal: - someWhile(blockTypeParam(), `{`) - - def blockTypeParam(): (Option[IdDef], BlockType) = - nonterminal: - braces { (backtrack { idDef() <~ `:` }, blockType()) } - - def lambdaParams(): (List[Id], List[ValueParam], List[BlockParam]) = - nonterminal: - if isVariable then (Nil, List(ValueParam(idDef(), None)), Nil) else paramsOpt() - - def params(): (List[Id], List[ValueParam], List[BlockParam]) = - nonterminal: - maybeTypeParams() ~ maybeValueParams() ~ maybeBlockParams() match { - case tps ~ vps ~ bps => (tps, vps, bps) - } - - def paramsOpt(): (List[Id], List[ValueParam], List[BlockParam]) = - nonterminal: - maybeTypeParams() ~ maybeValueParamsOpt() ~ maybeBlockParamsOpt() match { - case (tps ~ vps ~ bps) => - // fail("Expected a parameter list (multiple value parameters or one block parameter; only type annotations of value parameters can be currently omitted)") - (tps, vps, bps) - } - - def maybeValueParamsOpt(): List[ValueParam] = - nonterminal: - if peek(`(`) then valueParamsOpt() else Nil - - def valueParamsOpt(): List[ValueParam] = - nonterminal: - many(valueParamOpt, `(`, `,`, `)`) - - def maybeValueParams(): List[ValueParam] = - nonterminal: - if peek(`(`) then valueParams() else Nil - - def valueParams(): List[ValueParam] = - nonterminal: - many(valueParam, `(`, `,`, `)`) - - def valueParam(): ValueParam = - nonterminal: - ValueParam(idDef(), Some(valueTypeAnnotation())) - - def valueParamOpt(): ValueParam = - nonterminal: - ValueParam(idDef(), maybeValueTypeAnnotation()) - - def maybeBlockParams(): List[BlockParam] = - nonterminal: - manyWhile(`{` ~> blockParam() <~ `}`, `{`) - - def blockParams(): List[BlockParam] = - nonterminal: - someWhile(`{` ~> blockParam() <~ `}`, `{`) - - def maybeBlockParamsOpt(): List[BlockParam] = - nonterminal: - manyWhile(`{` ~> blockParamOpt() <~ `}`, `{`) - - def blockParamsOpt(): List[BlockParam] = - nonterminal: - someWhile(`{` ~> blockParamOpt() <~ `}`, `{`) - - def blockParam(): BlockParam = - nonterminal: - BlockParam(idDef(), Some(blockTypeAnnotation())) - - def blockParamOpt(): BlockParam = - nonterminal: - BlockParam(idDef(), when(`:`)(Some(blockType()))(None)) - - - def maybeValueTypes(): List[ValueType] = - nonterminal: - if peek(`(`) then valueTypes() else Nil - - def valueTypes(): List[ValueType] = - nonterminal: - many(valueType, `(`, `,`, `)`) - - def captureSet(): CaptureSet = - nonterminal: - CaptureSet(many(idRef, `{`, `,` , `}`)) - - def effectful(): Effectful = - nonterminal: - Effectful(valueType(), maybeEffects()) - - def maybeEffects(): Effects = - nonterminal: - when(`/`) { effects() } { Effects.Pure } - - // TODO error "Expected an effect set" - def effects(): Effects = - nonterminal: - if peek(`{`) then Effects(many(interfaceType, `{`, `,`, `}`)) - else Effects(interfaceType()) - - - // Generic utility functions - // ------------------------- - // ... for writing parsers. - - /** - * Aborts parsing with the given message - */ - def fail(message: String): Nothing = throw Fail(message, position) - - def softFail(message: String, start: Int, end: Int): Unit = { - softFails += SoftFail(message, start, end) - } - - /** - * Guards `thn` by token `t` and consumes the token itself, if present. - */ - inline def when[T](t: TokenKind)(inline thn: => T)(inline els: => T): T = - if peek(t) then { consume(t); thn } else els - - inline def backtrack[T](inline p: => T): Option[T] = - val before = position - try { Some(p) } catch { - case Fail(_, _) => position = before; None - } - - def interleave[A](xs: List[A], ys: List[A]): List[A] = (xs, ys) match { - case (x :: xs, y :: ys) => x :: y :: interleave(xs, ys) - case (Nil, ys) => ys - case (xs, Nil) => xs - } - - /** - * Tiny combinator DSL to sequence parsers - */ - case class ~[+T, +U](_1: T, _2: U) { - override def toString = s"(${_1}~${_2})" - } - - extension [A](self: A) { - @targetName("seq") - inline def ~[B](other: B): (A ~ B) = new ~(self, other) - - @targetName("seqLeftToken") - inline def <~(t: TokenKind): A = { consume(t); self } - - @targetName("seqLeftUnit") - inline def <~(t: Unit): A = { self } - - inline def |(other: A): A = { backtrack(self).getOrElse(other) } - } - - extension (self: TokenKind) { - @targetName("seqRightToken") - inline def ~>[R](other: => R): R = { consume(self); other } - } - - extension (self: Unit) { - @targetName("seqRightUnit") - inline def ~>[R](other: => R): R = { other } - } - - /** - * Repeats [[p]], separated by [[sep]] enclosed by [[before]] and [[after]] - */ - inline def some[T](p: () => T, before: TokenKind, sep: TokenKind, after: TokenKind): List[T] = - consume(before) - val res = some(p, sep) - consume(after) - res - - inline def some[T](p: () => T, sep: TokenKind): List[T] = - val components: ListBuffer[T] = ListBuffer.empty - components += p() - while (peek(sep)) { - consume(sep) - components += p() - } - components.toList - - inline def someWhile[T](p: => T, lookahead: TokenKind): List[T] = - someWhile(p, peek(lookahead)) - - inline def someWhile[T](p: => T, predicate: => Boolean): List[T] = - val components: ListBuffer[T] = ListBuffer.empty - components += p - while (predicate) { - components += p - } - components.toList - - inline def manyWhile[T](p: => T, lookahead: TokenKind): List[T] = - manyWhile(p, peek(lookahead)) - - inline def manyWhile[T](p: => T, predicate: => Boolean): List[T] = - val components: ListBuffer[T] = ListBuffer.empty - while (predicate) { - components += p - } - components.toList - - inline def parens[T](p: => T): T = - consume(`(`) - val res = p - consume(`)`) - res - - inline def braces[T](p: => T): T = - consume(`{`) - val res = p - consume(`}`) - res - - inline def many[T](p: () => T, before: TokenKind, sep: TokenKind, after: TokenKind): List[T] = - consume(before) - if (peek(after)) { - consume(after) - Nil - } else { - val components: ListBuffer[T] = ListBuffer.empty - components += p() - while (peek(sep)) { - consume(sep) - components += p() - } - consume(after) - components.toList - } - - inline def manyTrailing[T](p: () => T, before: TokenKind, sep: TokenKind, after: TokenKind): List[T] = - consume(before) - if (peek(after)) { - consume(after) - Nil - } else if (peek(sep)) { - consume(sep) - consume(after) - Nil - } else { - val components: ListBuffer[T] = ListBuffer.empty - components += p() - while (peek(sep)) { - consume(sep) - - if (!peek(after)) { - components += p() - } - } - consume(after) - components.toList - } - - - // Positions - - inline def nonterminal[T](inline p: => T): T = { - val startToken = peek - val start = startToken.start - val res = p - val endToken = previous - val end = endToken.end + 1 // since positions by lexer are inclusive, but kiama is exclusive - - // val sc: Any = res - // if sc.isInstanceOf[Implementation] then sc match { - // case Implementation(_, List(op)) => - // println(op) - // println(positions.getStart(op)) - // println(positions.getFinish(op)) - // - // // println(s"start: ${startToken.kind} (${source.offsetToPosition(start)})") - // // println(s"end: ${endToken} (${source.offsetToPosition(end)})") - // // println(s"peek: ${peek}") - // - // case _ => () - // } - - val startPos = source.offsetToPosition(start) - val endPos = source.offsetToPosition(end) - - // recursively add positions to subtrees that are not yet annotated - // this is better than nothing and means we have positions for desugared stuff - def annotatePositions(res: Any): Unit = res match { - case l: List[_] => - if (positions.getRange(l).isEmpty) { - positions.setStart(l, startPos) - positions.setFinish(l, endPos) - l.foreach(annotatePositions) - } - case t: Tree => - val recurse = positions.getRange(t).isEmpty - if(positions.getStart(t).isEmpty) positions.setStart(t, startPos) - if(positions.getFinish(t).isEmpty) positions.setFinish(t, endPos) - t match { - case p: Product if recurse => - p.productIterator.foreach { c => - annotatePositions(c) - } - case _ => () - } - case _ => () - } - annotatePositions(res) - - // still annotate, in case it is not Tree - positions.setStart(res, startPos) - positions.setFinish(res, endPos) - - res - } - - extension [T](self: T) { - inline def withPositionOf(other: Any): self.type = { positions.dupPos(other, self); self } - inline def withRangeOf(first: Any, last: Any): self.type = { positions.dupRangePos(first, last, self); self } - - // Why did we need those? - private def dupIfEmpty(from: Any, to: Any): Unit = - if (positions.getStart(to).isEmpty) { positions.dupPos(from, to) } - - private def dupAll(from: Any, to: Any): Unit = to match { - case t: Tree => - dupIfEmpty(from, t) - t.productIterator.foreach { dupAll(from, _) } - case t: Iterable[t] => t.foreach { dupAll(from, _) } - case _ => () - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala deleted file mode 100644 index 70fb51521..000000000 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ /dev/null @@ -1,1699 +0,0 @@ -package effekt -package typer - -/** - * In this file we fully qualify source types, but use symbols directly - */ -import effekt.context.{Annotation, Annotations, Context, ContextOps} -import effekt.context.assertions.* -import effekt.source.{AnyPattern, Def, Effectful, IgnorePattern, MatchGuard, MatchPattern, ModuleDecl, OpClause, Stmt, TagPattern, Term, Tree, resolve, symbol} -import effekt.source.Term.BlockLiteral -import effekt.symbols.* -import effekt.symbols.builtins.* -import effekt.symbols.kinds.* -import effekt.util.messages.* -import effekt.util.foreachAborting - -import scala.language.implicitConversions - -/** - * Typechecking - * ============ - * - * Preconditions: - * -------------- - * Typer assumes that all dependencies already have been type checked. - * In particular, it assumes that all definitions / symbols (functions, parameters etc.) - * have been annotated with a type: this models a (global) typing context. - * - * Postconditions: - * --------------- - * All trees will be annotated with intermediate types (and effects). This is useful for - * IDE support. - * Also, after type checking, all definitions of the file will be annotated with their type. - * - * Invariants: - * ----------- - * All effects inferred by Typer are concrete and dealiased. This is established by - * type [[ConcreteEffects]] and constructor [[Typer.asConcrete]]. - */ -case class Result[+T](tpe: T, effects: ConcreteEffects) - - -object Typer extends Phase[NameResolved, Typechecked] { - - val phaseName = "typer" - - def run(input: NameResolved)(using Context) = Context.using(module = input.mod, focus = input.tree) { - try { - val NameResolved(source, tree, mod) = input - - Context.initTyperstate() - - Context.timed(phaseName, source.name) { - Context in { - Context.withUnificationScope { - flowingInto(builtins.toplevelCaptures) { - // We split the type-checking of definitions into "pre-check" and "check" - // to allow mutually recursive defs - tree.defs.foreach { d => precheckDef(d) } - tree.defs.foreach { d => - val Result(_, effs) = synthDef(d) - val unhandled = effs.toEffects - if (unhandled.nonEmpty) - Context.at(d) { - Context.error(pretty"Unhandled effects ${unhandled}") - } - } - } - } - } - } - - if (Context.messaging.hasErrors) { - None - } else { - Some(Typechecked(source, tree, mod)) - } - } finally { - // Store the backtrackable annotations into the global DB - // This is done regardless of errors, since - Context.commitTypeAnnotations() - } - } - - - // - - def checkExpr(expr: Term, expected: Option[ValueType])(using Context, Captures): Result[ValueType] = - checkAgainst(expr, expected) { - case source.Literal(_, tpe) => Result(tpe, Pure) - - case source.If(guards, thn, els) => - val Result((), guardEffs) = checkGuards(guards) - val Result(thnTpe, thnEffs) = checkStmt(thn, expected) - val Result(elsTpe, elsEffs) = checkStmt(els, expected) - - Result(Context.join(thnTpe, elsTpe), guardEffs ++ thnEffs ++ elsEffs) - - case source.While(guards, body, default) => - val Result((), guardEffs) = checkGuards(guards) - val expectedType = if default.isDefined then expected else Some(TUnit) - val Result(bodyTpe, bodyEffs) = checkStmt(body, expectedType) - val Result(defaultTpe, defaultEffs) = default.map { s => - checkStmt(s, expectedType) - }.getOrElse(Result(TUnit, ConcreteEffects.empty)) - - Result(Context.join(bodyTpe, defaultTpe), defaultEffs ++ bodyEffs ++ guardEffs) - - case source.Var(id) => id.symbol match { - case x: RefBinder => Context.lookup(x) match { - case (btpe, capt) => - val vtpe = TState.extractType(btpe) - usingCapture(capt) - Result(vtpe, Pure) - } - case b: BlockSymbol => Context.abort("Expected an expression, but got a block.") - case x: ValueSymbol => Result(Context.lookup(x), Pure) - } - - case e @ source.Assign(id, expr) => e.definition match { - case x: RefBinder => - val stTpe = Context.lookup(x) match { - case (btpe, capt) => - usingCapture(capt) - TState.extractType(btpe) - } - val Result(_, eff) = expr checkAgainst stTpe - Result(TUnit, eff) - } - - case l @ source.Box(annotatedCapture, block) => - - val expectedTpe = expected.collect { case BoxedType(tpe, cap) => tpe } - val inferredCap: Captures = annotatedCapture.map { _.resolve }.getOrElse { - Context.freshCaptVar(CaptUnificationVar.InferredBox(l)) - } - - flowingInto(inferredCap) { - val Result(inferredTpe, inferredEff) = checkExprAsBlock(block, expectedTpe) - val tpe = Context.unification(BoxedType(inferredTpe, inferredCap)) - expected.map(Context.unification.apply) foreach { matchExpected(tpe, _) } - Result(tpe, inferredEff) - } - - case source.Unbox(_) => - Context.abort("Expected an expression, but got an unbox (which is a block).") - - case c @ source.Select(receiver, field) => - checkOverloadedFunctionCall(c, field, Nil, List(receiver), Nil, expected) - - case c @ source.Do(effect, op, targs, vargs, bargs) => - // (1) first check the call - val Result(tpe, effs) = checkOverloadedFunctionCall(c, op, targs map { _.resolve }, vargs, bargs, expected) - // (2) now we need to find a capability as the receiver of this effect operation - // (2a) compute substitution for inferred type arguments - val typeArgs = Context.annotatedTypeArgs(c) - val operation = c.definition - val subst = Substitutions.types(operation.tparams, typeArgs) - - // (2b) substitute into effect type of operation - val effect = subst.substitute(operation.appliedInterface) - // (2c) search capability - val capability = Context.capabilityReceiver(c, effect) - // (2d) register capability as being used - usingCapture(CaptureSet(capability.capture)) - - // (3) add effect to used effects - Result(tpe, effs ++ ConcreteEffects(List(effect))) - - case c @ source.Call(t: source.IdTarget, targs, vargs, bargs) => - checkOverloadedFunctionCall(c, t.id, targs map { _.resolve }, vargs, bargs, expected) - - case c @ source.Call(source.ExprTarget(e), targs, vargs, bargs) => - val Result(tpe, funEffs) = checkExprAsBlock(e, None) match { - case Result(b: FunctionType, capt) => Result(b, capt) - case _ => Context.abort("Cannot infer function type for callee.") - } - - val Result(t, eff) = checkCallTo(c, "function", tpe, targs map { _.resolve }, vargs, bargs, expected) - Result(t, eff ++ funEffs) - - // precondition: PreTyper translates all uniform-function calls to `Call`. - // so the method calls here are actually methods calls on blocks as receivers. - case c @ source.MethodCall(receiver, id, targs, vargs, bargs) => - checkOverloadedMethodCall(c, receiver, id, targs map { _.resolve }, vargs, bargs, expected) - - case tree @ source.Region(name, body) => - val reg = tree.symbol - Context.bind(reg) - - val inferredCapture = Context.freshCaptVar(CaptUnificationVar.RegionRegion(tree)) - usingCapture(inferredCapture) - - flowingInto(Context.without(inferredCapture, List(reg.capture))) { - checkStmt(body, expected) - } - - case tree @ source.TryHandle(prog, handlers) => - - // (1) extract all handled effects and capabilities - val providedCapabilities: List[symbols.BlockParam] = handlers map Context.withFocus { h => - val effect: InterfaceType = h.effect.resolve - val capability = h.capability.map { _.symbol }.getOrElse { Context.freshCapabilityFor(effect) } - val tpe = capability.tpe.getOrElse { INTERNAL_ERROR("Block type annotation required") } - Context.bind(capability, tpe, CaptureSet(capability.capture)) - capability - } - - // Create a fresh capture variable for the continuations ?Ck - val continuationCapt = Context.freshCaptVar(CaptUnificationVar.HandlerRegion(tree)) - - // All used captures flow into the continuation capture, except the ones handled by this handler. - val continuationCaptHandled = Context.without(continuationCapt, providedCapabilities.map(_.capture)) - - // (2) Check the handled program - val Result(ret, effs) = Context.bindingCapabilities(tree, providedCapabilities) { - flowingInto(continuationCaptHandled) { - checkStmt(prog, expected) - } - } - - // (3) Check the handlers - // Also all capabilities used by the handler flow into the capture of the continuation - - var handlerEffs: ConcreteEffects = Pure - - handlers foreach Context.withFocus { h => - flowingInto(continuationCaptHandled) { - val Result(_, usedEffects) = checkImplementation(h.impl, Some((ret, continuationCapt))) - handlerEffs = handlerEffs ++ usedEffects - } - } - - // (4) Wellformedness checks - val handlerFor = providedCapabilities.map { cap => - // all effects have to be concrete at this point in time - // safety: (1) ensures there's a type annotation - val concreteEffect = Context.unification(cap.tpe.getOrElse { INTERNAL_ERROR("Block type annotation required") }.asInterfaceType) - (concreteEffect, cap) - } - - // Helper definitions: - // - capabilities that are bound explicitly by the user - val explicitCapabilities = handlers.flatMap { _.capability.map(_.symbol) }.toSet - // - all effects that are handled - val handled = ConcreteEffects(handlerFor.map(_._1)) - // - capabilities grouped by effect - val capabilityGroups = handlerFor.groupBy(_._1).view.mapValues(_.map(_._2)).toList - - // Compute groups of capabilities that handle the same effects, then - // 1) check whether all are bound explicitly (which would be fine) - // 2) it is only a singleton set (which is fine) - capabilityGroups.foreach { - case (effect, capabilities) if capabilities.size > 1 => - val allExplicit = capabilities.forall { c => explicitCapabilities contains c } - - if (!allExplicit) - Context.warning(pp"There are multiple handlers for effect ${effect}; this might not be intended.\nMaybe bind capabilities explicitly (e.g. `try { ... } with c: MyEffect { ... }`)?") - case _ => () - } - capabilityGroups.foreach { - case (effect, capabilities) => - val allImplicit = capabilities.forall { c => !(explicitCapabilities contains c) } - val used = effs.exists(e => e == effect) - - if (allImplicit && !used) - Context.warning(pp"Handling effect ${effect}, which seems not to be used by the program.") - } - - // The captures of the handler continue flowing into the outer scope - usingCapture(continuationCapt) - - Result(ret, (effs -- handled) ++ handlerEffs) - - case tree @ source.Match(scs, clauses, default) => - - // (1) Check scrutinees - // for example. tpe = List[Int] - val results = scs.map{ sc => checkExpr(sc, None) } - - var resEff = ConcreteEffects.union(results.map{ case Result(tpe, effs) => effs }) - - // check that number of patterns matches number of scrutinees - val arity = scs.length - clauses.foreach { - case cls @ source.MatchClause(source.MultiPattern(patterns), guards, body) => - if (patterns.length != arity) { - Context.at(cls){ - Context.error(pp"Number of patterns (${patterns.length}) does not match number of parameters / scrutinees (${arity}).") - } - } - case cls @ source.MatchClause(pattern, guards, body) => - if (arity != 1) { - Context.at(cls) { - Context.error(pp"Number of patterns (1) does not match number of parameters / scrutinees (${arity}).") - } - } - } - - val tpes = clauses.map { - case source.MatchClause(p, guards, body) => - // (3) infer types for pattern(s) - p match { - case source.MultiPattern(ps) => - (results zip ps).foreach { case (Result(tpe, effs), p) => - Context.bind(checkPattern(tpe, p)) - } - case p => - val Result(tpe, effs) = results.head - Context.bind(checkPattern(tpe, p)) - } - // infer types for guards - val Result((), guardEffs) = checkGuards(guards) - // check body of the clause - val Result(clTpe, clEff) = Context in { checkStmt(body, expected) } - - resEff = resEff ++ clEff ++ guardEffs - clTpe - } ++ default.map { body => - val Result(defaultTpe, defaultEff) = Context in { checkStmt(body, expected) } - resEff = resEff ++ defaultEff - defaultTpe - } - - // Clauses could in general be empty if there are no constructors - // In that case the scrutinee couldn't have been constructed and - // we can unify with everything. - Result(Context.join(tpes: _*), resEff) - - case source.Hole(stmt) => - val Result(tpe, effs) = checkStmt(stmt, None) - Result(expected.getOrElse(TBottom), Pure) - - case tree : source.New => Context.abort("Expected an expression, but got an object implementation (which is a block).") - case tree : source.BlockLiteral => Context.abort("Expected an expression, but got a block literal.") - } - - // Sideeffect: binds names in the current scope - def checkGuards(guards: List[MatchGuard])(using Context, Captures): Result[Unit] = - var effs = ConcreteEffects.empty - guards foreach { g => - val Result(bindings, guardEffs) = checkGuard(g) - Context.bind(bindings) - effs = effs ++ guardEffs - } - Result((), effs) - - - /** - * The [[continuationDetails]] are only provided, if a continuation is captured (that is for implementations as part of effect handlers). - */ - def checkImplementation(impl: source.Implementation, continuationDetails: Option[(ValueType, CaptUnificationVar)])(using Context, Captures): Result[InterfaceType] = Context.focusing(impl) { - case source.Implementation(sig, clauses) => - - var handlerEffects: ConcreteEffects = Pure - - // Extract interface and type arguments from annotated effect - val tpe @ InterfaceType(constructor, targs) = sig.resolve - val interface = constructor.asInterface // can only implement concrete interfaces - - // (3) check all operations are covered - val covered = clauses.map { _.definition } - val notCovered = interface.operations.toSet -- covered.toSet - - if (notCovered.nonEmpty) { - val explanation = notCovered.map { op => pp"${op.name} of interface ${op.interface.name}" }.mkString(", ") - Context.error(pretty"Missing definitions for operations: ${explanation}") - } - - if (covered.size > covered.distinct.size) - Context.error("Duplicate definitions of operations") - - clauses foreach Context.withFocus { - case d @ source.OpClause(op, tparams, vparams, bparams, retAnnotation, body, resume) => - - retAnnotation.foreach { - case Effectful(otherTpe, otherEffs2) => - // if there is a return type annotation from the user, report an error - // see PR #148 for more details - // TODO: Can we somehow use the return type provided by the user? - Context.abort(pretty"Unexpected type annotation on operation ${op}.") - } - - val declaredOperation = interface.operations.find(o => o.name.name == op.name).getOrElse { - Context.abort(pretty"Operation ${op.name} not defined in ${interface.name}.") - } - val declared = Context.lookupFunctionType(d.definition) - - def assertArity(kind: String, got: Int, expected: Int): Unit = - if (got != expected) - Context.abort(pretty"Number of ${kind} (${got}) does not match declaration of '${op.name}', which expects ${expected}.") - - // if we have zero given type parameters, we synthesize them -- no need to check then - if (tparams.nonEmpty) assertArity("type parameters", tparams.size, declared.tparams.size - targs.size) - assertArity("value parameters", vparams.size, declared.vparams.size) - - // effect E[A, B, ...] { def op[C, D, ...]() = ... } !--> op[A, B, ..., C, D, ...] - // The parameters C, D, ... are existentials - val existentialParams: List[TypeVar] = if (tparams.size == declared.tparams.size - targs.size) { - tparams.map { tparam => tparam.symbol.asTypeParam } - } else { - // using the invariant that the universals are prepended to type parameters of the operation - declared.tparams.drop(targs.size).map { tp => - // recreate "fresh" type variables - val name = tp.name - TypeVar.TypeParam(name) - } - } - val existentials = existentialParams.map(ValueTypeRef.apply) - - Context.annotate(Annotations.TypeParameters, d, existentialParams) - - // canonical ordering on annotated effects - val canonical = CanonicalOrdering(declared.effects.toList) - - // distinguish between handler operation or object operation (which does not capture a cont.) - val Result(_, effs) = continuationDetails match { - // normal object: no continuation there - case None => - // block parameters are to be bound by the definition itself instead of by resume when using handlers - assertArity("block parameters", bparams.size, declared.bparams.size) - - val cparamsForBlocks = bparams.map { p => p.symbol.capture } - // will be introduced as capabilities in a later phase - val cparamsForEffects = canonical.map { tpe => CaptureParam(tpe.name) } - val cparams = cparamsForBlocks ++ cparamsForEffects - - // substitute actual type parameter and capture parameters for declared ones - val (vps, bps, tpe, effs) = - Context.instantiate(declared, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked - - (vparams zip vps).foreach { - case (param, declaredType) => - val sym = param.symbol - val annotatedType = sym.tpe - annotatedType.foreach(matchDeclared(_, declaredType, param)) - Context.bind(sym, annotatedType.getOrElse(declaredType)) - } - - (bparams zip bps zip cparamsForBlocks).foreach { - case ((param, declaredType), capture) => - val sym = param.symbol - val annotatedType = sym.tpe - annotatedType.foreach { matchDeclared(_, declaredType, param) } - Context.bind(sym, annotatedType.getOrElse(declaredType), CaptureSet(capture)) - } - - // these capabilities are later introduced as parameters in capability passing - val capabilities = (CanonicalOrdering(effs) zip cparamsForEffects).map { - case (tpe, capt) => Context.freshCapabilityFor(tpe, CaptureSet(capt)) - } - - val Result(bodyType, bodyEffs) = Context.bindingCapabilities(d, capabilities) { - body checkAgainst tpe - } - Result(bodyType, bodyEffs -- Effects(effs)) - - // handler implementation: we have a continuation - case Some(ret, continuationCapt) => - - if (bparams.nonEmpty) - Context.error("Block parameters are bound by resume and not the effect operation itself") - - def isBidirectional = canonical.nonEmpty || declared.bparams.nonEmpty - - val cparamsForBlocks = declaredOperation.bparams.map { p => CaptureParam(p.name) } // use the original name - val cparamsForEffects = canonical.map { tpe => CaptureParam(tpe.name) } // use the type name - val cparams = cparamsForBlocks ++ cparamsForEffects - - val (vps, bps, tpe, effs) = - Context.instantiate(declared, targs ++ existentials, cparams.map(cap => CaptureSet(cap))) : @unchecked - - (vparams zip vps).foreach { - case (param, declaredType) => - val sym = param.symbol - val annotatedType = sym.tpe - annotatedType.foreach(matchDeclared(_, declaredType, param)) - Context.bind(sym, annotatedType.getOrElse(declaredType)) - } - - // (4) synthesize type of continuation - val resumeType = if (isBidirectional) { - // resume { {f} => e } - val resumeType = FunctionType(Nil, cparams, Nil, bps, tpe, Effects(effs)) - val resumeCapt = CaptureParam(Name.local("resumeBlock")) - FunctionType(Nil, List(resumeCapt), Nil, List(resumeType), ret, Effects.Pure) - } else { - // resume(v) - FunctionType(Nil, Nil, List(tpe), Nil, ret, Effects.Pure) - } - Context.bind(Context.symbolOf(resume).asBlockSymbol, resumeType, continuationCapt) - - body checkAgainst ret - } - - handlerEffects = handlerEffects ++ effs - } - - // The implementation has the annotated block type - Context.annotateInferredType(impl, tpe) - - Result(tpe, handlerEffects) - } - - - - /** - * We defer checking whether something is first-class or second-class to Typer now. - */ - def checkExprAsBlock(expr: Term, expected: Option[BlockType])(using Context, Captures): Result[BlockType] = - checkBlockAgainst(expr, expected) { - case u @ source.Unbox(expr) => - val expectedTpe = expected map { - tpe => - val captVar = Context.freshCaptVar(CaptUnificationVar.InferredUnbox(u)) - BoxedType(tpe, captVar) - } - val Result(vtpe, eff1) = checkExpr(expr, expectedTpe) - // TODO here we also need unification variables for block types! - // C.unify(tpe, BoxedType()) - Context.unification(vtpe) match { - case BoxedType(btpe, capt2) => - usingCapture(capt2) - Result(btpe, eff1) - case _ => - Context.annotationOption(Annotations.UnboxParentDef, u) match { - case Some(source.DefDef(id, annot, block)) => - // Since this `unbox` was synthesized by the compiler from `def foo = E`, - // it's possible that the user simply doesn't know that they should have used the `val` keyword to specify a value - // instead of using `def`; see [issue #130](https://github.com/effekt-lang/effekt/issues/130) for more details - Context.abort(pretty"Expected the right-hand side of a `def` binding to be a block, but got a value of type $vtpe.\nMaybe try `val` if you're defining a value.") - case _ => - Context.abort(pretty"Unbox requires a boxed type, but got $vtpe.") - } - } - - case source.Var(id) => id.symbol match { - case b: BlockSymbol => - val (tpe, capt) = Context.lookup(b) - expected.foreach(exp => matchExpected(tpe, exp)) - usingCapture(capt) - Result(tpe, Pure) - case e: ValueSymbol => - Context.abort(pretty"Expected a block variable, but ${id} is a value. Maybe use explicit syntax: { () => ${id} }") - } - - case source.New(impl) => checkImplementation(impl, None) - - case s : source.MethodCall => sys error "Nested capability selection not yet supported" - - case arg: source.BlockLiteral => expected match { - case Some(tpe: FunctionType) => checkFunctionArgument(arg, tpe) - case Some(tpe) => Context.abort(pretty"Block literal has a function type, but expected type is: ${tpe}") - case None => inferFunctionArgument(arg) - } - - case other => Context.abort("Expected block, but got an expression.") - } - - // - - // - def checkPattern(sc: ValueType, pattern: MatchPattern)(using Context, Captures): Map[Symbol, ValueType] = Context.focusing(pattern) { - case source.IgnorePattern() => Map.empty - case p @ source.AnyPattern(id) => Map(p.symbol -> sc) - case p @ source.LiteralPattern(lit) => - Context.requireSubtype(sc, lit.tpe, ErrorContext.PatternMatch(p)) - Map.empty - case p @ source.TagPattern(id, patterns) => - - // symbol of the constructor we match against - val sym: Constructor = p.definition - - val universals = sym.tparams.take(sym.tpe.tparams.size) - val existentials = sym.tparams.drop(sym.tpe.tparams.size) - - // create fresh unification variables - val freshUniversals = universals.map { t => Context.freshTypeVar(t, pattern) } - // create fresh **bound** variables - val freshExistentials = existentials.map { t => TypeVar.TypeParam(t.name) } - - Context.annotate(Annotations.TypeParameters, p, freshExistentials) - - val targs = (freshUniversals ++ freshExistentials).map { t => ValueTypeRef(t) } - - // (4) Compute blocktype of this constructor with rigid type vars - // i.e. Cons : `(?t1, List[?t1]) => List[?t1]` - val (vps, _, ret, _) = Context.instantiate(sym.toType, targs, Nil) - - // (5) given a scrutinee of `List[Int]`, we learn `?t1 -> Int` - matchPattern(sc, ret, p) - - // (6) check nested patterns - var bindings = Map.empty[Symbol, ValueType] - - if (patterns.size != vps.size) - Context.abort(s"Wrong number of pattern arguments, given ${patterns.size}, expected ${vps.size}.") - - (patterns zip vps) foreach { - case (pat, par: ValueType) => - bindings ++= checkPattern(par, pat) - } - - bindings - case source.MultiPattern(patterns) => - Context.panic("Multi-pattern should have been split at the match and not occur nested.") - } match { case res => Context.annotateInferredType(pattern, sc); res } - - def checkGuard(guard: MatchGuard)(using Context, Captures): Result[Map[Symbol, ValueType]] = guard match { - case MatchGuard.BooleanGuard(condition) => - val Result(tpe, effs) = checkExpr(condition, Some(TBoolean)) - Result(Map.empty, effs) - case MatchGuard.PatternGuard(scrutinee, pattern) => - val Result(tpe, effs) = checkExpr(scrutinee, None) - Result(checkPattern(tpe, pattern), effs) - } - - // - - // - - def checkStmt(stmt: Stmt, expected: Option[ValueType])(using Context, Captures): Result[ValueType] = - checkAgainst(stmt, expected) { - // local mutable state - case source.DefStmt(d @ source.VarDef(id, annot, binding), rest) => - val sym = d.symbol - val stCapt = CaptureSet(sym.capture) - - val Result(tpeBind, effBind) = d.symbol.tpe match { - case Some(t) => binding checkAgainst t - case None => checkStmt(binding, None) - } - val stTpe = TState(tpeBind) - - Context in { - Context.bind(sym, stTpe, stCapt) - val inferredCapture = Context.freshCaptVar(CaptUnificationVar.VarRegion(d)) - usingCapture(inferredCapture) - - flowingInto(Context.without(inferredCapture, List(sym.capture))) { - val Result(res, eff) = checkStmt(rest, expected) - Result(res, eff ++ effBind) - } - } - - case source.DefStmt(b, rest) => - val Result(t, effBinding) = Context in { precheckDef(b); synthDef(b) } - val Result(r, effStmt) = checkStmt(rest, expected) - Result(r, effBinding ++ effStmt) - - // ; - case source.ExprStmt(e, rest) => - val Result(_, eff1) = checkExpr(e, None) - val Result(r, eff2) = checkStmt(rest, expected) - Result(r, eff1 ++ eff2) - - case source.Return(e) => checkExpr(e, expected) - - case source.BlockStmt(stmts) => Context in { checkStmt(stmts, expected) } - } - - // not really checking, only if defs are fully annotated, we add them to the typeDB - // this is necessary for mutually recursive definitions - def precheckDef(d: Def)(using Context): Unit = Context.focusing(d) { - case d @ source.FunDef(id, tps, vps, bps, ret, body) => - val fun = d.symbol - - // (1) make up a fresh capture unification variable and annotate on function symbol - val cap = Context.freshCaptVar(CaptUnificationVar.FunctionRegion(d)) - Context.bind(fun, cap) - - // (2) Store the annotated type (important for (mutually) recursive and out-of-order definitions) - fun.annotatedType.foreach { tpe => Context.bind(fun, tpe) } - - case d @ source.DefDef(id, annot, source.New(source.Implementation(tpe, clauses))) => - val obj = d.symbol - - // (1) make up a fresh capture unification variable - val cap = Context.freshCaptVar(CaptUnificationVar.BlockRegion(d)) - - // (2) annotate capture variable and implemented blocktype - Context.bind(obj, Context.resolvedType(tpe).asInterfaceType, cap) - - case d @ source.ExternDef(cap, id, tps, vps, bps, tpe, body) => - val fun = d.symbol - - Context.bind(fun, fun.toType, fun.capture) - if (fun.effects.canonical.nonEmpty) { - Context.abort("Unhandled control effects on extern defs not allowed") - } - - case d @ source.ExternResource(id, tpe) => - Context.bind(d.symbol) - - case d @ source.InterfaceDef(id, tparams, ops) => - d.symbol.operations.foreach { op => - if (op.effects.toList contains op.appliedInterface) { - Context.error("Bidirectional effects that mention the same effect recursively are not (yet) supported.") - } - - val tpe = op.toType - wellformed(tpe) - Context.bind(op, tpe) - } - - case source.DataDef(id, tparams, ctors) => - ctors.foreach { c => - val constructor = c.symbol - Context.bind(constructor, constructor.toType, CaptureSet()) - constructor.fields.foreach { field => - val tpe = field.toType - wellformed(tpe) - } - } - - case d @ source.RecordDef(id, tparams, fields) => - val constructor = d.symbol.constructor - Context.bind(constructor, constructor.toType, CaptureSet()) - constructor.fields.foreach { field => - val tpe = field.toType - wellformed(tpe) - Context.bind(field, tpe, CaptureSet()) - } - - case d: source.TypeDef => wellformed(d.symbol.tpe) - case d: source.EffectDef => wellformed(d.symbol.effs) - - case _ => () - } - - def synthDef(d: Def)(using Context, Captures): Result[Unit] = Context.at(d) { - d match { - case d @ source.FunDef(id, tps, vps, bps, ret, body) => - val sym = d.symbol - // was assigned by precheck - val functionCapture = Context.lookupCapture(sym) - - // We can also try to solve for the function capture, after checking the function. - // Hence we provide it to `withUnificationScope`. - val captVars = functionCapture match { - case x: CaptUnificationVar => List(x) - case _ => Nil - } - val Result(funTpe, unhandledEffects) = Context.withUnificationScope(captVars) { - - sym.vparams foreach Context.bind - sym.bparams foreach Context.bind - - // to subtract the capabilities, which are only inferred bottom up, we need a **second** unification variable - val inferredCapture = Context.freshCaptVar(CaptUnificationVar.FunctionRegion(d)) - - flowingInto(inferredCapture) { - - (sym.annotatedType: @unchecked) match { - case Some(annotated) => - // the declared effects are considered as bound - val bound: ConcreteEffects = annotated.effects - val capabilities = bound.canonical.map { tpe => Context.freshCapabilityFor(tpe) } - val captures = capabilities.map { _.capture } - - // block parameters and capabilities for effects are assumed bound - val Result(tpe, effs) = Context.bindingCapabilities(d, capabilities) { - Context in { body checkAgainst annotated.result } - } - Context.annotateInferredType(d, tpe) - Context.annotateInferredEffects(d, effs.toEffects) - - // TODO also annotate the capabilities - flowsIntoWithout(inferredCapture, functionCapture) { - annotated.cparams ++ captures - } - - Result(annotated, effs -- bound) - - case None => - // all effects are handled by the function itself (since they are inferred) - val (Result(tpe, effs), caps) = Context.bindingAllCapabilities(d) { - Context in { checkStmt(body, None) } - } - - // We do no longer use the order annotated on the function, but always the canonical ordering. - val capabilities = effs.canonical.map { caps.apply } - val captures = capabilities.map(_.capture) - - Context.bindCapabilities(d, capabilities) - Context.annotateInferredType(d, tpe) - Context.annotateInferredEffects(d, effs.toEffects) - - // we subtract all capabilities introduced by this function to compute its capture - flowsIntoWithout(inferredCapture, functionCapture) { - (sym.bparams ++ capabilities).map(_.capture) - } - - // TODO also add capture parameters for inferred capabilities - val funType = sym.toType(tpe, effs.toEffects, captures) - Result(funType, Pure) - } - } - } - // we bind the function type outside of the unification scope to solve for variables. - val substituted = Context.unification(funTpe) - assertConcreteFunction(id, substituted) - Context.bind(sym, substituted) - - Result((), unhandledEffects) - - case d @ source.ValDef(id, annot, binding) => - val Result(t, effBinding) = d.symbol.tpe match { - case Some(t) => - val Result(_, eff) = binding checkAgainst t - // use annotated, not inferred type - Result(t, eff) - case None => checkStmt(binding, None) - } - - Context.bind(d.symbol, t) - - Result((), effBinding) - - // regions - case d @ source.RegDef(id, annot, reg, binding) => - val sym = d.symbol - // we use the current region as an approximation for the state - val stCapt = Context.symbolOf(reg) match { - case b: BlockSymbol => - Context.lookup(b) match { - case (TRegion, capt) => capt - case _ => Context.at(reg) { Context.abort("Expected a region.") } - } - case _ => Context.at(reg) { Context.abort("Expected a region.") } - } - - // bind region as capture for the variable - Context.bind(sym, stCapt) - - val Result(tpeBind, effBind) = d.symbol.tpe match { - case Some(t) => binding checkAgainst t - case None => checkStmt(binding, None) - } - val stTpe = TState(tpeBind) - - // to allocate into the region, it needs to be live... - usingCapture(stCapt) - - Context.bind(sym, stTpe, stCapt) - - Result((), effBind) - - case d @ source.DefDef(id, annot, binding) => - given inferredCapture: CaptUnificationVar = Context.freshCaptVar(CaptUnificationVar.BlockRegion(d)) - - // we require inferred Capture to be solved after checking this block. - Context.withUnificationScope(List(inferredCapture)) { - val Result(t, effBinding) = checkExprAsBlock(binding, d.symbol.tpe) - Context.bind(d.symbol, t, inferredCapture) - Result((), effBinding) - } - - case d @ source.ExternDef(captures, id, tps, vps, bps, tpe, bodies) => Context.withUnificationScope { - val sym = d.symbol - sym.vparams foreach Context.bind - sym.bparams foreach Context.bind - - flowingInto(Context.lookupCapture(sym)) { - - // Note: Externs are always annotated with a type - val expectedReturnType = d.symbol.annotatedType.get.result - bodies.foreach { - case source.ExternBody.StringExternBody(ff, body) => - body.args.foreach { arg => checkExpr(arg, None) } - case source.ExternBody.EffektExternBody(ff, body) => - checkStmt(body, Some(expectedReturnType)) - case u: source.ExternBody.Unsupported => u - } - - } - - Result((), Pure) - } - - // all other definitions have already been prechecked - case d => - Result((), Pure) - } - } - - // - - // - - // Example. - // BlockParam: def foo { f: Int => String / Print } - // BlockArg: foo { n => println("hello" + n) } - // or - // BlockArg: foo { (n: Int) => println("hello" + n) } - // - // TODO For now we assume that handled effects can not show up in the return type of the block argument. - def checkFunctionArgument(arg: source.BlockLiteral, expected: FunctionType)(using Context, Captures): Result[BlockType] = Context.focusing(arg) { - - case decl @ source.BlockLiteral(tparams, vparams, bparams, body) => - - // (1) Apply what we already know. - val bt @ FunctionType(tps, cps, vps, bps, tpe1, effs) = expected - - // (2) Check wellformedness - if (tps.size != tparams.size) - Context.abort(s"Wrong number of type arguments, given ${tparams.size}, but function expects ${tps.size}.") - - if (vps.size != vparams.size) - Context.abort(s"Wrong number of value arguments, given ${vparams.size}, but function expects ${vps.size}.") - - if (bps.size != bparams.size) - Context.abort(s"Wrong number of block arguments, given ${bparams.size}, but function expects ${bps.size}.") - - // (3) Substitute type parameters - val typeParams = tparams.map { p => p.symbol.asTypeParam } - val typeSubst = Substitutions.types(tps, typeParams.map { p => ValueTypeRef(p) }) - - // (4) Check type annotations against declaration - val valueTypes = (vparams zip vps) map { - case (param, expected) => - val adjusted = typeSubst substitute expected - // check given matches the expected, if given at all - val tpe = param.symbol.tpe.map { got => - matchDeclared(got, adjusted, param); - got - } getOrElse { adjusted } - // bind types to check body - Context.bind(param.symbol, tpe) - tpe - } - - val blockTypes = (bparams zip bps) map { - case (param, expTpe) => - val adjusted = typeSubst substitute expTpe - val sym = param.symbol - // check given matches the expected, if given at all - val got = sym.tpe.map { got => - matchDeclared(got, adjusted, param) - got - } getOrElse { adjusted } - // bind types to check body - Context.bind(param.symbol, got, CaptureSet(sym.capture)) - got - } - - // (4) Bind capabilities for all effects "handled" by this function - val effects: ConcreteEffects = typeSubst substitute effs - val capabilities = effects.canonical.map { tpe => Context.freshCapabilityFor(tpe) } - - // (5) Substitute capture params - val captParams = (bparams.map(_.symbol) ++ capabilities).map { p => p.capture } - val captSubst = Substitutions.captures(cps, captParams.map { p => CaptureSet(p) }) - - // (6) Substitute both types and captures into expected return type - val subst = typeSubst ++ captSubst - - val expectedReturn = subst substitute tpe1 - - // (7) Check function body - val bodyRegion = Context.freshCaptVar(CaptUnificationVar.AnonymousFunctionRegion(arg)) - - val Result(bodyType, bodyEffs) = Context.bindingCapabilities(decl, capabilities) { - flowingInto(bodyRegion) { body checkAgainst expectedReturn } - } - - usingCaptureWithout(bodyRegion) { captParams } - - val tpe = FunctionType(typeParams, captParams, valueTypes, blockTypes, bodyType, effects.toEffects) - - Result(tpe, bodyEffs -- effects) - } - - def inferFunctionArgument(arg: source.BlockLiteral)(using Context, Captures): Result[BlockType] = Context.focusing(arg) { - case arg @ source.BlockLiteral(tparams, vparams, bparams, body) => Context in { - val tps = tparams.map { p => p.symbol.asTypeParam } - val vps = vparams.map { p => - val param = p.symbol - val tpe = p.symbol.tpe.getOrElse { - INTERNAL_ERROR("Expected type needs to be known for function arguments at the moment.") - } - Context.bind(param, tpe) - tpe - } - val bps = bparams.map { p => - val param = p.symbol - val tpe = param.tpe.getOrElse { - INTERNAL_ERROR("Expected type need to be know for function arguments at the moment.") - } - Context.bind(param, tpe) - tpe - } - - // like with non-annotated function definitions, we need to use a separate unification variable to - // subtract bound (but inferred) capabilities later. - val inferredCapture = Context.freshCaptVar(CaptUnificationVar.AnonymousFunctionRegion(arg)) - val (Result(tpe, effs), caps) = Context.bindingAllCapabilities(arg) { - flowingInto(inferredCapture) { - Context in { checkStmt(body, None) } - } - } - - // The order of effects annotated to the function is the canonical ordering for capabilities - val capabilities = effs.canonical.map { caps.apply } - Context.bindCapabilities(arg, capabilities) - - val cps = (bparams.map(_.symbol) ++ capabilities).map(_.capture) - - val funType = FunctionType(tps, cps, vps, bps, tpe, effs.toEffects) - - // Like with functions, bound parameters and capabilities are not closed over - usingCaptureWithout(inferredCapture) { - (bparams.map(_.symbol) ++ capabilities).map(_.capture) - } - - Result(funType, Pure) - } - } - - def findFunctionTypeFor(sym: BlockSymbol)(using Context): (FunctionType, Captures) = sym match { - // capture of effect operations is dealt with by type checking Do or MethodCall - case b: Operation => (Context.lookupFunctionType(b), CaptureSet.empty) - case b: BlockSymbol => (Context.lookupFunctionType(b), Context.lookupCapture(b)) - } - - def attempt[T](f: => T)(using Context): Either[EffektMessages, (T, TyperState)] = - val stateBefore = Context.backupTyperstate() - try { - Try { - val result = f - (result, Context.backupTyperstate()) - } - } finally { - Context.restoreTyperstate(stateBefore) - } - - /** - * We do not respect nested scoping on overload resolution for methods right now. - * - * Uniform function syntax has been desugared by [[PreTyper]] - */ - def checkOverloadedMethodCall( - call: source.CallLike, - receiver: source.Term, - id: source.IdRef, - targs: List[ValueType], - vargs: List[source.Term], - bargs: List[source.Term], - expected: Option[ValueType] - )(using Context, Captures): Result[ValueType] = { - val sym = id.symbol - - val methods = sym match { - // an overloaded call target - case CallTarget(syms) => syms.flatten.collect { case op: Operation => op } - // already resolved by a previous attempt to typecheck - case sym: Operation => List(sym) - case s => Context.panic(s"Not a valid method: ${s} : ${s.getClass.getSimpleName}") - } - - val Result(recvTpe, recvEffs) = checkExprAsBlock(receiver, None) - - val interface = recvTpe.asInterfaceType - // filter out operations that do not fit the receiver - val candidates = methods.filter(op => op.interface == interface.typeConstructor) - - val (successes, errors) = tryEach(candidates) { op => - val (funTpe, capture) = findFunctionTypeFor(op) - - // 1) check arity of explicitly provided type arguments - - if (targs.nonEmpty && targs.size != funTpe.tparams.size) - Context.abort(s"Wrong number of type arguments, given ${targs.size} but expected ${funTpe.tparams.size}") - - // 2) - // args present: check prefix against receiver - (targs zip interface.args).foreach { case (manual, inferred) => - matchExpected(inferred, manual) - } - - // args missing: synthesize args from receiver and unification variables (e.g. [Int, String, ?A, ?B]) - def fillInTypeArguments(tparams: List[TypeParam], interfaceArgs: List[ValueType]): List[ValueType] = - (tparams, interfaceArgs) match { - // we have an argument, provided by the interface: use it - case (param :: params, arg :: args) => arg :: fillInTypeArguments(params, args) - // we don't have an argument, create fresh unification variable - case (param :: params, Nil) => ValueTypeRef(Context.freshTypeVar(param, call)) :: fillInTypeArguments(params, Nil) - case (Nil, _) => Nil - } - - val synthTargs = if targs.nonEmpty then targs else fillInTypeArguments(funTpe.tparams, interface.args) - - // TODO maybe type checking all calls should have a similar structure like above: - // 1. make up and annotate unification variables, if no type arguments are there - // 2. type check call with either existing or made up type arguments - - checkCallTo(call, op.name.name, funTpe, synthTargs, vargs, bargs, expected) - } - resolveOverload(id, List(successes), errors) - } - - /** - * Attempts to check a potentially overladed call, not reporting any errors but returning them instead. - * - * This is necessary for overload resolution by trying all alternatives. - * - if there is multiple without errors: Report ambiguity - * - if there is no without errors: report all possible solutions with corresponding errors - */ - def checkOverloadedFunctionCall( - call: source.CallLike, - id: source.IdRef, - targs: List[ValueType], - vargs: List[source.Term], - bargs: List[source.Term], - expected: Option[ValueType] - )(using Context, Captures): Result[ValueType] = { - - val scopes = id.symbol match { - // an overloaded call target - case CallTarget(syms) => syms - // already resolved by a previous attempt to typecheck - case sym: BlockSymbol => List(Set(sym)) - case id: ValueSymbol => Context.abort(pp"Cannot call value ${id}") - } - - // TODO right now unhandled effects (via capability search) influences overload resolution. - // examples/neg/see existential_effect_leaks.effekt - // - // We should establish proper shadowing here! - // - // Potential Design - // ---------------- - // We can get rid of the complexity of backtracking by - // 1) filter out overloads that do not match arity / kind wise - // 2) check whether there are multiple functions that overlap on their value arguments (raise ambiguity if that is the case) - // 3) infer the value arguments *once* without expected type. - // - // For each (non-empty) scope, - // For each candidate in that scope (might require backtracking on the unifier!): - // + see whether the value arguments *could* unify with the expected value parameter types - // - If there are multiple possible candidates -> Ambiguity Error - // - If there is none: proceed to outer scope - // - If there is exactly one match, fully typecheck the call with this. - val results = scopes map { scope => tryEach(scope.toList) { receiver => - val (funTpe, capture) = findFunctionTypeFor(receiver) - val Result(tpe, effs) = checkCallTo(call, receiver.name.name, funTpe, targs, vargs, bargs, expected) - // This is different, compared to method calls: - usingCapture(capture) - Result(tpe, effs) - }} - - val successes = results.map { scope => scope._1 } - val errors = results.flatMap { scope => scope._2 } - - resolveOverload(id, successes, errors) - } - - private def resolveOverload( - id: source.IdRef, - successes: List[List[(BlockSymbol, Result[ValueType], TyperState)]], - failures: List[(BlockSymbol, EffektMessages)] - )(using Context): Result[ValueType] = { - - successes foreachAborting { - // continue in outer scope - case Nil => () - - // Exactly one successful result in the current scope - case List((sym, tpe, st)) => - // use the typer state after this checking pass - Context.restoreTyperstate(st) - // reassign symbol of fun to resolved calltarget symbol - Context.assignSymbol(id, sym) - - return tpe - - // Ambiguous reference - case results => - val successfulOverloads = results.map { (sym, res, st) => (sym, findFunctionTypeFor(sym)._1) } - Context.abort(AmbiguousOverloadError(successfulOverloads, Context.rangeOf(id))) - } - - failures match { - case Nil => - Context.abort("Cannot typecheck call.") - - // exactly one error - case List((sym, errs)) => - Context.abortWith(errs) - - case failed => - // reraise all and abort - val failures = failed.map { case (block, msgs) => (block, findFunctionTypeFor(block)._1, msgs) } - Context.abort(FailedOverloadError(failures, Context.currentRange)) - } - } - - def checkCallTo( - call: source.CallLike, - name: String, - funTpe: FunctionType, - targs: List[ValueType], - vargs: List[source.Term], - bargs: List[source.Term], - expected: Option[ValueType] - )(using Context, Captures): Result[ValueType] = { - - if (targs.nonEmpty && targs.size != funTpe.tparams.size) - Context.abort(s"Wrong number of type arguments, given ${targs.size}, but ${name} expects ${funTpe.tparams.size}.") - - if (vargs.size != funTpe.vparams.size) - Context.abort(s"Wrong number of value arguments, given ${vargs.size}, but ${name} expects ${funTpe.vparams.size}.") - - if (bargs.size != funTpe.bparams.size) - Context.abort(s"Wrong number of block arguments, given ${bargs.size}, but ${name} expects ${funTpe.bparams.size}.") - - val callsite = currentCapture - - // (1) Instantiate blocktype - // e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B` - val (typeArgs, captArgs, (vps, bps, ret, retEffs)) = Context.instantiateFresh(CanonicalOrdering(funTpe)) - - // provided type arguments flow into the fresh unification variables (i.e., Int <: ?A) - if (targs.nonEmpty) (targs zip typeArgs).foreach { case (targ, tvar) => matchExpected(tvar, targ) } - - // (2) check return type - expected.foreach { expected => matchExpected(ret, expected) } - - var effs: ConcreteEffects = Pure - - (vps zip vargs) foreach { case (tpe, expr) => - val Result(t, eff) = checkExpr(expr, Some(tpe)) - effs = effs ++ eff - } - - // To improve inference, we first type check block arguments that DO NOT subtract effects, - // since those need to be fully known. - - val (withoutEffects, withEffects) = (bps zip (bargs zip captArgs)).partitionMap { - // TODO refine and check that eff.args refers to (inferred) type arguments of this application (`typeArgs`) - case (tpe : FunctionType, rest) if tpe.effects.exists { eff => eff.args.nonEmpty } => Right((tpe, rest)) - case (tpe, rest) => Left((tpe, rest)) - } - - (withoutEffects ++ withEffects) foreach { case (tpe, (expr, capt)) => - flowsInto(capt, callsite) - // capture of block <: ?C - flowingInto(capt) { - val Result(t, eff) = checkExprAsBlock(expr, Some(tpe)) - effs = effs ++ eff - } - } - - // We add return effects last to have more information at this point to - // concretize the effect. - effs = effs ++ Effects(retEffs) - - // annotate call node with inferred type arguments - Context.annotateTypeArgs(call, typeArgs) - - // Annotate the call target tree with the additional capabilities - // The canonical ordering of capabilities is defined by the ordering of the definition site, - // not the callsite, so we need to be careful to not use `distinct` on `retEffs` - // since this might conflate State[A] and State[B] after A -> Int, B -> Int. - val capabilities = Context.provideCapabilities(call, retEffs.map(Context.unification.apply)) - - val captParams = captArgs.drop(bargs.size) - (captParams zip capabilities) foreach { case (param, cap) => - flowsInto(CaptureSet(cap.capture), param) - } - usingCapture(CaptureSet(capabilities.map(_.capture))) - - Result(ret, effs) - } - - def tryEach[K, R](inputs: List[K])(f: K => R)(using Context): (List[(K, R, TyperState)], List[(K, EffektMessages)]) = { - val stateBefore = Context.backupTyperstate() - val results = inputs.map { - case input => - try { input -> - Try { - val result = f(input) - val state = Context.backupTyperstate() - (result, state) - } - } finally { Context.restoreTyperstate(stateBefore) } - } - val successes = results.collect { case (sym, Right((r, st))) => (sym, r, st) } - val errors = results.collect { case (sym, Left(r)) => (sym, r) } - (successes, errors) - } - - /** - * Returns Left(Messages) if there are any errors - * - * In the case of nested calls, currently only the errors of the innermost failing call - * are reported - */ - private def Try[T](block: => T)(using C: Context): Either[EffektMessages, T] = { - import kiama.util.Severities.Error - - val (msgs, optRes) = Context withMessages { - try { Some(block) } catch { - case FatalPhaseError(msg) => - C.report(msg) - None - } - } - - if (msgs.exists { m => m.severity == Error } || optRes.isEmpty) { - Left(msgs) - } else { - Right(optRes.get) - } - } - - // - - // - - /** - * The current capture / region / scope for which we collect constraints. - * - * Functions [[usingCapture]] and [[usingCaptureWithout]] implicitly look - * up this scope and have their arguments flow into the current Capture. - * - * Function [[flowingInto]] sets the current capture. - * - * Example illustrating the interaction: - * - * {{{ - * val inferredCapture: Captures = ??? - * flowingInto(inferredCapture) { - * // inferredCapture is now the current capture - * assert(currentCapture == inferredCapture) - * - * // other flows into inferredCapture (i.e. other <: inferredCapture) - * usingCapture(other) - * } - * }}} - */ - def currentCapture(using current: Captures): Captures = current - - /** - * Sets the [[currentCapture]] in a given scope. - */ - def flowingInto[T](c: Captures)(prog: Captures ?=> T): T = prog(using c) - - /** - * Requires that [[c]] is included in the [[currentCapture]] (i.e. [[c]] <: [[currentCapture]]) - */ - def usingCapture(c: Captures)(using C: Context, current: Captures): Unit = - C.requireSubregion(c, current) - - /** - * Requires that [[c]] - [[filter]] is included in the [[currentCapture]] (i.e. ([[c]] - [[filter]]) <: [[currentCapture]]) - */ - def usingCaptureWithout(c: Captures)(filter: List[Capture])(using C: Context, current: Captures): Unit = - flowsIntoWithout(c, current)(filter) - - /** - * Requires that [[from]] is included in [[to]] (i.e. [[from]] <: [[to]]) - */ - def flowsInto(from: Captures, to: Captures)(using C: Context): Unit = - C.requireSubregion(from, to) - - /** - * Requires that [[from]] - [[filter]] is included in the [[to]] (i.e. ([[from]] - [[filter]]) <: [[to]]) - */ - def flowsIntoWithout(from: Captures, to: Captures)(filter: List[Capture])(using C: Context): Unit = - C.requireSubregionWithout(from, to, filter) - - // - - // - def matchDeclared(got: BlockType, declared: BlockType, param: source.Param)(using Context): Unit = - Context.at(param) { - Context.requireSubtype(got, declared, - ErrorContext.Declaration(param, Context.unification(declared), Context.unification(got))) - } - def matchDeclared(got: ValueType, declared: ValueType, param: source.Param)(using Context): Unit = - Context.at(param) { - Context.requireSubtype(got, declared, - ErrorContext.Declaration(param, Context.unification(declared), Context.unification(got))) - } - - def matchPattern(scrutinee: ValueType, patternTpe: ValueType, pattern: source.MatchPattern)(using Context): Unit = - Context.requireSubtype(scrutinee, patternTpe, ErrorContext.PatternMatch(pattern)) - - def matchExpected(got: ValueType, expected: ValueType)(using Context): Unit = - Context.requireSubtype(got, expected, - ErrorContext.Expected(Context.unification(got), Context.unification(expected), Context.focus)) - - def matchExpected(got: BlockType, expected: BlockType)(using Context): Unit = - Context.requireSubtype(got, expected, - ErrorContext.Expected(Context.unification(got), Context.unification(expected), Context.focus)) - - extension (expr: Term) { - def checkAgainst(tpe: ValueType)(using Context, Captures): Result[ValueType] = - checkExpr(expr, Some(tpe)) - } - - extension (stmt: Stmt) { - def checkAgainst(tpe: ValueType)(using Context, Captures): Result[ValueType] = - checkStmt(stmt, Some(tpe)) - } - - /** - * Combinators that also store the computed type for a tree in the TypesDB - */ - def checkAgainst[T <: Tree](t: T, expected: Option[ValueType])(f: T => Result[ValueType])(using Context, Captures): Result[ValueType] = - Context.at(t) { - val Result(got, effs) = f(t) - wellformed(got) - wellformed(effs.toEffects) - expected foreach { matchExpected(got, _) } - Context.annotateInferredType(t, got) - Context.annotateInferredEffects(t, effs.toEffects) - Result(got, effs) - } - - def checkBlockAgainst[T <: Tree](t: T, expected: Option[BlockType])(f: T => Result[BlockType])(using Context, Captures): Result[BlockType] = - Context.at(t) { - val Result(got, effs) = f(t) - wellformed(got) - wellformed(effs.toEffects) - expected foreach { matchExpected(got, _) } - Context.annotateInferredType(t, got) - Context.annotateInferredEffects(t, effs.toEffects) - Result(got, effs) - } - // - -} - -/** - * Instances of this class represent an immutable backup of the typer state - */ -private[typer] case class TyperState(annotations: Annotations, unification: UnificationState, capabilityScope: CapabilityScope) - -trait TyperOps extends ContextOps { self: Context => - - - /** - * Local annotations database, only used by Typer - * - * It is used to (1) model the typing context, (2) collect information - * used for elaboration (i.e., capabilities), and (3) gather inferred - * types for LSP support. - * - * (1) "Typing Context" - * -------------------- - * Since symbols are unique, we can use mutable state instead of reader. - * Typer uses local annotations that are immutable and can be backtracked. - * - * The "Typing Context" consists of: - * - typing context for value types [[Annotations.ValueType]] - * - typing context for block types [[Annotations.BlockType]] - * - modalities on typing context for block symbol [[Annotations.Captures]] - * - * (2) Elaboration Info - * -------------------- - * - [[Annotations.CapabilityReceiver]] - * - [[Annotations.CapabilityArguments]] - * - [[Annotations.BoundCapabilities]] - * - [[Annotations.TypeArguments]] - * - * (3) Inferred Information for LSP - * -------------------------------- - * We first store the inferred types here, before substituting and committing to the - * global DB, later. - * - [[Annotations.InferredValueType]] - * - [[Annotations.InferredBlockType]] - * - [[Annotations.InferredEffect]] - */ - private [typer] var annotations: Annotations = Annotations.empty - - // - - /** - * The unification engine, keeping track of constraints and the current unification scope - * - * Contains mutable variables. The methods [[unification.backup()]] and [[unification.restore()]] - * allow to save a copy of the current state. - */ - private[typer] val unification = new Unification(using this) - export unification.{ requireSubtype, requireSubregion, join, instantiate, instantiateFresh, freshTypeVar, freshCaptVar, without, requireSubregionWithout } - - // opens a fresh unification scope - private[typer] def withUnificationScope[T](additional: List[CaptUnificationVar])(block: => T): T = { - unification.enterScope() - val res = block - unification.leaveScope(additional) - res - } - private[typer] def withUnificationScope[T](block: => T): T = withUnificationScope(Nil)(block) - - // - - // - - private [typer] var capabilityScope: CapabilityScope = GlobalCapabilityScope - - private [typer] def bindingCapabilities[R](binder: source.Tree, caps: List[symbols.BlockParam])(f: => R): R = { - bindCapabilities(binder, caps) - capabilityScope = BindSome( - binder, - caps.map { c => c.tpe.getOrElse { INTERNAL_ERROR("Capability type needs to be know.") }.asInterfaceType -> c }.toMap, - capabilityScope - ) - val result = f - capabilityScope = capabilityScope.parent - result - } - - private [typer] def bindCapabilities[R](binder: source.Tree, caps: List[symbols.BlockParam]): Unit = - val capabilities = caps map { cap => - assertConcreteEffect(cap.tpe.getOrElse { INTERNAL_ERROR("Capability type needs to be know.") }.asInterfaceType) - positions.dupPos(binder, cap) - cap - } - annotations.update(Annotations.BoundCapabilities, binder, capabilities) - - private [typer] def bindingAllCapabilities[R](binder: source.Tree)(f: => R): (R, Map[InterfaceType, symbols.BlockParam]) = { - capabilityScope = BindAll(binder, Map.empty, capabilityScope) - val result = f - val caps = capabilityScope.asInstanceOf[BindAll].capabilities - capabilityScope = capabilityScope.parent - (result, caps) - } - - /** - * Has the potential side-effect of creating a fresh capability. Also see [[BindAll.capabilityFor()]] - */ - private [typer] def capabilityFor(tpe: InterfaceType): symbols.BlockParam = - assertConcreteEffect(tpe) - val cap = capabilityScope.capabilityFor(tpe) - annotations.update(Annotations.Captures, cap, CaptureSet(cap.capture)) - cap - - private [typer] def freshCapabilityFor(tpe: InterfaceType): symbols.BlockParam = - val param: BlockParam = BlockParam(tpe.name, Some(tpe)) - // TODO FIXME -- generated capabilities need to be ignored in LSP! -// { -// override def synthetic = true -// } - bind(param, tpe) - param - - private [typer] def freshCapabilityFor(tpe: InterfaceType, capture: CaptureSet): symbols.BlockParam = - val param = freshCapabilityFor(tpe) - bind(param, capture) - param - - private [typer] def provideCapabilities(call: source.CallLike, effs: List[InterfaceType]): List[BlockParam] = - val caps = effs.map(capabilityFor) - annotations.update(Annotations.CapabilityArguments, call, caps) - caps - - private [typer] def capabilityReceiver(call: source.Do, eff: InterfaceType): BlockParam = - val cap = capabilityFor(eff) - annotations.update(Annotations.CapabilityReceiver, call, cap) - cap - - // - - // - - // first tries to find the type in the local typing context - // if not found, it tries the global DB, since it might be a symbol of an already checked dependency - private[typer] def lookup(s: ValueSymbol) = - annotations.getOrElse(Annotations.ValueType, s, valueTypeOf(s)) - - private[typer] def lookup(s: BlockSymbol) = (lookupBlockType(s), lookupCapture(s)) - - private[typer] def lookupFunctionType(s: BlockSymbol): FunctionType = - annotations.get(Annotations.BlockType, s) - .map { - case f: FunctionType => unification(f) // here we apply the substitutions known so far. - case tpe => abort(pretty"Expected function type, but got ${tpe}.") - } - .orElse(functionTypeOption(s)) - .getOrElse { - if (s.name.name == "resume") - abort(pretty"Cannot find `resume`. Maybe you are trying to resume inside of an object literal and not as part of `try { ... } with ...`?") - else - abort(pretty"Cannot find type for ${s.name} -- forward uses and recursive functions need annotated return types.") - } - - private[typer] def lookupBlockType(s: BlockSymbol): BlockType = - annotations.get(Annotations.BlockType, s).orElse(blockTypeOption(s)).getOrElse(abort(pretty"Cannot find type for ${s.name}.")) - - private[typer] def lookupCapture(s: BlockSymbol) = - annotations.get(Annotations.Captures, s).orElse(captureOfOption(s)).getOrElse { - s match { - case b: TrackedParam => CaptureSet(b.capture) - case _ => panic(pretty"Shouldn't happen: we do not have a capture for ${s}, yet.") - } - } - - private[typer] def bind(s: ValueSymbol, tpe: ValueType): Unit = - annotations.update(Annotations.ValueType, s, tpe) - - private[typer] def bind(s: BlockSymbol, tpe: BlockType, capt: Captures): Unit = { bind(s, tpe); bind(s, capt) } - - private[typer] def bind(s: BlockSymbol, tpe: BlockType): Unit = - annotations.update(Annotations.BlockType, s, tpe) - - private[typer] def bind(s: BlockSymbol, capt: Captures): Unit = - annotations.update(Annotations.Captures, s, capt) - - private[typer] def bind(bs: Map[Symbol, ValueType]): Unit = - bs foreach { - case (v: ValueSymbol, t: ValueType) => bind(v, t) - case (sym, other) => panic(pretty"Internal Error: wrong combination of symbols and types: ${sym}:${other}") - } - - private[typer] def bind(p: ValueParam): Unit = p match { - case s @ ValueParam(name, Some(tpe)) => bind(s, tpe) - case s => panic(pretty"Internal Error: Cannot add $s to typing context.") - } - - private[typer] def bind(p: TrackedParam): Unit = p match { - case s @ BlockParam(name, tpe) => bind(s, tpe.get, CaptureSet(p.capture)) - case s @ ExternResource(name, tpe) => bind(s, tpe, CaptureSet(p.capture)) - case s : VarBinder => bind(s, CaptureSet(s.capture)) - case r : ResumeParam => panic("Cannot bind resume") - } - - // - - - // - - private[typer] def annotateInferredType(t: Tree, e: ValueType) = - annotations.update(Annotations.InferredValueType, t, e) - - private[typer] def annotateInferredType(t: Tree, e: BlockType) = - annotations.update(Annotations.InferredBlockType, t, e) - - private[typer] def annotateInferredEffects(t: Tree, e: Effects) = - annotations.update(Annotations.InferredEffect, t, e) - - private[typer] def annotateTypeArgs(call: source.CallLike, targs: List[symbols.ValueType]): Unit = { - // apply what we know before saving - annotations.update(Annotations.TypeArguments, call, targs map unification.apply) - } - - private[typer] def annotatedTypeArgs(call: source.CallLike): List[symbols.ValueType] = { - annotations.apply(Annotations.TypeArguments, call) - } - - // - - // - - private[typer] def initTyperstate(): Unit = { - annotations = Annotations.empty - capabilityScope = GlobalCapabilityScope - unification.init() - } - - private[typer] def backupTyperstate(): TyperState = - TyperState(annotations.copy, unification.backup(), capabilityScope.copy) - - private[typer] def restoreTyperstate(st: TyperState): Unit = { - annotations = st.annotations.copy - unification.restore(st.unification) - capabilityScope = st.capabilityScope.copy - } - - private[typer] def commitTypeAnnotations(): Unit = { - val subst = unification.substitution - - var capturesForLSP: List[(Tree, CaptureSet)] = Nil - - // Since (in comparison to System C) we now have type directed overload resolution again, - // we need to make sure the typing context and all the annotations are backtrackable. - // This can be achieved by going back to local `annotations` which are easily backtrackable. - // In the end, we need to postprocess the annotations; see draft below... - annotations.updateAndCommit(Annotations.ValueType) { case (t, tpe) => subst.substitute(tpe) } - annotations.updateAndCommit(Annotations.BlockType) { case (t, tpe) => subst.substitute(tpe) } - annotations.updateAndCommit(Annotations.Captures) { case (t, capt) => subst.substitute(capt) } - - // Update and write out all inferred types and captures for LSP support - // This info is currently also used by Transformer! - annotations.updateAndCommit(Annotations.InferredValueType) { case (t, tpe) => subst.substitute(tpe) } - annotations.updateAndCommit(Annotations.InferredBlockType) { case (t, tpe) => subst.substitute(tpe) } - annotations.updateAndCommit(Annotations.InferredEffect) { case (t, effs) => subst.substitute(effs) } - - annotations.updateAndCommit(Annotations.TypeArguments) { case (t, targs) => targs map subst.substitute } - - annotations.updateAndCommit(Annotations.BoundCapabilities) { case (t, caps) => caps } - annotations.updateAndCommit(Annotations.CapabilityArguments) { case (t, caps) => caps } - annotations.updateAndCommit(Annotations.CapabilityReceiver) { case (t, caps) => caps } - } - - // -} diff --git a/effekt/shared/src/main/scala/effekt/context/Annotations.scala b/effekt/shared/src/main/scala/effekt/context/Annotations.scala deleted file mode 100644 index 0e34c48c8..000000000 --- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala +++ /dev/null @@ -1,627 +0,0 @@ -package effekt -package context - -import effekt.symbols.{BlockSymbol, BlockType, ResumeParam, Symbol, ValueSymbol} -import effekt.util.messages.ErrorReporter - -import java.util - -sealed trait Annotation[K, V] - -case class SymbolAnnotation[K <: symbols.Symbol, V](name: String, description: String) extends Annotation[K, V] { - type Value = V - - override def toString = name -} - -case class SourceAnnotation[K <: kiama.util.Source, V](name: String, description: String) extends Annotation[K, V] { - type Value = V - - override def toString = name -} - -case class TreeAnnotation[K <: source.Tree, V](name: String, description: String) extends Annotation[K, V] { - type Value = V - - override def toString = name -} - -/** - * "local" annotations that can be backtracked - * - * Local annotations can be backed-up and restored to allow backtracking - * (mostly for typer and overload resolution). - * - * Local annotations can be "comitted" to become global ones in the DB, - * that are assumed to not change anymore. - */ -class Annotations private( - /** - * Local Annotations are organized differently to allow simple access. - */ - private var annotations: Map[Annotation[_, _], Map[Annotations.Key[Any], Any]] -) { - import Annotations._ - - def copy: Annotations = new Annotations(annotations) - - def annotationsAt[K, V](ann: Annotation[K, V]): Map[Key[K], V] = - annotations.getOrElse(ann, Map.empty).asInstanceOf - - def update[K, V](ann: Annotation[K, V], key: K, value: V): Unit = { - val anns = annotationsAt(ann) - val updatedAnns = anns.updated(Key(key), value) - annotations = annotations.updated(ann, updatedAnns.asInstanceOf) - } - - def get[K, V](ann: Annotation[K, V], key: K): Option[V] = - annotationsAt(ann).get(Key(key)) - - def getOrElse[K, V](ann: Annotation[K, V], key: K, default: => V): V = - annotationsAt(ann).getOrElse(Key(key), default) - - def getOrElseUpdate[K, V](ann: Annotation[K, V], key: K, default: => V): V = - annotationsAt(ann).getOrElse(Key(key), { - val value = default - update(ann, key, value) - value - }) - - def removed[K, V](ann: Annotation[K, V], key: K): Unit = - annotations = annotations.updated(ann, annotationsAt(ann).removed(Key(key)).asInstanceOf) - - def apply[K, V](ann: Annotation[K, V]): List[(K, V)] = - annotationsAt(ann).map { case (k, v) => (k.key, v) }.toList - - def apply[K, V](ann: Annotation[K, V], key: K)(using C: ErrorReporter): V = - get(ann, key).getOrElse { C.abort(s"Cannot find ${ann.toString} '${key}'") } - - def updateAndCommit[K, V](ann: Annotation[K, V])(f: (K, V) => V)(using treesDB: TreeAnnotations, symbolsDB: SymbolAnnotations): Unit = - val anns = annotationsAt(ann) - anns.foreach { case (kk, v) => - kk.key match { - case sym: symbols.Symbol => - symbolsDB.annotate(ann.asInstanceOf[SymbolAnnotation[_, V]], sym, f(sym, v)) - case key: source.Tree => - treesDB.annotate(ann.asInstanceOf[TreeAnnotation[_, V]], key, f(key, v)) - } - } - - override def toString = s"Annotations(${annotations})" -} -object Annotations { - - def empty: Annotations = new Annotations(Map.empty) - - class Key[T](val key: T) { - override val hashCode = System.identityHashCode(key) - - override def equals(o: Any) = o match { - case k: Key[_] => hashCode == k.hashCode - case _ => false - } - } - - /** - * The as inferred by typer at a given position in the tree - * - * Can also be used by LSP server to display type information for type-checked trees - */ - val InferredEffect = TreeAnnotation[source.Tree, symbols.Effects]( - "InferredEffect", - "the inferred effect of" - ) - - /** - * The type as inferred by typer at a given position in the tree - * - * Important for finding the types of temporary variables introduced by transformation - * Can also be used by LSP server to display type information for type-checked trees - */ - val InferredValueType = TreeAnnotation[source.Tree, symbols.ValueType]( - "InferredValueType", - "the inferred type of" - ) - - /** - * The type as inferred by typer at a given position in the tree - */ - val InferredBlockType = TreeAnnotation[source.Tree, symbols.BlockType]( - "InferredBlockType", - "the inferred block type of" - ) - - /** - * Type arguments of a _function call_ as inferred by typer - */ - val TypeArguments = TreeAnnotation[source.CallLike, List[symbols.ValueType]]( - "TypeArguments", - "the inferred or annotated type arguments of" - ) - - /** - * Existential type parameters inferred by the typer when type-checking pattern matches. - */ - val TypeParameters = TreeAnnotation[source.TagPattern | source.OpClause, List[symbols.TypeVar]]( - "TypeParameters", - "the existentials of the constructor pattern or operation clause" - ) - - /** - * Value type of symbols like value binders or value parameters - */ - val ValueType = SymbolAnnotation[symbols.ValueSymbol, symbols.ValueType]( - "ValueType", - "the type of value symbol" - ) - - /** - * Block type of symbols like function definitions, block parameters, or continuations - */ - val BlockType = SymbolAnnotation[symbols.BlockSymbol, symbols.BlockType]( - "BlockType", - "the type of block symbol" - ) - - /** - * Capability set used by a function definition, block parameter, ... - */ - val Captures = SymbolAnnotation[symbols.BlockSymbol, symbols.Captures]( - "Captures", - "the set of used capabilities of a block symbol" - ) - - /** - * Used by LSP to list all captures - */ - val CaptureForFile = SourceAnnotation[kiama.util.Source, List[(source.Tree, symbols.CaptureSet)]]( - "CaptureSet", - "all inferred captures for file" - ) - - /** - * The module a given symbol is defined in - * - * @deprecated - */ - val SourceModule = SymbolAnnotation[symbols.Symbol, symbols.Module]( - "SourceModule", - "the source module of symbol" - ) - - /** - * Used by LSP for jump-to-definition of imports - */ - val IncludedSymbols = TreeAnnotation[source.Include, symbols.Module]( - "IncludedSymbols", - "the symbol for an import / include" - ) - - /** - * All symbols defined in a source file - */ - val DefinedSymbols = SourceAnnotation[kiama.util.Source, Set[symbols.Symbol]]( - "DefinedSymbols", - "all symbols for source file" - ) - - /** - * The definition tree of a symbol in source - * - * Annotated by namer and used by the LSP server for jump-to-definition - * - * TODO maybe store the whole definition tree instead of the name, which requries refactoring of assignSymbol - */ - val DefinitionTree = SymbolAnnotation[symbols.Symbol, source.IdDef]( - "DefinitionTree", - "the tree identifying the definition site of symbol" - ) - - /** - * Approximate list of all references pointing to a symbol - * - * Filled by namer and used for reverse lookup in LSP server - */ - val References = SymbolAnnotation[symbols.Symbol, List[source.Reference]]( - "References", - "the references referring to symbol" - ) - - /** - * The symbol for an identifier as resolved by namer - * - * Id can be the definition-site (IdDef) or use-site (IdRef) of the - * specific symbol - */ - val Symbol = TreeAnnotation[source.Id, symbols.Symbol]( - "Symbol", - "the symbol for identifier" - ) - - /** - * The resolved type for a type tree that appears in the source program - * - * Resolved and annotated by namer and used by typer. - */ - val Type = TreeAnnotation[source.Type, symbols.Type]( - "Type", - "the resolved type for" - ) - - val Capture = TreeAnnotation[source.CaptureSet, symbols.CaptureSet]( - "Capture", - "the resolved capture set for" - ) - - /** - * Similar to TypeAndEffect: the capture set of a program - */ - val InferredCapture = TreeAnnotation[source.Tree, symbols.CaptureSet]( - "InferredCapture", - "the inferred capture for source tree" - ) - - /** - * Capabilities bound by either a [[source.TryHandle]], [[source.FunDef]], - * [[source.VarDef]], or [[source.BlockLiteral]]. - * - * Inferred by typer, used by elaboration. - */ - val BoundCapabilities = TreeAnnotation[source.Tree, List[symbols.BlockParam]]( - "BoundCapabilities", - "capabilities bound by this tree" - ) - - /** - * Capabilities inferred as additional arguments to a call. - * - * Inferred by typer, used by elaboration. - */ - val CapabilityArguments = TreeAnnotation[source.CallLike, List[symbols.BlockParam]]( - "CapabilityArguments", - "capabilities inferred as additional arguments for this call" - ) - - val CapabilityReceiver = TreeAnnotation[source.Do, symbols.BlockParam]( - "CapabilityReceiver", - "the receiver as inferred for this effect operation call" - ) - - /** - * The lexical region as introduced by namer - * - * Used by typer for region checking mutable variables. - */ - val SelfRegion = TreeAnnotation[source.Tree, symbols.TrackedParam]( - "SelfRegion", - "the region corresponding to a lexical scope" - ) - - /** - * The [[source.Def]] which is a parent of this [[source.Unbox]] node - * provided that the [[source.Unbox]] was synthesized by the compiler. - * - * Introduced by the pretyper. - * Used by typer in order to display a more precise error message. - */ - val UnboxParentDef = TreeAnnotation[source.Unbox, source.Def]( - "UnboxParentDef", - "the parent definition of an Unbox if it was synthesized" - ) -} - -/** - * Global annotations on syntax trees - * - * This database is mixed into the compiler `Context` and is - * globally visible across all phases. If you want to hide changes in - * subsequent phases, consider using an instance of `Annotations`, instead. - * - * Calling `Annotations.commit` transfers all annotations into the global databases. - * - * The DB is also "global" in the sense, that modifications cannot be backtracked. - * It should thus only be used to store a "ground" truth that will not be changed again. - */ -trait TreeAnnotations { self: Context => - private type AnnotationsMap = Map[TreeAnnotation[_, _], Any] - - private type Annotations = Map[TreeAnnotation[_, _], Any] - type DB = util.IdentityHashMap[source.Tree, Map[TreeAnnotation[_, _], Any]] - var db: DB = new util.IdentityHashMap() - - private def annotationsAt[K](key: K): AnnotationsMap = - db.getOrDefault(key, Map.empty) - - /** - * Copies annotations, keeping existing annotations at `to` - */ - def copyAnnotations(from: source.Tree, to: source.Tree): Unit = { - val existing = annotationsAt(to) - val source = annotationsAt(from) - annotate(to, source ++ existing) - } - - /** - * Bulk annotating the key - * - * Used by Annotations.commit to commit all temporary annotations to the DB - */ - def annotate(key: source.Tree, value: AnnotationsMap): Unit = { - val anns = db.getOrDefault(key, Map.empty) - db.put(key, anns ++ value) - } - - def annotate[K <: source.Tree, V](ann: TreeAnnotation[K, V], key: source.Tree, value: V): Unit = { - val anns = db.getOrDefault(key, Map.empty) - db.put(key, anns + (ann -> value)) - } - - def annotationOption[V](ann: TreeAnnotation[_, V], key: source.Tree): Option[V] = - annotationsAt(key).get(ann).asInstanceOf[Option[V]] - - def annotation[V](ann: TreeAnnotation[_, V], key: source.Tree): V = - annotationOption(ann, key).getOrElse { - panic(s"Cannot find ${ann.description} for '${key}'") - } - - def hasAnnotation[V](ann: TreeAnnotation[_, V], key: source.Tree): Boolean = - annotationsAt(key).isDefinedAt(ann) - - // Customized Accessors - // ==================== - import symbols.{ Symbol, Type, ValueType, FunctionType, BlockType, ValueSymbol, BlockSymbol, Module, Effects } - - // Types - // ----- - - def typeArguments(c: source.CallLike): List[symbols.ValueType] = - annotation(Annotations.TypeArguments, c) - - def inferredTypeOption(t: source.Tree): Option[ValueType] = - annotationOption(Annotations.InferredValueType, t) - - def inferredTypeOf(t: source.Tree): ValueType = - inferredTypeOption(t).getOrElse { - panic(s"Internal Error: Missing type of source expression: '${t}'") - } - - def inferredBlockTypeOption(t: source.Tree): Option[BlockType] = - annotationOption(Annotations.InferredBlockType, t) - - def inferredBlockTypeOf(t: source.Tree): BlockType = - inferredBlockTypeOption(t).getOrElse { - panic(s"Internal Error: Missing type of source block: '${ t }'") - } - - def inferredEffectOption(t: source.Tree): Option[Effects] = - annotationOption(Annotations.InferredEffect, t) - - def inferredEffectOf(t: source.Tree): Effects = - inferredEffectOption(t).getOrElse { - panic(s"Internal Error: Missing effect of source expression: '${t}'") - } - - def inferredTypeAndEffectOption(t: source.Tree): Option[(ValueType, Effects)] = - for { - tpe <- inferredTypeOption(t) - eff <- inferredEffectOption(t) - } yield (tpe, eff) - - def inferredTypeAndEffectOf(t: source.Tree): (ValueType, Effects) = - inferredTypeAndEffectOption(t).getOrElse { - panic(s"Internal Error: Missing type of source expression: '${t}'") - } - - def inferredCapture(t: source.Tree): symbols.CaptureSet = - annotation(Annotations.InferredCapture, t) - - def inferredCaptureOption(t: source.Tree): Option[symbols.CaptureSet] = - annotationOption(Annotations.InferredCapture, t) - - def annotateResolvedType(tree: source.Type)(tpe: symbols.Type): Unit = - annotate(Annotations.Type, tree, tpe) - - def resolvedType(tree: source.Type): symbols.Type = - annotation(Annotations.Type, tree) - - def annotateResolvedCapture(tree: source.CaptureSet)(capt: symbols.CaptureSet): Unit = - annotate(Annotations.Capture, tree, capt) - - def resolvedCapture(tree: source.CaptureSet): symbols.CaptureSet = - annotation(Annotations.Capture, tree) - - // Symbols - // ------- - - /** - * Stores symbol `sym` as the corresponding symbol for `id` - * - * Almost all calls to this method are performed by Namer, which - * resolves identifier and then assigns the symbols. - * - * Typer also calls this method to resolve overloads and store - * the result of overload resolution. - */ - def assignSymbol(id: source.Id, sym: Symbol): Unit = id match { - case id: source.IdDef => - annotate(Annotations.DefinitionTree, sym, id) - sym match { - case _: ResumeParam => - case s: symbols.TrackedParam => - // for tracked params, also note the id als definition site for the capture. - annotate(Annotations.DefinitionTree, s.capture, id) - case _ => - } - annotate(Annotations.Symbol, id, sym) - addDefinedSymbolToSource(sym) - case _ => - annotate(Annotations.Symbol, id, sym) - // addDefinedSymbolToSource(sym) - } - - def symbolOf(id: source.Id): Symbol = symbolOption(id) getOrElse { - panic(s"Internal Compiler Error: Cannot find symbol for ${id}") - } - def symbolOption(id: source.Id): Option[Symbol] = - annotationOption(Annotations.Symbol, id) - - /** - * Searching the definitions for a Reference - * - * This one can fail. - */ - def symbolOf(tree: source.Reference): Symbol = { - val sym = symbolOf(tree.id) - - val refs = annotationOption(Annotations.References, sym).getOrElse(Nil) - annotate(Annotations.References, sym, tree :: refs) - sym - } - - /** - * Searching the symbol for a definition - * - * These lookups should not fail (except there is a bug in the compiler) - */ - def symbolOf(tree: source.Definition): Symbol = - symbolOf(tree.id) -} - -/** - * Global annotations on entire Source objects - * - * It is very important that the comparison between keys is based on value rather than object identity: - * Even when a separate Source object is crated for the same file contents, it should track the same annotations. - * This situation frequently occurs in the language server where sources are transmitted from the language client (editor). - */ -trait SourceAnnotations { self: Context => - import scala.collection.mutable - - private val sourceAnnotationsDB: mutable.Map[kiama.util.Source, Map[SourceAnnotation[_, _], Any]] = - mutable.Map.empty - - private def annotationsAt(source: kiama.util.Source): Map[SourceAnnotation[_, _], Any] = - sourceAnnotationsDB.getOrElse(source, Map.empty) - - def annotate[A](ann: SourceAnnotation[_, A], source: kiama.util.Source, value: A): Unit = { - val anns = annotationsAt(source) - sourceAnnotationsDB.update(source, anns + (ann -> value)) - } - - def annotationOption[A](ann: SourceAnnotation[_, A], source: kiama.util.Source): Option[A] = - annotationsAt(source).get(ann).asInstanceOf[Option[A]] - - /** - * List all symbols that have a source module - * - * Used by the LSP server to generate outline - */ - def sourceSymbolsFor(src: kiama.util.Source): Set[symbols.Symbol] = - annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty) - - /** - * Adds [[s]] to the set of defined symbols for the current module, by writing - * it into the [[Annotations.DefinedSymbols]] annotation. - */ - def addDefinedSymbolToSource(s: symbols.Symbol): Unit = - if (module != null) { - val src = module.source - val syms = annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty) - annotate(Annotations.DefinedSymbols, src, syms + s) - } -} - -/** - * Global annotations on symbols - */ -trait SymbolAnnotations { self: Context => - - private val symbolAnnotationsDB: util.IdentityHashMap[symbols.Symbol, Map[SymbolAnnotation[_, _], Any]] = - new util.IdentityHashMap() - - // Retrieve the annotations for a given symbol. - private def annotationsAt(sym: symbols.Symbol): Map[SymbolAnnotation[_, _], Any] = - symbolAnnotationsDB.getOrDefault(sym, Map.empty) - - // Annotate a symbol with an annotation and its value. - def annotate[A](ann: SymbolAnnotation[_, A], sym: symbols.Symbol, value: A): Unit = { - val key = sym - val anns = annotationsAt(sym) - symbolAnnotationsDB.put(key, anns + (ann -> value)) - } - - // Retrieve an optional annotation for a symbol. - def annotationOption[A](ann: SymbolAnnotation[_, A], sym: symbols.Symbol): Option[A] = - annotationsAt(sym).get(ann).asInstanceOf[Option[A]] - - def typeOf(s: Symbol): symbols.Type = s match { - case s: ValueSymbol => valueTypeOf(s) - case s: BlockSymbol => blockTypeOf(s) - case _ => panic(s"Cannot find a type for symbol '${s}'") - } - - // Retrieve the value type of a value symbol. - def valueTypeOption(s: symbols.Symbol): Option[symbols.ValueType] = s match { - case vs: symbols.ValueSymbol => annotationOption(Annotations.ValueType, vs) - case _ => panic(s"Trying to find a value type for non-value '${s}'") - } - - def valueTypeOf(s: symbols.Symbol): symbols.ValueType = - valueTypeOption(s).getOrElse(panic(s"Cannot find value type for ${s}")) - - def blockTypeOption(s: Symbol): Option[BlockType] = - s match { - case b: BlockSymbol => annotationOption(Annotations.BlockType, b) flatMap { - case b: BlockType => Some(b) - } - case _ => panic(s"Trying to find a interface type for non block '${s}'") - } - - def blockTypeOf(s: symbols.Symbol): symbols.BlockType = - blockTypeOption(s).getOrElse(panic(s"Cannot find block type for ${s}")) - - // Retrieve the function type of a block symbol. - def functionTypeOption(s: symbols.Symbol): Option[symbols.FunctionType] = s match { - case bs: symbols.BlockSymbol => - annotationOption(Annotations.BlockType, bs) match { - case Some(ft: symbols.FunctionType) => Some(ft) - case _ => None - } - // The callsite should be adjusted, this is NOT the job of annotations... - case v: ValueSymbol => ??? - // valueTypeOption(v).flatMap { v => - // v.dealias match { - // case symbols.BoxedType(tpe: FunctionType, _) => Some(tpe) - // case _ => None - // } - // } - case _ => None - } - - def functionTypeOf(s: symbols.Symbol): symbols.FunctionType = - functionTypeOption(s).getOrElse(panic(s"Cannot find function type for ${s}")) - - /** - * Searching the definition for a symbol - */ - def definitionTreeOption(s: symbols.Symbol): Option[source.IdDef] = - annotationOption(Annotations.DefinitionTree, s) - - /** - * List all references for a symbol - * - * Used by the LSP server for reverse lookup - */ - def distinctReferencesTo(sym: symbols.Symbol): List[source.Reference] = - annotationOption(Annotations.References, sym) - .getOrElse(Nil) - .asInstanceOf[List[source.Reference]] - .distinctBy(r => System.identityHashCode(r)) - - def captureOf(sym: symbols.BlockSymbol): symbols.Captures = - annotationOption(Annotations.Captures, sym) - .getOrElse(panic(s"Cannot find captures for ${sym}")) - - def captureOfOption(sym: symbols.BlockSymbol): Option[symbols.Captures] = - annotationOption(Annotations.Captures, sym) -} diff --git a/effekt/shared/src/main/scala/effekt/context/Assertions.scala b/effekt/shared/src/main/scala/effekt/context/Assertions.scala deleted file mode 100644 index 139c3babd..000000000 --- a/effekt/shared/src/main/scala/effekt/context/Assertions.scala +++ /dev/null @@ -1,96 +0,0 @@ -package effekt -package context - -import effekt.symbols._ -import effekt.util.messages.ErrorReporter - -object assertions { - - // format: -alignSingleLineCaseStatements - - /** - * The Assertions trait is designed to keep all error messages - * in one place - */ - extension(s: Symbol)(using reporter: ErrorReporter) { - def asTypeParam: TypeParam = s match { - case t: TypeParam => t - case _ => reporter.abort("Expected a type parameter") - } - def asValueParam: ValueParam = s match { - case t: ValueParam => t - case _ => reporter.abort("Expected a value parameter") - } - def asBlockParam: BlockParam = s match { - case t: BlockParam => t - case _ => reporter.abort("Expected a block parameter") - } - def asEffectOp: Operation = s match { - case t: Operation => t - case _ => reporter.abort("Expected an effect operation, but got " + s) - } - def asUserFunction: UserFunction = s match { - case t: UserFunction => t - case _ => reporter.abort("Expected a user defined function") - } - def asBuiltinFunction: ExternFunction = s match { - case t: ExternFunction => t - case _ => reporter.abort("Expected a builtin function") - } - def asConstructor: Constructor = s match { - case t: Constructor => t - case _ => reporter.abort("Expected a constructor") - } - def asDataType: DataType = s match { - case t: DataType => t - case _ => reporter.abort("Expected a data type") - } - def asRecord: Record = s match { - case t: Record => t - case _ => reporter.abort("Expected a record") - } - def asValBinder: ValBinder = s match { - case t: ValBinder => t - case _ => reporter.abort("Expected a value binder") - } - def asVarBinder: VarBinder = s match { - case t: VarBinder => t - case _ => reporter.abort("Expected a mutable variable") - } - def asBinder: Binder = s match { - case t: Binder => t - case _ => reporter.abort("Expected a binder") - } - def asInterface: Interface = s match { - case t: Interface => t - case t => reporter.abort("Expected an interface") - } - def asOperation: Operation = s match { - case t: Operation => t - case t => reporter.abort("Expected an operation") - } - def asFun: Callable = s match { - case t: Callable => t - case _ => reporter.abort("Expected a function") - } - def asCallTarget: CallTarget = s match { - case t: CallTarget => t - case _ => reporter.abort("Expected a call target") - } - def asTermSymbol: TermSymbol = s match { - case t: TermSymbol => t - case _ => reporter.panic("Expected a term symbol") - } - def asBlockSymbol: BlockSymbol = s match { - case t: BlockSymbol => t - case _ => reporter.panic("Expected a block symbol") - } - } - - extension(t: symbols.Type)(using reporter: ErrorReporter) { - def asInterfaceType: InterfaceType = t match { - case t: InterfaceType => t - case _ => reporter.abort("Expected a capability type") - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/context/Context.scala b/effekt/shared/src/main/scala/effekt/context/Context.scala index 594f287f8..e02344612 100644 --- a/effekt/shared/src/main/scala/effekt/context/Context.scala +++ b/effekt/shared/src/main/scala/effekt/context/Context.scala @@ -1,102 +1,13 @@ package effekt package context -import effekt.namer.NamerOps -import effekt.typer.TyperOps -import effekt.core.TransformerOps -import effekt.source.Tree -import effekt.util.messages.{ ErrorReporter, EffektMessages } -import effekt.util.Timers -import effekt.symbols.Module - -import kiama.util.Positions - -/** - * Phases like Typer can add operations to the context by extending this trait - * - * For example, see TyperOps - */ -trait ContextOps - extends ErrorReporter - with TreeAnnotations - with SourceAnnotations - with SymbolAnnotations { self: Context => - - /** - * Used throughout the compiler to create a new "scope" - * - * Each XOps slice can define what a new "scope" means and - * backup and restore state accordingly. Overriding definitions - * should call `super.in(block)`. - */ - def in[T](block: => T): T = block -} - -/** - * The compiler context consists of - * - configuration (immutable) - * - symbols (mutable database) - * - types (mutable database) - * - error reporting (mutable focus) - */ -abstract class Context(val positions: Positions) - extends NamerOps - with TyperOps - with ModuleDB - with TransformerOps - with Timers { - - // bring the context itself in scope - implicit val context: Context = this - - // the currently processed module - var module: Module = _ - - // the currently processed node - var focus: Tree = _ - - var _config: EffektConfig = _ - def config = _config +import util.{ PlainMessaging, Timers } +import util.messages.{ EffektMessages, ErrorReporter } +trait Context extends ErrorReporter, Timers { // cache used by tasks to save their results (in addition to information in the AnnotationsDB) var cache: util.Task.Cache = util.Task.emptyCache - - // We assume the backend never changes - lazy val backend = config.backend() - lazy val compiler = backend.compiler - lazy val runner = backend.runner - - /** - * Clear current context to start processing a fresh unit - */ - def setup(cfg: EffektConfig): Unit = { - messaging.clear() - // No timings are captured in server mode to keep the memory footprint small. Since the server is run continuously, - // the memory claimed by the timing information would increase continuously. - clearTimers(cfg.timed()) - _config = cfg - } - - def using[T](module: Module = module, focus: Tree = focus)(block: => T): T = this in { - this.module = module - this.focus = focus - block - } - - /** - * This is useful to write code like: reporter in { ... implicitly uses reporter ... } - */ - override def in[T](block: => T): T = { - val focusBefore = focus - val moduleBefore = module - val result = super.in(block) - - // we purposefully do not include the reset into `finally` to preserve the - // state at the error position - focus = focusBefore - module = moduleBefore - result - } + val messaging = new PlainMessaging // temporarily switches the message buffer to collect messages def withMessages[T](block: => T): (EffektMessages, T) = { @@ -110,22 +21,9 @@ abstract class Context(val positions: Positions) (msgs, res) } - /** - * The compiler state - */ - case class State(annotations: DB, cache: util.Task.Cache) - - /** - * Export the compiler state - */ - def backup: State = State(this.db, this.cache) - - /** - * Restores the compiler state from a backup - */ - def restore(s: State) = { - db = s.annotations - cache = s.cache + object config { + def maxInlineSize() = 30L + def optimize() = true } } diff --git a/effekt/shared/src/main/scala/effekt/context/ModuleDB.scala b/effekt/shared/src/main/scala/effekt/context/ModuleDB.scala deleted file mode 100644 index e650ff0ed..000000000 --- a/effekt/shared/src/main/scala/effekt/context/ModuleDB.scala +++ /dev/null @@ -1,103 +0,0 @@ -package effekt -package context - -import effekt.symbols._ -import effekt.context.assertions.* -import kiama.util.Source - -/** - * The ModuleDB depends on three things: - * - method `contentsOf` to resolve FFI includes (js files) - * - method `findSource` to resolve module sources (effekt files) - * - field `Compiler.frontend` to run the compiler on sources, on demand - */ -trait ModuleDB { self: Context => - - /** - * Tries to find a file in the workspace, that matches the import path - * - * Used by Namer to resolve FFI includes - */ - // def contentsOf: Task[String, String] - - def contentsOf(path: String): Option[String] - - /** - * Find the source for a given module name / path - */ - private[context] def findSource(path: String): Option[Source] - - /** - * Tries to find a module for the given path, will run compiler on demand - * - * Used by Namer and Evaluator to resolve imports - */ - def moduleOf(path: String): Module = - moduleOf(findSource(path).getOrElse { abort(s"Cannot find source for $path") }) - - /** - * Tries to find a module for the given source, will run compiler on demand - */ - def moduleOf(source: Source): Module = - tryModuleOf(source).getOrElse { - abort(s"Cannot compile dependency: ${stripSuffix(source.name)}") - } - - private def stripSuffix(path: String): String = - path.stripSuffix(".md").stripSuffix(".effekt") - - /** - * Tries to find a module for the given source, will run compiler on demand - */ - def tryModuleOf(source: Source): Option[Module] = for { - mod <- compiler.runFrontend(source)(using this) - } yield mod - - /** - * Util to check whether main exists on the given module - */ - def checkMain(mod: Module)(implicit C: Context): TermSymbol = C.at(mod.decl) { - - // deep discovery of main: should be replaced by explicit @main annotation - def findMain(b: Bindings): Set[TermSymbol] = - b.terms.getOrElse("main", Set()) ++ b.namespaces.flatMap { case (_, b) => findMain(b) } - - val mains = findMain(mod.exports) - - if (mains.isEmpty) { - C.abort("No main function defined") - } - - if (mains.size > 1) { - val names = mains.toList.map(sym => pp"${sym.name}").mkString(", ") - C.abort(pp"Multiple main functions defined: ${names}") - } - - val main = mains.head.asUserFunction - - Context.at(main.decl) { - val mainValueParams = C.functionTypeOf(main).vparams - val mainBlockParams = C.functionTypeOf(main).bparams - if (mainValueParams.nonEmpty || mainBlockParams.nonEmpty) { - C.abort("Main does not take arguments") - } - - val tpe = C.functionTypeOf(main) - val controlEffects = tpe.effects - if (controlEffects.nonEmpty) { - C.abort(pp"Main cannot have effects, but includes effects: ${controlEffects}") - } - - tpe.result match { - case symbols.builtins.TInt => - C.abort(pp"Main must return Unit, please use `exit(n)` to return an error code.") - case symbols.builtins.TUnit => - () - case other => - C.abort(pp"Main must return Unit, but returns ${other}.") - } - - main - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index 9907dd340..3a53fa4cf 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -2,7 +2,6 @@ package effekt package core import effekt.core.ValueParam -import effekt.source.{FeatureFlag, NoSource} import effekt.util.messages.{ DebugMessaging, ErrorReporter, ParseError } import kiama.parsing.{ Failure, Input, NoSuccess, ParseResult, Success } import kiama.util.{ Position, Positions, Range, Source, StringSource } diff --git a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala b/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala deleted file mode 100644 index 04ae6f906..000000000 --- a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala +++ /dev/null @@ -1,332 +0,0 @@ -package effekt -package core - -import effekt.core.substitutions.Substitution - -import scala.collection.mutable - - -/** - * Pattern Matching Compiler - * ------------------------- - * The implementation of the match compiler follows closely the short paper: - * - * Jules Jacobs - * How to compile pattern matching - * https://julesjacobs.com/notes/patternmatching/patternmatching.pdf - & - * A match is represented as a list of [[ Clause ]]s, e.g. - * - * case a is Some(x) => j1(x) - * case a is None => j2() - * - * Each clause represents one disjunct. That is, one of the clauses needs to match. We compile them - * to match in order (from top-to-bottom). - * - * We generalize Jacob's original draft, by also supporting literal patterns, multiple guards, - * and evaluating expressions. - * - * That is, one clause is the conjunction of multiple [[ Condition ]]s, e.g.: - * - * case a is Some(x); val tmp = comp(x); pred(tmp)? => j(x, tmp) - * - * Here, we compile conditions to be evaluated left-to-right. - * - * In general, [[ core.Transformer.preprocess ]] takes a source pattern match and translates - * it into an internal representation (defined below, [[ Clause ]], [[ Condition ]], etc.). - * This internal representation is designed to have all information readily available to - * immediately generate the [[ core.Stmt ]] corresponding to the compiled pattern. - * - * This way, we can also write [[ core.PatternMatchingTests ]] independent of a concrete source - * pattern. - * - * TODO test matching on the same scrutinee multiple times in a row Maps are the wrong datatype to represent disjunctions - * - * While exhaustivity checking could be integrated in the compiler, we decided to have a separate implementation - * in [[ effekt.typer.ExhaustivityChecker ]], which is run during the [[ effekt.typer.Wellformedness ]] phase. - * - * Changes here need to be synchronized with the checker, sadly. - * - * @see https://github.com/effekt-lang/effekt/issues/383 - */ -object PatternMatchingCompiler { - - /** - * The conditions need to be met in sequence before the block at [[label]] can be evaluated with given [[targs]] and [[args]]. - */ - case class Clause(conditions: List[Condition], label: BlockVar, targs: List[ValueType], args: List[ValueVar]) - - enum Condition { - // all of the patterns need to match for this condition to be met - case Patterns(patterns: Map[ValueVar, Pattern]) - // a boolean predicate that needs to be branched on at runtime - case Predicate(pred: Pure) - // a predicate trivially met by running and binding the statement - case Val(x: Id, tpe: core.ValueType, binding: Stmt) - case Let(x: Id, tpe: core.ValueType, binding: Expr) - } - - enum Pattern { - // sub-patterns are annotated with the inferred type of the scrutinee at this point - // i.e. Cons(Some(x : TInt): Option[Int], xs: List[Option[Int]]) - case Tag(id: Id, tparams: List[Id], patterns: List[(Pattern, ValueType)]) - case Ignore() - case Any(id: Id) - case Or(patterns: List[Pattern]) - case Literal(l: core.Literal, equals: (core.Pure, core.Pure) => core.Pure) - } - - /** - * The match compiler works with - * - a sequence of clauses that represent alternatives (disjunction) - * - each sequence contains a list of conditions that all have to match (conjunction). - */ - def compile(clauses: List[Clause]): core.Stmt = { - // This shouldn't be reachable anymore since we specialize matching on void before calling compile - if (clauses.isEmpty) return core.Hole() - - // (0) normalize clauses - val normalized @ (headClause :: remainingClauses) = clauses.map(normalize) : @unchecked - - // (1) Check the first clause to be matched (we can immediately handle non-pattern cases) - val patterns = headClause match { - // - The top-most clause already matches successfully - case Clause(Nil, target, targs, args) => - return core.App(target, targs, args, Nil) - // - We need to perform a computation - case Clause(Condition.Val(x, tpe, binding) :: rest, target, targs, args) => - return core.Val(x, tpe, binding, compile(Clause(rest, target, targs, args) :: remainingClauses)) - // - We need to perform a computation - case Clause(Condition.Let(x, tpe, binding) :: rest, target, targs, args) => - return core.Let(x, tpe, binding, compile(Clause(rest, target, targs, args) :: remainingClauses)) - // - We need to check a predicate - case Clause(Condition.Predicate(pred) :: rest, target, targs, args) => - return core.If(pred, - compile(Clause(rest, target, targs, args) :: remainingClauses), - compile(remainingClauses) - ) - case Clause(Condition.Patterns(patterns) :: rest, target, targs, args) => - patterns - } - - // (2) Choose the variable to split on - val scrutinee: ValueVar = branchingHeuristic(patterns, normalized) - - object Split { - def unapply(c: List[Condition]): Option[(Pattern, Map[ValueVar, Pattern], List[Condition])] = - c match { - case Condition.Patterns(patterns) :: rest => - patterns.get(scrutinee).map { p => (p, patterns - scrutinee, rest) } - case _ => None - } - } - - // (3a) Match on a literal - def splitOnLiteral(lit: Literal, equals: (Pure, Pure) => Pure): core.Stmt = { - // the different literal values that we match on - val variants: List[core.Literal] = normalized.collect { - case Clause(Split(Pattern.Literal(lit, _), _, _), _, _, _) => lit - }.distinct - - // for each literal, we collect the clauses that match it correctly - val clausesFor = mutable.Map.empty[core.Literal, List[Clause]] - def addClause(c: core.Literal, cl: Clause): Unit = - clausesFor.update(c, clausesFor.getOrElse(c, List.empty) :+ cl) - - // default clauses always match with respect to the current scrutinee - var defaults = List.empty[Clause] - def addDefault(cl: Clause): Unit = - defaults = defaults :+ cl - - normalized.foreach { - case Clause(Split(Pattern.Literal(lit, _), restPatterns, restConds), label, targs, args) => - addClause(lit, Clause(Condition.Patterns(restPatterns) :: restConds, label, targs, args)) - case c => - addDefault(c) - variants.foreach { v => addClause(v, c) } - } - - // special case matching on () - val unit: Literal = Literal((), core.Type.TUnit) - if (lit == unit) return compile(clausesFor.getOrElse(unit, Nil)) - - // (4) assemble syntax tree for the pattern match - variants.foldRight(compile(defaults)) { - case (lit, elsStmt) => - val thnStmt = compile(clausesFor.getOrElse(lit, Nil)) - core.If(equals(scrutinee, lit), thnStmt, elsStmt) - } - } - - // (3b) Match on a data type constructor - def splitOnTag() = { - // collect all variants that are mentioned in the clauses - val variants: List[Id] = normalized.collect { - case Clause(Split(p: Pattern.Tag, _, _), _, _, _) => p.id - }.distinct - - // for each tag, we collect the clauses that match it correctly - val clausesFor = mutable.Map.empty[Id, List[Clause]] - def addClause(c: Id, cl: Clause): Unit = - clausesFor.update(c, clausesFor.getOrElse(c, List.empty) :+ cl) - - // default clauses always match with respect to the current scrutinee - var defaults = List.empty[Clause] - def addDefault(cl: Clause): Unit = - defaults = defaults :+ cl - - // used to make up new scrutinees - val varsFor = mutable.Map.empty[Id, List[ValueVar]] - val tvarsFor = mutable.Map.empty[Id, List[Id]] - def fieldVarsFor(constructor: Id, tparams: List[Id], fieldInfo: List[((Pattern, ValueType), String)]): List[ValueVar] = - tvarsFor.getOrElseUpdate(constructor, tparams) - varsFor.getOrElseUpdate( - constructor, - fieldInfo.map { - // if it's a pattern named by the user in the program, use the supplied name - case ((Pattern.Any(id), tpe), _) => ValueVar(Id(id.name.name), tpe) - // otherwise, use the field name of the given field - case ((_, tpe), fieldName) => ValueVar(Id(fieldName), tpe) - } - ) - - normalized.foreach { - case Clause(Split(Pattern.Tag(constructor, tparams, patternsAndTypes), restPatterns, restConds), label, targs, args) => - // NOTE: Ideally, we would use a `DeclarationContext` here, but we cannot: we're currently in the Source->Core transformer, so we do not have all of the details yet. - val fieldNames: List[String] = constructor match { - case c: symbols.Constructor => c.fields.map(_.name.name) - case _ => List.fill(patternsAndTypes.size) { "y" } // NOTE: Only reached in PatternMatchingTests - } - val fieldVars = fieldVarsFor(constructor, tparams, patternsAndTypes.zip(fieldNames)) - val nestedMatches = fieldVars.zip(patternsAndTypes.map { case (pat, tpe) => pat }).toMap - addClause(constructor, - // it is important to add nested matches first, since they might include substitutions for the rest. - Clause(Condition.Patterns(nestedMatches) :: Condition.Patterns(restPatterns) :: restConds, label, targs, args)) - - case c => - // Clauses that don't match on that var are duplicated. - // So we want to choose our branching heuristic to minimize this - addDefault(c) - // here we duplicate clauses! - variants.foreach { v => addClause(v, c) } - } - - // (4) assemble syntax tree for the pattern match - val branches = variants.map { v => - val body = compile(clausesFor.getOrElse(v, Nil)) - val tparams = tvarsFor(v) - val params = varsFor(v).map { case ValueVar(id, tpe) => core.ValueParam(id, tpe): core.ValueParam } - val blockLit: BlockLit = BlockLit(tparams, Nil, params, Nil, body) - (v, blockLit) - } - - val default = if defaults.isEmpty then None else Some(compile(defaults)) - core.Match(scrutinee, branches, default) - } - - patterns(scrutinee) match { - case Pattern.Literal(lit, equals) => splitOnLiteral(lit, equals) - case p: Pattern.Tag => splitOnTag() - case _ => ??? - } - } - - def branchingHeuristic(patterns: Map[ValueVar, Pattern], clauses: List[Clause]): ValueVar = - patterns.keys.maxBy(v => clauses.count { - case Clause(ps, _, _, _) => ps.contains(v) - }) - - /** - * Substitutes AnyPattern and removes wildcards. - */ - def normalize(clause: Clause): Clause = clause match { - case Clause(conditions, label, targs, args) => - val (normalized, substitution) = normalize(Map.empty, conditions, Map.empty) - // TODO also substitute types? - Clause(normalized, label, targs, args.map(v => substitution.getOrElse(v.id, v))) - } - - - /** - * 1) merges all subsequent Pattern(ps) :: Pattern(qs) :: ... into Pattern(ps ::: qs) :: ... - * 2) drops all Ignore and Any patterns and substitutes them in the remaining patterns - * 3) returns the substitution that needs to be applied to the body of the clause - * - * case a is x; x is Cons => f(a, x) - * ~= - * case a is Cons => f(a, a) - * - * TODO in the future we could "pull" pattern matches across bindings that are not free in scrutinees. - * this way, we would potentially exhaust more matches before performing computation - */ - def normalize( - patterns: Map[ValueVar, Pattern], - conditions: List[Condition], - substitution: Map[Id, ValueVar] - ): (List[Condition], Map[Id, ValueVar]) = { - - val subst = Substitution(Map.empty, Map.empty, substitution, Map.empty) - - def prefix(p: Map[ValueVar, Pattern], cs: List[Condition]): List[Condition] = - if p.isEmpty then cs else Condition.Patterns(p) :: cs - - def substitute(pattern: (ValueVar, Pattern)) = pattern match { - case (sc, p) => substitution.getOrElse(sc.id, sc) -> p - } - - conditions match { - case Condition.Patterns(other) :: rest => - val substituted = other.map(substitute) - val additionalSubst = substituted.collect { case (sc, Pattern.Any(id)) => id -> sc } - val filtered = substituted.collect { - case (sc, p: Pattern.Tag) => sc -> p - case (sc, p: Pattern.Literal) => sc -> p - } - normalize(patterns ++ filtered, rest, substitution ++ additionalSubst) - - case Condition.Val(x, tpe, binding) :: rest => - val substitutedBinding = core.substitutions.substitute(binding)(using subst) - val substitutedType = core.substitutions.substitute(tpe)(using subst) - val (resCond, resSubst) = normalize(Map.empty, rest, substitution) - val substituted = Condition.Val(x, substitutedType, substitutedBinding) - (prefix(patterns, substituted :: resCond), resSubst) - - case Condition.Let(x, tpe, binding) :: rest => - val substitutedBinding = core.substitutions.substitute(binding)(using subst) - val (resCond, resSubst) = normalize(Map.empty, rest, substitution) - (prefix(patterns, Condition.Let(x, tpe, substitutedBinding) :: resCond), resSubst) - - case Condition.Predicate(p) :: rest => - val substitutedPredicate = core.substitutions.substitute(p)(using subst) - val (resCond, resSubst) = normalize(Map.empty, rest, substitution) - (prefix(patterns, Condition.Predicate(substitutedPredicate) :: resCond), resSubst) - - case Nil => - (prefix(patterns, Nil), substitution) - } - } - - - // For development and debugging - // ----------------------------- - - def show(cl: Clause): String = cl match { - case Clause(conditions, label, targs, args) => - s"case ${conditions.map(show).mkString("; ")} => ${util.show(label.id)}${targs.map(x => util.show(x))}${args.map(x => util.show(x)).mkString("(", ", ", ")")}" - } - - def show(c: Condition): String = c match { - case Condition.Patterns(patterns) => patterns.map { case (v, p) => s"${util.show(v)} is ${show(p)}" }.mkString(", ") - case Condition.Predicate(pred) => util.show(pred) + "?" - case Condition.Val(x, tpe, binding) => s"val ${util.show(x)} = ${util.show(binding)}" - case Condition.Let(x, tpe, binding) => s"let ${util.show(x)} = ${util.show(binding)}" - } - - def show(p: Pattern): String = p match { - case Pattern.Tag(id, tparams, patterns) => util.show(id) + tparams.map(util.show).mkString("[", ",", "]") + patterns.map { case (p, tpe) => show(p) }.mkString("(", ", ", ")") - case Pattern.Ignore() => "_" - case Pattern.Any(id) => util.show(id) - case Pattern.Or(patterns) => patterns.map(show).mkString(" | ") - case Pattern.Literal(lit, equals) => util.show(lit.value) - } -} diff --git a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala deleted file mode 100644 index 05f4fbef2..000000000 --- a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala +++ /dev/null @@ -1,538 +0,0 @@ -package effekt -package core - -import effekt.PhaseResult.CoreTransformed -import effekt.context.Context -import effekt.core.PolymorphismBoxing.ValueCoercer.IdentityCoercer -import effekt.symbols -import effekt.symbols.{ TmpBlock, TmpValue } -import effekt.{ CoreTransformed, Phase } -import effekt.symbols.builtins.{ TBoolean, TByte, TChar, TDouble, TInt, TState, TUnit } -import effekt.symbols.ErrorMessageInterpolator - -import scala.util.boundary -import scala.annotation.targetName - -/** - * [[Phase]] on [[CoreTransformed]] to automatically box values when used polymorphically - * (i.e., their types are type parameters). - * - * By default, it will box all primitive types (i.e. all types of the form [[ValueType.Data]]`(T,List())` - * with `T` in [[symbols.builtins.rootTypes]]) using the data type `BoxedT` found in the prelude - * (see [[symbols.Module.findPrelude]]). - * This behavior can be changed by overriding [[box]]. - */ -object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] { - - override val phaseName: String = "polymorphism boxing" - - /** - * Describes how to box/unbox values - */ - trait Boxer { - /** The type of the boxed values */ - def tpe: ValueType - /** Generates a pure expression boxing the parameter */ - def box(p: Pure): Pure - /** Generates a pure expression unboxing the parameter */ - def unbox(p: Pure): Pure - } - /** - * Describes how to box/unbox values using extern functions - * @param tpe The type of the boxed values - * @param boxFn The extern function to call to box - * @param unboxFn The extern function to call to unbox - */ - case class ExternFnBoxer(val tpe: ValueType, boxFn: Block.BlockVar, unboxFn: Block.BlockVar) extends Boxer { - def box(p: Pure) = Pure.PureApp(boxFn, Nil, List(p)) - def unbox(p: Pure) = Pure.PureApp(unboxFn, Nil, List(p)) - } - - /** - * Partial function to describe which values to box and how. - * Is defined iff values of the given type should be boxed. - * - * @param tpe The [[ValueType]] of the value - * @return a [[Boxer]] that describes how to box values of that type - */ - def box(using PContext): PartialFunction[ValueType, Boxer] = { - case core.Type.TInt => PContext.boxer("Int") - case core.Type.TChar => PContext.boxer("Char") - case core.Type.TByte => PContext.boxer("Byte") - case core.Type.TDouble => PContext.boxer("Double") - } - - class PContext(declarations: List[Declaration], externs: List[Extern])(using val Context: Context) extends DeclarationContext(declarations, externs){ - /** - * Find a pure extern def with one value parameter named [[name]] in the prelude (or some namespace in the prelude). - */ - def findPureExternFn(name: String): Option[Block.BlockVar] = { - this.externs.collectFirst { - case d@effekt.core.Extern.Def(id, List(), List(), List(vparam), List(), ret, capts, body) - if id.name.name == name && capts.isEmpty => d - }.map { definition => - val id = definition.id - val tpe = core.BlockType.Function(Nil, Nil, definition.vparams.map(_.tpe), Nil, definition.ret) - Block.BlockVar(id, tpe, Set.empty) - } - } - - /** - * Find a record declaration named [[name]] with one field in the prelude (or some namespace in the prelude). - */ - def findRecord(name: String): Option[Declaration.Data] = { - this.declarations.collectFirst { - case d@effekt.core.Declaration.Data(tpe, List(), List(Constructor(cns, List(fld)))) - if tpe.name.name == name => d - } - } - - /** - * Finds the corresponding boxer for a primitive type. - * - * @param name The name of the [[ValueType]] - * @return a [[Boxer]] that describes how to box values of that type - */ - def boxer(name: String): Boxer = { - /** Try to find `boxT` and `unboxT` externs */ - def findExternFnBoxer() = boundary { - val box = findPureExternFn("box" ++ name).getOrElse { boundary.break(None) } - val unbox = findPureExternFn("unbox" ++ name).getOrElse { boundary.break(None) } - val boxRet = box.annotatedTpe match { - case BlockType.Function(_, _, _, _, result) => result - case _ => Context.abort(pp"${box} is not of function type.") - } - unbox.annotatedTpe match { - case BlockType.Function(_, _, List(unboxArg), List(), _) => - if (unboxArg != boxRet) { - Context.abort(pp"Argument type of ${unbox} and return type of ${box} do not match up.") - } - case _ => Context.abort(pp"${unbox} is not of function type.") - } - Some(ExternFnBoxer(boxRet, box, unbox)) - } - - findExternFnBoxer() getOrElse { - Context.abort(s"Type ${name}, which needs to be boxed, is used as a type argument but no " + - s"corresponding pure externs box${name} and unbox${name} were defined in the prelude.") - } - } - } - def PContext(using ctx: PContext): PContext = ctx - implicit def Context(using ctx: PContext): Context = ctx.Context - - override def run(input: CoreTransformed)(using Context): Option[CoreTransformed] = input match { - case CoreTransformed(source, tree, mod, core) => { - implicit val pctx: PContext = new PContext(core.declarations, core.externs) - Context.module = mod - val transformed = Context.timed(phaseName, source.name) { transform(core) } - - Some(CoreTransformed(source, tree, mod, transformed)) - } - } - - def transform(decl: ModuleDecl)(using PContext): ModuleDecl = decl match { - case ModuleDecl(path, includes, declarations, externs, definitions, exports) => - ModuleDecl(path, includes, declarations map transform, externs map transform, definitions map transform, exports) - } - - def transform(declaration: Declaration)(using PContext): Declaration = declaration match { - case Declaration.Data(id, tparams, constructors) => - Declaration.Data(id, tparams, constructors map transform) - case Declaration.Interface(id, tparams, properties) => - Declaration.Interface(id, tparams, properties map transform) - } - - def transform(constructor: Constructor)(using PContext): Constructor = constructor match { - case Constructor(id, fields) => Constructor(id, fields map transform) - } - - def transform(property: Property)(using PContext): Property = property match { - case Property(id, tpe) => Property(id, transform(tpe)) - } - - def transform(field: Field)(using PContext): Field = field match { - case Field(id, tpe) => Field(id, transform(tpe)) - } - - def transform(extern: Extern)(using PContext): Extern = extern match { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => - Extern.Def(id, tparams, cparams, vparams map transform, bparams map transform, transform(ret), - annotatedCapture, body match { - case ExternBody.StringExternBody(ff, bbody) => ExternBody.StringExternBody(ff, Template(bbody.strings, bbody.args map transform)) - case e @ ExternBody.Unsupported(_) => e - } ) - case Extern.Include(ff, contents) => Extern.Include(ff, contents) - } - - def transform(valueParam: ValueParam)(using PContext): ValueParam = valueParam match { - case ValueParam(id, tpe) => ValueParam(id, transform(tpe)) - } - - def transform(blockParam: BlockParam)(using PContext): BlockParam = blockParam match { - case BlockParam(id, tpe, capt) => BlockParam(id, transform(tpe), capt) - } - - def transform(toplevel: Toplevel)(using PContext): Toplevel = toplevel match { - case Toplevel.Def(id, block) => Toplevel.Def(id, transform(block)) - case Toplevel.Val(id, tpe, binding) => Toplevel.Val(id, transform(tpe), coerce(transform(binding), transform(tpe))) - } - - def transform(block: Block.BlockLit)(using PContext): Block.BlockLit = block match { - case Block.BlockLit(tparams, cparams, vparams, bparams, body) => - Block.BlockLit(tparams, cparams, vparams map transform, bparams map transform, - transform(body)) - } - def transform(block: Block)(using PContext): Block = block match { - case Block.BlockVar(id, annotatedTpe, annotatedCapt) => - Block.BlockVar(id, transform(annotatedTpe), annotatedCapt) - case b: Block.BlockLit => transform(b) - case Block.Unbox(pure) => - Block.Unbox(transform(pure)) - case Block.New(impl) => - Block.New(transform(impl)) - } - def transform(blockVar: BlockVar)(using PContext): BlockVar = blockVar match { - case Block.BlockVar(id, annotatedTpe, annotatedCapt) => - Block.BlockVar(id, transform(annotatedTpe), annotatedCapt) - } - - def transform(implementation: Implementation)(using PContext): Implementation = implementation match { - case Implementation(BlockType.Interface(symbol, targs), operations) => - val ifce = PContext.findInterface(symbol).getOrElse { Context.abort(s"No declaration for interface ${symbol}") } - Implementation(BlockType.Interface(symbol, targs map transformArg), operations map transform(ifce, targs)) - } - - def transform(ifce: Interface, targs: List[ValueType])(operation: Operation)(using PContext): Operation = operation match { - case Operation(name, tparams, cparams, vparams, bparams, body) => - val prop = ifce.properties.find { p => p.id == name }.getOrElse { Context.abort(s"Interface ${ifce} declares no operation ${name}.") } - val propTpe = prop.tpe.asInstanceOf[BlockType.Function] - - val blockTpe = BlockType.Function(tparams, propTpe.cparams, propTpe.vparams.map(transform), propTpe.bparams.map(transform), transform(propTpe.result)) - val implBlock: Block.BlockLit = Block.BlockLit(tparams, cparams, vparams, bparams, transform(body)) - val transformed: Block.BlockLit = coerce(implBlock, blockTpe) - Operation(name, transformed.tparams, transformed.cparams, transformed.vparams, transformed.bparams, - transformed.body) - } - - def transform(stmt: Stmt)(using PContext): Stmt = stmt match { - case Stmt.Def(id, block, rest) => - Stmt.Def(id, transform(block), transform(rest)) - case Stmt.Let(id, tpe, binding, rest) => - transform(binding).flatMap { e => coerce(e, transform(tpe)) }.run { e => - Stmt.Let(id, transform(tpe), e, transform(rest)) - } - case Stmt.Return(expr) => - Stmt.Return(transform(expr)) - case Stmt.Val(id, tpe, binding, body) => - Stmt.Val(id, transform(tpe), coerce(transform(binding), transform(tpe)), transform(body)) - case Stmt.App(callee, targs, vargs, bargs) => - val calleeT = transform(callee) - val tpe: BlockType.Function = calleeT.tpe match { - case tpe: BlockType.Function => tpe - case _ => sys error "Callee does not have function type" - } - val itpe = Type.instantiate(tpe, targs, tpe.cparams.map(Set(_))) - - val vCoerced = (vargs zip tpe.vparams).map { (v, tpe) => coerce(transform(v), tpe) } // coerce(coerce(transform(v), itpe), itpe, tpe) } - val bCoerced = (bargs zip tpe.bparams).map { (b, tpe) => coerce(transform(b), tpe) } // coerce(coerce(transform(b), itpe), itpe, tpe) } - - coerce(App(calleeT, targs.map(transformArg), vCoerced, bCoerced), itpe.result) - - // [S](S) => (Int, S) - case Stmt.Invoke(callee, method, methodTpe: BlockType.Function, targs, vargs, bargs) => - // Double - - val calleeT = transform(callee) - - // [S](S) => (T, S) - val (tpe: BlockType.Function, interfaceParams, interfaceArgs) = calleeT.tpe match { - // [Int] - case BlockType.Interface(name, targs) => - PContext.findInterface(name) match { - // [T] - case Some(Interface(id, tparams, properties)) => - // op: [S](S) => (T, S) - val prop = properties.find { p => p.id == method }.getOrElse { Context.panic(pp"Cannot find field ${method} in declaration of ${id}") } - - (prop.tpe.asInstanceOf[BlockType.Function], tparams, targs) - case _ => - Context.panic(pp"Should not happen. Method call on extern interface: ${stmt}") - } - case _ => - Context.panic("Should not happen. Method call on non-interface") - } - - // [S](S) => (BoxedInt, S) - val boxedTpe = Type.substitute(tpe, (interfaceParams zip interfaceArgs.map(transformArg)).toMap, Map.empty).asInstanceOf[BlockType.Function] - - // duplicated from App - val itpe = Type.instantiate(methodTpe, targs, methodTpe.cparams.map(Set(_))) - val vCoerced = (vargs zip boxedTpe.vparams).map { (a, tpe) => coerce(transform(a), tpe) } - val bCoerced = (bargs zip boxedTpe.bparams).map { (a, tpe) => coerce(transform(a), tpe) } - - // (T, S) (Int, Double) - coerce(Invoke(calleeT, method, boxedTpe, targs.map(transformArg), vCoerced, bCoerced), itpe.result) - - case Stmt.Invoke(callee, method, methodTpe, targs, vargs, bargs) => ??? - - case Stmt.Get(id, tpe, ref, capt, body) => Stmt.Get(id, transform(tpe), ref, capt, transform(body)) - case Stmt.Put(ref, capt, value, body) => Stmt.Put(ref, capt, transform(value), transform(body)) - case Stmt.If(cond, thn, els) => - Stmt.If(transform(cond), transform(thn), transform(els)) - case Stmt.Match(scrutinee, clauses, default) => - scrutinee.tpe match { - case ValueType.Data(symbol, targs) => - val Declaration.Data(tpeId, tparams, constructors) = PContext.getData(symbol) - Stmt.Match(transform(scrutinee), clauses.map { - case (id, clause: Block.BlockLit) => - val constructor = constructors.find(_.id == id).get - val casetpe: BlockType.Function = BlockType.Function(tparams, List(), - constructor.fields.map(_.tpe), List(), Type.inferType(clause.body) - ) - (id, coerce(transform(clause), Type.instantiate(casetpe, targs map transformArg, List()))) - }, default map transform) - case t => Context.abort(pp"Match on value of type ${t}") - } - case Stmt.Alloc(id, init, region, body) => - Stmt.Alloc(id, transform(init), region, transform(body)) - case Stmt.Var(id, init, cap, body) => - Stmt.Var(id, transform(init), cap, transform(body)) - case Stmt.Reset(BlockLit(tps, cps, vps, prompt :: Nil, body)) => - Stmt.Reset(BlockLit(tps, cps, vps, prompt :: Nil, coerce(transform(body), stmt.tpe))) - case Stmt.Reset(body) => ??? - case Stmt.Shift(prompt, body) => - Stmt.Shift(prompt, transform(body)) - case Stmt.Resume(k, body) => - val expected = k.tpe match { - case core.Type.TResume(result, answer) => result - case _ => ??? - } - Stmt.Resume(k, coerce(transform(body), expected)) - - case Stmt.Region(body) => - transform(body) match { - case BlockLit(tparams, cparams, vparams, bparams, body) => - val originalResult = body.tpe - val boxedResult = transformArg(originalResult) - coerce(Stmt.Region(BlockLit(tparams, cparams, vparams, bparams, coerce(body, boxedResult))), originalResult) - } - case Stmt.Hole() => Stmt.Hole() - } - - def transform(expr: Expr)(using PContext): Bind[Expr] = expr match { - case DirectApp(b, targs, vargs, bargs) => - val callee = transform(b) - val tpe: BlockType.Function = callee.tpe match { - case tpe: BlockType.Function => tpe - case _ => sys error "Callee does not have function type" - } - val itpe = Type.instantiate(tpe, targs, tpe.cparams.map(Set(_))) - val vCoerced = (vargs zip tpe.vparams).map { case (a, tpe) => coerce(transform(a), tpe) } // this was "a.tpe -> itpe -> tpe" - val bCoerced = (bargs zip tpe.bparams).map { case (a, tpe) => coerce(transform(a), tpe) } - - coerce(DirectApp(callee, targs.map(transformArg), vCoerced, bCoerced), itpe.result) - - case pure: Pure => Bind.pure(transform(pure)) - } - - def transform(pure: Pure)(using PContext): Pure = pure match { - case Pure.ValueVar(id, annotatedType) => Pure.ValueVar(id, transform(annotatedType)) - case Pure.Literal(value, annotatedType) => Pure.Literal(value, transform(annotatedType)) - case Pure.PureApp(b, targs, vargs) => - val callee = transform(b) - val tpe: BlockType.Function = callee.tpe match { - case tpe: BlockType.Function => tpe - case _ => sys error "Callee does not have function type" - } - val itpe = Type.instantiate(tpe, targs, tpe.cparams.map(Set(_))) - val vCoerced = (vargs zip tpe.vparams).map { (a, tpe) => coerce(transform(a), tpe) } - coerce(PureApp(callee, targs.map(transformArg), vCoerced), itpe.result) - - case Pure.Make(data, tag, targs, vargs) => - val dataDecl = PContext.getData(data.name) - val ctorDecl = dataDecl.constructors.find(_.id == tag).getOrElse { - Context.panic(pp"No constructor found for tag ${tag} in data type: ${data}") - } - val paramTypes = ctorDecl.fields.map(_.tpe) - - val coercedArgs = (vargs zip paramTypes).map { case (arg, paramTpe) => coerce(transform(arg), paramTpe) } - Pure.Make(transform(data), tag, targs.map(transformArg), coercedArgs) - - case Pure.Box(b, annotatedCapture) => - Pure.Box(transform(b), annotatedCapture) - } - - def transform(valueType: ValueType)(using PContext): ValueType = valueType match { - case ValueType.Var(name) => ValueType.Var(name) - case ValueType.Data(symbol, targs) => ValueType.Data(symbol, targs map transformArg) - case ValueType.Boxed(tpe, capt) => ValueType.Boxed(transform(tpe), capt) - } - - def transform(valueType: ValueType.Data)(using PContext): ValueType.Data = valueType match { - case ValueType.Data(symbol, targs) => ValueType.Data(symbol, targs map transformArg) - } - - def transform(blockType: BlockType)(using PContext): BlockType = blockType match { - case BlockType.Function(tparams, cparams, vparams, bparams, result) => - BlockType.Function(tparams, cparams, vparams map transform, bparams map transform, transform(result)) - - // Special case some types to not introduce boxing - case i @ BlockType.Interface(TState.interface, _) => i - case i @ BlockType.Interface(core.Type.ResumeSymbol, _) => i - case i @ BlockType.Interface(core.Type.PromptSymbol, _) => i - - case BlockType.Interface(symbol, targs) => BlockType.Interface(symbol, targs map transformArg) - } - - def transformArg(valueType: ValueType)(using PContext): ValueType = valueType match { - case ValueType.Var(name) => ValueType.Var(name) // assume vars are always OK - case t if box.isDefinedAt(t) => box(t).tpe - case ValueType.Data(symbol, targs) => ValueType.Data(symbol, targs map transformArg) - case ValueType.Boxed(tpe, capt) => ValueType.Boxed(transform(tpe), capt) - } - - - // Coercions - // --------- - def coerce(stmt: Stmt, to: ValueType)(using PContext): Stmt = - val from = stmt.tpe - val coerce = ValueCoercer(from, to) - if (coerce.isIdentity) { stmt } - else { - val orig = TmpValue("coe") - Stmt.Val(orig, coerce.from, stmt, Stmt.Return(coerce(ValueVar(orig, coerce.from)))) - } - - def coerce(expr: Expr, to: ValueType)(using PContext): Bind[Expr] = - val from = expr.tpe - val coerce = ValueCoercer(from, to) - if (coerce.isIdentity) { Bind.pure(expr) } - else { Bind.bind(expr).map { x => coerce(x) } } - - def coerce(pure: Pure, to: ValueType)(using PContext): Pure = ValueCoercer(pure.tpe, to)(pure) - - def coerce(block: Block, to: BlockType)(using PContext): Block = BlockCoercer(block.tpe, to)(block) - - def coerce(block: BlockLit, to: BlockType)(using PContext): BlockLit = BlockCoercer(block.tpe, to)(block) - - - sealed trait ValueCoercer { - def from: ValueType - def to: ValueType - def apply(t: Pure): Pure - def isIdentity: Boolean = false - } - object ValueCoercer { - - def apply(from: ValueType, to: ValueType)(using PContext): ValueCoercer = (from, to) match { - case (f, t) if f == t => IdentityCoercer(f, t) - case (_: ValueType.Var, _: ValueType.Var) => IdentityCoercer(from, to) // are always boxed - case (unboxed, boxed) if box.isDefinedAt(unboxed) && box(unboxed).tpe == boxed => BoxCoercer(unboxed) - case (unboxed, _: ValueType.Var) if box.isDefinedAt(unboxed) => BoxCoercer(unboxed) - case (boxed, unboxed) if box.isDefinedAt(unboxed) && box(unboxed).tpe == boxed => UnboxCoercer(unboxed) - case (_: ValueType.Var, unboxed) if box.isDefinedAt(unboxed) => UnboxCoercer(unboxed) - case (unboxed, core.Type.TTop) if box.isDefinedAt(unboxed) => BoxCoercer(unboxed) - case (core.Type.TBottom, unboxed) if box.isDefinedAt(unboxed) => BottomCoercer(unboxed) - - // assert(cs1 == cs2) // FIXME this seems to fail, what would be the correct check for subcapturing (or similar) here? - case (f @ core.ValueType.Boxed(bt1, cs1), t @ core.ValueType.Boxed(bt2, cs2)) => - new ValueCoercer { - val from: ValueType = f - val to: ValueType = t - private val bcoercer = BlockCoercer(bt1, bt2) - override def isIdentity: Boolean = bcoercer.isIdentity - override def apply(t: Pure): Pure = if isIdentity then t else t match { - case Pure.Box(b, annotatedCapture) => Pure.Box(bcoercer(b), annotatedCapture) - case other => Pure.Box(bcoercer(Block.Unbox(t)), cs2) - } - } - case _ => - //Context.warning(s"Coercing ${PrettyPrinter.format(from)} to ${PrettyPrinter.format(to)}") - IdentityCoercer(from, to) - } - - class IdentityCoercer(val from: ValueType, val to: ValueType) extends ValueCoercer { - override def apply(t: Pure): Pure = t - override def isIdentity: Boolean = true - } - case class BoxCoercer(tpe: ValueType)(using PContext) extends ValueCoercer { - override def from = tpe - override def to = box(tpe).tpe - override def apply(t: Pure): Pure = box(tpe).box(t) - } - case class UnboxCoercer(tpe: ValueType)(using PContext) extends ValueCoercer { - override def from = box(tpe).tpe - override def to = tpe - override def apply(t: Pure): Pure = box(tpe).unbox(t) - } - case class BottomCoercer(tpe: ValueType)(using PContext) extends ValueCoercer { - override def from = core.Type.TBottom - override def to = tpe - - override def apply(t: Pure): Pure = to match { - case core.Type.TInt => Pure.Literal(1337L, core.Type.TInt) - case core.Type.TDouble => Pure.Literal(13.37, core.Type.TDouble) - // Do strings need to be boxed? Really? - case core.Type.TString => Pure.Literal("", core.Type.TString) - case core.Type.TByte => Pure.Literal(1337, core.Type.TByte) - case t if box.isDefinedAt(t) => sys error s"No default value defined for ${t}" - case _ => sys error s"Trying to unbox Nothing to ${t}" - } - } - } - - sealed trait BlockCoercer { - def from: BlockType - def to: BlockType - - def apply[Te >: Block.BlockLit <: Block](t: Te): Te - def isIdentity: Boolean - } - object BlockCoercer { - def apply(from: BlockType, to: BlockType, targs: List[ValueType] = Nil)(using PContext): BlockCoercer = - (from, to) match { - case (f, t) if f == t => IdentityCoercer(f, t) - case (f: BlockType.Function, t: BlockType.Function) => FunctionCoercer(f, t, targs) - case (f: BlockType.Interface, t: BlockType.Interface) => IdentityCoercer(f, t) - case _ => Context.abort(pp"Unsupported coercion from ${from} to ${to}") - } - - class IdentityCoercer(val from: BlockType, val to: BlockType) extends BlockCoercer { - override def apply[Te >: Block.BlockLit <: Block](t: Te): Te = t - override def isIdentity: Boolean = true - } - - class FunctionCoercer( - val from: BlockType.Function, - val to: BlockType.Function, - targs: List[ValueType] - )(using PContext) extends BlockCoercer { - - private val BlockType.Function(ftparams, fcparams, fvparams, fbparams, fresult) = from - private val BlockType.Function(ttparams, tcparams, tvparams, tbparams, tresult) = to - - val vcoercers = (fvparams zip tvparams).map { case (t, f) => ValueCoercer(f, t) } - val bcoercers = (fbparams zip tbparams).map { case (t, f) => BlockCoercer(f,t) } - val rcoercer = ValueCoercer(fresult, tresult) - - override def isIdentity = (rcoercer :: vcoercers).forall(_.isIdentity) && bcoercers.forall(_.isIdentity) - - override def apply[Te >: Block.BlockLit <: Block](block: Te): Te = if (isIdentity) block else { - val vparams = vcoercers.map { c => ValueParam(TmpValue("coe"), transform(c.from)) } - val bparams = bcoercers.map { c => val id = TmpBlock("coe"); BlockParam(id, transform(c.from), Set(id)) } - - val inner = TmpBlock() - val vargs = (vcoercers zip vparams).map { case (c, p) => c(Pure.ValueVar(p.id, p.tpe)) } - val bargs = (bcoercers zip bparams).map { case (c, p) => c(Block.BlockVar(p.id, p.tpe, Set.empty)) } - Block.BlockLit(ftparams, bparams.map(_.id), vparams, bparams, - Def(inner, block, - coerce(Stmt.App( - Block.BlockVar(inner, block.tpe, block.capt), - (targs map transformArg) ++ (ftparams map core.ValueType.Var.apply), - vargs, - bargs), tresult))) - } - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index d69fc45a8..f91ee7e3b 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -1,7 +1,6 @@ package effekt package core -import effekt.source.FeatureFlag import kiama.output.ParenPrettyPrinter import scala.language.implicitConversions diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala deleted file mode 100644 index dfee5412e..000000000 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ /dev/null @@ -1,954 +0,0 @@ -package effekt -package core - -import scala.collection.mutable.ListBuffer -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.symbols.* -import effekt.symbols.builtins.* -import effekt.context.assertions.* -import effekt.core.PatternMatchingCompiler.Clause -import effekt.source.{ MatchGuard, MatchPattern, ResolveExternDefs } -import effekt.symbols.Binder.{ RegBinder, VarBinder } -import effekt.typer.Substitutions -import effekt.util.messages.{ ErrorReporter, INTERNAL_ERROR } - -object Transformer extends Phase[Typechecked, CoreTransformed] { - - val phaseName = "transformer" - - def run(input: Typechecked)(using Context) = - val Typechecked(source, tree, mod) = input - Context.initTransformerState() - - if (Context.messaging.hasErrors) { - None - } else { - val transformed = Context.timed(phaseName, source.name) { transform(mod, tree) } - Some(CoreTransformed(source, tree, mod, transformed)) - } - - enum CallingConvention { - case Pure, Direct, Control - } - def callingConvention(callable: Callable)(using Context): CallingConvention = callable match { - case f @ ExternFunction(name, _, _, _, _, _, capture, bodies) => - // resolve the preferred body again and hope it's the same - val body = ResolveExternDefs.findPreferred(bodies) - body match { - case b: source.ExternBody.EffektExternBody => CallingConvention.Control - case _ if f.capture.pure => CallingConvention.Pure - case _ if f.capture.pureOrIO => CallingConvention.Direct - case _ => CallingConvention.Control - } - case _ => CallingConvention.Control - } - - def transform(mod: Module, tree: source.ModuleDecl)(using Context): ModuleDecl = Context.using(mod) { - val source.ModuleDecl(path, imports, defs) = tree - val exports = transform(mod.exports) - val toplevelDeclarations = defs.flatMap(d => transformToplevel(d)) - - val definitions = toplevelDeclarations.collect { case d: Toplevel => d } - val externals = toplevelDeclarations.collect { case d: Extern => d } - val declarations = toplevelDeclarations.collect { case d: Declaration => d } - - // add data declarations for Bottom - val preludeDeclarations = if (mod.isPrelude) - List(Declaration.Data(builtins.BottomSymbol, Nil, Nil)) - else Nil - - // We use the includes on the symbol (since they include the prelude) - ModuleDecl(path, mod.includes.map { _.path }, preludeDeclarations ++ declarations, externals, definitions, exports) - } - - def transformToplevel(d: source.Def)(using Context): List[Toplevel | Declaration | Extern] = d match { - case f @ source.FunDef(id, tps, vps, bps, ret, body) => - val tparams = tps.map { p => p.symbol } - val cparams = bps.map { b => b.symbol.capture } - val vparams = vps map transform - val bparams = bps map transform - List(Toplevel.Def(f.symbol, BlockLit(tparams, cparams, vparams, bparams, transform(body)))) - - case d @ source.DataDef(id, _, ctors) => - val datatype = d.symbol - List(Data(datatype, datatype.tparams, datatype.constructors.map(transform))) - - case d @ source.RecordDef(id, _, _) => - val rec = d.symbol - List(Data(rec, rec.tparams, List(transform(rec.constructor)))) - - case v @ source.ValDef(id, tpe, binding) if pureOrIO(binding) => - val transformed = transform(binding) - val transformedTpe = v.symbol.tpe match { - case Some(tpe) => transform(tpe) - case None => transformed.tpe - } - List(Toplevel.Val(v.symbol, transformedTpe, transformed)) - - case v @ source.ValDef(id, _, binding) => - Context.at(d) { Context.abort("Effectful bindings not allowed on the toplevel") } - - case v @ source.DefDef(id, annot, binding) => - val sym = v.symbol - val (definition, bindings) = Context.withBindings { - Toplevel.Def(sym, transformAsBlock(binding)) - } - - bindings.map(core.Binding.toToplevel) ++ List(definition) - - case _: source.VarDef | _: source.RegDef => - Context.at(d) { Context.abort("Mutable variable bindings not allowed on the toplevel") } - - case d @ source.InterfaceDef(id, tparamsInterface, ops) => - val interface = d.symbol - List(core.Interface(interface, interface.tparams, - interface.operations.map { op => core.Property(op, operationAtDeclaration(interface.tparams, op)) })) - - case f @ source.ExternDef(pure, id, _, vps, bps, _, bodies) => - val sym@ExternFunction(name, tps, _, _, ret, effects, capt, _) = f.symbol - assert(effects.isEmpty) - val cps = bps.map(b => b.symbol.capture) - val tBody = bodies match { - case source.ExternBody.StringExternBody(ff, body) :: Nil => - val args = body.args.map(transformAsExpr).map { - case p: Pure => p: Pure - case _ => Context.abort("Spliced arguments need to be pure expressions.") - } - ExternBody.StringExternBody(ff, Template(body.strings, args)) - case source.ExternBody.Unsupported(err) :: Nil => - ExternBody.Unsupported(err) - case _ => - Context.abort("Externs should be resolved and desugared before core.Transformer") - } - List(Extern.Def(sym, tps, cps, vps map transform, bps map transform, transform(ret), transform(capt), tBody)) - - case e @ source.ExternInclude(ff, path, contents, _) => - List(Extern.Include(ff, contents.get)) - - // For now we forget about all of the following definitions in core: - case d: source.Def.Extern => Nil - case d: source.Def.Alias => Nil - - case d: source.Def.NamespaceDef => Context.panic("Should have been removed by BoxUnboxInference") - } - - /** - * In core, we only export value binders and proper functions - */ - def transform(exports: Bindings): List[Id] = exports.terms.flatMap { - case (name, syms) => syms.collect { - case sym: Callable if !sym.isInstanceOf[Operation] && !sym.isInstanceOf[Field] => sym - case sym: ValBinder => sym - } - }.toList ++ exports.namespaces.values.flatMap(transform) - - def transform(c: symbols.Constructor)(using Context): core.Constructor = - core.Constructor(c, c.fields.map(f => core.Field(f, transform(f.returnType)))) - - def transform(tree: source.Stmt)(using Context): Stmt = tree match { - // { e; stmt } --> { let _ = e; stmt } - case source.ExprStmt(e, rest) if pureOrIO(e) => - val (expr, bs) = Context.withBindings { transformAsExpr(e) } - val let = Let(Wildcard(), expr.tpe, expr, transform(rest)) - Binding(bs, let) - - // { e; stmt } --> { val _ = e; stmt } - case source.ExprStmt(e, rest) => - val binding = insertBindings { Return(transformAsPure(e)) } - Val(Wildcard(), binding.tpe, binding, transform(rest)) - - // return e - case source.Return(e) => - insertBindings { Return(transformAsPure(e)) } - - // simply drop superfluous {}s - case source.BlockStmt(b) => - transform(b) - - case source.DefStmt(d, rest) => d match { - case f @ source.FunDef(id, tps, vps, bps, ret, body) => - val tparams = tps.map { p => p.symbol } - val cparams = bps.map { b => b.symbol.capture } - val vparams = vps map transform - val bparams = bps map transform - Def(f.symbol, BlockLit(tparams, cparams, vparams, bparams, transform(body)), transform(rest)) - - case v @ source.ValDef(id, tpe, binding) => - val transformed = transform(binding) - val transformedTpe = v.symbol.tpe match { - case Some(tpe) => transform(tpe) - case None => transformed.tpe - } - Val(v.symbol, transformedTpe, transformed, transform(rest)) - - case v @ source.DefDef(id, annot, binding) => - val sym = v.symbol - insertBindings { - Def(sym, transformAsBlock(binding), transform(rest)) - } - - case v @ source.RegDef(id, _, reg, binding) => - val sym = v.symbol - insertBindings { - Alloc(sym, Context.bind(transform(binding)), sym.region, transform(rest)) - } - - case v @ source.VarDef(id, _, binding) => - val sym = v.symbol - insertBindings { - Var(sym, Context.bind(transform(binding)), sym.capture, transform(rest)) - } - - case d: source.Def.Extern => Context.panic("Only allowed on the toplevel") - case d: source.Def.Declaration => Context.panic("Only allowed on the toplevel") - - // For now we forget about all of the following definitions in core: - case d: source.Def.Alias => transform(rest) - - case d: source.Def.NamespaceDef => Context.panic("Should have been removed by BoxUnboxInference") - } - } - - def transformUnbox(tree: source.Term)(implicit C: Context): Block = tree match { - case source.Unbox(b) => Unbox(transformAsPure(b)) - case _ => Unbox(transformAsPure(tree)) - } - - def transformBox(tree: source.Term)(implicit C: Context): Pure = tree match { - case source.Box(capt, block) => Box(transformAsBlock(tree), transform(Context.inferredCapture(block))) - case _ => Box(transformAsBlock(tree), transform(Context.inferredCapture(tree))) - } - - /** - * Transforms the source to a function (expecting to be called using [[core.Stmt.App]] or an interface. - */ - def transformAsBlock(tree: source.Term)(using Context): Block = tree match { - case v: source.Var => - val sym = v.definition - Context.blockTypeOf(sym) match { - case _: BlockType.FunctionType => transformAsControlBlock(tree) - case _: BlockType.InterfaceType => transformAsObject(tree) - } - case _: source.BlockLiteral => transformAsControlBlock(tree) - case _: source.New => transformAsObject(tree) - case _ => transformUnbox(tree) - } - - /** - * Transforms the source to an interface block - */ - def transformAsObject(tree: source.Term)(using Context): Block = tree match { - case v: source.Var => - BlockVar(v.definition.asInstanceOf[BlockSymbol]) - - case source.BlockLiteral(tparams, vparams, bparams, body) => - Context.panic(s"Using block literal ${tree} but an object was expected.") - - case source.New(impl) => - New(transform(impl, None)) - - case _ => transformUnbox(tree) - } - /** - * Transforms the source to a function block that expects to be called using [[core.Stmt.App]]. - */ - def transformAsControlBlock(tree: source.Term)(using Context): Block = tree match { - case v: source.Var => - val sym = v.definition - val tpe = Context.blockTypeOf(sym) - tpe match { - case BlockType.FunctionType(tparams, cparams, vparamtps, bparamtps, restpe, effects) => - // if this block argument expects to be called using PureApp or DirectApp, make sure it is - // by wrapping it in a BlockLit - val targs = tparams.map(core.ValueType.Var.apply) - val vparams = vparamtps.map { t => core.ValueParam(TmpValue("valueParam"), transform(t))} - val vargs = vparams.map { case core.ValueParam(id, tpe) => Pure.ValueVar(id, tpe) } - - // [[ f ]] = { (x) => f(x) } - def etaExpandPure(b: ExternFunction): BlockLit = { - assert(bparamtps.isEmpty) - assert(effects.isEmpty) - assert(cparams.isEmpty) - BlockLit(tparams, Nil, vparams, Nil, - Stmt.Return(PureApp(BlockVar(b), targs, vargs))) - } - - // [[ f ]] = { [A](x) => make f[A](x) } - def etaExpandConstructor(b: Constructor): BlockLit = { - assert(bparamtps.isEmpty) - assert(effects.isEmpty) - assert(cparams.isEmpty) - BlockLit(tparams, Nil, vparams, Nil, - Stmt.Return(Make(core.ValueType.Data(b.tpe, targs), b, targs, vargs))) - } - - // [[ f ]] = { (x){g} => let r = f(x){g}; return r } - def etaExpandDirect(f: ExternFunction): BlockLit = { - assert(effects.isEmpty) - val bparams = bparamtps.map { t => val id = TmpBlock("etaParam"); core.BlockParam(id, transform(t), Set(id)) } - val bargs = bparams.map { - case core.BlockParam(id, tpe, capt) => Block.BlockVar(id, tpe, capt) - } - val result = TmpValue("etaBinding") - val resultBinding = DirectApp(BlockVar(f), targs, vargs, bargs) - BlockLit(tparams, bparams.map(_.id), vparams, bparams, - core.Let(result, resultBinding.tpe, resultBinding, - Stmt.Return(Pure.ValueVar(result, transform(restpe))))) - } - - sym match { - case _: ValueSymbol => transformUnbox(tree) - case cns: Constructor => etaExpandConstructor(cns) - case f: ExternFunction if callingConvention(f) == CallingConvention.Pure => etaExpandPure(f) - case f: ExternFunction if callingConvention(f) == CallingConvention.Direct => etaExpandDirect(f) - // does not require change of calling convention, so no eta expansion - case sym: BlockSymbol => BlockVar(sym) - } - case t: BlockType.InterfaceType => - Context.abort(s"Expected a function but got an object of type ${t}") - } - - case source.BlockLiteral(tps, vps, bps, body) => - val tparams = tps.map(t => t.symbol) - val cparams = bps.map { b => b.symbol.capture } - BlockLit(tparams, cparams, vps map transform, bps map transform, transform(body)) - - case s @ source.New(impl) => - Context.abort(s"Expected a function but got an object instantiation: ${s}") - - case _ => transformUnbox(tree) - } - - def transformAsPure(tree: source.Term)(using Context): Pure = transformAsExpr(tree) match { - case p: Pure => p - case e: Expr => Context.bind(e) - } - - def transformAsExpr(tree: source.Term)(using Context): Expr = tree match { - case v: source.Var => v.definition match { - case sym: RefBinder => - val stateType = Context.blockTypeOf(sym) - val tpe = TState.extractType(stateType) - val stateId = Id("s") - // emits `let s = !ref; return s` - Context.bind(Get(stateId, transform(tpe), sym, transform(Context.captureOf(sym)), Return(core.ValueVar(stateId, transform(tpe))))) - case sym: ValueSymbol => ValueVar(sym) - case sym: BlockSymbol => transformBox(tree) - } - - case source.Literal(value, tpe) => - Literal(value, transform(tpe)) - - // [[ sc.field ]] = val x = sc match { tag: { (_, _, x, _) => return x } }; ... - case s @ source.Select(receiver, selector) => - val field: Field = s.definition - - val constructor = field.constructor - val dataType: symbols.TypeConstructor = constructor.tpe - val universals: List[symbols.TypeParam] = dataType.tparams - - // allTypeParams = universals ++ existentials - val allTypeParams: List[symbols.TypeParam] = constructor.tparams - - assert(allTypeParams.length == universals.length, "Existentials on record selection not supported, yet.") - - val scrutineeTypeArgs = Context.inferredTypeOf(receiver) match { - case effekt.symbols.ValueType.ValueTypeApp(constructor, args) => args - case _ => Context.panic("Should not happen: selection from non ValueTypeApp") - } - - val substitution = Substitutions((universals zip scrutineeTypeArgs).toMap, Map.empty) - - val selected = Id("x") - val tpe = transform(Context.inferredTypeOf(s)) - val params = constructor.fields.map { - case f: Field => - val tpe = transform(substitution.substitute(f.returnType)) - core.ValueParam(if f == field then selected else Id("_"), tpe) - } - Context.bind(Stmt.Match(transformAsPure(receiver), - List((constructor, BlockLit(Nil, Nil, params, Nil, Stmt.Return(Pure.ValueVar(selected, tpe))))), None)) - - case source.Box(capt, block) => - transformBox(block) - - case source.New(impl) => - transformBox(tree) - - case source.Unbox(b) => - transformBox(tree) - - case source.BlockLiteral(tps, vps, bps, body) => - transformBox(tree) - - case source.If(List(MatchGuard.BooleanGuard(cond)), thn, els) => - val c = transformAsPure(cond) - Context.bind(If(c, transform(thn), transform(els))) - - case source.If(guards, thn, els) => - val thnClause = preprocess("thn", Nil, guards, transform(thn)) - val elsClause = preprocess("els", Nil, Nil, transform(els)) - Context.bind(PatternMatchingCompiler.compile(List(thnClause, elsClause))) - - // case i @ source.If(guards, thn, els) => - // val compiled = collectClauses(i) - // .map(PatternMatchingCompiler.compile) - // .getOrElse(Context.panic("Should not happen")) - // Context.bind(compiled) - - // [[ while(cond) { body } ]] = - // def loop$13() = if ([[cond]]) { [[ body ]]; loop$13() } else { return () } - // loop$13() - case source.While(guards, body, default) => - val loopName = TmpBlock("while") - val loopType = core.BlockType.Function(Nil, Nil, Nil, Nil, core.Type.TUnit) - - // TODO double check: probably we are forgetting the capture of the guards! - val loopCapt = transform(Context.inferredCapture(body)) - val loopCall = Stmt.App(core.BlockVar(loopName, loopType, loopCapt), Nil, Nil, Nil) - - val transformedBody = transform(body) - val thenBranch = Stmt.Val(TmpValue("while_thn"), transformedBody.tpe, transformedBody, loopCall) - val elseBranch = default.map(transform).getOrElse(Return(Literal((), core.Type.TUnit))) - - val loopBody = guards match { - case List(MatchGuard.BooleanGuard(cond)) => - insertBindings { core.If(transformAsPure(cond), thenBranch, elseBranch) } - case _ => - insertBindings { - val thenClause = preprocess("guard_thn", Nil, guards, thenBranch) - val elseClause = preprocess("guard_els", Nil, Nil, elseBranch) - PatternMatchingCompiler.compile(List(thenClause, elseClause)) - } - } - - Context.bind(loopName, Block.BlockLit(Nil, Nil, Nil, Nil, loopBody)) - - Context.bind(loopCall) - - // Empty match (matching on Nothing) - case source.Match(List(sc), Nil, None) => - val scrutinee: ValueVar = Context.bind(transformAsPure(sc)) - Context.bind(core.Match(scrutinee, Nil, None)) - - case source.Match(scs, cs, default) => - // (1) Bind scrutinee and all clauses so we do not have to deal with sharing on demand. - val scrutinees: List[ValueVar] = scs.map{ sc => Context.bind(transformAsPure(sc)) } - val clauses = cs.zipWithIndex.map((c, i) => preprocess(s"k${i}", scrutinees, c)) - val defaultClause = default.map(stmt => preprocess("k_els", Nil, Nil, transform(stmt))).toList - val compiledMatch = PatternMatchingCompiler.compile(clauses ++ defaultClause) - Context.bind(compiledMatch) - - case source.TryHandle(prog, handlers) => - - val transformedProg = transform(prog) - - val answerType = transformedProg.tpe - - // create a fresh prompt, a variable referring to it, and a parameter binding it - val promptId = Id("p") - val promptCapt = Id("pCapt") - val promptTpe = Type.TPrompt(answerType) - val promptVar: core.BlockVar = core.BlockVar(promptId, Type.TPrompt(answerType), Set(promptCapt)) - val promptParam: core.BlockParam = core.BlockParam(promptId, promptTpe, Set(promptCapt)) - - val transformedHandlers = handlers.map { - case h @ source.Handler(cap, impl) => - val id = h.capability.get.symbol - Binding.Def(id, New(transform(impl, Some(promptVar)))) - } - - val body: BlockLit = BlockLit(Nil, List(promptCapt), Nil, List(promptParam), - Binding(transformedHandlers, transform(prog))) - - Context.bind(Reset(body)) - - case r @ source.Region(name, body) => - val region = r.symbol - val tpe = Context.blockTypeOf(region) - val cap: core.BlockParam = core.BlockParam(region, transform(tpe), Set(region.capture)) - Context.bind(Region(BlockLit(Nil, List(region.capture), Nil, List(cap), transform(body)))) - - case source.Hole(stmts) => - Context.bind(Hole()) - - case a @ source.Assign(id, expr) => - val sym = a.definition - // emits `ref := value; return ()` - Context.bind(Put(sym, transform(Context.captureOf(sym)), transformAsPure(expr), Return(Literal((), core.Type.TUnit)))) - Literal((), core.Type.TUnit) - - // methods are dynamically dispatched, so we have to assume they are `control`, hence no PureApp. - case c @ source.MethodCall(receiver, id, targs, vargs, bargs) => - val rec = transformAsObject(receiver) - val typeArgs = Context.typeArguments(c).map(transform) - val valueArgs = vargs.map(transformAsPure) - val blockArgs = bargs.map(transformAsBlock) - - // TODO if we always just use .capt, then why annotate it? - // val captArgs = blockArgs.map(_.capt) //bargs.map(b => transform(Context.inferredCapture(b))) - - val receiverType = Context.inferredBlockTypeOf(receiver) - val operation = c.definition.asOperation - val opType = transform(operationAtCallsite(receiverType, operation)) - - // Do not pass type arguments for the type constructor of the receiver. - val remainingTypeArgs = typeArgs.drop(operation.interface.tparams.size) - - Context.bind(Invoke(rec, operation, opType, remainingTypeArgs, valueArgs, blockArgs)) - - case c @ source.Call(source.ExprTarget(source.Unbox(expr)), targs, vargs, bargs) => - - val (funTpe, capture) = Context.inferredTypeOf(expr) match { - case BoxedType(s: FunctionType, c: CaptureSet) => (s, c) - case _ => Context.panic("Should be a boxed function type with a known capture set.") - } - val e = transformAsPure(expr) - val typeArgs = Context.typeArguments(c).map(transform) - val valueArgs = vargs.map(transformAsPure) - val blockArgs = bargs.map(transformAsBlock) - // val captArgs = blockArgs.map(b => b.capt) //transform(Context.inferredCapture(b))) - - Context.bind(App(Unbox(e), typeArgs, valueArgs, blockArgs)) - - case c @ source.Call(fun: source.IdTarget, _, vargs, bargs) => - // assumption: typer removed all ambiguous references, so there is exactly one - makeFunctionCall(c, fun.definition, vargs, bargs) - - case c @ source.Call(s: source.ExprTarget, targs, vargs, bargs) => - Context.panic("Should not happen. Unbox should have been inferred.") - - case source.Do(effect, id, targs, vargs, bargs) => - Context.panic("Should have been translated away (to explicit selection `@CAP.op()`) by capability passing.") - } - - /** - * Aims to flatten sequenced ifs into a single match - */ - def collectClauses(term: source.Term)(using Context): Option[List[Clause]] = term match { - case source.If(guards, thn, els) => - val thenClause = preprocess("thn", Nil, guards, transform(thn)) - val elseClauses = collectClauses(els) match { - case Some(clauses) => clauses - case None => List(preprocess("els", Nil, Nil, transform(els))) - } - Some(thenClause :: elseClauses) - case _ => None - } - def collectClauses(stmt: source.Stmt)(using Context): Option[List[Clause]] = stmt match { - case source.Stmt.Return(d) => collectClauses(d) - case _ => None - } - - /** - * Establishes a canonical ordering of methods by using - * the order in which they are declared in the signature (like with handlers) - */ - def transform(impl: source.Implementation, prompt: Option[core.BlockVar])(using Context): core.Implementation = { - val members = impl.clauses - val clauses = members.map { cl => (cl.definition, cl) }.toMap - val sig = impl.definition - - val coreType = transform(Context.inferredBlockTypeOf(impl)) match { - case i: core.BlockType.Interface => i - case _ => Context.panic("Should be an interface type.") - } - - Implementation(coreType, sig.operations.map(clauses.apply).map { - case op @ source.OpClause(id, tparams, vparams, bparams, ret, body, resume) => - val vps = vparams.map(transform) - val tps = tparams.map { p => p.symbol } - - prompt match { - case Some(prompt) => - val resumeSymbol = resume.symbol.asInstanceOf[BlockSymbol] - Context.blockTypeOf(resumeSymbol) match { - - // uni-directional - // --------------- - // [[ def op(x) = ... resume123(...) ... ]] - // = - // def op(x) = shift(p) { k => - // def resume123(y) = resume(k) { return y }; - // ... resume123(...) ... - // } - // - // Function resume123 will hopefully be inlined by the inliner / optimizer - case BlockType.FunctionType(_, _, List(result), _, answer, _) => - - val resultTpe = transform(result) - val answerTpe = transform(answer) - - // (1) bind the continuation (k)itself - - // THIS IS NOT CORRECT: in the source language the capture of resume is transparent - // This suggests we need to change the representation of Shift and its typing... - val resumeCapture = Id("resume") - val resumeId = Id("k") - val resumeTpe = core.Type.TResume(resultTpe, answerTpe) - val resumeParam = core.BlockParam(resumeId, resumeTpe, Set(resumeCapture)) - val resumeVar: core.BlockVar = core.BlockVar(resumeId, resumeTpe, Set(resumeCapture)) - - // (2) eta-expand and bind continuation as a function - val resumeArgId = Id("a") - val resumeArgParam: core.ValueParam = core.ValueParam(resumeArgId, resultTpe) - val resumeFun: core.BlockLit = core.BlockLit(Nil, Nil, List(resumeArgParam), Nil, - core.Stmt.Resume(resumeVar, core.Stmt.Return(core.ValueVar(resumeArgId, resultTpe)))) - - core.Operation(op.definition, tps, Nil, vps, Nil, - core.Shift(prompt, core.BlockLit(Nil, List(resumeCapture), Nil, resumeParam :: Nil, - core.Def(resumeSymbol, resumeFun, - transform(body))))) - - // bi-directional - // -------------- - // [[ def op(x) = ... resume123 { {f} => ... } ... ]] - // = - // def op(x) {g} = shift(p) { k => - // def resume123 {h} = resume(k) { h {g} } - // ... resume123 { {f} => ... } ... - // } - // - // Again the typing is wrong in core now. `g` will be tracked, but resume should subtract it. - case BlockType.FunctionType(_, _, _, List(argTpe @ BlockType.FunctionType(_, _, _, _, result, _)), answer, _) => - val resultTpe = transform(result) - val answerTpe = transform(answer) - - val resumeArgTpe @ core.BlockType.Function(_, cps, _, bps, _) = transform(argTpe) : @unchecked - - // (0) compute the block parameters from the type of the continuation (since this is what typer annotates) - val bparams: List[core.BlockParam] = (cps zip bps) map { case (capt, tpe) => - core.BlockParam(Id("g"), tpe, Set(capt)) - } - val bvars = bparams.map { b => core.BlockVar(b.id, b.tpe, b.capt) } - - // (1) bind the continuation (k) itself - val resumeCapture = Id("resume") - val resumeId = Id("k") - val resumeTpe = core.Type.TResume(resultTpe, answerTpe) - val resumeParam: core.BlockParam = core.BlockParam(resumeId, resumeTpe, Set(resumeCapture)) - val resumeVar: core.BlockVar = core.BlockVar(resumeId, resumeTpe, Set(resumeCapture)) - - // (2) eta-expand and bind continuation as a function - val resumeArgId = Id("h") - val resumeArgCapture = Id("h") - val resumeArgParam: core.BlockParam = core.BlockParam(resumeArgId, resumeArgTpe, Set(resumeArgCapture)) - val resumeArgVar: core.BlockVar = core.BlockVar(resumeArgId, resumeArgTpe, Set(resumeArgCapture)) - val resumeFun: core.BlockLit = core.BlockLit(Nil, List(resumeArgCapture), Nil, List(resumeArgParam), - core.Stmt.Resume(resumeVar, core.Stmt.App(resumeArgVar, Nil, Nil, bvars))) - - core.Operation(op.definition, tps, cps, vps, bparams, - core.Shift(prompt, core.BlockLit(Nil, List(resumeCapture), Nil, resumeParam :: Nil, - core.Stmt.Def(resumeSymbol, resumeFun, - transform(body))))) - - case _ => ??? - } - - case None => - val bps = bparams map transform - val cps = bparams map { b => b.symbol.capture } - core.Operation(op.definition, tps, cps, vps, bps, transform(body)) - } - }) - } - - def preprocess(label: String, scs: List[ValueVar], clause: source.MatchClause)(using Context): Clause = { - val patterns = (clause.pattern, scs) match { - case (source.MultiPattern(ps), scs) => scs.zip(ps) - case (pattern, List(sc)) => List((sc, clause.pattern)) - case (_, _) => Context.abort("Malformed multi-match") - } - preprocess(label, patterns, clause.guards, transform(clause.body)) - } - - def preprocess(label: String, patterns: List[(ValueVar, source.MatchPattern)], guards: List[source.MatchGuard], body: core.Stmt)(using Context): Clause = { - import PatternMatchingCompiler.* - - def boundInPattern(p: source.MatchPattern): List[core.ValueParam] = p match { - case p @ source.AnyPattern(id) => List(ValueParam(p.symbol)) - case source.TagPattern(id, patterns) => patterns.flatMap(boundInPattern) - case _: source.LiteralPattern | _: source.IgnorePattern => Nil - case source.MultiPattern(patterns) => patterns.flatMap(boundInPattern) - } - def boundInGuard(g: source.MatchGuard): List[core.ValueParam] = g match { - case MatchGuard.BooleanGuard(condition) => Nil - case MatchGuard.PatternGuard(scrutinee, pattern) => boundInPattern(pattern) - } - def boundTypesInPattern(p: source.MatchPattern): List[Id] = p match { - case source.AnyPattern(id) => List() - case p @ source.TagPattern(id, patterns) => Context.annotation(Annotations.TypeParameters, p) ++ patterns.flatMap(boundTypesInPattern) - case _: source.LiteralPattern | _: source.IgnorePattern => Nil - case source.MultiPattern(patterns) => patterns.flatMap(boundTypesInPattern) - } - def boundTypesInGuard(g: source.MatchGuard): List[Id] = g match { - case MatchGuard.BooleanGuard(condition) => Nil - case MatchGuard.PatternGuard(scrutinee, pattern) => boundTypesInPattern(pattern) - } - def equalsFor(tpe: symbols.ValueType): (Pure, Pure) => Pure = - val prelude = Context.module.findDependency(QualifiedName(Nil, "effekt")).getOrElse { - Context.panic(pp"${Context.module.name.name}: Cannot find 'effekt' in prelude, which is necessary to compile pattern matching.") - } - prelude.exports.terms("infixEq") collect { - case sym: Callable => (sym, sym.toType) - } collectFirst { - // specialized version - case (sym, FunctionType(Nil, Nil, List(`tpe`, `tpe`), Nil, builtins.TBoolean, _)) => - (lhs: Pure, rhs: Pure) => core.PureApp(BlockVar(sym), Nil, List(lhs, rhs)) - // generic version - case (sym, FunctionType(List(tparam), Nil, List(ValueTypeRef(t1), ValueTypeRef(t2)), Nil, builtins.TBoolean, _)) - if t1 == tparam && t2 == tparam => - (lhs: Pure, rhs: Pure) => core.PureApp(BlockVar(sym), List(transform(tpe)), List(lhs, rhs)) - } getOrElse { Context.panic(pp"Cannot find == for type ${tpe} in prelude!") } - - // create joinpoint - val tparams = patterns.flatMap { case (sc, p) => boundTypesInPattern(p) } ++ guards.flatMap(boundTypesInGuard) - val params = patterns.flatMap { case (sc, p) => boundInPattern(p) } ++ guards.flatMap(boundInGuard) - val joinpoint = Context.bind(TmpBlock(label), BlockLit(tparams, Nil, params, Nil, body)) - - def transformPattern(p: source.MatchPattern): Pattern = p match { - case source.AnyPattern(id) => - Pattern.Any(id.symbol) - case p @ source.TagPattern(id, patterns) => - val tparams = Context.annotation(Annotations.TypeParameters, p) - Pattern.Tag(id.symbol, tparams, patterns.map { p => (transformPattern(p), transform(Context.inferredTypeOf(p))) }) - case source.IgnorePattern() => - Pattern.Ignore() - case source.LiteralPattern(source.Literal(value, tpe)) => - Pattern.Literal(Literal(value, transform(tpe)), equalsFor(tpe)) - case source.MultiPattern(patterns) => - Context.panic("Multi-pattern should have been split on toplevel / nested MultiPattern") - } - - def transformGuard(p: source.MatchGuard): List[Condition] = - val (cond, bindings) = Context.withBindings { - p match { - case MatchGuard.BooleanGuard(condition) => - Condition.Predicate(transformAsPure(condition)) - case MatchGuard.PatternGuard(scrutinee, pattern) => - val x = Context.bind(transformAsPure(scrutinee)) - Condition.Patterns(Map(x -> transformPattern(pattern))) - } - } - bindings.toList.map { - case Binding.Val(name, tpe, binding) => Condition.Val(name, tpe, binding) - case Binding.Let(name, tpe, binding) => Condition.Let(name, tpe, binding) - case Binding.Def(name, binding) => Context.panic("Should not happen") - } :+ cond - - val transformedPatterns = patterns.map { case (sc, p) => sc -> transformPattern(p) }.toMap - val transformedGuards = guards.flatMap(transformGuard) - val conditions = if transformedPatterns.isEmpty then transformedGuards else Condition.Patterns(transformedPatterns) :: guards.flatMap(transformGuard) - - Clause(conditions, joinpoint, tparams.map(x => core.ValueType.Var(x)), params.map(p => core.ValueVar(p.id, p.tpe))) - } - - /** - * Computes the block type the selected symbol. - * - * For instance, receiver can be `State[Int]`, interface could be the symbol of `State` and member could be `get`. - * If `member` is an operation, the type arguments of the receiver are substituted for the leading type parameters, - * while the remaining type parameters are kept. - */ - def operationAtCallsite(receiver: symbols.BlockType, member: symbols.Operation)(using Context): BlockType = receiver.asInterfaceType match { - case InterfaceType(i: Interface, targs) => member match { - // For operations, we substitute the first type parameters by concrete type args. - case Operation(name, tparams, vparams, bparams, resultType, effects, _) => - val substitution = Substitutions((tparams zip targs).toMap, Map.empty) - val remainingTypeParams = tparams.drop(targs.size) - // TODO this is exactly like in [[Callable.toType]] -- TODO repeated here: - // TODO currently the return type cannot refer to the annotated effects, so we can make up capabilities - // in the future namer needs to annotate the function with the capture parameters it introduced. - val cparams = bparams.map(b => b.capture) ++ CanonicalOrdering(effects.toList).map { tpe => symbols.CaptureParam(tpe.name) } - val vparamTpes = vparams.map(t => substitution.substitute(t.tpe.getOrElse { - INTERNAL_ERROR("Operation value parameters should have an annotated type.") - })) - val bparamTpes = bparams.map(b => substitution.substitute(b.tpe.getOrElse { - INTERNAL_ERROR("Operation block parameters should have an annotated type.") - })) - - FunctionType(remainingTypeParams, cparams, vparamTpes, bparamTpes, substitution.substitute(resultType), substitution.substitute(effects)) - } - - case InterfaceType(i: ExternInterface, targs) => - Context.panic("Cannot select from an extern interface") - } - - def operationAtDeclaration(tparamsInterface: List[Id], op: symbols.Operation)(using Context): core.BlockType = op match { - case symbols.Operation(name, tps, vps, bps, resultType, effects, interface) => - // like in asSeenFrom we need to make up cparams, they cannot occur free in the result type - val capabilities = CanonicalOrdering(effects.toList) - val tparams = tps.drop(tparamsInterface.size) - val bparamsBlocks = bps.map(b => transform(b.tpe.getOrElse { - INTERNAL_ERROR("Interface declarations should have annotated types.") - })) - val bparamsCapabilities = capabilities.map(transform) - val bparams = bparamsBlocks ++ bparamsCapabilities - val vparams = vps.map(p => transform(p.tpe.get)) - val cparams = bps.map(_.capture) ++ capabilities.map { tpe => symbols.CaptureParam(tpe.name) } - - // here we reconstruct the block type - core.BlockType.Function(tparams, cparams, vparams, bparams, transform(resultType)) - } - - def makeFunctionCall(call: source.CallLike, sym: TermSymbol, vargs: List[source.Term], bargs: List[source.Term])(using Context): Expr = { - // the type arguments, inferred by typer - val targs = Context.typeArguments(call).map(transform) - // val cargs = bargs.map(b => transform(Context.inferredCapture(b))) - - val vargsT = vargs.map(transformAsPure) - val bargsT = bargs.map(transformAsBlock) - - sym match { - case f: Callable if callingConvention(f) == CallingConvention.Pure => - PureApp(BlockVar(f), targs, vargsT) - case f: Callable if callingConvention(f) == CallingConvention.Direct => - DirectApp(BlockVar(f), targs, vargsT, bargsT) - case r: Constructor => - if (bargs.nonEmpty) Context.abort("Constructors cannot take block arguments.") - val universals = targs.take(r.tpe.tparams.length) - val existentials = targs.drop(r.tpe.tparams.length) - Make(core.ValueType.Data(r.tpe, universals), r, existentials, vargsT) - case f: Operation => - Context.panic("Should have been translated to a method call!") - case f: Field => - Context.panic("Should have been translated to a select!") - case f: BlockSymbol => - Context.bind(App(BlockVar(f), targs, vargsT, bargsT)) - case f: ValueSymbol => - Context.bind(App(Unbox(ValueVar(f)), targs, vargsT, bargsT)) - } - } - - def transform(p: source.BlockParam)(using Context): core.BlockParam = BlockParam(p.symbol) - - def transform(p: source.ValueParam)(using Context): core.ValueParam = ValueParam(p.symbol) - - def insertBindings(stmt: => Stmt)(using Context): Stmt = { - val (body, bindings) = Context.withBindings { stmt } - Binding(bindings, body) - } - - // Translation on Types - // -------------------- - def transform(tpe: ValueType)(using Context): core.ValueType = tpe match { - case ValueType.BoxedType(tpe, capture) => core.ValueType.Boxed(transform(tpe), transform(capture)) - case ValueType.ValueTypeRef(tvar) => core.ValueType.Var(tvar) - case ValueType.ValueTypeApp(tc, args) => core.ValueType.Data(tc, args.map(transform)) - } - - def transform(tpe: BlockType)(using Context): core.BlockType = tpe match { - case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => - - val capabilityTypes = CanonicalOrdering(effects.toList).map(transform) - val allBlockParams = bparams.map(transform) ++ capabilityTypes - - assert(cparams.size == allBlockParams.size, - s"""Internal error: number of block parameters does not match number of capture parameters. - | - | Blockparams: ${bparams} - | Effects: ${capabilityTypes} - | Captures: ${cparams} - |""".stripMargin) - - core.BlockType.Function(tparams, cparams, vparams.map(transform), allBlockParams, transform(result)) - case BlockType.InterfaceType(tc, args) => core.BlockType.Interface(tc, args.map(transform)) - } - - def transform(capt: Captures)(using Context): core.Captures = capt match { - case CaptUnificationVar(role) => Context.panic(pp"$capt should be a concrete capture set in this phase.") - case CaptureSet(captures) => captures.map(x => x: Symbol) // that is really a noop... - } - - // Helpers - // ------- - - // Helpers to constructed typed trees - def ValueParam(id: ValueSymbol)(using Context): core.ValueParam = - core.ValueParam(id, transform(Context.valueTypeOf(id))) - - def BlockParam(id: BlockSymbol)(using Context): core.BlockParam = - core.BlockParam(id, transform(Context.blockTypeOf(id)), Set(id)) - - def ValueVar(id: ValueSymbol)(using Context): core.ValueVar = - core.ValueVar(id, transform(Context.valueTypeOf(id))) - - def BlockVar(id: BlockSymbol)(using Context): core.BlockVar = - core.BlockVar(id, transform(Context.blockTypeOf(id)), transform(Context.captureOf(id))) - - def asConcreteCaptureSet(c: Captures)(using Context): CaptureSet = c match { - case c: CaptureSet => c - case _ => Context.panic("All capture unification variables should have been replaced by now.") - } - - // we can conservatively approximate to false, in order to disable the optimizations - def pureOrIO(t: source.Tree)(using Context): Boolean = - Context.inferredCaptureOption(t) match { - case Some(capt) => asConcreteCaptureSet(capt).pureOrIO - case _ => false - } - - def isPure(t: source.Tree)(using Context): Boolean = Context.inferredCaptureOption(t) match { - case Some(capt) => asConcreteCaptureSet(capt).pure - case _ => false - } - - def pureOrIO(t: BlockSymbol)(using Context): Boolean = asConcreteCaptureSet(Context.captureOf(t)).pureOrIO - -} - -trait TransformerOps extends ContextOps { Context: Context => - - /** - * A _mutable_ ListBuffer that stores all bindings to be inserted at the current scope - */ - private var bindings: ListBuffer[Binding] = ListBuffer() - - private[core] def initTransformerState() = { - bindings = ListBuffer() - } - - /** - * Introduces a binding for the given statement. - * - * @param tpe the type of the bound statement - * @param s the statement to be bound - */ - private[core] def bind(s: Stmt): ValueVar = { - - // create a fresh symbol and assign the type - val x = TmpValue("r") - - val binding = Binding.Val(x, s.tpe, s) - bindings += binding - - ValueVar(x, s.tpe) - } - - private[core] def bind(e: Expr): ValueVar = e match { - case x: ValueVar => x - case e => - // create a fresh symbol and assign the type - val x = TmpValue("r") - - val binding = Binding.Let(x, e.tpe, e) - bindings += binding - - ValueVar(x, e.tpe) - } - - private[core] def bind(name: BlockSymbol, b: Block): BlockVar = { - val binding = Binding.Def(name, b) - bindings += binding - BlockVar(name, b.tpe, b.capt) - } - - private[core] def withBindings[R](block: => R): (R, List[Binding]) = Context in { - val before = bindings - val b = ListBuffer.empty[Binding] - bindings = b - val result = block - bindings = before - (result, b.toList) - } -} diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index bdf591826..d1d042319 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -1,7 +1,6 @@ package effekt package core -import effekt.source.FeatureFlag import effekt.util.Structural import effekt.util.messages.INTERNAL_ERROR import effekt.util.messages.ErrorReporter @@ -125,6 +124,11 @@ case class Constructor(id: Id, fields: List[Field]) extends Tree case class Field(id: Id, tpe: ValueType) extends Tree case class Property(id: Id, tpe: BlockType) extends Tree +enum FeatureFlag extends Tree { + case NamedFeatureFlag(id: String) + case Default +} + /** * FFI external definitions */ diff --git a/effekt/shared/src/main/scala/effekt/core/Type.scala b/effekt/shared/src/main/scala/effekt/core/Type.scala index cdba76670..bcdf86d21 100644 --- a/effekt/shared/src/main/scala/effekt/core/Type.scala +++ b/effekt/shared/src/main/scala/effekt/core/Type.scala @@ -56,7 +56,7 @@ object Type { val TInt = ValueType.Data(builtins.IntSymbol, Nil) val TChar = ValueType.Data(builtins.CharSymbol, Nil) val TByte = ValueType.Data(builtins.ByteSymbol, Nil) - val TBoolean = ValueType.Data(builtins.BooleanSymbol, Nil) + val TBoolean = ValueType.Data(builtins.BoolSymbol, Nil) val TString = ValueType.Data(builtins.StringSymbol, Nil) val TDouble = ValueType.Data(builtins.DoubleSymbol, Nil) diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/DropBindings.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/DropBindings.scala index 7afe05cf0..2535cc515 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/DropBindings.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/DropBindings.scala @@ -24,9 +24,8 @@ object DropBindings extends Phase[CoreTransformed, CoreTransformed] { def run(input: CoreTransformed)(using C: Context): Option[CoreTransformed] = input match { - case CoreTransformed(source, tree, mod, core) => - val main = C.checkMain(mod) - Some(CoreTransformed(source, tree, mod, apply(Set(main), core))) + case CoreTransformed(source, core, main) => + Some(CoreTransformed(source, apply(Set(main), core), main)) } def apply(entrypoints: Set[Id], m: ModuleDecl): ModuleDecl = diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala index b96da7e12..efbec6319 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala @@ -390,7 +390,7 @@ object Normalizer { normal => bvars = bvars :+ x // introduce a binding case (bparam, block) => - val id = symbols.TmpBlock("blockBinding") + val id = Id("blockBinding") bindings = bindings :+ Binding.Def(id, block) bvars = bvars :+ Block.BlockVar(id, block.tpe, block.capt) copyUsage(bparam.id, id) diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala index 2d87e2d07..bb38977d9 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala @@ -2,7 +2,6 @@ package effekt package core package optimizer -import effekt.PhaseResult.CoreTransformed import effekt.context.Context import kiama.util.Source @@ -13,15 +12,14 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] { def run(input: CoreTransformed)(using Context): Option[CoreTransformed] = input match { - case CoreTransformed(source, tree, mod, core) => - val term = Context.checkMain(mod) - val optimized = Context.timed("optimize", source.name) { optimize(source, term, core) } - Some(CoreTransformed(source, tree, mod, optimized)) + case CoreTransformed(source, core, main) => + val optimized = Context.timed("optimize", source.name) { optimize(source, main, core) } + Some(CoreTransformed(source, optimized, main)) } - def optimize(source: Source, mainSymbol: symbols.Symbol, core: ModuleDecl)(using Context): ModuleDecl = + def optimize(source: Source, mainSymbol: Id, core: ModuleDecl)(using Context): ModuleDecl = - val isLLVM = Context.config.backend().name == "llvm" + val isLLVM = false var tree = core diff --git a/effekt/shared/src/main/scala/effekt/core/syntax/Parser.scala b/effekt/shared/src/main/scala/effekt/core/syntax/Parser.scala new file mode 100644 index 000000000..5a2ee25fc --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/syntax/Parser.scala @@ -0,0 +1,5 @@ +package effekt +package core +package syntax + +class Parser \ No newline at end of file diff --git a/effekt/shared/src/main/scala/effekt/core/syntax/Tree.scala b/effekt/shared/src/main/scala/effekt/core/syntax/Tree.scala new file mode 100644 index 000000000..327d50894 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/syntax/Tree.scala @@ -0,0 +1,136 @@ +package effekt +package core +package syntax + +type Id = String + +sealed trait Tree + +case class ModuleDecl( + path: String, + includes: List[String], + declarations: List[Declaration], + externs: List[Extern], + definitions: List[Toplevel], + exports: List[Id] +) extends Tree + +/** + * Toplevel data and interface declarations + */ +enum Declaration extends Tree { + def id: Id + + case Data(id: Id, tparams: List[Id], constructors: List[Constructor]) + case Interface(id: Id, tparams: List[Id], properties: List[Property]) +} +export Declaration.* + +case class Constructor(id: Id, fields: List[Field]) extends Tree +case class Field(id: Id, tpe: ValueType) extends Tree +case class Property(id: Id, tpe: BlockType) extends Tree + +enum FeatureFlag extends Tree { + case NamedFeatureFlag(id: String) + case Default +} + +/** + * FFI external definitions + */ +enum Extern extends Tree { + case Def(id: Id, tparams: List[Id], cparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: ValueType, body: ExternBody) + case Include(featureFlag: FeatureFlag, contents: String) +} +sealed trait ExternBody extends Tree +object ExternBody { + case class StringExternBody(featureFlag: FeatureFlag, contents: Template[Pure]) extends ExternBody + case class Unsupported(err: util.messages.EffektError) extends ExternBody +} + +enum Toplevel { + def id: Id + + case Def(id: Id, block: Block) + case Val(id: Id, tpe: ValueType, binding: core.Stmt) +} + + +/** + * Expressions (with potential IO effects) + * + * - [[DirectApp]] + * - [[Pure]] + */ +sealed trait Expr extends Tree + +// invariant, block b is {io}. +case class DirectApp(b: Block.BlockVar, targs: List[ValueType], vargs: List[Pure], bargs: List[Block]) extends Expr + +enum Pure extends Expr { + + case ValueVar(id: Id) + + case Literal(value: Any) + + case PureApp(b: Block.BlockVar, targs: List[ValueType], vargs: List[Pure]) + + case Make(data: ValueType.Data, tag: Id, targs: List[ValueType], vargs: List[Pure]) + + case Box(b: Block) +} +export Pure.* + +enum Block extends Tree { + case BlockVar(id: Id) + case BlockLit(tparams: List[Id], cparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: ValueType, body: Stmt) + case Unbox(pure: Pure) + case New(impl: Implementation) +} +export Block.* + +case class ValueParam(id: Id, tpe: ValueType) +case class BlockParam(id: Id, tpe: BlockType) + +enum Stmt extends Tree { + case Def(id: Id, block: Block, body: Stmt) + case Let(id: Id, tpe: ValueType, binding: Expr, body: Stmt) + case Return(expr: Pure) + case Val(id: Id, tpe: ValueType, binding: Stmt, body: Stmt) + case App(callee: Block, targs: List[ValueType], vargs: List[Pure], bargs: List[Block]) + case Invoke(callee: Block, method: Id, targs: List[ValueType], vargs: List[Pure], bargs: List[Block]) + case If(cond: Pure, thn: Stmt, els: Stmt) + case Match(scrutinee: Pure, clauses: List[(Id, BlockLit)], default: Option[Stmt]) + case Region(body: BlockLit) + case Alloc(id: Id, init: Pure, region: Id, body: Stmt) + case Var(ref: Id, init: Pure, capture: Id, body: Stmt) + case Get(id: Id, ref: Id, body: Stmt) + case Put(ref: Id, value: Pure, body: Stmt) + case Reset(body: Block.BlockLit) + case Shift(prompt: BlockVar, body: BlockLit) + case Resume(k: BlockVar, body: Stmt) + case Hole() +} +export Stmt.* + + +case class Implementation(interface: BlockType.Interface, operations: List[Operation]) extends Tree + +case class Operation(name: Id, tparams: List[Id], cparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], body: Stmt) + + +/** + * Types + */ +sealed trait Type + +enum ValueType extends Type { + case Var(name: Id) + case Data(name: Id, targs: List[ValueType]) + case Boxed(tpe: BlockType, capt: Captures) +} + +enum BlockType extends Type { + case Function(tparams: List[Id], cparams: List[Id], vparams: List[ValueType], bparams: List[BlockType], result: ValueType) + case Interface(name: Id, targs: List[ValueType]) +} 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 541f5d27b..0aba584e7 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -3,7 +3,6 @@ package core package vm import effekt.core.vm.Computation.Reference -import effekt.source.FeatureFlag import scala.annotation.tailrec diff --git a/effekt/shared/src/main/scala/effekt/cps/Contify.scala b/effekt/shared/src/main/scala/effekt/cps/Contify.scala deleted file mode 100644 index a86c2ab12..000000000 --- a/effekt/shared/src/main/scala/effekt/cps/Contify.scala +++ /dev/null @@ -1,348 +0,0 @@ -package effekt -package cps - -import core.Id -import effekt.cps.Cont.{ ContLam, ContVar } -import substitutions.* - -object Contify { - - def rewrite(module: ModuleDecl): ModuleDecl = module match { - case ModuleDecl(path, includes, declarations, externs, definitions, exports) => - ModuleDecl(path, includes, declarations, externs.map(rewrite), definitions.map(rewrite), exports) - } - - def rewrite(definition: ToplevelDefinition): ToplevelDefinition = definition match { - case ToplevelDefinition.Def(id, block) => ToplevelDefinition.Def(id, rewrite(block)) - case ToplevelDefinition.Val(id, ks, k, binding) => ToplevelDefinition.Val(id, ks, k, rewrite(binding)) - case ToplevelDefinition.Let(id, binding) => ToplevelDefinition.Let(id, rewrite(binding)) - } - - def rewrite(extern: Extern): Extern = extern match { - case Extern.Def(id, vparams, bparams, async, body) => - Extern.Def(id, vparams, bparams, async, rewrite(body)) - case include: Extern.Include => include - } - - def rewrite(body: ExternBody): ExternBody = body match { - case ExternBody.StringExternBody(ff, template) => - ExternBody.StringExternBody(ff, Template(template.strings, template.args.map(rewrite))) - case unsupported: ExternBody.Unsupported => unsupported - } - - def rewrite(expr: Expr): Expr = expr match { - case DirectApp(id, vargs, bargs) => DirectApp(id, vargs.map(rewrite), bargs.map(rewrite)) - case p: Pure => rewrite(p) - } - - def rewrite(pure: Pure): Pure = pure match { - case Pure.ValueVar(id) => Pure.ValueVar(id) - case Pure.Literal(value) => Pure.Literal(value) - case Pure.PureApp(id, vargs) => Pure.PureApp(id, vargs.map(rewrite)) - case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(rewrite)) - case Pure.Box(b) => Pure.Box(rewrite(b)) - } - - def rewrite(block: Block): Block = block match { - case Block.BlockVar(id) => Block.BlockVar(id) - case Block.BlockLit(vparams, bparams, ks, k, body) => - Block.BlockLit(vparams, bparams, ks, k, rewrite(body)) - case Block.Unbox(pure) => Block.Unbox(rewrite(pure)) - case Block.New(impl) => Block.New(rewrite(impl)) - } - - def rewrite(stmt: Stmt): Stmt = stmt match { - - case Stmt.LetDef(id, b @ BlockLit(vparams, Nil, ks, k, body), rest) => - val rewrittenBody = rewrite(body) - val rewrittenRest = rewrite(rest) - val recursiveConts = returnsTo(id, rewrittenBody) - val continuations = returnsTo(id, rewrittenRest) - - def returnsUnique = continuations.size == 1 - def isRecursive = recursiveConts.nonEmpty - - // this is problematic: - // - // function l(k1) { - // ... k1 ... - // } - // function loop(k2) { - // ... l(k2) ... - // } - // - // we cannot simply specialize l to k2 since it is not lexically in scope! - // - // This is not a problem in LLVM since there all blocks are toplevel - // - // We approximate whether k is in scope by checking whether it is free in `rest` - // - // In the future we could perform lambda-dropping to discover more cases - def inScope(k: Id) = Variables.free(rewrittenRest) contains k - - continuations.headOption match { - case Some(Cont.ContVar(k2)) if returnsUnique && !isRecursive && inScope(k2) => - given Substitution = Substitution(conts = Map(k -> ContVar(k2))) - Stmt.LetCont(id, ContLam(vparams, ks, substitute(rewrittenBody)), contify(id, rewrittenRest)) - - // leads to `k_6 is not defined` when disabling optimizations on issue861.effekt - // case Some(cont: ContLam) if returnsUnique && !isRecursive => - // val k2 = Id("k") - // given Substitution = Substitution(conts = Map(k -> ContVar(k2))) - // LetCont(k2, cont, - // Stmt.LetCont(id, ContLam(vparams, ks, substitute(rewrittenBody)), - // contify(id, rewrittenRest))) - case _ => - Stmt.LetDef(id, BlockLit(vparams, Nil, ks, k, rewrittenBody), rewrittenRest) - } - - // Congruences - - case Stmt.LetDef(id, binding, body) => - Stmt.LetDef(id, rewrite(binding), rewrite(body)) - - case Stmt.Jump(k, vargs, ks) => - Stmt.Jump(k, vargs.map(rewrite), ks) - - case Stmt.App(callee, vargs, bargs, ks, k) => - Stmt.App(rewrite(callee), vargs.map(rewrite), bargs.map(rewrite), ks, rewrite(k)) - - case Stmt.Invoke(callee, method, vargs, bargs, ks, k) => - Stmt.Invoke(rewrite(callee), method, vargs.map(rewrite), bargs.map(rewrite), ks, rewrite(k)) - - case Stmt.If(cond, thn, els) => - Stmt.If(rewrite(cond), rewrite(thn), rewrite(els)) - - case Stmt.Match(scrutinee, clauses, default) => - Stmt.Match( - rewrite(scrutinee), - clauses.map { case (id, cl) => (id, rewrite(cl)) }, - default.map(rewrite)) - - case Stmt.LetExpr(id, binding, body) => - Stmt.LetExpr(id, rewrite(binding), rewrite(body)) - - case Stmt.LetCont(id, binding, body) => - Stmt.LetCont(id, rewrite(binding), rewrite(body)) - - case Stmt.Region(id, ks, body) => - Stmt.Region(id, ks, rewrite(body)) - - case Stmt.Alloc(id, init, region, body) => - Stmt.Alloc(id, rewrite(init), region, rewrite(body)) - - case Stmt.Var(id, init, ks, body) => - Stmt.Var(id, rewrite(init), ks, rewrite(body)) - - case Stmt.Dealloc(ref, body) => - Stmt.Dealloc(ref, rewrite(body)) - - case Stmt.Get(ref, id, body) => - Stmt.Get(ref, id, rewrite(body)) - - case Stmt.Put(ref, value, body) => - Stmt.Put(ref, rewrite(value), rewrite(body)) - - case Stmt.Reset(prog, ks, k) => - Stmt.Reset(rewrite(prog), ks, rewrite(k)) - - case Stmt.Shift(prompt, body, ks, k) => - Stmt.Shift(prompt, rewrite(body), ks, rewrite(k)) - - case Stmt.Resume(r, body, ks, k) => - Stmt.Resume(r, rewrite(body), ks, rewrite(k)) - - case Stmt.Hole() => Stmt.Hole() - } - - def rewrite(clause: Clause): Clause = clause match { - case Clause(vparams, body) => Clause(vparams, rewrite(body)) - } - - def rewrite(k: Cont): Cont = k match { - case Cont.ContVar(id) => Cont.ContVar(id) - case Cont.ContLam(results, ks, body) => Cont.ContLam(results, ks, rewrite(body)) - case Cont.Abort => Cont.Abort - } - - def rewrite(impl: Implementation): Implementation = impl match { - case Implementation(interface, operations) => - Implementation(interface, operations.map(rewrite)) - } - - def rewrite(op: Operation): Operation = op match { - case Operation(name, vparams, bparams, ks, k, body) => - Operation(name, vparams, bparams, ks, k, rewrite(body)) - } - def rewrite(b: BlockLit): BlockLit = b match { - case BlockLit(vparams, bparams, ks, k, body) => - BlockLit(vparams, bparams, ks, k, rewrite(body)) - } - - def rewrite(c: Cont.ContLam): Cont.ContLam = c match { - case Cont.ContLam(results, ks, body) => - Cont.ContLam(results, ks, rewrite(body)) - } - - def all[T](t: IterableOnce[T], f: T => Set[Cont]): Set[Cont] = - t.iterator.foldLeft(Set.empty[Cont]) { case (cs, t) => f(t) ++ cs } - - def returnsTo(id: Id, s: Stmt): Set[Cont] = s match { - case Stmt.App(callee, vargs, bargs, ks, k) => - val self = callee match { - case Block.BlockVar(id2) if id == id2 => Set(k) - case _ => Set.empty - } - self ++ returnsTo(id, callee) ++ all(vargs, returnsTo(id, _)) ++ all(bargs, returnsTo(id, _)) ++ returnsTo(id, k) - case Stmt.Invoke(callee, _, vargs, bargs, ks, k) => - returnsTo(id, callee) ++ all(vargs, returnsTo(id, _)) ++ all(bargs, returnsTo(id, _)) ++ returnsTo(id, k) - case Stmt.If(cond, thn, els) => - returnsTo(id, cond) ++ returnsTo(id, thn) ++ returnsTo(id, els) - case Stmt.Match(scrutinee, clauses, default) => - returnsTo(id, scrutinee) ++ all(clauses, { case (_, cl) => returnsTo(id, cl.body) }) ++ all(default, returnsTo(id, _)) - case Stmt.LetDef(_, binding, body) => - returnsTo(id, binding) ++ returnsTo(id, body) - case Stmt.LetExpr(_, binding, body) => - returnsTo(id, binding) ++ returnsTo(id, body) - case Stmt.LetCont(_, binding, body) => - returnsTo(id, binding) ++ returnsTo(id, body) - case Stmt.Region(_, _, body) => returnsTo(id, body) - case Stmt.Alloc(_, init, _, body) => - returnsTo(id, init) ++ returnsTo(id, body) - case Stmt.Var(_, init, _, body) => - returnsTo(id, init) ++ returnsTo(id, body) - case Stmt.Dealloc(_, body) => returnsTo(id, body) - case Stmt.Get(_, _, body) => returnsTo(id, body) - case Stmt.Put(_, value, body) => - returnsTo(id, value) ++ returnsTo(id, body) - case Stmt.Reset(prog, _, k) => - returnsTo(id, prog) ++ returnsTo(id, k) - case Stmt.Shift(_, body, _, k) => - returnsTo(id, body) ++ returnsTo(id, k) - case Stmt.Resume(_, body, _, k) => - returnsTo(id, body) ++ returnsTo(id, k) - case Stmt.Jump(_, vargs, _) => - all(vargs, returnsTo(id, _)) - case Stmt.Hole() => Set.empty - } - - def returnsTo(id: Id, b: Block): Set[Cont] = b match { - case Block.BlockVar(_) => Set.empty - case b: Block.BlockLit => returnsTo(id, b.body) - case Block.Unbox(p) => returnsTo(id, p) - case Block.New(impl) => all(impl.operations, op => returnsTo(id, op.body)) - } - - def returnsTo(id: Id, e: Expr): Set[Cont] = e match { - case DirectApp(_, vargs, bargs) => - all(vargs, returnsTo(id, _)) ++ all(bargs, returnsTo(id, _)) - case Pure.ValueVar(_) => Set.empty - case Pure.Literal(_) => Set.empty - case Pure.PureApp(_, vargs) => all(vargs, returnsTo(id, _)) - case Pure.Make(_, _, vargs) => all(vargs, returnsTo(id, _)) - case Pure.Box(b) => returnsTo(id, b) - } - - def returnsTo(id: Id, k: Cont): Set[Cont] = k match { - case Cont.ContVar(_) => Set.empty - case Cont.ContLam(results, ks, body) => returnsTo(id, body) - case Cont.Abort => Set.empty - } - - def contify(id: Id, s: Stmt): Stmt = s match { - case Stmt.App(Block.BlockVar(callee), vargs, Nil, ks, k) if callee == id => - Stmt.Jump(id, vargs, ks) - - // Congruences - - case Stmt.App(callee, vargs, bargs, ks, k) => - Stmt.App(contify(id, callee), vargs.map(contify(id, _)), bargs.map(contify(id, _)), ks, contify(id, k)) - - case Stmt.Invoke(callee, method, vargs, bargs, ks, k) => - Stmt.Invoke(contify(id, callee), method, vargs.map(contify(id, _)), bargs.map(contify(id, _)), ks, contify(id, k)) - - case Stmt.If(cond, thn, els) => - Stmt.If(contify(id, cond), contify(id, thn), contify(id, els)) - - case Stmt.Match(scrutinee, clauses, default) => - Stmt.Match( - contify(id, scrutinee), - clauses.map { case (tag, Clause(vparams, body)) => (tag, Clause(vparams, contify(id, body))) }, - default.map(contify(id, _))) - - case Stmt.LetDef(id2, binding, body) => - Stmt.LetDef(id2, contify(id, binding), contify(id, body)) - - case Stmt.LetExpr(id2, binding, body) => - Stmt.LetExpr(id2, contify(id, binding), contify(id, body)) - - case Stmt.LetCont(id2, binding, body) => - Stmt.LetCont(id2, contify(id, binding), contify(id, body)) - - case Stmt.Region(id2, ks, body) => - Stmt.Region(id2, ks, contify(id, body)) - - case Stmt.Alloc(id2, init, region, body) => - Stmt.Alloc(id2, contify(id, init), region, contify(id, body)) - - case Stmt.Var(id2, init, ks, body) => - Stmt.Var(id2, contify(id, init), ks, contify(id, body)) - - case Stmt.Dealloc(ref, body) => - Stmt.Dealloc(ref, contify(id, body)) - - case Stmt.Get(ref, id2, body) => - Stmt.Get(ref, id2, contify(id, body)) - - case Stmt.Put(ref, value, body) => - Stmt.Put(ref, contify(id, value), contify(id, body)) - - case Stmt.Reset(prog, ks, k) => - Stmt.Reset(contify(id, prog), ks, contify(id, k)) - - case Stmt.Shift(prompt, body, ks, k) => - Stmt.Shift(prompt, contify(id, body), ks, contify(id, k)) - - case Stmt.Resume(r, body, ks, k) => - Stmt.Resume(r, contify(id, body), ks, contify(id, k)) - - case other => other - } - - def contify(id: Id, b: Block): Block = b match { - case b: Block.BlockVar => b - case b: Block.BlockLit => Block.BlockLit(b.vparams, b.bparams, b.ks, b.k, contify(id, b.body)) - case Block.Unbox(p) => Block.Unbox(contify(id, p)) - case Block.New(impl) => Block.New(contify(id, impl)) - } - - def contify(id: Id, e: Expr): Expr = e match { - case DirectApp(id2, vargs, bargs) => - DirectApp(id2, vargs.map(contify(id, _)), bargs.map(contify(id, _))) - case p: Pure => contify(id, p) - } - - def contify(id: Id, p: Pure): Pure = p match { - case Pure.ValueVar(_) => p - case Pure.Literal(_) => p - case Pure.PureApp(id2, vargs) => Pure.PureApp(id2, vargs.map(contify(id, _))) - case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(contify(id, _))) - case Pure.Box(b) => Pure.Box(contify(id, b)) - } - - def contify(id: Id, k: Cont): Cont = k match { - case Cont.ContVar(_) => k - case Cont.ContLam(results, ks, body) => Cont.ContLam(results, ks, contify(id, body)) - case Cont.Abort => k - } - - def contify(id: Id, impl: Implementation): Implementation = - Implementation(impl.interface, impl.operations.map(op => - Operation(op.name, op.vparams, op.bparams, op.ks, op.k, contify(id, op.body)))) - - def contify(id: Id, b: BlockLit): BlockLit = - BlockLit(b.vparams, b.bparams, b.ks, b.k, contify(id, b.body)) - - def contify(id: Id, c: Cont.ContLam): Cont.ContLam = - Cont.ContLam(c.results, c.ks, contify(id, c.body)) -} diff --git a/effekt/shared/src/main/scala/effekt/cps/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/cps/PrettyPrinter.scala deleted file mode 100644 index 6d33f21f6..000000000 --- a/effekt/shared/src/main/scala/effekt/cps/PrettyPrinter.scala +++ /dev/null @@ -1,206 +0,0 @@ -package effekt -package cps - -import core.Id - -import kiama.output.ParenPrettyPrinter -import effekt.source.FeatureFlag - -object PrettyPrinter extends ParenPrettyPrinter { - - import kiama.output.PrettyPrinterTypes.Document - - override val defaultIndent = 2 - - def format(t: ModuleDecl): Document = - pretty(toDoc(t), 4) - - def format(defs: List[ToplevelDefinition]): String = - pretty(toDoc(defs), 60).layout - - def format(s: Stmt): String = - pretty(toDoc(s), 60).layout - - def format(t: Block): String = - pretty(toDoc(t), 60).layout - - def format(e: Expr): String = - pretty(toDoc(e), 60).layout - - val show: PartialFunction[Any, String] = { - case m: ModuleDecl => format(m).layout - case d: ToplevelDefinition => format(List(d)) - case s: Stmt => format(s) - case b: Block => format(b) - case e: Expr => format(e) - case x: Id => x.show - } - - val emptyline: Doc = line <> line - - def toDoc(m: ModuleDecl): Doc = { - "module" <+> m.path <> emptyline <> - toDoc(m.definitions) - } - - def toDoc(definitions: List[ToplevelDefinition]): Doc = - vsep(definitions map toDoc, semi) - - def toDoc(d: ToplevelDefinition): Doc = d match { - case ToplevelDefinition.Def(id, block) => - "def" <+> toDoc(id) <+> "=" <+> toDoc(block) - case ToplevelDefinition.Val(id, ks, k, binding) => - "let" <+> toDoc(id) <+> "|" <+> toDoc(ks) <> "," <+> toDoc(k) <+> "=" <+> toDoc(binding) - case ToplevelDefinition.Let(id, binding) => - "let" <+> toDoc(id) <+> "=" <+> toDoc(binding) - } - - def toDoc(e: Expr): Doc = e match { - case DirectApp(id, vargs, bargs) => - toDoc(id) <> argsToDoc(vargs, bargs) - case Pure.ValueVar(id) => toDoc(id) - case Pure.Literal(()) => "()" - case Pure.Literal(s: String) => "\"" + s + "\"" - case Pure.Literal(value) => value.toString - case Pure.PureApp(id, vargs) => toDoc(id) <> argsToDoc(vargs, Nil) - case Pure.Make(data, tag, vargs) => "make" <+> toDoc(data.name) <+> toDoc(tag) <> argsToDoc(vargs, Nil) - case Pure.Box(b) => parens("box" <+> toDoc(b)) - } - - def toDoc(b: Block): Doc = b match { - case Block.BlockVar(id) => toDoc(id) - case b: Block.BlockLit => toDoc(b) - case Block.Unbox(e) => parens("unbox" <+> toDoc(e)) - case Block.New(handler) => "new" <+> toDoc(handler) - } - - def toDoc(b: Block.BlockLit): Doc = b match { - case Block.BlockLit(vps, bps, ks, k, body) => - val params = if (vps.isEmpty && bps.isEmpty) emptyDoc else - parens(hsep(vps.map(toDoc), comma)) <+> hsep(bps.map(toDoc)) - braces { - space <> params <+> "|" <+> toDoc(ks) <> "," <+> toDoc(k) <+> "=>" <+> - nest(line <> toDoc(body)) <> line - } - } - - def toDoc(s: Stmt): Doc = s match { - case Stmt.Jump(k, vargs, ks) => - "jump" <+> toDoc(k) <> argsToDoc(vargs, Nil) <+> "@" <+> toDoc(ks) - - case Stmt.App(callee, vargs, bargs, ks, k) => - toDoc(callee) <> argsToDoc(vargs, bargs) <+> "@" <+> toDoc(ks) <> "," <+> toDoc(k) - - case Stmt.Invoke(callee, method, vargs, bargs, ks, k) => - toDoc(callee) <> "." <> method.name.toString <> argsToDoc(vargs, bargs) <+> "@" <+> toDoc(ks) <> "," <+> toDoc(k) - - case Stmt.If(cond, thn, els) => - "if" <+> parens(toDoc(cond)) <+> block(toDoc(thn)) <+> "else" <+> block(toDoc(els)) - - case Stmt.Match(scrutinee, clauses, default) => - val cs = braces(nest(line <> vsep(clauses map { case (p, b) => - "case" <+> toDoc(p) <+> toDoc(b) - })) <> line) - val d = default.map { body => - space <> "else" <+> block(toDoc(body)) - }.getOrElse(emptyDoc) - toDoc(scrutinee) <+> "match" <+> cs <> d - - case Stmt.LetDef(id, binding, body) => - "def" <+> toDoc(id) <+> "=" <+> toDoc(binding) <> line <> - toDoc(body) - - case Stmt.LetExpr(id, binding, body) => - "let" <+> toDoc(id) <+> "=" <+> toDoc(binding) <> line <> - toDoc(body) - - case Stmt.LetCont(id, binding, body) => - "let" <+> toDoc(id) <+> "=" <+> toDoc(binding) <> line <> - toDoc(body) - - case Stmt.Region(id, ks, body) => - "region" <+> toDoc(id) <+> "@" <+> toDoc(ks) <+> block(toDoc(body)) - - case Stmt.Alloc(id, init, region, body) => - "var" <+> toDoc(id) <+> "in" <+> toDoc(region) <+> "=" <+> toDoc(init) <> ";" <> line <> - toDoc(body) - - case Stmt.Dealloc(ref, body) => - "dealloc" <> parens(toDoc(ref)) <> ";" <> line <> - toDoc(body) - - case Stmt.Var(id, init, ks, body) => - "var" <+> toDoc(id) <+> "=" <+> toDoc(init) <+> "@" <+> toDoc(ks) <> ";" <> line <> - toDoc(body) - - case Stmt.Get(ref, id, body) => - "let" <+> toDoc(id) <+> "=" <+> "!" <> toDoc(ref) <> ";" <> line <> - toDoc(body) - - case Stmt.Put(ref, value, body) => - toDoc(ref) <+> ":=" <+> toDoc(value) <> ";" <> line <> - toDoc(body) - - case Stmt.Reset(prog, ks, k) => - "reset" <+> toDoc(prog) <+> "@" <+> toDoc(ks) <> "," <+> toDoc(k) - - case Stmt.Shift(prompt, body, ks, k) => - "shift" <> parens(toDoc(prompt)) <+> toDoc(body) <+> "@" <+> toDoc(ks) <> "," <+> toDoc(k) - - case Stmt.Resume(r, body, ks, k) => - "resume" <> parens(toDoc(r)) <+> toDoc(body) <+> "@" <+> toDoc(ks) <> "," <+> toDoc(k) - - case Stmt.Hole() => - "<>" - } - - def toDoc(clause: Clause): Doc = clause match { - case Clause(vparams, body) => - parens(hsep(vparams.map(toDoc), comma)) <+> "=>" <+> block(toDoc(body)) - } - - def toDoc(impl: Implementation): Doc = { - val handlerName = toDoc(impl.interface.name) - val clauses = impl.operations.map { op => - "def" <+> toDoc(op.name) <> paramsToDoc(op.vparams, op.bparams, op.ks, op.k) <+> "=" <+> - nested(toDoc(op.body)) - } - handlerName <+> block(vsep(clauses)) - } - - def toDoc(k: Cont): Doc = k match { - case Cont.ContVar(id) => toDoc(id) - case Cont.ContLam(results, ks, body) => - braces { - space <> parens(hsep(results.map(toDoc), comma)) <+> "|" <+> toDoc(ks) <+> "=>" <+> - nest(line <> toDoc(body)) <> line - } - case Cont.Abort => "abort" - } - - def toDoc(ks: MetaCont): Doc = toDoc(ks.id) - - def toDoc(s: symbols.Symbol): Doc = s.show - - def argsToDoc(vargs: List[Pure], bargs: List[Block]): Doc = { - val vargsDoc = if (vargs.isEmpty && !bargs.isEmpty) emptyDoc else parens(vargs.map(toDoc)) - val bargsDoc = if (bargs.isEmpty) emptyDoc else hcat(bargs.map { b => braces(toDoc(b)) }) - vargsDoc <> bargsDoc - } - - def paramsToDoc(vps: List[Id], bps: List[Id], ks: Id, k: Id): Doc = { - val vpsDoc = if (vps.isEmpty && !bps.isEmpty) emptyDoc else parens(vps.map(toDoc)) - val bpsDoc = if (bps.isEmpty) emptyDoc else hcat(bps.map(toDoc)) - vpsDoc <> bpsDoc <+> "|" <+> toDoc(ks) <> "," <+> toDoc(k) - } - - def nested(content: Doc): Doc = group(nest(line <> content)) - - def parens(docs: List[Doc]): Doc = parens(hsep(docs, comma)) - - def braces(docs: List[Doc]): Doc = braces(hsep(docs, semi)) - - def block(content: Doc): Doc = braces(nest(line <> content) <> line) - - def block(docs: List[Doc]): Doc = block(vsep(docs, line)) -} diff --git a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala deleted file mode 100644 index b43c41633..000000000 --- a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala +++ /dev/null @@ -1,238 +0,0 @@ -package effekt -package cps - -import core.Id -import symbols.builtins.TState - -case class TransformationContext(values: Map[Id, Pure], blocks: Map[Id, Block]) { - def lookupValue(id: Id): Pure = values.getOrElse(id, ValueVar(id)) - def lookupBlock(id: Id): Block = blocks.getOrElse(id, BlockVar(id)) - def bind(id: Id, value: Pure): TransformationContext = copy(values = values + (id -> value)) - def bind(id: Id, block: Block): TransformationContext = copy(blocks = blocks + (id -> block)) -} - -def binding[R](id: Id, value: Pure)(body: TransformationContext ?=> R)(using C: TransformationContext): R = - body(using C.bind(id, value)) - -def binding[R](id: Id, block: Block)(body: TransformationContext ?=> R)(using C: TransformationContext): R = - body(using C.bind(id, block)) - - -object Transformer { - - def transform(module: core.ModuleDecl): ModuleDecl = module match { - case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) => - given TransformationContext(Map.empty, Map.empty) - ModuleDecl(path, includes, declarations, externs.map(transform), definitions.map(transformToplevel), exports) - } - - def transformToplevel(definition: core.Toplevel)(using TransformationContext): ToplevelDefinition = definition match { - case core.Toplevel.Def(id, block) => ToplevelDefinition.Def(id, transform(block)) - case core.Toplevel.Val(id, tpe, stmt) => - val ks = Id("ks") - val k = Id("k") - ToplevelDefinition.Val(id, ks, k, transform(stmt, ks, Continuation.Dynamic(k))) - } - - def transform(extern: core.Extern)(using TransformationContext): Extern = extern match { - case core.Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => - Extern.Def(id, vparams.map(_.id), bparams.map(_.id), annotatedCapture.contains(symbols.builtins.AsyncCapability.capture), transform(body)) - case core.Extern.Include(featureFlag, contents) => Extern.Include(featureFlag, contents) - } - - def transform(externBody: core.ExternBody)(using TransformationContext): ExternBody = externBody match { - case core.ExternBody.StringExternBody(featureFlag, Template(strings, args)) => - ExternBody.StringExternBody(featureFlag, Template(strings, args.map(transform))) - case core.ExternBody.Unsupported(err) => ExternBody.Unsupported(err) - } - - def transform(stmt: core.Stmt, ks: Id, k: Continuation)(using C: TransformationContext): Stmt = stmt match { - - // dealiasing - case core.Stmt.Def(id, core.BlockVar(x, _, _), body) => - binding(id, C.lookupBlock(x)) { transform(body, ks, k) } - - case core.Stmt.Def(id, block, body) => - LetDef(id, transform(block), transform(body, ks, k)) - - // dealiasing - case core.Stmt.Let(id, tpe, core.Pure.ValueVar(x, _), body) => - binding(id, C.lookupValue(x)) { transform(body, ks, k) } - - case core.Stmt.Let(id, tpe, core.DirectApp(b, targs, vargs, bargs), body) => - transform(b) match { - case Block.BlockVar(f) => - LetExpr(id, DirectApp(f, vargs.map(transform), bargs.map(transform)), - transform(body, ks, k)) - case _ => sys error "Should not happen" - } - - case core.Stmt.Let(id, tpe, pure: core.Pure, body) => - LetExpr(id, transform(pure), transform(body, ks, k)) - - case core.Stmt.Return(value) => - k(transform(value), ks) - - case core.Stmt.Val(id, annotatedTpe, rhs, body) => - transform(rhs, ks, Continuation.Static(id) { (value, ks) => - binding(id, value) { transform(body, ks, k) } - }) - - case core.Stmt.App(callee, targs, vargs, bargs) => - App(transform(callee), vargs.map(transform), bargs.map(transform), MetaCont(ks), k.reifyAt(stmt.tpe)) - - case core.Stmt.Invoke(callee, method, tpe, targs, vargs, bargs) => - Invoke(transform(callee), method, vargs.map(transform), bargs.map(transform), MetaCont(ks), k.reifyAt(stmt.tpe)) - - case core.Stmt.If(cond, thn, els) => - withJoinpoint(k) { k2 => - If(transform(cond), transform(thn, ks, k2), transform(els, ks, k2)) - } - - case core.Stmt.Match(scrutinee, List((id, rhs)), None) => - Match( - transform(scrutinee), - List((id, transformClause(rhs, ks, k))), None) - - case core.Stmt.Match(scrutinee, clauses, default) => - withJoinpoint(k) { k => - Match( - transform(scrutinee), - clauses.map { case (id, rhs) => (id, transformClause(rhs, ks, k)) }, - default.map(transform(_, ks, k))) - } - - case core.Stmt.Reset(core.Block.BlockLit(_, _, _, prompt :: Nil, body)) => - val ks2 = Id("ks") - val k2 = Id("k") - Reset(Block.BlockLit(Nil, List(prompt.id), ks2, k2, transform(body, ks2, Continuation.Dynamic(k2))), - MetaCont(ks), k.reify) - - case core.Stmt.Reset(body) => sys error "Shouldn't happen" - - // Only unidirectional, yet - // core.Block.BlockLit(tparams, cparams, vparams, List(resume), body) - case core.Stmt.Shift(prompt, core.Block.BlockLit(tparams, cparams, vparams, List(resume), body)) => - val ks2 = Id("ks") - val k2 = Id("k") - - val translatedBody: BlockLit = BlockLit(vparams.map { p => p.id }, List(resume.id), ks2, k2, - transform(body, ks2, Continuation.Dynamic(k2))) - - Shift(prompt.id, translatedBody, MetaCont(ks), k.reifyAt(stmt.tpe)) - - case core.Stmt.Shift(prompt, body) => sys error "Shouldn't happen" - - case core.Stmt.Resume(cont, body) => - val ks2 = Id("ks") - val k2 = Id("k") - Resume(cont.id, Block.BlockLit(Nil, Nil, ks2, k2, transform(body, ks2, Continuation.Dynamic(k2))), - MetaCont(ks), k.reifyAt(stmt.tpe)) - - case core.Stmt.Hole() => Hole() - - case core.Stmt.Region(core.Block.BlockLit(_, _, _, List(region), body)) => - cps.Region(region.id, MetaCont(ks), - transform(body, ks, - Continuation.Static(Id("tmp")) { (x, ks) => - Dealloc(region.id, k(x, ks)) - })) - - case core.Stmt.Region(_) => sys error "Shouldn't happen" - - case core.Stmt.Alloc(id, init, region, body) => - cps.Alloc(id, transform(init), region, transform(body, ks, k)) - - case core.Stmt.Var(id, init, capture, body) => - cps.Var(id, transform(init), MetaCont(ks), - transform(body, ks, - Continuation.Static(Id("tmp")) { (x, ks) => - Dealloc(id, k(x, ks)) - })) - - case core.Stmt.Get(id, tpe, ref, capt, body) => - cps.Get(ref, id, transform(body, ks, k)) - - case core.Stmt.Put(ref, capt, value, body) => - cps.Put(ref, transform(value), transform(body, ks, k)) - } - - def transformClause(clause: core.Block.BlockLit, ks: Id, k: Continuation)(using C: TransformationContext): Clause = - clause match { - case core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => - Clause(vparams.map(_.id), transform(body, ks, k)) - } - - def transform(impl: core.Implementation)(using C: TransformationContext): Implementation = - Implementation(impl.interface, impl.operations.map(transform)) - - def transform(op: core.Operation)(using C: TransformationContext): Operation = - op match { - case core.Operation(name, tparams, cparams, vparams, bparams, body) => - val ks = Id("ks") - val k = Id("k") - Operation(name, vparams.map(_.id), bparams.map(_.id), ks, k, - transform(body, ks, Continuation.Dynamic(k))) - } - - def transform(pure: core.Pure)(using C: TransformationContext): Pure = pure match { - case core.Pure.ValueVar(id, annotatedType) => C.lookupValue(id) - case core.Pure.Literal(value, annotatedType) => Literal(value) - case core.Pure.PureApp(b, targs, vargs) => transform(b) match { - case Block.BlockVar(id) => PureApp(id, vargs.map(transform)) - case _ => sys error "Should not happen" - } - case core.Pure.Make(data, tag, targs, vargs) => Make(data, tag, vargs.map(transform)) - case core.Pure.Box(b, annotatedCapture) => Box(transform(b)) - } - - def transformBlockLit(b: core.BlockLit)(using TransformationContext): BlockLit = b match { - case core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => - val ks = Id("ks") - val k = Id("k") - BlockLit(vparams.map { p => p.id }, bparams.map { p => p.id }, ks, k, transform(body, ks, Continuation.Dynamic(k))) - } - - def transform(block: core.Block)(using C: TransformationContext): Block = block match { - case core.Block.BlockVar(id, annotatedTpe, annotatedCapt) => C.lookupBlock(id) - case b @ core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => transformBlockLit(b) - case core.Block.Unbox(pure) => Unbox(transform(pure)) - case core.Block.New(impl) => New(transform(impl)) - } -} - -def withJoinpoint(k: Continuation)(body: Continuation => Stmt): Stmt = k match { - case k : Continuation.Dynamic => body(k) - case k : Continuation.Static => - val name = core.Id("k") - val reified = k.reify.asInstanceOf[Cont.ContLam] - - // do not create a joinpoint for continuations roughly of the size of a return cont... - if reified.body.size <= 5 then - body(k) - else - LetCont(name, reified.asInstanceOf, body(Continuation.Dynamic(name))) -} - -enum Continuation { - case Dynamic(id: Id) // in ml this is an arbitrary term, why? - case Static(hint: Id, k: (Pure, Id) => Stmt) - - def apply(arg: Pure, ks: Id): Stmt = this match { - case Continuation.Dynamic(id) => Jump(id, arg :: Nil, MetaCont(ks)) - case Continuation.Static(hint, k) => k(arg, ks) - } - def reify: Cont = - this match { - case c : Continuation.Dynamic => cps.Cont.ContVar(c.id) - case Continuation.Static(hint, k) => - val ks = Id("ks") - cps.Cont.ContLam(hint :: Nil, ks, k(Pure.ValueVar(hint), ks)) - } - - def reifyAt(tpe: core.ValueType): Cont = - if (tpe == core.Type.TBottom) Cont.Abort else reify -} -object Continuation { - def Static(hint: Id)(k: (Pure, Id) => Stmt): Continuation.Static = Continuation.Static(hint, k) -} diff --git a/effekt/shared/src/main/scala/effekt/cps/Tree.scala b/effekt/shared/src/main/scala/effekt/cps/Tree.scala deleted file mode 100644 index ddff92035..000000000 --- a/effekt/shared/src/main/scala/effekt/cps/Tree.scala +++ /dev/null @@ -1,412 +0,0 @@ -package effekt -package cps - -import core.{ Id, ValueType, BlockType, Captures } -import effekt.source.FeatureFlag -import effekt.util.messages.ErrorReporter -import effekt.util.messages.INTERNAL_ERROR - -sealed trait Tree extends Product { - /** - * The number of nodes of this tree (used by inlining heuristics) - */ - lazy val size: Int = { - var nodeCount = 1 - - def all(t: IterableOnce[_]): Unit = t.iterator.foreach(one) - def one(obj: Any): Unit = obj match { - case t: Tree => nodeCount += t.size - case s: effekt.symbols.Symbol => () - case p: Product => all(p.productIterator) - case t: Iterable[t] => all(t) - case leaf => () - } - this.productIterator.foreach(one) - nodeCount - } -} - -/** - * A module declaration, the path should be an Effekt include path, not a system dependent file path - */ -case class ModuleDecl( - path: String, - includes: List[String], - declarations: List[core.Declaration], - externs: List[Extern], - definitions: List[ToplevelDefinition], - exports: List[Id] -) extends Tree - -enum ToplevelDefinition { - case Def(id: Id, block: Block) - case Val(id: Id, ks: Id, k: Id, binding: Stmt) // this is a let-run - case Let(id: Id, binding: Pure) -} - -/** - * FFI external definitions - */ -enum Extern extends Tree { - case Def(id: Id, vparams: List[Id], bparams: List[Id], async: Boolean, body: ExternBody) - case Include(featureFlag: FeatureFlag, contents: String) -} -sealed trait ExternBody extends Tree -object ExternBody { - case class StringExternBody(featureFlag: FeatureFlag, contents: Template[Pure]) extends ExternBody - case class Unsupported(err: util.messages.EffektError) extends ExternBody { - def report(using E: ErrorReporter): Unit = E.report(err) - } -} - -case class Def(id: Id, block: Block) extends Tree - -sealed trait Expr extends Tree - -/** - * Impure FFI calls. - */ -case class DirectApp(id: Id, vargs: List[Pure], bargs: List[Block]) extends Expr - -enum Pure extends Expr { - - case ValueVar(id: Id) - - case Literal(value: Any) - - /** - * Pure FFI calls. Invariant, block b is pure. - */ - case PureApp(id: Id, vargs: List[Pure]) - - case Make(data: ValueType.Data, tag: Id, vargs: List[Pure]) - - case Box(b: Block) -} -export Pure.* - - -enum Block extends Tree { - case BlockVar(id: Id) - case BlockLit(vparams: List[Id], bparams: List[Id], ks: Id, k: Id, body: Stmt) - case Unbox(pure: Pure) - case New(impl: Implementation) -} -export Block.* - -enum Stmt extends Tree { - case Jump(k: Id, vargs: List[Pure], ks: MetaCont) - case App(callee: Block, vargs: List[Pure], bargs: List[Block], ks: MetaCont, k: Cont) - case Invoke(callee: Block, method: Id, vargs: List[Pure], bargs: List[Block], ks: MetaCont, k: Cont) - - // Local Control Flow - case If(cond: Pure, thn: Stmt, els: Stmt) - case Match(scrutinee: Pure, clauses: List[(Id, Clause)], default: Option[Stmt]) - - case LetDef(id: Id, binding: Block, body: Stmt) - case LetExpr(id: Id, binding: Expr, body: Stmt) - case LetCont(id: Id, binding: Cont.ContLam, body: Stmt) - - // Regions - case Region(id: Id, ks: MetaCont, body: Stmt) - case Alloc(id: Id, init: Pure, region: Id, body: Stmt) - - // creates a fresh state handler to model local (backtrackable) state. - // [[capture]] is a binding occurence. - // val id = ks.fresh(init); body - case Var(id: Id, init: Pure, ks: MetaCont, body: Stmt) - // dealloc(ref); body - case Dealloc(ref: Id, body: Stmt) - - // val id = !ref; body - case Get(ref: Id, id: Id, body: Stmt) - - case Put(ref: Id, value: Pure, body: Stmt) - - // reset( { (p, ks, k) => STMT }, ks, k) - case Reset(prog: BlockLit, ks: MetaCont, k: Cont) - - // shift(p, { (resume, ks, k) => STMT }, ks, k) - case Shift(prompt: Id, body: BlockLit, ks: MetaCont, k: Cont) - - // resume(r, (ks, k) => STMT, ks, k) - case Resume(resumption: Id, body: BlockLit, ks: MetaCont, k: Cont) - - // Others - case Hole() -} -export Stmt.* - -case class Clause(vparams: List[Id], body: Stmt) extends Tree - -enum Cont extends Tree { - case ContVar(id: Id) - case ContLam(results: List[Id], ks: Id, body: Stmt) - case Abort -} - -case class MetaCont(id: Id) extends Tree - -case class Implementation(interface: BlockType.Interface, operations: List[Operation]) extends Tree - -case class Operation(name: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, body: Stmt) extends Tree - - -// unless we need some more information, we keep it simple for now: -type Variable = Id -type Variables = Set[Id] - -object Variables { - - def value(id: Id) = Set(id) - def block(id: Id) = Set(id) - def cont(id: Id) = Set(id) - def meta(id: Id) = Set(id) - - def empty: Variables = Set.empty - - def all[T](t: IterableOnce[T], f: T => Variables): Variables = - t.iterator.foldLeft(Variables.empty) { case (xs, t) => f(t) ++ xs } - - - def free(e: Expr): Variables = e match { - case DirectApp(id, vargs, bargs) => block(id) ++ all(vargs, free) ++ all(bargs, free) - case Pure.ValueVar(id) => value(id) - case Pure.Literal(value) => empty - case Pure.PureApp(id, vargs) => block(id) ++ all(vargs, free) - case Pure.Make(data, tag, vargs) => all(vargs, free) - case Pure.Box(b) => free(b) - } - - def free(b: Block): Variables = b match { - case Block.BlockVar(id) => block(id) - case Block.BlockLit(vparams, bparams, ks, k, body) => free(body) -- vparams -- bparams - ks - k - case Block.Unbox(pure) => free(pure) - case Block.New(impl) => free(impl) - } - - def free(impl: Implementation): Variables = all(impl.operations, free) - - def free(op: Operation): Variables = op match { - case Operation(name, vparams, bparams, ks, k, body) => - free(body) -- all(vparams, value) -- all(bparams, block) -- meta(ks) -- cont(k) - } - - def free(s: Stmt): Variables = s match { - case Stmt.Jump(k, vargs, ks) => cont(k) ++ all(vargs, free) ++ free(ks) - case Stmt.App(callee, vargs, bargs, ks, k) => free(callee) ++ all(vargs, free) ++ all(bargs, free) ++ free(ks) ++ free(k) - case Stmt.Invoke(callee, method, vargs, bargs, ks, k) => free(callee) ++ all(vargs, free) ++ all(bargs, free) ++ free(ks) ++ free(k) - case Stmt.If(cond, thn, els) => free(cond) ++ free(thn) ++ free(els) - case Stmt.Match(scrutinee, clauses, default) => free(scrutinee) ++ all(clauses, free) ++ all(default, free) - case Stmt.LetDef(id, binding, body) => (free(binding) ++ free(body)) -- block(id) - case Stmt.LetExpr(id, binding, body) => free(binding) ++ (free(body) -- value(id)) - case Stmt.LetCont(id, binding, body) => free(binding) ++ (free(body) -- cont(id)) - - case Stmt.Region(id, ks, body) => free(ks) ++ (free(body) -- block(id)) - case Stmt.Alloc(id, init, region, body) => free(init) ++ block(region) ++ (free(body) -- block(id)) - - case Stmt.Var(id, init, ks, body) => free(init) ++ free(ks) ++ (free(body) -- block(id)) - case Stmt.Dealloc(ref, body) => block(ref) ++ free(body) - case Stmt.Get(ref, id, body) => block(ref) ++ (free(body) -- value(id)) - case Stmt.Put(ref, value, body) => block(ref) ++ free(value) ++ free(body) - - case Stmt.Reset(prog, ks, k) => free(prog) ++ free(ks) ++ free(k) - case Stmt.Shift(prompt, body, ks, k) => block(prompt) ++ free(body) ++ free(ks) ++ free(k) - case Stmt.Resume(r, body, ks, k) => block(r) ++ free(body) ++ free(ks) ++ free(k) - case Stmt.Hole() => empty - } - - def free(cl: (Id, Clause)): Variables = cl match { - case (_, Clause(vparams, body)) => free(body) -- all(vparams, value) - } - - def free(d: Def): Variables = d match { - case Def(id, binding) => free(binding) -- block(id) - } - - def free(ks: MetaCont): Variables = meta(ks.id) - def free(k: Cont): Variables = k match { - case Cont.ContVar(id) => cont(id) - case Cont.Abort => Set() - case Cont.ContLam(results, ks, body) => free(body) -- all(results, value) -- meta(ks) - } -} - - -object substitutions { - - case class Substitution( - values: Map[Id, Pure] = Map.empty, - blocks: Map[Id, Block] = Map.empty, - conts: Map[Id, Cont] = Map.empty, - metaconts: Map[Id, MetaCont] = Map.empty - ) { - def shadowValues(shadowed: IterableOnce[Id]): Substitution = copy(values = values -- shadowed) - def shadowBlocks(shadowed: IterableOnce[Id]): Substitution = copy(blocks = blocks -- shadowed) - def shadowConts(shadowed: IterableOnce[Id]): Substitution = copy(conts = conts -- shadowed) - def shadowMetaconts(shadowed: IterableOnce[Id]): Substitution = copy(metaconts = metaconts -- shadowed) - - def shadowParams(vparams: Seq[Id], bparams: Seq[Id]): Substitution = - copy(values = values -- vparams, blocks = blocks -- bparams) - } - - def substitute(expression: Expr)(using Substitution): Expr = expression match { - case DirectApp(id, vargs, bargs) => - DirectApp(id, vargs.map(substitute), bargs.map(substitute)) - case p: Pure => substitute(p) - } - - def substitute(pure: Pure)(using subst: Substitution): Pure = pure match { - case ValueVar(id) if subst.values.isDefinedAt(id) => subst.values(id) - case ValueVar(id) => ValueVar(id) - case Literal(value) => Literal(value) - case Make(tpe, tag, vargs) => Make(tpe, tag, vargs.map(substitute)) - case PureApp(id, vargs) => PureApp(id, vargs.map(substitute)) - case Box(b) => Box(substitute(b)) - } - - def substitute(block: Block)(using subst: Substitution): Block = block match { - case BlockVar(id) if subst.blocks.isDefinedAt(id) => subst.blocks(id) - case BlockVar(id) => BlockVar(id) - case b: BlockLit => substitute(b) - case Unbox(pure) => Unbox(substitute(pure)) - case New(impl) => New(substitute(impl)) - } - - def substitute(b: BlockLit)(using subst: Substitution): BlockLit = b match { - case BlockLit(vparams, bparams, ks, k, body) => - BlockLit(vparams, bparams, ks, k, - substitute(body)(using subst - .shadowParams(vparams, bparams) - .shadowMetaconts(List(ks)) - .shadowConts(List(k)))) - } - - def substitute(stmt: Stmt)(using subst: Substitution): Stmt = stmt match { - case Jump(k, vargs, ks) => - Jump( - substituteAsContVar(k), - vargs.map(substitute), - substitute(ks)) - - case App(callee, vargs, bargs, ks, k) => - App( - substitute(callee), - vargs.map(substitute), - bargs.map(substitute), - substitute(ks), - substitute(k)) - - case Invoke(callee, method, vargs, bargs, ks, k) => - Invoke( - substitute(callee), - method, - vargs.map(substitute), - bargs.map(substitute), - substitute(ks), - substitute(k)) - - case If(cond, thn, els) => - If(substitute(cond), substitute(thn), substitute(els)) - - case Match(scrutinee, clauses, default) => - Match( - substitute(scrutinee), - clauses.map { case (id, cl) => (id, substitute(cl)) }, - default.map(substitute)) - - case LetDef(id, binding, body) => - LetDef(id, substitute(binding), - substitute(body)(using subst.shadowBlocks(List(id)))) - - case LetExpr(id, binding, body) => - LetExpr(id, substitute(binding), - substitute(body)(using subst.shadowValues(List(id)))) - - case LetCont(id, binding, body) => - LetCont(id, substitute(binding), - substitute(body)(using subst.shadowConts(List(id)))) - - case Region(id, ks, body) => - Region(id, substitute(ks), - substitute(body)(using subst.shadowBlocks(List(id)))) - - case Alloc(id, init, region, body) => - Alloc(id, substitute(init), substituteAsBlockVar(region), - substitute(body)(using subst.shadowBlocks(List(id)))) - - case Var(id, init, ks, body) => - Var(id, substitute(init), substitute(ks), - substitute(body)(using subst.shadowBlocks(List(id)))) - - case Dealloc(ref, body) => - Dealloc(substituteAsBlockVar(ref), substitute(body)) - - case Get(ref, id, body) => - Get(substituteAsBlockVar(ref), id, - substitute(body)(using subst.shadowValues(List(id)))) - - case Put(ref, value, body) => - Put(substituteAsBlockVar(ref), substitute(value), substitute(body)) - - case Reset(prog, ks, k) => - Reset(substitute(prog), substitute(ks), substitute(k)) - - case Shift(prompt, body, ks, k) => - Shift(substituteAsBlockVar(prompt), substitute(body), substitute(ks), substitute(k)) - - case Resume(r, body, ks, k) => - Resume(substituteAsBlockVar(r), substitute(body), substitute(ks), substitute(k)) - - case h: Hole => h - } - - def substitute(impl: Implementation)(using Substitution): Implementation = impl match { - case Implementation(interface, operations) => - Implementation(interface, operations.map(substitute)) - } - - def substitute(op: Operation)(using subst: Substitution): Operation = op match { - case Operation(name, vparams, bparams, ks, k, body) => - Operation(name, vparams, bparams, ks, k, - substitute(body)(using subst - .shadowParams(vparams, bparams) - .shadowMetaconts(List(ks)) - .shadowConts(List(k)))) - } - - def substitute(clause: Clause)(using subst: Substitution): Clause = clause match { - case Clause(vparams, body) => - Clause(vparams, substitute(body)(using subst.shadowValues(vparams))) - } - - def substitute(k: Cont)(using subst: Substitution): Cont = k match { - case Cont.ContVar(id) if subst.conts.isDefinedAt(id) => subst.conts(id) - case Cont.ContVar(id) => Cont.ContVar(id) - case lam @ Cont.ContLam(result, ks, body) => substitute(lam) - case Cont.Abort => Cont.Abort - } - - def substitute(k: Cont.ContLam)(using subst: Substitution): Cont.ContLam = k match { - case Cont.ContLam(results, ks, body) => - Cont.ContLam(results, ks, - substitute(body)(using subst - .shadowValues(results) - .shadowMetaconts(List(ks)))) - } - - def substitute(ks: MetaCont)(using subst: Substitution): MetaCont = - subst.metaconts.getOrElse(ks.id, ks) - - def substituteAsBlockVar(id: Id)(using subst: Substitution): Id = - subst.blocks.get(id) map { - case BlockVar(x) => x - case _ => INTERNAL_ERROR("References should always be variables") - } getOrElse id - - def substituteAsContVar(id: Id)(using subst: Substitution): Id = - subst.conts.get(id) map { - case Cont.ContVar(x) => x - case _ => INTERNAL_ERROR("Continuation references should always be variables") - } getOrElse id -} diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/ChezScheme.scala b/effekt/shared/src/main/scala/effekt/generator/chez/ChezScheme.scala deleted file mode 100644 index e91f2b568..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/chez/ChezScheme.scala +++ /dev/null @@ -1,83 +0,0 @@ -package effekt -package generator -package chez - -import effekt.context.Context -import effekt.core.optimizer.Optimizer -import effekt.symbols.{ Module, Symbol } -import effekt.util.messages.ErrorReporter -import kiama.output.PrettyPrinterTypes.Document -import kiama.util.Source - -class ChezSchemeMonadic extends ChezScheme { - def compilationUnit(mainSymbol: Symbol, mod: Module, decl: core.ModuleDecl)(using ErrorReporter): chez.Block = - chez.TransformerMonadic.compilationUnit(mainSymbol, mod, decl) - - override def supportedFeatureFlags: List[String] = List("chezMonadic", "chez") -} - - -class ChezSchemeCallCC extends ChezScheme { - def compilationUnit(mainSymbol: Symbol, mod: Module, decl: core.ModuleDecl)(using ErrorReporter): chez.Block = - chez.TransformerCallCC.compilationUnit(mainSymbol, mod, decl) - - override def supportedFeatureFlags: List[String] = List("chezCallCC", "chez") -} - - -trait ChezScheme extends Compiler[String] { - - def compilationUnit(mainSymbol: Symbol, mod: Module, decl: core.ModuleDecl)(using ErrorReporter): chez.Block - - // Implementation of the Compiler Interface: - // ----------------------------------------- - def extension = ".ss" - - override def supportedFeatureFlags: List[String] = List("chez") - - override def prettyIR(source: Source, stage: Stage)(using Context): Option[Document] = stage match { - case Stage.Core => Core(source).map { res => core.PrettyPrinter.format(res.core) } - case Stage.Machine => None - case Stage.Target => Separate(source).map { res => pretty(res) } - } - - override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = stage match { - case Stage.Core => Core(source).map { res => res.core } - case Stage.Machine => None - case Stage.Target => Separate(source) - } - - override def compile(source: Source)(using C: Context) = Compile(source) - - // The Compilation Pipeline - // ------------------------ - // Source => Core => Chez - lazy val Compile = - allToCore(Core) andThen Aggregate andThen Optimizer andThen Chez map { case (main, expr) => - (Map(main -> pretty(expr).layout), main) - } - - lazy val Core = Phase.cached("core") { - Frontend andThen Middleend - } - - lazy val Chez = Phase("chez") { - case CoreTransformed(source, tree, mod, core) => - val mainSymbol = Context.checkMain(mod) - val mainFile = path(mod) - mainFile -> chez.Let(Nil, compilationUnit(mainSymbol, mod, core)) - } - - // The Compilation Pipeline for VSCode - // ----------------------------------- - lazy val Separate = - allToCore(Core) map { all => all.main } andThen Chez map { case (_, expr) => expr } - - // Helpers - // ------- - def pretty(expr: chez.Expr): Document = - chez.PrettyPrinter.pretty(chez.PrettyPrinter.toDoc(expr), 100) - - def pretty(defs: List[chez.Def]): Document = - chez.PrettyPrinter.format(defs) -} diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/chez/PrettyPrinter.scala deleted file mode 100644 index 293575814..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/chez/PrettyPrinter.scala +++ /dev/null @@ -1,66 +0,0 @@ -package effekt -package generator -package chez - -import effekt.util.intercalate -import kiama.output.ParenPrettyPrinter -import kiama.output.PrettyPrinterTypes.Document - -import scala.language.implicitConversions - -object PrettyPrinter extends ParenPrettyPrinter { - - override val defaultIndent = 2 - - val prelude = "#!/usr/local/bin/scheme --script\n\n(import (chezscheme))\n\n" - - def toDoc(name: ChezName): Doc = name.name - - def format(defs: List[Def]): Document = - pretty(vsep(defs map toDoc, line <> line)) - - def toDoc(binding: Binding): Doc = brackets(toDoc(binding.name) <+> toDoc(binding.expr)) - - def toDoc(expr: Expr): Doc = expr match { - case Call(callee, Nil) => parens(toDoc(callee)) - case Call(callee, arguments) => parens(toDoc(callee) <+> group(align(hsep(arguments map toDoc, line)))) - case RawExpr(strings, args) => hcat(intercalate(strings.map(string), args.map(toDoc))) - case RawValue(raw) => string(raw) - case Let(bindings, body) => parens("let" <+> parens(align(vcat(bindings map toDoc))) <> toDoc(body)) - case Let_*(bindings, body) => parens("let*" <+> parens(align(vcat(bindings map toDoc))) <> toDoc(body)) - case Lambda(params, body) => parens("lambda" <+> parens(params.map(toDoc)) <> toDoc(body)) - case If(cond, thn, els) => parens("if" <+> toDoc(cond) <> nest(line <> toDoc(thn)) <> nest(line <> toDoc(els))) - case Variable(name) => toDoc(name) - case Cond(clauses, default) => - val els = default.toList.map(d => brackets("else" <+> toDoc(d))) - parens("cond" <+> nest(line <> align(vcat(clauses.map { - case (pattern, branch) => brackets(group(toDoc(pattern) <+> toDoc(branch))) - } ++ els)))) - case Reset(body) => parens("reset" <+> nest(line <> toDoc(body))) - case Shift(p, body) => parens("shift" <+> toDoc(p) <+> nest(line <> toDoc(body))) - } - - def toDoc(definition: Def): Doc = definition match { - case Def.Constant(name, value) => parens("define" <+> toDoc(name) <+> toDoc(value)) - case Def.Function(name, Nil, body) => parens("define" <+> parens(toDoc(name)) <> toDoc(body)) - case Def.Function(name, params, body) => parens("define" <+> parens(toDoc(name) <+> hsep(params map toDoc)) <> toDoc(body)) - case RawDef(raw) => string(raw) - - case Record(typeName, constructorName, predicateName, uid, fields) => - parens("define-record-type" <+> parens(toDoc(typeName) <+> toDoc(constructorName) <+> toDoc(predicateName)) <> - nest(line <> parens("fields" <+> align(vcat(fields map { f => brackets("immutable" <+> toDoc(f) <+> toDoc(f)) })))) <> - nest(line <> parens("nongenerative" <+> toDoc(uid)))) - } - - def toDoc(block: Block): Doc = block match { - case Block(Nil, Nil, result) => nest(line <> toDoc(result)) - case Block(definitions, expressions, result) => - nest(line <> - vsep(definitions map toDoc, line) <> - vcat(expressions map toDoc) <> - line <> - toDoc(result)) - } - - def parens(docs: List[Doc]): Doc = parens(hsep(docs)) -} diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala deleted file mode 100644 index 90882531d..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala +++ /dev/null @@ -1,271 +0,0 @@ -package effekt -package generator -package chez - -import effekt.context.Context -import effekt.core.* -import effekt.symbols.{ Module, Symbol, TermSymbol, Wildcard } -import effekt.util.paths.* -import effekt.util.messages.ErrorReporter -import kiama.output.PrettyPrinterTypes.Document -import util.messages.{ INTERNAL_ERROR, NOT_SUPPORTED } - -import scala.language.implicitConversions -import scala.util.matching.Regex -import scala.collection.mutable - -object TransformerMonadic extends Transformer { - - def run(expr: chez.Expr): chez.Expr = - Builtin("run", expr) - - def pure(expr: chez.Expr): chez.Expr = - Builtin("pure", expr) - - def bind(binding: chez.Expr, param: ChezName, body: chez.Block): chez.Expr = - Builtin("then", binding, chez.Lambda(List(param), body)) - - def runMain(main: ChezName): chez.Expr = - chez.Builtin("run", chez.Call(main)) -} - -object TransformerCallCC extends Transformer { - - def run(expr: chez.Expr): chez.Expr = expr - - def pure(expr: chez.Expr): chez.Expr = expr - - def bind(binding: chez.Expr, param: ChezName, body: chez.Block): chez.Expr = - chez.Let(List(Binding(param, binding)), body) - - def runMain(main: ChezName): chez.Expr = - chez.Builtin("run", chez.Variable(main)) -} - -trait Transformer { - - val escapeSeqs: Map[Char, String] = Map('\'' -> raw"'", '\"' -> raw"\"", '\\' -> raw"\\", '\n' -> raw"\n", '\t' -> raw"\t", '\r' -> raw"\r") - - def run(expr: chez.Expr): chez.Expr - def pure(expr: chez.Expr): chez.Expr - def bind(binding: chez.Expr, param: ChezName, body: chez.Block): chez.Expr - - def runMain(main: ChezName): chez.Expr - - def state(id: ChezName, init: chez.Expr, body: chez.Block): chez.Expr = - Builtin("state", init, chez.Lambda(List(id), body)) - - def compilationUnit(mainSymbol: Symbol, mod: Module, core: ModuleDecl)(using ErrorReporter): chez.Block = { - val definitions = toChez(core) - chez.Block(generateStateAccessors(pure) ++ definitions, Nil, runMain(nameRef(mainSymbol))) - } - - def toChez(p: ValueParam): ChezName = nameDef(p.id) - def toChez(p: BlockParam): ChezName = nameDef(p.id) - - def toChez(module: ModuleDecl)(using ErrorReporter): List[chez.Def] = { - val decls = module.declarations.flatMap(toChez) - val externs = module.externs.map(toChez) - // TODO FIXME, once there is a let _ = ... in there, we are doomed! - val defns = module.definitions.map(toChez) - decls ++ externs ++ defns - } - - def toChezExpr(stmt: Stmt): chez.Expr = stmt match { - case Return(e) => pure(toChez(e)) - case App(b, targs, vargs, bargs) => chez.Call(toChez(b), vargs.map(toChez) ++ bargs.map(toChez)) - case Invoke(b, method, methodTpe, targs, vargs, bargs) => - chez.Call(chez.Call(chez.Variable(nameRef(method)), List(toChez(b))), vargs.map(toChez) ++ bargs.map(toChez)) - case If(cond, thn, els) => chez.If(toChez(cond), toChezExpr(thn), toChezExpr(els)) - case Val(id, tpe, binding, body) => bind(toChezExpr(binding), nameDef(id), toChez(body)) - // empty matches are translated to a hole in chez scheme - case Match(scrutinee, Nil, None) => chez.Builtin("hole") - case Match(scrutinee, clauses, default) => - val sc = toChez(scrutinee) - val cls = clauses.map { case (constr, branch) => - val names = RecordNames(constr) - val pred = chez.Call(chez.Variable(names.predicate), List(sc)) - val matcher = chez.Call(chez.Variable(names.matcher), List(sc, toChez(branch))) - (pred, matcher) - } - chez.Cond(cls, default.map(toChezExpr)) - - case Hole() => chez.Builtin("hole") - - case Var(ref, init, capt, body) => - state(nameDef(ref), toChez(init), toChez(body)) - - case Alloc(id, init, region, body) => - chez.Let(List(Binding(nameDef(id), chez.Builtin("fresh", chez.Variable(nameRef(region)), toChez(init)))), toChez(body)) - - case Reset(body) => chez.Reset(toChez(body)) - - case Shift(p, body) => chez.Shift(nameRef(p.id), toChez(body)) - - // currently bidirectional handlers are not supported - case Resume(k, Return(expr)) => chez.Call(toChez(k), List(toChez(expr))) - - case Resume(k, other) => sys error s"Not supported yet: ${util.show(stmt)}" - - case Region(body) => chez.Builtin("with-region", toChez(body)) - - case stmt: (Def | Let | Get | Put) => - chez.Let(Nil, toChez(stmt)) - } - - def toChez(decl: core.Declaration): List[chez.Def] = decl match { - case Data(did, tparams, ctors) => - ctors.flatMap { ctor => generateConstructor(ctor.id, ctor.fields.map(f => f.id)) } - - // We use chez scheme records to also represent capabilities. - case Declaration.Interface(id, tparams, operations) => - generateConstructor(id, operations.map(op => op.id)) - } - - def toChez(decl: core.Extern)(using ErrorReporter): chez.Def = decl match { - case Extern.Def(id, tpe, cps, vps, bps, ret, capt, body) => - val tBody = body match { - case ExternBody.StringExternBody(featureFlag, contents) => toChez(contents) - case u: ExternBody.Unsupported => - u.report - chez.Builtin("hole") - } - chez.Constant(nameDef(id), - chez.Lambda(vps.map { p => nameDef(p.id) } ++ bps.map { p => nameDef(p.id) }, - tBody)) - - case Extern.Include(ff, contents) => - RawDef(contents) - } - - def toChez(t: Template[core.Expr]): chez.Expr = - chez.RawExpr(t.strings, t.args.map(e => toChez(e))) - - def toChez(defn: Toplevel): chez.Def = defn match { - case Toplevel.Def(id, block) => chez.Constant(nameDef(id), toChez(block)) - case Toplevel.Val(id, tpe, binding) => chez.Constant(nameDef(id), run(toChezExpr(binding))) - } - - def toChez(stmt: Stmt): chez.Block = stmt match { - case Stmt.Def(id, block, body) => - val chez.Block(defs, exprs, result) = toChez(body) - chez.Block(chez.Constant(nameDef(id), toChez(block)) :: defs, exprs, result) - - case Stmt.Let(Wildcard(), tpe, binding, body) => - toChez(binding) match { - // drop the binding altogether, if it is of the form: - // let _ = myVariable; BODY - // since this might lead to invalid scheme code. - case _: chez.Variable => toChez(body) - case expr => - toChez(body) match { - case chez.Block(Nil, exprs, result) => chez.Block(Nil, expr :: exprs, result) - case rest => chez.Block(Nil, expr :: Nil, chez.Let(Nil, rest)) - } - } - - case Stmt.Let(id, tpe, binding, body) => - val chez.Block(defs, exprs, result) = toChez(body) - chez.Block(chez.Constant(nameDef(id), toChez(binding)) :: defs, exprs, result) - - case Stmt.Get(id, tpe, ref, capt, body) => - val reading = chez.Constant(nameDef(id), chez.Call(chez.Call(nameRef(symbols.builtins.TState.get), nameRef(ref)))) - val chez.Block(defs, exprs, result) = toChez(body) - chez.Block(reading :: defs, exprs, result) - - case Stmt.Put(ref, capt, value, body) => - val writing = chez.Call(chez.Call(nameRef(symbols.builtins.TState.put), nameRef(ref)), toChez(value)) - toChez(body) match { - case chez.Block(Nil, exprs, result) => chez.Block(Nil, writing :: exprs, result) - case rest => chez.Block(Nil, writing :: Nil, chez.Let(Nil, rest)) - } - - - case other => chez.Block(Nil, Nil, toChezExpr(other)) - } - - def toChez(block: BlockLit): chez.Lambda = block match { - case BlockLit(tps, cps, vps, bps, body) => - chez.Lambda(vps.map(toChez) ++ bps.map(toChez), toChez(body)) - } - - def toChez(block: Block): chez.Expr = block match { - case BlockVar(id, _, _) => - chez.Variable(nameRef(id)) - - case b @ BlockLit(tps, cps, vps, bps, body) => toChez(b) - - case Unbox(e) => toChez(e) - - case New(impl) => toChez(impl) - } - - def toChez(impl: Implementation): chez.Expr = - val ChezName(name) = nameRef(impl.interface.name) - chez.Call(chez.Variable(ChezName(name)), impl.operations.map(toChez)) - - def toChez(op: Operation): chez.Expr = op match { - case Operation(name, tps, cps, vps, bps, body) => - chez.Lambda(vps.map(toChez) ++ bps.map(toChez), toChez(body)) - } - - def toChez(expr: Expr): chez.Expr = expr match { - case Literal((), _) => chez.RawValue("#f") - - case Literal(s: String, _) => escape(s) - case Literal(b: Boolean, _) => if (b) chez.RawValue("#t") else chez.RawValue("#f") - case l: Literal => chez.RawValue(l.value.toString) - case ValueVar(id, _) => chez.Variable(nameRef(id)) - - case DirectApp(b, targs, vargs, bargs) => chez.Call(toChez(b), vargs.map(toChez) ++ bargs.map(toChez)) - case PureApp(b, targs, args) => chez.Call(toChez(b), args map toChez) - case Make(data, tag, targs, args) => chez.Call(chez.Variable(nameRef(tag)), args map toChez) - - case Box(b, _) => toChez(b) - } - - - // STATE - // ----- - - // (define (getter ref) - // (lambda () (pure (unbox ref)))) - // - // (define (setter ref) - // (lambda (v) (pure (set-box! ref v)))) - def generateStateAccessors(pure: chez.Expr => chez.Expr): List[chez.Function] = { - val ref = ChezName("ref") - val value = ChezName("value") - - val getter = chez.Function(nameDef(symbols.builtins.TState.get), List(ref), - chez.Lambda(Nil, chez.Builtin("unbox", chez.Variable(ref)))) - - val setter = chez.Function(nameDef(symbols.builtins.TState.put), List(ref), - chez.Lambda(List(value), chez.Builtin("set-box!", chez.Variable(ref), chez.Variable(value)))) - - List(getter, setter) - } - - def escape(scalaString: String): chez.Expr = { - val parts = mutable.ListBuffer[chez.Expr]() - val strPart = StringBuilder() - scalaString.codePoints().forEach { - case c if escapeSeqs.contains(c.toChar) => strPart.append(escapeSeqs(c.toChar)) - case c if c >= 32 && c <= 126 => strPart.append(String.valueOf(Character.toChars(c))) - case c if c < 8 * 8 * 8 => - strPart.append("\\" + Integer.toString(c, 8).reverse.padTo(3, '0').reverse) - case c => - parts.addOne(chez.RawValue("\"" ++ strPart.mkString ++ "\"")) - strPart.clear() - parts.addOne(chez.Call(chez.RawExpr("string"), chez.Call(chez.RawExpr("integer->char"), - chez.RawExpr("#x" ++ c.toHexString)))) - } - parts.addOne(chez.RawValue("\"" ++ strPart.mkString ++ "\"")) - - if (parts.size == 1) { - parts(0) - } else { - chez.Call(chez.RawExpr("string-append"), parts.toList) - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/chez/Tree.scala deleted file mode 100644 index 571dfb933..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Tree.scala +++ /dev/null @@ -1,249 +0,0 @@ -package effekt -package generator -package chez - -import scala.language.implicitConversions - -// TODO choose appropriate representation and apply conversions -case class ChezName(name: String) - -case class Binding(name: ChezName, expr: Expr) - -case class Block(definitions: List[Def], expressions: List[Expr], result: Expr) - -case class Operation(name: ChezName, params: List[ChezName], k: ChezName, body: Expr) - -/** - * This file defines the syntax of Chez Scheme as it is the image of our translation. - */ -enum Expr { - - // e.g. ( ) - case Call(callee: Expr, arguments: List[Expr]) - - // e.g. "" " + " - // raw scheme splices, always start with a prefix string, then interleaved with arguments - case RawExpr(raw: List[String], args: List[Expr]) - - // e.g. 42 (represented as Scala string "42") and inserted verbatim - case RawValue(raw: String) - - // e.g. (let ([x 42]) (+ x x)) - case Let(bindings: List[Binding], body: Block) - - // e.g. (let* ([x 42] [y x]) (+ x y)) - case Let_*(bindings: List[Binding], body: Block) - - // e.g. (lambda (x y) body) - case Lambda(params: List[ChezName], body: Block) - - // e.g. (if COND THEN ELSE) - case If(cond: Expr, thn: Expr, els: Expr) - - // e.g. (cond ([COND1 BRANCH1]... [else DEFAULT])) - case Cond(clauses: List[(Expr, Expr)], default: Option[Expr]) - - // e.g x - case Variable(name: ChezName) - - // reset is a macro, stable across Chez variants - case Reset(body: Expr) - - case Shift(prompt: ChezName, body: Expr) -} -export Expr.* - -def RawExpr(str: String): chez.Expr = Expr.RawExpr(List(str), Nil) - -enum Def { - // e.g. (define x 42) - case Constant(name: ChezName, value: Expr) - - // e.g. (define (f x y) ...) - case Function(name: ChezName, params: List[ChezName], body: Block) - - case RawDef(raw: String) - - case Record(typeName: ChezName, constructorName: ChezName, predicateName: ChezName, uid: ChezName, fields: List[ChezName]) -} -export Def.* - -// smart constructors -def Call(callee: Expr, args: Expr*): Expr = Call(callee, args.toList) - -def Lambda(params: List[ChezName], body: Expr): Lambda = Lambda(params, Block(Nil, Nil, body)) - -def Function(name: ChezName, params: List[ChezName], body: Expr): Function = Function(name, params, Block(Nil, Nil, body)) - -def Let(bindings: List[Binding], body: Expr): Expr = Let(bindings, Block(Nil, Nil, body)) - -def ChezString(chezString: String): Expr = RawExpr(s"\"${chezString}\"") - -def Builtin(name: String, args: Expr*): Expr = Call(Variable(ChezName(name)), args.toList) - -def curry(lam: chez.Lambda): chez.Lambda = lam.params.foldRight[chez.Lambda](chez.Lambda(Nil, lam.body)) { - case (p, body) => chez.Lambda(List(p), body) -} - -def unit = chez.Expr.RawValue("'()") - -implicit def autoVar(n: ChezName): Expr = Variable(n) - -def cleanup(expr: Expr): Expr = LetFusion.rewrite(DeadCodeElimination.rewrite(expr)(using ()))(using ()) - -object LetFusion extends Tree.Rewrite[Unit] { - override def expr(using Unit) = { - case Let(bindings, body) => rewrite(body) match { - case Block(Nil, Nil, Let(otherBindings, body)) => Let_*((bindings map rewrite) ++ otherBindings, body) - case Block(Nil, Nil, Let_*(otherBindings, body)) => Let_*((bindings map rewrite) ++ otherBindings, body) - case b => Let(bindings map rewrite, b) - } - case Let_*(bindings, body) => rewrite(body) match { - case Block(Nil, Nil, Let(otherBindings, body)) => Let_*((bindings map rewrite) ++ otherBindings, body) - case Block(Nil, Nil, Let_*(otherBindings, body)) => Let_*((bindings map rewrite) ++ otherBindings, body) - case b => Let(bindings map rewrite, b) - } - } -} - -object DeadCodeElimination extends Tree.Rewrite[Unit] { - override def expr(using Unit) = { - case Let(bindings, body) => - val transformedBody = rewrite(body) - val fv = FreeVariables.query(transformedBody) - val bs = bindings.collect { - case b @ Binding(name, binding) if (fv contains name) || !isInlinable(binding) => rewrite(b) - } - Let(bs, transformedBody) - } - - override def rewrite(block: Block)(using Unit): Block = block match { - - case Block(defs, exprs, result) => - val transformedResult = rewrite(result) - val transformedExprs = exprs.map(rewrite) - val transformedDefs = defs.map(rewrite) - - val fv = transformedDefs.flatMap(FreeVariables.query) ++ transformedExprs.flatMap(FreeVariables.query) ++ FreeVariables.query(transformedResult) - - val filteredDefs = transformedDefs.filter { - case Constant(name, expr) => (fv contains name) || !isInlinable(expr) - case Function(name, params, body) => fv contains name - case _ => true - } - - Block(filteredDefs, transformedExprs, transformedResult) - } -} - -def isInlinable(e: Expr): Boolean = e match { - case _: Variable => true - case _: RawValue => true - case _: Lambda => true - case _ => false -} - -object FreeVariables extends Tree.Query[Unit, Set[ChezName]] { - - given Unit = () - - def empty = Set.empty - def combine = _ ++ _ - - def bound(bindings: List[Binding]): Set[ChezName] = bindings.map { b => b.name }.toSet - def free(bindings: List[Binding]): Set[ChezName] = bindings.flatMap { b => query(b.expr) }.toSet - - override def expr(using Unit) = { - case Variable(name) => Set(name) - - case Let(bindings, body) => - free(bindings) ++ (query(body) -- bound(bindings)) - - case Let_*(bindings, body) => - val freeInBindings = bindings.foldRight(Set.empty[ChezName]) { - case (Binding(name, b), free) => (free - name) ++ query(b) - } - freeInBindings ++ (query(body) -- bound(bindings)) - - case Lambda(params, body) => query(body) -- params.toSet - } - - override def query(operation: Operation)(using Unit): Set[ChezName] = - query(operation.body) -- operation.params.toSet - operation.k - - override def defn(using Unit) = { - case chez.Function(name, params, body) => query(body) -- params.toSet - name // recursive functions - case chez.Constant(name, expr) => query(expr) - } - - override def query(b: Block)(using Unit): Set[ChezName] = b match { - // defs are potentially recursive! - case Block(defs, exprs, result) => - - val boundByDefs = defs.collect { - case f: chez.Function => f.name - case f: chez.Constant => f.name - }.toSet - - val freeInDefs = defs.flatMap(query).toSet - - (freeInDefs ++ query(result) ++ exprs.flatMap(query)) -- boundByDefs - } -} - - -object Tree { - - // This solution is between a fine-grained visitor and a untyped and unsafe traversal. - trait Rewrite[Ctx] extends util.Structural { - - // Hooks to override - def expr(using C: Ctx): PartialFunction[Expr, Expr] = PartialFunction.empty - def defn(using C: Ctx): PartialFunction[Def, Def] = PartialFunction.empty - - def rewrite(block: Block)(using Ctx): Block = rewriteStructurally(block) - def rewrite(binding: Binding)(using Ctx): Binding = rewriteStructurally(binding) - def rewrite(op: Operation)(using Ctx): Operation = rewriteStructurally(op) - - def rewrite(e: Expr)(using Ctx): Expr = rewriteStructurally(e, expr) - def rewrite(t: Def)(using C: Ctx): Def = rewriteStructurally(t, defn) - - def rewrite(clause: (Expr, Expr))(using Ctx): (Expr, Expr) = clause match { - case (c, t) => (rewrite(c), rewrite(t)) - } - } - - trait Visit[Ctx] extends Query[Ctx, Unit] { - override def empty = () - override def combine = (_, _) => () - } - - trait Query[Ctx, Res] extends util.Structural { - - def empty: Res - def combine: (Res, Res) => Res - - // Hooks to override - def expr(using Ctx): PartialFunction[Expr, Res] = PartialFunction.empty - def defn(using Ctx): PartialFunction[Def, Res] = PartialFunction.empty - - /** - * Hook that can be overridden to perform an action at every node in the tree - */ - def visit[T](t: T)(visitor: T => Res)(using Ctx): Res = visitor(t) - - inline def structuralQuery[T](el: T, pf: PartialFunction[T, Res] = PartialFunction.empty)(using Ctx): Res = visit(el) { t => - if pf.isDefinedAt(el) then pf.apply(el) else queryStructurally(t, empty, combine) - } - - def query(e: Expr)(using Ctx): Res = structuralQuery(e, expr) - def query(d: Def)(using Ctx): Res = structuralQuery(d, defn) - def query(b: Block)(using Ctx): Res = structuralQuery(b) - def query(b: Binding)(using Ctx): Res = structuralQuery(b) - def query(e: Operation)(using Ctx): Res = structuralQuery(e) - - def query(clause: (Expr, Expr))(using Ctx): Res = clause match { - case (p, e) => combine(query(p), query(e)) - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/package.scala b/effekt/shared/src/main/scala/effekt/generator/chez/package.scala deleted file mode 100644 index b13fb4628..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/chez/package.scala +++ /dev/null @@ -1,75 +0,0 @@ -package effekt -package generator -package chez - -import effekt.context.Context -import effekt.core.* -import effekt.symbols.{ Module, Symbol, Wildcard } - -import scala.language.implicitConversions -import effekt.util.paths.* - -import scala.util.matching.Regex - - -def uniqueName(id: Symbol): String = id.name.toString.replace("?", "").replace("!", "") + "_" + id.id - -def nameRef(id: Symbol): ChezName = nameDef(id) - -// TODO sanitize -def nameDef(id: Symbol): ChezName = ChezName(uniqueName(id)) - -def intersperse[T](l: List[T], el: T): List[T] = l match { - case Nil => Nil - case head :: Nil => head :: Nil - case head :: rest => head :: el :: intersperse(rest, el) -} - -case class RecordNames(sym: Symbol) { - val name = uniqueName(sym) - val basename = sym.name.name - val id = sym.id.toString - - val uid = ChezName(name) - val typeName = ChezName(basename + "$Type" + id) - val predicate = ChezName(name + "?") - val matcher = ChezName("match-" + name) - val constructor = ChezName(name) -} - -// https://www.scheme.com/csug8/objects.html -// https://scheme.com/tspl4/records.html -// -// There is also a paper by Andrey and Kent on records in Chez Scheme: -// https://andykeep.com/pubs/scheme-12b.pdf -def generateConstructor(id: Symbol, fields: List[Symbol]): List[chez.Def] = { - - val did = id match { - case c: symbols.Constructor => c - case r: symbols.Record => r.constructor - // right now, we also use core.Records to represent capabilities - case i: symbols.Interface => i - case other => other - // sys error s"Compiler error: cannot generate a scheme record for internal symbol ${other}" - } - - val names = RecordNames(did) - - // Record - val record = chez.Record(names.typeName, names.constructor, names.predicate, names.uid, fields map nameDef) - - // The matcher only applies the given block with the extracted fields - // (define ( sc block) (block (field-1 sc) (field-2 sc) ...))) - def matcher = { - val sc = ChezName("sc") - val block = ChezName("block") - - val selectors = fields map { field => - chez.Call(nameRef(field), chez.Variable(sc)) - } - chez.Function(names.matcher, List(sc, block), - chez.Call(chez.Variable(block), selectors)) - } - - List(record, matcher) -} diff --git a/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala b/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala deleted file mode 100644 index f9d88cf93..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala +++ /dev/null @@ -1,86 +0,0 @@ -package effekt -package generator -package js - -import effekt.PhaseResult.CoreTransformed -import effekt.context.Context -import effekt.core.optimizer.{ DropBindings, Optimizer } -import kiama.output.PrettyPrinterTypes.Document -import kiama.util.Source - - -class JavaScript(additionalFeatureFlags: List[String] = Nil) extends Compiler[String] { - - // Implementation of the Compiler Interface: - // ----------------------------------------- - def extension = ".js" - - override def supportedFeatureFlags: List[String] = additionalFeatureFlags ++ TransformerCps.jsFeatureFlags - - override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = stage match { - case Stage.Core if C.config.optimize() => Optimized(source).map { (_, _, res) => core.PrettyPrinter.format(res) } - case Stage.Core => Core(source).map { res => core.PrettyPrinter.format(res.core) } - case Stage.Machine => None - case Stage.Target => CompileLSP(source).map { pretty } - } - - override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = stage match { - case Stage.Core => Core(source).map { res => res.core } - case Stage.Machine => None - case Stage.Target => CompileLSP(source) - } - - override def compile(source: Source)(using C: Context) = Compile(source) - - def compileWeb(source: Source)(using C: Context) = CompileWeb(source) - - - // The Compilation Pipeline - // ------------------------ - // Source => Core => CPS => JS - lazy val Core = Phase.cached("core") { - Frontend andThen Middleend - } - - lazy val Optimized = allToCore(Core) andThen Aggregate andThen Optimizer andThen DropBindings map { - case input @ CoreTransformed(source, tree, mod, core) => - val mainSymbol = Context.checkMain(mod) - val mainFile = path(mod) - (mainSymbol, mainFile, core) - } - - lazy val CPSTransformed = Optimized map { - case (mainSymbol, mainFile, core) => - val cpsTransformed = effekt.cps.Transformer.transform(core) - val contified = cps.Contify.rewrite(cpsTransformed) - (mainSymbol, mainFile, core, contified) - } - - lazy val Compile = CPSTransformed map { - case (mainSymbol, mainFile, core, cps) => - val doc = pretty(TransformerCps.compile(cps, core, mainSymbol).commonjs) - (Map(mainFile -> doc.layout), mainFile) - } - - /** - * Like [[Compile]], but uses module layout [[js.Module.virtual]] - */ - lazy val CompileWeb = CPSTransformed map { - case (mainSymbol, mainFile, core, cps) => - val doc = pretty(TransformerCps.compile(cps, core, mainSymbol).virtual) - (Map(mainFile -> doc.layout), mainFile) - } - - /** - * Like [[Compile]], but shows only the generated javascript functions (no externs, no declarations) - */ - lazy val CompileLSP = CPSTransformed map { - case (mainSymbol, mainFile, core, cps) => - TransformerCps.compileLSP(cps, core) - } - - private def pretty(stmts: List[js.Stmt]): Document = - js.PrettyPrinter.format(stmts) -} -class JavaScriptWeb extends JavaScript(List("jsWeb")) {} -class JavaScriptNode extends JavaScript(List("jsNode")) {} diff --git a/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala deleted file mode 100644 index deee57eb7..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala +++ /dev/null @@ -1,115 +0,0 @@ -package effekt -package generator -package js - -import effekt.util.intercalate -import kiama.output.ParenPrettyPrinter -import kiama.output.PrettyPrinterTypes.Document - -import scala.language.implicitConversions - -object PrettyPrinter extends ParenPrettyPrinter { - - override val defaultIndent = 2 - - def toDoc(name: JSName): Doc = name.name - - def format(stmts: List[Stmt]): Document = - pretty(vsep(stmts map toDoc, line)) - - val show: PartialFunction[Any, String] = { - case m: js.Module => format(m.stmts).layout - } - - def toDoc(expr: Expr): Doc = expr match { - case Call(callee, args) => toDocParens(callee) <> parens(args map toDoc) - case New(callee, args) => "new" <+> toDocParens(callee) <> parens(args map toDoc) - case RawExpr(strings, args) => hcat(intercalate(strings.map(string), args.map(toDocAsAtom))) - case RawLiteral(content) => string(content) - case Member(callee, selection) => toDocParens(callee) <> "." <> toDoc(selection) - case IfExpr(cond, thn, els) => parens(toDoc(cond)) <+> "?" <+> toDoc(thn) <+> ":" <+> toDoc(els) - case Lambda(params, Return(obj: js.Object)) => parens(params map toDoc) <+> "=>" <> nested(parens(toDoc(expr))) - case Lambda(params, Return(expr)) => parens(params map toDoc) <+> "=>" <> nested(toDoc(expr)) - case Lambda(params, Block(stmts)) => parens(params map toDoc) <+> "=>" <+> jsBlock(stmts.map(toDoc)) - case Lambda(params, body) => parens(params map toDoc) <+> "=>" <+> jsBlock(toDoc(body)) - case Object(properties) => group(jsBlock(vsep(properties.map { case (n, d) => toDoc(n) <> ":" <+> toDoc(d) }, comma))) - case ArrayLiteral(elements) => brackets(elements map toDoc) - case Variable(name) => toDoc(name) - } - - // to be used in low precedence positions - def toDocParens(e: Expr): Doc = e match { - case e: IfExpr => parens(toDoc(e)) - case e: Lambda => parens(toDoc(e)) - case o: js.Object => parens(toDoc(e)) - case e => toDoc(e) - } - - // to be used in really low precedence positions - def toDocAsAtom(e: Expr): Doc = e match { - case e: Variable => toDoc(e) - case e: Object => toDoc(e) - case e: ArrayLiteral => toDoc(e) - case e: RawLiteral => toDoc(e) - case e => parens(toDoc(e)) - } - - def toDoc(stmt: Stmt): Doc = stmt match { - case RawStmt(strings, args) => hcat(intercalate(strings.map(string), args.map(toDocAsAtom))) - case Block(stmts) => jsBlock(stmts map toDoc) - case Return(expr) => "return" <+> toDoc(expr) <> ";" - case ExprStmt(expr) => toDoc(expr) <> ";" - case Const(id, expr) => "const" <+> toDoc(id) <+> "=" <+> toDoc(expr) <> ";" - case Let(id, expr) => "let" <+> toDoc(id) <+> "=" <+> toDoc(expr) <> ";" - case Destruct(ids, expr) => "const" <+> braces(hsep(ids.map(toDoc), comma)) <+> "=" <+> toDoc(expr) <> ";" - case Assign(target, expr) => toDoc(target) <+> "=" <+> toDoc(expr) <> ";" - case Function(name, params, stmts) => "function" <+> toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts map toDoc) - case Class(name, methods) => "class" <+> toDoc(name) <+> jsBlock(methods.map(jsMethod)) - case If(cond, thn, Block(Nil)) => "if" <+> parens(toDoc(cond)) <+> toDocBlock(thn) - case If(cond, thn, els) => "if" <+> parens(toDoc(cond)) <+> toDocBlock(thn) <+> "else" <+> toDocBlock(els) - case Try(prog, id, handler, Nil) => "try" <+> jsBlock(prog.map(toDoc)) <+> "catch" <+> parens(toDoc(id)) <+> jsBlock(handler.map(toDoc)) - case Try(prog, id, handler, fin) => "try" <+> jsBlock(prog.map(toDoc)) <+> "catch" <+> parens(toDoc(id)) <+> jsBlock(handler.map(toDoc)) <+> "finally" <+> jsBlock(fin.map(toDoc)) - case Throw(expr) => "throw" <+> toDoc(expr) <> ";" - case Break() => "break;" - case Continue(label) => "continue" <> label.map(l => space <> toDoc(l)).getOrElse(emptyDoc) <> ";" - case While(cond, stmts, label) => - label.map(l => toDoc(l) <> ":" <> space).getOrElse(emptyDoc) <> - "while" <+> parens(toDoc(cond)) <+> jsBlock(stmts.map(toDoc)) - - case Switch(sc, branches, default) => "switch" <+> parens(toDoc(sc)) <+> jsBlock(branches.map { - case (tag, stmts) => "case" <+> toDoc(tag) <> ":" <+> nested(stmts map toDoc) - } ++ default.toList.map { stmts => "default:" <+> nested(stmts map toDoc) }) - } - - def toDocBlock(stmt: Stmt): Doc = stmt match { - case Block(stmts) => toDoc(stmt) - case If(cond, thn, els) => toDoc(stmt) - case _ => jsBlock(toDoc(stmt)) - } - - def jsMethod(c: js.Function): Doc = c match { - case js.Function(name, params, stmts) => - toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts.map(toDoc)) - } - - def toDoc(pattern: Pattern): Doc = pattern match { - case Pattern.Variable(name) => toDoc(name) - case Pattern.Array(ps) => brackets(ps map toDoc) - } - - // some helpers - - val emptyline: Doc = line <> line - - def nested(content: Doc): Doc = group(nest(line <> content)) - - def nested(docs: List[Doc]): Doc = group(nest(line <> vcat(docs))) - - def parens(docs: List[Doc]): Doc = parens(hsep(docs, comma)) - - def brackets(docs: List[Doc]): Doc = brackets(hsep(docs, comma)) - - def jsBlock(content: Doc): Doc = braces(nest(line <> content) <> line) - - def jsBlock(docs: List[Doc]): Doc = jsBlock(vcat(docs)) -} diff --git a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala deleted file mode 100644 index 0ddb299aa..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala +++ /dev/null @@ -1,187 +0,0 @@ -package effekt -package generator -package js - -import effekt.context.Context -import effekt.context.assertions.* -import effekt.core.{ *, given } -import effekt.core.Variables -import effekt.core.Variables.{ all, bound, free } -import effekt.symbols.{ Module, Symbol, Wildcard, Bindings } - -import scala.collection.mutable - -/** - * Parent all JS transformers. - * - * Shares generic JS specific definitions - */ -trait Transformer { - - val jsFeatureFlags: List[String] = List("js") - - val escapeSeqs: Map[Char, String] = Map('\'' -> raw"'", '\"' -> raw"\"", '\\' -> raw"\\", '\n' -> raw"\n", '\t' -> raw"\t", '\r' -> raw"\r") - - def shouldExport(id: Id)(using D: DeclarationContext): Boolean = true - - // Representation of Data / Codata - // ---- - def tagFor(constructor: Id)(using D: DeclarationContext, C: Context): js.Expr = { - js.RawExpr(D.getConstructorTag(constructor).toString) - } - - def generateConstructor(constructor: Constructor, tagValue: Int): js.Stmt = { - val fields = constructor.fields - // class Id { - // constructor(param...) { this.param = param; ... } - // __reflect() { return { name: "NAME", data: [this.param...] } - // __equals(other) { ... } - // } - val params = fields.map { f => nameDef(f.id) } - - def set(field: JSName, value: js.Expr): js.Stmt = js.Assign(js.Member(js"this", field), value) - def get(field: JSName): js.Expr = js.Member(js"this", field) - - val initParams = params.map { param => set(param, js.Variable(param)) } - val initTag = set(`tag`, js.RawExpr(tagValue.toString)) - val jsConstructor: js.Function = js.Function(JSName("constructor"), params, initTag :: initParams) - - val jsReflect: js.Function = js.Function(`reflect`, Nil, List(js.Return(js.Object(List( - `tag` -> js.RawExpr(tagValue.toString), - `name` -> JsString(constructor.id.name.name), - `data` -> js.ArrayLiteral(fields map { f => get(memberNameRef(f.id)) })))))) - - val other = freshName("other") - def otherGet(field: JSName): js.Expr = js.Member(js.Variable(other), field) - def compare(field: JSName): js.Expr = js"!${$effekt.call("equals", get(field), otherGet(field))}" - val noop = js.Block(Nil) - val abort = js.Return(js"false") - val succeed = js.Return(js"true") - val otherExists = js.If(js"!${js.Variable(other)}", abort, noop) - val compareTags = js.If(compare(`tag`), abort, noop) - val compareFields = params.map(f => js.If(compare(f), abort, noop)) - - val jsEquals: js.Function = js.Function(`equals`, List(other), otherExists :: compareTags :: compareFields ::: List(succeed)) - - js.Class(nameDef(constructor.id), List(jsConstructor, jsReflect, jsEquals)) - } - - // Names - // ----- - - val reserved = List( - // reserved words (according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords) - "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", - "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "let", "new", "null", "return", - "static", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield", - - // future reserved words - "enum", "implements", "interface", "package", "private", "protected", "public", - - // identifiers with special meanings - "get", "set", "arguments", "async", "eval", - - // special names in CommonJS module systems - "module", "exports", "require", - - // other special names - "window", "document", "alert", "console", "this" - ) - - def jsEscape(name: String): String = - if (reserved contains name) "$" + name else name.replace("?", "").replace("!", "") - - def jsModuleName(path: String): String = "$" + path.replace('/', '_').replace('-', '_') - - def jsModuleFile(path: String): String = path.replace('/', '_').replace('-', '_') + ".js" - - val `fresh` = JSName("fresh") - val `ref` = JSName("ref") - val `tag` = JSName("__tag") - val `name` = JSName("__name") - val `data` = JSName("__data") - val `reflect` = JSName("__reflect") - val `equals` = JSName("__equals") - - def nameDef(id: Id): JSName = uniqueName(id) - - // attempt to have better / shorter names - val usedNames: mutable.Map[String, Int] = mutable.Map.empty - val names: mutable.Map[Id, String] = mutable.Map.empty - val baseNameRx = """([A-Za-z$]*(?:_[A-Za-z]+)*)""".r // extracts the base number up until the first number - - def uniqueName(sym: Id): JSName = { - def uniqueNameFor(base: String): String = - val nextId = usedNames.getOrElse(base, 0) - usedNames.update(base, nextId + 1) - s"${base}_${nextId}" - - val name = names.getOrElseUpdate(sym, baseNameRx.findFirstIn(sym.name.name) match { - case Some(base) => uniqueNameFor(base) - case None => - println(sym.name) - uniqueNameFor("tmp") - }) - JSName(jsEscape(name)) - } - - def nameRef(id: Id): js.Expr = js.Variable(uniqueName(id)) - - // name references for fields and methods - def memberNameRef(id: Id): JSName = uniqueName(id) - - def freshName(s: String): JSName = - JSName(s + Symbol.fresh.next()) - - def escape(scalaString: String): String = - scalaString.foldLeft(StringBuilder()) { (acc, c) => - escapeSeqs.get(c) match { - case Some(s) => acc ++= s - case None => acc += c - } - }.toString() - - - // Separate Compilation (Website) - // ------------------------------ - - /** - * Analyse core to find references to symbols defined in other modules. - * - * Necessary for generating the linker code (separate compilation for the web) - */ - def usedIncludes(input: CoreTransformed): Map[Module, Set[Id]] = { - val dependencies = input.mod.dependencies - - // Create a mapping Termsymbol -> Module - def definedIn(m: Module, b: Bindings): Map[Id, Module] = - b.terms.values.flatten.map { sym => (sym : Id) -> m }.toMap ++ - b.namespaces.values.flatMap(bs => definedIn(m, bs)) - - val publicDependencySymbols = dependencies.flatMap(m => definedIn(m, m.exports)).toMap - - var usedFrom: Map[Module, Set[Id]] = Map.empty - - def register(m: Module, sym: Id) = { - val before = usedFrom.getOrElse(m, Set.empty) - usedFrom = usedFrom.updated(m, before + sym) - } - - // Traverse tree once more to find all used symbols, defined in other modules. - def findUsedDependencies(t: Toplevel) = - def go(t: Any): Unit = Tree.visit(t) { - case BlockVar(x, tpe, capt) if publicDependencySymbols.isDefinedAt(x) => - register(publicDependencySymbols(x), x) - case ValueVar(x, tpe) if publicDependencySymbols.isDefinedAt(x) => - register(publicDependencySymbols(x), x) - case Make(tpe, id, targs, args) if publicDependencySymbols.isDefinedAt(id) => - register(publicDependencySymbols(id), id) - args.foreach(go) - } - go(t) - - input.core.definitions.foreach(findUsedDependencies) - - usedFrom - } -} diff --git a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala deleted file mode 100644 index db67fabf7..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ /dev/null @@ -1,551 +0,0 @@ -package effekt -package generator -package js - -import effekt.context.Context -import effekt.context.assertions.* -import effekt.cps.* -import effekt.core.{ DeclarationContext, Id } -import effekt.cps.Variables.{ all, free } -import effekt.cps.substitutions.Substitution -import scala.collection.mutable - -object TransformerCps extends Transformer { - - // Defined in effekt_runtime.js - // --------------------------- - val RUN_TOPLEVEL = Variable(JSName("RUN_TOPLEVEL")) - val RESET = Variable(JSName("RESET")) - val SHIFT = Variable(JSName("SHIFT")) - val RESUME = Variable(JSName("RESUME")) - val THUNK = Variable(JSName("THUNK")) - val DEALLOC = Variable(JSName("DEALLOC")) - val TRAMPOLINE = Variable(JSName("TRAMPOLINE")) - - class RecursiveUsage(var jumped: Boolean) - case class RecursiveDefInfo(id: Id, label: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, used: RecursiveUsage) - case class ContinuationInfo(k: Id, vparams: List[Id], ks: Id) - - case class TransformerContext( - requiresThunk: Boolean, - // definitions of externs (used to inline them) - externs: Map[Id, cps.Extern.Def], - // the innermost (in direct style) enclosing functions (used to rewrite a definition to a loop) - recursive: Option[RecursiveDefInfo], - // the direct-style continuation, if available (used in case cps.Stmt.LetCont) - directStyle: Option[ContinuationInfo], - // the current direct-style metacontinuation - metacont: Option[Id], - // the original declaration context (used to compile pattern matching) - declarations: DeclarationContext, - // the usual compiler context - errors: Context - ) - implicit def autoContext(using C: TransformerContext): Context = C.errors - - - /** - * Entrypoint used by the compiler to compile whole programs - */ - def compile(input: cps.ModuleDecl, coreModule: core.ModuleDecl, mainSymbol: symbols.TermSymbol)(using Context): js.Module = - val exports = List(js.Export(JSName("main"), js.Lambda(Nil, - js.Return(Call(RUN_TOPLEVEL, nameRef(mainSymbol)))))) - - given DeclarationContext = new DeclarationContext(coreModule.declarations, coreModule.externs) - toJS(input, exports) - - def toJS(module: cps.ModuleDecl, exports: List[js.Export])(using D: DeclarationContext, C: Context): js.Module = - module match { - case cps.ModuleDecl(path, includes, declarations, externs, definitions, _) => - given TransformerContext( - false, - externs.collect { case d: Extern.Def => (d.id, d) }.toMap, - None, - None, - None, - D, C) - - val name = JSName(jsModuleName(module.path)) - val jsExterns = module.externs.filterNot(canInline).map(toJS) - val jsDecls = module.declarations.flatMap(toJS) - val stmts = module.definitions.map(toJS) - - js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ stmts) - } - - def compileLSP(input: cps.ModuleDecl, coreModule: core.ModuleDecl)(using C: Context): List[js.Stmt] = - val D = new DeclarationContext(coreModule.declarations, coreModule.externs) - given TransformerContext( - false, - input.externs.collect { case d: Extern.Def => (d.id, d) }.toMap, - None, - None, - None, - D, C) - - input.definitions.map(toJS) - - - def toJS(d: cps.ToplevelDefinition)(using TransformerContext): js.Stmt = d match { - case cps.ToplevelDefinition.Def(id, block) => - js.Const(nameDef(id), requiringThunk { toJS(id, block) }) - case cps.ToplevelDefinition.Val(id, ks, k, binding) => - js.Const(nameDef(id), Call(RUN_TOPLEVEL, js.Lambda(List(nameDef(ks), nameDef(k)), toJS(binding).stmts))) - case cps.ToplevelDefinition.Let(id, binding) => - js.Const(nameDef(id), toJS(binding)) - } - - def toJSParam(id: Id): JSName = nameDef(id) - - def toJS(e: cps.Extern)(using C: TransformerContext): js.Stmt = e match { - case cps.Extern.Def(id, vps, bps, true, body) => - body match { - case ExternBody.StringExternBody(_, contents) => - val ks = freshName("ks_") - val k = freshName("k_") - js.Function(nameDef(id), (vps ++ bps).map(toJSParam) ++ List(ks, k), - List(js.Return(js.Call(toJS(contents), List(js.Variable(ks), js.Variable(k)))))) - case ExternBody.Unsupported(err) => - C.errors.report(err) - js.Function(nameDef(id), (vps ++ bps) map toJSParam, List(js.Return($effekt.call("hole")))) - } - - - case cps.Extern.Def(id, vps, bps, false, body) => - body match { - case ExternBody.StringExternBody(_, contents) => - js.Function(nameDef(id), (vps ++ bps) map toJSParam, List(js.Return(toJS(contents)))) - case ExternBody.Unsupported(err) => - C.errors.report(err) - js.Function(nameDef(id), (vps ++ bps) map toJSParam, List(js.Return($effekt.call("hole")))) - } - - case cps.Extern.Include(ff, contents) => - js.RawStmt(contents) - } - - def toJS(t: Template[Pure])(using TransformerContext): js.Expr = - js.RawExpr(t.strings, t.args.map(toJS)) - - def toJS(d: core.Declaration): List[js.Stmt] = d match { - case core.Data(did, tparams, ctors) => - ctors.zipWithIndex.map { case (ctor, index) => generateConstructor(ctor, index) } - - // interfaces are structurally typed at the moment, no need to generate anything. - case core.Interface(id, tparams, operations) => - Nil - } - - def toJS(id: Id, b: cps.Block)(using TransformerContext): js.Expr = b match { - case cps.Block.BlockLit(vparams, bparams, ks, k, body) => maybeToLoop(id, vparams, bparams, ks, k, body) - case other => toJS(other) - } - - def maybeToLoop(id: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, body: Stmt)(using TransformerContext) = - val used = new RecursiveUsage(false) - val label = Id(id) - - val translatedBody = toJS(body)(using recursive(id, vparams, bparams, ks, k, label, used)).stmts - - if used.jumped then - js.Lambda(vparams.map(nameDef) ++ bparams.map(nameDef) ++ List(nameDef(ks), nameDef(k)), - List(js.While(RawExpr("true"), translatedBody, Some(uniqueName(label))))) - else - js.Lambda(vparams.map(nameDef) ++ bparams.map(nameDef) ++ List(nameDef(ks), nameDef(k)), - translatedBody) - - def toJS(b: cps.Block)(using TransformerContext): js.Expr = b match { - case cps.BlockVar(v) => nameRef(v) - case cps.Unbox(e) => toJS(e) - case cps.New(handler) => toJS(handler) - - case cps.BlockLit(vps, bps, ks, k, body) => - js.Lambda(vps.map(nameDef) ++ bps.map(nameDef) ++ List(nameDef(ks), nameDef(k)), toJS(body).stmts) - } - - def argumentToJS(b: cps.Block)(using TransformerContext): js.Expr = b match { - case cps.BlockLit(vps, bps, ks, k, body) => toJS(b)(using nonrecursive(ks)) - case other => toJS(b) - } - - def toJS(handler: cps.Implementation)(using TransformerContext): js.Expr = handler match { - case cps.Implementation(interface, operations) => - js.Object(operations.map { - case cps.Operation(id, vps, bps, ks, k, body) => - nameDef(id) -> js.Lambda(vps.map(nameDef) ++ bps.map(nameDef) ++ List(nameDef(ks), nameDef(k)), toJS(body)(using nonrecursive(ks)).stmts) - }) - } - - def toJS(ks: cps.MetaCont)(using T: TransformerContext): js.Expr = nameRef(ks.id) - - def toJS(k: cps.Cont)(using T: TransformerContext): js.Expr = k match { - case Cont.ContVar(id) => - nameRef(id) - case Cont.ContLam(results, ks, body) => - js.Lambda(results.map(nameDef) :+ nameDef(ks), toJS(body)(using nonrecursive(ks)).stmts) - case Cont.Abort => js.Undefined - } - - def toJS(e: cps.Expr)(using D: TransformerContext): js.Expr = e match { - case Pure.ValueVar(id) => nameRef(id) - case Pure.Literal(()) => $effekt.field("unit") - case Pure.Literal(s: String) => JsString(escape(s)) - case literal: Pure.Literal => js.RawExpr(literal.value.toString) - case DirectApp(id, vargs, Nil) => inlineExtern(id, vargs) - case DirectApp(id, vargs, bargs) => js.Call(nameRef(id), vargs.map(toJS) ++ bargs.map(argumentToJS)) - case Pure.PureApp(id, vargs) => inlineExtern(id, vargs) - case Pure.Make(data, tag, vargs) => js.New(nameRef(tag), vargs map toJS) - case Pure.Box(b) => argumentToJS(b) - } - - def toJS(s: cps.Stmt)(using D: TransformerContext): Binding[List[js.Stmt]] = s match { - - case cps.Stmt.LetDef(id, block, body) => - Binding { k => - js.Const(nameDef(id), requiringThunk { toJS(id, block) }) :: toJS(body).run(k) - } - - case cps.Stmt.If(cond, thn, els) => - pure(js.If(toJS(cond), toJS(thn).block, toJS(els).block) :: Nil) - - case cps.Stmt.LetExpr(id, binding, body) => - Binding { k => - js.Const(nameDef(id), toJS(binding)) :: toJS(body).run(k) - } - - // [[ let k(x, ks) = ...; if (...) jump k(42, ks2) else jump k(10, ks3) ]] = - // let x; if (...) { x = 42; ks = ks2 } else { x = 10; ks = ks3 } ... - case cps.Stmt.LetCont(id, Cont.ContLam(params, ks, body), body2) if canBeDirect(id, body2) => - Binding { k => - params.map { p => js.Let(nameDef(p), js.Undefined) } ::: - toJS(body2)(using markDirectStyle(id, params, ks)).stmts ++ - toJS(maintainDirectStyle(ks, body)).run(k) - } - - case cps.Stmt.LetCont(id, binding @ Cont.ContLam(result2, ks2, body2), body) => - Binding { k => - js.Const(nameDef(id), toJS(binding)(using nonrecursive(ks2))) :: requiringThunk { toJS(body) }.run(k) - } - - case cps.Stmt.Match(sc, Nil, None) => - pure(js.Return($effekt.call("emptyMatch")) :: Nil) - - case cps.Stmt.Match(sc, List((tag, clause)), None) => - val scrutinee = toJS(sc) - val (_, stmts) = toJS(scrutinee, tag, clause) - stmts - - // (function () { switch (sc.tag) { case 0: return f17.apply(null, sc.data) } - case cps.Stmt.Match(sc, clauses, default) => - val scrutinee = toJS(sc) - - pure(js.Switch(js.Member(scrutinee, `tag`), - clauses.map { case (tag, clause) => - val (e, binding) = toJS(scrutinee, tag, clause); - - val stmts = binding.stmts - - stmts.lastOption match { - case Some(terminator : (js.Stmt.Return | js.Stmt.Break | js.Stmt.Continue)) => (e, stmts) - case other => (e, stmts :+ js.Break()) - } - }, - default.map { s => toJS(s).stmts }) :: Nil) - - case cps.Stmt.Jump(k, vargs, ks) if D.directStyle.exists(c => c.k == k) => D.directStyle match { - case Some(ContinuationInfo(k2, params2, ks2)) => - pure((params2 zip vargs).map { case (p, a) => js.Assign(nameRef(p), toJS(a)) }) - case None => sys error "Should not happen" - } - - case cps.Stmt.Jump(k, vargs, ks) => - pure(js.Return(maybeThunking(js.Call(nameRef(k), - vargs.map(toJS) ++ List(toJS(ks))))) :: Nil) - - - case cps.Stmt.App(Recursive(id, label, vparams, bparams, ks1, k1, used), vargs, bargs, MetaCont(ks), k) => - Binding { k2 => - val stmts = mutable.ListBuffer.empty[js.Stmt] - stmts.append(js.RawStmt("/* prepare call */")) - - used.jumped = true - - // We need to create temporaries for all free variables that appear in arguments - val freeInArgs = (vargs.flatMap(Variables.free) ++ bargs.flatMap(Variables.free)).toSet - // Only compute free vars of continuation if it's not a ContVar - val (isTailCall, freeInK) = k match { - case Cont.ContVar(kid) => (kid == k1 && ks == ks1, Set.empty[Id]) - case _ => (false, Variables.free(k)) - } - val allFreeVars = freeInArgs ++ freeInK - val needsKsTmp = allFreeVars.contains(ks1) - val overlapping = allFreeVars.intersect((vparams ++ bparams).toSet) - - // Create temporaries for parameters that are used in the arguments or continuation - val paramTmps = overlapping.map { param => - val tmp = Id(s"tmp_${param}") - stmts.append(js.Const(nameDef(tmp), nameRef(param))) - param -> tmp - }.toMap - - val tmp_ks = if (needsKsTmp) { - val tmp = Id("tmp_ks") - stmts.append(js.Const(nameDef(tmp), nameRef(ks1))) - Some(tmp) - } else None - - // For non-tail calls, we need a continuation temporary if it's not a simple variable rename - val tmp_k = if (!isTailCall) k match { - case Cont.ContVar(kid) if kid != k1 => - // simple continuation variable, no need for temp binding - None - case _ => - val tmp = Id("tmp_k") - stmts.append(js.Const(nameDef(tmp), nameRef(k1))) - Some(tmp) - } else { - // For tail calls, only create temporary if k1 appears in arguments - if (freeInArgs.contains(k1)) { - val tmp = Id("tmp_k") - stmts.append(js.Const(nameDef(tmp), nameRef(k1))) - Some(tmp) - } else None - } - - // Prepare the substitution - val subst = Substitution( - values = paramTmps.map { case (p, t) => p -> Pure.ValueVar(t) }, - blocks = Map.empty, - conts = tmp_k.map(t => k1 -> Cont.ContVar(t)).toMap, - metaconts = tmp_ks.map(t => ks1 -> MetaCont(t)).toMap - ) - - // Update the continuation if this is not a tail call - if (!isTailCall) k match { - case Cont.ContVar(kid) if kid != k1 => - // simple variable rename - stmts.append(js.Assign(nameRef(k1), nameRef(kid))) - case _ => - stmts.append(js.Assign(nameRef(k1), toJS(substitutions.substitute(k)(using subst)))) - } - - // Assign the substituted arguments - (vparams zip vargs).foreach { (param, arg) => - stmts.append(js.Assign(nameRef(param), toJS(substitutions.substitute(arg)(using subst)))) - } - (bparams zip bargs).foreach { (param, arg) => - stmts.append(js.Assign(nameRef(param), argumentToJS(substitutions.substitute(arg)(using subst)))) - } - - // Restore metacont if needed - if (needsKsTmp) stmts.append(js.Assign(nameRef(ks1), nameRef(tmp_ks.get))) - - val jump = js.Continue(Some(uniqueName(label))) - stmts.appendAll(k2(jump :: Nil)) - stmts.toList - } - - case cps.Stmt.App(callee, vargs, bargs, ks, k) => - pure(js.Return(js.Call(toJS(callee), vargs.map(toJS) ++ bargs.map(argumentToJS) ++ List(toJS(ks), - requiringThunk { toJS(k) }))) :: Nil) - - case cps.Stmt.Invoke(callee, method, vargs, bargs, ks, k) => - val args = vargs.map(toJS) ++ bargs.map(argumentToJS) ++ List(toJS(ks), toJS(k)) - pure(js.Return(MethodCall(toJS(callee), memberNameRef(method), args:_*)) :: Nil) - - // const r = ks.arena.newRegion(); body - case cps.Stmt.Region(id, ks, body) => - Binding { k => - js.Const(nameDef(id), js.MethodCall(js.Member(toJS(ks), JSName("arena")), JSName("newRegion"))) :: - toJS(body).run(k) - } - - // const x = r.alloc(init); body - case cps.Stmt.Alloc(id, init, region, body) => - Binding { k => - js.Const(nameDef(id), js.MethodCall(nameRef(region), JSName("fresh"), toJS(init))) :: - toJS(body).run(k) - } - - // const x = ks.arena.fresh(1); body - case cps.Stmt.Var(id, init, ks, body) => - Binding { k => - js.Const(nameDef(id), js.MethodCall(js.Member(toJS(ks), JSName("arena")), JSName("fresh"), toJS(init))) :: - toJS(body).run(k) - } - - // DEALLOC(ref); body - case cps.Stmt.Dealloc(ref, body) => - toJS(body) - - // const id = ref.value; body - case cps.Stmt.Get(ref, id, body) => - Binding { k => - js.Const(nameDef(id), js.Member(nameRef(ref), JSName("value"))) :: - toJS(body).run(k) - } - - // ref.set(value); body - case cps.Stmt.Put(ref, value, body) => Binding { k => - js.ExprStmt(js.MethodCall(nameRef(ref), JSName("set"), toJS(value))) :: - toJS(body).run(k) - } - - case cps.Stmt.Reset(prog, ks, k) => - pure(js.Return(Call(RESET, requiringThunk { toJS(prog)(using nonrecursive(prog)) }, toJS(ks), toJS(k))) :: Nil) - - case cps.Stmt.Shift(prompt, body, ks, k) => - pure(js.Return(Call(SHIFT, nameRef(prompt), requiringThunk { toJS(body)(using nonrecursive(body)) }, toJS(ks), toJS(k))) :: Nil) - - case cps.Stmt.Resume(r, b, ks2, k2) => - pure(js.Return(js.Call(RESUME, nameRef(r), toJS(b)(using nonrecursive(b)), toJS(ks2), requiringThunk { toJS(k2) })) :: Nil) - - case cps.Stmt.Hole() => - pure(js.Return($effekt.call("hole")) :: Nil) - } - - def toJS(scrutinee: js.Expr, variant: Id, clause: cps.Clause)(using C: TransformerContext): (js.Expr, Binding[List[js.Stmt]]) = - clause match { - case cps.Clause(vparams, body) => - val fields = C.declarations.getConstructor(variant).fields.map(_.id) - val tag = js.RawExpr(C.declarations.getConstructorTag(variant).toString) - - val freeVars = cps.Variables.free(body) - def isUsed(x: Id) = freeVars contains x - - val extractedFields = (vparams zip fields).collect { case (p, f) if isUsed(p) => - js.Const(nameDef(p), js.Member(scrutinee, memberNameRef(f))) - } - - (tag, Binding { k => extractedFields ++ toJS(body).run(k) }) - } - - def toJS(d: cps.Def)(using T: TransformerContext): js.Stmt = d match { - case cps.Def(id, block) => - js.Const(nameDef(id), requiringThunk { toJS(id, block) }) - } - - - // Inlining Externs - // ---------------- - - private def inlineExtern(id: Id, args: List[cps.Pure])(using T: TransformerContext): js.Expr = - T.externs.get(id) match { - case Some(cps.Extern.Def(id, params, Nil, async, - ExternBody.StringExternBody(featureFlag, Template(strings, templateArgs)))) if !async => - val subst = substitutions.Substitution( - values = (params zip args).toMap, - blocks = Map.empty, - conts = Map.empty, - metaconts = Map.empty - ) - - // Apply substitution to template arguments - val substitutedArgs = templateArgs.map { arg => - toJS(substitutions.substitute(arg)(using subst)) - } - - js.RawExpr(strings, substitutedArgs) - case _ => js.Call(nameRef(id), args.map(toJS)) - } - - private def canInline(extern: cps.Extern): Boolean = extern match { - case cps.Extern.Def(_, _, Nil, async, ExternBody.StringExternBody(_, Template(_, _))) => !async - case _ => false - } - - // Helpers for Direct-Style Transformation - // --------------------------------------- - - /** - * Marks continuation `id` to be optimized to direct assignments to `param` instead of return statements. - * This is only valid in the same metacontinuation scope `ks`. - */ - private def markDirectStyle(id: Id, params: List[Id], ks: Id)(using C: TransformerContext): TransformerContext = - C.copy(directStyle = Some(ContinuationInfo(id, params, ks))) - - private def recursive(id: Id, vparams: List[Id], bparams: List[Id], ks: Id, k: Id, label: Id, used: RecursiveUsage)(using C: TransformerContext): TransformerContext = - C.copy(recursive = Some(RecursiveDefInfo(id, label, vparams, bparams, ks, k, used)), directStyle = None, metacont = Some(ks)) - - private def nonrecursive(ks: Id)(using C: TransformerContext): TransformerContext = - C.copy(recursive = None, directStyle = None, metacont = Some(ks)) - - private def nonrecursive(block: cps.BlockLit)(using C: TransformerContext): TransformerContext = nonrecursive(block.ks) - - /** - * Ensures let-bound continuations can stay in direct style by aligning metacont scopes. - * This is used when the let-bound body jumps to an outer continuation. - * ks | let k1 x1 ks1 = { let k2 x2 ks2 = jump k v ks2 }; ... = jump k v ks - */ - private def maintainDirectStyle(ks: Id, body: Stmt)(using C: TransformerContext): Stmt = { - val outer = C.metacont.getOrElse { sys error "Metacontinuation missing..." } - substitutions.substitute(body)(using Substitution(metaconts = Map(ks -> MetaCont(outer)))) - } - - private object Recursive { - def unapply(b: cps.Block)(using C: TransformerContext): Option[(Id, Id, List[Id], List[Id], Id, Id, RecursiveUsage)] = b match { - case cps.Block.BlockVar(id) => C.recursive.collect { - case RecursiveDefInfo(id2, label, vparams, bparams, ks, k, used) if id == id2 => (id, label, vparams, bparams, ks, k, used) - } - case _ => None - } - } - - private def canBeDirect(k: Id, stmt: Stmt)(using T: TransformerContext): Boolean = - def notIn(term: Stmt | Block | Expr | (Id, Clause) | Cont) = - val freeVars = term match { - case s: Stmt => free(s) - case b: Block => free(b) - case p: Expr => free(p) - case (id, Clause(_, body)) => free(body) - case c: Cont => free(c) - } - !freeVars.contains(k) - stmt match { - case Stmt.Jump(k2, vargs, ks2) if k2 == k => vargs.forall(notIn) && T.metacont.contains(ks2.id) - case Stmt.Jump(k2, vargs, ks2) => vargs.forall(notIn) - // TODO this could be a tailcall! - case Stmt.App(callee, vargs, bargs, ks, k) => notIn(stmt) - case Stmt.Invoke(callee, method, vargs, bargs, ks, k2) => notIn(stmt) - case Stmt.If(cond, thn, els) => canBeDirect(k, thn) && canBeDirect(k, els) - case Stmt.Match(scrutinee, clauses, default) => clauses.forall { - case (id, Clause(vparams, body)) => canBeDirect(k, body) - } && default.forall(body => canBeDirect(k, body)) - case Stmt.LetDef(id, binding, body) => notIn(binding) && canBeDirect(k, body) - case Stmt.LetExpr(id, binding, body) => notIn(binding) && canBeDirect(k, body) - case Stmt.LetCont(id, Cont.ContLam(result, ks2, body), body2) => - def willBeDirectItself = canBeDirect(id, body2) && canBeDirect(k, maintainDirectStyle(ks2, body)) - def notFreeinContinuation = notIn(body) && canBeDirect(k, body2) - willBeDirectItself || notFreeinContinuation - case Stmt.Region(id, ks, body) => notIn(body) - case Stmt.Alloc(id, init, region, body) => notIn(init) && canBeDirect(k, body) - case Stmt.Var(id, init, ks2, body) => notIn(init) && canBeDirect(k, body) - case Stmt.Dealloc(ref, body) => canBeDirect(k, body) - case Stmt.Get(ref, id, body) => canBeDirect(k, body) - case Stmt.Put(ref, value, body) => notIn(value) && canBeDirect(k, body) - case Stmt.Reset(prog, ks, k) => notIn(stmt) - case Stmt.Shift(prompt, body, ks, k) => notIn(stmt) - case Stmt.Resume(resumption, body, ks, k) => notIn(stmt) - case Stmt.Hole() => true - } - - - - // Thunking - // -------- - - def thunked(stmt: js.Stmt): js.Stmt = js.Return(js.Lambda(Nil, stmt)) - def thunked(expr: js.Expr): js.Expr = js.Lambda(Nil, expr) - - def requiringThunk[T](prog: TransformerContext ?=> T)(using C: TransformerContext): T = - prog(using C.copy(requiresThunk = true)) - - def noThunking[T](prog: TransformerContext ?=> T)(using C: TransformerContext): T = - prog(using C.copy(requiresThunk = false)) - - def maybeThunking(stmt: js.Stmt)(using T: TransformerContext): js.Stmt = - if T.requiresThunk then thunked(stmt) else stmt - - def maybeThunking(expr: js.Expr)(using T: TransformerContext): js.Expr = - if T.requiresThunk then thunked(expr) else expr -} diff --git a/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala deleted file mode 100644 index a5ba7f216..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala +++ /dev/null @@ -1,262 +0,0 @@ -package effekt -package generator -package js - -import scala.collection.immutable.{ AbstractSeq, LinearSeq } - -// TODO choose appropriate representation and apply conversions -case class JSName(name: String) - -object $effekt { - val namespace = Variable(JSName("$effekt")) - def field(name: String): js.Expr = - js.Member(namespace, JSName(name)) - def call(name: String, args: js.Expr*): js.Expr = - js.MethodCall(namespace, JSName(name), args: _*) -} - -enum Import { - // import * as from ""; - case All(name: JSName, file: String) - - // import {, ...} from ""; - case Selective(members: List[JSName], file: String) -} - -case class Export(name: JSName, expr: Expr) - -case class Module(name: JSName, imports: List[Import], exports: List[Export], stmts: List[Stmt]) { - - /** - * Generates the Javascript module skeleton for whole program compilation - */ - def commonjs: List[Stmt] = { - val effekt = js.Const(JSName("$effekt"), js.Object()) - - val importStmts = imports.map { - // const MOD = require(PATH) - case Import.All(name, file) => - js.Const(name, js.Call(Variable(JSName("require")), List(JsString(s"./${file}")))) - - // const {NAMES, ...} = require(PATH) - case Import.Selective(names, file) => - js.Destruct(names, js.Call(Variable(JSName("require")), List(JsString(s"./${ file }")))) - } - - val exportStatement = js.Assign(RawExpr(s"(typeof module != \"undefined\" && module !== null ? module : {}).exports = ${name.name}"), - js.Object(exports.map { e => e.name -> e.expr }) - ) - - List(effekt) ++ importStmts ++ stmts ++ List(exportStatement) - } - - /** - * Generates the Javascript module skeleton for virtual modules that are compiled separately - * - * {{{ - * const $effekt = {} - * // ... contents of the module - * module.exports = { - * // EXPORTS... - * } - * }}} - */ - def virtual : List[Stmt] = { - val effekt = js.Const(JSName("$effekt"), js.Object()) - - val importStmts = imports.map { - // const MOD = load(PATH) - case Import.All(name, file) => - js.Const(name, js.Call(Variable(JSName("load")), List(JsString(file)))) - - // const {NAMES, ...} = load(PATH) - case Import.Selective(names, file) => - js.Destruct(names, js.Call(Variable(JSName("load")), List(JsString(file)))) - } - - val declaration = js.Const(name, js.Object()) - - // module.exports = { EXPORTS } - val exportStatement = js.Assign(RawExpr("module.exports"), js.Object(exports.map { e => e.name -> e.expr })) - List(effekt) ++ importStmts ++ List(declaration) ++ stmts ++ List(exportStatement) - } -} - - -/** - * This file defines the syntax of JavaScript as it is the image of our translation. - */ -enum Expr { - - // e.g. () - case Call(callee: Expr, arguments: List[Expr]) - - // e.g. new () - case New(callee: Expr, arguments: List[Expr]) - - // e.g. "" " + " - // raw JS splices, always start with a prefix string, then interleaved with arguments - case RawExpr(raw: List[String], args: List[Expr]) - - // e.g. 42 - // raw JS literal, already converted to a string. Similar to RawExpr but will not be parenthesized - case RawLiteral(raw: String) - - // e.g. ( ? : ) - case IfExpr(cond: Expr, thn: Expr, els: Expr) - - // e.g. (x, y) => - case Lambda(params: List[JSName], body: Stmt) - - // e.g. { x: , y: , ... } - case Object(properties: List[(JSName, Expr)]) - - // e.g. . - case Member(callee: Expr, selection: JSName) - - // e.g. [*] (we cannot call it "Array") - case ArrayLiteral(elements: List[Expr]) - - // e.g. x - case Variable(name: JSName) -} -export Expr.* - -def RawExpr(str: String): js.Expr = Expr.RawExpr(List(str), Nil) -def RawStmt(str: String): js.Stmt = Stmt.RawStmt(List(str), Nil) - -implicit class JavaScriptInterpolator(private val sc: StringContext) extends AnyVal { - def js(args: Expr*): Expr = RawExpr(sc.parts.toList, args.toList) -} - - -enum Pattern { - case Variable(name: JSName) - case Array(ps: List[Pattern]) -} - -enum Stmt { - // e.g. { * } - case Block(stmts: List[Stmt]) - - // e.g. return - case Return(expr: Expr) - - // A raw JS String - case RawStmt(raw: List[String], args: List[Expr]) - - // e.g. const x = - case Const(pattern: Pattern, binding: Expr) - - // e.g. let x = - case Let(pattern: Pattern, binding: Expr) - - // e.g. = - case Assign(target: Expr, value: Expr) - - // e.g. const {x,y,z} = - case Destruct(names: List[JSName], binding: Expr) - - // e.g. switch (sc) { case : ; ...; default: } - case Switch(scrutinee: Expr, branches: List[(Expr, List[Stmt])], default: Option[List[Stmt]]) // TODO maybe flatten? - - // e.g. function (x, y) { * } - case Function(name: JSName, params: List[JSName], stmts: List[Stmt]) - - // e.g. class { - // (x, y) { * }... - // } - case Class(name: JSName, methods: List[Stmt.Function]) - - // e.g. if () { } else { } - case If(cond: Expr, thn: Stmt, els: Stmt) - - // e.g. try { * } catch(x) { * } - case Try(prog: List[Stmt], name: JSName, handler: List[Stmt], fin: List[Stmt] = Nil) - - // e.g. throw e - case Throw(expr: Expr) - - // label : while () { * } - case While(cond: Expr, stmts: List[Stmt], label: Option[JSName]) - - // e.g. break - case Break() - - // e.g. continue l - case Continue(label: Option[JSName]) - - // e.g. ; - case ExprStmt(expr: Expr) -} -export Stmt.* - - -// Smart constructors -// ------------------ - -def Const(name: JSName, binding: Expr): Stmt = binding match { - case Expr.Lambda(params, Block(stmts)) => js.Function(name, params, stmts) - case Expr.Lambda(params, stmt) => js.Function(name, params, List(stmt)) - case _ => js.Const(Pattern.Variable(name), binding) -} - -def Let(name: JSName, binding: Expr): Stmt = js.Let(Pattern.Variable(name), binding) - -def Let(names: List[JSName], binding: Expr): Stmt = names match { - case Nil => ExprStmt(binding) - case name :: Nil => js.Let(Pattern.Variable(name), binding) - case names => js.Let(Pattern.Array(names.map(name => Pattern.Variable(name))), binding) -} - - -def Call(receiver: Expr, args: Expr*): Expr = Call(receiver, args.toList) - -def MethodCall(receiver: Expr, method: JSName, args: Expr*): Expr = Call(Member(receiver, method), args.toList) - -def Lambda(params: List[JSName], body: Expr): Expr = Lambda(params, Return(body)) - -def JsString(scalaString: String): Expr = RawLiteral(s"\"${scalaString}\"") - -def Object(properties: (JSName, Expr)*): Expr = Object(properties.toList) - -def MaybeBlock(stmts: List[Stmt]): Stmt = stmts match { - case head :: Nil => head - case _ => js.Block(stmts) -} - -val Undefined = RawLiteral("undefined") - -def Lambda(params: List[JSName], stmts: List[Stmt]): Expr = stmts match { - case Nil => sys error "Lambda should have at least one statement as body" - case js.Return(e) :: Nil => Lambda(params, e) - case stmt :: Nil => Lambda(params, stmt) - case stmts => Lambda(params, Block(stmts)) -} - -// Code generation monad -// --------------------- - -case class Binding[A](run: (A => List[js.Stmt]) => List[js.Stmt]) { - def flatMap[B](rest: A => Binding[B]): Binding[B] = { - Binding(k => run(a => rest(a).run(k))) - } - def map[B](f: A => B): Binding[B] = flatMap { a => pure(f(a)) } -} -extension (b: Binding[List[js.Stmt]]) { - def block: js.Stmt = js.MaybeBlock(b.stmts) - def toExpr: js.Expr = b.stmts match { - case Nil => ??? - case js.Return(e) :: Nil => e - case stmts => js.Call(js.Lambda(Nil, Block(stmts)), Nil) - } - def stmts: List[js.Stmt] = b.run(x => x) -} - -def traverse[S, T](l: List[S])(f: S => Binding[T]): Binding[List[T]] = - l match { - case Nil => pure(Nil) - case head :: tail => for { x <- f(head); xs <- traverse(tail)(f) } yield x :: xs - } - -def pure[A](a: A): Binding[A] = Binding(k => k(a)) diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/LLVM.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/LLVM.scala deleted file mode 100644 index 9079f71cd..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/LLVM.scala +++ /dev/null @@ -1,68 +0,0 @@ -package effekt -package generator -package llvm - -import effekt.context.Context -import effekt.core.optimizer -import effekt.machine -import kiama.output.PrettyPrinterTypes.{ Document, emptyLinks } -import kiama.util.Source - - -class LLVM extends Compiler[String] { - - // Implementation of the Compiler Interface: - // ----------------------------------------- - def extension = ".ll" - - override def supportedFeatureFlags: List[String] = Transformer.llvmFeatureFlags - - override def prettyIR(source: Source, stage: Stage)(using Context): Option[Document] = stage match { - case Stage.Core => steps.afterCore(source).map { res => core.PrettyPrinter.format(res.core) } - case Stage.Machine => steps.afterMachine(source).map { res => machine.PrettyPrinter.format(res.program) } - case Stage.Target => steps.afterLLVM(source).map { res => pretty(res) } - } - - override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = stage match { - case Stage.Core => steps.afterCore(source).map { res => res.core } - case Stage.Machine => steps.afterMachine(source).map { res => res.program } - case Stage.Target => steps.afterLLVM(source) - } - - override def compile(source: Source)(using C: Context) = - Compile(source) map { (mod, defs) => - val mainFile = path(mod) - (Map(mainFile -> pretty(defs).layout), mainFile) - } - - - // The Compilation Pipeline - // ------------------------ - // Source => Core => Machine => LLVM - lazy val Compile = allToCore(Core) andThen Aggregate andThen core.PolymorphismBoxing andThen optimizer.Optimizer andThen Machine map { - case (mod, main, prog) => (mod, llvm.Transformer.transform(prog)) - } - - lazy val Core = Phase.cached("core") { - Frontend andThen Middleend - } - - - // The Compilation Pipeline for VSCode - // ----------------------------------- - object steps { - // intermediate steps for VSCode - val afterCore = allToCore(Core) andThen Aggregate andThen core.PolymorphismBoxing andThen optimizer.Optimizer - val afterMachine = afterCore andThen Machine map { case (mod, main, prog) => prog } - val afterLLVM = afterMachine map { - case machine.Program(decls, defns, entry) => - // we don't print declarations here. - llvm.Transformer.transform(machine.Program(Nil, defns, entry)) - } - } - - // Helpers - // ------- - private def pretty(defs: List[llvm.Definition])(using Context): Document = - Document(llvm.PrettyPrinter.show(defs), emptyLinks) -} diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala deleted file mode 100644 index 68910ee20..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala +++ /dev/null @@ -1,169 +0,0 @@ -package effekt -package generator -package llvm - -import effekt.context.Context - -object PrettyPrinter { - - type LLVMString = String - - def show(definitions: List[Definition])(using Context): LLVMString = - definitions.map(show).mkString("\n\n") - - def show(definition: Definition)(using C: Context): LLVMString = definition match { - case Function(linkage, callingConvention, returnType, name, parameters, basicBlocks) => - s""" -define ${show(linkage)} ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) { - ${indentedLines(basicBlocks.map(show).mkString)} -} -""" - case VerbatimFunction(callingConvention, returnType, name, parameters, body) => - s""" -define ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) { - $body -} -""" - case Verbatim(content) => content - - case GlobalConstant(name, ConstantArray(IntegerType8(), members)) => - val bytes = members.map { ini => ini match { - case ConstantInteger8(b) => b - case _ => ??? - }} - val escaped = bytes.map(b => "\\" + f"$b%02x").mkString; - s"@$name = private constant [${bytes.length} x i8] c\"$escaped\"" - - case GlobalConstant(name, initializer) => - s"@$name = private constant ${show(initializer)}" - } - - def show(linkage: Linkage): LLVMString = linkage match { - case External() => "external" - case Private() => "private" - } - - def show(callingConvention: CallingConvention): LLVMString = callingConvention match { - case Ccc() => "ccc" - case Tailcc(_) => "tailcc" - } - - def show(basicBlock: BasicBlock)(using Context): LLVMString = basicBlock match { - case BasicBlock(name, instructions, terminator) => - s""" -${name}: -${indentedLines(instructions.map(show).mkString("\n"))} - ${show(terminator)} -""" - } - - def show(instruction: Instruction)(using C: Context): LLVMString = instruction match { - - case Call(_, Ccc(), VoidType(), ConstantGlobal(name), arguments) => - s"call ccc void ${globalName(name)}(${commaSeparated(arguments.map(show))})" - case Call(result, Ccc(), tpe, ConstantGlobal(name), arguments) => - s"${localName(result)} = call ccc ${show(tpe)} ${globalName(name)}(${commaSeparated(arguments.map(show))})" - case Call(_, Ccc(), VoidType(), LocalReference(_, name), arguments) => - s"call ccc void ${localName(name)}(${commaSeparated(arguments.map(show))})" - case Call(_, Ccc(), _, nonglobal, _) => - C.abort(s"cannot call non-global operand: $nonglobal") // why not? - case Call(_, Tailcc(false), VoidType(), ConstantGlobal(name), arguments) => - s"call tailcc void ${globalName(name)}(${commaSeparated(arguments.map(show))})" - case Call(_, Tailcc(false), VoidType(), LocalReference(_, name), arguments) => - s"call tailcc void ${localName(name)}(${commaSeparated(arguments.map(show))})" - case Call(result, Tailcc(true), resultType, function, arguments) => - s"musttail ${show(Call(result, Tailcc(false), resultType, function, arguments))}" - case Call(_, Tailcc(_), tpe, _, _) => - C.abort(s"tail call to non-void function returning: $tpe") - - case Load(result, tpe, LocalReference(PointerType(), name), alias) => - s"${localName(result)} = load ${show(tpe)}, ${show(LocalReference(PointerType(), name))}, !noalias ${alias.noalias}, !alias.scope ${alias.scope}" - case Load(_, _, operand, _) => C.abort(s"WIP: loading anything but local references not yet implemented: $operand") - - // TODO [jfrech, 2022-07-26] Why does `Load` explicitly check for a local reference and `Store` does not? - case Store(address, value, alias) => - s"store ${show(value)}, ${show(address)}, !noalias ${alias.noalias}, !alias.scope ${alias.scope}" - - case GetElementPtr(result, tpe, ptr @ LocalReference(_, name), i :: is) => - s"${localName(result)} = getelementptr ${show(tpe)}, ${show(ptr)}, i64 $i" + is.map(", i32 " + _).mkString - case GetElementPtr(_, _, operand, _) => C.abort(s"can only form a pointer to a local reference, not: $operand") - - case BitCast(result, operand, tpe) => - s"${localName(result)} = bitcast ${show(operand)} to ${show(tpe)}" - - case Add(result, operand0, ConstantInt(n)) => - s"${localName(result)} = add ${show(operand0)}, $n" - case Add(result, LocalReference(t1, n1), LocalReference(t2, n2)) => - assert(t1 == t2) - s"${localName(result)} = add ${show(t1)} ${localName(n1)}, ${localName(n2)}" - case Add(_, _, operand1) => - C.abort(s"WIP: currently only right-constant additions are supported, not: $operand1") - - case FAdd(result, operand0, ConstantDouble(x)) => - s"${localName(result)} = fadd ${show(operand0)}, $x" - case FAdd(_, _, operand1) => - C.abort(s"WIP: currently only right-constant floating-point additions are supported, not: $operand1") - - case InsertValue(result, aggregate, element, index) => - s"${localName(result)} = insertvalue ${show(aggregate)}, ${show(element)}, $index" - - case ExtractValue(result, aggregate, index) => - s"${localName(result)} = extractvalue ${show(aggregate)}, $index" - - case Comment(msg) if C.config.debug() => - val sanitized = msg.map((c: Char) => if (' ' <= c && c != '\\' && c <= '~') c else '?').mkString - s"\n; $sanitized" - - case Comment(msg) => "" - } - - def show(terminator: Terminator): LLVMString = terminator match { - case RetVoid() => - s"ret void" - case Switch(operand, defaultDest, dests) => - def destAsFragment(dest: (Int, String)) = s"i64 ${dest._1}, label ${localName(dest._2)}"; - s"switch ${show(operand)}, label ${localName(defaultDest)} [${spaceSeparated(dests.map(destAsFragment))}]" - case CondBr(condition, trueDest, falseDest) => - s"br ${show(condition)}, label ${localName(trueDest)}, label ${localName(falseDest)}" - } - - def show(operand: Operand): LLVMString = operand match { - case LocalReference(tpe, name) => s"${show(tpe)} ${localName(name)}" - case ConstantGlobal(name) => s"ptr ${globalName(name)}" - case ConstantInt(n) => s"i64 $n" - case ConstantDouble(n) => s"double $n" - case ConstantAggregateZero(tpe) => s"${show(tpe)} zeroinitializer" - case ConstantNull(tpe) => s"${show(tpe)} null" - case ConstantArray(memberType, members) => s"[${members.length} x ${show(memberType)}] [${commaSeparated(members.map(show))}]" - case ConstantInteger8(b) => s"i8 $b" - } - - def show(tpe: Type): LLVMString = tpe match { - case VoidType() => "void" - case IntegerType1() => "i1" - case IntegerType8() => "i8" - case IntegerType64() => "i64" - case DoubleType() => "double" - case PointerType() => "ptr" - case ArrayType(size, of) => s"[$size x ${show(of)}]" - case StructureType(elementTypes) => s"<{${commaSeparated(elementTypes.map(show))}}>" - case FunctionType(returnType, argumentTypes) => s"${show(returnType)} (${commaSeparated(argumentTypes.map(show))})" - case NamedType(name) => localName(name) - } - - def show(parameter: Parameter): LLVMString = parameter match { - case Parameter(tpe, name) => s"${show(tpe)} ${localName(name)}" - } - - def sanitize(name: String): String = name.replace("?", "Q").replace("!", "B") - - def localName(name: String): LLVMString = "%" + sanitize(name) - def globalName(name: String): LLVMString = "@" + sanitize(name) - - // indent all lines with four spaces - def indentedLines(text: String): String = text.split("\n").map(" " + _).mkString("\n") - - def commaSeparated(args: List[String]): String = args.mkString(", ") - - def spaceSeparated(args: List[String]): String = args.mkString(" ") -} diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala deleted file mode 100644 index ded566b62..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ /dev/null @@ -1,779 +0,0 @@ -package effekt -package generator -package llvm - -import effekt.machine -import effekt.util.intercalate -import effekt.util.messages.ErrorReporter -import effekt.machine.analysis.* - -import scala.annotation.tailrec -import scala.collection.mutable - -object Transformer { - - val llvmFeatureFlags: List[String] = List("llvm") - - def transform(program: machine.Program)(using ErrorReporter): List[Definition] = program match { - case machine.Program(declarations, definitions, entry) => - - given MC: ModuleContext = ModuleContext(); - definitions.foreach(transform); - - val globals = MC.definitions; MC.definitions = null; - - val entryInstructions = List( - Call("stack", Ccc(), stackType, withEmptyStack, List()), - Call("_", Tailcc(false), VoidType(), transform(entry), List(LocalReference(stackType, "stack")))) - val entryBlock = BasicBlock("entry", entryInstructions, RetVoid()) - val entryFunction = Function(External(), Ccc(), VoidType(), "effektMain", List(), List(entryBlock)) - - declarations.map(transform) ++ globals :+ entryFunction - } - - // context getters - private def MC(using MC: ModuleContext): ModuleContext = MC - private def FC(using FC: FunctionContext): FunctionContext = FC - private def BC(using BC: BlockContext): BlockContext = BC - - def transform(declaration: machine.Declaration)(using ErrorReporter): Definition = - declaration match { - case machine.Extern(functionName, parameters, returnType, async, body) => - val transformedParameters = parameters.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - if (async) { - VerbatimFunction(Tailcc(true), VoidType(), functionName, transformedParameters :+ Parameter(stackType, "stack"), transform(body)) - } else { - VerbatimFunction(Ccc(), transform(returnType), functionName, transformedParameters, transform(body)) - } - case machine.Include(ff, content) => - Verbatim("; declaration include" ++ content) - } - - def transform(body: machine.ExternBody)(using ErrorReporter): String = body match { - case machine.ExternBody.StringExternBody(_, contents) => - "; declaration extern\n " ++ transform(contents) - case u: machine.ExternBody.Unsupported => - u.report - """call void @hole() - |unreachable - |""".stripMargin - } - - def transform(template: Template[machine.Variable]): String = "; variable\n " ++ intercalate(template.strings, template.args.map { - case machine.Variable(name, tpe) => PrettyPrinter.localName(name) - }).mkString - - def transform(definition: machine.Definition)(using ModuleContext): Unit = definition match { - case machine.Definition(machine.Label(name, environment), body) => - val parameters = environment.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - defineLabel(name, parameters) { - emit(Comment(s"definition $name, environment length ${environment.length}")) - eraseValues(environment, freeVariables(body)) - transform(body) - } - } - - def transform(statement: machine.Statement)(using ModuleContext, FunctionContext, BlockContext): Terminator = - statement match { - - case machine.Jump(label) => - emit(Comment(s"jump ${label.name}")) - shareValues(label.environment, Set()) - - val arguments = label.environment.map(transform) - emit(callLabel(transform(label), arguments)) - RetVoid() - - case machine.Substitute(bindings, rest) => - emit(Comment("substitution")) - bindings.foreach { (from, to) => emit(Comment(s"substitution [${from.name} !-> ${to.name}]")) } - withBindings(bindings) { () => - transform(rest) - } - - case machine.Construct(variable, tag, values, rest) => - emit(Comment(s"construct ${variable.name}, tag ${tag}, ${values.length} values")) - val fields = produceObject("fields", values, freeVariables(rest)) - val temporaryName = freshName(variable.name + "_temporary") - emit(InsertValue(temporaryName, ConstantAggregateZero(positiveType), ConstantInt(tag), 0)) - emit(InsertValue(variable.name, LocalReference(positiveType, temporaryName), fields, 1)) - - eraseValues(List(variable), freeVariables(rest)) - transform(rest) - - case machine.Switch(value, clauses, default) => - emit(Comment(s"switch ${value.name}, ${clauses.length} clauses")) - val freeInClauses = clauses.flatMap(freeVariables).toSet ++ default.map(freeVariables).getOrElse(Set.empty) - shareValues(List(value), freeInClauses) - - val tagName = freshName("tag") - val objectName = freshName("fields") - emit(ExtractValue(tagName, transform(value), 0)) - emit(ExtractValue(objectName, transform(value), 1)) - - val stack = getStack() - def labelClause(clause: machine.Clause, isDefault: Boolean): String = { - implicit val BC = BlockContext() - BC.stack = stack - - consumeObject(LocalReference(objectType, objectName), clause.parameters, freeVariables(clause.body)); - eraseValues(freeInClauses.toList, freeVariables(clause)); - if (isDefault) eraseValue(value) - - val terminator = transform(clause.body); - - val instructions = BC.instructions; - BC.instructions = null; - - val label = freshName("label"); - emit(BasicBlock(label, instructions, terminator)) - label - } - - val defaultLabel = default match { - case Some(clause) => labelClause(clause, isDefault = true) - case None => - val label = freshName("label"); - emit(BasicBlock(label, List(), RetVoid())) - label - } - - val labels = clauses.map { - case (tag, clause) => (tag, labelClause(clause, isDefault = false)) - } - - Switch(LocalReference(IntegerType64(), tagName), defaultLabel, labels) - - case machine.New(variable, clauses, rest) => - val closureEnvironment = freeVariables(clauses).toList; - emit(Comment(s"new ${variable.name}, ${clauses.length} clauses, ${closureEnvironment.size} free variables")) - - val clauseNames = clauses.map { clause => - val clauseName = freshName(variable.name + "_clause"); - val parameters = clause.parameters.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - defineLabel(clauseName, Parameter(objectType, "closure") +: parameters) { - emit(Comment(s"new ${clauseName}, ${clause.parameters.length} parameters")) - consumeObject(LocalReference(objectType, "closure"), closureEnvironment, freeVariables(clause)); - eraseValues(clause.parameters, freeVariables(clause.body)); - transform(clause.body); - } - ConstantGlobal(clauseName) - } - - val vtableName = freshName("vtable") - emit(GlobalConstant(vtableName, ConstantArray(methodType, clauseNames))) - - val vtable = produceObject("closure", closureEnvironment, freeVariables(rest)); - val temporaryName = freshName("vtable_temporary"); - emit(InsertValue(temporaryName, ConstantAggregateZero(negativeType), ConstantGlobal(vtableName), 0)); - emit(InsertValue(variable.name, LocalReference(negativeType, temporaryName), vtable, 1)); - - eraseValues(List(variable), freeVariables(rest)); - transform(rest) - - case machine.Invoke(value, tag, values) => - emit(Comment(s"invoke ${value.name}, tag ${tag}, ${values.length} values")) - shareValues(value :: values, Set()); - - val vtableName = freshName("vtable"); - val objectName = freshName("closure"); - val pointerName = freshName("functionPointer_pointer"); - val functionName = freshName("functionPointer"); - val arguments = values.map(transform) - - emit(ExtractValue(vtableName, transform(value), 0)); - emit(ExtractValue(objectName, transform(value), 1)); - emit(GetElementPtr(pointerName, methodType, LocalReference(PointerType(), vtableName), List(tag))) - emit(Load(functionName, methodType, LocalReference(PointerType(), pointerName), VTable)) - emit(callLabel(LocalReference(methodType, functionName), LocalReference(objectType, objectName) +: arguments)) - RetVoid() - - case machine.Var(ref @ machine.Variable(name, machine.Type.Reference(tpe)), init, retType, rest) => - val environment = List(init) - val returnAddressName = freshName("returnAddress") - val returnType = transform(retType) - val returnValue = freshName("returnValue") - val parameters = List(Parameter(returnType, returnValue)) - defineLabel(returnAddressName, parameters) { - emit(Comment(s"var $name / return address")) - popEnvironmentFrom(getStack(), environment) - eraseValue(init) - val nextReturn = LocalReference(returnAddressType, freshName("returnAddress")) - popReturnAddressFrom(getStack(), nextReturn.name) - emit(callLabel(nextReturn, List(LocalReference(returnType, returnValue)))) - RetVoid() - } - - val sharer = getSharer(environment, StackFrameSharer) - val eraser = getEraser(environment, StackFrameEraser) - - emit(Call(name, Ccc(), referenceType, newReference, List(getStack()))) - - shareValues(environment, freeVariables(rest)); - pushFrameOnto(getStack(), environment, returnAddressName, sharer, eraser); - - transform(rest) - - case machine.Var(_, _, _, _) => ??? - - case machine.LoadVar(name, ref, rest) => - emit(Comment(s"loadvar ${name.name}, reference ${ref.name}")) - - val ptr = freshName(name.name + "_pointer"); - val ptrRef = LocalReference(PointerType(), ptr) - emit(Call(ptr, Ccc(), PointerType(), getVarPointer, List(transform(ref), getStack()))) - - // TODO why do we need this? - val oldVal = machine.Variable(freshName(ref.name + "_old"), name.tpe) - emit(Load(oldVal.name, transform(oldVal.tpe), ptrRef, StackPointer)) - shareValue(oldVal) - - emit(Load(name.name, transform(name.tpe), ptrRef, StackPointer)) - eraseValues(List(name), freeVariables(rest)) - transform(rest) - - case machine.StoreVar(ref, value, rest) => - emit(Comment(s"storevar ${ref.name}, value ${value.name}")) - - val ptr = freshName(ref.name + "pointer"); - val ptrRef = LocalReference(PointerType(), ptr) - emit(Call(ptr, Ccc(), PointerType(), getVarPointer, List(transform(ref), getStack()))) - - val oldVal = machine.Variable(freshName(ref.name + "_old"), value.tpe) - emit(Load(oldVal.name, transform(oldVal.tpe), ptrRef, StackPointer)) - eraseValue(oldVal) - - emit(Store(ptrRef, transform(value), StackPointer)) - shareValues(List(value), freeVariables(rest)) - transform(rest) - - case machine.PushFrame(frame, rest) => - val frameEnvironment = freeVariables(frame).toList; - - val returnAddressName = freshName("returnAddress"); - val parameters = frame.parameters.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - defineLabel(returnAddressName, parameters) { - emit(Comment(s"pushFrame / return address, ${frameEnvironment.length} free variables")) - emit(Call("", Ccc(), VoidType(), ConstantGlobal("assumeFrameHeaderWasPopped"), List(getStack()))) - popEnvironmentFrom(getStack(), frameEnvironment); - // eraseValues(frameEnvironment, frameEnvironment) (unnecessary) - eraseValues(frame.parameters, freeVariables(frame.body)) - - transform(frame.body); - } - - val sharer = getSharer(frameEnvironment, StackFrameSharer) - val eraser = getEraser(frameEnvironment, StackFrameEraser) - - shareValues(frameEnvironment, freeVariables(rest)); - pushFrameOnto(getStack(), frameEnvironment, returnAddressName, sharer, eraser); - - transform(rest) - - case machine.Return(values) => - emit(Comment(s"return, ${values.length} values")) - shareValues(values, Set()) - - val returnAddress = LocalReference(returnAddressType, freshName("returnAddress")); - popReturnAddressFrom(getStack(), returnAddress.name); - emit(callLabel(returnAddress, values.map(transform))) - RetVoid() - - case machine.Reset(prompt, frame, rest) => - emit(Comment(s"Reset ${prompt.name}")) - - val newStack = LocalReference(stackType, freshName("stack")) - emit(Call(newStack.name, Ccc(), stackType, reset, List(getStack()))); - setStack(newStack) - - emit(Call(prompt.name, Ccc(), promptType, currentPrompt, List(getStack()))) - - val frameEnvironment = freeVariables(frame).toList; - - val returnAddressName = freshName("returnAddress"); - val parameters = frame.parameters.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - defineLabel(returnAddressName, parameters) { - emit(Comment(s"Reset / return address, ${frameEnvironment.length} free variables")) - popEnvironmentFrom(getStack(), frameEnvironment); - // eraseValues(frameEnvironment, frameEnvironment) (unnecessary) - eraseValues(frame.parameters, freeVariables(frame.body)); - - val nextStack = LocalReference(stackType, freshName("stack")); - emit(Call(nextStack.name, Ccc(), nextStack.tpe, underflowStack, List(getStack()))); - setStack(nextStack); - - transform(frame.body); - } - - val sharer = getSharer(frameEnvironment, StackSharer) - val eraser = getEraser(frameEnvironment, StackEraser) - - shareValues(frameEnvironment, freeVariables(rest)); - - pushFrameOnto(getStack(), frameEnvironment, returnAddressName, sharer, eraser); - - transform(rest) - - case machine.Resume(value, rest) => - emit(Comment(s"Resume ${value.name}")) - shareValues(List(value), freeVariables(rest)); - val newStackName = freshName("stack"); - emit(Call(newStackName, Ccc(), stackType, resume, List(transform(value), getStack()))); - setStack(LocalReference(stackType, newStackName)); - transform(rest) - - case machine.Shift(variable, prompt, rest) => - emit(Comment(s"Shift ${variable.name}, prompt=${prompt.name}")) - val pair = LocalReference(StructureType(List(resumptionType, stackType)), freshName("pair")); - emit(Call(pair.name, Ccc(), pair.tpe, shift, List(getStack(), transform(prompt)))); - - emit(ExtractValue(variable.name, pair, 0)) - - val newStack = LocalReference(stackType, freshName("stack")); - emit(ExtractValue(newStack.name, pair, 1)) - setStack(newStack); - - eraseValues(List(variable), freeVariables(rest)); - transform(rest) - - case machine.LiteralInt(machine.Variable(name, _), n, rest) => - emit(Comment(s"literalInt $name, n=$n")) - emit(Add(name, ConstantInt(n), ConstantInt(0))); - transform(rest) - - case machine.LiteralDouble(machine.Variable(name, _), x, rest) => - emit(Comment(s"literalDouble $name, x=$x")) - emit(FAdd(name, ConstantDouble(x), ConstantDouble(0))); - transform(rest) - - case machine.LiteralUTF8String(v@machine.Variable(bind, _), utf8, rest) => - emit(Comment(s"literalUTF8String $bind, ${utf8.length} bytes")) - emit(GlobalConstant(s"$bind.lit", ConstantArray(IntegerType8(), utf8.map { b => ConstantInteger8(b) }.toList))) - - val res = positiveType - val args = List(ConstantInt(utf8.size), ConstantGlobal(s"$bind.lit")) - val argsT = List(IntegerType64(), PointerType()) - emit(Call(bind, Ccc(), res, ConstantGlobal("c_bytearray_construct"), args)) - - eraseValues(List(v), freeVariables(rest)); - transform(rest) - - case machine.ForeignCall(variable @ machine.Variable(resultName, resultType), foreign, values, rest) => - emit(Comment(s"foreignCall $resultName : $resultType, foreign $foreign, ${values.length} values")) - val functionType = PointerType(); - shareValues(values, freeVariables(rest)); - emit(Call(resultName, Ccc(), transform(resultType), ConstantGlobal(foreign), values.map(transform))); - eraseValues(List(variable), freeVariables(rest)) - transform(rest) - - case machine.Statement.Hole => - emit(Comment("Hole")) - emit(Call("_", Ccc(), VoidType(), ConstantGlobal("hole"), List.empty)) - RetVoid() - } - - def transform(label: machine.Label): ConstantGlobal = - label match { - case machine.Label(name, _) => ConstantGlobal(name) - } - - def transform(value: machine.Variable)(using FunctionContext): Operand = - substitute(value) match { - case machine.Variable(name, tpe) => LocalReference(transform(tpe), name) - } - - val positiveType = NamedType("Pos"); - // TODO multiple methods (should be pointer to vtable) - val negativeType = NamedType("Neg"); - val methodType = PointerType(); - val returnAddressType = NamedType("ReturnAddress"); - val sharerType = NamedType("Sharer"); - val eraserType = NamedType("Eraser"); - val frameHeaderType = NamedType("FrameHeader"); - val environmentType = NamedType("Environment"); - val objectType = NamedType("Object"); - val stackPointerType = NamedType("StackPointer"); - val stackType = NamedType("Stack"); - val resumptionType = NamedType("Resumption"); - val promptType = NamedType("Prompt"); - val referenceType = NamedType("Reference"); - - def transform(tpe: machine.Type): Type = tpe match { - case machine.Positive() => positiveType - case machine.Negative() => negativeType - case machine.Type.Prompt() => promptType - case machine.Type.Stack() => resumptionType - case machine.Type.Int() => IntegerType64() - case machine.Type.Byte() => IntegerType8() - case machine.Type.Double() => DoubleType() - case machine.Type.Reference(tpe) => referenceType - } - - def environmentSize(environment: machine.Environment): Int = - environment.map { case machine.Variable(_, typ) => typeSize(typ) }.sum - - def typeSize(tpe: machine.Type): Int = - tpe match { - case machine.Positive() => 16 - case machine.Negative() => 16 - case machine.Type.Prompt() => 8 // TODO Make fat? - case machine.Type.Stack() => 8 // TODO Make fat? - case machine.Type.Int() => 8 // TODO Make fat? - case machine.Type.Byte() => 1 - case machine.Type.Double() => 8 // TODO Make fat? - case machine.Type.Reference(_) => 16 - } - - def defineFunction(name: String, parameters: List[Parameter])(prog: (FunctionContext, BlockContext) ?=> Terminator): ModuleContext ?=> Unit = { - implicit val FC = FunctionContext(); - implicit val BC = BlockContext(); - - val terminator = prog; - - val basicBlocks = FC.basicBlocks; FC.basicBlocks = null; - val instructions = BC.instructions; BC.instructions = null; - - val entryBlock = BasicBlock("entry", instructions, terminator); - val function = Function(Private(), Ccc(), VoidType(), name, parameters, entryBlock :: basicBlocks); - - emit(function) - } - - def defineLabel(name: String, parameters: List[Parameter])(prog: (FunctionContext, BlockContext) ?=> Terminator): ModuleContext ?=> Unit = { - implicit val FC = FunctionContext(); - implicit val BC = BlockContext(); - - val terminator = prog; - - val basicBlocks = FC.basicBlocks; FC.basicBlocks = null; - val instructions = BC.instructions; BC.instructions = null; - - val entryBlock = BasicBlock("entry", instructions, terminator); - val function = Function(Private(), Tailcc(true), VoidType(), name, parameters :+ Parameter(stackType, "stack"), entryBlock :: basicBlocks); - - emit(function) - } - - def callLabel(name: Operand, arguments: List[Operand])(using BlockContext): Instruction = - Call("_", Tailcc(true), VoidType(), name, arguments :+ getStack()) - - def callLabelTransition(name: Operand, arguments: List[Operand])(using BlockContext): Instruction = - Call("_", Tailcc(false), VoidType(), name, arguments :+ getStack()) - - def initialEnvironmentPointer = LocalReference(environmentType, "environment") - - def loadEnvironment(environmentPointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - loadEnvironmentAt(environmentPointer, environment, alias); - } - } - - def storeEnvironment(environmentPointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - storeEnvironmentAt(environmentPointer, environment, alias); - } - } - - def getEraser(environment: machine.Environment, kind: EraserKind)(using C: ModuleContext): Operand = { - val types = environment.map{ _.tpe }; - val freshEnvironment = environment.map{ - case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) - }; - - C.erasers.getOrElseUpdate((types, kind), { - kind match { - case ObjectEraser => - val eraser = ConstantGlobal(freshName("eraser")); - defineFunction(eraser.name, List(Parameter(environmentType, "environment"))) { - emit(Comment(s"${kind} eraser, ${freshEnvironment.length} free variables")) - - // TODO avoid unnecessary loads - loadEnvironmentAt(LocalReference(environmentType, "environment"), freshEnvironment, Object); - eraseValues(freshEnvironment, Set()); - RetVoid() - }; - eraser - case StackEraser | StackFrameEraser => - val eraser = ConstantGlobal(freshName("eraser")); - defineFunction(eraser.name, List(Parameter(stackPointerType, "stackPointer"))) { - emit(Comment(s"${kind} eraser, ${freshEnvironment.length} free variables")) - - val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); - loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer); - - eraseValues(freshEnvironment, Set()); - val next = if (kind == StackEraser) freeStack else eraseFrames // TODO: improve this (in RTS?) - emit(Call("_", Ccc(), VoidType(), next, List(nextStackPointer))); - RetVoid() - }; - eraser - } - }) - } - - def getSharer(environment: machine.Environment, kind: SharerKind)(using C: ModuleContext): Operand = { - val types = environment.map{ _.tpe }; - val freshEnvironment = environment.map{ - case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) - }; - - C.sharers.getOrElseUpdate((types, kind), { - val sharer = ConstantGlobal(freshName("sharer")); - defineFunction(sharer.name, List(Parameter(stackPointerType, "stackPointer"))) { - emit(Comment(s"${kind} sharer, ${freshEnvironment.length} free variables")) - - val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); - loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer); - - shareValues(freshEnvironment, Set.from(freshEnvironment)); - - if (kind == StackFrameSharer) // TODO: improve this (in RTS?) - emit(Call("_", Ccc(), VoidType(), shareFrames, List(nextStackPointer))); - RetVoid() - } - sharer - }) - } - - def produceObject(role: String, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Operand = { - if (environment.isEmpty) { - ConstantNull(objectType) - } else { - val objectReference = LocalReference(objectType, freshName(role)); - val environmentReference = LocalReference(environmentType, freshName("environment")); - val size = ConstantInt(environmentSize(environment)); - val eraser = getEraser(environment, ObjectEraser) - - emit(Call(objectReference.name, Ccc(), objectType, newObject, List(eraser, size))); - emit(Call(environmentReference.name, Ccc(), environmentType, objectEnvironment, List(objectReference))); - shareValues(environment, freeInBody); - storeEnvironment(environmentReference, environment, Object); - objectReference - } - } - - def consumeObject(`object`: Operand, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - val environmentReference = LocalReference(environmentType, freshName("environment")); - emit(Call(environmentReference.name, Ccc(), environmentType, objectEnvironment, List(`object`))); - loadEnvironment(environmentReference, environment, Object); - shareValues(environment, freeInBody); - emit(Call("_", Ccc(), VoidType(), eraseObject, List(`object`))); - } - } - - def pushFrameOnto(stack: Operand, environment: machine.Environment, returnAddressName: String, sharer: Operand, eraser: Operand)(using ModuleContext, FunctionContext, BlockContext) = { - val size = environmentSize(environment); - - val newStack = LocalReference(stackType, freshName("stack")) - emit(Call(newStack.name, Ccc(), stackType, checkLimit, List(stack, ConstantInt(size + 24)))); - setStack(newStack); - - val environmentPointer = LocalReference(stackPointerType, freshName("environmentPointer")); - emit(Call(environmentPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(size)))); - - storeEnvironmentAt(environmentPointer, environment, StackPointer); - - val headerPointer = LocalReference(stackPointerType, freshName("headerPointer")); - emit(Call(headerPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(24)))); - - val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); - emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, headerPointer, List(0, 0))); - val sharerPointer = LocalReference(PointerType(), freshName("sharer_pointer")); - emit(GetElementPtr(sharerPointer.name, frameHeaderType, headerPointer, List(0, 1))); - val eraserPointer = LocalReference(PointerType(), freshName("eraser_pointer")); - emit(GetElementPtr(eraserPointer.name, frameHeaderType, headerPointer, List(0, 2))); - - emit(Store(returnAddressPointer, ConstantGlobal(returnAddressName), StackPointer)); - emit(Store(sharerPointer, sharer, StackPointer)); - emit(Store(eraserPointer, eraser, StackPointer)); - } - - def popEnvironmentFrom(stack: Operand, environment: machine.Environment)(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - val size = ConstantInt(environmentSize(environment)); - emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); - loadEnvironmentAt(stackPointer, environment, StackPointer) - } - } - - def environmentType(environment: machine.Environment): Type = - StructureType(environment.map { - case machine.Variable(_, tpe) => transform(tpe) - }) - - def storeEnvironmentAt(pointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { - val `type` = environmentType(environment) - environment.zipWithIndex.foreach { - case (machine.Variable(name, tpe), i) => - val field = LocalReference(PointerType(), freshName(name + "_pointer")); - emit(GetElementPtr(field.name, `type`, pointer, List(0, i))); - emit(Store(field, transform(machine.Variable(name, tpe)), alias)) - } - } - - def loadEnvironmentAt(pointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { - val `type` = environmentType(environment) - environment.zipWithIndex.foreach { - case (machine.Variable(name, tpe), i) => - val field = LocalReference(PointerType(), freshName(name + "_pointer")); - emit(GetElementPtr(field.name, `type`, pointer, List(0, i))); - emit(Load(name, transform(tpe), field, alias)) - } - } - - def shareValues(values: machine.Environment, freeInBody: Set[machine.Variable])(using FunctionContext, BlockContext): Unit = { - @tailrec - def loop(values: machine.Environment): Unit = { - values match { - case Nil => () - case value :: values => - if values.map(substitute).contains(substitute(value)) then { - shareValue(value); - loop(values) - } else if freeInBody.map(substitute).contains(substitute(value)) then { - shareValue(value); - loop(values) - } else { - loop(values) - } - } - }; - loop(values) - } - - def eraseValues(environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = - environment.foreach { value => - if !freeInBody.map(substitute).contains(substitute(value)) then eraseValue(value) - } - - def shareValue(value: machine.Variable)(using FunctionContext, BlockContext): Unit = { - Option(value.tpe).collect { - case machine.Positive() => Call("_", Ccc(), VoidType(), sharePositive, List(transform(value))) - case machine.Negative() => Call("_", Ccc(), VoidType(), shareNegative, List(transform(value))) - case machine.Type.Stack() => Call("_", Ccc(), VoidType(), shareResumption, List(transform(value))) - }.map(emit) - } - - def eraseValue(value: machine.Variable)(using FunctionContext, BlockContext): Unit = { - Option(value.tpe).collect { - case machine.Positive() => Call("_", Ccc(), VoidType(), erasePositive, List(transform(value))) - case machine.Negative() => Call("_", Ccc(), VoidType(), eraseNegative, List(transform(value))) - case machine.Type.Stack() => Call("_", Ccc(), VoidType(), eraseResumption, List(transform(value))) - }.map(emit) - } - - def popReturnAddressFrom(stack: Operand, returnAddressName: String)(using ModuleContext, FunctionContext, BlockContext): Unit = { - - val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - // TODO properly find size - val size = ConstantInt(24); - emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); - - val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); - emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, stackPointer, List(0, 0))); - - emit(Load(returnAddressName, returnAddressType, returnAddressPointer, StackPointer)); - } - - val newObject = ConstantGlobal("newObject"); - val objectEnvironment = ConstantGlobal("objectEnvironment"); - - val sharePositive = ConstantGlobal("sharePositive"); - val shareNegative = ConstantGlobal("shareNegative"); - val shareResumption = ConstantGlobal("shareResumption"); - val shareFrames = ConstantGlobal("shareFrames"); - - val eraseObject = ConstantGlobal("eraseObject"); - val erasePositive = ConstantGlobal("erasePositive"); - val eraseNegative = ConstantGlobal("eraseNegative"); - val eraseResumption = ConstantGlobal("eraseResumption"); - val eraseFrames = ConstantGlobal("eraseFrames"); - - val freeStack = ConstantGlobal("freeStack") - - val newReference = ConstantGlobal("newReference") - val getVarPointer = ConstantGlobal("getVarPointer") - - val reset = ConstantGlobal("reset"); - val resume = ConstantGlobal("resume"); - val shift = ConstantGlobal("shift"); - val currentPrompt = ConstantGlobal("currentPrompt"); - val underflowStack = ConstantGlobal("underflowStack"); - val withEmptyStack = ConstantGlobal("withEmptyStack"); - val checkLimit = ConstantGlobal("checkLimit") - val stackAllocate = ConstantGlobal("stackAllocate"); - val stackDeallocate = ConstantGlobal("stackDeallocate"); - - /** - * Extra info in context - */ - class ModuleContext() { - var counter = 0; - var definitions: List[Definition] = List(); - val erasers = mutable.HashMap[(List[machine.Type], EraserKind), Operand](); - val sharers = mutable.HashMap[(List[machine.Type], SharerKind), Operand](); - } - - def emit(definition: Definition)(using C: ModuleContext) = - C.definitions = C.definitions :+ definition - - def freshName(name: String)(using C: ModuleContext): String = { - C.counter = C.counter + 1; - name + "_" + C.counter - } - - class FunctionContext() { - var substitution: Map[machine.Variable, machine.Variable] = Map(); - var basicBlocks: List[BasicBlock] = List(); - } - - def emit(basicBlock: BasicBlock)(using C: FunctionContext) = - C.basicBlocks = C.basicBlocks :+ basicBlock - - def withBindings[R](bindings: List[(machine.Variable, machine.Variable)])(prog: () => R)(using C: FunctionContext): R = { - val substitution = C.substitution; - C.substitution = substitution ++ bindings.map { case (variable -> value) => (variable -> substitution.getOrElse(value, value) ) }.toMap; - val result = prog(); - C.substitution = substitution - result - } - - def substitute(value: machine.Variable)(using C: FunctionContext): machine.Variable = - C.substitution.toMap.getOrElse(value, value) - - class BlockContext() { - var stack: Operand = LocalReference(stackType, "stack"); - var instructions: List[Instruction] = List(); - } - - def emit(instruction: Instruction)(using C: BlockContext) = - C.instructions = C.instructions :+ instruction - - def getStack()(using C: BlockContext) = - C.stack - - def setStack(stack: Operand)(using C: BlockContext) = - C.stack = stack; - - val escapeSeqs: Map[Char, String] = Map('\'' -> raw"'", '\"' -> raw"\"", '\\' -> raw"\\", '\n' -> raw"\n", '\t' -> raw"\t", '\r' -> raw"\r") - - def escape(scalaString: String): String = - scalaString.foldLeft(StringBuilder()) { (acc, c) => - escapeSeqs.get(c) match { - case Some(s) => acc ++= s - case None => acc += c - } - }.toString() -} diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala deleted file mode 100644 index 6bcd5666e..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala +++ /dev/null @@ -1,130 +0,0 @@ -package effekt -package generator -package llvm - - -/** - * see: https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST.html#t:Definition - */ -enum Definition { - case Function(linkage: Linkage, callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], basicBlocks: List[BasicBlock]) - case VerbatimFunction(callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], body: String) - case Verbatim(content: String) - case GlobalConstant(name: String, initializer: Operand) // initializer should be constant -} -export Definition.* - -enum Linkage { - case External() - case Private() -} -export Linkage.* - -enum CallingConvention { - case Ccc() - case Tailcc(musttail: Boolean) -} -export CallingConvention.* - -case class BasicBlock(name: String, instructions: List[Instruction], terminator: Terminator) - -enum AliasInfo { - case StackBase; - case StackPointer; - case Object; - case Prompt; - case VTable; - - def scope = { - this match { - case StackBase => "!11" - case StackPointer => "!12" - case Prompt => "!13" - case Object => "!14" - case VTable => "!15" - } - } - def noalias = { - this match { - case StackBase => "!21" - case StackPointer => "!22" - case Prompt => "!23" - case Object => "!24" - case VTable => "!25" - } - } -} -export AliasInfo.* - - - -/** - * see: https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST-Instruction.html#t:Instruction - */ -enum Instruction { - case Call(result: String, callingConvention: CallingConvention, resultType: Type, function: Operand, arguments: List[Operand]) - case Load(result: String, tpe: Type, address: Operand, alias: AliasInfo) - case Store(address: Operand, value: Operand, alias: AliasInfo) - case GetElementPtr(result: String, tpe: Type, address: Operand, indices: List[Int]) - case BitCast(result: String, operand: Operand, typ: Type) - case Add(result: String, operand0: Operand, operand1: Operand) - case FAdd(result: String, operand0: Operand, operand1: Operand) - case InsertValue(result: String, aggregate: Operand, element: Operand, index: Int) - case ExtractValue(result: String, aggregate: Operand, index: Int) - case Comment(msg: String) -} -export Instruction.* - -enum Terminator { - case RetVoid() - case Switch(operand: Operand, defaultDest: String, dests: List[(Int, String)]) - case CondBr(condition: Operand, trueDest: String, falseDest: String) -} -export Terminator.* - -case class Parameter(typ: Type, name: String) - -// Operands cannot be an enum since we use the more specific types massively. -// Scala 3 will perform widening way too often. -sealed trait Operand -object Operand { - case class LocalReference(tpe: Type, name: String) extends Operand - case class ConstantInt(n: Long) extends Operand - case class ConstantDouble(x: Double) extends Operand - case class ConstantAggregateZero(typ: Type) extends Operand - case class ConstantGlobal(name: String) extends Operand - case class ConstantNull(tpe: Type) extends Operand - case class ConstantArray(memberType: Type, members: List[Operand]) extends Operand // members should be homogeneous - case class ConstantInteger8(b: Byte) extends Operand -} -export Operand.* - -enum EraserKind { - case ObjectEraser - case StackEraser - case StackFrameEraser -} -export EraserKind.* - -enum SharerKind { - case StackSharer - case StackFrameSharer -} -export SharerKind.* - -/** - * see: https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST.html#t:Type - */ -enum Type { - case VoidType() - case IntegerType1() - case IntegerType8() // required for `void*` (which only exists as `i8*` in LLVM) and `char*` - case IntegerType64() - case DoubleType() - case PointerType() - case ArrayType(size: Int, of: Type) - case StructureType(elementTypes: List[Type]) - case FunctionType(resultType: Type, argumentTypes: List[Type]) - case NamedType(name: String) -} -export Type.* diff --git a/effekt/shared/src/main/scala/effekt/generator/vm/VM.scala b/effekt/shared/src/main/scala/effekt/generator/vm/VM.scala deleted file mode 100644 index 68031760a..000000000 --- a/effekt/shared/src/main/scala/effekt/generator/vm/VM.scala +++ /dev/null @@ -1,44 +0,0 @@ -package effekt -package generator -package vm - -import effekt.PhaseResult.CoreTransformed -import effekt.context.Context -import effekt.core.{ ModuleDecl, Id } - -import kiama.output.PrettyPrinterTypes.Document -import kiama.util.Source - -/** - * A "backend" that simply outputs the aggregated core module. - * This is called IR and note Core to avoid name clashes with package `effekt.core` - * - * This is, for example, used by the interpreter. - */ -class VM extends Compiler[(Id, symbols.Module, ModuleDecl)] { - - def extension = ".effekt-core.ir" - - override def supportedFeatureFlags: List[String] = List("vm") - - override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = None - - override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = None - - override def compile(source: Source)(using C: Context): Option[(Map[String, String], (Id, symbols.Module, ModuleDecl))] = - Optimized.run(source).map { res => (Map.empty, res) } - - - // The Compilation Pipeline - // ------------------------ - // Source => Core => CPS => JS - lazy val Core = Phase.cached("core") { - Frontend andThen Middleend - } - - lazy val Optimized = allToCore(Core) andThen Aggregate andThen core.optimizer.Optimizer map { - case input @ CoreTransformed(source, tree, mod, core) => - val mainSymbol = Context.checkMain(mod) - (mainSymbol, mod, core) - } -} diff --git a/effekt/shared/src/main/scala/effekt/machine/Analysis.scala b/effekt/shared/src/main/scala/effekt/machine/Analysis.scala deleted file mode 100644 index f3672eac6..000000000 --- a/effekt/shared/src/main/scala/effekt/machine/Analysis.scala +++ /dev/null @@ -1,55 +0,0 @@ -package effekt -package machine -package analysis - -def freeVariables(clauses: List[Clause]): Set[Variable] = { - clauses.flatMap(freeVariables).toSet -} - -def freeVariables(clause: Clause): Set[Variable] = - clause match { - case Clause(parameters, body) => freeVariables(body) -- parameters.toSet - } - -def freeVariables(taggedClause: (Int, Clause)): Set[Variable] = freeVariables(taggedClause._2) - -def freeVariables(statement: Statement): Set[Variable] = - statement match { - case Jump(Label(_, environment)) => - environment.toSet - case Substitute(bindings, rest) => - freeVariables(rest).map { variable => bindings.toMap.getOrElse(variable, variable) } - case Construct(name, tag, values, rest) => - Set.from(values) ++ (freeVariables(rest) -- Set(name)) - case Switch(value, clauses, default: Option[Clause]) => - Set(value) ++ clauses.flatMap(freeVariables) ++ default.map(freeVariables).getOrElse(Set.empty) - case New(name, clauses, rest) => - freeVariables(clauses) ++ (freeVariables(rest) -- Set(name)) - case Invoke(value, tag, values) => - Set(value) ++ Set.from(values) - case Var(name, init, tpe, rest) => - Set(init) ++ (freeVariables(rest) -- Set(name)) - case LoadVar(name, ref, rest) => - Set(ref) ++ (freeVariables(rest) -- Set(name)) - case StoreVar(ref, value, rest) => - Set(ref, value) ++ freeVariables(rest) - case PushFrame(frame, rest) => - freeVariables(frame) ++ freeVariables(rest) - case Return(values) => - Set.from(values) - case Reset(prompt, frame, rest) => - freeVariables(frame) ++ (freeVariables(rest) -- Set(prompt)) - case Resume(value, rest) => - Set(value) ++ freeVariables(rest) - case Shift(name, prompt, rest) => - Set(prompt) ++ (freeVariables(rest) -- Set(name)) - case LiteralInt(name, value, rest) => - freeVariables(rest) - name - case LiteralDouble(name, value, rest) => - freeVariables(rest) - name - case LiteralUTF8String(name, utf8, rest) => - freeVariables(rest) - name - case ForeignCall(name, builtin, arguments, rest) => - arguments.toSet ++ (freeVariables(rest) - name) - case Hole => Set.empty - } diff --git a/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala deleted file mode 100644 index 58967dd1a..000000000 --- a/effekt/shared/src/main/scala/effekt/machine/PrettyPrinter.scala +++ /dev/null @@ -1,124 +0,0 @@ -package effekt -package machine - -import kiama.output.ParenPrettyPrinter -import kiama.output.PrettyPrinterTypes.Document - -import scala.language.implicitConversions - -object PrettyPrinter extends ParenPrettyPrinter { - - override val defaultIndent = 2 - - def format(definitions: List[Definition]): Document = pretty(vsep(definitions.map(toDoc), line), 2) - - implicit def toDoc(v: Variable): Doc = string(v.name) - - implicit def toDoc(v: Label): Doc = string(v.name) - - def toDoc(clause: Clause): Doc = clause match { - case Clause(parameters, body) => braces(space <> toDoc(parameters) <+> "=>" <> group(nest(line <> toDocStmts(body)) <> line)) - } - - def toDoc(e: Environment): Doc = parens(e map { - case Variable(name, tpe) => name <+> ":" <+> toDoc(tpe) - }) - - def toDoc(tpe: Type): Doc = tpe match { - case Positive() => "Positive" - case Negative() => "Negative" - case Type.Prompt() => "Prompt" - case Type.Stack() => "Stack" - case Type.Int() => "Int" - case Type.Byte() => "Byte" - case Type.Double() => "Double" - case Type.Reference(tpe) => toDoc(tpe) <> "*" - } - - def toDoc(defi: Definition): Doc = defi match { - case Definition(label, body) => - "def" <+> label.name <> toDoc(label.environment) <+> "=" <+> block(toDocStmts(body)) - } - - def toDoc(stmt: Statement): Doc = - stmt match { - // terminators that do not require a block to be readable: - case _ : (Return | Jump | Invoke | Switch | Reset | Shift) => toDocStmts(stmt) - case other => block(toDocStmts(stmt)) - } - - - def toDocStmts(stmt: Statement): Doc = stmt match { - - case Jump(label) => - "jump" <+> label - - case Substitute(bindings, rest) => - hsep(bindings map { case (left, right) => left <+> ":=" <+> right }, comma) <> ";" <> line <> toDocStmts(rest) - - case Construct(name, tag, arguments, rest) => - "let" <+> name <+> "=" <+> tag.toString <> parens(arguments map toDoc) <> ";" <> line <> toDocStmts(rest) - - case Switch(scrutinee, clauses, default) => - val cls = clauses.map { case (idx, cl) => idx.toString <+> ":" <+> toDoc(cl) } - val d = default.map(d => space <> "else" <+> toDoc(d)).getOrElse(emptyDoc) - "switch" <+> scrutinee <+> line <> indent(vcat(cls)) <> d - - case New(name, operations, rest) => - "let" <+> name <+> "=" <+> "new" <+> block(operations map toDoc) <> ";" <> line <> toDocStmts(rest) - - case Invoke(receiver, tag, arguments) => - "invoke" <+> receiver <> "." <> tag.toString <> parens(arguments map toDoc) - - case Var(name, init, _, rest) => - "var" <+> name <+> "=" <+> toDoc(init) <> ";" <> line <> toDocStmts(rest) - - case LoadVar(name, reference, rest) => - "let" <+> name <+> "=" <+> "loadVar" <> parens(toDoc(reference)) <> ";" <> line <> toDocStmts(rest) - - case StoreVar(reference, value, rest) => - "storeVar" <> parens(List(reference, value) map toDoc) <> ";" <> line <> toDocStmts(rest) - - case PushFrame(Clause(parameters, body), rest) => - "val" <+> toDoc(parameters) <+> "=" <+> toDoc(rest) <> ";" <> line <> - toDocStmts(body) - - case Return(arguments) => - "return" <+> hsep(arguments map toDoc, ",") - - case Reset(prompt, frame, rest) => - "reset" <+> braces(prompt <+> "=>" <+> nest(line <> toDocStmts(rest))) - - case Resume(stack, rest) => - "resume" <+> stack <> ";" <> line <> toDocStmts(rest) - - case Shift(name, prompt, rest) => - "shift" <+> prompt <+> braces(name <+> "=>" <+> nest(line <> toDocStmts(rest))) - - case ForeignCall(name, builtin, arguments, rest) => - "let" <+> name <+> "=" <+> builtin <> parens(arguments map toDoc) <> ";" <> line <> toDocStmts(rest) - - case LiteralInt(name, value, rest) => - "let" <+> name <+> "=" <+> value.toString <> ";" <> line <> toDocStmts(rest) - - case LiteralDouble(name, value, rest) => - "let" <+> name <+> "=" <+> value.toString <> ";" <> line <> toDocStmts(rest) - - case LiteralUTF8String(name, utf8, rest) => - "let" <+> name <+> "=" <+> ("\"" + (utf8.map { b => "\\" + f"$b%02x" }).mkString + "\"") <> ";" <> line <> toDocStmts(rest) - - case Hole => "<>" - } - - def nested(content: Doc): Doc = group(nest(line <> content)) - - def parens(docs: List[Doc]): Doc = parens(hsep(docs, comma)) - - def brackets(docs: List[Doc]): Doc = brackets(hsep(docs, comma)) - - def braces(docs: List[Doc]): Doc = braces(hsep(docs, comma)) - - def block(content: Doc): Doc = braces(nest(line <> content) <> line) - - def block(docs: List[Doc]): Doc = block(vsep(docs, line)) -} diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala deleted file mode 100644 index 90751be3c..000000000 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ /dev/null @@ -1,597 +0,0 @@ -package effekt -package machine - -import effekt.context.Context -import effekt.core.{ Block, DeclarationContext, Toplevel, Id, given } -import effekt.symbols.{ Symbol, TermSymbol } -import effekt.symbols.builtins.TState -import effekt.util.messages.ErrorReporter -import effekt.symbols.ErrorMessageInterpolator -import scala.annotation.tailrec - - -object Transformer { - - private def ErrorReporter(using E: ErrorReporter): ErrorReporter = E - - def transform(main: CoreTransformed, mainSymbol: TermSymbol)(using C: Context): Program = { - C.using(module = main.mod) { - transform(mainSymbol, main.core); - } - } - - def transform(mainSymbol: TermSymbol, mod: core.ModuleDecl)(using E: ErrorReporter): Program = { - - val mainName = transform(mainSymbol) - given BC: BlocksParamsContext = BlocksParamsContext(); - given DC: DeclarationContext = core.DeclarationContext(mod.declarations, mod.externs) - - // collect all information - val declarations = mod.externs.map(transform) - val definitions = mod.definitions - val mainEntry = Label(mainName, Nil) - - findToplevelBlocksParams(definitions) - - val toplevelDefinitions = definitions.map { - case core.Toplevel.Def(id, core.BlockLit(tparams, cparams, vparams, bparams, body)) => - Definition(Label(transform(id), vparams.map(transform) ++ bparams.map(transform)), transform(body)) - case core.Toplevel.Val(id, tpe, binding) => - Definition(BC.globals(id), transform(binding)) - case core.Toplevel.Def(id, block @ core.New(impl)) => - val variable = Variable(freshName("returned"), transform(block.tpe)) - Definition(BC.globals(id), New(variable, transform(impl), Return(List(variable)))) - case d => - ErrorReporter.abort(s"Other toplevel definitions not yet supported: ${d}") - } - - val localDefinitions = BC.definitions - - Program(declarations, toplevelDefinitions ++ localDefinitions, mainEntry) - } - - def transform(extern: core.Extern)(using BlocksParamsContext, ErrorReporter): Declaration = extern match { - case core.Extern.Def(name, tps, cparams, vparams, bparams, ret, capture, body) => - if bparams.nonEmpty then ErrorReporter.abort("Foreign functions currently cannot take block arguments.") - - val transformedParams = vparams.map(transform) - noteDefinition(name, transformedParams, Nil) - Extern(transform(name), transformedParams, transform(ret), capture.contains(symbols.builtins.AsyncCapability.capture), transform(body)) - - case core.Extern.Include(ff, contents) => - Include(ff, contents) - } - - def transform(body: core.ExternBody)(using ErrorReporter): machine.ExternBody = body match { - case core.ExternBody.StringExternBody(ff, Template(strings, args)) => - ExternBody.StringExternBody(ff, Template(strings, args map { - case core.ValueVar(id, tpe) => Variable(transform(id), transform(tpe)) - case _ => ErrorReporter.abort("In the LLVM backend, only variables are allowed in templates") - })) - case core.ExternBody.Unsupported(err) => - ExternBody.Unsupported(err) - } - - def transform(stmt: core.Stmt)(using BPC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Statement = - stmt match { - - case core.Def(id, block @ core.BlockLit(tparams, cparams, vparams, bparams, body), rest) => - // (1) Collect all the information about free variables of local definitions - noteParameters(bparams) - - // Does not work for mutually recursive local definitions (which are not supported anyway, at the moment) - val freeVariables = core.Variables.free(block).toSet - .filterNot(x => BPC.globals.contains(x.id)) // globals are NOT free - - val freeParams = freeVariables.flatMap { - case core.Variable.Value(id, tpe) => - Set(Variable(transform(id), transform(tpe))) - - // Mutable variables are blocks and can be free, but do not have info. - case core.Variable.Block(id, core.Type.TState(stTpe), capt) => - Set(Variable(transform(id), Type.Reference(transform(stTpe)))) - - // Regions are blocks and can be free, but do not have info. - case core.Variable.Block(id, core.Type.TRegion, capt) => - Set(Variable(transform(id), Type.Prompt())) - - case core.Variable.Block(pid, tpe, capt) if pid != id => BPC.info.get(pid) match { - // For each known free block we have to add its free variables to this one (flat closure) - case Some(BlockInfo.Definition(freeParams, blockParams)) => - freeParams.toSet - - // Unknown free blocks stay free variables - case Some(BlockInfo.Parameter(tpe)) => - Set(Variable(transform(pid), transform(tpe))) - - // Everything else is considered bound or global - case None => - ErrorReporter.panic(s"Could not find info for free variable $pid") - } - case _ => Set.empty - } - - noteDefinition(id, vparams.map(transform) ++ bparams.map(transform), freeParams.toList) - - // (2) Actually translate the definitions - emitDefinition(transformLabel(id), transform(body)) - transform(rest) - - case core.Def(id, block @ core.New(impl), rest) => - // this is just a hack... - noteParameter(id, block.tpe) - New(Variable(transform(id), transform(impl.interface)), transform(impl), transform(rest)) - - case core.Def(id, core.BlockVar(other, tpe, capt), rest) => - getBlockInfo(other) match { - case BlockInfo.Definition(free, params) => - noteDefinition(id, free, params) - emitDefinition(transformLabel(id), Jump(transformLabel(other))) - transform(rest) - case BlockInfo.Parameter(_) => - noteParameter(id, tpe) - Substitute(List(Variable(transform(id), transform(tpe)) -> Variable(transform(other), transform(tpe))), transform(rest)) - } - - case core.Def(id, block @ core.Unbox(pure), rest) => - noteParameter(id, block.tpe) - transform(pure).run { boxed => - ForeignCall(Variable(transform(id), Type.Negative()), "unbox", List(boxed), transform(rest)) - } - - case core.Let(id, tpe, binding, rest) => - transform(binding).run { value => - // TODO consider passing the environment to [[transform]] instead of explicit substitutions here. - // TODO it is important that we use the inferred [[binding.tpe]] and not the annotated type [[tpe]], but why? - Substitute(List(Variable(transform(id), transform(binding.tpe)) -> value), transform(rest)) - } - - case core.Return(expr) => - transform(expr).run { value => Return(List(value)) } - - case core.Val(id, annot, binding, rest) => - PushFrame( - Clause(List(Variable(transform(id), transform(binding.tpe))), transform(rest)), - transform(binding) - ) - - case core.App(callee, targs, vargs, bargs) => - if (targs.exists(requiresBoxing)) { ErrorReporter.panic(s"Types ${targs} are used as type parameters but would require boxing.") } - transform(vargs, bargs).run { (values, blocks) => - callee match { - case Block.BlockVar(id, annotatedTpe, annotatedCapt) => - BPC.info.getOrElse(id, sys.error(pp"In ${stmt}. Cannot find block info for ${id}: ${annotatedTpe}.\n${BPC.info}")) match { - // Unknown Jump to function - case BlockInfo.Parameter(tpe: core.BlockType.Function) => - Invoke(Variable(transform(id), transform(tpe)), builtins.Apply, values ++ blocks) - - // Known Jump - case BlockInfo.Definition(freeParams, blockParams) => - val label = machine.Label(transform(id), blockParams ++ freeParams) - Substitute(label.environment.zip(values ++ blocks), Jump(label)) - - case _ => ErrorReporter.panic("Applying an object") - } - - case Block.Unbox(pure) => - transform(pure).run { boxedCallee => - val callee = Variable(freshName(boxedCallee.name), Type.Negative()) - - ForeignCall(callee, "unbox", List(boxedCallee), - Invoke(callee, builtins.Apply, values ++ blocks)) - } - - case Block.New(impl) => - ErrorReporter.panic("Applying an object") - - case Block.BlockLit(tparams, cparams, vparams, bparams, body) => - ErrorReporter.panic(pp"Call to block literal should have been reduced: ${stmt}") - } - } - - case core.Invoke(callee, method, methodTpe, targs, vargs, bargs) => - if (targs.exists(requiresBoxing)) { ErrorReporter.abort(s"Types ${targs} are used as type parameters but would require boxing.") } - - val opTag = DeclarationContext.getPropertyTag(method) - transform(vargs, bargs).run { (values, blocks) => - callee match { - case Block.BlockVar(id, tpe, capt) if BPC.globals contains id => - val variable = Variable(freshName("receiver"), transform(tpe)) - PushFrame(Clause(List(variable), Invoke(variable, opTag, values ++ blocks)), Jump(BPC.globals(id))) - - case Block.BlockVar(id, tpe, capt) => - Invoke(Variable(transform(id), transform(tpe)), opTag, values ++ blocks) - - case Block.Unbox(pure) => - transform(pure).run { boxedCallee => - val callee = Variable(freshName(boxedCallee.name), Type.Negative()) - - ForeignCall(callee, "unbox", List(boxedCallee), - Invoke(callee, opTag, values ++ blocks)) - } - - case Block.New(impl) => - ErrorReporter.panic("Method call to known object should have been reduced") - - case Block.BlockLit(tparams, cparams, vparams, bparams, body) => - ErrorReporter.panic("Invoking a method on a function") - } - } - - case core.If(cond, thenStmt, elseStmt) => - transform(cond).run { value => - Switch(value, List(0 -> Clause(List(), transform(elseStmt)), 1 -> Clause(List(), transform(thenStmt))), None) - } - - case core.Match(scrutinee, clauses, default) => - val transformedClauses = clauses.map { case (constr, core.BlockLit(tparams, cparams, vparams, bparams, body)) => - DeclarationContext.getConstructorTag(constr) -> Clause(vparams.map(transform), transform(body)) - } - val transformedDefault = default.map { clause => - Clause(List(), transform(clause)) - } - - transform(scrutinee).run { value => - Switch(value, transformedClauses, transformedDefault) - } - - case core.Reset(core.BlockLit(Nil, cparams, Nil, List(prompt), body)) => - noteParameters(List(prompt)) - - val answerType = stmt.tpe - val variable = Variable(freshName("returned"), transform(answerType)) - val returnClause = Clause(List(variable), Return(List(variable))) - - Reset(Variable(transform(prompt.id), Type.Prompt()), returnClause, transform(body)) - - case core.Shift(prompt, core.BlockLit(Nil, cparams, Nil, List(k), body)) => - - noteParameter(k.id, core.Type.TResume(core.Type.TUnit, core.Type.TUnit)) - - Shift(Variable(transform(k.id), Type.Stack()), Variable(transform(prompt.id), Type.Prompt()), - transform(body)) - - case core.Resume(k, body) => - Resume(Variable(transform(k.id), Type.Stack()), transform(body)) - - case core.Region(core.BlockLit(tparams, cparams, vparams, List(region), body)) => - noteParameters(List(region)) - - val variable = Variable(freshName("returned"), transform(body.tpe)) - val returnClause = Clause(List(variable), Return(List(variable))) - val prompt = transform(region) - - Reset(prompt, returnClause, transform(body)) - - case core.Alloc(id, init, region, body) => - transform(init).run { value => - val reference = Variable(transform(id), Type.Reference(value.tpe)) - val prompt = Variable(transform(region), Type.Prompt()) - val temporary = Variable(freshName("temporaryStack"), Type.Stack()) - - Shift(temporary, prompt, - Var(reference, value, Type.Positive(), - Resume(temporary, transform(body)))) - } - - case core.Var(ref, init, capture, body) => - val stateType = transform(init.tpe) - val reference = Variable(transform(ref), Type.Reference(stateType)) - val prompt = Variable(freshName("prompt"), Type.Prompt()) - - transform(init).run { value => - Var(reference, value, transform(body.tpe), - transform(body)) - } - - case core.Get(id, tpe, ref, capt, body) => - val stateType = transform(tpe) - val reference = Variable(transform(ref), Type.Reference(stateType)) - val variable = Variable(transform(id), stateType) - - LoadVar(variable, reference, transform(body)) - - case core.Put(ref, capt, arg, body) => - val stateType = transform(arg.tpe) - val reference = Variable(transform(ref), Type.Reference(stateType)) - val variable = Variable(freshName("put"), Positive()) - - transform(arg).run { value => - StoreVar(reference, value, transform(body)) - } - - case core.Hole() => machine.Statement.Hole - - case _ => - ErrorReporter.abort(s"Unsupported statement: $stmt") - } - - // Merely sequences the transformation of the arguments monadically - def transform(vargs: List[core.Pure], bargs: List[core.Block])(using BPC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Binding[(List[Variable], List[Variable])] = - for { - values <- traverse(vargs)(transform) - blocks <- traverse(bargs)(transformBlockArg) - } yield (values, blocks) - - def transformBlockArg(block: core.Block)(using BPC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Binding[Variable] = block match { - case core.BlockVar(id, tpe, _) if BPC.globals contains id => - val variable = Variable(transform(id), transform(tpe)) - shift { k => - PushFrame(Clause(List(variable), k(variable)), Jump(BPC.globals(id))) - } - case core.BlockVar(id, tpe, capt) => getBlockInfo(id) match { - case BlockInfo.Definition(_, parameters) => - // Passing a top-level function directly, so we need to eta-expand turning it into a closure - // TODO cache the closure somehow to prevent it from being created on every call - val variable = Variable(freshName(id.name.name ++ "$closure"), Negative()) - shift { k => - New(variable, List(Clause(parameters, - // conceptually: Substitute(parameters zip parameters, Jump(...)) but the Substitute is a no-op here - Jump(transformLabel(id)) - )), k(variable)) - } - case BlockInfo.Parameter(tpe) => - pure(Variable(transform(id), transform(tpe))) - } - - case core.BlockLit(tparams, cparams, vparams, bparams, body) => - noteParameters(bparams) - val parameters = vparams.map(transform) ++ bparams.map(transform); - val variable = Variable(freshName("blockLit"), Negative()) - shift { k => - New(variable, List(Clause(parameters, transform(body))), k(variable)) - } - - case core.New(impl) => - val variable = Variable(freshName("new"), Negative()) - shift { k => - New(variable, transform(impl), k(variable)) - } - - case core.Unbox(pure) => - transform(pure) - } - - def transform(expr: core.Expr)(using BC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Binding[Variable] = expr match { - - case core.ValueVar(id, tpe) if BC.globals contains id => - val variable = Variable(freshName("run"), transform(tpe)) - shift { k => - // TODO this might introduce too many pushes. - PushFrame(Clause(List(variable), k(variable)), - Substitute(Nil, Jump(BC.globals(id)))) - } - - case core.ValueVar(id, tpe) => - pure(Variable(transform(id), transform(tpe))) - - case core.Literal((), _) => - val variable = Variable(freshName("unitLiteral"), Positive()); - shift { k => - Construct(variable, builtins.Unit, List(), k(variable)) - } - - case core.Literal(value: Long, _) => - val variable = Variable(freshName("longLiteral"), Type.Int()); - shift { k => - LiteralInt(variable, value, k(variable)) - } - - // for characters - case core.Literal(value: Int, _) => - val variable = Variable(freshName("intLiteral"), Type.Int()); - shift { k => - LiteralInt(variable, value, k(variable)) - } - - case core.Literal(value: Boolean, _) => - val variable = Variable(freshName("booleanLiteral"), Positive()) - shift { k => - Construct(variable, if (value) builtins.True else builtins.False, List(), k(variable)) - } - - case core.Literal(v: Double, _) => - val literal_binding = Variable(freshName("doubleLiteral"), Type.Double()); - shift { k => - LiteralDouble(literal_binding, v, k(literal_binding)) - } - - case core.Literal(javastring: String, _) => - val literal_binding = Variable(freshName("utf8StringLiteral"), builtins.StringType); - shift { k => - LiteralUTF8String(literal_binding, javastring.getBytes("utf-8"), k(literal_binding)) - } - - case core.PureApp(core.BlockVar(blockName: symbols.ExternFunction, tpe: core.BlockType.Function, capt), targs, vargs) => - if (targs.exists(requiresBoxing)) { ErrorReporter.abort(s"Types ${targs} are used as type parameters but would require boxing.") } - - val variable = Variable(freshName("pureApp"), transform(tpe.result)) - transform(vargs, Nil).flatMap { (values, blocks) => - shift { k => - ForeignCall(variable, transform(blockName), values ++ blocks, k(variable)) - } - } - - case core.DirectApp(core.BlockVar(blockName: symbols.ExternFunction, tpe: core.BlockType.Function, capt), targs, vargs, bargs) => - if (targs.exists(requiresBoxing)) { ErrorReporter.abort(s"Types ${targs} are used as type parameters but would require boxing.") } - - val variable = Variable(freshName("pureApp"), transform(tpe.result)) - transform(vargs, bargs).flatMap { (values, blocks) => - shift { k => - ForeignCall(variable, transform(blockName), values ++ blocks, k(variable)) - } - } - - case core.Make(data, constructor, targs, vargs) => - if (targs.exists(requiresBoxing)) { ErrorReporter.abort(s"Types ${targs} are used as type parameters but would require boxing.") } - - val variable = Variable(freshName("make"), transform(data)); - val tag = DeclarationContext.getConstructorTag(constructor) - - transform(vargs, Nil).flatMap { (values, blocks) => - shift { k => - Construct(variable, tag, values ++ blocks, k(variable)) - } - } - - case core.Box(block, annot) => - transformBlockArg(block).flatMap { unboxed => - shift { k => - val boxed = Variable(freshName(unboxed.name), Type.Positive()) - ForeignCall(boxed, "box", List(unboxed), k(boxed)) - } - } - - case _ => - ErrorReporter.abort(s"Unsupported expression: $expr") - } - - def transform(impl: core.Implementation)(using BlocksParamsContext, DeclarationContext, ErrorReporter): List[Clause] = - impl.operations.sortBy { - case core.Operation(operationName, _, _, _, _, _) => - DeclarationContext.getInterface(impl.interface.name).properties.indexWhere(_.id == operationName) - }.map(op => transform(op)) - - def transform(op: core.Operation)(using BlocksParamsContext, DeclarationContext, ErrorReporter): Clause = - op match { - // No continuation, implementation of an object - case core.Operation(name, tparams, cparams, vparams, bparams, body) => - noteParameters(bparams) - Clause(vparams.map(transform) ++ bparams.map(transform), transform(body)) - } - - def transform(param: core.ValueParam)(using BlocksParamsContext, ErrorReporter): Variable = - param match { - case core.ValueParam(name, tpe) => - Variable(transform(name), transform(tpe)) - } - - def transform(param: core.BlockParam)(using BlocksParamsContext, ErrorReporter): Variable = - param match { - case core.BlockParam(name, tpe, capt) => - Variable(transform(name), transform(tpe)) - } - - def transform(tpe: core.ValueType)(using ErrorReporter): Type = tpe match { - case core.ValueType.Var(name) => Positive() // assume all value parameters are data - case core.ValueType.Boxed(tpe, capt) => Positive() - case core.Type.TUnit => builtins.UnitType - case core.Type.TInt => Type.Int() - case core.Type.TChar => Type.Int() - case core.Type.TByte => Type.Byte() - case core.Type.TBoolean => builtins.BooleanType - case core.Type.TDouble => Type.Double() - case core.Type.TString => Positive() - case core.ValueType.Data(symbol, targs) => Positive() - } - - def transform(tpe: core.BlockType)(using ErrorReporter): Type = tpe match { - case core.Type.TRegion => Type.Prompt() - case core.Type.TResume(result, answer) => Type.Stack() - case core.Type.TPrompt(answer) => Type.Prompt() - case core.BlockType.Function(tparams, cparams, vparams, bparams, result) => Negative() - case core.BlockType.Interface(symbol, targs) => Negative() - } - - def transformLabel(id: Id)(using BPC: BlocksParamsContext): Label = getBlockInfo(id) match { - case BlockInfo.Definition(freeParams, boundParams) => Label(transform(id), boundParams ++ freeParams) - case BlockInfo.Parameter(_) => sys error s"Expected a function definition, but got a block parameter: ${id}" - } - - def transform(id: Id): String = - s"${id.name}_${id.id}" - - def requiresBoxing(tpe: core.ValueType): Boolean = { - tpe match - case core.ValueType.Var(_) => false // assume by induction all type variables must be data - case core.ValueType.Data(_, args) => { - args.exists(requiresBoxing) - } - case core.ValueType.Boxed(_, _) => false // TODO check somehow? - } - - def freshName(baseName: String): String = baseName + "_" + symbols.Symbol.fresh.next() - - def findToplevelBlocksParams(definitions: List[core.Toplevel])(using BlocksParamsContext, ErrorReporter): Unit = - definitions.foreach { - case Toplevel.Def(id, core.BlockLit(tparams, cparams, vparams, bparams, body)) => - noteDefinition(id, vparams.map(transform) ++ bparams.map(transform), Nil) - noteParameters(bparams) - case Toplevel.Val(id, tpe, binding) => - noteDefinition(id, Nil, Nil) - noteGlobal(id) - case Toplevel.Def(id, core.New(impl)) => - noteDefinition(id, Nil, Nil) - noteGlobal(id) - case other => () - } - - /** - * Extra info in context - */ - - class BlocksParamsContext() { - var info: Map[Symbol, BlockInfo] = Map.empty - var globals: Map[Id, Label] = Map.empty - var definitions: List[Definition] = List.empty - } - - enum BlockInfo { - case Definition(free: Environment, params: Environment) - case Parameter(tpe: core.BlockType) - } - - def DeclarationContext(using DC: DeclarationContext): DeclarationContext = DC - - def noteDefinition(id: Id, params: Environment, free: Environment)(using BC: BlocksParamsContext): Unit = - assert(!BC.info.isDefinedAt(id), s"Registering info twice for ${id} (was: ${BC.info(id)}, now: Definition)") - BC.info += (id -> BlockInfo.Definition(free, params)) - - def noteParameter(id: Id, tpe: core.BlockType)(using BC: BlocksParamsContext): Unit = - assert(!BC.info.isDefinedAt(id), s"Registering info twice for ${id} (was: ${BC.info(id)}, now: Parameter)") - BC.info += (id -> BlockInfo.Parameter(tpe)) - - def noteParameters(ps: List[core.BlockParam])(using BC: BlocksParamsContext): Unit = - ps.foreach { - case core.BlockParam(id, tpe, capt) => noteParameter(id, tpe) - } - - def noteGlobal(id: Id)(using BPC: BlocksParamsContext): Unit = - BPC.globals += (id -> Label(transform(id), Nil)) - - def emitDefinition(label: Label, statement: Statement)(using BPC: BlocksParamsContext): Unit = - BPC.definitions = Definition(label, statement) :: BPC.definitions - - def getBlockInfo(id: Id)(using BPC: BlocksParamsContext): BlockInfo = - BPC.info.getOrElse(id, sys error s"No block info for ${util.show(id)}") - - def shift[A](body: (A => Statement) => Statement): Binding[A] = - Binding { k => Trampoline.Done(body { x => trampoline(k(x)) }) } - - case class Binding[A](body: (A => Trampoline[Statement]) => Trampoline[Statement]) { - def flatMap[B](rest: A => Binding[B]): Binding[B] = { - Binding(k => Trampoline.More { () => body(a => Trampoline.More { () => rest(a).body(k) }) }) - } - def run(k: A => Statement): Statement = trampoline(body { x => Trampoline.Done(k(x)) }) - def map[B](f: A => B): Binding[B] = flatMap { a => pure(f(a)) } - } - - enum Trampoline[A] { - case Done(value: A) - case More(thunk: () => Trampoline[A]) - } - - @tailrec - def trampoline[A](body: Trampoline[A]): A = body match { - case Trampoline.Done(value) => value - case Trampoline.More(thunk) => trampoline(thunk()) - } - - def traverse[S, T](l: List[S])(f: S => Binding[T]): Binding[List[T]] = - l match { - case Nil => pure(Nil) - case head :: tail => for { x <- f(head); xs <- traverse(tail)(f) } yield x :: xs - } - - def pure[A](a: A): Binding[A] = Binding(k => k(a)) -} diff --git a/effekt/shared/src/main/scala/effekt/machine/Tree.scala b/effekt/shared/src/main/scala/effekt/machine/Tree.scala deleted file mode 100644 index 6856bdf46..000000000 --- a/effekt/shared/src/main/scala/effekt/machine/Tree.scala +++ /dev/null @@ -1,246 +0,0 @@ -package effekt -package machine - -import effekt.source.FeatureFlag - -/** - * Variables stand for values - */ -case class Variable(name: String, tpe: Type) - -/** - * An environment is an ordered list of type-annotated variable names. - * - * Used to represent contexts, free variables, parameters, ... - */ -type Environment = List[Variable] - -/** - * Labels - * - * def l = { s1 }; s2 - * - * Here l is the label and [[environment]] is the list of free variables of s1. - * It thus can be understood as the type of the label. - */ -case class Label(name: String, environment: Environment) - -/** - * Applying a substitution - * - * List(x -> y) - * - * will replace all occurrences of x by y. Here x is a binding occurrence - * and y is a bound occurrence. - */ -type Substitution = List[(Variable, Variable)] - -type Tag = Int - - -/** - * A module declaration - */ -case class Program(declarations: List[Declaration], program: List[Definition], entry: Label) - -/** - * Toplevel declarations for FFI - */ -enum Declaration { - case Extern(name: String, parameters: Environment, returnType: Type, async: Boolean, body: ExternBody) - case Include(featureFlag: FeatureFlag, contents: String) -} -export Declaration.* - -sealed trait ExternBody -object ExternBody { - case class StringExternBody(featureFlag: FeatureFlag, contents: Template[Variable]) extends ExternBody - case class Unsupported(err: util.messages.EffektError) extends ExternBody { - def report(using E: util.messages.ErrorReporter): Unit = E.report(err) - } -} - -/** - * Clauses are parametrized statements - * - * e.g. { (x1...) => s } - * - * The parameters (x1, etc.) are binding occurrences. - * - * Gamma ++ Delta |- s - * Gamma ++ Delta |- s - * ----------------------- - * Gamma |- { Delta => s } - * - * In the typing rule Delta corresponds to [[parameters]]. - */ -case class Clause(parameters: Environment, body: Statement) - -/** - * All definitions are on the toplevel - * e.g. def l = s, ... - */ -case class Definition(label: Label, body: Statement) - - -/** - * Statements - * - * ----------[[ effekt.machine.Statement ]]---------- - * - * ─ [[ Statement ]] - * │─ [[ Jump ]] - * │─ [[ Substitute ]] - * │─ [[ Construct ]] - * │─ [[ Switch ]] - * │─ [[ New ]] - * │─ [[ Invoke ]] - * │─ [[ Var ]] - * │─ [[ LoadVar ]] - * │─ [[ StoreVar ]] - * │─ [[ PushFrame ]] - * │─ [[ Return ]] - * │─ [[ Reset ]] - * │─ [[ Resume ]] - * │─ [[ Shift ]] - * │─ [[ ForeignCall ]] - * │─ [[ LiteralInt ]] - * │─ [[ LiteralDouble ]] - * │─ [[ LiteralUTF8String ]] - * │─ [[ Hole ]] - * - * -------------------------------------------------- - */ -enum Statement { - - /** - * e.g. jump l - */ - case Jump(label: Label) - - /** - * e.g. s[x1 -> v1, ...] - */ - case Substitute(bindings: Substitution, rest: Statement) - - /** - * e.g. let x = make C(v1, ...); s - */ - case Construct(name: Variable, tag: Int, arguments: Environment, rest: Statement) - - /** - * e.g. switch v { (x1, ...) => s1; ... } - */ - case Switch(scrutinee: Variable, clauses: List[(Int, Clause)], default: Option[Clause]) - - /** - * e.g. let x = new { (x1, ...) => s1; ... }; s - */ - case New(name: Variable, operations: List[Clause], rest: Statement) - - /** - * e.g. v.m(v1, ...) - */ - case Invoke(receiver: Variable, tag: Tag, arguments: Environment) - - /** - * e.g. var x = 42; s - */ - case Var(name: Variable, init: Variable, returnType: Type, rest: Statement) - - /** - * e.g. let y = loadVar(x); s - */ - case LoadVar(name: Variable, ref: Variable, rest: Statement) - - /** - * e.g. storeVar(x, 42); s - */ - case StoreVar(ref: Variable, value: Variable, rest: Statement) - - /** - * e.g. push { (x, ...) => s }; s - */ - case PushFrame(frame: Clause, rest: Statement) - - /** - * e.g. return (v1: t1, ...) - */ - case Return(arguments: Environment) - - /** - * e.g. let prompt = reset { (x, ...) => s }; s - */ - case Reset(name: Variable, frame: Clause, rest: Statement) - - /** - * e.g. resume k; s - */ - case Resume(stack: Variable, rest: Statement) - - /** - * e.g. let k = shift prompt; s - */ - case Shift(name: Variable, prompt: Variable, rest: Statement) - - /** - * let x = #infix_add(v1, ...); s - */ - case ForeignCall(name: Variable, builtin: String, arguments: Environment, rest: Statement) - - /** - * let x = 42; s - */ - case LiteralInt(name: Variable, value: Long, rest: Statement) - - /** - * let x = 42.2; s - */ - case LiteralDouble(name: Variable, value: Double, rest: Statement) - - /** - * let x = "hello"; s - */ - case LiteralUTF8String(name: Variable, utf8: Array[Byte], rest: Statement) - - /** - * Statement that is executed when a Hole is encountered. - */ - case Hole -} -export Statement.* - - -/** - * Types - */ -enum Type { - case Positive() - case Negative() - case Prompt() - case Stack() - case Int() - case Byte() - case Double() - case Reference(tpe: Type) -} -export Type.{ Positive, Negative } - -object builtins { - - /** - * Blocks types are interfaces with a single operation. - */ - val Apply: Tag = 0 - - val Unit: Tag = 0 - val UnitType = Positive() - - val True: Tag = 1 - val False: Tag = 0 - val BooleanType = Positive() - - val StringType = Positive() - - val SingletonRecord: Tag = 0 -} diff --git a/effekt/shared/src/main/scala/effekt/package.scala b/effekt/shared/src/main/scala/effekt/package.scala new file mode 100644 index 000000000..2e73bf2b1 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/package.scala @@ -0,0 +1,3 @@ +package effekt + +case class CoreTransformed(source: kiama.util.Source, core: effekt.core.ModuleDecl, main: effekt.core.Id) \ No newline at end of file diff --git a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala deleted file mode 100644 index 2f7e520e9..000000000 --- a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala +++ /dev/null @@ -1,125 +0,0 @@ -package effekt -package source - -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.symbols.* -import effekt.context.assertions.* -import effekt.source.Tree.Query - -import kiama.util.Source - - -/** - * Computes the capture of each subexpression, provided that Typer already solved the constraints. - * - * TODO annotate unbox in Typer and use it here - */ -object AnnotateCaptures extends Phase[Typechecked, Typechecked], Query[Unit, CaptureSet] { - - val phaseName = "annotate-captures" - - def run(input: Typechecked)(using C: Context) = - // reset allCaptures since multiple runs of this phase may pollute it with outdated information - allCaptures = Nil - Context.timed(phaseName, input.source.name) { annotate(input.tree, input.source) } - Some(input) - - // We collect all captures while traversing the tree. - // They are then annotated to the source file for LSP to query. - private var allCaptures: List[(source.Tree, symbols.CaptureSet)] = Nil - - def empty: CaptureSet = CaptureSet.empty - def combine(r1: CaptureSet, r2: CaptureSet): CaptureSet = r1 ++ r2 - - override def expr(using Context, Unit) = { - case source.Var(id) => id.symbol match { - case b: BlockSymbol => captureOf(b) - case x: ValueSymbol => CaptureSet.empty - } - - case e @ source.Assign(id, expr) => - query(expr) ++ captureOf(id.symbol.asBlockSymbol) - - case l @ source.Box(annotatedCapture, block) => - query(block) - CaptureSet.empty - - case source.Unbox(term) => - val capt = Context.inferredTypeOption(term) match { - case Some(BoxedType(_, capture: CaptureSet)) => capture - case _ => Context.panic(pp"Should have an inferred a concrete capture set for ${term}") - } - query(term) ++ capt - - case t @ source.Do(effect, op, targs, vargs, bargs) => - val cap = Context.annotation(Annotations.CapabilityReceiver, t) - combineAll(vargs.map(query)) ++ combineAll(bargs.map(query)) ++ CaptureSet(cap.capture) - - case t @ source.TryHandle(prog, handlers) => - val progCapture = query(prog) - val boundCapture = boundCapabilities(t) - val usedCapture = combineAll(handlers.map(query)) - (progCapture -- boundCapture) ++ usedCapture - - case t @ source.Region(id, body) => - query(body) -- captureOf(id.symbol.asBlockSymbol) - - case c @ source.Call(target, targs, vargs, bargs) => - // TODO what's with unboxed value references??? - // maybe that's solved by inserting explicit box and unbox in Elaboration - val tcaps = target match { - case IdTarget(id) => captureOf(id.symbol.asBlockSymbol) - case ExprTarget(receiver) => query(receiver) - } - - tcaps ++ combineAll(vargs map query) ++ combineAll(bargs map query) - - case b @ source.BlockLiteral(tps, vps, bps, body) => - query(body) -- boundCapabilities(b) -- CaptureSet(bps.map(_.symbol.capture)) - } - - override def stmt(using Context, Unit) = { - // local state - case source.DefStmt(tree @ VarDef(id, annot, binding), rest) => - query(binding) ++ (query(rest) -- CaptureSet(tree.symbol.capture)) - } - - override def defn(using Context, Unit) = { - case tree @ source.FunDef(id, tps, vps, bps, ret, body) => - val cpt = query(body) -- boundCapabilities(tree) -- CaptureSet(bps.map(_.symbol.capture)) - // TODO Why do we need to update the annotation on the symbol here? Is the inferred capture for recursive functions - // wrong? Problematic example: examples/benchmarks/tree.effekt (chooseHandler has the empty set, but should have {this}) - Context.annotate(Annotations.Captures, tree.symbol, cpt) - cpt - - // regions - case tree @ RegDef(id, annot, region, binding) => - val regSymbol = region.symbol.asBlockSymbol - val regCapture = captureOf(regSymbol) - Context.annotate(Annotations.Captures, tree.symbol, regCapture) - query(binding) ++ regCapture - } - - def boundCapabilities(t: Tree)(using Context): CaptureSet = - val bound = Context.annotation(Annotations.BoundCapabilities, t) - CaptureSet(bound.map(_.capture)) - - def captureOf(b: BlockSymbol)(using Context): CaptureSet = - asConcreteCaptureSet(Context.captureOf(b)) - - def asConcreteCaptureSet(c: Captures)(using Context): CaptureSet = c match { - case c: CaptureSet => c - case _ => Context.panic("All capture unification variables should have been replaced by now.") - } - - def annotate(tree: source.ModuleDecl, src: Source)(using Context): Unit = - given Unit = (); - query(tree) - Context.annotate(Annotations.CaptureForFile, src, allCaptures) - - override def visit[T <: Tree](t: T)(visitor: T => CaptureSet)(using Context, Unit): CaptureSet = - val capt = visitor(t) - Context.annotate(Annotations.InferredCapture, t, capt) - allCaptures = (t, capt) :: allCaptures - capt -} diff --git a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala deleted file mode 100644 index 1816ad48c..000000000 --- a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala +++ /dev/null @@ -1,158 +0,0 @@ -package effekt -package source - -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.symbols.* -import effekt.context.assertions.* -import effekt.source.Tree.Rewrite - -/** - * Transformation on source trees that translates programs into explicit capability-passing style - * - * That is, block parameters are introduced to bind capabilities and arguments are introduced at - * the call sites. Resume is currently _not_ introduced as a block parameter. - * - * Also applies elaboration which conceptually is done by Typer. After this phase - * - there are no `Do` nodes anymore (replaced by method calls) - * - `l.foo(x)` where `foo` is a function and not an operation is desugared to `foo(l, x)` - * - all method calls `l.op()` will have `op : Operation` - * - all regions are explicitly bound by `region this { ... }` constructs. - * - */ -object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { - - val phaseName = "explicit-capabilities" - - def run(input: Typechecked)(using C: Context) = - val rewritten = C.timed(phaseName, input.source.name) { rewrite(input.tree) } - - Some(input.copy(tree = rewritten)) - - override def defn(using Context) = { - case f @ FunDef(id, tps, vps, bps, ret, body) => - val capabilities = Context.annotation(Annotations.BoundCapabilities, f) - val capParams = capabilities.map(definitionFor) - f.copy(bparams = bps ++ capParams, body = rewrite(body)) - case extDef @ ExternDef(capture, id, tparams, vparams, bparams, ret, bodies) => - val rewrittenBodies = bodies.map { rewrite } - extDef.copy(bodies = rewrittenBodies) - } - - override def expr(using Context) = { - - // an effect call -- translate to method call on the inferred capability - case c @ Do(effect, id, targs, vargs, bargs) => - val transformedValueArgs = vargs.map(rewrite) - val transformedBlockArgs = bargs.map(rewrite) - - // the receiver of this effect operation call - val receiver = Context.annotation(Annotations.CapabilityReceiver, c) - - // for bidirectional effects - val others = Context.annotation(Annotations.CapabilityArguments, c) - - // the remaining capabilities are provided as arguments - val capabilityArgs = others.map(referenceToCapability) - - val typeArguments = Context.annotation(Annotations.TypeArguments, c) - val typeArgs = typeArguments.map { e => ValueTypeTree(e) } - - // construct the member selection on the capability as receiver - MethodCall(referenceToCapability(receiver).inheritPosition(id), id, typeArgs, transformedValueArgs, transformedBlockArgs ++ capabilityArgs) - - // the function is a field, desugar to select - case c @ Call(fun: IdTarget, targs, List(receiver), Nil) if fun.definition.isInstanceOf[Field] => - Select(rewrite(receiver), fun.id) - - case c @ MethodCall(receiver, id, targs, vargs, bargs) => - val valueArgs = vargs.map { a => rewrite(a) } - val blockArgs = bargs.map { a => rewrite(a) } - - val capabilities = Context.annotation(Annotations.CapabilityArguments, c) - val capabilityArgs = capabilities.map(referenceToCapability) - - val typeArguments = Context.annotation(Annotations.TypeArguments, c) - val typeArgs = typeArguments.map { e => ValueTypeTree(e) } - - val recv = rewrite(receiver) - - MethodCall(recv, id, typeArgs, valueArgs, blockArgs ++ capabilityArgs) - - case c @ Call(recv, targs, vargs, bargs) => - val receiver = rewrite(recv) - val valueArgs = vargs.map { a => rewrite(a) } - val blockArgs = bargs.map { a => rewrite(a) } - - val capabilities = Context.annotation(Annotations.CapabilityArguments, c) - val capabilityArgs = capabilities.map(referenceToCapability) - - val typeArguments = Context.annotation(Annotations.TypeArguments, c) - val typeArgs = typeArguments.map { e => ValueTypeTree(e) } - - Call(receiver, typeArgs, valueArgs, blockArgs ++ capabilityArgs) - - case h @ TryHandle(prog, handlers) => - val body = rewrite(prog) - - val capabilities = Context.annotation(Annotations.BoundCapabilities, h) - - assert(capabilities.size == handlers.size) - - // here we use the invariant that the order of capabilities is the same as the order of handlers - - val hs = (handlers zip capabilities).map { - case (h, cap) => visit(h) { - // here we annotate the synthesized capability - case h @ Handler(_, impl) => Handler(Some(definitionFor(cap)), rewrite(impl)) - } - } - - TryHandle(body, hs) - - case n @ source.New(impl @ Implementation(interface, clauses)) => { - val cs = clauses map { - case op @ OpClause(id, tparams, vparams, bparams, ret, body, resume) => { - val capabilities = Context.annotation(Annotations.BoundCapabilities, op) - val capabilityParams = capabilities.map(definitionFor) - OpClause(id, tparams, vparams, bparams ++ capabilityParams, ret, rewrite(body), resume) - } - } - val newImpl = Implementation(interface, cs) - val tree = source.New(newImpl) - Context.copyAnnotations(impl, newImpl) - tree - } - - case b @ source.BlockLiteral(tps, vps, bps, body) => - val capabilities = Context.annotation(Annotations.BoundCapabilities, b) - val capParams = capabilities.map(definitionFor) - source.BlockLiteral(tps, vps, bps ++ capParams, rewrite(body)) - } - - override def rewrite(body: ExternBody)(using context.Context): ExternBody = - body match { - case b @ source.ExternBody.StringExternBody(ff, body) => - val rewrittenTemplate = - body.copy( - args = body.args.map { rewrite } - ) - b.copy(template = rewrittenTemplate) - case b @ source.ExternBody.EffektExternBody(ff, body) => - val rewrittenBody = rewrite(body) - b.copy(body = rewrittenBody) - case u: source.ExternBody.Unsupported => u - } - - def referenceToCapability(capability: BlockParam)(using C: Context): Var = - val id = IdRef(Nil, capability.name.name) - C.assignSymbol(id, capability) - val ref: Var = Var(id) - C.annotate(Annotations.InferredBlockType, ref, C.blockTypeOf(capability)) - ref - - def definitionFor(s: symbols.BlockParam)(using C: Context): source.BlockParam = - val id = IdDef(s.name.name) - C.assignSymbol(id, s) - val tree: source.BlockParam = source.BlockParam(id, s.tpe.map { source.BlockTypeTree.apply }) - tree -} diff --git a/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala b/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala deleted file mode 100644 index dd32522a4..000000000 --- a/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala +++ /dev/null @@ -1,82 +0,0 @@ -package effekt.source - -import effekt.Phase -import effekt.PhaseResult.Typechecked -import effekt.context.{ Context, Annotations } -import effekt.source.Tree.Rewrite - -object ResolveExternDefs extends Phase[Typechecked, Typechecked] { - - val phaseName = "resolve-extern-defs" - - override def run(input: Typechecked)(using C: Context) = input match { - case Typechecked(source, tree, mod) => Some(Typechecked(source, rewrite(tree), mod)) - } - - def supported(using Context): List[String] = Context.compiler.supportedFeatureFlags - - def defaultExternBody(warning: String)(using Context): ExternBody = - ExternBody.Unsupported(Context.plainMessage(warning, kiama.util.Severities.Warning)) - - def rewrite(decl: ModuleDecl)(using Context): ModuleDecl = decl match { - case ModuleDecl(path, includes, defs) => - ModuleDecl(path, includes, defs.flatMap(rewrite)) - } - - def findPreferred(bodies: List[ExternBody])(using Context): ExternBody = { - // IMPORTANT: Should be deterministic. - bodies.filter { b => b.featureFlag.matches(supported) } - .minByOption { b => - supported.indexOf(b.featureFlag) match { - case -1 => supported.length - case p => p - } - }.getOrElse { - val featureFlags = bodies.map(_.featureFlag) - defaultExternBody(s"Extern definition is not supported as it is only defined for feature flags ${featureFlags.mkString(", ")}," + - s"but the current backend only supports ${Context.compiler.supportedFeatureFlags.mkString(", ")}.") - } - } - - def rewrite(defn: Def)(using Context): Option[Def] = Context.focusing(defn) { - case Def.ExternDef(capture, id, tparams, vparams, bparams, ret, bodies) => - findPreferred(bodies) match { - case body@ExternBody.StringExternBody(featureFlag, template) => - if (featureFlag.isDefault) { - Context.warning(s"Extern definition ${id} contains extern string without feature flag. This will likely not work in other backends, " - + s"please annotate it with a feature flag (Supported by the current backend: ${Context.compiler.supportedFeatureFlags.mkString(", ")})") - } - - val d = Def.ExternDef(capture, id, tparams, vparams, bparams, ret, List(body)) - Context.copyAnnotations(defn, d) - Some(d) - case ExternBody.EffektExternBody(featureFlag, body) => - val d = Def.FunDef(id, tparams, vparams, bparams, Some(ret), body) - Context.copyAnnotations(defn, d) - Context.annotate(Annotations.BoundCapabilities, d, Nil) // TODO ?? - Some(d) - case u: ExternBody.Unsupported => - val d = Def.ExternDef(capture, id, tparams, vparams, bparams, ret, List(u)) - Context.copyAnnotations(defn, d) - Some(d) - } - - case Def.ExternInclude(featureFlag, path, contents, id) if featureFlag.matches(supported) => - if (featureFlag.isDefault) { - val supported = Context.compiler.supportedFeatureFlags.mkString(", ") - Context.warning("Found extern include without feature flag. It is likely that this will fail in other backends, " - + s"please annotate it with a feature flag (Supported in current backend: ${supported})") - } - - Some(defn) - case Def.ExternInclude(_, _, _, _) => None // Drop, not for this backend - - // recurse into namespaces - case Def.NamespaceDef(id, defs) => - val d = Def.NamespaceDef(id, defs.flatMap(rewrite)) - Context.copyAnnotations(defn, d) - Some(d) - case defn => Some(defn) - } - -} diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala deleted file mode 100644 index dd4985350..000000000 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ /dev/null @@ -1,815 +0,0 @@ -package effekt -package source - -import effekt.context.Context -import effekt.symbols.Symbol - -import scala.annotation.tailrec - -/** - * Data type representing source program trees. - * - * ----------[[ effekt.source.Tree ]]---------- - * - * ─ [[ Tree ]] - * │─ [[ NoSource ]] - * │─ [[ Comment ]] - * │─ [[ FeatureFlag ]] - * │ │─ [[ NamedFeatureFlag ]] - * │ │─ [[ Default ]] - * │ - * │─ [[ ExternBody ]] - * │ │─ [[ StringExternBody ]] - * │ │─ [[ EffektExternBody ]] - * │ │─ [[ Unsupported ]] - * │ - * │─ [[ Id ]] - * │ │─ [[ IdDef ]] - * │ │─ [[ IdRef ]] - * │ - * │─ [[ Named ]] - * │ │─ [[ Definition ]] - * │ │─ [[ Reference ]] - * │ - * │─ [[ ModuleDecl ]] - * │─ [[ Include ]] - * │─ [[ Stmt ]] - * │ │─ [[ DefStmt ]] - * │ │─ [[ ExprStmt ]] - * │ │─ [[ Return ]] - * │ │─ [[ BlockStmt ]] - * │ - * │─ [[ Term ]] - * │ │─ [[ Var ]] - * │ │─ [[ Assign ]] - * │ │─ [[ Literal ]] - * │ │─ [[ Hole ]] - * │ │─ [[ Box ]] - * │ │─ [[ Unbox ]] - * │ │─ [[ Select ]] - * │ │─ [[ Do ]] - * │ │─ [[ Call ]] - * │ │─ [[ MethodCall ]] - * │ │─ [[ If ]] - * │ │─ [[ While ]] - * │ │─ [[ Match ]] - * │ │─ [[ TryHandle ]] - * │ │─ [[ Region ]] - * │ │─ [[ BlockLiteral ]] - * │ │─ [[ New ]] - * │ - * │─ [[ CallTarget ]] - * │ │─ [[ IdTarget ]] - * │ │─ [[ ExprTarget ]] - * │ - * │─ [[ MatchClause ]] - * │─ [[ MatchGuard ]] - * │ │─ [[ BooleanGuard ]] - * │ │─ [[ PatternGuard ]] - * │ - * │─ [[ MatchPattern ]] - * │ │─ [[ AnyPattern ]] - * │ │─ [[ TagPattern ]] - * │ │─ [[ IgnorePattern ]] - * │ │─ [[ LiteralPattern ]] - * │ - * │─ [[ Type ]] - * │ │─ [[ ValueType ]] - * │ │─ [[ BlockType ]] - * │ - * │─ [[ Effectful ]] - * │─ [[ Effects ]] - * │─ [[ CaptureSet ]] - * - * -------------------------------------------------------------- - * - * We extend product to allow reflective access by Kiama. - */ -sealed trait Tree extends Product { - def inheritPosition(from: Tree)(implicit C: Context): this.type = { - C.positions.dupPos(from, this); - this - } -} - -/** - * Used for builtin and synthesized trees - */ -case object NoSource extends Tree - -// only used by the lexer -case class Comment() extends Tree - -/** - * Used to mark externs for different backends - */ -enum FeatureFlag extends Tree { - case NamedFeatureFlag(id: String) - case Default - - def matches(name: String, matchDefault: Boolean = true): Boolean = this match { - case NamedFeatureFlag(n) if n == name => true - case Default => matchDefault - case _ => false - } - def isDefault: Boolean = this == Default - - def matches(names: List[String]): Boolean = this match { - case NamedFeatureFlag(n) if names.contains(n) => true - case Default => true - case _ => false - } - - override def toString: String = this match { - case FeatureFlag.NamedFeatureFlag(id) => id - case FeatureFlag.Default => "else" - } -} -object FeatureFlag { - extension (self: List[ExternBody]) { - @tailrec - def supportedByFeatureFlags(names: List[String]): Boolean = names match { - case Nil => false - case name :: other => - self.collectFirst { - case ExternBody.StringExternBody(flag, a) if flag.matches(name) => () - case ExternBody.EffektExternBody(flag, a) if flag.matches(name) => () - }.isDefined || (self.supportedByFeatureFlags(other)) - } - } -} - -sealed trait ExternBody extends Tree { - def featureFlag: FeatureFlag -} -object ExternBody { - case class StringExternBody(featureFlag: FeatureFlag, template: Template[source.Term]) extends ExternBody - case class EffektExternBody(featureFlag: FeatureFlag, body: source.Stmt) extends ExternBody - case class Unsupported(message: util.messages.EffektError) extends ExternBody { - override def featureFlag: FeatureFlag = FeatureFlag.Default - } -} - - -/** - * We distinguish between identifiers corresponding to - * - binding sites (IdDef) - * - use site (IdRef) - * in the syntax. This way, we can simplify the traversal in Namer - */ -sealed trait Id extends Tree { - def name: String - def symbol(using C: Context): Symbol = C.symbolOf(this) - def clone(using C: Context): Id -} -case class IdDef(name: String) extends Id { - def clone(using C: Context): IdDef = { - val copy = IdDef(name) - C.positions.dupPos(this, copy) - copy - } -} -case class IdRef(path: List[String], name: String) extends Id { - def clone(using C: Context): IdRef = { - val copy = IdRef(path, name) - C.positions.dupPos(this, copy) - copy - } -} - -sealed trait Named extends Tree - -// Something that later will be stored in the symbol table -sealed trait Definition extends Named { - def id: IdDef -} - -// Something that later can be looked up in the symbol table -sealed trait Reference extends Named { - def id: IdRef -} - -/** - * The type of whole compilation units - * - * Only a subset of definitions (FunDef and EffDef) is allowed on the toplevel - * - * A module declaration, the path should be an Effekt include path, not a system dependent file path - * - */ -case class ModuleDecl(path: String, includes: List[Include], defs: List[Def]) extends Tree -case class Include(path: String) extends Tree - -/** - * Parameters and arguments - */ -enum Param extends Definition { - case ValueParam(id: IdDef, tpe: Option[ValueType]) - case BlockParam(id: IdDef, tpe: Option[BlockType]) -} -export Param.* - - -/** - * Global and local definitions - * - * ----------[[ effekt.source.Def ]]---------- - * - * ─ [[ Def ]] - * │─ [[ FunDef ]] - * │─ [[ ValDef ]] - * │─ [[ RegDef ]] - * │─ [[ VarDef ]] - * │─ [[ DefDef ]] - * │─ [[ NamespaceDef ]] - * │─ [[ InterfaceDef ]] - * │─ [[ DataDef ]] - * │─ [[ RecordDef ]] - * │─ [[ TypeDef ]] - * │─ [[ EffectDef ]] - * │─ [[ ExternType ]] - * │─ [[ ExternDef ]] - * │─ [[ ExternResource ]] - * │─ [[ ExternInterface ]] - * │─ [[ ExternInclude ]] - * - * ------------------------------------------- - */ -enum Def extends Definition { - - case FunDef(id: IdDef, tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: Option[Effectful], body: Stmt) - case ValDef(id: IdDef, annot: Option[ValueType], binding: Stmt) - case RegDef(id: IdDef, annot: Option[ValueType], region: IdRef, binding: Stmt) - case VarDef(id: IdDef, annot: Option[ValueType], binding: Stmt) - case DefDef(id: IdDef, annot: Option[BlockType], block: Term) - - case NamespaceDef(id: IdDef, definitions: List[Def]) - - case InterfaceDef(id: IdDef, tparams: List[Id], ops: List[Operation]) - case DataDef(id: IdDef, tparams: List[Id], ctors: List[Constructor]) - case RecordDef(id: IdDef, tparams: List[Id], fields: List[ValueParam]) - - /** - * Type aliases like `type Matrix[T] = List[List[T]]` - */ - case TypeDef(id: IdDef, tparams: List[Id], tpe: ValueType) - - /** - * Effect aliases like `effect Set = { Get, Put }` - */ - case EffectDef(id: IdDef, tparams: List[Id], effs: Effects) - - /** - * Only valid on the toplevel! - */ - case ExternType(id: IdDef, tparams: List[Id]) - - case ExternDef(capture: CaptureSet, id: IdDef, - tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: Effectful, - bodies: List[ExternBody]) extends Def - - case ExternResource(id: IdDef, tpe: BlockType) - - case ExternInterface(id: IdDef, tparams: List[Id]) - - /** - * Namer resolves the path and loads the contents in field [[contents]] - * - * @note Storing content and id as user-visible fields is a workaround for the limitation that Enum's cannot - * have case specific refinements. - */ - case ExternInclude(featureFlag: FeatureFlag, path: String, var contents: Option[String] = None, val id: IdDef = IdDef("")) -} -object Def { - type Extern = ExternType | ExternDef | ExternResource | ExternInterface | ExternInclude - type Declaration = InterfaceDef | DataDef | RecordDef - type Alias = TypeDef | EffectDef - type Toplevel = FunDef | ValDef | DefDef | Alias | Extern | Declaration - type Local = FunDef | ValDef | DefDef | Alias | VarDef -} -export Def.* - - -/** - * ----------[[ effekt.source.Stmt ]]---------- - * - * ─ [[ Stmt ]] - * │─ [[ DefStmt ]] - * │─ [[ ExprStmt ]] - * │─ [[ Return ]] - * │─ [[ BlockStmt ]] - * - * -------------------------------------------- - * - */ -enum Stmt extends Tree { - case DefStmt(d: Def, rest: Stmt) - case ExprStmt(d: Term, rest: Stmt) - case Return(d: Term) - case BlockStmt(stmts: Stmt) -} -export Stmt.* - - -/** - * In our source language, almost everything is an expression. - * Effectful calls, if, while, ... - * - * ----------[[ effekt.source.Term ]]---------- - * - * ─ [[ Term ]] - * │─ [[ Var ]] - * │─ [[ Assign ]] - * │─ [[ Literal ]] - * │─ [[ Hole ]] - * │─ [[ Box ]] - * │─ [[ Unbox ]] - * │─ [[ Select ]] - * │─ [[ Do ]] - * │─ [[ Call ]] - * │─ [[ MethodCall ]] - * │─ [[ If ]] - * │─ [[ While ]] - * │─ [[ Match ]] - * │─ [[ TryHandle ]] - * │─ [[ Region ]] - * │─ [[ BlockLiteral ]] - * │─ [[ New ]] - * - * -------------------------------------------- - */ -enum Term extends Tree { - - // Variable / Value use (can now also stand for blocks) - case Var(id: IdRef) extends Term, Reference - case Assign(id: IdRef, expr: Term) extends Term, Reference - - case Literal(value: Any, tpe: symbols.ValueType) - case Hole(stmts: Stmt) - - // Boxing and unboxing to represent first-class values - case Box(capt: Option[CaptureSet], block: Term) - case Unbox(term: Term) - - /** - * Models: - * - field selection, i.e., `record.field` (receiver is an expression, result is an expression) - * - future: nested capability / module selection, i.e., `mymod.nested.foo` (receiver is a block, result is a block) - * - * The resolved target can help to determine whether the receiver needs to be type-checked as first- or second-class. - */ - case Select(receiver: Term, id: IdRef) extends Term, Reference - - /** - * A call to an effect operation, i.e., `do raise()`. - * - * The [[effect]] is the optionally annotated effect type (not possible in source ATM). In the future, this could - * look like `do Exc.raise()`, or `do[Exc] raise()`, or do[Exc].raise(), or simply Exc.raise() where Exc is a type. - */ - case Do(effect: Option[BlockType.BlockTypeRef], id: IdRef, targs: List[ValueType], vargs: List[Term], bargs: List[Term]) extends Term, Reference - - /** - * A call to either an expression, i.e., `(fun() { ...})()`; or a named function, i.e., `foo()` - */ - case Call(target: CallTarget, targs: List[ValueType], vargs: List[Term], bargs: List[Term]) - - /** - * Models: - * - uniform function call, i.e., `list.map { ... }` (receiver is an expression, result is an expression) - * - capability call, i.e., `exc.raise()` (receiver is a block, result is an expression) - * - * The resolved target can help to determine whether the receiver needs to be type-checked as first- or second-class. - */ - case MethodCall(receiver: Term, id: IdRef, targs: List[ValueType], vargs: List[Term], bargs: List[Term]) extends Term, Reference - - // Control Flow - case If(guards: List[MatchGuard], thn: Stmt, els: Stmt) - case While(guards: List[MatchGuard], block: Stmt, default: Option[Stmt]) - case Match(scrutinees: List[Term], clauses: List[MatchClause], default: Option[Stmt]) - - /** - * Handling effects - * - * try { - * - * } with : { ... } - * - * Each with-clause is modeled as an instance of type [[Handler]]. - */ - case TryHandle(prog: Stmt, handlers: List[Handler]) - case Region(id: IdDef, body: Stmt) extends Term, Definition - - /** - * Lambdas / function literals (e.g., { x => x + 1 }) - */ - case BlockLiteral(tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], body: Stmt) extends Term - case New(impl: Implementation) -} -export Term.* - -// Smart Constructors for literals -// ------------------------------- -def UnitLit(): Literal = Literal((), symbols.builtins.TUnit) -def IntLit(value: Long): Literal = Literal(value, symbols.builtins.TInt) -def BooleanLit(value: Boolean): Literal = Literal(value, symbols.builtins.TBoolean) -def DoubleLit(value: Double): Literal = Literal(value, symbols.builtins.TDouble) -def StringLit(value: String): Literal = Literal(value, symbols.builtins.TString) -def CharLit(value: Int): Literal = Literal(value, symbols.builtins.TChar) - -type CallLike = Call | Do | Select | MethodCall - - -enum CallTarget extends Tree { - - // potentially overloaded - case IdTarget(id: IdRef) extends CallTarget, Reference - - // not overloaded - case ExprTarget(receiver: Term) -} -export CallTarget.* - - -// Declarations -// ------------ -case class Constructor(id: IdDef, tparams: List[Id], params: List[ValueParam]) extends Definition -case class Operation(id: IdDef, tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: Effectful) extends Definition - -// Implementations -// --------------- - -/** - * An implementation of a given interface - * - * { - * def = ... - * } - * - * Called "template" or "class" in other languages. - */ -case class Implementation(interface: BlockType.BlockTypeRef, clauses: List[OpClause]) extends Reference { - def id = interface.id -} - -/** - * A handler is a pair of an optionally named capability and the handler [[Implementation]] - */ -case class Handler(capability: Option[BlockParam] = None, impl: Implementation) extends Reference { - def effect = impl.interface - def clauses = impl.clauses - def id = impl.id -} - -// `ret` is an optional user-provided type annotation for the return type -// currently the annotation is rejected by [[Typer]] -- after that phase, `ret` should always be `None` -case class OpClause(id: IdRef, tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: Option[Effectful], body: Stmt, resume: IdDef) extends Reference - -// Pattern Matching -// ---------------- - -case class MatchClause(pattern: MatchPattern, guards: List[MatchGuard], body: Stmt) extends Tree - -enum MatchGuard extends Tree { - - case BooleanGuard(condition: Term) - - /** - * i.e. is - */ - case PatternGuard(scrutinee: Term, pattern: MatchPattern) -} -export MatchGuard.* - -enum MatchPattern extends Tree { - - /** - * Pattern matching anything: - * - * case a => ... - */ - case AnyPattern(id: IdDef) extends MatchPattern, Definition - - /** - * Pattern matching on a constructor - * - * case Cons(a, as) => ... - */ - case TagPattern(id: IdRef, patterns: List[MatchPattern]) extends MatchPattern, Reference - - /** - * A wildcard pattern ignoring the matched value - * - * case _ => ... - */ - case IgnorePattern() - - /** - * A pattern that matches a single literal value - */ - case LiteralPattern(l: Literal) - - /** - * A pattern for multiple values - * - * case a, b => ... - * - * Currently should *only* occur in lambda-cases during Parsing - */ - case MultiPattern(patterns: List[MatchPattern]) extends MatchPattern -} -export MatchPattern.* - - -/** - * Types and Effects - * - * TODO generalize to blocks that can take blocks - * - * ----------[[ effekt.source.Type ]]---------- - * - * ─ [[ Type ]] - * │─ [[ ValueType ]] - * │ │─ [[ ValueTypeTree ]] - * │ │─ [[ BoxedType ]] - * │ │─ [[ ValueTypeRef ]] - * │ - * │─ [[ BlockType ]] - * │ │─ [[ BlockTypeTree ]] - * │ │─ [[ FunctionType ]] - * │ │─ [[ BlockTypeRef ]] - * │ - * - * -------------------------------------------- - */ -sealed trait Type extends Tree - -/** - * Value Types - */ -enum ValueType extends Type { - - /** - * Trees that represent inferred or synthesized types (not present in the source) - */ - case ValueTypeTree(tpe: symbols.ValueType) - - /** - * Types of first-class functions - */ - case BoxedType(tpe: BlockType, capt: CaptureSet) - - // Bound occurrences (args can be empty) - case ValueTypeRef(id: IdRef, args: List[ValueType]) extends ValueType, Reference -} -export ValueType.* - -/** - * Block Types - */ -enum BlockType extends Type { - - /** - * Trees that represent inferred or synthesized types (not present in the source) - */ - case BlockTypeTree(eff: symbols.BlockType) - case FunctionType(tparams: List[Id], vparams: List[ValueType], bparams: List[(Option[IdDef], BlockType)], result: ValueType, effects: Effects) - case BlockTypeRef(id: IdRef, args: List[ValueType]) extends BlockType, Reference -} - -export BlockType.* - -// We have Effectful as a tree in order to apply code actions on it (see Server.inferEffectsAction) -case class Effectful(tpe: ValueType, eff: Effects) extends Tree - -/** - * Represents an annotated set of effects. Before name resolution, we cannot know - * the concrete nature of its elements (so it is generic [[BlockTypeRef]]). - */ -case class Effects(effs: List[BlockType.BlockTypeRef]) extends Tree -object Effects { - val Pure: Effects = Effects() - def apply(effs: BlockTypeRef*): Effects = Effects(effs.toSet) - def apply(effs: Set[BlockTypeRef]): Effects = Effects(effs.toList) -} - -case class CaptureSet(captures: List[IdRef]) extends Tree - - -object Named { - - type Params = ValueParam | BlockParam - type Externs = ExternDef | ExternResource | ExternInterface | ExternType - type Defs = FunDef | ValDef | VarDef | DefDef | RegDef | InterfaceDef | DataDef | RecordDef | TypeDef | EffectDef - type Definitions = Externs | Defs | Params | Operation | Constructor | Region | AnyPattern - - type Types = ValueTypeRef | BlockTypeRef - type Vars = Var | Assign - type Calls = Do | Select | MethodCall | IdTarget - type References = Types | Vars | Calls | TagPattern | Handler | OpClause | Implementation - - type ResolvedDefinitions[T <: Definitions] = T match { - // Defs - case FunDef => symbols.UserFunction - case ValDef => symbols.Binder.ValBinder // export Binder.* doesn't seem to work here (maybe because the packages are cyclic?) - case VarDef => symbols.Binder.VarBinder - case RegDef => symbols.Binder.RegBinder - case DefDef => symbols.Binder.DefBinder - case InterfaceDef => symbols.BlockTypeConstructor.Interface - case DataDef => symbols.TypeConstructor.DataType - case RecordDef => symbols.TypeConstructor.Record - case TypeDef => symbols.TypeAlias - case EffectDef => symbols.EffectAlias - - // Params - case ValueParam => symbols.ValueParam - case BlockParam => symbols.TrackedParam.BlockParam - - // Externs - case ExternDef => symbols.ExternFunction - case ExternResource => symbols.TrackedParam.BlockParam - case ExternInterface => symbols.BlockTypeConstructor.ExternInterface - case ExternType => symbols.TypeConstructor.ExternType - - // Others - case Operation => symbols.Operation - case Constructor => symbols.Constructor - case Region => symbols.TrackedParam - case AnyPattern => symbols.ValueParam - } - - type ResolvedReferences[T <: References] = T match { - // Types - case ValueTypeRef => symbols.TypeConstructor - case BlockTypeRef => symbols.BlockTypeConstructor - - // Vars - case Var => symbols.TermSymbol - case Assign => symbols.RefBinder - - // CallLike - case Do => symbols.Operation - case Select => symbols.Field - case MethodCall => symbols.Operation | symbols.CallTarget | symbols.BlockParam - case IdTarget => symbols.TermSymbol - - // Others - case Handler => symbols.BlockTypeConstructor.Interface - case OpClause => symbols.Operation - case Implementation => symbols.BlockTypeConstructor.Interface - case TagPattern => symbols.Constructor - } - - extension [T <: Definitions](t: T & Definition) { - def symbol(using C: Context): ResolvedDefinitions[T] = C.symbolOf(t).asInstanceOf - } - extension [T <: References](t: T & Reference) { - def definition(using C: Context): ResolvedReferences[T] = C.symbolOf(t).asInstanceOf - } - -} -export Named.symbol - -// MOVE TO NAMER -object Resolvable { - - // Value Types - // ----------- - extension (t: ValueType) { - def resolve(using C: Context): symbols.ValueType = C.resolvedType(t).asInstanceOf - } - - // BLock Types - // ----------- - // we need to avoid widening, so here we define BlockType as a sum without a common parent - // (see https://github.com/lampepfl/dotty/issues/16299) - type BlockTypes = BlockTypeTree | FunctionType | BlockTypeRef - - type Resolved[T <: BlockTypes] = T match { - case BlockTypeTree => symbols.BlockType - case FunctionType => symbols.FunctionType - case BlockTypeRef => symbols.InterfaceType - } - - extension [T <: BlockTypes] (t: T) { - def resolve(using C: Context): Resolved[T] = C.resolvedType(t).asInstanceOf - } - - // Capture Sets - // ------------ - extension (capt: source.CaptureSet) { - def resolve(using C: Context): symbols.CaptureSet = C.resolvedCapture(capt) - } -} -export Resolvable.resolve - - -object Tree { - - // Generic traversal of trees, applying the partial function `f` to every contained - // element of type Tree. - def visit(obj: Any)(f: PartialFunction[Tree, Unit]): Unit = obj match { - case t: Iterable[t] => t.foreach { t => visit(t)(f) } - case p: Product => p.productIterator.foreach { - case t: Tree => f(t) - case other => () - } - case leaf => () - } - - // This solution is between a fine-grained visitor and a untyped and unsafe traversal. - trait Rewrite extends util.Structural { - - def Context(using C: Context): Context = C - - // Hooks to override - def expr(using Context): PartialFunction[Term, Term] = PartialFunction.empty - def stmt(using Context): PartialFunction[Stmt, Stmt] = PartialFunction.empty - def defn(using Context): PartialFunction[Def, Def] = PartialFunction.empty - - // Entrypoints to use the traversal on, defined in terms of the above hooks - def rewrite(e: Term)(using Context): Term = structuralVisit(e, expr) - def rewrite(t: Def)(using Context): Def = structuralVisit(t, defn) - def rewrite(t: Stmt)(using Context): Stmt = structuralVisit(t, stmt) - - def rewrite(e: ModuleDecl)(using Context): ModuleDecl = structuralVisit(e) - def rewrite(h: Handler)(using Context): Handler = structuralVisit(h) - def rewrite(i: Implementation)(using Context): Implementation = structuralVisit(i) - def rewrite(h: OpClause)(using Context): OpClause = structuralVisit(h) - def rewrite(c: MatchClause)(using Context): MatchClause = structuralVisit(c) - def rewrite(c: MatchGuard)(using Context): MatchGuard = structuralVisit(c) - def rewrite(t: source.CallTarget)(using Context): source.CallTarget = structuralVisit(t) - def rewrite(b: ExternBody)(using Context): source.ExternBody = structuralVisit(b) - - /** - * Hook that can be overridden to perform an action at every node in the tree - * - * Copies all annotations and position information from source to target - */ - def visit[T](source: T)(visitor: T => T)(using Context): T = - val target = visitor(source) - (source, target) match { - case (src: Tree, tgt: Tree) => - tgt.inheritPosition(src) - Context.copyAnnotations(src, tgt) - case _ => - } - target - - inline def structuralVisit[T](sc: T, p: PartialFunction[T, T])(using Context): T = - visit(sc) { t => rewriteStructurally(t, p) } - - inline def structuralVisit[T](sc: T)(using Context): T = - visit(sc) { t => rewriteStructurally(t) } - } - - trait Visit[Ctx] extends Query[Ctx, Unit] { - override def empty = () - override def combine(r1: Unit, r2: Unit): Unit = () - override def combineAll(rs: List[Unit]): Unit = () - } - - trait Query[Ctx, Res] extends util.Structural { - - def empty: Res - def combine(r1: Res, r2: Res): Res - def combineAll(rs: List[Res]): Res = rs.foldLeft(empty)(combine) - - // Hooks to override - def expr(using Context, Ctx): PartialFunction[Term, Res] = PartialFunction.empty - def stmt(using Context, Ctx): PartialFunction[Stmt, Res] = PartialFunction.empty - def defn(using Context, Ctx): PartialFunction[Def, Res] = PartialFunction.empty - - /** - * Hook that can be overridden to perform an action at every node in the tree - */ - def visit[T <: Tree](t: T)(visitor: T => Res)(using Context, Ctx): Res = scoped { visitor(t) } - - /** - * Hook that can be overriden to perform something for each new lexical scope - */ - def scoped(action: => Res)(using Context, Ctx): Res = action - - // - // Entrypoints to use the traversal on, defined in terms of the above hooks - def query(e: Term)(using C: Context, ctx: Ctx): Res = structuralQuery(e, expr) - def query(t: Def)(using C: Context, ctx: Ctx): Res = structuralQuery(t, defn) - def query(t: Stmt)(using C: Context, ctx: Ctx): Res = structuralQuery(t, stmt) - - def query(e: ModuleDecl)(using Context, Ctx): Res = structuralQuery(e) - def query(h: Handler)(using Context, Ctx): Res = structuralQuery(h) - def query(h: Implementation)(using Context, Ctx): Res = structuralQuery(h) - def query(h: OpClause)(using Context, Ctx): Res = structuralQuery(h) - def query(c: MatchClause)(using Context, Ctx): Res = structuralQuery(c) - def query(c: MatchGuard)(using Context, Ctx): Res = structuralQuery(c) - def query(b: ExternBody)(using Context, Ctx): Res = structuralQuery(b) - - def query(t: Template[Term])(using Context, Ctx): Res = - combineAll(t.args.map(query)) - - - inline def structuralQuery[T <: Tree](el: T, pf: PartialFunction[T, Res] = PartialFunction.empty)(using Context, Ctx): Res = - visit(el) { t => - if pf.isDefinedAt(el) then pf.apply(el) else queryStructurally(t, empty, combine) - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala deleted file mode 100644 index 1f238d0c0..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala +++ /dev/null @@ -1,93 +0,0 @@ -package effekt -package symbols - -import effekt.context.Context -import kiama.output.ParenPrettyPrinter - -import scala.language.implicitConversions -import TypePrinter.show - -object DeclPrinter extends ParenPrettyPrinter { - - override val defaultIndent = 2 - - def apply(t: Symbol)(implicit context: Context): String = - pretty(toDoc(t, context)).layout - - def toDoc(t: Symbol, context: Context): Doc = t match { - - case e @ Interface(name, tparams, List(op)) => - format("effect", op, op.annotatedResult, op.annotatedEffects) - - case e @ Interface(name, tparams, ops) => - val tps = show(tparams) - val effs = ops.map { op => format("def", op, op.annotatedResult, op.annotatedEffects) } - "effect" <+> name.toString <> tps <+> braces(nest(line <> vsep(effs)) <> line) - - case b @ ValBinder(name, tps, decl) => - val tpe = context.valueTypeOption(b).getOrElse { b.tpe.get } - pp"val ${name}: ${tpe}" - - case b: VarBinder => - val tpe = context.valueTypeOption(b).getOrElse { b.tpe.get } - pp"var ${b.name}: ${tpe}" - - case TypeAlias(name, tparams, tpe) => - val tps = show(tparams) - "type" <+> name.toString <> tps <+> "=" <+> pp"$tpe" - - case EffectAlias(name, tparams, eff) => - val tps = show(tparams) - "effect" <+> name.toString <> tps <+> "=" <+> pp"${eff}" - - case DataType(name, tparams, ctors) => - val tps = show(tparams) - val ctrs = ctors map { ctor => - format("def", ctor, ctor.annotatedResult, ctor.annotatedEffects) - } - "type" <+> name.toString <> tps <+> braces(nest(line <> vsep(ctrs)) <> line) - - case Record(name, tparams, ctor) => - val tps = show(tparams) - val ctrs = format("def", ctor, ctor.annotatedResult, ctor.annotatedEffects) - "type" <+> name.toString <> tps <+> braces(nest(line <> ctrs) <> line) - - case f: ExternFunction => - format("extern def", f, f.annotatedResult, f.annotatedEffects) - - case ExternType(name, tparams) => - val tps = show(tparams) - pp"extern type ${name}$tps" - - case ExternInterface(name, tparams) => - pp"extern interface ${name}${show(tparams)}" - - case ExternResource(name, tpe) => - pp"extern resource ${name}: ${tpe}" - - case c: Callable => - val tpe = context.functionTypeOption(c) - format("def", c, tpe.map { _.result }, tpe.map { _.effects }) - - case d: DefBinder => - val tpe = context.blockTypeOption(d).getOrElse { d.tpe.get } - pp"def ${ d.name }: ${ tpe }" - } - - def format(kw: String, f: Callable, result: Option[ValueType], effects: Option[Effects]): Doc = { - val tps = if (f.tparams.isEmpty) "" else s"[${f.tparams.mkString(", ")}]" - - val valueParams = f.vparams.map { p => pp"${p.name}: ${p.tpe.get}" }.mkString(", ") - val vps = if valueParams.isEmpty then "" else s"($valueParams)" - val bps = f.bparams.map { b => pp"{ ${b.name}: ${b.tpe.get} }" }.mkString("") - - val ps = if (vps.isEmpty && bps.isEmpty) "()" else s"$vps$bps" - - val returnType = for { - tpe <- result - eff <- effects - } yield pp": $tpe / $eff" - - s"$kw ${f.name}$tps$ps${returnType.getOrElse("")}" - } -} diff --git a/effekt/shared/src/main/scala/effekt/symbols/Name.scala b/effekt/shared/src/main/scala/effekt/symbols/Name.scala index 5f0b0de8c..55f25fb42 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Name.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Name.scala @@ -1,6 +1,5 @@ package effekt.symbols -import effekt.source.Id import effekt.context.Context sealed trait Name { @@ -41,8 +40,6 @@ case class QualifiedName(prefix: List[String], name: String) extends Name { // Pseudo-constructors to safely convert ids and strings into names. object Name { - def local(id: Id): LocalName = local(id.name) - def local(id: String): LocalName = LocalName(id) } diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala deleted file mode 100644 index ae7dad515..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ /dev/null @@ -1,290 +0,0 @@ -package effekt -package symbols - -import effekt.source.IdRef -import effekt.util.messages.ErrorReporter - -import scala.annotation.tailrec -import scala.collection.mutable - -/** - * An immutable container of bindings - */ -case class Bindings( - terms: Map[String, Set[TermSymbol]], // terms can be overloaded - types: Map[String, TypeSymbol], - captures: Map[String, Capture], - namespaces: Map[String, Bindings] -) - -object Bindings { - def empty: Bindings = Bindings(Map.empty, Map.empty, Map.empty, Map.empty) -} - -/** - * A mutable container of bindings - */ -class Namespace( - var terms: mutable.Map[String, Set[TermSymbol]], - var types: mutable.Map[String, TypeSymbol], - var captures: mutable.Map[String, Capture], - var namespaces: mutable.Map[String, Namespace] -) { - def importAll(other: Bindings): Unit = other match { - case Bindings(terms2, types2, captures2, namespaces2) => - terms2.foreach { case (k, syms) => syms.foreach(addTerm(k, _)) } - types2.foreach { case (k, v) => setType(k, v) } - captures2.foreach { case (k, v) => setCapture(k, v) } - namespaces2.foreach { case (k, v) => getNamespace(k).importAll(v) } - } - - def addTerm(name: String, sym: TermSymbol): Unit = - val before = terms.getOrElse(name, Set.empty) - terms.update(name, before + sym) - def setType(name: String, sym: TypeSymbol): Unit = - types.update(name, sym) - def setCapture(name: String, sym: Capture): Unit = - captures.update(name, sym) - - def getNamespace(name: String): Namespace = - namespaces.getOrElseUpdate(name, Namespace.empty) - - def operations: Map[String, Set[Operation]] = - types.values.toSet.flatMap { - case BlockTypeConstructor.Interface(_, _, operations) => operations.toSet - case _ => Set.empty - }.groupMap(_.name.name)(op => op) - - /** - * Convert to immutable bindings - */ - def toBindings: Bindings = Bindings( - terms.toMap, - types.toMap, - captures.toMap, - namespaces.map { case (k, v) => k -> v.toBindings }.toMap) -} -object Namespace { - def empty: Namespace = Namespace(mutable.Map.empty, mutable.Map.empty, mutable.Map.empty, mutable.Map.empty) -} - -object scopes { - - enum Scope { self => - - /** - * The toplevel global scope ("project scope") - */ - case Global(imports: Namespace, bindings: Namespace) - - /** - * A scope introducing a new namespace - */ - case Named(name: String, bindings: Namespace, outer: Scope) - - /** - * A local scope introduced by functions, blocks, etc. - */ - case Local(imports: Namespace, bindings: Namespace, outer: Scope) - - /** - * All scopes introduce (mutable) namespaces for their bindings - */ - def bindings: Namespace - - def imports(using E: ErrorReporter): Namespace = this match { - case s: Scope.Named => E.abort("Can only import at the top of a file or function definition.") - case s @ Scope.Global(imports, bindings) => imports - case s @ Scope.Local(imports, bindings, outer) => imports - } - } - - case class Scoping(modulePath: List[String], var scope: Scope) { - def importAs(imports: Bindings, path: List[String])(using E: ErrorReporter): Unit = - @tailrec - def go(path: List[String], in: Namespace): Unit = path match { - case pathSeg :: rest => go(rest, in.getNamespace(pathSeg)) - case Nil => in.importAll(imports) - } - go(path, scope.imports) - - // TODO check shadowing etc. Also here concrete functions will *always* shadow imports, - // regardless of the order of importing / defining. - def importAll(imports: Bindings)(using E: ErrorReporter): Unit = scope.imports.importAll(imports) - - /** - * Defines the scoping rules by searching with [[ search ]] in an - * inside-out manner through all nested scopes. - */ - private def first[T](path: List[String], scope: Scope)(select: Namespace => Option[T]): Option[T] = - - def qualified(path: List[String], bindings: Namespace): Option[T] = path match { - case Nil => select(bindings) - case pathSegment :: rest => bindings.namespaces.get(pathSegment).flatMap { - namespace => qualified(rest, namespace) - } - } - - scope match { - case Scope.Global(imports, bindings) => - qualified(path, bindings) orElse qualified(path, imports) - case Scope.Named(name, bindings, outer) => - qualified(path, bindings) orElse first(path, outer)(select) - case Scope.Local(imports, bindings, outer) => - qualified(path, bindings) orElse qualified(path, imports) orElse first(path, outer)(select) - } - - private def all[T](path: List[String], scope: Scope)(select: Namespace => T): List[T] = - - def qualified(path: List[String], bindings: Namespace): List[T] = path match { - case Nil => select(bindings) :: Nil - case pathSegment :: rest => bindings.namespaces.get(pathSegment).toList.flatMap { - namespace => qualified(rest, namespace) - } - } - - scope match { - case Scope.Global(imports, bindings) => - qualified(path, bindings) ++ qualified(path, imports) - case Scope.Named(name, bindings, outer) => - qualified(path, bindings) ++ all(path, outer)(select) - case Scope.Local(imports, bindings, outer) => - qualified(path, bindings) ++ qualified(path, imports) ++ all(path, outer)(select) - } - - /** - * Searches the nested scopes to find the first term. - * Fails if: - * - there are multiple matching terms in the same scope - * - there a no matching terms at all - */ - def lookupFirstTerm(id: IdRef)(using E: ErrorReporter): TermSymbol = - lookupFirstTermOption(id) getOrElse { E.abort(pp"Could not resolve term ${id}") } - - def lookupFirstTermOption(id: IdRef)(using E: ErrorReporter): Option[TermSymbol] = - first(id.path, scope) { _.terms.get(id.name).map { syms => - if (syms.size > 1) E.abort(pp"Ambiguous reference to ${id}") - else syms.head - }} - - def lookupType(id: IdRef)(using E: ErrorReporter): TypeSymbol = - lookupTypeOption(id.path, id.name) getOrElse { E.abort(pp"Could not resolve type ${id}") } - - def lookupTypeOption(path: List[String], name: String)(using E: ErrorReporter): Option[TypeSymbol] = - first(path, scope) { _.types.get(name) } - - def lookupCapture(id: IdRef)(using E: ErrorReporter): Capture = - first(id.path, scope) { _.captures.get(id.name) } getOrElse E.abort(pp"Could not resolve capture ${id}") - - def lookupOverloaded(id: IdRef, filter: TermSymbol => Boolean)(using ErrorReporter): List[Set[TermSymbol]] = - all(id.path, scope) { _.terms.getOrElse(id.name, Set.empty).filter(filter) } - - def lookupOverloadedMethod(id: IdRef, filter: TermSymbol => Boolean)(using ErrorReporter): List[Set[Operation]] = - all(id.path, scope) { namespace => - namespace.operations.getOrElse(id.name, Set.empty).filter(filter) - } - - def lookupOperation(path: List[String], name: String)(using ErrorReporter): List[Set[Operation]] = - all(path, scope) { namespace => - namespace.operations.getOrElse(name, Set.empty) - }.filter { namespace => namespace.nonEmpty } - - def lookupFunction(path: List[String], name: String)(using ErrorReporter): List[Set[Callable]] = - all(path, scope) { namespace => - namespace.terms.getOrElse(name, Set.empty).collect { case c: Callable if !c.isInstanceOf[Operation] => c } - }.filter { namespace => namespace.nonEmpty } - - def lookupFirstBlockParam(path: List[String], name: String)(using ErrorReporter): Set[BlockParam] = - first(path, scope) { namespace => - namespace.terms.get(name).map(set => - set.collect { case bp: BlockParam => bp } - ) - }.getOrElse(Set.empty) - - // can be a term OR a type symbol - def lookupFirst(path: List[String], name: String)(using E: ErrorReporter): Symbol = - lookupFirstOption(path, name) getOrElse { E.abort(s"Could not resolve ${name}") } - - def lookupFirstOption(path: List[String], name: String)(using E: ErrorReporter): Option[Symbol] = - first(path, scope) { bindings => - (bindings.terms.get(name).map(_.toList), bindings.types.get(name)) match { - case (Some(List(t)), None) => Some(t) - case (None, Some(t)) => Some(t) - // give precedence to the type level effect, if an equally named effect op is in scope - case (Some(List(t1: Operation)), Some(t2: Interface)) => Some(t2) - case (Some(t1), Some(t2)) => - E.abort(s"Ambiguous reference to ${name}. Can refer to a term or a type.") - case (None, None) => None - case _ => E.abort(s"Ambiguous reference to ${name}.") - } - } - - def currentTermsFor(name: String): Set[TermSymbol] = - scope.bindings.terms.getOrElse(name, Set.empty) - - def allTermsFor(path: List[String], name: String): Set[TermSymbol] = - all(path, scope) { _.terms.getOrElse(name, Set.empty) }.flatten.toSet - - def define(name: String, sym: TermSymbol)(using E: ErrorReporter): Unit = { - val bindings = scope.bindings - val termsInScope = currentTermsFor(name) - sym match { - case v: ValueSymbol => - if (termsInScope.exists(_.isInstanceOf[BlockSymbol])) { - E.abort(s"Value ${name} has the same name as a block definition in the same scope, which is not allowed.") - } - case b: BlockSymbol => - if (termsInScope.exists(_.isInstanceOf[ValueSymbol])) { - E.abort(s"Block ${name} has the same name as a value definition in the same scope, which is not allowed.") - } - } - bindings.addTerm(name, sym) - } - - def define(name: String, sym: TypeSymbol)(using E: ErrorReporter): Unit = - val bindings = scope.bindings - if bindings.types.isDefinedAt(name) then - E.error(pp"Type ${name} already defined in the current scope") - - lookupTypeOption(Nil, name).foreach { shadowed => - if sym.isInstanceOf[TypeVar] && !shadowed.isInstanceOf[TypeVar] then - E.warning(pp"Type parameter ${name} shadows outer definition of ${sym}") - } - bindings.setType(name, sym) - - def define(name: String, capt: Capture)(using ErrorReporter): Unit = - scope.bindings.setCapture(name, capt) - - def exports: Bindings = scope.bindings.toBindings - - def scoped[R](block: => R): R = - val before = scope - scope = Scope.Local(Namespace.empty, Namespace.empty, before) - try { block } finally { scope = before } - - // (re-)enter the namespace - def namespace[R](name: String)(block: => R): R = - val before = scope - val namespace = before.bindings.getNamespace(name) - scope = Scope.Named(name, namespace, before) - try { block } finally { scope = before } - - // returns the current path - def path: Option[List[String]] = - def collect(scope: Scope): Option[List[String]] = scope match { - case Scope.Global(imports, bindings) => Some(modulePath) - case Scope.Named(name, bindings, outer) => collect(outer) match { - case Some(path) => Some(path :+ name) - // name spaces also START a new path, if there hasn't been one, already - case None => Some(List(name)) - } - case Scope.Local(imports, bindings, outer) => None - } - collect(scope) - } - - def toplevel(modulePath: List[String], prelude: Bindings): Scoping = - val imports = Namespace.empty - imports.importAll(prelude) - Scoping(modulePath, Scope.Global(imports, Namespace.empty)) -} diff --git a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala index 9b2e64966..14fe8ce46 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala @@ -44,4 +44,13 @@ trait Symbol { } object Symbol { val fresh = new Counter(0) + + def apply(n: Name): Symbol = new Symbol { + val name = n + } } + +object Wildcard { + def apply() = Symbol(Name.local("_")) + def unapply(s: Symbol): Boolean = s.name == Name.local("_") +} \ No newline at end of file diff --git a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala deleted file mode 100644 index 96a14bfa3..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala +++ /dev/null @@ -1,124 +0,0 @@ -package effekt -package symbols - -import effekt.symbols.builtins.* -import effekt.typer.ConcreteEffects -import kiama.output.ParenPrettyPrinter - -import scala.language.implicitConversions - -object TypePrinter extends ParenPrettyPrinter { - - import kiama.output.PrettyPrinterTypes.Document - - val debug = false - - def show(id: source.IdDef): String = id.name - def show(id: source.IdRef): String = (id.path :+ id.name).mkString("::") - - def show(n: Name): String = n match { - case name: NoName.type => name.name - case name: LocalName => name.name - case name: QualifiedName => name.qualifiedName - } - def show(t: Type): String = pretty(toDoc(t), 80).layout - def show(t: TypeVar): String = pretty(toDoc(t), 80).layout - def show(t: Capture): String = pretty(toDoc(t), 80).layout - def show(t: Captures): String = pretty(toDoc(t), 80).layout - def show(t: Effects): String = pretty(toDoc(t), 80).layout - def show(t: List[ValueType | TypeVar]): String = pretty(maybeTypeParams(t), 80).layout - - val show: PartialFunction[Any, String] = { - case id: source.IdRef => TypePrinter.show(id) - case id: source.IdDef => TypePrinter.show(id) - case n: Name => TypePrinter.show(n) - case t: symbols.Type => TypePrinter.show(t) - case t: symbols.TypeVar => TypePrinter.show(t) - case t: Capture => TypePrinter.show(t) - case t: Captures => TypePrinter.show(t) - case t: Effects => TypePrinter.show(t) - case t: ConcreteEffects => TypePrinter.show(t.toEffects) - case c: TypeConstructor => c.name.name - case c: Constructor => c.name.name - case c: Field => c.name.name - case c: Operation => c.name.name - } - - def toDoc(m: Type): Doc = m match { - case tpe: ValueType => toDoc(tpe) - case tpe: BlockType => toDoc(tpe) - } - - def toDoc(tpe: ValueType): Doc = tpe match { - case BoxedType(tpe, capture) => toDoc(tpe) <+> "at" <+> toDoc(capture) - case ValueTypeApp(tpe, Nil) => tpe.name - case ValueTypeApp(tpe, args) => tpe.name <> brackets(hsep(args.map(toDoc), comma)) - case ValueTypeRef(x) => toDoc(x) - } - - def toDoc(tpe: TypeVar): Doc = tpe match { - case typeVar: UnificationVar => typeVar.toString - case typeVar: TypeVar => typeVar.name - } - - def toDoc(tpe: BlockType): Doc = tpe match { - case FunctionType(tparams, cparams, vparams, bparams, result, effects) => - val tps = if (tparams.isEmpty) emptyDoc else typeParams(tparams) - val ps: Doc = (vparams, bparams) match { - case (Nil, Nil) => "()" - case (List(tpe: BoxedType), Nil) => parens(toDoc(tpe)) - case (List(tpe), Nil) => if (tparams.isEmpty) toDoc(tpe) else parens(toDoc(tpe)) - case (_, _) => - val vps = if (vparams.isEmpty) emptyDoc else parens(hsep(vparams.map(toDoc), comma)) - val bps = if (bparams.isEmpty) emptyDoc else hcat(bparams.map(toDoc).map(braces)) - vps <> bps - } - val ret = result match { - case _: BoxedType => parens(toDoc(result)) - case _ => toDoc(result) - } - val eff = if (effects.isEmpty) emptyDoc else space <> "/" <+> toDoc(effects) - tps <> ps <+> "=>" <+> ret <> eff - - case InterfaceType(tpe, Nil) => toDoc(tpe) - case InterfaceType(tpe, args) => toDoc(tpe) <> typeParams(args) - } - - def toDoc(t: BlockTypeConstructor): Doc = t match { - case Interface(name, tparams, ops) => name - case ExternInterface(name, tparams) => name - } - - def toDoc(t: TypeConstructor): Doc = t match { - case DataType(name, tparams, constructors) => name <> typeParams(tparams) - case Record(name, tparams, constructor) => name <> typeParams(tparams) - case ExternType(name, tparams) => name - } - - def toDoc(eff: Effects): Doc = - if (eff.isEmpty) "{}" else - braces(space <> hsep(eff.effects.map(toDoc), comma) <> space) - - def toDoc(c: Captures): Doc = c match { - case CaptureSet(captures) => braces { hsep(captures.toList.map(toDoc), comma) } - case c: CaptUnificationVar => if (debug) c.name <> c.id.toString else c.name - } - - def toDoc(c: Capture): Doc = c.name - - implicit def toDoc(name: Name): Doc = name.name - - def typeParams(tparams: List[ValueType | TypeVar]): Doc = brackets(hsep(tparams.map { - case tpe: ValueType => toDoc(tpe) - case tpe: TypeVar => toDoc(tpe) - }, comma)) - - def maybeTypeParams(tparams: List[ValueType | TypeVar]): Doc = - if (tparams.isEmpty) "" else typeParams(tparams) - -} - -implicit class ErrorMessageInterpolator(private val sc: StringContext) extends AnyVal { - def pp(args: Any*): String = sc.s(args.map(effekt.util.show): _*) -} - diff --git a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala index c6e2d3813..fb709c9ae 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala @@ -1,83 +1,38 @@ package effekt package symbols -import effekt.source.ModuleDecl -import effekt.context.Context -import effekt.symbols.ErrorMessageInterpolator -import effekt.util.messages.ErrorMessageReifier - -import kiama.util.StringSource - -/** - * The symbols, which are built into Effekt - */ object builtins { - // a dummy module for built in types. Can be dropped, once they are - // defined in the prelude - lazy val prelude = Module(ModuleDecl("effekt", Nil, Nil), StringSource("", "effekt.effekt")) - private def name(s: String) = QualifiedName(List("effekt"), s) - val UnitSymbol = ExternType(name("Unit"), Nil) - val TUnit = ValueTypeApp(UnitSymbol, Nil) - - val BooleanSymbol = ExternType(name("Bool"), Nil) - val TBoolean = ValueTypeApp(BooleanSymbol, Nil) - - val IntSymbol = ExternType(name("Int"), Nil) - val TInt = ValueTypeApp(IntSymbol, Nil) - - val DoubleSymbol = ExternType(name("Double"), Nil) - val TDouble = ValueTypeApp(DoubleSymbol, Nil) - - val StringSymbol = ExternType(name("String"), Nil) - val TString = ValueTypeApp(StringSymbol, Nil) - - val CharSymbol = ExternType(name("Char"), Nil) - val TChar = ValueTypeApp(CharSymbol, Nil) - - val ByteSymbol = ExternType(name("Byte"), Nil) - val TByte = ValueTypeApp(ByteSymbol, Nil) - - val TopSymbol = ExternType(name("Any"), Nil) - val TTop = ValueTypeApp(TopSymbol, Nil) - - // should this be a datatype, not an extern type? - val BottomSymbol = ExternType(name("Nothing"), Nil) - val TBottom = ValueTypeApp(BottomSymbol, Nil) - - val IOSymbol = Interface(Name.local("IO"), Nil, Nil) - val IOCapability = ExternResource(name("io"), InterfaceType(IOSymbol, Nil)) - - val AsyncSymbol = Interface(Name.local("Async"), Nil, Nil) - val AsyncCapability = ExternResource(name("async"), InterfaceType(AsyncSymbol, Nil)) - - val GlobalSymbol = Interface(Name.local("Global"), Nil, Nil) - val GlobalCapability = ExternResource(name("global"), InterfaceType(GlobalSymbol, Nil)) + val UnitSymbol = Symbol(name("Unit")) + val IntSymbol = Symbol(name("Int")) + val DoubleSymbol = Symbol(name("Double")) + val StringSymbol = Symbol(name("String")) + val CharSymbol = Symbol(name("Char")) + val ByteSymbol = Symbol(name("Byte")) + val BoolSymbol = Symbol(name("Bool")) + val TopSymbol = Symbol(name("Any")) + val BottomSymbol = Symbol(name("Nothing")) + val IOSymbol = Symbol(name("IO")) + val AsyncSymbol = Symbol(name("Async")) + val GlobalSymbol = Symbol(name("Global")) + val RegionSymbol = Symbol(name("Region")) + + val globalCapture = Symbol(name("global")) + val ioCapture = Symbol(name("io")) + val asyncCapture = Symbol(name("async")) object TState { - val S: TypeParam = TypeParam(Name.local("S")) - val interface: Interface = Interface(Name.local("Ref"), List(S), Nil) - val get = Operation(name("get"), List(S), Nil, Nil, ValueTypeRef(S), Effects.Pure, interface) - val put = Operation(name("put"), List(S), List(ValueParam(Name.local("s"), Some(ValueTypeRef(S)))), Nil, TUnit, Effects.Pure, interface) - interface.operations = List(get, put) - - def apply(stateType: ValueType) = InterfaceType(interface, List(stateType)) - - def extractType(state: BlockType)(using C: Context): ValueType = - state match { - case InterfaceType(i, List(tpe)) if i == interface => tpe - case tpe => C.panic(pretty"Expected builtin state, but got $tpe") - } + val interface = Symbol(name("Ref")) + val get = Symbol(name("get")) + val put = Symbol(name("put")) } - val RegionSymbol = Interface(Name.local("Region"), Nil, Nil) - val TRegion = InterfaceType(RegionSymbol, Nil) - val rootTypes: Map[String, TypeSymbol] = Map( + val rootTypes: Map[String, Symbol] = Map( "Unit" -> UnitSymbol, - "Bool" -> BooleanSymbol, + "Bool" -> BoolSymbol, "Int" -> IntSymbol, "Double" -> DoubleSymbol, "String" -> StringSymbol, @@ -89,16 +44,9 @@ object builtins { "Region" -> RegionSymbol ) - val rootCaptures: Map[String, Capture] = Map( - "io" -> IOCapability.capture, - "async" -> AsyncCapability.capture, - "global" -> GlobalCapability.capture + val rootCaptures: Map[String, Symbol] = Map( + "io" -> globalCapture, + "async" -> asyncCapture, + "global" -> globalCapture ) - - // captures which are allowed on the toplevel - val toplevelCaptures: CaptureSet = CaptureSet() // CaptureSet(IOCapability.capture, GlobalCapability.capture) - - lazy val rootBindings: Bindings = - Bindings(Map.empty, rootTypes, rootCaptures, Map("effekt" -> Bindings(Map.empty, rootTypes, rootCaptures, Map.empty))) - } diff --git a/effekt/shared/src/main/scala/effekt/symbols/kinds.scala b/effekt/shared/src/main/scala/effekt/symbols/kinds.scala deleted file mode 100644 index 97d962140..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/kinds.scala +++ /dev/null @@ -1,92 +0,0 @@ -package effekt -package symbols - -import effekt.context.Context - -package object kinds { - - def wellformed(tpe: Type)(using Context): Unit = tpe match { - case t: ValueType => wellformed(t) - case t: BlockType => wellformed(t) - } - - def wellformed(effs: Effects)(using C: Context): Unit = - val effects = effs.toList - if (effects.size != effects.distinct.size) { - C.panic("Compiler invariant violated: duplicate effects.") - } - effs.toList foreach { wellformed } - - def wellformed(tpe: ValueType)(using C: Context): Unit = wellformedType(tpe) match { - case Kind.VType => () - case Kind.Fun(args, Kind.VType) => C.abort(s"${tpe} needs to be applied to ${args.size} type arguments") - case _ => C.abort(s"Expected a value type but got ${tpe}") - } - - def wellformed(tpe: BlockType)(using Context): Unit = tpe match { - case b: FunctionType => wellformed(b) - case c: InterfaceType => wellformed(c) - } - - def wellformed(eff: InterfaceType)(using C: Context): Unit = wellformedInterfaceType(eff) match { - case Kind.BType => () - case Kind.Fun(args, Kind.BType) => C.abort(s"${eff} needs to be applied to ${args.size} type arguments") - case o => C.abort(s"Expected a block type but got a type ${eff} of kind ${o}") - } - - def wellformed(b: FunctionType)(using C: Context): Unit = b match { - case FunctionType(tps, cps, vps, bps, res, effs) => - // TODO we could also check whether the same type variable shows up twice - if (cps.size != (bps.size + effs.distinct.size)) { - C.panic(s"Compiler invariant violated: different size of capture parameters and block parameters: ${b}") - } - vps.foreach { tpe => wellformed(tpe) } - bps.foreach { tpe => wellformed(tpe) } - wellformed(res) - wellformed(effs) - } - - private sealed trait Kind - private object Kind { - case object VType extends Kind - case object BType extends Kind - case class Fun(params: List[Kind], res: Kind) extends Kind - } - - private def wellformedTypeConstructor(tpe: TypeConstructor)(using C: Context): Kind.Fun = tpe match { - case DataType(_, tparams, _) => Kind.Fun(tparams map { p => Kind.VType }, Kind.VType) - case Record(_, tparams, _) => Kind.Fun(tparams map { p => Kind.VType }, Kind.VType) - case ExternType(_, tparams) => Kind.Fun(tparams map { p => Kind.VType }, Kind.VType) - } - - private def wellformedType(tpe: ValueType)(using C: Context): Kind = tpe match { - case BoxedType(tpe, region) => wellformed(tpe); Kind.VType - case _: ValueTypeRef => Kind.VType - case ValueTypeApp(tpe, args) => wellformedTypeConstructor(tpe) match { - case Kind.Fun(params, res) if params.isEmpty && args.nonEmpty => - C.abort(s"Cannot apply type ${tpe}. Type ${ tpe } does not expect any arguments, but is applied to ${ args.size }.") - case Kind.Fun(params, res) if (args.size != params.size) => - C.abort(s"Wrong type constructor arity. Type constructor ${tpe} expects ${params.size} parameters, but got ${args.size} arguments.") - case Kind.Fun(params, res) => - args foreach { a => wellformedType(a) }; - Kind.VType - } - } - - private def wellformedInterfaceType(e: InterfaceType)(using C: Context): Kind = e match { - case InterfaceType(eff, args) => wellformedBlockTypeConstructor(eff) match { - case Kind.Fun(params, res) if args.isEmpty && params.nonEmpty => - C.abort(s"Wrong number of type arguments. Interface ${eff} expects ${params.size} parameters, but no arguments were provided.") - case Kind.Fun(params, res) if args.size != params.size => - C.abort(s"Wrong number of type arguments. Interface ${ eff } expects ${ params.size } parameters, but got ${ args.size } arguments.") - case Kind.Fun(params, res) => - args foreach { a => wellformed(a) } - Kind.BType - } - } - - private def wellformedBlockTypeConstructor(e: BlockTypeConstructor)(using Context): Kind.Fun = e match { - case Interface(_, tparams, _) => Kind.Fun(tparams map { p => Kind.VType }, Kind.BType) - case ExternInterface(_, tparams) => Kind.Fun(tparams map { p => Kind.VType }, Kind.BType) - } -} diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala deleted file mode 100644 index 5a335df84..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ /dev/null @@ -1,426 +0,0 @@ -package effekt -package symbols - -import effekt.source.{Def, DefDef, FeatureFlag, FunDef, ModuleDecl, RegDef, ValDef, VarDef} -import effekt.context.Context -import kiama.util.Source -import effekt.context.assertions.* -import effekt.util.messages.ErrorReporter - -/** - * The symbol table contains things that can be pointed to: - * - function definitions - * - type definitions - * - effect definitions - * - parameters - * - value / variable binders - * - ... - */ - - -sealed trait TermSymbol extends Symbol - -// the two universes of values and blocks -sealed trait ValueSymbol extends TermSymbol -sealed trait BlockSymbol extends TermSymbol - -/** - * The result of running the frontend on a module. - * Symbols and types are stored globally in CompilerContext. - */ -case class Module( - decl: ModuleDecl, - source: Source -) extends Symbol { - - val name: QualifiedName = { - val segments = decl.path.split("/").toList - QualifiedName(segments.init, segments.last) - } - - val namespace = name.prefix :+ name.name - - def path = decl.path - - private var _exports: Bindings = _ - def exports: Bindings = _exports - - def terms = exports.terms - def types = exports.types - def captures = exports.captures - - - private var _includes: List[Module] = _ - def includes = _includes - - // a topological ordering of all transitive dependencies - // this is the order in which the modules need to be compiled / loaded - lazy val dependencies: List[Module] = includes.flatMap { im => im.dependencies :+ im }.distinct - - // toplevel declared effects - def effects: List[Interface] = types.values.toList.collect { - case e: Interface => e - } - - def isPrelude: Boolean = name.name == "effekt" - - def findPrelude: Module = { - // Either this module is already Prelude - if (isPrelude) { - return this - } - - // ... or we try to find Prelude in our dependencies - dependencies.find(_.isPrelude).getOrElse { - sys error "Cannot find Prelude, this should not happen" - } - } - - def findDependency(path: QualifiedName): Option[Module] = { - // Either this module is already Prelude - if (name == path) { - return Some(this) - } - - // ... or we try to find Prelude in our dependencies - dependencies.find(dep => dep.name == path) - } - - /** - * It is actually possible, that exports is invoked on a single module multiple times: - * The dependencies of a module might change, which triggers frontend on the same module - * again. It is the same, since the source and AST did not change. - */ - def exports( - includes: List[Module], - exports: Bindings - ): this.type = { - _includes = includes - _exports = exports - this - } -} - -/** - * A binder of references (type Ref[T]), can be a local variable - * or a region allocation. - */ -sealed trait RefBinder extends BlockSymbol - -sealed trait Param extends TermSymbol -case class ValueParam(name: Name, tpe: Option[ValueType]) extends Param, ValueSymbol - - -sealed trait TrackedParam extends Param, BlockSymbol { - // Every tracked block gives rise to a capture parameter (except resumptions, they are transparent) - lazy val capture: Capture = this match { - case b: BlockParam => CaptureParam(b.name) - case r: ResumeParam => ??? - case s: VarBinder => LexicalRegion(name, s.decl) - case r: ExternResource => Resource(name) - } -} -object TrackedParam { - case class BlockParam(name: Name, tpe: Option[BlockType]) extends TrackedParam - case class ResumeParam(module: Module) extends TrackedParam { val name = Name.local("resume") } - case class ExternResource(name: Name, tpe: BlockType) extends TrackedParam -} -export TrackedParam.* - - -trait Callable extends BlockSymbol { - def tparams: List[TypeParam] - def vparams: List[ValueParam] - def bparams: List[BlockParam] - def annotatedResult: Option[ValueType] - def annotatedEffects: Option[Effects] - - // invariant: only works if ret is defined! - def toType: FunctionType = annotatedType.get - - def toType(ret: ValueType, effects: Effects, capabilityParams: List[Capture]): FunctionType = - val tps = tparams - val vps = vparams.map { p => p.tpe.get } - val (bcapt, bps) = bparams.map { p => (p.capture, p.tpe) }.unzip - val bps_ = bps.collect { case Some(bp) => bp } - FunctionType(tps, bcapt ++ capabilityParams, vps, bps_, ret, effects) - - def annotatedType: Option[FunctionType] = - for { - ret <- annotatedResult; - effs <- annotatedEffects - effects = effs.distinct - // TODO currently the return type cannot refer to the annotated effects, so we can make up capabilities - // in the future namer needs to annotate the function with the capture parameters it introduced. - capt = CanonicalOrdering(effects.toList).map { tpe => CaptureParam(tpe.name) } - } yield toType(ret, effects, capt) -} - -case class UserFunction( - name: Name, - tparams: List[TypeParam], - vparams: List[ValueParam], - bparams: List[BlockParam], - annotatedResult: Option[ValueType], - annotatedEffects: Option[Effects], - decl: FunDef -) extends Callable - -/** - * Anonymous symbols used to represent scopes / regions in the region checker - */ -sealed trait Anon extends TermSymbol { - val name = NoName - def decl: source.Tree -} - -case class Lambda(vparams: List[ValueParam], bparams: List[BlockParam], decl: source.Tree) extends Callable, Anon { - // Lambdas currently do not have an annotated return type - def annotatedResult = None - def annotatedEffects = None - - // Lambdas currently do not take type parameters - def tparams = Nil -} - -/** - * Binders represent local value and variable binders - * - * They also store a reference to the original defition in the source code - */ -enum Binder extends TermSymbol { - def tpe: Option[Type] - def decl: Def - - case ValBinder(name: Name, tpe: Option[ValueType], decl: ValDef) extends Binder, ValueSymbol - case RegBinder(name: Name, tpe: Option[ValueType], region: BlockSymbol, decl: RegDef) extends Binder, RefBinder - case VarBinder(name: Name, tpe: Option[ValueType], decl: VarDef) extends Binder, RefBinder, TrackedParam - case DefBinder(name: Name, tpe: Option[BlockType], decl: DefDef) extends Binder, BlockSymbol -} -export Binder.* - - -/** - * Synthetic symbol representing potentially multiple call targets - * - * Refined by typer. - */ -case class CallTarget(symbols: List[Set[BlockSymbol]]) extends BlockSymbol { val name = NoName } - -/** - * Introduced by Transformer - */ -case class Wildcard() extends ValueSymbol { val name = Name.local("_") } -case class TmpValue(hint: String = "tmp") extends ValueSymbol { val name = Name.local("v_" + hint) } -case class TmpBlock(hint: String = "tmp") extends BlockSymbol { val name = Name.local("b_" + hint) } - -/** - * Type Symbols - * - [[ValueTypeSymbol]] - * - [[BlockTypeSymbol]] - * - [[Capture]] - */ -sealed trait TypeSymbol extends Symbol -sealed trait ValueTypeSymbol extends TypeSymbol -sealed trait BlockTypeSymbol extends TypeSymbol - - -/** - * Value Type Symbols - */ - -/** - * Type variables are symbols that can be substituted for. - * - [[TypeParam]] type variables in user programs - * - [[UnificationVar]] type variables inserted by the type checker. - */ -enum TypeVar(val name: Name) extends ValueTypeSymbol { - - /** - * Type parameters that show up in user programs - */ - case TypeParam(n: Name) extends TypeVar(n) - - /** - * Introduced when instantiating type schemes - * - * Should neither occur in source programs, nor in inferred types - */ - case UnificationVar(underlying: TypeVar.TypeParam, call: source.Tree) extends TypeVar(underlying.name) -} -export TypeVar.* - -case class TypeAlias(name: Name, tparams: List[TypeParam], tpe: ValueType) extends ValueTypeSymbol - -/** - * Types that _can_ be used in type constructor position. e.g. >>>List<<<[T] - * - * - [[DataType]] - * - [[Record]] - * - [[ExternType]] - */ -enum TypeConstructor extends TypeSymbol { - def tparams: List[TypeParam] - - case DataType(name: Name, tparams: List[TypeParam], var constructors: List[Constructor] = Nil) - case Record(name: Name, tparams: List[TypeParam], var constructor: Constructor) - case ExternType(name: Name, tparams: List[TypeParam]) -} -export TypeConstructor.* - - -case class Constructor(name: Name, tparams: List[TypeParam], var fields: List[Field], tpe: TypeConstructor) extends Callable { - // Parameters and return type of the constructor - lazy val vparams: List[ValueParam] = fields.map { f => f.param } - val bparams: List[BlockParam] = Nil - - val appliedDatatype: ValueType = ValueTypeApp(tpe, tpe.tparams map ValueTypeRef.apply) - def annotatedResult: Option[ValueType] = Some(appliedDatatype) - def annotatedEffects: Option[Effects] = Some(Effects.Pure) -} - -// TODO maybe split into Field (the symbol) and Selector (the synthetic function) -case class Field(name: Name, param: ValueParam, constructor: Constructor) extends Callable { - val tparams: List[TypeParam] = constructor.tparams - val vparams = List(ValueParam(constructor.name, Some(constructor.appliedDatatype))) - val bparams = List.empty[BlockParam] - - val returnType = param.tpe.get - def annotatedResult = Some(returnType) - def annotatedEffects = Some(Effects.Pure) -} - - -enum BlockTypeConstructor extends BlockTypeSymbol { - def tparams: List[TypeParam] - - case Interface(name: Name, tparams: List[TypeParam], var operations: List[Operation] = Nil) - case ExternInterface(name: Name, tparams: List[TypeParam]) -} -export BlockTypeConstructor.* - - -case class Operation(name: Name, tparams: List[TypeParam], vparams: List[ValueParam], bparams: List[BlockParam], resultType: ValueType, effects: Effects, interface: BlockTypeConstructor.Interface) extends Callable { - def annotatedResult: Option[ValueType] = Some(resultType) - def annotatedEffects: Option[Effects] = Some(Effects(effects.toList)) - def appliedInterface: InterfaceType = InterfaceType(interface, interface.tparams map ValueTypeRef.apply) -} - -/** - * Effect aliases are *not* block types, or block type constructors. They have to be dealiased by [[Namer]] - * before usage. - */ -case class EffectAlias(name: Name, tparams: List[TypeParam], effs: Effects) extends BlockTypeSymbol - - -/** - * Something that can be substituted by a capture set - */ -sealed trait CaptVar extends TypeSymbol - -/** - * "Tracked" capture parameters. Like [[TypeParam]] used to abstract - * over capture. Also see [[BlockParam.capture]]. - */ -enum Capture extends CaptVar { - - /** - * Capture parameters introduced by block parameters (they count as `control`, since they can close over arbitrary capabilities) - */ - case CaptureParam(name: Name) - - /** - * Region of local mutable state (they count in as `control` when considering direct style) - */ - case LexicalRegion(name: Name, tree: source.Tree) - - /** - * Represents external resources (they count in as `io` when considering direct style) - */ - case Resource(name: Name) -} -export Capture.* - -case class CaptUnificationVar(role: CaptUnificationVar.Role) extends Captures, CaptVar { - val name = Name.local("?C") - override def toString = role match { - case CaptUnificationVar.VariableInstantiation(underlying, _) => "?" + underlying.toString + id - case CaptUnificationVar.Subtraction(handled, underlying) => s"?filter" + id - case CaptUnificationVar.FunctionRegion(fun) => s"?${fun.id.name}" + id - case CaptUnificationVar.AnonymousFunctionRegion(fun) => s"?anon" + id - case CaptUnificationVar.HandlerRegion(handler) => s"?Ck" + id - case _ => "?" + id - } -} -object CaptUnificationVar { - sealed trait Role - case class VariableInstantiation(underlying: Capture, call: source.Tree) extends Role - case class HandlerRegion(handler: source.TryHandle) extends Role - case class RegionRegion(handler: source.Region) extends Role - case class VarRegion(definition: source.VarDef) extends Role - case class FunctionRegion(fun: source.FunDef) extends Role - case class BlockRegion(fun: source.DefDef) extends Role - case class AnonymousFunctionRegion(fun: source.BlockLiteral) extends Role - case class InferredBox(box: source.Box) extends Role - case class InferredUnbox(unbox: source.Unbox) extends Role - // underlying should be a UnificationVar - case class Subtraction(handled: List[Capture], underlying: CaptUnificationVar) extends Role - case class Substitution() extends Role -} - -/** - * Capture Sets - */ - -sealed trait Captures - -case class CaptureSet(captures: Set[Capture]) extends Captures { - // This is a very simple form of subtraction, make sure that all constraints have been solved before using it! - def --(other: CaptureSet): CaptureSet = CaptureSet(captures -- other.captures) - def ++(other: CaptureSet): CaptureSet = CaptureSet(captures ++ other.captures) - def +(c: Capture): CaptureSet = CaptureSet(captures + c) - def flatMap(f: Capture => CaptureSet): CaptureSet = CaptureSet(captures.flatMap(x => f(x).captures)) - - def pureOrIO: Boolean = captures.forall { c => - def isIO = c == builtins.IOCapability.capture - // mutable state is now in CPS and not considered IO anymore. - def isMutableState = c.isInstanceOf[LexicalRegion] - def isResource = c.isInstanceOf[Resource] - def isAsync = c == builtins.AsyncCapability.capture - !(isAsync || isMutableState) && (isIO || isResource) - } - - def pure: Boolean = captures.isEmpty -} -object CaptureSet { - def apply(captures: Capture*): CaptureSet = CaptureSet(captures.toSet) - def apply(captures: List[Capture]): CaptureSet = CaptureSet(captures.toSet) - def empty = CaptureSet() -} - -/** - * FFI - */ -case class ExternFunction( - name: Name, - tparams: List[TypeParam], - vparams: List[ValueParam], - bparams: List[BlockParam], - result: ValueType, - effects: Effects, - capture: CaptureSet, - bodies: List[source.ExternBody] -) extends Callable { - def annotatedResult = Some(result) - def annotatedEffects = Some(effects) -} - -/** - * Extension method for LSP to filter out synthetically generated symbols - */ -extension (s: Symbol) { - def isSynthetic: Boolean = s match { - case _: Field | _: Constructor | _: CallTarget | _: Wildcard | _: TmpValue | _: ResumeParam => true - case s => s.synthetic - } -} diff --git a/effekt/shared/src/main/scala/effekt/symbols/types.scala b/effekt/shared/src/main/scala/effekt/symbols/types.scala deleted file mode 100644 index 050c6639e..000000000 --- a/effekt/shared/src/main/scala/effekt/symbols/types.scala +++ /dev/null @@ -1,149 +0,0 @@ -package effekt -package symbols - -import TypeVar.* - -/** - * Types - */ -sealed trait Type - -/** - * Value Types - * - * [[ValueType]] - * | - * |- [[ValueTypeRef]] references to type params - * |- [[ValueTypeApp]] references to type constructors - * |- [[BoxedType]] boxed block types - */ - -enum ValueType extends Type { - - /** - * Types of first-class functions - */ - case BoxedType(tpe: BlockType, capture: Captures) - - /** - * Reference to a type variable (we don't have type constructor polymorphism, so variables do not take arguments) - */ - case ValueTypeRef(tvar: TypeVar) - - /** - * Reference to a type constructor with optional type arguments - */ - case ValueTypeApp(constructor: TypeConstructor, args: List[ValueType]) -} -export ValueType.* - -/** - * [[BlockType]] - * | - * |- [[FunctionType]] - * |- [[InterfaceType]] - * - * Effects are a list of [[InterfaceType]] - * - * Outside of the hierarchy are - * [[EffectAlias]] - * which are resolved by [[Namer]] to a list of [[InterfaceType]]s - */ -enum BlockType extends Type { - - case FunctionType( - tparams: List[TypeParam], - cparams: List[Capture], - vparams: List[ValueType], - bparams: List[BlockType], - result: ValueType, - effects: Effects - ) - - case InterfaceType(typeConstructor: BlockTypeConstructor, args: List[ValueType]) -} -export BlockType.* - -extension (i: BlockType.InterfaceType) { - def name: Name = i.typeConstructor.name -} - -/** - * Represents effect _sets_ (no order, no duplicates) on function types. - * - * All effects are dealiased by namer. Effects are inferred via [[typer.ConcreteEffects]] so - * by construction all entries in the set of effects here should be concrete (no unification variables). - * - * Effect sets are themselves *not* symbols, they are just aggregates. - * - * We do not enforce entries to be distinct. This way we can substitute types and keep duplicate entries. - * For instances { State[S], State[T] }[S -> Int, T -> Int] then becomes { State[Int], State[Int] }. - * This is important since we need to pass two capabilities in this case. - * - * Member [[canonical]] computes the canonical ordering of capabilities for this set of effects. - * Disjointness needs to be ensured manually when constructing effect sets (for instance via [[typer.ConcreteEffects]]). - */ -case class Effects(effects: List[BlockType.InterfaceType]) { - - lazy val toList: List[InterfaceType] = effects.distinct - - def isEmpty: Boolean = effects.isEmpty - def nonEmpty: Boolean = effects.nonEmpty - - def filterNot(p: InterfaceType => Boolean): Effects = - Effects(effects.filterNot(p)) - - def forall(p: InterfaceType => Boolean): Boolean = effects.forall(p) - def exists(p: InterfaceType => Boolean): Boolean = effects.exists(p) - - def size: Int = effects.size - - def distinct: Effects = Effects(effects.distinct) -} -object Effects { - - def apply(effs: InterfaceType*): Effects = - new Effects(effs.toList) - - def apply(effs: Iterable[InterfaceType]): Effects = - new Effects(effs.toList) - - def empty: Effects = new Effects(Nil) - val Pure = empty -} - -/** - * The canonical ordering needs to be stable, but should also distinguish two types, - * if they are different. - * - * Bugs with the canonical ordering can lead to runtime errors as observed in ticket #108 - */ -object CanonicalOrdering extends Ordering[InterfaceType] { - def compare(tpe1: InterfaceType, tpe2: InterfaceType): Int = compareStructural(tpe1, tpe2) - - def compareStructural(tpe1: Any, tpe2: Any): Int = (tpe1, tpe2) match { - case (_: UnificationVar, _) | (_, _: UnificationVar) => - effekt.util.messages.INTERNAL_ERROR("Cannot compute canonical ordering, since type still contains unification variables.") - case (sym1: Symbol, sym2: Symbol) => - sym1.id - sym2.id - case (p1: Product, p2: Product) if p1.getClass == p2.getClass => - (p1.productIterator zip p2.productIterator).collectFirst { - case (child1, child2) if compareStructural(child1, child2) != 0 => compareStructural(child1, child2) - }.getOrElse(fallback(tpe1, tpe2)) - case _ => - fallback(tpe1, tpe2) - } - - def fallback(tpe1: Any, tpe2: Any): Int = tpe1.hashCode - tpe2.hashCode - - def apply(l: List[InterfaceType]): List[InterfaceType] = l.sorted(using this) - - def apply(f: FunctionType): FunctionType = f match { - case FunctionType(tparams, cparams, vparams, bparams, result, effects) => - val (cparamsBlocks, cparamsEffects) = cparams.splitAt(bparams.size) - val (cparams2, effects2) = (cparamsEffects zip effects.toList).sortBy(_._2)(using this).unzip - FunctionType(tparams, cparamsBlocks ++ cparams2, vparams, bparams, result, Effects(effects2)) - } -} - - diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala deleted file mode 100644 index f2c4b5e2e..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala +++ /dev/null @@ -1,220 +0,0 @@ -package effekt -package typer - -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.symbols.* - -object BoxUnboxInference extends Phase[NameResolved, NameResolved] { - - import source._ - - val phaseName = "box-unbox" - - def run(input: NameResolved)(using Context) = { - val transformedTree = Context.timed(phaseName, input.source.name) { rewrite(input.tree) } - - if (Context.messaging.hasErrors) { None } - else { Some(input.copy(tree = transformedTree)) } - } - - - def rewrite(e: ModuleDecl)(using C: Context): ModuleDecl = visit(e) { - case ModuleDecl(path, imports, defs) => - ModuleDecl(path, imports, defs.flatMap(flattenNamespaces)) - } - - /** - * There are only a few limited constructors for blocks: - * - * - identifiers (e.g., `f`) - * - explicit unboxing (e.g. `unbox EXPR`, or maybe postfix `EXPR!`) - * - object literals (e.g. `new T {}`) - * - selection (e.g., `f.m.n`) - */ - def rewriteAsBlock(e: Term)(using C: Context): Term = visit(e) { - case v: Var => v.definition match { - case sym: (ValueSymbol | symbols.RefBinder) => Unbox(v).inheritPosition(v) - case sym: BlockSymbol => v - } - - case Unbox(t) => Unbox(rewriteAsExpr(t)) - case New(impl) => New(rewrite(impl)) - case BlockLiteral(tps, vps, bps, body) => BlockLiteral(tps, vps, bps, rewrite(body)) - case other => Unbox(rewriteAsExpr(other)) - } - - def rewriteAsExpr(e: Term)(using C: Context): Term = visit(e) { - - case Unbox(expr) => rewriteAsExpr(expr) - - case v: Var => v.definition match { - // TODO maybe we should synthesize a call to get here already? - case sym: (ValueSymbol | symbols.RefBinder) => v - case sym: BlockSymbol => Box(None, v).inheritPosition(v) - } - - case n: New => Box(None, rewriteAsBlock(n)).inheritPosition(n) - - case b: BlockLiteral => Box(None, rewriteAsBlock(b)).inheritPosition(b) - - case l: Literal => l - - case Assign(id, expr) => - Assign(id, rewriteAsExpr(expr)) - - case If(guards, thn, els) => - If(guards.map(rewrite), rewrite(thn), rewrite(els)) - - case While(guards, body, default) => - While(guards.map(rewrite), rewrite(body), default.map(rewrite)) - - case Match(scs, clauses, default) => - Match(scs.map(rewriteAsExpr), clauses.map(rewrite), default.map(rewrite)) - - case s @ Select(recv, name) if s.definition.isInstanceOf[Field] => - Select(rewriteAsExpr(recv), name) - - case s @ Select(recv, name) => - C.abort("selection on blocks not supported yet.") - - case Do(effect, id, targs, vargs, bargs) => - Do(effect, id, targs, vargs.map(rewriteAsExpr), bargs.map(rewriteAsBlock)) - - case Call(fun, targs, vargs, bargs) => - Call(rewrite(fun), targs, vargs.map(rewriteAsExpr), bargs.map(rewriteAsBlock)) - - case m @ MethodCall(receiver, id, targs, vargs, bargs) => - val vargsTransformed = vargs.map(rewriteAsExpr) - val bargsTransformed = bargs.map(rewriteAsBlock) - - val hasMethods = m.definition match { - // an overloaded call target - case symbols.CallTarget(syms) => syms.flatten.exists(b => b.isInstanceOf[symbols.Operation]) - case s => false - } - - // we prefer methods over uniform call syntax - if (hasMethods) { - MethodCall(rewriteAsBlock(receiver), id, targs, vargsTransformed, bargsTransformed) - } else { - Call(IdTarget(id).inheritPosition(id), targs, rewriteAsExpr(receiver) :: vargsTransformed, bargsTransformed) - } - - case TryHandle(prog, handlers) => - TryHandle(rewrite(prog), handlers.map(rewrite)) - - case Region(name, body) => - Region(name, rewrite(body)) - - case Hole(stmts) => - Hole(rewrite(stmts)) - - case Box(c, b) => - Box(c, rewriteAsBlock(b)) - } - - def rewrite(target: source.CallTarget)(using C: Context): source.CallTarget = visit(target) { - case source.ExprTarget(receiver) => source.ExprTarget(rewriteAsBlock(receiver)) - case t: source.IdTarget => t.definition match { - case sym: (ValueSymbol | symbols.RefBinder) => - source.ExprTarget(source.Unbox(source.Var(t.id).inheritPosition(t)).inheritPosition(t)).inheritPosition(t) - case sym: BlockSymbol => - t - } - } - - def flattenNamespaces(t: Def)(using C: Context): List[Def] = t match { - case Def.NamespaceDef(name, defs) => defs.flatMap(flattenNamespaces) - case d => List(rewrite(d)) - } - - def rewrite(t: Def)(using C: Context): Def = visit(t) { - - case FunDef(id, tparams, vparams, bparams, ret, body) => - FunDef(id, tparams, vparams, bparams, ret, rewrite(body)) - - case ValDef(id, annot, binding) => - ValDef(id, annot, rewrite(binding)) - - case RegDef(id, annot, region, binding) => - RegDef(id, annot, region, rewrite(binding)) - - case VarDef(id, annot, binding) => - VarDef(id, annot, rewrite(binding)) - - case DefDef(id, annot, binding) => - val block = rewriteAsBlock(binding) - (binding, block) match { - case (Unbox(_), _) => () - // If the binding wasn't an `Unbox` and now it is, it means that the compiler synthesized it. - // We therefore annotate the new `Unbox` expression with its original definition. - // See [[Annotations.UnboxParentDef]] for more details about this annotation. - case (_, u @ Unbox(_)) => C.annotate(Annotations.UnboxParentDef, u, t) - case (_, _) => () - } - DefDef(id, annot, block) - - case d: InterfaceDef => d - case d: DataDef => d - case d: RecordDef => d - case d: TypeDef => d - case d: EffectDef => d - - case d: ExternType => d - case d: ExternDef => d - case d: ExternResource => d - case d: ExternInterface => d - case d: ExternInclude => d - - case d: NamespaceDef => Context.panic("Should have been removed by flattenNamespaces") - } - - def rewrite(t: Stmt)(using C: Context): Stmt = visit(t) { - case DefStmt(d, rest) => - flattenNamespaces(d).foldRight(rewrite(rest)) { case (d, rest) => DefStmt(d, rest) } - - case ExprStmt(e, rest) => - ExprStmt(rewriteAsExpr(e), rewrite(rest)) - - case Return(e) => - Return(rewriteAsExpr(e)) - - case BlockStmt(b) => - BlockStmt(rewrite(b)) - } - - def rewrite(h: Handler)(using Context): Handler = visit(h) { - case Handler(capability, impl) => - Handler(capability, rewrite(impl)) - } - - def rewrite(i: Implementation)(using Context): Implementation = visit(i) { - case Implementation(interface, clauses) => - Implementation(interface, clauses.map(rewrite)) - } - - def rewrite(h: OpClause)(using Context): OpClause = visit(h) { - case OpClause(id, tparams, vparams, bparams, ret, body, resume) => - OpClause(id, tparams, vparams, bparams, ret, rewrite(body), resume) - } - - def rewrite(c: MatchClause)(using Context): MatchClause = visit(c) { - case MatchClause(pattern, guards, body) => - MatchClause(pattern, guards.map(rewrite), rewrite(body)) - } - - def rewrite(g: MatchGuard)(using Context): MatchGuard = g match { - case BooleanGuard(condition) => BooleanGuard(rewriteAsExpr(condition)) - case PatternGuard(scrutinee, pattern) => PatternGuard(rewriteAsExpr(scrutinee), pattern) - } - - /** - * Copies all annotations and position information from source to target - */ - def visit[T <: Tree, R <: Tree](source: T)(block: T => R)(using C: Context): R = { - val target = block(source) - target.inheritPosition(source) - C.copyAnnotations(source, target) - target - } -} diff --git a/effekt/shared/src/main/scala/effekt/typer/CapabilityScope.scala b/effekt/shared/src/main/scala/effekt/typer/CapabilityScope.scala deleted file mode 100644 index b882a2bd1..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/CapabilityScope.scala +++ /dev/null @@ -1,39 +0,0 @@ -package effekt -package typer - -import effekt.context.Context -import effekt.{ source, symbols } -import effekt.util.messages.ErrorMessageReifier - -/** - * Invariant: Like the result effects of Typer, all types of bound capabilities need to be concrete! - */ -sealed trait CapabilityScope { - def copy: CapabilityScope - def capabilityFor(tpe: symbols.InterfaceType)(using C: Context): symbols.BlockParam - def parent: CapabilityScope -} -case object GlobalCapabilityScope extends CapabilityScope { - def copy: CapabilityScope = this - def parent: CapabilityScope = sys error "No parent" - // If we try to find a capability for an effect that is known to be unhandled (that is no outer scope could - // potentially handle it, then we raise an error. - def capabilityFor(tpe: symbols.InterfaceType)(using C: Context): symbols.BlockParam = - C.abort(pretty"Effect ${tpe} is not allowed in this context.") -} -class BindSome(binder: source.Tree, capabilities: Map[symbols.InterfaceType, symbols.BlockParam],val parent: CapabilityScope) extends CapabilityScope { - def copy: CapabilityScope = BindSome(binder, capabilities, parent.copy) - def capabilityFor(tpe: symbols.InterfaceType)(using C: Context): symbols.BlockParam = - capabilities.getOrElse(tpe, parent.capabilityFor(tpe)) - override def toString: String = s"BindSome(${binder.getClass.getSimpleName}, ${capabilities}, ${parent})" -} -class BindAll(binder: source.Tree, var capabilities: Map[symbols.InterfaceType, symbols.BlockParam], val parent: CapabilityScope) extends CapabilityScope { - def copy: CapabilityScope = BindAll(binder, capabilities, parent.copy) - def capabilityFor(tpe: symbols.InterfaceType)(using C: Context): symbols.BlockParam = - capabilities.getOrElse(tpe, { - val freshCapability = C.freshCapabilityFor(tpe) - capabilities = capabilities.updated(tpe, freshCapability) - freshCapability - }) - override def toString: String = s"BindAll(${binder.getClass.getSimpleName}, ${capabilities}, ${parent})" -} diff --git a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala b/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala deleted file mode 100644 index f68e66f04..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala +++ /dev/null @@ -1,123 +0,0 @@ -package effekt -package typer - -import effekt.context.Context -import effekt.symbols._ -import effekt.util.messages.ErrorMessageReifier - - -/** - * All effects inferred by Typer are required to be concrete and dealiased. - * - * This way, we can easily compare them for equality. - * - * This is a _set_ of effects and does not guarantee order! - */ -class ConcreteEffects private[typer] (protected val effects: List[InterfaceType]) { - - def toList: List[InterfaceType] = effects - def toEffects: Effects = Effects(effects) - - // both are known to be concrete, no need to go through validation again - def ++(other: ConcreteEffects): ConcreteEffects = ConcreteEffects.fromList(this.effects ++ other.effects) - - // we can use set difference since type constructors are assumed to be invariant and all unification variables - // are substituted away. - def --(other: ConcreteEffects): ConcreteEffects = ConcreteEffects.fromList( - (this.effects.toSet -- other.effects.toSet).toList - ) - - def isEmpty: Boolean = effects.isEmpty - def nonEmpty: Boolean = effects.nonEmpty - - def filterNot(p: InterfaceType => Boolean): ConcreteEffects = ConcreteEffects.fromList(effects.filterNot(p)) - - def canonical: List[InterfaceType] = effects.sorted(using CanonicalOrdering) - - def forall(p: InterfaceType => Boolean): Boolean = effects.forall(p) - def exists(p: InterfaceType => Boolean): Boolean = effects.exists(p) -} -object ConcreteEffects { - // unsafe, doesn't perform check - private def fromList(eff: List[InterfaceType]): ConcreteEffects = new ConcreteEffects(eff.distinct) - - /** - * These smart constructors should not be used directly. - * [[Typer.asConcrete]] should be used instead, since it performs substitution and dealiasing. - */ - def apply(eff: List[InterfaceType])(using Context): ConcreteEffects = - eff foreach assertConcreteEffect - fromList(eff) - - def apply(effs: Effects)(using Context): ConcreteEffects = apply(effs.toList) - - def empty: ConcreteEffects = fromList(Nil) - - def union(effs: IterableOnce[ConcreteEffects]): ConcreteEffects = { - ConcreteEffects.fromList(effs.iterator.flatMap{ e => e.effects }.toList) - } -} - -val Pure = ConcreteEffects.empty - -implicit def asConcrete(effs: Effects)(using C: Context): ConcreteEffects = - ConcreteEffects(C.unification(effs)) - - -/** - * Asserts that all effects in the list are _concrete_, that is, - * no unification variables (neither type, nor region) are allowed. - * - * If all effects are concrete (and we assume effect type constructors are INVARIANT): - * - we can use structural equality to compare them - * - we can use sets and hash maps - * - * Consequences: - * - If we try to add an effect that is not concrete, we should raise an "Could not infer..." error. - * - We need to substitute early in order to have more concrete effects. - * - Requiring effects to be concrete also simplifies effect-set comparison in [[TypeComparer]]. - * - * TODO Question: should we ALWAYS require effects to be concrete, also when compared with [[TypeUnifier]]? - */ -private[typer] def assertConcreteEffects(effs: Effects)(using C: Context): Unit = - effs.effects.foreach(assertConcreteEffect) - -private[typer] def assertConcreteEffect(eff: InterfaceType)(using C: Context): Unit = - unknowns(eff) match { - case us if us.nonEmpty => - C.abort(pretty"Effects need to be fully known, but effect ${eff}'s type parameter(s) ${us.mkString(", ")} could not be inferred.\n\nMaybe try annotating them?") - case _ => () - } - -private[typer] def assertConcreteFunction(id: source.Id, tpe: BlockType)(using C: Context): Unit = - unknowns(tpe) match { - case us if us.nonEmpty => - C.abort(pretty"Cannot fully infer type for ${id}: ${tpe}") - case _ => () - } - -private type Unknown = CaptUnificationVar | UnificationVar - -private def unknowns(tpe: ValueType): Set[Unknown] = tpe match { - case ValueTypeRef(x) => unknowns(x) - case ValueTypeApp(tpe, args) => args.flatMap(unknowns).toSet - case BoxedType(tpe, capture) => unknowns(tpe) ++ unknowns(capture) -} - -private def unknowns(tpe: TypeVar): Set[Unknown] = tpe match { - case x: UnificationVar => Set(x) - case x: TypeVar => Set.empty -} - -private def unknowns(tpe: BlockType): Set[Unknown] = tpe match { - case FunctionType(tparams, cparams, vparams, bparams, result, effects) => - vparams.flatMap(unknowns).toSet ++ bparams.flatMap(unknowns).toSet ++ unknowns(result) ++ unknowns(effects) - case InterfaceType(tpe, args) => args.flatMap(unknowns).toSet -} - -private def unknowns(capt: Captures): Set[Unknown] = capt match { - case x @ CaptUnificationVar(role) => Set(x) - case CaptureSet(captures) => Set.empty -} - -private def unknowns(effs: Effects): Set[Unknown] = effs.toList.flatMap(unknowns).toSet diff --git a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala b/effekt/shared/src/main/scala/effekt/typer/Constraints.scala deleted file mode 100644 index 2764d3b3f..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala +++ /dev/null @@ -1,486 +0,0 @@ -package effekt -package typer - -import effekt.symbols.* -import effekt.util.messages.{ ErrorReporter, ErrorMessageReifier } -import effekt.util.foreachAborting - -// Auxiliary Definitions -// --------------------- - -private[typer] -class Node - -private[typer] -type CNode = CaptUnificationVar - -// non present filter stands for "everything is passed through" -private[typer] -type Filter = Set[Capture] - -private[typer] -case class CaptureNodeData( - // non present bounds represent bottom (the empty set) - lower: Option[Set[Capture]], - // non present bounds represent top (the universal set) - upper: Option[Set[Capture]], - lowerNodes: Map[CNode, Filter], - upperNodes: Map[CNode, Filter] -) -private val emptyData = CaptureNodeData(None, None, Map.empty, Map.empty) - -private[typer] -type CaptureConstraints = Map[CNode, CaptureNodeData] - -/** - * A network of constraints. - * - * We distinguish between type constraints and capture constraints. - * - * Type Constraints - * ---------------- - * Right now, we are implementing a simplified version of subtyping. Whenever two - * unification variables are required to be in a relation (subtyping, equivalence, ...) we - * conservatively assume they have to be **equal**. - * - * Whenever we learn something about a variable (such as the upper type bound), we immediately - * solve the variable and all of the other variables in the same class and compute a - * substitution ([[typeSubstitution]]). - * - * To record that two variables are equal, we associate them with the same [[Node]] - * and store them in an equivalence class [[classes]]. - * - * Invariants: - * - substitutions should never refer to variables that we already solved. - * (they should have been substituted away) - * - all types in an equivalence class have the same substitution; thus everytime we - * learn something about one of them it needs to be compatible with the others. - * - * Capture Constraints - * ------------------- - * Constraints on capture unification variables are represented by a network of nodes: - * ┏━━━━━━━━━━━━━━━━━━┓ - * --------------> ┃ ?MyCaptureVar ┃ --------------> - * lower nodes ┠─────────┬────────┨ upper nodes - * --------------> ┃ Lower │ Upper ┃ --------------> - * ┗━━━━━━━━━┷━━━━━━━━┛ - * - * Such nodes in the graph are represented by instances of [[CaptureNodeData]]. - * Lower and upper bounds ([[CaptureNodeData.lower]], and [[CaptureNodeData.upper]]) on a - * node are always fully known sets of type [[Capture]]. - * - * The arrows in the picture above indicate the subcapturing relationship. - * They are in fact navigatable in both directions, since new _upper_ bounds - * need to flow through the node from right-to-left. The are represented - * as fields [[CaptureNodeData.lowerNodes]] and [[CaptureNodeData.lowerNodes]]. - * - * Connections to other nodes are potentially "filtered" ([[Filter]]). That is, - * newly learnt bounds only flow to the related nodes modulo filtering. This is - * necessary to implement subtraction of concrete captures. - * - * Invariants: - * - Intra-Consistency: Lower is always consistent to Upper - * - Inter-Consistency: if two nodes ?S <: ?T, then the lower bound of ?S has been - * propagated as lower bound to ?T and the upper bound of ?T has been propagated - * as upper bound of ?S. - * - * - Transitivity: The bounds are always fully propagated to all connected nodes. - * - * However, for now, we do not compute equivalence classes -- we also do not establish transitive connections. - */ -class Constraints( - /** - * Everything we know so far -- right hand sides of substitutions are *always* concrete types. - * They can, however, mention unification variables. e.g. `?U !-> List[?S]` is allowed. - * The mentioned unification variables (`?S` in the example) must not have a substitution. - * They can be part of an equivalence class. - * - * Once one member of the equivalence class becomes concrete, all members are assigned the same type - * in the substitution. - */ - private var typeSubstitution: Map[Node, ValueType] = Map.empty, - - /** - * A map from a member in the equivalence class to the class' representative - */ - private var classes: Map[UnificationVar, Node] = Map.empty, - - /** - * Concrete bounds for each unification variable - */ - private var captureConstraints: CaptureConstraints = Map.empty, - - /** - * Everything we know so far -- right hand sides of substitutions are *always* concrete capture sets. - * Once solved and part of the substitution, we cannot learn something about a unification variable anymore; - * only check consistency. - */ - private var captSubstitution: Map[CNode, CaptureSet] = Map.empty, - - /** - * Unification variables which are not in scope anymore, but also haven't been solved, yet. - */ - private var pendingInactive: Set[CNode] = Set.empty -)(using C: ErrorReporter) { - - /** - * Caches type substitutions, which are only invalidated if the mapping from node to typevar changes. - * - * This significantly improves the performance of Typer (see https://github.com/effekt-lang/effekt/pull/954) - */ - private var _typeSubstitution: Map[TypeVar, ValueType] = _ - private def invalidate(): Unit = _typeSubstitution = null - - /** - * The currently known substitutions - */ - def subst: Substitutions = - if _typeSubstitution == null then { - _typeSubstitution = classes.flatMap[TypeVar, ValueType] { case (k, v) => typeSubstitution.get(v).map { k -> _ } } - } - val captures = captSubstitution.asInstanceOf[Map[CaptVar, Captures]] - Substitutions(_typeSubstitution, captures) - - /** - * Should only be called on unification variables where we do not know any types, yet - * - * It will *not* compare the types, but only equality imposed by constraints. - */ - def isEqual(x: UnificationVar, y: UnificationVar): Boolean = - getNode(x) == getNode(y) - - def isSubset(lower: Captures, upper: Captures): Boolean = - def knownUpper(node: CNode) = captSubstitution.get(node).map(_.captures).orElse { node.upper } - def knownLower(node: CNode) = captSubstitution.get(node).map(_.captures).orElse { node.lower } - - (lower, upper) match { - // (1) they are concrete, we can compare them - case (CaptureSet(xs), CaptureSet(ys)) => xs.subsetOf(ys) - // (2) lhs is a unification variable, we need to check whether its upper bound is smaller than ys - case (x: CaptUnificationVar, CaptureSet(ys)) => knownUpper(x).exists(_ subsetOf ys) - // (2) rhs is a unification variable, we need to check whether xs is smaller than its lower bound - case (CaptureSet(xs), y: CaptUnificationVar) => knownLower(y).exists(xs subsetOf _) - // (3) both are unification variables - case (x: CaptUnificationVar, y: CaptUnificationVar) => - // either the bounds imply subcapturing ... - val boundsImply = knownUpper(x).flatMap(xs => knownLower(y).map(ys => xs subsetOf ys)).getOrElse(false) - // or the two are known to be related - val areBounded = x.upperNodes.get(y).exists(f => f.isEmpty) - return boundsImply || areBounded - } - - /** - * Retreive the potentially known type of [[x]] - */ - def typeOf(x: UnificationVar): Option[ValueType] = - typeOf(getNode(x)) - - /** - * Learn that unification variable [[x]] needs to be compatible with [[y]]. If there already is - * a type for [[x]], we will invoke [[merge]] to check compatibility (and potentially error out). - */ - def learn(x: UnificationVar, y: ValueType)(merge: (ValueType, ValueType) => Unit): Unit = { - - def learnType(x: Node, tpe: ValueType): Unit = { - // tpe should not be a reference to a unification variable - typeOf(x) foreach { otherTpe => merge(tpe, otherTpe) } - typeSubstitution = typeSubstitution.updated(x, tpe) - updateSubstitution() - } - - def connectNodes(x: Node, y: Node): Unit = { - // Already connected - if (x == y) return () - - (typeOf(x), typeOf(y)) match { - case (Some(typeOfX), Some(typeOfY)) => merge(typeOfX, typeOfY) - case (Some(typeOfX), None) => learnType(y, typeOfX) - case (None, Some(typeOfY)) => learnType(x, typeOfY) - case (None, None) => () - } - - // create mapping to representative - classes = classes.view.mapValues { node => if (node == x) y else node }.toMap - } - - y match { - case ValueTypeRef(y: UnificationVar) => connectNodes(getNode(x), getNode(y)) - case tpe => learnType(getNode(x), tpe) - } - } - - /** - * Marks [[xs]] as pending inactive and solves them, if they do not have active bounds. - */ - def leave(types: List[UnificationVar], capts: List[CaptUnificationVar]): Unit = - // Check that we could infer all types of type variable instantiations. - types.foreach { - case x @ UnificationVar(underlying, callTree) => - if (!typeSubstitution.isDefinedAt(getNode(x))) C.at(callTree) { - C.error(s"Cannot infer type argument ${underlying}, maybe consider annotating it?") - } - } - - // (0) only add those to pending that haven't been solved already - pendingInactive = pendingInactive ++ (capts.toSet -- captSubstitution.keySet) - - // (1) collect all nodes that can be solved - val toRemove: Set[CNode] = removableNodes() - - // nothing to do - if (toRemove.isEmpty) return; - - // (2) solve variables that can be removed - solve(toRemove) - - // (3) update the substitution - updateSubstitution() - - def solve(toRemove: Set[CNode]): Unit = - - // (1) Remove them from pending - pendingInactive = pendingInactive -- toRemove - - // (2) Solve them - val solved = toRemove.toList.map { n => - // bounds are consistent, we simply choose the lower bound as it is always concrete. - val bounds = n.lower.getOrElse(Set.empty) - n -> CaptureSet(bounds) - }.toMap - captSubstitution = captSubstitution ++ solved - - // (3) remove them from the constraint set - captureConstraints = captureConstraints.collect { - case (n, CaptureNodeData(lower, upper, lowerNodes, upperNodes)) if !(toRemove.contains(n)) => - n -> CaptureNodeData(lower, upper, lowerNodes -- toRemove, upperNodes -- toRemove) - } - - def forceSolving(node: CNode): CaptureSet = - captSubstitution.getOrElse(node, { - solve(Set(node)) - captSubstitution(node) - }) - - - override def clone(): Constraints = new Constraints(typeSubstitution, classes, captureConstraints, captSubstitution, pendingInactive) - - def dumpTypeConstraints() = - println("\n--- Type Constraints ---") - val cl = classes.groupMap { case (el, repr) => repr } { case (el, repr) => el } - cl foreach { - case (n, vars) => typeSubstitution.get(n) match { - case None => println(s"{${vars.mkString(", ")}}") - case Some(tpe) => println(s"{${vars.mkString(", ")}} --> ${tpe}") - } - } - println("------------------\n") - - def dumpCaptureConstraints() = - println("\n--- Capture Constraints ---") - captureConstraints foreach { - case (x, CaptureNodeData(lower, upper, lowerNodes, upperNodes)) => - def printFilterNode(filtered: (CNode, Filter)): String = filtered match { - case (n, filter) if filter.isEmpty => n.toString - case (n, filter) => s"${n.toString}[${filter.mkString(", ")}]" - } - - val colWidth = 25 - val prettyLower = lower.map { cs => "{" + cs.mkString(",") + "}" }.getOrElse("{}").padTo(colWidth, ' ') - val prettyUpper = upper.map { cs => "{" + cs.mkString(",") + "}" }.getOrElse("*").padTo(colWidth, ' ') - val prettyLowerNodes = lowerNodes.map(printFilterNode).mkString(", ").padTo(colWidth, ' ') - val prettyUpperNodes = upperNodes.map(printFilterNode).mkString(", ").padTo(colWidth, ' ') - val varName = x.toString.padTo(15, ' ') - println(s"${prettyLowerNodes} | ${prettyLower} <: $varName <: ${prettyUpper} | ${prettyUpperNodes}") - } - println("------------------\n") - captSubstitution.foreach { - case (node, set) => println(node.toString.padTo(25, ' ') + " -> " + set.captures.mkString(", ")) - } - println("------------------\n") - - def dumpConstraints() = - dumpTypeConstraints() - dumpCaptureConstraints() - - // - - private def getData(x: CNode): CaptureNodeData = - captureConstraints.getOrElse(x, emptyData) - - /** - * Accessing data on a node in the constraint graph. - */ - extension (x: CNode) { - private [typer] def lower: Option[Set[Capture]] = getData(x).lower - private [typer] def upper: Option[Set[Capture]] = getData(x).upper - private [typer] def lowerNodes: Map[CNode, Filter] = getData(x).lowerNodes - private [typer] def upperNodes: Map[CNode, Filter] = getData(x).upperNodes - private def lower_=(bounds: Set[Capture]): Unit = - captureConstraints = captureConstraints.updated(x, getData(x).copy(lower = Some(bounds))) - - private def upper_=(bounds: Set[Capture]): Unit = - captureConstraints = captureConstraints.updated(x, getData(x).copy(upper = Some(bounds))) - - private def addLower(other: CNode, exclude: Filter): Unit = - val oldData = getData(x) - - // compute the intersection of filters - val oldFilter = oldData.lowerNodes.get(other) - val newFilter = oldFilter.map { _ intersect exclude }.getOrElse { exclude } - captureConstraints = captureConstraints.updated(x, oldData.copy(lowerNodes = oldData.lowerNodes + (other -> newFilter))) - - private def addUpper(other: CNode, exclude: Filter): Unit = - val oldData = getData(x) - - // compute the union of filters - val oldFilter = oldData.lowerNodes.get(other) - val newFilter = oldFilter.map { _ union exclude }.getOrElse { exclude } - captureConstraints = captureConstraints.updated(x, oldData.copy(upperNodes = oldData.upperNodes + (other -> newFilter))) - } - - /** - * Computes the set of all nodes that are inactive (that is, we left its unification scope and it - * can be solved by unification) - * - * In order to do so, it checks whether the node itself is inactive and whether the transitive closure of - * its bounds is inactive. - */ - private def removableNodes(): Set[CNode] = { - - // The results of inactivity is cached to avoid expensive recomputation. - var cache: Map[CNode, Boolean] = Map.empty - - /** - * This helper function computes inactivity of nodes as the - * transitive closure of x's bounds. - */ - def checkInactivity(x: CNode): Unit = - if (cache contains x) { return } - - if (captSubstitution isDefinedAt x) { cache += (x -> true); return } - - // is the node itself inactive? - if (pendingInactive contains x) { - cache += (x -> true) - - val allBoundsInactive = (x.lowerNodes.keys ++ x.upperNodes.keys).forall { n => - checkInactivity(n) - cache(n) - } - cache += (x -> allBoundsInactive) - } else { - cache += (x -> false) - } - - // collect all nodes that can be solved - pendingInactive filter { n => checkInactivity(n); cache(n) } - } - - - private def checkConsistency(lower: Set[Capture], upper: Set[Capture]): Unit = - val diff = lower -- upper - if (diff.nonEmpty) { C.abort(pp"Not allowed ${CaptureSet(diff)}") } - - private def checkEquality(xs: Set[Capture], ys: Set[Capture]): Unit = - if (xs != ys) { C.abort(pp"Capture set ${CaptureSet(xs)} is not equal to ${CaptureSet(ys)}") } - - // we do not necessarily need mergeLower, since we can take the free union - private def mergeLower(xs: Set[Capture], ys: Set[Capture]): Set[Capture] = - xs ++ ys - - /** - * Since we do not implement subregioning at the moment, it is safe to simply compute the - * intersection. - */ - private def mergeUpper(xs: Set[Capture], ys: Set[Capture]): Set[Capture] = - xs intersect ys - - - /** - * Adds x as a lower bound to y, and y as a lower bound to x. - */ - def connect(x: CNode, y: CNode, exclude: Set[Capture] = Set.empty): Unit = - if (x == y /* || (y.lowerNodes contains x) */) { return () } - - // we already solved one of them? Or both? - (captSubstitution.get(x), captSubstitution.get(y)) match { - case (Some(CaptureSet(xs)), Some(CaptureSet(ys))) => checkConsistency(xs, ys ++ exclude) - case (Some(CaptureSet(xs)), None) => requireLower(xs -- exclude, y) - case (None, Some(CaptureSet(ys))) => requireUpper(ys ++ exclude, x) - case (None, None) => - // compose the filters - x.addUpper(y, exclude) - y.addLower(x, exclude) - - val upperFilter = x.upperNodes(y) - val lowerFilter = y.lowerNodes(x) - - x.lower foreach { bounds => requireLower(bounds -- lowerFilter, y) } - y.upper foreach { bounds => requireUpper(bounds ++ upperFilter, x) } - } - - def requireLower(bounds: Set[Capture], x: CNode): Unit = propagateLower(bounds, x)(using Set.empty) - def requireUpper(bounds: Set[Capture], x: CNode): Unit = propagateUpper(bounds, x)(using Set.empty) - - private def propagateLower(bounds: Set[Capture], x: CNode)(using seen: Set[CNode]): Unit = - if (seen contains x) return() - - captSubstitution.get(x) foreachAborting { - // we already have fixed the capture set, check equality - case CaptureSet(capt) => - checkEquality(capt, bounds) - return; - } - - given Set[CNode] = seen + x - - x.upper foreach { upperBounds => checkConsistency(bounds, upperBounds) } - - x.lower = x.lower map { existing => - mergeLower(existing, bounds) - } getOrElse bounds - - x.upperNodes.foreach { case (y, filter) => propagateLower(bounds -- filter, y) } - - - private def propagateUpper(bounds: Set[Capture], x: CNode)(using seen: Set[CNode]): Unit = - if (seen contains x) return() - - captSubstitution.get(x) foreachAborting { - // we already have fixed the capture set, check equality - case CaptureSet(capt) => - checkEquality(capt, bounds) - return; - } - given Set[CNode] = seen + x - - x.lower foreach { lowerBounds => checkConsistency(lowerBounds, bounds) } - - x.upper = x.upper map { existing => - mergeUpper(existing, bounds) - } getOrElse bounds - - x.lowerNodes.foreach { case (y, filter) => propagateUpper(bounds ++ filter, y) } - - // - - // - - /** - * Updates substitution by applying it to itself. - * - * This way we replace {?B} -> Box[?R] with {?B} -> Box[Int] when learning ?B =:= Int - */ - private def updateSubstitution(): Unit = - val substitution = subst - invalidate() - typeSubstitution = typeSubstitution.map { case (node, tpe) => node -> substitution.substitute(tpe) } - - private def getNode(x: UnificationVar): Node = - classes.getOrElse(x, { val rep = new Node; classes += (x -> rep); rep }) - - private def typeOf(n: Node): Option[ValueType] = - typeSubstitution.get(n) - - // -} diff --git a/effekt/shared/src/main/scala/effekt/typer/ErrorContext.scala b/effekt/shared/src/main/scala/effekt/typer/ErrorContext.scala deleted file mode 100644 index e6bdedda4..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/ErrorContext.scala +++ /dev/null @@ -1,92 +0,0 @@ -package effekt -package typer - -import effekt.symbols.ErrorMessageInterpolator - -sealed trait ErrorContext { def polarity: Polarity } -sealed trait PositiveContext extends ErrorContext { def polarity = Covariant } -sealed trait NegativeContext extends ErrorContext { def polarity = Contravariant } -sealed trait InvariantContext extends ErrorContext { def polarity = Invariant } - -object ErrorContext { - - /** - * A generic context, checking a type against an expected type at position [[checkedTree]]. - */ - case class Expected(got: symbols.Type, exp: symbols.Type, checkedTree: source.Tree) extends PositiveContext - - /** - * A pattern matching context, checking a scrutinee against the return type of the match pattern [[pattern]] - */ - case class PatternMatch(pattern: source.MatchPattern) extends PositiveContext - - /** - * A context matching a declared type [[declared]] against the type defined at the use site [[defined]]. - */ - case class Declaration(param: source.Param, declared: symbols.Type, defined: symbols.Type) extends NegativeContext - - - case class CaptureFlow(from: symbols.Captures, to: symbols.Captures, checkedTree: source.Tree) extends PositiveContext - - case class MergeTypes(left: symbols.Type, right: symbols.Type) extends PositiveContext - case class MergeCaptures() extends PositiveContext - - case class MergeInvariant(outer: ErrorContext) extends InvariantContext - case class TypeConstructor(outer: ErrorContext) extends InvariantContext - case class TypeConstructorArgument(outer: ErrorContext) extends InvariantContext - case class BoxedTypeBlock(left: symbols.BoxedType, right: symbols.BoxedType, outer: ErrorContext) extends ErrorContext { def polarity = outer.polarity } - case class BoxedTypeCapture(left: symbols.BoxedType, right: symbols.BoxedType, outer: ErrorContext) extends ErrorContext { def polarity = outer.polarity } - case class FunctionArgument(left: symbols.FunctionType, right: symbols.FunctionType, outer: ErrorContext) extends ErrorContext { def polarity = outer.polarity.flip } - case class FunctionReturn(outer: ErrorContext) extends ErrorContext { def polarity = outer.polarity } - case class FunctionEffects(outer: ErrorContext) extends InvariantContext - - // TODO defer rendering of error messages to Context - def explainMismatch(tpe1: symbols.Type, tpe2: symbols.Type, outerCtx: ErrorContext): String = - - def go(ctx: ErrorContext): String = ctx match { - - case Expected(got, exp, tree) => - val expRendered = pp"$exp" - val gotRendered = pp"$got" - val msg = if ((expRendered.size + gotRendered.size) < 25) { - s"Expected $expRendered but got $gotRendered." - } else { - s"Expected type\n $expRendered\nbut got type\n $gotRendered" - } - if (tpe1 != got || tpe2 != exp) - pp"$msg\n\nType mismatch between $tpe2 and $tpe1." - else - msg - case PatternMatch(pattern) => pp"Pattern matches against type $tpe2 but scrutinee has type $tpe1." - case Declaration(param, declared, defined) => pp"Type $defined does not match the declared type $declared." - - // TODO - // (1) carry through position information about left and right trees. - // (2) refactor so that explainMismatch is part of Messages and can add additional - // messages (with info about the trees and their types). - case MergeTypes(left, right) => - val msg = pp"Different arms of a conditional/match have incompatible types.\n\nOne arm has type\n $left\nwhile another one has type\n $right" - - if (tpe2 != left || tpe1 != right) - pp"$msg\n\nType mismatch between $tpe2 and $tpe1." - else - msg - - case MergeCaptures() => sys error "Should not occur" - case CaptureFlow(from, to, tree) => sys error "Should not occur" - - case MergeInvariant(outer) => go(outer) - case BoxedTypeBlock(left, right, outer) => go(outer) - case BoxedTypeCapture(left, right, outer) => go(outer) - case FunctionArgument(left, right, outer) => - go(outer) + pp"\n comparing the argument types of\n ${left} (given)\n ${right} (expected)" - case FunctionReturn(outer) => go(outer) + pp"\n when comparing the return type of the function." - case FunctionEffects(outer) => go(outer) - case TypeConstructor(outer) => go(outer) - case TypeConstructorArgument(outer) => go(outer) - } - go(outerCtx) - - def explainInContext(msg: String, ctx: ErrorContext): String = - msg -} diff --git a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala b/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala deleted file mode 100644 index f7faeaec1..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala +++ /dev/null @@ -1,344 +0,0 @@ -package effekt -package typer - -import effekt.context.{ Annotations, Context, ContextOps } -import effekt.symbols.* -import effekt.context.assertions.* -import effekt.source.{ Def, ExprTarget, IdTarget, MatchGuard, MatchPattern, Tree } -import effekt.source.Tree.{ Query, Visit } -import effekt.util.messages.{ ErrorReporter, INTERNAL_ERROR } - -import scala.annotation.tailrec -import scala.collection.mutable - -/** - * Like the [[ effekt.core.PatternMatchingCompiler ]], the exhaustivity checker first - * translates clauses into an internal representation, made up of: - * - * - [[ Clause ]]s as list of conditions and a pointer to the original clause - * - [[ Condition ]]s that can either be pattern matches or guards - * - [[ Pattern ]] consisting of already resolved symbols so that the exhaustivity checker does not rely on [[ Context ]] - * - * Scrutinees are identified by [[ Trace ]]s from the original scrutinee. For example: - * - * {{{ - * e match { case Cons(Some(n), _) => ... } - * ^ - * }}} - * - * is identified by the trace - * - * {{{ - * Child(Some, value, Child(Cons, head, Root(e))) - * }}} - * - * Exhaustivity checking is performed by two mutually recursive functions that correspond to **two modes**: - * - * - [[ checkScrutinee ]] inspects the scrutinee and tries to cover the different values the scrutinee can take. - * For example, if it is of type `Option`, then we will check whether `Some` and `None` are covered. - * - [[ matchClauses ]] is more similar to [[ effekt.core.PatternMatchingCompiler.compile ]] in that it follows the - * structure of the clauses top-to-bottom, left-to-right. - * - * Most of the work is performed in [[ checkScrutinee ]], [[ matchClauses ]] mostly searches for the next scrutinee - * and while doing this also handles guards. - */ -object ExhaustivityChecker { - - import source.MatchPattern.* - - enum Pattern { - case Any() - case Tag(c: Constructor, patterns: List[Pattern]) - case Literal(value: scala.Any, tpe: ValueType) - } - - enum Condition { - case Patterns(patterns: Map[Trace, Pattern]) - case Guard(condition: source.Term) - } - - case class Clause(conditions: List[Condition], tree: source.MatchClause) { - def matchesOn(scrutinee: Trace): Option[Pattern] = conditions match { - case Condition.Patterns(patterns) :: rest => - patterns.get(scrutinee) - case _ => None - } - def -(scrutinee: Trace): Clause = conditions match { - case Condition.Patterns(patterns) :: rest => - Clause.normalized(Condition.Patterns(patterns - scrutinee) :: rest, tree) - case _ => this - } - def ++(others: Map[Trace, Pattern]): Clause = conditions match { - case Condition.Patterns(patterns) :: rest => - Clause.normalized(Condition.Patterns(patterns ++ others) :: rest, tree) - case _ => Clause.normalized(Condition.Patterns(others) :: conditions, tree) - } - - def isSatisfied: Boolean = conditions.isEmpty - } - object Clause { - def normalized(conditions: List[Condition], tree: source.MatchClause): Clause = - Clause(conditions.flatMap { - case Condition.Patterns(ps) => - val norm = normalized(ps) - if norm.isEmpty then None else Some(Condition.Patterns(norm)) - case other => Some(other) - }, tree) - def normalized(patterns: Map[Trace, Pattern]): Map[Trace, Pattern] = patterns collect { - case (trace, p: Pattern.Tag) => (trace, p) - case (trace, p: Pattern.Literal) => (trace, p) - } - } - - // Scrutinees are identified by tracing from the original scrutinee. - enum Trace { - case Root(scrutinees: source.Term) - case Child(c: Constructor, field: Field, outer: Trace) - } - - - def preprocess(roots: List[source.Term], cl: source.MatchClause)(using Context): Clause = (roots, cl) match { - case (List(root), source.MatchClause(pattern, guards, body)) => - Clause.normalized(Condition.Patterns(Map(Trace.Root(root) -> preprocessPattern(pattern))) :: guards.map(preprocessGuard), cl) - case (roots, source.MatchClause(MultiPattern(patterns), guards, body)) => - val rootConds: Map[Trace, Pattern] = (roots zip patterns).map { case (root, pattern) => - Trace.Root(root) -> preprocessPattern(pattern) - }.toMap - Clause.normalized(Condition.Patterns(rootConds) :: guards.map(preprocessGuard), cl) - case (_, _) => Context.abort("Malformed multi-match") - } - def preprocessPattern(p: source.MatchPattern)(using Context): Pattern = p match { - case AnyPattern(id) => Pattern.Any() - case IgnorePattern() => Pattern.Any() - case p @ TagPattern(id, patterns) => Pattern.Tag(p.definition, patterns.map(preprocessPattern)) - case LiteralPattern(lit) => Pattern.Literal(lit.value, lit.tpe) - case MultiPattern(patterns) => - Context.panic("Multi-pattern should have been split in preprocess already / nested MultiPattern") - } - def preprocessGuard(g: source.MatchGuard)(using Context): Condition = g match { - case MatchGuard.BooleanGuard(condition) => - Condition.Guard(condition) - case MatchGuard.PatternGuard(scrutinee, pattern) => - Condition.Patterns(Map(Trace.Root(scrutinee) -> preprocessPattern(pattern))) - } - - /** - * Gathers and reports information about redundant and missing clauses - * - * TODO exhaustivity check should be constructive: - * - non exhaustive pattern match should generate a list of patterns, so the IDE can insert them - * - redundant cases should generate a list of cases that can be deleted. - */ - class Exhaustivity(allClauses: List[source.MatchClause], originalScrutinees: List[source.Term]) { - - // Redundancy Information - // ---------------------- - // clauses are redundant if we never mark them as used during compilation - private var redundantClauses: Set[(Int, source.MatchClause)] = allClauses.map(hashClause).toSet - private def hashClause(cl: source.MatchClause): (Int, source.MatchClause) = (System.identityHashCode(cl), cl) - private def redundant: List[source.MatchClause] = redundantClauses.map(_._2).toList - - def use(cl: source.MatchClause): Unit = redundantClauses = redundantClauses - hashClause(cl) - - // Missing Clauses - // --------------- - enum Missing { - case Tag(constructor: Constructor, at: Trace) - case Guard(term: source.Term) - case Literal(value: Any, tpe: ValueType, at: Trace) - case Default(tpe: ValueType, at: Trace) - } - private var missingCases: List[Missing] = Nil - - def missingConstructor(c: Constructor, at: Trace) = missingCases = Missing.Tag(c, at) :: missingCases - def missingGuard(guard: source.Term) = missingCases = Missing.Guard(guard) :: missingCases - def missingDefault(tpe: ValueType, at: Trace) = missingCases = Missing.Default(tpe, at) :: missingCases - def missingLiteral(value: Any, tpe: ValueType, at: Trace) = missingCases = Missing.Literal(value, tpe, at) :: missingCases - - // Error Reporting - // --------------- - def reportNonExhaustive()(using C: ErrorReporter): Unit = { - @tailrec - def traceToCase(at: Trace, acc: String): String = at match { - case Trace.Root(_) if originalScrutinees.length == 1 => acc - case Trace.Root(e) => originalScrutinees.map { f => if e == f then acc else "_" }.mkString(", ") - case Trace.Child(childCtor, field, outer) => - val newAcc = s"${childCtor.name}(${childCtor.fields.map { f => if f == field then acc else "_" }.mkString(", ")})" - traceToCase(outer, newAcc) - } - - missingCases.foreach { - case Missing.Tag(ctor, at) => - val missingSubcase = ctor.fields match - case Nil => s"${ctor.name}()" - case _ => s"${ctor.name}(${ctor.fields.map { _f => "_" }.mkString(", ")})" - val missingCase = traceToCase(at, missingSubcase) - - C.error(pp"Non-exhaustive pattern matching, missing case ${missingCase}") - case Missing.Guard(term) => - C.at(term) { - C.error(pp"Non-exhaustive pattern matching, guard could be false which is not covered") - } - case Missing.Literal(value, tpe, at) => - val missingCase = traceToCase(at, pp"${value}") - - C.error(pp"Non-exhaustive pattern matching, missing case $missingCase") - case Missing.Default(tpe, at) => - val missingCase = traceToCase(at, "_") - - C.error(pp"Non-exhaustive pattern matching, scrutinees of type ${tpe} require a default case ${missingCase}") - } - } - - def reportRedundant()(using C: ErrorReporter): Unit = redundant.foreach { p => - C.at(p) { C.warning(pp"Unreachable case.") } - } - - def report()(using C: ErrorReporter): Unit = { - reportNonExhaustive() - reportRedundant() - } - } - - def checkExhaustive(scrutinees: List[source.Term], cls: List[source.MatchClause])(using C: Context): Unit = { - val initialClauses: List[Clause] = cls.map(preprocess(scrutinees, _)) - given E: Exhaustivity = new Exhaustivity(cls, scrutinees) - checkScrutinees(scrutinees.map(Trace.Root(_)), scrutinees.map{ scrutinee => Context.inferredTypeOf(scrutinee) }, initialClauses) - E.report() - } - - def checkScrutinees(scrutinees: List[Trace], tpes: List[ValueType], clauses: List[Clause])(using E: Exhaustivity): Unit = { - (scrutinees, tpes) match { - case (List(scrutinee), List(tpe)) => checkScrutinee(scrutinee, tpe, clauses) - case _ => - clauses match { - case Nil => E.missingDefault(tpes.head, scrutinees.head) - case head :: tail => matchClauses(head, tail) - } - } - } - - def checkScrutinee(scrutinee: Trace, tpe: ValueType, clauses: List[Clause])(using E: Exhaustivity): Unit = { - - /** - * checks whether [[ clauses ]] at position [[ trace ]] cover all [[ constructors ]] - */ - def checkDatatype(constructors: List[Constructor]): Unit = { - - def cover(ctor: Constructor): List[Clause] = clauses.flatMap { cl => - cl.matchesOn(scrutinee) match { - // keep clauses that match on the same constructor - case Some(Pattern.Tag(c, patterns)) if c == ctor => - val nestedPatterns = (ctor.fields zip patterns).map { case (field, pattern) => - (Trace.Child(ctor, field, scrutinee) : Trace) -> pattern - }.toMap - Some(cl - scrutinee ++ nestedPatterns) - - // drop clauses that match on a different constructor - case Some(Pattern.Tag(other, patterns)) => None - // also keep if they do not match on the scrutinee at all - case _ => - Some(cl) - } - } - constructors.foreach { c => cover(c) match { - // no clause covers this case: it is missing... - case Nil => E.missingConstructor(c, scrutinee) - case cl :: rest => matchClauses(cl, rest) - }} - } - - def checkLiteral(tpe: ValueType): Unit = { - val allLiterals: List[Any] = clauses.flatMap(_.matchesOn(scrutinee)).collect { case Pattern.Literal(v, tpe) => v }.distinct - - val finite: Option[List[Any]] = tpe match { - case builtins.TBoolean => Some(List(true, false)) - case builtins.TUnit => Some(List(())) - case builtins.TBottom => Some(Nil) - case _ => None - } - - var matches = Map.empty[Any, List[Clause]] - val defaults = mutable.ListBuffer.empty[Clause] - - clauses.foreach { cl => - cl.matchesOn(scrutinee) match { - case Some(Pattern.Literal(v, tpe)) => - matches = matches.updated(v, matches.getOrElse(v, Nil) :+ (cl - scrutinee)) - case _ => - // defaults are duplicated - allLiterals.foreach { v => matches = matches.updated(v, matches.getOrElse(v, Nil) :+ cl) } - defaults.addOne(cl) - } - } - - finite match { - // we have a finite domain - case Some(cases) => - cases.foreach { v => - matches.get(v) match { - case Some(head :: tail) => matchClauses(head, tail) - case _ => defaults.toList match { - case Nil => E.missingLiteral(v, tpe, scrutinee) - case head :: tail => matchClauses(head, tail) - } - } - } - - // clauses can never exhaust the domain (Int, String, etc.) - case None => - matches.collect { - case (_, head :: tail) => matchClauses(head, tail) - } - defaults.toList match { - case Nil => E.missingDefault(tpe, scrutinee) - case head :: tail => matchClauses(head, tail) - } - } - } - - tpe match { - case ValueType.ValueTypeApp(DataType(_, _, ctors), _) => checkDatatype(ctors) - case ValueType.ValueTypeApp(Record(_, _, ctor), _) => checkDatatype(List(ctor)) - case tpe @ (builtins.TInt | builtins.TDouble | builtins.TString | builtins.TBoolean | builtins.TUnit | builtins.TBottom | builtins.TChar | builtins.TByte) => - checkLiteral(tpe) - - // missing.add(trace); - case tpe => - clauses match { - case Nil => E.missingDefault(tpe, scrutinee) - // ignore scrutinee - case head :: tail => - matchClauses(head, tail) - } - } - } - - def matchClauses(head: Clause, alternatives: List[Clause])(using E: Exhaustivity): Unit = head match { - // the first clause already matches, mark it as used - case Clause(Nil, tree) => E.use(tree) - - // we are not yet done and need to continue matching - case Clause(Condition.Guard(term) :: rest, body) => - // case: guard is true - matchClauses(Clause.normalized(rest, body), alternatives) - // case: guard is false - alternatives match { - case Nil => E.missingGuard(term) - case head :: next => matchClauses(head, next) - } - case Clause(Condition.Patterns(patterns) :: rest, body) if patterns.isEmpty => - matchClauses(Clause.normalized(rest, body), alternatives) - case Clause(Condition.Patterns(patterns) :: rest, body) => - // We have to choose the next scrutinee. We guess the approximate type by looking at the pattern it is - // matched against. - patterns.head match { - case (sc, Pattern.Tag(constructor, ps)) => - checkScrutinee(sc, constructor.appliedDatatype, head :: alternatives) - case (sc, Pattern.Literal(lit, tpe)) => - checkScrutinee(sc, tpe, head :: alternatives) - case (sc, Pattern.Any()) => - INTERNAL_ERROR("Should not happen, since clauses are normalized.") - } - } -} diff --git a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala b/effekt/shared/src/main/scala/effekt/typer/Substitution.scala deleted file mode 100644 index a736de9de..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala +++ /dev/null @@ -1,92 +0,0 @@ -package effekt -package typer - -import effekt.symbols._ - - -case class SubstitutionException(x: CaptUnificationVar, subst: Map[Capture, Captures]) extends Exception - -/** - * Substitutions not only have unification variables as keys, since we also use the same mechanics to - * compare two types with each other (see [[TypeComparer.subFunctionType]]). - * - * We should **not** use these Substitutions for instantiation anymore, since this introduces fresh Unification variables. - */ -case class Substitutions( - values: Map[TypeVar, ValueType], - // invariant: we alway only map - // - a single CaptureParam -> CaptureParam - // - a CaptUnificationVar -> Captures - captures: Map[CaptVar, Captures] -) { - - def isDefinedAt(t: TypeVar) = values.isDefinedAt(t) - def isDefinedAt(c: CaptVar) = captures.isDefinedAt(c) - - def get(t: TypeVar) = values.get(t) - - def get(c: Capture): Option[Capture] = captures.get(c) map { - case CaptureSet(cs) if cs.size == 1 => cs.head - case other => sys error "Substitutions should map single CaptureParams to single CaptureParams, got ${other}" - } - def get(x: CaptUnificationVar): Option[Captures] = captures.get(x) - - // amounts to parallel substitution - def ++(other: Substitutions): Substitutions = Substitutions(values ++ other.values, captures ++ other.captures) - - // shadowing - private def without(tps: List[TypeVar], cps: List[Capture]): Substitutions = - Substitutions( - values.filterNot { case (t, _) => tps.contains(t) }, - captures.filterNot { case (t, _) => cps.contains(t) } - ) - - // TODO we DO need to distinguish between substituting unification variables for unification variables - // and substituting concrete captures in unification variables... These are two fundamentally different operations. - def substitute(c: Captures): Captures = c match { - case x: CaptUnificationVar => captures.getOrElse(x, x) - case CaptureSet(cs) => CaptureSet(cs.map { - case x: Capture => - get(x).getOrElse(x) - }) - } - - def substitute(t: ValueType): ValueType = t match { - case ValueTypeRef(x) => - values.getOrElse(x, t) - case ValueTypeApp(t, args) => - ValueTypeApp(t, args.map { substitute }) - case BoxedType(tpe, capt) => - BoxedType(substitute(tpe), substitute(capt)) - } - - def substitute(t: Effects): Effects = Effects(t.toList.map(substitute)) - def substitute(t: InterfaceType): InterfaceType = t match { - case InterfaceType(cons, args) => InterfaceType(cons, args.map(substitute)) - } - - def substitute(t: BlockType): BlockType = t match { - case e: InterfaceType => substitute(e) - case b: FunctionType => substitute(b) - } - - def substitute(t: FunctionType): FunctionType = t match { - case FunctionType(tps, cps, vps, bps, ret, eff) => - // do not substitute with types parameters bound by this function! - val substWithout = without(tps, cps) - FunctionType( - tps, - cps, - vps map substWithout.substitute, - bps map substWithout.substitute, - substWithout.substitute(ret), - substWithout.substitute(eff)) - } -} - -object Substitutions { - val empty: Substitutions = Substitutions(Map.empty[TypeVar, ValueType], Map.empty[CaptVar, Captures]) - def apply(values: List[(TypeVar, ValueType)], captures: List[(CaptVar, Captures)]): Substitutions = Substitutions(values.toMap, captures.toMap) - def types(keys: List[TypeVar], values: List[ValueType]): Substitutions = Substitutions((keys zip values).toMap, Map.empty) - def captures(keys: List[CaptVar], values: List[Captures]): Substitutions = Substitutions(Map.empty, (keys zip values).toMap) -} diff --git a/effekt/shared/src/main/scala/effekt/typer/TypeComparer.scala b/effekt/shared/src/main/scala/effekt/typer/TypeComparer.scala deleted file mode 100644 index c4756fedc..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/TypeComparer.scala +++ /dev/null @@ -1,225 +0,0 @@ -package effekt -package typer - -import effekt.symbols.* -import effekt.symbols.builtins.{ TBottom, TTop } -import effekt.typer.ErrorContext.FunctionEffects - -/** - * A side effecting type comparer - * - * TODO the comparer should build up a "deconstruction trace" that can be used for better type errors. - */ -trait TypeUnifier { - // "unification effects" - def requireLowerBound(x: UnificationVar, tpe: ValueType, ctx: ErrorContext): Unit - def requireUpperBound(x: UnificationVar, tpe: ValueType, ctx: ErrorContext): Unit - def requireEqual(x: UnificationVar, tpe: ValueType, ctx: ErrorContext): Unit - - def requireSubregion(lower: Captures, upper: Captures, ctx: ErrorContext): Unit - - def abort(msg: String, ctx: ErrorContext): Nothing - def error(msg: String, ctx: ErrorContext): Unit - def error(left: Type, right: Type, ctx: ErrorContext): Unit - - def unify(c1: Captures, c2: Captures, ctx: ErrorContext): Unit = ctx.polarity match { - case Covariant => requireSubregion(c1, c2, ctx) - case Contravariant => requireSubregion(c2, c1, ctx) - case Invariant => requireSubregion(c1, c2, ctx); requireSubregion(c2, c1, ctx) - } - def unify(c1: Capture, c2: Capture, ctx: ErrorContext): Unit = unify(CaptureSet(Set(c1)), CaptureSet(Set(c2)), ctx) - - def unifyValueTypes(tpe1: ValueType, tpe2: ValueType, ctx: ErrorContext): Unit = (tpe1, tpe2, ctx.polarity) match { - case (t, s, _) if t == s => () - - case (_, TTop, Covariant) => () - case (TBottom, _, Covariant) => () - - case (TTop, _, Contravariant) => () - case (_, TBottom, Contravariant) => () - - case (ValueTypeRef(s: UnificationVar), t: ValueType, Covariant) => requireUpperBound(s, t, ctx) - case (s: ValueType, ValueTypeRef(t: UnificationVar), Covariant) => requireLowerBound(t, s, ctx) - - case (ValueTypeRef(s: UnificationVar), t: ValueType, Contravariant) => requireLowerBound(s, t, ctx) - case (s: ValueType, ValueTypeRef(t: UnificationVar), Contravariant) => requireUpperBound(t, s, ctx) - - case (ValueTypeRef(s: UnificationVar), t: ValueType, Invariant) => requireEqual(s, t, ctx) - case (s: ValueType, ValueTypeRef(t: UnificationVar), Invariant) => requireEqual(t, s, ctx) - - // For now, we treat all type constructors as invariant. - case (ValueTypeApp(t1, args1), ValueTypeApp(t2, args2), _) => - - if (t1 != t2) { error(tpe1, tpe2, ErrorContext.TypeConstructor(ctx)) } - else if (args1.size != args2.size) { - abort(pp"Argument count does not match $t1 vs. $t2", ctx) // TODO add to context - } - - // TODO here we assume that the type constructor is covariant - (args1 zip args2) foreach { case (t1, t2) => unifyValueTypes(t1, t2, ErrorContext.TypeConstructorArgument(ctx)) } - - case (t @ BoxedType(tpe1, capt1), s @ BoxedType(tpe2, capt2), p) => - unifyBlockTypes(tpe1, tpe2, ErrorContext.BoxedTypeBlock(t, s, ctx)) - unify(capt1, capt2, ErrorContext.BoxedTypeCapture(t, s, ctx)) - - case (t, s, p) => - error(t, s, ctx) - } - - def unifyBlockTypes(tpe1: BlockType, tpe2: BlockType, ctx: ErrorContext): Unit = (tpe1, tpe2) match { - case (t: FunctionType, s: FunctionType) => unifyFunctionTypes(t, s, ctx) - case (t: InterfaceType, s: InterfaceType) => unifyInterfaceTypes(t, s, ctx) - case (t, s) => error(t, s, ctx) - } - - def unifyInterfaceTypes(tpe1: InterfaceType, tpe2: InterfaceType, ctx: ErrorContext): Unit = (tpe1, tpe2) match { - // for now block type constructors are invariant - case (InterfaceType(c1, targs1), InterfaceType(c2, targs2)) => - if (c1 != c2) { error(tpe1, tpe2, ErrorContext.TypeConstructor(ctx)) } - (targs1 zip targs2) foreach { case (t1, t2) => unifyValueTypes(t1, t2, ErrorContext.TypeConstructorArgument(ctx)) } - } - - def unifyEffects(eff1: Effects, eff2: Effects, ctx: ErrorContext): Unit = - if (eff1.toList.toSet != eff2.toList.toSet) error(pp"${eff2} is not equal to ${eff1}", ctx) - - def unifyFunctionTypes(tpe1: FunctionType, tpe2: FunctionType, ctx: ErrorContext): Unit = (tpe1, tpe2) match { - case ( - f1 @ FunctionType(tparams1, cparams1, vparams1, bparams1, ret1, eff1), - f2 @ FunctionType(tparams2, cparams2, vparams2, bparams2, ret2, eff2)) => - - if (tparams1.size != tparams2.size) - abort(pp"Type parameter count does not match $f1 vs. $f2", ctx) - - if (vparams1.size != vparams2.size) - abort(pp"Value parameter count does not match $f1 vs. $f2", ctx) - - if (bparams1.size != bparams2.size) - abort(pp"Block parameter count does not match $f1 vs. $f2", ctx) - - if (cparams1.size != cparams2.size) - abort(pp"Capture parameter count does not match $f1 vs. $f2", ctx) - - val targs1 = tparams1.map(ValueTypeRef.apply) - - val subst = Substitutions(tparams2 zip targs1, cparams2 zip cparams1.map(c => CaptureSet(c))) - val substVParams2 = vparams2 map subst.substitute - val substBParams2 = bparams2 map subst.substitute - val substRet2 = subst.substitute(ret2) - val substEffs2 = subst.substitute(eff2) - - (vparams1 zip substVParams2) foreach { case (t1, t2) => unifyValueTypes(t1, t2, ErrorContext.FunctionArgument(f1, f2, ctx)) } - (bparams1 zip substBParams2) foreach { case (t1, t2) => unifyBlockTypes(t1, t2, ErrorContext.FunctionArgument(f1, f2, ctx)) } - - unifyValueTypes(ret1, substRet2, ErrorContext.FunctionReturn(ctx)) - - // We compare effects to be equal, since we do not have subtyping on effects - // TODO verify that a different ordering doesn't interact badly with capture polymorphism - // i.e. () => (T at {@Exc}) / {Exc, Amb} vs () => (T at {@Exc}) / {Amb, Exc} - // this should be ruled out by canonical ordering. - unifyEffects(eff1, substEffs2, ErrorContext.FunctionEffects(ctx)) - } -} - -/** - * Merges two types by traversing them structurally. - * - * Is side effecting in that it influences the constraint graph. - * - * effect types need to be concrete to be mergable. - */ -trait TypeMerger extends TypeUnifier { - - /** - * Merging captures requires introducing new unification variables and is thus deferred to [[Unification.mergeCaptures]] - */ - def mergeCaptures(oldBound: Captures, newBound: Captures, ctx: ErrorContext): Captures - - // computes union or intersection, based on polarity - def mergeValueTypes(oldBound: ValueType, newBound: ValueType, ctx: ErrorContext): ValueType = - (oldBound, newBound, ctx.polarity) match { - case (t, s, _) if t == s => t - case (TBottom, t, Covariant) => t - case (t, TBottom, Covariant) => t - case (TTop, t, Contravariant) => t - case (t, TTop, Contravariant) => t - - // reuses the type unifier implementation (will potentially register new constraints) - // TODO we need to create a fresh unification variable here! - case (x @ ValueTypeRef(_: UnificationVar), tpe: ValueType, p) => - unifyValueTypes(x, tpe, ctx); x - case (tpe: ValueType, x @ ValueTypeRef(_: UnificationVar), p) => - unifyValueTypes(tpe, x, ctx); x - - case (ValueTypeApp(cons1, args1), ValueTypeApp(cons2, args2), _) => - if (cons1 != cons2) abort(pp"Cannot merge different constructors: $cons1 vs. $cons2", ErrorContext.TypeConstructor(ctx)) - if (args1.size != args2.size) abort(pp"Different count of argument to type constructor: $oldBound vs $newBound", ctx) - - // TODO Here we assume the constructor is invariant - val mergedArgs = (args1 zip args2).map { - case (t1, t2) => - mergeValueTypes(t1, t2, ErrorContext.TypeConstructorArgument(ctx)) - } - ValueTypeApp(cons1, mergedArgs) - - case (s @ BoxedType(tpe1, capt1), t @ BoxedType(tpe2, capt2), p) => - BoxedType( - mergeBlockTypes(tpe1, tpe2, ErrorContext.BoxedTypeBlock(s, t, ctx)), - mergeCaptures(capt1, capt2, ErrorContext.BoxedTypeCapture(s, t, ctx)) - ) - - case _ => - abort(pp"Cannot merge ${oldBound} with ${newBound}", ctx) - } - - def mergeBlockTypes(oldBound: BlockType, newBound: BlockType, ctx: ErrorContext): BlockType = (oldBound, newBound) match { - case (t: FunctionType, s: FunctionType) => mergeFunctionTypes(t, s, ctx) - case (t: InterfaceType, s: InterfaceType) => mergeInterfaceTypes(t, s, ctx) - case (t, s) => abort(pp"The two types ${t} and ${s} are not compatible", ctx) - } - - def mergeInterfaceTypes(tpe1: InterfaceType, tpe2: InterfaceType, ctx: ErrorContext): InterfaceType = (tpe1, tpe2) match { - // for now block type constructors are invariant - case (InterfaceType(c1, targs1), InterfaceType(c2, targs2)) => - if (c1 != c2) abort(pp"The two types ${ c1 } and ${ c2 } are not compatible", ErrorContext.TypeConstructor(ctx)) - val mergedConstructor = c1 - val mergedArgs = (targs1 zip targs2) map { case (t1, t2) => mergeValueTypes(t1, t2, ErrorContext.TypeConstructorArgument(ctx)) } - InterfaceType(mergedConstructor, mergedArgs) - } - - def mergeFunctionTypes(tpe1: FunctionType, tpe2: FunctionType, ctx: ErrorContext): FunctionType = (tpe1, tpe2) match { - case ( - f1 @ FunctionType(tparams1, cparams1, vparams1, bparams1, ret1, eff1), - f2 @ FunctionType(tparams2, cparams2, vparams2, bparams2, ret2, eff2) - ) => - - if (tparams1.size != tparams2.size) - abort(pp"Type parameter count does not match $f1 vs. $f2", ctx) - - if (vparams1.size != vparams2.size) - abort(pp"Value parameter count does not match $f1 vs. $f2", ctx) - - if (bparams1.size != bparams2.size) - abort(pp"Block parameter count does not match $f1 vs. $f2", ctx) - - if (cparams1.size != cparams2.size) - abort(pp"Capture parameter count does not match $f1 vs. $f2", ctx) - - // TODO potentially share code with unifyFunctionTypes and instantiate - - val targs1 = tparams1.map(ValueTypeRef.apply) - val subst = Substitutions(tparams2 zip targs1, cparams2 zip cparams1.map(c => CaptureSet(c))) - val substVParams2 = vparams2 map subst.substitute - val substBParams2 = bparams2 map subst.substitute - val substRet2 = subst.substitute(ret2) - val substEffs2 = subst.substitute(eff2) - - val mergedVps = (vparams1 zip substVParams2) map { case (t1, t2) => mergeValueTypes(t1, t2, ErrorContext.FunctionArgument(f1, f2, ctx)) } - val mergedBps = (bparams1 zip substBParams2) map { case (t1, t2) => mergeBlockTypes(t1, t2, ErrorContext.FunctionArgument(f1, f2, ctx)) } - val mergedRet = mergeValueTypes(ret1, substRet2, ErrorContext.FunctionReturn(ctx)) - - // We compare effects to be equal, since we do not have subtyping on effects - unifyEffects(eff1, substEffs2, ErrorContext.FunctionEffects(ctx)) - - FunctionType(tparams1, cparams1, mergedVps, mergedBps, mergedRet, eff1) - } -} diff --git a/effekt/shared/src/main/scala/effekt/typer/Unification.scala b/effekt/shared/src/main/scala/effekt/typer/Unification.scala deleted file mode 100644 index 98db1c99d..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/Unification.scala +++ /dev/null @@ -1,367 +0,0 @@ -package effekt -package typer - -import effekt.context.Context -import effekt.source.MatchPattern -import effekt.symbols.* -import effekt.symbols.builtins.{ TBottom, TTop } -import effekt.util.messages.ErrorReporter - - -sealed trait Polarity { def flip: Polarity } -case object Covariant extends Polarity { def flip = Contravariant} -case object Contravariant extends Polarity { def flip = Covariant } -case object Invariant extends Polarity { def flip = Invariant } - -/** - * The state of the unification scope, used for backtracking on overload resolution - * - * See [[Unification.backup]] and [[Unification.restore]] - */ -case class UnificationState( - scope: Scope, - constraints: Constraints -) - -sealed trait Scope -case object GlobalScope extends Scope -case class LocalScope( - types: List[UnificationVar], - captures: List[CaptUnificationVar], - parent: Scope) extends Scope - -/** - * A unification scope -- every fresh unification variable is associated with a scope. - * - * Structural comparison of types are outsourced into [[TypeComparer]], [[TypeUnifier]], and - * [[TypeMerger]]. This way, the dependencies are a bit clearer and testability is improved. - * - * TODO - * - [ ] All incoming types need to be "normalized": substituted and dealiased. - */ -class Unification(using C: ErrorReporter) extends TypeUnifier, TypeMerger, TypeInstantiator { self => - - // State of the unification engine - // ------------------------------- - private var scope: Scope = GlobalScope - protected var constraints = new Constraints - - - // Creating fresh unification variables - // ------------------------------------ - def freshTypeVar(underlying: TypeVar.TypeParam, call: source.Tree): UnificationVar = scope match { - case GlobalScope => sys error "Cannot add unification variables to global scope" - case s : LocalScope => - val x = new UnificationVar(underlying, call) - scope = s.copy(types = x :: s.types) - x - } - - def freshCaptVar(role: CaptUnificationVar.Role): CaptUnificationVar = scope match { - case GlobalScope => sys error "Cannot add unification variables to global scope" - case s : LocalScope => - val x = CaptUnificationVar(role) - scope = s.copy(captures = x :: s.captures) - x - } - - // Substitution - // ------------ - def substitution = constraints.subst - - def apply(e: Effects): Effects = - substitution.substitute(e) - - def apply(e: InterfaceType): InterfaceType = - substitution.substitute(e) - - def apply(tpe: BlockType): BlockType = - substitution.substitute(tpe) - - def apply(tpe: FunctionType): FunctionType = - substitution.substitute(tpe) - - def apply(tpe: ValueType): ValueType = - substitution.substitute(tpe) - - // Lifecycle management - // -------------------- - def backup(): UnificationState = UnificationState(scope, constraints.clone()) - def restore(state: UnificationState): Unit = - scope = state.scope - constraints = state.constraints.clone() - - def init() = - scope = GlobalScope - constraints = new Constraints - - def enterScope() = { - scope = LocalScope(Nil, Nil, scope) - } - - def forceSolve(vars: List[CaptUnificationVar]) = { - constraints.leave(Nil, vars) - } - - def leaveScope(additional: List[CaptUnificationVar]) = { - val LocalScope(types, captures, parent) = scope match { - case GlobalScope => sys error "Cannot leave global scope" - case l : LocalScope => l - } - scope = parent - - constraints.leave(types, captures ++ additional) - } - - def dumpConstraints() = - constraints.dumpConstraints() - - - - - // Registering new constraints - // --------------------------- - - /** - * Checks whether t1 <: t2 - * - * Has the side effect of registering constraints. - */ - def requireSubtype(t1: ValueType, t2: ValueType, errorContext: ErrorContext): Unit = - unifyValueTypes( - substitution.substitute(t1), - substitution.substitute(t2), - errorContext) - - def requireSubtype(t1: BlockType, t2: BlockType, errorContext: ErrorContext): Unit = - unifyBlockTypes( - substitution.substitute(t1), - substitution.substitute(t2), - errorContext) - - def requireSubregion(c1: Captures, c2: Captures)(using C: Context): Unit = - requireSubregion(c1, c2, ErrorContext.CaptureFlow(c1, c2, C.focus)) - - def requireSubregion(c1: Captures, c2: Captures, ctx: ErrorContext): Unit = requireSubregionWithout(c1, c2, Set.empty, ctx) - - /** - * Computes the join of all types, only called to merge the different arms of if and match - */ - def join(tpes: ValueType*): ValueType = - tpes.foldLeft[ValueType](TBottom) { (t1, t2) => - mergeValueTypes(t1, t2, ErrorContext.MergeTypes(apply(t1), apply(t2))) - } - - def requireSubregionWithout(lower: Captures, upper: Captures, filter: List[Capture])(using C: Context): Unit = - requireSubregionWithout(lower, upper, filter.toSet, ErrorContext.CaptureFlow(lower, upper, C.focus)) - - def requireSubregionWithout(lower: Captures, upper: Captures, filter: List[Capture], ctx: ErrorContext): Unit = - requireSubregionWithout(lower, upper, filter.toSet, ctx) - - def requireSubregionWithout(lower: Captures, upper: Captures, filter: Set[Capture], ctx: ErrorContext): Unit = - if (lower == CaptureSet()) return; - if (lower == upper) return; - (lower, upper) match { - case (CaptureSet(lows), CaptureSet(ups)) => - val notAllowed = lows -- (ups ++ filter) - if (notAllowed.nonEmpty) - error(pp"Used captures ${CaptureSet(notAllowed)} are not in the allowed set ${upper}", ctx) - case (x: CaptUnificationVar, y: CaptUnificationVar) => - constraints.connect(x, y, filter) - case (x: CaptUnificationVar, CaptureSet(cs)) => - constraints.requireUpper(cs ++ filter, x) - case (CaptureSet(cs), x: CaptUnificationVar) => - constraints.requireLower(cs -- filter, x) - } - - def without(caps: CaptUnificationVar, others: List[Capture]): Captures = - if (others.isEmpty) caps else caps match { - case x: CaptUnificationVar => - val y = freshCaptVar(CaptUnificationVar.Subtraction(others, x)) - constraints.connect(y, x, others.toSet) - y - } - - // Using collected information - // --------------------------- - - /** - * Instantiate a typescheme with provided type and capture arguments. - */ - def instantiate(tpe: FunctionType, targs: List[ValueType], cargs: List[Captures]): (List[ValueType], List[BlockType], ValueType, List[InterfaceType]) = { - val position = C.focus - val FunctionType(tparams, cparams, vparams, bparams, ret, eff) = substitution.substitute(tpe) - - assert(targs.size == tparams.size, - pp"Type argument and parameter size mismatch: ${targs.size} vs ${tparams.size} ($targs, $tparams)") - assert(cargs.size == cparams.size, - pp"Capture arguments and parameter size mismatch: ${cargs.size} vs ${cparams.size} ($cargs, $cparams)") - assert(cparams.size == (bparams.size + eff.distinct.size), - pp"Capture param count ${cparams.size} is not equal to bparam ${bparams.size} + controleffects ${eff.size}.\n ${tpe}") - - given Instantiation = Instantiation((tparams zip targs).toMap, (cparams zip cargs).toMap) - - val substitutedVparams = vparams map instantiate - val substitutedBparams = bparams map instantiate - val substitutedReturn = instantiate(ret) - - val substitutedEffects = instantiate(eff) - - (substitutedVparams, substitutedBparams, substitutedReturn, substitutedEffects) - } - - /** - * Instantiate a typescheme with fresh, rigid type variables - * - * i.e. `[T1, T2, C] (T1, T1) {sigma} => T2` becomes `(?T1, ?T1){} => ?T2[C !-> ?C]` - */ - def instantiateFresh(tpe: FunctionType): (List[ValueType], List[Captures], (List[ValueType], List[BlockType], ValueType, List[InterfaceType])) = { - val position = C.focus - val FunctionType(tparams, cparams, vparams, bparams, ret, eff) = substitution.substitute(tpe) - - val typeRigids = tparams map { t => ValueTypeRef(freshTypeVar(t, position)) } - val captRigids = cparams.map { param => freshCaptVar(CaptUnificationVar.VariableInstantiation(param, position)) } - - (typeRigids, captRigids, instantiate(tpe, typeRigids, captRigids)) - } - - - // Implementation Details - // ---------------------- - - def abort(msg: String) = C.abort(msg) - def abort(msg: String, ctx: ErrorContext) = C.abort(ErrorContext.explainInContext(msg, ctx)) - - def error(msg: String) = C.error(msg) - def error(msg: String, ctx: ErrorContext) = C.error(ErrorContext.explainInContext(msg, ctx)) - def error(left: symbols.Type, right: symbols.Type, ctx: ErrorContext) = C.error(ErrorContext.explainMismatch(left, right, ctx)) - - def requireEqual(x: UnificationVar, tpe: ValueType, ctx: ErrorContext): Unit = - requireLowerBound(x, tpe, ctx) - requireUpperBound(x, tpe, ctx) - - def requireLowerBound(x: UnificationVar, tpe: ValueType, ctx: ErrorContext) = - constraints.learn(x, tpe)((tpe1, tpe2) => unifyValueTypes(tpe1, tpe2, ErrorContext.MergeInvariant(ctx))) - - def requireUpperBound(x: UnificationVar, tpe: ValueType, ctx: ErrorContext) = - constraints.learn(x, tpe)((tpe1, tpe2) => unifyValueTypes(tpe1, tpe2, ErrorContext.MergeInvariant(ctx))) - - def mergeCaptures(oldBound: Captures, newBound: Captures, ctx: ErrorContext): Captures = (oldBound, newBound, ctx.polarity) match { - case (CaptureSet(xs), CaptureSet(ys), Covariant) => CaptureSet(xs intersect ys) - case (CaptureSet(xs), CaptureSet(ys), Contravariant) => CaptureSet(xs union ys) - case (CaptureSet(xs), CaptureSet(ys), Invariant) => if (xs == ys) oldBound else abort(pp"Capture set ${CaptureSet(xs)} is not equal to ${CaptureSet(ys)}", ctx) - case (x: CaptUnificationVar, CaptureSet(ys), p) if ys.isEmpty => x - case (CaptureSet(xs), y: CaptUnificationVar, p) if xs.isEmpty => y - case (x: CaptUnificationVar, CaptureSet(ys), p) => mergeCaptures(ys.toList, List(x), ctx) - case (CaptureSet(xs), y: CaptUnificationVar, p) => mergeCaptures(xs.toList, List(y), ctx) - case (x: CaptUnificationVar, y: CaptUnificationVar, p) => mergeCaptures(Nil, List(x, y), ctx) - } - - def mergeCaptures(cs: List[Captures], ctx: ErrorContext): Captures = - val (concrete, variables) = cs.partitionMap { - case CaptureSet(xs) => Left(xs) - case x: CaptUnificationVar => Right(x) - } - mergeCaptures(concrete.flatten, variables, ctx) - - /** - * Should create a fresh unification variable bounded by the given captures - */ - def mergeCaptures(concreteBounds: List[Capture], variableBounds: List[CaptUnificationVar], ctx: ErrorContext): Captures = - // do not introduce a unification variable if there are ONLY concrete bounds - if (variableBounds.isEmpty) return CaptureSet(concreteBounds) - - val newVar = freshCaptVar(CaptUnificationVar.Substitution()) - ctx.polarity match { - case Covariant => - constraints.requireLower(concreteBounds.toSet, newVar) - variableBounds.foreach { b => constraints.connect(b, newVar, Set.empty) } - case Contravariant => - constraints.requireUpper(concreteBounds.toSet, newVar) - variableBounds.foreach { b => constraints.connect(newVar, b, Set.empty) } - case Invariant => - constraints.requireLower(concreteBounds.toSet, newVar) - constraints.requireUpper(concreteBounds.toSet, newVar) - variableBounds.foreach { b => constraints.connect(newVar, b, Set.empty) } - variableBounds.foreach { b => constraints.connect(b, newVar, Set.empty) } - } - - newVar -} - - - -case class Instantiation(values: Map[TypeVar, ValueType], captures: Map[Capture, Captures]) - -trait TypeInstantiator { self: Unification => - - private def valueInstantiations(using i: Instantiation): Map[TypeVar, ValueType] = i.values - private def captureInstantiations(using i: Instantiation): Map[Capture, Captures] = i.captures - - private def captureParams(using Instantiation) = captureInstantiations.keys.toSet - - // shadowing - private def without(tps: List[TypeVar], cps: List[Capture])(using Instantiation): Instantiation = - Instantiation( - valueInstantiations.filterNot { case (t, _) => tps.contains(t) }, - captureInstantiations.filterNot { case (t, _) => cps.contains(t) } - ) - - def instantiate(c: Captures)(using Instantiation): Captures = - val concreteCapture = - c match { - case CaptureSet(cs) => cs - - // Right now, we can only substitute into concrete capture sets, not unification variables - // since we only have negation nodes, but no substitution nodes. - // - // we *could* use the fact that some variables cannot occur lexically in the variable (depending on the scope - // it was introduced in). - // - // right now, we force solving to continue, instead of giving up. - case x: CaptUnificationVar => - val capt = constraints.forceSolving(x) - capt.captures - } - val contained = captureParams intersect concreteCapture // Should not contain CaptureOf - if (contained.isEmpty) return CaptureSet(concreteCapture) - - val remaining = (concreteCapture -- captureParams).toList - - // TODO do we need to respect the polarity here? Maybe wellformedness should exclude captures in negative position? - mergeCaptures(CaptureSet(remaining) :: contained.toList.map { p => captureInstantiations(p) }, ErrorContext.MergeCaptures()) - - - def instantiate(t: ValueType)(using Instantiation): ValueType = t match { - case ValueTypeRef(x) => - valueInstantiations.getOrElse(x, t) - case ValueTypeApp(t, args) => - ValueTypeApp(t, args.map { instantiate }) - case BoxedType(tpe, capt) => - BoxedType(instantiate(tpe), instantiate(capt)) - } - - def instantiate(t: Effects)(using Instantiation): List[InterfaceType] = t.toList.map(instantiate) - - def instantiate(t: BlockType)(using Instantiation): BlockType = t match { - case e: InterfaceType => instantiate(e) - case b: FunctionType => instantiate(b) - } - - def instantiate(t: InterfaceType)(using Instantiation): InterfaceType = t match { - case InterfaceType(c, targs) => InterfaceType(c, targs map instantiate) - } - - def instantiate(t: FunctionType)(using i: Instantiation): FunctionType = t match { - case FunctionType(tps, cps, vps, bps, ret, eff) => - // do not substitute with types parameters bound by this function! - given Instantiation = without(tps, cps)(using i) - FunctionType( - tps, - cps, - vps map instantiate, - bps map instantiate, - instantiate(ret), - Effects(instantiate(eff))) - } -} diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala deleted file mode 100644 index 3ab2440b6..000000000 --- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala +++ /dev/null @@ -1,360 +0,0 @@ -package effekt -package typer - -import effekt.context.{ Annotations, Context } -import effekt.symbols.* -import effekt.context.assertions.* -import effekt.source.{ CallTarget, Def, ExprTarget, IdTarget, MatchGuard, MatchPattern, Tree } -import effekt.source.Tree.Visit -import effekt.util.messages.ErrorReporter - -/** - * Lexical context used to check wellformedness of inferred types - */ -case class WFContext(typesInScope: Set[TypeVar], capturesInScope: Set[Capture]) - -def binding(types: Set[TypeVar] = Set.empty, captures: Set[Capture] = Set.empty)(prog: WFContext ?=> Unit)(using WF: WFContext, C: Context): Unit = - prog(using WFContext(WF.typesInScope ++ types, WF.capturesInScope ++ captures)) - -/** - * Performs additional wellformedness checks that are easier to do on already - * inferred and annotated trees. - * - * Checks: - * - exhaustivity of pattern matching - * - escape of capabilities through types - * - scope extrusion of type parameters (of existentials and higher-rank types) - * - * Note: Resources (like io and global) are not treated as being free (alternatively, we could also prepopulate the WFContext) - */ -object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] { - - val phaseName = "wellformedness" - - def run(input: Typechecked)(using C: Context) = - val Typechecked(source, tree, mod) = input - C.timed(phaseName, source.name) { check(mod, tree) } - - if (Context.messaging.hasErrors) { None } - else { Some(input) } - - def check(mod: Module, tree: source.ModuleDecl)(using Context): Unit = - given WFContext = WFContext(Set.empty, Set.empty) - query(tree) - - /** - * Constructive evidence indicating the result of checking wellformedness - */ - enum Wellformed { - case Yes - case No(captures: Set[Capture], types: Set[TypeVar]) - } - - def wellformed(o: Type)(using ctx: WFContext): Wellformed = - val captures = freeCapture(o) -- ctx.capturesInScope - val types = freeTypes(o) -- ctx.typesInScope - if captures.isEmpty && types.isEmpty then Wellformed.Yes - else Wellformed.No(captures, types) - - inline def wellformed(o: Type, tree: Tree)(msgCapture: Set[Capture] => String)(msgType: Set[TypeVar] => String)(using ctx: WFContext, E: ErrorReporter): Unit = - wellformed(o) match { - case Wellformed.No(captures, types) => - if captures.nonEmpty then E.at(tree) { E.abort(msgCapture(captures)) } - if types.nonEmpty then E.at(tree) { E.abort(msgType(types)) } - case _ => () - } - - inline def wellformed(tpe: Type, tree: Tree, - inline locationDetail: => String, - specificTypes: PartialFunction[Set[TypeVar], String] = PartialFunction.empty, - specificCaptures: PartialFunction[Set[Capture], String] = PartialFunction.empty - )(using ctx: WFContext, E: ErrorReporter): Unit = - wellformed(tpe, tree) { - case captures if specificCaptures.isDefinedAt(captures) => specificCaptures(captures) - case captures if captures.size == 1 => pp"Capture ${captures.head} escapes through type ${tpe}${locationDetail}." - case captures => pp"Captures ${showCaptures(captures)} escape through type ${tpe}${locationDetail}." - } { - case tpes if specificTypes.isDefinedAt(tpes) => specificTypes(tpes) - case tpes if tpes.size == 1 => pp"Type ${tpes.head} escapes through type ${tpe}${locationDetail}." - case tpes => pp"Types ${showTypes(tpes)} escape through type ${tpe}${locationDetail}." - } - - def reportCaptures(wf: Wellformed)(msg: Set[Capture] => String)(using E: ErrorReporter): Unit = - wf match { - case Wellformed.No(captures, types) if captures.nonEmpty => E.error(msg(captures)) - case _ => () - } - - def showTypes(types: Set[TypeVar]): String = types.map(TypePrinter.show).mkString(", ") - def showCaptures(captures: Set[Capture]): String = pp"${CaptureSet(captures)}" - def showPosition(n: Int): String = n match { - case 1 => "first" - case 2 => "second" - case 3 => "third" - case 4 => "forth" - case 5 => "fifth" - case 6 => "sixth" - case n if n % 10 == 3 => n.toString + "rd" - case n => n.toString + "th" - } - - override def stmt(using Context, WFContext) = { - case stmt @ source.DefStmt(tree @ source.VarDef(id, annot, rhs), rest) => - val capt = tree.symbol.capture - binding(captures = Set(capt)) { - - query(rhs) - query(rest) - - val tpe = Context.inferredTypeOf(rest) - - wellformed(tpe, stmt, " inferred as return type", - specificCaptures = { - case captures if captures.size == 1 && captures.contains(capt) => - pp"Local variable ${id} escapes through the returned value of type ${tpe}." - }) - - val varTpe = Context.inferredTypeOf(rhs) - - wellformed(varTpe, stmt, pp" inferred for mutable variable ${id}") - } - } - - override def expr(using Context, WFContext) = { - - /** - * For handlers we check that the return type does not mention any bound capabilities - */ - case tree @ source.TryHandle(prog, handlers) => - val bound = Context.annotation(Annotations.BoundCapabilities, tree).map(_.capture).toSet - val usedEffects = Context.annotation(Annotations.InferredEffect, tree) - val tpe = Context.inferredTypeOf(prog) - - val free = freeCapture(tpe) - - // first check prog and handlers (to report the "tightest" error) - binding(captures = bound) { query(prog) } - - handlers foreach { query } - - // Now check the return type ... - wellformed(tpe, prog, " inferred as return type of the handled statement", - specificCaptures = { - case captures if (captures intersect bound).nonEmpty => - pp"The return type ${tpe} of the handled statement is not allowed to refer to any of the bound capabilities, but mentions: ${showCaptures(captures)}." - }) - - // ... and the handled effects - usedEffects.effects.foreach { tpe => - wellformed(tpe, prog, locationDetail = " as part of the inferred effect") - } - - case tree @ source.Region(id, body) => - val reg = tree.symbol - val tpe = Context.inferredTypeOf(body) - - binding(captures = Set(reg.capture)) { query(body) } - - wellformed(tpe, body, " inferred as return type of the region body", - specificCaptures = { - case captures if captures contains reg.capture => - pp"The return type ${tpe} of the region body is not allowed to refer to region ${reg.capture}." - }) - - case tree @ source.Match(scrutinees, clauses, default) => Context.at(tree) { - // TODO copy annotations from default to synthesized defaultClause (in particular positions) - val defaultPattern = scrutinees match { - case List(_) => source.IgnorePattern() - case scs => source.MultiPattern(List.fill(scs.length){source.IgnorePattern()}) - } - - val defaultClause = default.toList.map(body => source.MatchClause(defaultPattern, Nil, body)) - ExhaustivityChecker.checkExhaustive(scrutinees, clauses ++ defaultClause) - - scrutinees foreach { query } - clauses foreach { query } - default foreach query - - val tpe = Context.inferredTypeOf(tree) - wellformed(tpe, tree, " inferred as return type of the match") - } - - case tree @ source.BlockLiteral(tps, vps, bps, body) => - val boundTypes = tps.map(_.symbol.asTypeParam).toSet[TypeVar] - val capabilities = Context.annotation(Annotations.BoundCapabilities, tree).map(_.capture).toSet - val blocks = bps.map(_.id.symbol.asBlockParam.capture).toSet - binding(types = boundTypes, captures = capabilities ++ blocks) { - query(body) - - val tpe = Context.inferredTypeOf(body) - wellformed(tpe, tree, " inferred as return type") - } - - case tree @ source.Call(target, targs, vargs, bargs) => - target match { - case CallTarget.IdTarget(id) => () - case ExprTarget(e) => query(e) - } - val inferredTypeArgs = Context.typeArguments(tree) - inferredTypeArgs.zipWithIndex.foreach { case (tpe, index) => - wellformed(tpe, tree, pp" inferred as ${showPosition(index + 1)} type argument") - } - vargs.foreach(query) - bargs.foreach(query) - - case tree @ source.MethodCall(receiver, id, targs, vargs, bargs) => - query(receiver) - - val inferredTypeArgs = Context.typeArguments(tree) - inferredTypeArgs.zipWithIndex.foreach { case (tpe, index) => - wellformed(tpe, tree, pp" inferred as ${showPosition(index + 1)} type argument") - } - vargs.foreach(query) - bargs.foreach(query) - - case tree @ source.Do(effect, id, targs, vargs, bargs) => - val inferredTypeArgs = Context.typeArguments(tree) - inferredTypeArgs.zipWithIndex.foreach { case (tpe, index) => - wellformed(tpe, tree, pp" inferred as ${showPosition(index + 1)} type argument") - } - vargs.foreach(query) - bargs.foreach(query) - - case tree @ source.If(guards, thn, els) => - var bound: Set[TypeVar] = Set.empty - - guards.foreach { g => - binding(types = bound) { bound = bound ++ queryGuardsExistentials(g) } - } - - binding(types = bound) { - query(thn) - query(els) - } - - val tpe = Context.inferredTypeOf(tree) - wellformed(tpe, tree, " inferred for the result of the if statement") - - case tree @ source.While(guards, block, default) => - var bound: Set[TypeVar] = Set.empty - - guards.foreach { g => - binding(types = bound) { bound = bound ++ queryGuardsExistentials(g) } - } - - binding(types = bound) { - query(block) - default.foreach(query) - } - - val tpe = Context.inferredTypeOf(tree) - wellformed(tpe, tree, " inferred for the result of the while statement") - } - - override def query(c: source.MatchClause)(using Context, WFContext): Unit = c match { - case source.MatchClause(pattern, guards, body) => - - var bound: Set[TypeVar] = existentials(pattern) - - guards.foreach { g => - binding(types = bound) { bound = bound ++ queryGuardsExistentials(g) } - } - - binding(types = bound) { - query(body) - } - } - - override def query(op: source.OpClause)(using Context, WFContext): Unit = op match { - case tree @ source.OpClause(id, tps, vps, bps, ret, body, resume) => - val boundTypes = Context.annotation(Annotations.TypeParameters, op).toSet - // bound capabilities are ONLY available on New(Implementation(OpClause ... ))) - // but not TryHandle(Implementation(OpClause) since their they are bound by the resumption - val capabilities = Context.annotationOption(Annotations.BoundCapabilities, tree) match { - case Some(caps) => caps.map(_.capture).toSet - case None => Set.empty - } - val blocks = bps.map(_.id.symbol.asBlockParam.capture).toSet - binding(types = boundTypes, captures = capabilities ++ blocks) { - query(body) - - val tpe = Context.inferredTypeOf(body) - wellformed(tpe, body, pp" inferred as return type of operation ${id}") - } - } - - def queryGuardsExistentials(p: MatchGuard)(using Context, WFContext): Set[TypeVar] = p match { - case MatchGuard.BooleanGuard(condition) => - query(condition); - Set.empty - case MatchGuard.PatternGuard(scrutinee, pattern) => - query(scrutinee); - existentials(pattern) - } - - def existentials(p: MatchPattern)(using Context): Set[TypeVar] = p match { - case p @ MatchPattern.TagPattern(id, patterns) => - Context.annotation(Annotations.TypeParameters, p).toSet ++ patterns.flatMap(existentials) - case _ => Set.empty - } - - override def defn(using C: Context, WF: WFContext) = { - case tree @ source.FunDef(id, tps, vps, bps, ret, body) => - val boundTypes = tps.map(_.symbol.asTypeParam).toSet[TypeVar] - val capabilities = Context.annotation(Annotations.BoundCapabilities, tree).map(_.capture).toSet - val blocks = bps.map(_.id.symbol.asBlockParam.capture).toSet - binding(types = boundTypes, captures = capabilities ++ blocks) { - - query(body) - - val tpe = Context.inferredTypeOf(body) - wellformed(tpe, body, pp" inferred as return type of ${id}") - } - - case tree @ source.ExternDef(capture, id, tps, vps, bps, ret, bodies) => - val boundTypes = tps.map(_.symbol.asTypeParam).toSet[TypeVar] - val boundCapts = bps.map(_.id.symbol.asBlockParam.capture).toSet - binding(types = boundTypes, captures = boundCapts) { bodies.foreach(query) } - - case tree @ source.RegDef(id, annot, region, init) => - wellformed(Context.typeOf(id.symbol), tree, pp" inferred as type of region-allocated variable") - query(init) - } - - // Can only compute free capture on concrete sets - def freeCapture(o: Any): Set[Capture] = o match { - // we do not treat resources as free - case t: symbols.CaptureParam => Set(t) - case t: symbols.LexicalRegion => Set(t) - - case FunctionType(tparams, cparams, vparams, bparams, ret, eff) => - // TODO what with capabilities introduced for eff--those are bound in ret? - freeCapture(vparams) ++ freeCapture(bparams) ++ freeCapture(ret) ++ freeCapture(eff) -- cparams.toSet - case CaptureSet(cs) => freeCapture(cs) - case x: CaptUnificationVar => sys error s"Cannot compute free variables for unification variable ${x}" - case x: UnificationVar => sys error s"Cannot compute free variables for unification variable ${x}" - case _: Symbol | _: String => Set.empty // don't follow symbols - case t: Iterable[t] => - t.foldLeft(Set.empty[Capture]) { case (r, t) => r ++ freeCapture(t) } - case p: Product => - p.productIterator.foldLeft(Set.empty[Capture]) { case (r, t) => r ++ freeCapture(t) } - case _ => - Set.empty - } - - def freeTypes(o: Any): Set[TypeVar] = o match { - case t: symbols.TypeVar => Set(t) - case FunctionType(tps, cps, vps, bps, ret, effs) => - freeTypes(vps) ++ freeTypes(bps) ++ freeTypes(ret) ++ freeTypes(effs) -- tps.toSet - case e: Effects => freeTypes(e.toList) - case _: Symbol | _: String => Set.empty // don't follow symbols - case t: Iterable[t] => - t.foldLeft(Set.empty[TypeVar]) { case (r, t) => r ++ freeTypes(t) } - case p: Product => - p.productIterator.foldLeft(Set.empty[TypeVar]) { case (r, t) => r ++ freeTypes(t) } - case _ => - Set.empty - } - - def Context(implicit ctx: Context): Context = ctx -} diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index 3808984c9..e3bcf0f03 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -1,7 +1,7 @@ package effekt package util -import effekt.symbols.{ Capture, Captures, Effects, ErrorMessageInterpolator, LocalName, Name, NoName, QualifiedName, TypePrinter } +import effekt.symbols.{ LocalName, Name, NoName, QualifiedName } import effekt.util.messages.* import kiama.util.{ Messaging, Position, Positions, Severities } import kiama.util.Severities._ @@ -61,44 +61,11 @@ trait ColoredMessaging extends EffektMessaging { case ParseError(msg, range) => msg case PlainTextError(msg, range, severity) => msg case StructuredError(StructuredMessage(sc, args), _, _) => sc.s(args.map { - case id: source.IdDef => highlight(TypePrinter.show(id)) - case id: source.IdRef => highlight(TypePrinter.show(id)) case name: Name => highlight(name.name) - case t: symbols.Type => highlight(TypePrinter.show(t)) - case t: Capture => highlight(TypePrinter.show(t)) - case t: Captures => highlight(TypePrinter.show(t)) - case t: Effects => highlight(TypePrinter.show(t)) case n: Int => highlight(n.toString) case nested: EffektError => formatContent(nested) case other => other.toString }: _*) - case AmbiguousOverloadError(matches, range) => - val title = bold("Ambiguous overload.\n") - val mainMessage = s"${title}There are multiple overloads, which all would type check:" - - val longestNameLength = matches.map { case (sym, tpe) => fullname(sym.name).size }.max - - val explanations = matches map { - case (sym, tpe) => - val name = fullname(sym.name) - val padding = " " * (longestNameLength - name.size) - pp"- ${highlight(name)}: ${padding}${tpe}" - } - - mainMessage + "\n" + explanations.mkString("\n") - case FailedOverloadError(failedAttempts, range) => - val title = bold("Cannot typecheck call.\n") - val mainMessage = s"${title}There are multiple overloads, which all fail to check:" - - val explanations = failedAttempts map { - case (sym, tpe, msgs) => - val nestedErrors = msgs.map { msg => formatContent(msg) }.mkString("\n") - - val header = underlined(pp"Possible overload: ${highlight(fullname(sym.name))}") + underlined(pp" of type ${tpe}") - s"$header\n${indent(nestedErrors)}" - } - - mainMessage + "\n\n" + explanations.mkString("\n\n") + "\n" } def fullname(n: Name): String = n match { diff --git a/effekt/shared/src/main/scala/effekt/util/Control.scala b/effekt/shared/src/main/scala/effekt/util/Control.scala deleted file mode 100644 index d85082d8a..000000000 --- a/effekt/shared/src/main/scala/effekt/util/Control.scala +++ /dev/null @@ -1,226 +0,0 @@ -package effekt -package util - -import scala.collection.mutable - -/** - * This is literally a copy of Scala-Effekt, a bit more simplified. - * Currently not used! Mostly here in case we want to implement an interpreter again. - */ -package object control { - - class Capability { - type Res - } - - type Cap[R] = Capability { type Res = R } - - private[control] type Frame[-A, +B] = A => Control[B] - - trait Control[+A] { outer => - - // alias for flatMap since it is used the most - // def apply[B](f: A => Control[B]): Control[B] = flatMap(f) - - def run(): A = Result.trampoline(Impure(this, ReturnCont(identity[A]))) - def map[B](f: A => B): Control[B] = flatMap { a => pure(f(a)) } - def andThen[B](f: Control[B]): Control[B] = this flatMap { _ => f } - def flatMap[B](f: A => Control[B]): Control[B] = - Computation { k => Impure(outer, k flatMap f) } - - def apply[R](k: MetaCont[A, R]): Result[R] - } - - def pure[A](v: A): Control[A] = new Trivial(v) - - def sequence[R](ar: List[Control[R]]): Control[List[R]] = ar match { - case Nil => pure { Nil } - case (r :: rs) => for { - rv <- r - rsv <- sequence(rs) - } yield rv :: rsv - } - - private[control] final class Trivial[+A](a: A) extends Control[A] { - def apply[R](k: MetaCont[A, R]): Result[R] = k(a) - - override def run(): A = a - override def toString = s"Trivial($a)" - } - - private[control] sealed trait ω - - private[control] final case class Computation[+A](body: MetaCont[A, ω] => Result[ω]) extends Control[A] { - def apply[R](k: MetaCont[A, R]): Result[R] = - body(k.asInstanceOf[MetaCont[A, ω]]).asInstanceOf[Result[R]] - } - - def use[A](c: Capability)(f: (A => Control[c.Res]) => Control[c.Res]): Control[A] = Computation { k => - val (init, tail) = k splitAt c - val localCont: A => Control[c.Res] = a => Computation[c.Res] { k => - val repushedPrompt = init append HandlerCont[c.Res, ω](c, k) - Impure(pure(a), repushedPrompt) - } - Impure(f(localCont), tail) - } - - final def handle(h: Capability)(c: Control[h.Res]): Control[h.Res] = Computation { k => - Impure(c, HandlerCont[h.Res, ω](h, k)) - } - - final def handle[R](f: Cap[R] => Control[R]): Control[R] = { - val c = new Capability { type Res = R } - handle(c)(f(c)) - } - - final def lookup[V](key: AnyRef): Control[V] = Computation { k => - Impure(pure((k lookup key).asInstanceOf[V]), k) - } - - // introduces a new scope - final def bind[R](key: AnyRef, value: Any)(body: Control[R]): Control[R] = Computation { k => - Impure(body, k.bind(key, value)) - } - - final def update[R](key: AnyRef, value: Any): Control[Unit] = Computation { k => - Impure(pure(k.update(key, value)), k) - } - - sealed trait Result[+A] { def isPure: Boolean } - - private[control] case class Pure[A](value: A) extends Result[A] { val isPure = true } - - private[control] case class Impure[A, R](c: Control[R], k: MetaCont[R, A]) extends Result[A] { - val isPure = false - } - - object Result { - def trampoline[A](y: Result[A]): A = { - var res: Result[A] = y - - while (!res.isPure) { - val imp = res.asInstanceOf[Impure[A, Any]] - res = imp.c.apply(imp.k) - } - res.asInstanceOf[Pure[A]].value - } - } - - private[control] sealed trait MetaCont[-A, +B] extends Serializable { - def apply(a: A): Result[B] - - def append[C](s: MetaCont[B, C]): MetaCont[A, C] - - def splitAt(c: Capability): (MetaCont[A, c.Res], MetaCont[c.Res, B]) - - def lookup(key: AnyRef): Any - - def update(key: AnyRef, value: Any): Unit - - def map[C](f: C => A): MetaCont[C, B] = flatMap(f andThen pure) - - def flatMap[C](f: Frame[C, A]): MetaCont[C, B] = FramesCont(List(f.asInstanceOf[Frame[Any, Any]]), this) - - def bind(key: AnyRef, value: Any): MetaCont[A, B] = StateCont(mutable.Map(key -> value), this) - } - - private[control] case class ReturnCont[-A, +B](f: A => B) extends MetaCont[A, B] { - final def apply(a: A): Result[B] = Pure(f(a)) - - final def append[C](s: MetaCont[B, C]): MetaCont[A, C] = s map f - - final def splitAt(c: Capability) = sys error s"Prompt $c not found on the stack." - - final def lookup(key: AnyRef): Any = sys error s"Key $key not found on the stack." - - final def update(key: AnyRef, value: Any) = sys error s"Key $key not found on the stack." - - override def map[C](g: C => A): MetaCont[C, B] = ReturnCont(g andThen f) - - override def toString = "[]" - } - - private[control] case class CastCont[-A, +B]() extends MetaCont[A, B] { - - final def apply(a: A): Result[B] = Pure(a.asInstanceOf[B]) - - final def append[C](s: MetaCont[B, C]): MetaCont[A, C] = s.asInstanceOf[MetaCont[A, C]] - - final def splitAt(c: Capability) = sys error s"Prompt $c not found on the stack." - - final def lookup(key: AnyRef): Any = sys error s"Key $key not found on the stack." - final def update(key: AnyRef, value: Any) = sys error s"Key $key not found on the stack." - - override def map[C](g: C => A): MetaCont[C, B] = ReturnCont(x => g(x).asInstanceOf[B]) - - override def toString = "{}" - } - - private[control] case class FramesCont[-A, B, +C](frames: List[Frame[Any, Any]], tail: MetaCont[B, C]) extends MetaCont[A, C] { - - final def apply(a: A): Result[C] = { - val first :: rest = frames : @unchecked - val result = first.asInstanceOf[Frame[A, B]](a) - if (rest.isEmpty) { - Impure(result, tail) - } else { - Impure(result, FramesCont(rest, tail)) - } - } - - final def append[D](s: MetaCont[C, D]): MetaCont[A, D] = FramesCont(frames, tail append s) - final def splitAt(c: Capability) = tail.splitAt(c) match { - case (head, tail) => (FramesCont(frames, head), tail) - } - final def lookup(key: AnyRef): Any = tail lookup key - final def update(key: AnyRef, value: Any) = tail.update(key, value) - override def flatMap[D](f: Frame[D, A]): MetaCont[D, C] = FramesCont(f.asInstanceOf[Frame[Any, Any]] :: frames, tail) - override def toString = s"fs(${frames.size}) :: ${tail}" - } - - // dynamically allocated state - // TODO change to mutable state, so that setting does not require the continuation - // otherwise the object lang. programs have to be written in CPS - private[control] case class StateCont[-A, +B](bindings: mutable.Map[AnyRef, Any], tail: MetaCont[A, B]) extends MetaCont[A, B] { - final def apply(a: A): Result[B] = tail(a) - final def append[C](s: MetaCont[B, C]): MetaCont[A, C] = StateCont(bindings.clone(), tail append s) - final def splitAt(c: Capability) = tail.splitAt(c) match { - case (head, tail) => (StateCont(bindings, head), tail) - } - final def lookup(searched: AnyRef): Any = - bindings.getOrElse(searched, tail lookup searched) - - final def update(key: AnyRef, value: Any) = - if (bindings.isDefinedAt(key)) { - bindings.update(key, value) - } else { - tail.update(key, value) - } - - // Also: Stateconts can be commuted if they don't shadow each other - final override def bind(key: AnyRef, value: Any): MetaCont[A, B] = - StateCont(bindings concat Map(key -> value), tail) - } - - private[effekt] case class HandlerCont[R, A](h: Capability { type Res = R }, tail: MetaCont[R, A]) extends MetaCont[R, A] { - final def apply(r: R): Result[A] = tail(r) - - final def append[C](s: MetaCont[A, C]): MetaCont[R, C] = HandlerCont(h, tail append s) - - final def splitAt(c: Capability) = - - if (h eq c) { - val head = CastCont[R, c.Res]() - val rest = tail.asInstanceOf[MetaCont[c.Res, A]] - (head, rest) - - } else tail.splitAt(c) match { - case (head, tail) => (HandlerCont(h, head), tail) - } - - final def lookup(key: AnyRef): Any = tail lookup key - final def update(key: AnyRef, value: Any) = tail.update(key, value) - - override def toString = s"${h} :: ${tail}" - } -} diff --git a/effekt/shared/src/main/scala/effekt/util/Debug.scala b/effekt/shared/src/main/scala/effekt/util/Debug.scala index 8bf699601..a9a44cb24 100644 --- a/effekt/shared/src/main/scala/effekt/util/Debug.scala +++ b/effekt/shared/src/main/scala/effekt/util/Debug.scala @@ -1,9 +1,6 @@ package effekt package util -import effekt.symbols.TypePrinter - - val showGeneric: PartialFunction[Any, String] = { case l: List[_] => l.map(show).mkString("List(", ", ", ")") @@ -13,10 +10,7 @@ val showGeneric: PartialFunction[Any, String] = { } val show: PartialFunction[Any, String] = - TypePrinter.show orElse core.PrettyPrinter.show orElse - generator.js.PrettyPrinter.show orElse - cps.PrettyPrinter.show orElse showGeneric inline def debug[A](inline value: A): A = diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index 4439e13e8..262c0cc78 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -1,7 +1,6 @@ package effekt package util -import effekt.source.{ NoSource, Tree } import kiama.util.{ Message, Messaging, Positions, Range, Severities } import kiama.util.Severities.* @@ -11,8 +10,6 @@ object messages { sealed trait EffektError extends Message case class ParseError(message: String, range: Option[Range]) extends EffektError { val severity = Error } - case class AmbiguousOverloadError(matches: List[(symbols.BlockSymbol, symbols.FunctionType)], range: Option[Range]) extends EffektError { val severity = Error } - case class FailedOverloadError(failedAttempts: List[(symbols.BlockSymbol, symbols.FunctionType, EffektMessages)], range: Option[Range]) extends EffektError { val severity = Error } case class PlainTextError(content: String, range: Option[Range], severity: Severity) extends EffektError case class StructuredError(content: StructuredMessage, range: Option[Range], severity: Severity) extends EffektError @@ -98,19 +95,17 @@ object messages { */ trait ErrorReporter { - var focus: Tree // the current focus of the compiler - - def currentRange: Option[Range] = rangeOf(focus) + def currentRange: Option[Range] = None val messaging: BufferedMessaging[EffektError] def positions: Positions // used to lookup positions of trees def plainMessage(text: String, severity: Severity): EffektError = - PlainTextError(text, rangeOf(focus), severity) + PlainTextError(text, None, severity) def structuredMessage(content: StructuredMessage, severity: Severity): EffektError = - StructuredError(content, rangeOf(focus), severity) + StructuredError(content, None, severity) def report(msg: EffektError): Unit = messaging.append(msg) @@ -140,25 +135,5 @@ object messages { reraise(msgs) abort(msg) } - - def at[T](t: Tree)(block: => T): T = { - val before = focus - focus = t; - val res = block; - focus = before; - res - } - - def withFocus[T <: Tree, R](block: T => R): T => R = { t => - at(t) { block(t) } - } - - /** - * Sets the given tree into focus for error reporting - */ - def focusing[T <: Tree, R](t: T)(f: T => R): R = - at(t) { f(t) } - - def rangeOf(t: Tree): Option[Range] = positions.getRange(t) } } diff --git a/effekt/shared/src/main/scala/effekt/util/Source.scala b/effekt/shared/src/main/scala/effekt/util/Source.scala index 7c02fee95..9181b9e0b 100644 --- a/effekt/shared/src/main/scala/effekt/util/Source.scala +++ b/effekt/shared/src/main/scala/effekt/util/Source.scala @@ -1,8 +1,6 @@ package effekt package util -import effekt.source.ModuleDecl - import kiama.util.Source /** @@ -34,23 +32,3 @@ case class MarkdownSource(source: Source) extends Source { lines.mkString("\n") } } - -/** - * Use by the REPL with synthesized modules. "input" contains the last REPL entry, - * not the whole source. - */ -case class VirtualSource(virtualModule: ModuleDecl, input: Source) extends Source { - val name = input.name - val content = input.content - - - // For purposes of caching, we have to use object identity to compare the virtual modules. - // Later compiler phases also use object identity to attach symbols etc. Not using - // object identity, will result in problems as observed in #77 - override def equals(obj: Any): Boolean = obj match { - case VirtualSource(otherModule, otherInput) => (otherModule eq virtualModule) && (otherInput == input) - case _ => false - } - - override def hashCode(): Int = super.hashCode() + System.identityHashCode(virtualModule) -} diff --git a/effekt/shared/src/main/scala/effekt/util/package.scala b/effekt/shared/src/main/scala/effekt/util/package.scala index a9915c061..7e15586da 100644 --- a/effekt/shared/src/main/scala/effekt/util/package.scala +++ b/effekt/shared/src/main/scala/effekt/util/package.scala @@ -18,4 +18,8 @@ extension(ch: Char) def escape: String = ch match { case '\\' => "\\\\" case ch if ch.toInt >= 32 && ch.toInt <= 126 => String.valueOf(ch) case ch => "\\u%04x".format(ch.toInt) +} + +implicit class ErrorMessageInterpolator(private val sc: StringContext) extends AnyVal { + def pp(args: Any*): String = sc.s(args.map(effekt.util.show): _*) } \ No newline at end of file