diff --git a/.github/actions/run-effekt-tests/action.yml b/.github/actions/run-effekt-tests/action.yml
index 0eff5407c..5fa78ad63 100644
--- a/.github/actions/run-effekt-tests/action.yml
+++ b/.github/actions/run-effekt-tests/action.yml
@@ -34,12 +34,12 @@ runs:
timeout_minutes: ${{ inputs.retry-timeout }}
max_attempts: ${{ inputs.retry-max-attempts }}
retry_on: error
- command: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt -J-Xss1G clean test
+ command: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt clean test
new_command_on_retry: sbt testQuick
- name: Run full test suite without retry
if: ${{ inputs.full-test == 'true' && runner.os != 'Windows' && inputs.use-retry != 'true' }}
- run: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt -J-Xss1G clean test
+ run: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt clean test
shell: bash
- name: Assemble fully optimized js file
diff --git a/.github/actions/setup-effekt/action.yml b/.github/actions/setup-effekt/action.yml
index 5a0dfee26..2730c730b 100644
--- a/.github/actions/setup-effekt/action.yml
+++ b/.github/actions/setup-effekt/action.yml
@@ -13,7 +13,7 @@ inputs:
llvm-version:
description: 'LLVM version to install'
required: false
- default: '15'
+ default: '18'
install-dependencies:
description: 'Whether to install system dependencies (Linux only)'
required: false
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 61dc64947..087605f49 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,5 +1,8 @@
name: Release Artifacts
+permissions:
+ id-token: write
+
on:
push:
tags:
diff --git a/.github/workflows/manual-npm-publish.yml b/.github/workflows/manual-npm-publish.yml
new file mode 100644
index 000000000..f636c815d
--- /dev/null
+++ b/.github/workflows/manual-npm-publish.yml
@@ -0,0 +1,69 @@
+# Do not use this, please. This is just for manual NPM publishing in a time of great need.
+name: Publish to NPM manually
+
+permissions:
+ id-token: write
+
+on:
+ workflow_dispatch: # For manual triggering
+
+jobs:
+ build-jar:
+ name: Build and assemble the Effekt compiler
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.get_version.outputs.VERSION }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: 'true'
+
+ - uses: ./.github/actions/setup-effekt
+
+ - name: Get the version
+ id: get_version
+ run: |
+ git fetch --tags
+ LATEST_TAG=$(git describe --tags --abbrev=0)
+ echo "VERSION=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
+
+ - name: Assemble jar file
+ run: sbt clean deploy
+
+ - name: Generate npm package
+ run: mv $(npm pack) effekt.tgz
+
+ - name: Upload Effekt binary
+ uses: actions/upload-artifact@v4
+ with:
+ name: effekt
+ path: bin/effekt
+
+ - name: Upload the npm package
+ uses: actions/upload-artifact@v4
+ with:
+ name: effekt-npm-package
+ path: effekt.tgz
+
+ publish-npm:
+ name: Publish NPM Package
+ runs-on: ubuntu-latest
+ needs: [build-jar]
+ steps:
+ - name: Download npm package
+ uses: actions/download-artifact@v4
+ with:
+ name: effekt-npm-package
+
+ - name: Set up NodeJS ${{ env.NODE_VERSION }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Publish to NPM as @effekt-lang/effekt
+ run: npm publish effekt.tgz --provenance --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/effekt/js/src/main/scala/effekt/LanguageServer.scala b/effekt/js/src/main/scala/effekt/LanguageServer.scala
index c724f6157..6c62143f2 100644
--- a/effekt/js/src/main/scala/effekt/LanguageServer.scala
+++ b/effekt/js/src/main/scala/effekt/LanguageServer.scala
@@ -98,18 +98,12 @@ class LanguageServer extends Intelligence {
file(path).lastModified
@JSExport
- def compileFile(path: String): String = {
- val mainOutputPath = compileCached(VirtualFileSource(path)).getOrElseAborting {
- throw js.JavaScriptException(s"Cannot compile ${path}")
+ 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)
}
- try {
- mainOutputPath
- } catch {
- case FatalPhaseError(msg) =>
- val formattedError = context.messaging.formatMessage(msg)
- throw js.JavaScriptException(formattedError)
- }
- }
@JSExport
def showCore(path: String): String = {
diff --git a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala
index 9fba7dd6d..4f68a3547 100644
--- a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala
+++ b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala
@@ -68,8 +68,8 @@ class EffektConfig(args: Seq[String]) extends REPLConfig(args.takeWhile(_ != "--
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 15)",
- default = Some(sys.env.getOrElse("EFFEKT_LLVM_VERSION", "15")),
+ 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
)
diff --git a/effekt/jvm/src/main/scala/effekt/Main.scala b/effekt/jvm/src/main/scala/effekt/Main.scala
index 5d8578e8d..d600889d9 100644
--- a/effekt/jvm/src/main/scala/effekt/Main.scala
+++ b/effekt/jvm/src/main/scala/effekt/Main.scala
@@ -17,14 +17,19 @@ object Main {
parseArgs(args)
} catch {
case e: ScallopException =>
- System.err.println(e.getMessage())
+ System.err.println(e.getMessage)
return
}
if (config.server()) {
- Server.launch(config)
+ val serverConfig = ServerConfig(
+ debug = config.debug(),
+ debugPort = config.debugPort()
+ )
+ val server = new Server(config)
+ server.launch(serverConfig)
} else if (config.repl()) {
- new Repl(Server).run(config)
+ new Repl(new Driver {}).run(config)
} else {
compileFiles(config)
}
@@ -43,8 +48,9 @@ object Main {
* Compile files specified in the configuration.
*/
private def compileFiles(config: EffektConfig): Unit = {
+ val driver = new Driver {}
for (filename <- config.filenames()) {
- Server.compileFile(filename, config)
+ driver.compileFile(filename, config)
}
}
}
diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala
index 33afd4d0a..994214b48 100644
--- a/effekt/jvm/src/main/scala/effekt/Runner.scala
+++ b/effekt/jvm/src/main/scala/effekt/Runner.scala
@@ -267,8 +267,8 @@ object LLVMRunner extends Runner[String] {
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-15", "llc-16"), List("--version"))
- lazy val optCmd = discoverExecutable(List("opt", "opt-15", "opt-16"), 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.") }
diff --git a/effekt/jvm/src/main/scala/effekt/Server.scala b/effekt/jvm/src/main/scala/effekt/Server.scala
index 3678ba8bc..17705f7be 100644
--- a/effekt/jvm/src/main/scala/effekt/Server.scala
+++ b/effekt/jvm/src/main/scala/effekt/Server.scala
@@ -1,60 +1,149 @@
package effekt
+import com.google.gson.JsonElement
+import kiama.util.Convert.*
import effekt.context.Context
-import effekt.core.PrettyPrinter
-import effekt.source.{ FunDef, Hole, ModuleDecl, Tree }
-import effekt.util.{ PlainMessaging, getOrElseAborting }
+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.{ Filenames, Notebook, NotebookCell, Position, Services, Source, Range }
-import kiama.output.PrettyPrinterTypes.Document
-import org.eclipse.lsp4j.{ Diagnostic, DocumentSymbol, ExecuteCommandParams, SymbolKind }
+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.Intelligence <--- gathers information -- LSPServer --- provides LSP interface ---> kiama.Server
- * |
- * v
- * effekt.Compiler
+ * 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.
*/
-trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with Driver with Intelligence {
+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
- import effekt.symbols._
+ // Track whether shutdown has been requested
+ private var shutdownRequested: Boolean = false
+ // Configuration sent by the language client
+ var settings: JsonElement = null
- import org.eclipse.lsp4j.{ Location, Range => LSPRange }
+ val getDriver: Driver = this
+ val getConfig: EffektConfig = config
- val name = "effekt"
-
- // Diagnostics
object lspMessaging extends PlainMessaging
- override def messageToDiagnostic(message: EffektError): Diagnostic =
- diagnostic(message.range, lspMessaging.formatContent(message), message.severity)
+ // 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 getDefinition(position: Position): Option[Tree] =
- getDefinitionAt(position)(using context)
+ override def shutdown(): CompletableFuture[Object] = {
+ shutdownRequested = true
+ CompletableFuture.completedFuture(null)
+ }
- /**
- * Overriding hook to also publish core and target for LSP server
- */
- override def afterCompilation(source: Source, config: EffektConfig)(using C: Context): Unit = try {
- super.afterCompilation(source, config)
+ override def exit(): Unit = {
+ System.exit(if (shutdownRequested) 0 else 1)
+ }
- // don't do anything, if we aren't running as a language server
- if (!C.config.server()) return ;
+ override def setTrace(params: SetTraceParams): Unit = {
+ // Do nothing
+ }
- val showIR = settingStr("showIR")
- val showTree = settingBool("showTree")
+ // 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
- def publishTree(name: String, tree: Any): Unit =
- publishProduct(source, name, "scala", util.PrettyPrinter.format(tree))
+ // LSP Diagnostics
+ //
+ //
- def publishIR(name: String, doc: Document): Unit =
- publishProduct(source, name, "ir", doc)
+ 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)
+ }
- if (showIR == "none") { return; }
+ // 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).getOrElseAborting { return; }
- publishTree("source", tree)
+ val tree = C.compiler.getAST(source)
+ if (tree.isEmpty) return
+ client.publishIR(EffektPublishIRParams(
+ basename(source.name) + ".scala",
+ PrettyPrinter.format(tree.get).layout
+ ))
return;
}
@@ -69,18 +158,123 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
}
if (showTree) {
- publishTree(showIR, C.compiler.treeIR(source, stage).getOrElse(unsupported))
+ client.publishIR(EffektPublishIRParams(
+ basename(source.name) + ".scala",
+ PrettyPrinter.format(C.compiler.treeIR(source, stage).getOrElse(unsupported)).layout
+ ))
} else if (showIR == "target") {
- publishProduct(source, "target", C.runner.extension, C.compiler.prettyIR(source, Stage.Target).getOrElse(unsupported))
+ client.publishIR(EffektPublishIRParams(
+ basename(source.name) + "." + C.runner.extension,
+ C.compiler.prettyIR(source, Stage.Target).getOrElse(unsupported).layout
+ ))
} else {
- publishIR(showIR, C.compiler.prettyIR(source, stage).getOrElse(unsupported))
+ 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
}
- } catch {
- case e => logMessage(e.toString + ":" + e.getMessage)
+ clearDiagnostics(document.getUri)
+ getDriver.compileString(document.getUri, text, getConfig)
}
- override def getHover(position: Position): Option[String] =
- getSymbolHover(position) orElse getHoleHover(position)
+ // 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)
@@ -93,56 +287,39 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
info <- getHoleInfo(tree)(using context)
} yield info
- def positionToLocation(p: Position): Location = {
- val s = convertPosition(Some(p))
- new Location(p.source.name, new LSPRange(s, s))
- }
+ // LSP Document Symbols
+ //
+ //
- def getSourceTreeFor(sym: 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)
- }
+ 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()))
- override def getSymbols(source: Source): Option[Vector[DocumentSymbol]] =
-
- context.compiler.runFrontend(source)(using context)
+ context.compiler.runFrontend(source.get)(using context)
val documentSymbols = for {
- sym <- context.sourceSymbolsFor(source).toVector
+ sym <- context.sourceSymbolsFor(source.get).toVector
if !sym.isSynthetic
id <- context.definitionTreeOption(sym)
decl <- getSourceTreeFor(sym)
kind <- getSymbolKind(sym)
detail <- getInfoOf(sym)(using context)
- } yield new DocumentSymbol(sym.name.name, kind, rangeOfNode(decl), rangeOfNode(id), detail.header)
- Some(documentSymbols)
-
- override def getReferences(position: Position, includeDecl: Boolean): Option[Vector[Tree]] =
- for {
- (tree, sym) <- getSymbolAt(position)(using context)
- refs = context.distinctReferencesTo(sym)
- allRefs = if (includeDecl) tree :: refs else refs
- } yield allRefs.toVector
-
- override def getInlayHints(range: kiama.util.Range): Option[Vector[InlayHint]] =
- val captures = getInferredCaptures(range)(using context).map {
- case (p, c) =>
- val prettyCaptures = TypePrinter.show(c)
- InlayHint(InlayHintKind.Type, p, prettyCaptures, markdownTooltip = s"captures: `${prettyCaptures}`", paddingRight = true, effektKind = "capture")
- }.toVector
-
- if captures.isEmpty then None else Some(captures)
-
- // settings might be null
- override def setSettings(settings: Object): Unit = {
- import com.google.gson.JsonObject
- if (settings == null) super.setSettings(new JsonObject())
- else super.setSettings(settings)
+ 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)
}
- //references
+ 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 {
@@ -160,25 +337,121 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
None
}
- override def getCodeActions(position: Position): Option[Vector[TreeAction]] =
- Some(for {
- trees <- getTreesAt(position)(using context).toVector
- actions <- trees.flatMap { t => action(t)(using context) }
- } yield actions)
+ // 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())))
+ }
- def action(tree: Tree)(using C: Context): Option[TreeAction] = tree match {
+ // 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
+ case h: Hole => closeHoleAction(h)
+ case _ => None
}
- def CodeAction(description: String, oldNode: Any, newText: String): Option[TreeAction] =
+ def EffektCodeAction(description: String, oldNode: Any, newText: String): Option[CodeAction] = {
for {
posFrom <- positions.getStart(oldNode)
posTo <- positions.getFinish(oldNode)
- } yield TreeAction(description, posFrom.source.name, posFrom, posTo, newText)
+ } 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).
@@ -186,7 +459,7 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
* 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[TreeAction] = for {
+ def inferEffectsAction(fun: FunDef)(using C: Context): Option[CodeAction] = for {
// the inferred type
(tpe, eff) <- C.inferredTypeAndEffectOption(fun)
// the annotated type
@@ -194,24 +467,26 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
result <- fun.symbol.annotatedResult
effects <- fun.symbol.annotatedEffects
} yield (result, effects)
- if ann.map { needsUpdate(_, (tpe, eff)) }.getOrElse(true)
- res <- CodeAction("Update return type with inferred effects", fun.ret, s": $tpe / $eff")
+ 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[TreeAction] = for {
+ 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 <- CodeAction("Close hole", hole, text)
+ res <- EffektCodeAction("Close hole", hole, text)
} yield res
// <{ s1 ; s2; ... }>
case Hole(stmts) => for {
text <- positions.textOf(stmts)
- res <- CodeAction("Close hole", hole, s"locally { ${text} }")
+ res <- EffektCodeAction("Close hole", hole, s"locally { ${text} }")
} yield res
}
} yield res
@@ -222,13 +497,97 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
tpe1 != tpe2 || effs1 != effs2
}
- override def createServices(config: EffektConfig) = new LSPServices(this, config)
+ // 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)
- // Class to easily test custom LSP services not (yet) meant to go into kiama.Services
- class LSPServices(server: LSPServer, config: EffektConfig) extends Services[Tree, EffektConfig, EffektError](server, config) {}
+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
}
/**
- * Singleton for the language server
+ * Custom LSP notification to publish Effekt IR
+ *
+ * @param filename The filename of the resulting IR file
+ * @param content The IR content
*/
-object Server extends LSPServer
+case class EffektPublishIRParams(filename: String,
+ content: String
+)
diff --git a/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala b/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala
index 3d7e3f89c..e73ada83f 100644
--- a/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala
+++ b/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala
@@ -44,7 +44,7 @@ object paths extends PathUtils {
implicit def file(uri: URI): File = file(uri.getPath)
implicit def file(filepath: String) =
if (filepath.startsWith("file:"))
- file(new URI(filepath).getPath)
+ file(new URI(filepath))
else
file(new JFile(filepath))
}
diff --git a/effekt/jvm/src/test/scala/effekt/EffektTests.scala b/effekt/jvm/src/test/scala/effekt/EffektTests.scala
index ee3eabbe1..aaaa014d8 100644
--- a/effekt/jvm/src/test/scala/effekt/EffektTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/EffektTests.scala
@@ -36,9 +36,12 @@ trait EffektTests extends munit.FunSuite {
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), expected)
+ assertNoDiff(run(input, true), expected)
}
// one shared driver for all tests in this test runner
@@ -59,7 +62,7 @@ trait EffektTests extends munit.FunSuite {
compiler.compileFile(input.getPath, configs)
compiler.context.backup
- def run(input: File): String =
+ def run(input: File, optimizations: Boolean): String =
val compiler = driver
var options = Seq(
"--Koutput", "string",
@@ -68,6 +71,7 @@ trait EffektTests extends munit.FunSuite {
)
if (valgrind) options = options :+ "--valgrind"
if (debug) options = options :+ "--debug"
+ if (!optimizations) options = options :+ "--no-optimize"
val configs = compiler.createConfig(options)
configs.verify()
@@ -96,6 +100,16 @@ trait EffektTests extends munit.FunSuite {
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 =
@@ -103,7 +117,7 @@ trait EffektTests extends munit.FunSuite {
case (f, None) => sys error s"Missing checkfile for ${f.getPath}"
case (f, Some(expected)) =>
test(s"${f.getPath} (${backendName})") {
- assertNoDiff(run(f), expected)
+ assertNoDiff(run(f, true), expected)
}
}
@@ -111,7 +125,7 @@ trait EffektTests extends munit.FunSuite {
foreachFileIn(dir) {
case (f, Some(expected)) =>
test(s"${f.getPath} (${backendName})") {
- assertNoDiff(run(f), expected)
+ assertNoDiff(run(f, true), expected)
}
case (f, None) =>
diff --git a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala
index c3626213d..de90df3b4 100644
--- a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala
@@ -24,6 +24,16 @@ class JavaScriptTests extends EffektTests {
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"
@@ -58,7 +68,7 @@ object TestUtils {
val shouldGenerate = regenerateAll || f.lastModified() > checkfile.lastModified()
if (!isIgnored && shouldGenerate) {
println(s"Writing checkfile for ${f}")
- val out = run(f)
+ val out = run(f, true)
// Save checkfile in source folder (e.g. examples/)
// We remove ansi colors to make check files human-readable.
diff --git a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala
index e9e5fd18e..6a1c55b2d 100644
--- a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala
@@ -53,6 +53,27 @@ class LLVMTests extends EffektTests {
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)
}
diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
new file mode 100644
index 000000000..9e995c452
--- /dev/null
+++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
@@ -0,0 +1,921 @@
+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
index 5bfb2f798..44731886a 100644
--- a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala
@@ -125,6 +125,12 @@ class RecursiveDescentTests extends munit.FunSuite {
parseExpr("fun() { foo(()) }")
parseExpr("10.seconds")
+
+ parseExpr("[1,2,3]")
+ parseExpr("[3,2,1,]")
+ parseExpr("[]")
+ parseExpr("[,]")
+ intercept[Throwable] { parseExpr("[,1]") }
}
test("Boxing") {
diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala
index f7e92745b..b053c633f 100644
--- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala
@@ -12,13 +12,30 @@ abstract class StdlibTests extends EffektTests {
)
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
@@ -39,6 +56,10 @@ class StdlibLLVMTests extends StdlibTests {
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",
diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala
index 192652eda..7d8f89167 100644
--- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala
@@ -10,7 +10,7 @@ import effekt.PhaseResult.CoreTransformed
*/
trait CoreTests extends munit.FunSuite {
- protected def defaultNames = symbols.builtins.rootTypes ++ symbols.builtins.rootTerms ++ symbols.builtins.rootCaptures
+ protected def defaultNames: Map[String, _root_.effekt.symbols.Symbol] = symbols.builtins.rootTypes ++ symbols.builtins.rootCaptures
def shouldBeEqual(obtained: ModuleDecl, expected: ModuleDecl, clue: => Any)(using Location) =
assertEquals(obtained, expected, {
@@ -46,14 +46,14 @@ trait CoreTests extends munit.FunSuite {
expected: ModuleDecl,
clue: => Any = "values are not alpha-equivalent",
names: Names = Names(defaultNames))(using Location): Unit = {
- val renamer = Renamer(names, "$")
+ val renamer = TestRenamer(names, "$")
shouldBeEqual(renamer(obtained), renamer(expected), clue)
}
def assertAlphaEquivalentStatements(obtained: Stmt,
expected: Stmt,
clue: => Any = "values are not alpha-equivalent",
names: Names = Names(defaultNames))(using Location): Unit = {
- val renamer = Renamer(names, "$")
+ val renamer = TestRenamer(names, "$")
shouldBeEqual(renamer(obtained), renamer(expected), clue)
}
def parse(input: String,
diff --git a/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala b/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala
index fe5f2381a..f46836d99 100644
--- a/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala
@@ -22,7 +22,7 @@ class OptimizerTests extends CoreTests {
val pExpected = parse(moduleHeader + transformed, "expected", names)
// the parser is not assigning symbols correctly, so we need to run renamer first
- val renamed = Renamer(names).rewrite(pInput)
+ val renamed = TestRenamer(names).rewrite(pInput)
val obtained = transform(renamed)
assertAlphaEquivalent(obtained, pExpected, "Not transformed to")
diff --git a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala b/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala
index d7a5ce5fd..521c00940 100644
--- a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala
@@ -38,10 +38,10 @@ class PatternMatchingTests extends CoreTests {
List(
Condition.Patterns(Map(x -> Pattern.Any(y.id))),
Condition.Patterns(Map(y -> Pattern.Any(z.id)))),
- f, List(z)))
+ f, Nil, List(z)))
// case => f(z)
- val expected = Clause(Nil, f, List(x))
+ val expected = Clause(Nil, f, Nil, List(x))
assertEquals(normalized, expected)
}
@@ -73,11 +73,11 @@ class PatternMatchingTests extends CoreTests {
Condition.Patterns(Map(sc -> Pattern.Any(x.id))),
Condition.Val(p.id, TBoolean, trivalPredicate),
Condition.Predicate(p)),
- b1, List(x)),
+ b1, Nil, List(x)),
Clause(
List(
Condition.Patterns(Map(sc -> Pattern.Ignore()))),
- b2, List())))
+ b2, Nil, List())))
val expected =
Val(p.id, TBoolean, trivalPredicate,
@@ -124,14 +124,14 @@ class PatternMatchingTests extends CoreTests {
val result = compile(List(
Clause(
List(
- Condition.Patterns(Map(opt -> Pattern.Tag(SomeC, List(Pattern.Any(v.id) -> TInt)))),
+ Condition.Patterns(Map(opt -> Pattern.Tag(SomeC, List(), List(Pattern.Any(v.id) -> TInt)))),
Condition.Val(p.id, TBoolean, trivalPredicate),
Condition.Predicate(p)),
- b1, List(v)),
+ b1, Nil, List(v)),
Clause(
List(
Condition.Patterns(Map(opt -> Pattern.Ignore()))),
- b2, List())))
+ b2, Nil, List())))
// opt match {
// case Some(tmp) => val p = return v > 0; if (p) { b1(tmp) } else { b2() }
diff --git a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala
index f28ff5bbf..4b7538dab 100644
--- a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala
@@ -1,17 +1,75 @@
package effekt.core
-import effekt.symbols
+import scala.collection.mutable
+
+/**
+ * This is testing the main/core.Renamer using the test/core.TestRenamer.
+ */
class RenamerTests extends CoreTests {
- def assertRenamedTo(input: String,
- renamed: String,
- clue: => Any = "Not renamed to given value",
- names: Names = Names(defaultNames))(using munit.Location) = {
+ /**
+ * Check that the renamed input preserves alpha-equivalence using [[assertAlphaEquivalent]]
+ */
+ def assertRenamingPreservesAlpha(input: String,
+ clue: => Any = "Not renamed to given value",
+ names: Names = Names(defaultNames))(using munit.Location) = {
val pInput = parse(input, "input", names)
- val pExpected = parse(renamed, "expected", names)
- val renamer = new Renamer(names, "renamed") // use "renamed" as prefix so we can refer to it
+ val renamer = new Renamer(names, "renamed")
val obtained = renamer(pInput)
- shouldBeEqual(obtained, pExpected, clue)
+ assertAlphaEquivalent(obtained, pInput, clue)
+ }
+
+ def assertDefsUnique(in: ModuleDecl,
+ clue: => Any = "Duplicate definition") = {
+ val seen = mutable.HashSet.empty[Id]
+
+ def isFresh(id: Id): Unit = {
+ assert(!seen.contains(id), clue)
+ seen.add(id)
+ }
+
+ object check extends Tree.Query[Unit, Unit] {
+ override def empty = ()
+
+ override def combine = (_, _) => ()
+
+ override def visit[T](t: T)(visitor: Unit ?=> T => Unit)(using Unit): Unit = {
+ visitor(t)
+ t match {
+ case m: ModuleDecl =>
+ m.definitions.foreach { d => isFresh(d.id) }
+ case d: Def => isFresh(d.id)
+ case v: Val => isFresh(v.id)
+ case l: Let => isFresh(l.id)
+ case d: Declaration => isFresh(d.id)
+ case e: Extern.Def =>
+ isFresh(e.id)
+ e.tparams.foreach(isFresh);
+ e.vparams.foreach { p => isFresh(p.id) }
+ e.bparams.foreach { p => isFresh(p.id) };
+ e.cparams.foreach { p => isFresh(p) }
+ case b: BlockLit =>
+ b.tparams.foreach(isFresh);
+ b.cparams.foreach(isFresh)
+ b.vparams.foreach { p => isFresh(p.id) };
+ b.bparams.foreach { p => isFresh(p.id) }
+ case i: Implementation =>
+ i.operations.foreach { o =>
+ o.vparams.foreach { p => isFresh(p.id) }; o.bparams.foreach { p => isFresh(p.id) }
+ }
+ case _ => ()
+ }
+ }
+ }
+ check.query(in)(using ())
+ }
+ def assertRenamingMakesDefsUnique(input: String,
+ clue: => Any = "Duplicate definition",
+ names: Names = Names(defaultNames))(using munit.Location) = {
+ val pInput = parse(input, "input", names)
+ val renamer = new Renamer(names, "renamed")
+ val obtained = renamer(pInput)
+ assertDefsUnique(obtained, clue)
}
test("No bound local variables"){
@@ -22,11 +80,12 @@ class RenamerTests extends CoreTests {
| return (bar: (Int) => Int @ {})(baz:Int)
|}
|""".stripMargin
- assertRenamedTo(code, code)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("val binding"){
- val input =
+ val code =
"""module main
|
|def foo = { () =>
@@ -34,19 +93,12 @@ class RenamerTests extends CoreTests {
| return x:Int
|}
|""".stripMargin
- val expected =
- """module main
- |
- |def foo = { () =>
- | val renamed1 = (foo:(Int)=>Int@{})(4);
- | return renamed1:Int
- |}
- |""".stripMargin
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("var binding"){
- val input =
+ val code =
"""module main
|
|def foo = { () =>
@@ -54,37 +106,24 @@ class RenamerTests extends CoreTests {
| return x:Int
|}
|""".stripMargin
- val expected =
- """module main
- |
- |def foo = { () =>
- | var renamed1 @ global = (foo:(Int)=>Int@{})(4);
- | return renamed1:Int
- |}
- |""".stripMargin
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("function (value) parameters"){
- val input =
+ val code =
"""module main
|
|def foo = { (x:Int) =>
| return x:Int
|}
|""".stripMargin
- val expected =
- """module main
- |
- |def foo = { (renamed1:Int) =>
- | return renamed1:Int
- |}
- |""".stripMargin
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("match clauses"){
- val input =
+ val code =
"""module main
|
|type Data { X(a:Int, b:Int) }
@@ -94,39 +133,24 @@ class RenamerTests extends CoreTests {
| }
|}
|""".stripMargin
- val expected =
- """module main
- |
- |type Data { X(a:Int, b:Int) }
- |def foo = { () =>
- | 12 match {
- | X : {(renamed1:Int, renamed2:Int) => return renamed1:Int }
- | }
- |}
- |""".stripMargin
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("type parameters"){
- val input =
+ val code =
"""module main
|
|def foo = { ['A](a: A) =>
| return a:Identity[A]
|}
|""".stripMargin
- val expected =
- """module main
- |
- |def foo = { ['renamed1](renamed2: renamed1) =>
- | return renamed2:Identity[renamed1]
- |}
- |""".stripMargin
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("pseudo recursive"){
- val input =
+ val code =
""" module main
|
| def bar = { () => return 1 }
@@ -136,22 +160,11 @@ class RenamerTests extends CoreTests {
| (foo : () => Unit @ {})()
| }
|""".stripMargin
-
- val expected =
- """ module main
- |
- | def bar = { () => return 1 }
- | def main = { () =>
- | def renamed1 = { () => (bar : () => Unit @ {})() }
- | def renamed2 = { () => return 2 }
- | (renamed1 : () => Unit @ {})()
- | }
- |""".stripMargin
-
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
}
test("shadowing let bindings"){
- val input =
+ val code =
""" module main
|
| def main = { () =>
@@ -160,17 +173,26 @@ class RenamerTests extends CoreTests {
| return x:Int
| }
|""".stripMargin
-
- val expected =
+ assertRenamingPreservesAlpha(code)
+ assertRenamingMakesDefsUnique(code)
+ }
+ test("shadowing let bindings inside a def") {
+ val code =
""" module main
|
| def main = { () =>
- | let renamed1 = 1
- | let renamed2 = 2
- | return renamed2:Int
+ | def foo = { () =>
+ | let x = 1
+ | return x: Int
+ | }
+ | let x = 2
+ | def bar = { () =>
+ | let x = 3
+ | return x: Int
+ | }
+ | return x:Int
| }
|""".stripMargin
-
- assertRenamedTo(input, expected)
+ assertRenamingPreservesAlpha(code)
}
}
diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala
new file mode 100644
index 000000000..b32fe7262
--- /dev/null
+++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala
@@ -0,0 +1,112 @@
+package effekt.core
+
+import effekt.{ core, symbols }
+import effekt.context.Context
+
+/**
+ * Freshens bound names in a given term for tests.
+ * Please use this _only_ for tests. Otherwise, prefer [[effekt.core.Renamer]].
+ *
+ * @param names used to look up a reference by name to resolve to the same symbols.
+ * This is only used by tests to deterministically rename terms and check for
+ * alpha-equivalence.
+ * @param prefix if the prefix is empty, the original name will be used as a prefix
+ *
+ * @param C the context is used to copy annotations from old symbols to fresh symbols
+ */
+class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends core.Tree.Rewrite {
+
+ // list of scopes that map bound symbols to their renamed variants.
+ private var scopes: List[Map[Id, Id]] = List.empty
+
+ // Here we track ALL renamings
+ var renamed: Map[Id, Id] = Map.empty
+
+ private var suffix: Int = 0
+
+ def freshIdFor(id: Id): Id =
+ suffix = suffix + 1
+ val uniqueName = if prefix.isEmpty then id.name.name + "_" + suffix.toString else prefix + suffix.toString
+ names.idFor(uniqueName)
+
+ def withBindings[R](ids: List[Id])(f: => R): R =
+ val before = scopes
+ try {
+ val newScope = ids.map { x => x -> freshIdFor(x) }.toMap
+ scopes = newScope :: scopes
+ renamed = renamed ++ newScope
+ f
+ } finally { scopes = before }
+
+ /** Alias for withBindings(List(id)){...} */
+ def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f)
+
+ // free variables are left untouched
+ override def id: PartialFunction[core.Id, core.Id] = {
+ case id => scopes.collectFirst {
+ case bnds if bnds.contains(id) => bnds(id)
+ }.getOrElse(id)
+ }
+
+ override def stmt: PartialFunction[Stmt, Stmt] = {
+ case core.Def(id, block, body) =>
+ // can be recursive
+ withBinding(id) { core.Def(rewrite(id), rewrite(block), rewrite(body)) }
+
+ case core.Let(id, tpe, binding, body) =>
+ val resolvedBinding = rewrite(binding)
+ withBinding(id) { core.Let(rewrite(id), rewrite(tpe), resolvedBinding, rewrite(body)) }
+
+ case core.Val(id, tpe, binding, body) =>
+ val resolvedBinding = rewrite(binding)
+ withBinding(id) { core.Val(rewrite(id), rewrite(tpe), resolvedBinding, rewrite(body)) }
+
+ case core.Alloc(id, init, reg, body) =>
+ val resolvedInit = rewrite(init)
+ val resolvedReg = rewrite(reg)
+ withBinding(id) { core.Alloc(rewrite(id), resolvedInit, resolvedReg, rewrite(body)) }
+
+ case core.Var(ref, init, capt, body) =>
+ val resolvedInit = rewrite(init)
+ val resolvedCapt = rewrite(capt)
+ withBinding(ref) { core.Var(rewrite(ref), resolvedInit, resolvedCapt, rewrite(body)) }
+
+ case core.Get(id, tpe, ref, capt, body) =>
+ val resolvedRef = rewrite(ref)
+ val resolvedCapt = rewrite(capt)
+ withBinding(id) { core.Get(rewrite(id), rewrite(tpe), resolvedRef, resolvedCapt, rewrite(body)) }
+
+ }
+
+ override def block: PartialFunction[Block, Block] = {
+ case Block.BlockLit(tparams, cparams, vparams, bparams, body) =>
+ withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) {
+ Block.BlockLit(tparams map rewrite, cparams map rewrite, vparams map rewrite, bparams map rewrite,
+ rewrite(body))
+ }
+ }
+
+ override def rewrite(o: Operation): Operation = o match {
+ case Operation(name, tparams, cparams, vparams, bparams, body) =>
+ withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) {
+ Operation(name,
+ tparams map rewrite,
+ cparams map rewrite,
+ vparams map rewrite,
+ bparams map rewrite,
+ rewrite(body))
+ }
+ }
+
+ def apply(m: core.ModuleDecl): core.ModuleDecl =
+ suffix = 0
+ m match {
+ case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) =>
+ core.ModuleDecl(path, includes, declarations, externs, definitions map rewrite, exports)
+ }
+
+ def apply(s: Stmt): Stmt = {
+ suffix = 0
+ rewrite(s)
+ }
+}
\ No newline at end of file
diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala
new file mode 100644
index 000000000..4f1a95113
--- /dev/null
+++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala
@@ -0,0 +1,179 @@
+package effekt.core
+
+/**
+ * This is testing the test/core.TestRenamer, not the main/core.Renamer :)
+ * These tests are important, because we use TestRenamer for deciding test-friendly alpha-equivalence in CoreTests.
+ */
+class TestRenamerTests extends CoreTests {
+
+ def assertRenamedTo(input: String,
+ renamed: String,
+ clue: => Any = "Not renamed to given value",
+ names: Names = Names(defaultNames))(using munit.Location) = {
+ val pInput = parse(input, "input", names)
+ val pExpected = parse(renamed, "expected", names)
+ val renamer = new TestRenamer(names, "renamed") // use "renamed" as prefix so we can refer to it
+ val obtained = renamer(pInput)
+ shouldBeEqual(obtained, pExpected, clue)
+ }
+
+ test("No bound local variables"){
+ val code =
+ """module main
+ |
+ |def foo = { () =>
+ | return (bar: (Int) => Int @ {})(baz:Int)
+ |}
+ |""".stripMargin
+ assertRenamedTo(code, code)
+ }
+
+ test("val binding"){
+ val input =
+ """module main
+ |
+ |def foo = { () =>
+ | val x = (foo:(Int)=>Int@{})(4) ;
+ | return x:Int
+ |}
+ |""".stripMargin
+ val expected =
+ """module main
+ |
+ |def foo = { () =>
+ | val renamed1 = (foo:(Int)=>Int@{})(4);
+ | return renamed1:Int
+ |}
+ |""".stripMargin
+ assertRenamedTo(input, expected)
+ }
+
+ test("var binding"){
+ val input =
+ """module main
+ |
+ |def foo = { () =>
+ | var x @ global = (foo:(Int)=>Int@{})(4) ;
+ | return x:Int
+ |}
+ |""".stripMargin
+ val expected =
+ """module main
+ |
+ |def foo = { () =>
+ | var renamed1 @ global = (foo:(Int)=>Int@{})(4);
+ | return renamed1:Int
+ |}
+ |""".stripMargin
+ assertRenamedTo(input, expected)
+ }
+
+ test("function (value) parameters"){
+ val input =
+ """module main
+ |
+ |def foo = { (x:Int) =>
+ | return x:Int
+ |}
+ |""".stripMargin
+ val expected =
+ """module main
+ |
+ |def foo = { (renamed1:Int) =>
+ | return renamed1:Int
+ |}
+ |""".stripMargin
+ assertRenamedTo(input, expected)
+ }
+
+ test("match clauses"){
+ val input =
+ """module main
+ |
+ |type Data { X(a:Int, b:Int) }
+ |def foo = { () =>
+ | 12 match {
+ | X : {(aa:Int, bb:Int) => return aa:Int }
+ | }
+ |}
+ |""".stripMargin
+ val expected =
+ """module main
+ |
+ |type Data { X(a:Int, b:Int) }
+ |def foo = { () =>
+ | 12 match {
+ | X : {(renamed1:Int, renamed2:Int) => return renamed1:Int }
+ | }
+ |}
+ |""".stripMargin
+ assertRenamedTo(input, expected)
+ }
+
+ test("type parameters"){
+ val input =
+ """module main
+ |
+ |def foo = { ['A](a: A) =>
+ | return a:Identity[A]
+ |}
+ |""".stripMargin
+ val expected =
+ """module main
+ |
+ |def foo = { ['renamed1](renamed2: renamed1) =>
+ | return renamed2:Identity[renamed1]
+ |}
+ |""".stripMargin
+ assertRenamedTo(input, expected)
+ }
+
+ test("pseudo recursive"){
+ val input =
+ """ module main
+ |
+ | def bar = { () => return 1 }
+ | def main = { () =>
+ | def foo = { () => (bar : () => Unit @ {})() }
+ | def bar = { () => return 2 }
+ | (foo : () => Unit @ {})()
+ | }
+ |""".stripMargin
+
+ val expected =
+ """ module main
+ |
+ | def bar = { () => return 1 }
+ | def main = { () =>
+ | def renamed1 = { () => (bar : () => Unit @ {})() }
+ | def renamed2 = { () => return 2 }
+ | (renamed1 : () => Unit @ {})()
+ | }
+ |""".stripMargin
+
+ assertRenamedTo(input, expected)
+ }
+ test("shadowing let bindings"){
+ val input =
+ """ module main
+ |
+ | def main = { () =>
+ | let x = 1
+ | let x = 2
+ | return x:Int
+ | }
+ |""".stripMargin
+
+ val expected =
+ """ module main
+ |
+ | def main = { () =>
+ | let renamed1 = 1
+ | let renamed2 = 2
+ | return renamed2:Int
+ | }
+ |""".stripMargin
+
+ assertRenamedTo(input, expected)
+ }
+}
diff --git a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala
index 575ee288e..0b6a26a9b 100644
--- a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala
@@ -357,8 +357,8 @@ class VMTests extends munit.FunSuite {
poppedFrames = 8661,
allocations = 0,
closures = 0,
- variableReads = 32437,
- variableWrites = 13699,
+ variableReads = 23776,
+ variableWrites = 5039,
resets = 0,
shifts = 0,
resumes = 0
@@ -373,8 +373,8 @@ class VMTests extends munit.FunSuite {
poppedFrames = 5462,
allocations = 5461,
closures = 0,
- variableReads = 13654,
- variableWrites = 9557,
+ variableReads = 8192,
+ variableWrites = 4096,
resets = 0,
shifts = 0,
resumes = 0
diff --git a/effekt/shared/src/main/scala/effekt/Lexer.scala b/effekt/shared/src/main/scala/effekt/Lexer.scala
index 53f52c991..da98747c6 100644
--- a/effekt/shared/src/main/scala/effekt/Lexer.scala
+++ b/effekt/shared/src/main/scala/effekt/Lexer.scala
@@ -340,7 +340,10 @@ class Lexer(source: Source) {
case None => err("Not a 64bit floating point literal.")
case Some(n) => TokenKind.Float(n)
}
- case _ => TokenKind.Integer(slice().toInt)
+ case _ => slice().toLongOption match {
+ case None => err("Not a 64bit integer literal.")
+ case Some(n) => TokenKind.Integer(n)
+ }
}
}
case _ =>
diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala
index 4aabfe03b..40420a814 100644
--- a/effekt/shared/src/main/scala/effekt/Namer.scala
+++ b/effekt/shared/src/main/scala/effekt/Namer.scala
@@ -13,6 +13,9 @@ 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
*
@@ -39,6 +42,24 @@ object Namer extends Phase[Parsed, NameResolved] {
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)
@@ -72,7 +93,8 @@ object Namer extends Phase[Parsed, NameResolved] {
// process all includes, updating the terms and types in scope
val includes = decl.includes collect {
case im @ source.Include(path) =>
- val mod = Context.at(im) { importDependency(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
}
@@ -633,6 +655,8 @@ object Namer extends Phase[Parsed, NameResolved] {
}
}
patterns.flatMap { resolve }
+ case source.MultiPattern(patterns) =>
+ patterns.flatMap { resolve }
}
def resolve(p: source.MatchGuard)(using Context): Unit = p match {
@@ -880,7 +904,10 @@ trait NamerOps extends ContextOps { Context: Context =>
val syms2 = if (syms.isEmpty) scope.lookupOperation(id.path, id.name) else syms
- if (syms2.nonEmpty) { assignSymbol(id, CallTarget(syms2.asInstanceOf)); true } else { false }
+ // 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 }
}
/**
@@ -894,14 +921,14 @@ trait NamerOps extends ContextOps { Context: Context =>
assignSymbol(id, value)
case Right(blocks) =>
if (blocks.isEmpty) {
- val allSyms = scope.lookupOverloaded(id, term => true).flatten
-
- if (allSyms.exists { case o: Operation => true; case _ => false })
- info(pretty"There is an equally named effect operation. Use syntax `do ${id}() to call it.`")
+ val ops = scope.lookupOperation(id.path, id.name).flatten
- if (allSyms.exists { case o: Field => true; case _ => false })
- info(pretty"There is an equally named field. Use syntax `obj.${id} to access it.`")
+ // 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))
@@ -918,6 +945,7 @@ trait NamerOps extends ContextOps { Context: Context =>
* 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.
diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
index 9db4032b3..c02e8ff35 100644
--- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
+++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
@@ -406,7 +406,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
val default = when(`else`) { Some(stmt()) } { None }
val body = semi() ~> stmts()
val clause = MatchClause(p, guards, body).withRangeOf(p, sc)
- val matching = Match(sc, List(clause), default).withRangeOf(startMarker, sc)
+ val matching = Match(List(sc), List(clause), default).withRangeOf(startMarker, sc)
Return(matching)
}
@@ -757,8 +757,13 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
def matchClause(): MatchClause =
nonterminal:
+ val patterns = `case` ~> some(matchPattern, `,`)
+ val pattern = patterns match {
+ case List(pat) => pat
+ case pats => MultiPattern(pats)
+ }
MatchClause(
- `case` ~> matchPattern(),
+ 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`
@@ -802,7 +807,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
while (peek(`match`)) {
val clauses = `match` ~> braces { manyWhile(matchClause(), `case`) }
val default = when(`else`) { Some(stmt()) } { None }
- sc = Match(sc, clauses, default)
+ sc = Match(List(sc), clauses, default)
}
sc
@@ -944,14 +949,18 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
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 name = "__tmpRes"
+ val names = List.tabulate(arity){ n => s"__arg${n}" }
BlockLiteral(
Nil,
- List(ValueParam(IdDef(name), None)),
+ names.map{ name => ValueParam(IdDef(name), None) },
Nil,
- Return(Match(Var(IdRef(Nil, name)), cs, None))) : BlockLiteral
+ Return(Match(names.map{ name => Var(IdRef(Nil, name)) }, cs, None))) : BlockLiteral
}
case _ =>
// { (x: Int) => ... }
@@ -993,7 +1002,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
}
def listLiteral(): Term =
nonterminal:
- many(expr, `[`, `,`, `]`).foldRight(NilTree) { ConsTree }
+ manyTrailing(expr, `[`, `,`, `]`).foldRight(NilTree) { ConsTree }
private def NilTree: Term =
Call(IdTarget(IdRef(List(), "Nil")), Nil, Nil, Nil)
@@ -1030,10 +1039,9 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
def templateString(): Term =
nonterminal:
backtrack(idRef()) ~ template() match {
- // We do not need to apply any transformation if there are no splices
- case _ ~ Template(str :: Nil, Nil) => StringLit(str)
- case _ ~ Template(strs, Nil) => fail("Cannot occur")
- // s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y) }
+ // 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 =>
@@ -1407,6 +1415,29 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
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
@@ -1431,8 +1462,36 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
// case _ => ()
// }
- positions.setStart(res, source.offsetToPosition(start))
- positions.setFinish(res, source.offsetToPosition(end))
+ 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
}
diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala
index ae6d34ced..70fb51521 100644
--- a/effekt/shared/src/main/scala/effekt/Typer.scala
+++ b/effekt/shared/src/main/scala/effekt/Typer.scala
@@ -54,21 +54,6 @@ object Typer extends Phase[NameResolved, Typechecked] {
Context in {
Context.withUnificationScope {
flowingInto(builtins.toplevelCaptures) {
- // bring builtins into scope
- builtins.rootTerms.values.foreach {
- case term: BlockParam =>
- Context.bind(term, term.tpe.getOrElse {
- INTERNAL_ERROR("Builtins should always be annotated with their types.")
- })
- Context.bind(term, CaptureSet(term.capture))
- case term: ExternResource =>
- Context.bind(term, term.tpe)
- Context.bind(term, CaptureSet(term.capture))
- case term: Callable =>
- Context.bind(term, term.toType)
- case term => Context.panic(s"Cannot bind builtin term: ${term}")
- }
-
// We split the type-checking of definitions into "pre-check" and "check"
// to allow mutually recursive defs
tree.defs.foreach { d => precheckDef(d) }
@@ -119,7 +104,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
checkStmt(s, expectedType)
}.getOrElse(Result(TUnit, ConcreteEffects.empty))
- Result(Context.join(bodyTpe, defaultTpe), defaultEffs ++ bodyEffs)
+ Result(Context.join(bodyTpe, defaultTpe), defaultEffs ++ bodyEffs ++ guardEffs)
case source.Var(id) => id.symbol match {
case x: RefBinder => Context.lookup(x) match {
@@ -287,18 +272,43 @@ object Typer extends Phase[NameResolved, Typechecked] {
Result(ret, (effs -- handled) ++ handlerEffs)
- case tree @ source.Match(sc, clauses, default) =>
+ case tree @ source.Match(scs, clauses, default) =>
- // (1) Check scrutinee
+ // (1) Check scrutinees
// for example. tpe = List[Int]
- val Result(tpe, effs) = checkExpr(sc, None)
+ val results = scs.map{ sc => checkExpr(sc, None) }
+
+ var resEff = ConcreteEffects.union(results.map{ case Result(tpe, effs) => effs })
- var resEff = 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
- Context.bind(checkPattern(tpe, p))
+ // (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
@@ -592,6 +602,8 @@ object Typer extends Phase[NameResolved, Typechecked] {
}
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 {
diff --git a/effekt/shared/src/main/scala/effekt/context/Annotations.scala b/effekt/shared/src/main/scala/effekt/context/Annotations.scala
index bfa619d39..0e34c48c8 100644
--- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala
+++ b/effekt/shared/src/main/scala/effekt/context/Annotations.scala
@@ -1,12 +1,28 @@
package effekt
package context
-import effekt.symbols.ResumeParam
+import effekt.symbols.{BlockSymbol, BlockType, ResumeParam, Symbol, ValueSymbol}
import effekt.util.messages.ErrorReporter
-import kiama.util.Memoiser
-case class Annotation[K, V](name: String, description: String, bindToObjectIdentity: Boolean = true) {
+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
}
@@ -34,35 +50,42 @@ class Annotations private(
def update[K, V](ann: Annotation[K, V], key: K, value: V): Unit = {
val anns = annotationsAt(ann)
- val updatedAnns = anns.updated(Annotations.makeKey(ann, key), value)
+ 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(Annotations.makeKey(ann, key))
+ annotationsAt(ann).get(Key(key))
def getOrElse[K, V](ann: Annotation[K, V], key: K, default: => V): V =
- annotationsAt(ann).getOrElse(Annotations.makeKey(ann, key), default)
+ annotationsAt(ann).getOrElse(Key(key), default)
def getOrElseUpdate[K, V](ann: Annotation[K, V], key: K, default: => V): V =
- annotationsAt(ann).getOrElse(Annotations.makeKey(ann, key), {
+ 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(Annotations.makeKey(ann, key)).asInstanceOf)
+ 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)(implicit C: ErrorReporter): V =
- get(ann, key).getOrElse { C.abort(s"Cannot find ${ann.name} '${key}'") }
+ 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)(implicit global: AnnotationsDB): Unit =
+ 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) => global.annotate(ann, kk.key, f(kk.key, v)) }
+ 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})"
}
@@ -70,38 +93,21 @@ object Annotations {
def empty: Annotations = new Annotations(Map.empty)
- sealed trait Key[T] { def key: T }
-
- private class HashKey[T](val key: T) extends Key[T] {
+ class Key[T](val key: T) {
override val hashCode = System.identityHashCode(key)
- override def equals(o: Any) = o match {
- case k: HashKey[_] => hashCode == k.hashCode
- case _ => false
- }
- }
- private class IdKey[T](val key: T) extends Key[T] {
- override val hashCode = key.hashCode()
override def equals(o: Any) = o match {
- case k: Key[_] => key == k.key
- case _ => false
+ case k: Key[_] => hashCode == k.hashCode
+ case _ => false
}
}
- object Key {
- def unapply[T](k: Key[T]): Option[T] = Some(k.key)
- }
-
- private def makeKey[K, V](ann: Annotation[K, V], k: K): Key[K] =
- if (ann.bindToObjectIdentity) new HashKey(k)
- else new IdKey(k)
-
/**
* 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 = Annotation[source.Tree, symbols.Effects](
+ val InferredEffect = TreeAnnotation[source.Tree, symbols.Effects](
"InferredEffect",
"the inferred effect of"
)
@@ -112,7 +118,7 @@ object Annotations {
* 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 = Annotation[source.Tree, symbols.ValueType](
+ val InferredValueType = TreeAnnotation[source.Tree, symbols.ValueType](
"InferredValueType",
"the inferred type of"
)
@@ -120,7 +126,7 @@ object Annotations {
/**
* The type as inferred by typer at a given position in the tree
*/
- val InferredBlockType = Annotation[source.Tree, symbols.BlockType](
+ val InferredBlockType = TreeAnnotation[source.Tree, symbols.BlockType](
"InferredBlockType",
"the inferred block type of"
)
@@ -128,7 +134,7 @@ object Annotations {
/**
* Type arguments of a _function call_ as inferred by typer
*/
- val TypeArguments = Annotation[source.CallLike, List[symbols.ValueType]](
+ val TypeArguments = TreeAnnotation[source.CallLike, List[symbols.ValueType]](
"TypeArguments",
"the inferred or annotated type arguments of"
)
@@ -136,7 +142,7 @@ object Annotations {
/**
* Existential type parameters inferred by the typer when type-checking pattern matches.
*/
- val TypeParameters = Annotation[source.TagPattern | source.OpClause, List[symbols.TypeVar]](
+ val TypeParameters = TreeAnnotation[source.TagPattern | source.OpClause, List[symbols.TypeVar]](
"TypeParameters",
"the existentials of the constructor pattern or operation clause"
)
@@ -144,7 +150,7 @@ object Annotations {
/**
* Value type of symbols like value binders or value parameters
*/
- val ValueType = Annotation[symbols.ValueSymbol, symbols.ValueType](
+ val ValueType = SymbolAnnotation[symbols.ValueSymbol, symbols.ValueType](
"ValueType",
"the type of value symbol"
)
@@ -152,7 +158,7 @@ object Annotations {
/**
* Block type of symbols like function definitions, block parameters, or continuations
*/
- val BlockType = Annotation[symbols.BlockSymbol, symbols.BlockType](
+ val BlockType = SymbolAnnotation[symbols.BlockSymbol, symbols.BlockType](
"BlockType",
"the type of block symbol"
)
@@ -160,7 +166,7 @@ object Annotations {
/**
* Capability set used by a function definition, block parameter, ...
*/
- val Captures = Annotation[symbols.BlockSymbol, symbols.Captures](
+ val Captures = SymbolAnnotation[symbols.BlockSymbol, symbols.Captures](
"Captures",
"the set of used capabilities of a block symbol"
)
@@ -168,7 +174,7 @@ object Annotations {
/**
* Used by LSP to list all captures
*/
- val CaptureForFile = Annotation[kiama.util.Source, List[(source.Tree, symbols.CaptureSet)]](
+ val CaptureForFile = SourceAnnotation[kiama.util.Source, List[(source.Tree, symbols.CaptureSet)]](
"CaptureSet",
"all inferred captures for file"
)
@@ -178,7 +184,7 @@ object Annotations {
*
* @deprecated
*/
- val SourceModule = Annotation[symbols.Symbol, symbols.Module](
+ val SourceModule = SymbolAnnotation[symbols.Symbol, symbols.Module](
"SourceModule",
"the source module of symbol"
)
@@ -186,7 +192,7 @@ object Annotations {
/**
* Used by LSP for jump-to-definition of imports
*/
- val IncludedSymbols = Annotation[source.Include, symbols.Module](
+ val IncludedSymbols = TreeAnnotation[source.Include, symbols.Module](
"IncludedSymbols",
"the symbol for an import / include"
)
@@ -194,7 +200,7 @@ object Annotations {
/**
* All symbols defined in a source file
*/
- val DefinedSymbols = Annotation[kiama.util.Source, Set[symbols.Symbol]](
+ val DefinedSymbols = SourceAnnotation[kiama.util.Source, Set[symbols.Symbol]](
"DefinedSymbols",
"all symbols for source file"
)
@@ -206,7 +212,7 @@ object Annotations {
*
* TODO maybe store the whole definition tree instead of the name, which requries refactoring of assignSymbol
*/
- val DefinitionTree = Annotation[symbols.Symbol, source.IdDef](
+ val DefinitionTree = SymbolAnnotation[symbols.Symbol, source.IdDef](
"DefinitionTree",
"the tree identifying the definition site of symbol"
)
@@ -216,7 +222,7 @@ object Annotations {
*
* Filled by namer and used for reverse lookup in LSP server
*/
- val References = Annotation[symbols.Symbol, List[source.Reference]](
+ val References = SymbolAnnotation[symbols.Symbol, List[source.Reference]](
"References",
"the references referring to symbol"
)
@@ -227,7 +233,7 @@ object Annotations {
* Id can be the definition-site (IdDef) or use-site (IdRef) of the
* specific symbol
*/
- val Symbol = Annotation[source.Id, symbols.Symbol](
+ val Symbol = TreeAnnotation[source.Id, symbols.Symbol](
"Symbol",
"the symbol for identifier"
)
@@ -237,12 +243,12 @@ object Annotations {
*
* Resolved and annotated by namer and used by typer.
*/
- val Type = Annotation[source.Type, symbols.Type](
+ val Type = TreeAnnotation[source.Type, symbols.Type](
"Type",
"the resolved type for"
)
- val Capture = Annotation[source.CaptureSet, symbols.CaptureSet](
+ val Capture = TreeAnnotation[source.CaptureSet, symbols.CaptureSet](
"Capture",
"the resolved capture set for"
)
@@ -250,7 +256,7 @@ object Annotations {
/**
* Similar to TypeAndEffect: the capture set of a program
*/
- val InferredCapture = Annotation[source.Tree, symbols.CaptureSet](
+ val InferredCapture = TreeAnnotation[source.Tree, symbols.CaptureSet](
"InferredCapture",
"the inferred capture for source tree"
)
@@ -261,7 +267,7 @@ object Annotations {
*
* Inferred by typer, used by elaboration.
*/
- val BoundCapabilities = Annotation[source.Tree, List[symbols.BlockParam]](
+ val BoundCapabilities = TreeAnnotation[source.Tree, List[symbols.BlockParam]](
"BoundCapabilities",
"capabilities bound by this tree"
)
@@ -271,12 +277,12 @@ object Annotations {
*
* Inferred by typer, used by elaboration.
*/
- val CapabilityArguments = Annotation[source.CallLike, List[symbols.BlockParam]](
+ val CapabilityArguments = TreeAnnotation[source.CallLike, List[symbols.BlockParam]](
"CapabilityArguments",
"capabilities inferred as additional arguments for this call"
)
- val CapabilityReceiver = Annotation[source.Do, symbols.BlockParam](
+ val CapabilityReceiver = TreeAnnotation[source.Do, symbols.BlockParam](
"CapabilityReceiver",
"the receiver as inferred for this effect operation call"
)
@@ -286,7 +292,7 @@ object Annotations {
*
* Used by typer for region checking mutable variables.
*/
- val SelfRegion = Annotation[source.Tree, symbols.TrackedParam](
+ val SelfRegion = TreeAnnotation[source.Tree, symbols.TrackedParam](
"SelfRegion",
"the region corresponding to a lexical scope"
)
@@ -298,39 +304,40 @@ object Annotations {
* Introduced by the pretyper.
* Used by typer in order to display a more precise error message.
*/
- val UnboxParentDef = Annotation[source.Unbox, source.Def](
+ val UnboxParentDef = TreeAnnotation[source.Unbox, source.Def](
"UnboxParentDef",
"the parent definition of an Unbox if it was synthesized"
)
}
-
/**
- * A global annotations database
+ * 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 `Annotions`, instead.
+ * subsequent phases, consider using an instance of `Annotations`, instead.
*
- * Calling `Annotations.commit` transfers all annotations into this global DB.
+ * 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 AnnotationsDB { self: Context =>
+trait TreeAnnotations { self: Context =>
+ private type AnnotationsMap = Map[TreeAnnotation[_, _], Any]
- private type Annotations = Map[Annotation[_, _], Any]
- type DB = Memoiser[Any, Map[Annotation[_, _], Any]]
- var db: DB = Memoiser.makeIdMemoiser()
+ private type Annotations = Map[TreeAnnotation[_, _], Any]
+ type DB = util.IdentityHashMap[source.Tree, Map[TreeAnnotation[_, _], Any]]
+ var db: DB = new util.IdentityHashMap()
- private def annotationsAt(key: Any): Map[Annotation[_, _], Any] = db.getOrDefault(key, Map.empty)
+ private def annotationsAt[K](key: K): AnnotationsMap =
+ db.getOrDefault(key, Map.empty)
/**
* Copies annotations, keeping existing annotations at `to`
*/
- def copyAnnotations(from: Any, to: Any): Unit = {
+ def copyAnnotations(from: source.Tree, to: source.Tree): Unit = {
val existing = annotationsAt(to)
- val source = annotationsAt(from)
+ val source = annotationsAt(from)
annotate(to, source ++ existing)
}
@@ -339,23 +346,25 @@ trait AnnotationsDB { self: Context =>
*
* Used by Annotations.commit to commit all temporary annotations to the DB
*/
- def annotate[K, V](key: K, value: Map[Annotation[_, _], Any]): Unit = {
- val anns = annotationsAt(key)
+ def annotate(key: source.Tree, value: AnnotationsMap): Unit = {
+ val anns = db.getOrDefault(key, Map.empty)
db.put(key, anns ++ value)
}
- def annotate[K, V](ann: Annotation[K, V], key: K, value: V): Unit = {
- val anns = annotationsAt(key)
+ 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[K, V](ann: Annotation[K, V], key: K): Option[V] =
+ def annotationOption[V](ann: TreeAnnotation[_, V], key: source.Tree): Option[V] =
annotationsAt(key).get(ann).asInstanceOf[Option[V]]
- def annotation[K, V](ann: Annotation[K, V], key: K): V =
- annotationOption(ann, key).getOrElse { panic(s"Cannot find ${ann.description} for '${key}'") }
+ def annotation[V](ann: TreeAnnotation[_, V], key: source.Tree): V =
+ annotationOption(ann, key).getOrElse {
+ panic(s"Cannot find ${ann.description} for '${key}'")
+ }
- def hasAnnotation[K, V](ann: Annotation[K, V], key: K): Boolean =
+ def hasAnnotation[V](ann: TreeAnnotation[_, V], key: source.Tree): Boolean =
annotationsAt(key).isDefinedAt(ann)
// Customized Accessors
@@ -421,51 +430,6 @@ trait AnnotationsDB { self: Context =>
def resolvedCapture(tree: source.CaptureSet): symbols.CaptureSet =
annotation(Annotations.Capture, tree)
- def typeOf(s: Symbol): Type = s match {
- case s: ValueSymbol => valueTypeOf(s)
- case s: BlockSymbol => blockTypeOf(s)
- case _ => panic(s"Cannot find a type for symbol '${s}'")
- }
-
- def functionTypeOf(s: Symbol): FunctionType =
- functionTypeOption(s) getOrElse { panic(s"Cannot find type for block '${s}'") }
-
- def functionTypeOption(s: Symbol): Option[FunctionType] =
- s match {
- case b: BlockSymbol => annotationOption(Annotations.BlockType, b) flatMap {
- case b: FunctionType => Some(b)
- 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
- // }
- // }
- }
-
- def blockTypeOf(s: Symbol): BlockType =
- blockTypeOption(s) getOrElse { panic(s"Cannot find interface type for block '${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 valueTypeOf(s: Symbol): ValueType =
- valueTypeOption(s) getOrElse { panic(s"Cannot find value binder for ${s}") }
-
- def valueTypeOption(s: Symbol): Option[ValueType] = s match {
- case s: ValueSymbol => annotationOption(Annotations.ValueType, s)
- case _ => panic(s"Trying to find a value type for non-value '${s}'")
- }
-
-
// Symbols
// -------
@@ -521,44 +485,143 @@ trait AnnotationsDB { self: Context =>
*/
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]]
/**
- * Searching the definition for a symbol
+ * List all symbols that have a source module
+ *
+ * Used by the LSP server to generate outline
*/
- def definitionTreeOption(s: Symbol): Option[source.IdDef] =
- annotationOption(Annotations.DefinitionTree, s)
+ 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: Symbol): Unit =
+ def addDefinedSymbolToSource(s: symbols.Symbol): Unit =
if (module != null) {
- val syms = annotationOption(Annotations.DefinedSymbols, module.source).getOrElse(Set.empty)
- annotate(Annotations.DefinedSymbols, module.source, syms + s)
+ 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}"))
+
/**
- * List all symbols that have a source module
- *
- * Used by the LSP server to generate outline
+ * Searching the definition for a symbol
*/
- def sourceSymbolsFor(src: kiama.util.Source): Set[Symbol] =
- annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty)
+ 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: Symbol): List[source.Reference] =
+ 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: BlockSymbol): symbols.Captures =
- annotation(Annotations.Captures, sym)
+ def captureOf(sym: symbols.BlockSymbol): symbols.Captures =
+ annotationOption(Annotations.Captures, sym)
+ .getOrElse(panic(s"Cannot find captures for ${sym}"))
- def captureOfOption(sym: BlockSymbol): Option[symbols.Captures] =
+ def captureOfOption(sym: symbols.BlockSymbol): Option[symbols.Captures] =
annotationOption(Annotations.Captures, sym)
}
diff --git a/effekt/shared/src/main/scala/effekt/context/Context.scala b/effekt/shared/src/main/scala/effekt/context/Context.scala
index aca90b526..594f287f8 100644
--- a/effekt/shared/src/main/scala/effekt/context/Context.scala
+++ b/effekt/shared/src/main/scala/effekt/context/Context.scala
@@ -18,7 +18,9 @@ import kiama.util.Positions
*/
trait ContextOps
extends ErrorReporter
- with AnnotationsDB { self: Context =>
+ with TreeAnnotations
+ with SourceAnnotations
+ with SymbolAnnotations { self: Context =>
/**
* Used throughout the compiler to create a new "scope"
diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala
index b83fcd22f..9907dd340 100644
--- a/effekt/shared/src/main/scala/effekt/core/Parser.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala
@@ -168,7 +168,7 @@ class CoreParsers(positions: Positions, names: Names) extends EffektLexers(posit
( literal
| id ~ (`:` ~> valueType) ^^ Pure.ValueVar.apply
| `box` ~> captures ~ block ^^ { case capt ~ block => Pure.Box(block, capt) }
- | `make` ~> dataType ~ id ~ valueArgs ^^ Pure.Make.apply
+ | `make` ~> dataType ~ id ~ maybeTypeArgs ~ valueArgs ^^ Pure.Make.apply
| maybeParens(blockVar) ~ maybeTypeArgs ~ valueArgs ^^ Pure.PureApp.apply
| failure("Expected a pure expression.")
)
diff --git a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala b/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala
index 83bd76f64..04ae6f906 100644
--- a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala
+++ b/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala
@@ -1,9 +1,7 @@
package effekt
package core
-import effekt.context.Context
import effekt.core.substitutions.Substitution
-import effekt.symbols.TmpValue
import scala.collection.mutable
@@ -54,9 +52,9 @@ import scala.collection.mutable
object PatternMatchingCompiler {
/**
- * The conditions need to be met in sequence before the block at [[label]] can be evaluated with given [[args]].
+ * 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, args: List[ValueVar])
+ 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
@@ -71,7 +69,7 @@ object PatternMatchingCompiler {
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, patterns: List[(Pattern, ValueType)])
+ case Tag(id: Id, tparams: List[Id], patterns: List[(Pattern, ValueType)])
case Ignore()
case Any(id: Id)
case Or(patterns: List[Pattern])
@@ -93,21 +91,21 @@ object PatternMatchingCompiler {
// (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, args) =>
- return core.App(target, Nil, args, Nil)
+ 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, args) =>
- return core.Val(x, tpe, binding, compile(Clause(rest, target, args) :: remainingClauses))
+ 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, args) =>
- return core.Let(x, tpe, binding, compile(Clause(rest, target, args) :: remainingClauses))
+ 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, args) =>
+ case Clause(Condition.Predicate(pred) :: rest, target, targs, args) =>
return core.If(pred,
- compile(Clause(rest, target, args) :: remainingClauses),
+ compile(Clause(rest, target, targs, args) :: remainingClauses),
compile(remainingClauses)
)
- case Clause(Condition.Patterns(patterns) :: rest, target, args) =>
+ case Clause(Condition.Patterns(patterns) :: rest, target, targs, args) =>
patterns
}
@@ -127,7 +125,7 @@ object PatternMatchingCompiler {
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
+ case Clause(Split(Pattern.Literal(lit, _), _, _), _, _, _) => lit
}.distinct
// for each literal, we collect the clauses that match it correctly
@@ -141,8 +139,8 @@ object PatternMatchingCompiler {
defaults = defaults :+ cl
normalized.foreach {
- case Clause(Split(Pattern.Literal(lit, _), restPatterns, restConds), label, args) =>
- addClause(lit, Clause(Condition.Patterns(restPatterns) :: restConds, label, args))
+ 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) }
@@ -164,7 +162,7 @@ object PatternMatchingCompiler {
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
+ case Clause(Split(p: Pattern.Tag, _, _), _, _, _) => p.id
}.distinct
// for each tag, we collect the clauses that match it correctly
@@ -179,7 +177,9 @@ object PatternMatchingCompiler {
// used to make up new scrutinees
val varsFor = mutable.Map.empty[Id, List[ValueVar]]
- def fieldVarsFor(constructor: Id, fieldInfo: List[((Pattern, ValueType), String)]): 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 {
@@ -191,17 +191,17 @@ object PatternMatchingCompiler {
)
normalized.foreach {
- case Clause(Split(Pattern.Tag(constructor, patternsAndTypes), restPatterns, restConds), label, args) =>
+ 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, patternsAndTypes.zip(fieldNames))
+ 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, args))
+ Clause(Condition.Patterns(nestedMatches) :: Condition.Patterns(restPatterns) :: restConds, label, targs, args))
case c =>
// Clauses that don't match on that var are duplicated.
@@ -214,8 +214,9 @@ object PatternMatchingCompiler {
// (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(Nil, Nil, params, Nil, body)
+ val blockLit: BlockLit = BlockLit(tparams, Nil, params, Nil, body)
(v, blockLit)
}
@@ -232,16 +233,17 @@ object PatternMatchingCompiler {
def branchingHeuristic(patterns: Map[ValueVar, Pattern], clauses: List[Clause]): ValueVar =
patterns.keys.maxBy(v => clauses.count {
- case Clause(ps, _, _) => ps.contains(v)
+ case Clause(ps, _, _, _) => ps.contains(v)
})
/**
* Substitutes AnyPattern and removes wildcards.
*/
def normalize(clause: Clause): Clause = clause match {
- case Clause(conditions, label, args) =>
+ case Clause(conditions, label, targs, args) =>
val (normalized, substitution) = normalize(Map.empty, conditions, Map.empty)
- Clause(normalized, label, args.map(v => substitution.getOrElse(v.id, v)))
+ // TODO also substitute types?
+ Clause(normalized, label, targs, args.map(v => substitution.getOrElse(v.id, v)))
}
@@ -309,8 +311,8 @@ object PatternMatchingCompiler {
// -----------------------------
def show(cl: Clause): String = cl match {
- case Clause(conditions, label, args) =>
- s"case ${conditions.map(show).mkString("; ")} => ${util.show(label.id)}${args.map(x => util.show(x)).mkString("(", ", ", ")")}"
+ 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 {
@@ -321,7 +323,7 @@ object PatternMatchingCompiler {
}
def show(p: Pattern): String = p match {
- case Pattern.Tag(id, patterns) => util.show(id) + patterns.map { case (p, tpe) => show(p) }.mkString("(", ", ", ")")
+ 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(" | ")
diff --git a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala
index 901e1a2ab..05f4fbef2 100644
--- a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala
+++ b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala
@@ -349,7 +349,7 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] {
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, vargs) =>
+ 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}")
@@ -357,7 +357,7 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] {
val paramTypes = ctorDecl.fields.map(_.tpe)
val coercedArgs = (vargs zip paramTypes).map { case (arg, paramTpe) => coerce(transform(arg), paramTpe) }
- Pure.Make(transform(data), tag, coercedArgs)
+ Pure.Make(transform(data), tag, targs.map(transformArg), coercedArgs)
case Pure.Box(b, annotatedCapture) =>
Pure.Box(transform(b), annotatedCapture)
diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala
index a417c5cfd..d69fc45a8 100644
--- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala
+++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala
@@ -100,7 +100,7 @@ object PrettyPrinter extends ParenPrettyPrinter {
case ValueVar(id, _) => toDoc(id)
case PureApp(b, targs, vargs) => toDoc(b) <> argsToDoc(targs, vargs, Nil)
- case Make(data, tag, vargs) => "make" <+> toDoc(data) <+> toDoc(tag) <> argsToDoc(Nil, vargs, Nil)
+ case Make(data, tag, targs, vargs) => "make" <+> toDoc(data) <+> toDoc(tag) <> argsToDoc(targs, vargs, Nil)
case DirectApp(b, targs, vargs, bargs) => toDoc(b) <> argsToDoc(targs, vargs, bargs)
case Box(b, capt) => parens("box" <+> toDoc(b))
diff --git a/effekt/shared/src/main/scala/effekt/core/Recursive.scala b/effekt/shared/src/main/scala/effekt/core/Recursive.scala
index 7542320e9..f21bf3e52 100644
--- a/effekt/shared/src/main/scala/effekt/core/Recursive.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Recursive.scala
@@ -94,7 +94,7 @@ class Recursive(
case Pure.ValueVar(id, annotatedType) => ()
case Pure.Literal(value, annotatedType) => ()
case Pure.PureApp(b, targs, vargs) => process(b); vargs.foreach(process)
- case Pure.Make(data, tag, vargs) => vargs.foreach(process)
+ case Pure.Make(data, tag, targs, vargs) => vargs.foreach(process)
case Pure.Box(b, annotatedCapture) => process(b)
}
diff --git a/effekt/shared/src/main/scala/effekt/core/Renamer.scala b/effekt/shared/src/main/scala/effekt/core/Renamer.scala
index 3b76ca1bb..2f99bc642 100644
--- a/effekt/shared/src/main/scala/effekt/core/Renamer.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Renamer.scala
@@ -1,10 +1,12 @@
package effekt.core
+import scala.collection.mutable
+
import effekt.{ core, symbols }
-import effekt.context.Context
/**
- * Freshens bound names in a given term
+ * Freshens bound names in a given Core term.
+ * Do not use for tests! See [[effekt.core.TestRenamer]].
*
* @param names used to look up a reference by name to resolve to the same symbols.
* This is only used by tests to deterministically rename terms and check for
@@ -15,36 +17,33 @@ import effekt.context.Context
*/
class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core.Tree.Rewrite {
- // list of scopes that map bound symbols to their renamed variants.
- private var scopes: List[Map[Id, Id]] = List.empty
-
- // Here we track ALL renamings
- var renamed: Map[Id, Id] = Map.empty
+ // Local renamings: map of bound symbols to their renamed variants in a given scope.
+ private var scope: Map[Id, Id] = Map.empty
- private var suffix: Int = 0
+ // All renamings: map of bound symbols to their renamed variants, globally!
+ val renamed: mutable.HashMap[Id, Id] = mutable.HashMap.empty
def freshIdFor(id: Id): Id =
- suffix = suffix + 1
- val uniqueName = if prefix.isEmpty then id.name.name + "_" + suffix.toString else prefix + suffix.toString
- names.idFor(uniqueName)
+ if prefix.isEmpty then Id(id) else Id(id.name.rename { _current => prefix })
def withBindings[R](ids: List[Id])(f: => R): R =
- val before = scopes
+ val scopeBefore = scope
try {
- val newScope = ids.map { x => x -> freshIdFor(x) }.toMap
- scopes = newScope :: scopes
- renamed = renamed ++ newScope
+ ids.foreach { x =>
+ val fresh = freshIdFor(x)
+ scope = scope + (x -> fresh)
+ renamed.put(x, fresh)
+ }
+
f
- } finally { scopes = before }
+ } finally { scope = scopeBefore }
/** Alias for withBindings(List(id)){...} */
def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f)
// free variables are left untouched
override def id: PartialFunction[core.Id, core.Id] = {
- case id => scopes.collectFirst {
- case bnds if bnds.contains(id) => bnds(id)
- }.getOrElse(id)
+ id => scope.getOrElse(id, id)
}
override def stmt: PartialFunction[Stmt, Stmt] = {
@@ -98,21 +97,19 @@ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core
}
def apply(m: core.ModuleDecl): core.ModuleDecl =
- suffix = 0
m match {
case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) =>
core.ModuleDecl(path, includes, declarations, externs, definitions map rewrite, exports)
}
def apply(s: Stmt): Stmt = {
- suffix = 0
rewrite(s)
}
}
object Renamer {
def rename(b: Block): Block = Renamer().rewrite(b)
- def rename(b: BlockLit): (BlockLit, Map[Id, Id]) =
+ def rename(b: BlockLit): (BlockLit, mutable.HashMap[Id, Id]) =
val renamer = Renamer()
val res = renamer.rewrite(b)
(res, renamer.renamed)
diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala
index d3c1ef545..dfee5412e 100644
--- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala
@@ -273,13 +273,13 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
Stmt.Return(PureApp(BlockVar(b), targs, vargs)))
}
- // [[ f ]] = { (x) => make f(x) }
+ // [[ 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, vargs)))
+ Stmt.Return(Make(core.ValueType.Data(b.tpe, targs), b, targs, vargs)))
}
// [[ f ]] = { (x){g} => let r = f(x){g}; return r }
@@ -427,14 +427,14 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
Context.bind(loopCall)
// Empty match (matching on Nothing)
- case source.Match(sc, Nil, None) =>
+ case source.Match(List(sc), Nil, None) =>
val scrutinee: ValueVar = Context.bind(transformAsPure(sc))
Context.bind(core.Match(scrutinee, Nil, None))
- case source.Match(sc, cs, default) =>
+ case source.Match(scs, cs, default) =>
// (1) Bind scrutinee and all clauses so we do not have to deal with sharing on demand.
- val scrutinee: ValueVar = Context.bind(transformAsPure(sc))
- val clauses = cs.zipWithIndex.map((c, i) => preprocess(s"k${i}", scrutinee, c))
+ 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)
@@ -653,8 +653,14 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
})
}
- def preprocess(label: String, sc: ValueVar, clause: source.MatchClause)(using Context): Clause =
- preprocess(label, List((sc, clause.pattern)), clause.guards, transform(clause.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.*
@@ -663,11 +669,22 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
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.")
@@ -685,18 +702,22 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
} 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(Nil, Nil, params, Nil, body))
+ 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 source.TagPattern(id, patterns) =>
- Pattern.Tag(id.symbol, patterns.map { p => (transformPattern(p), transform(Context.inferredTypeOf(p))) })
+ 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] =
@@ -719,7 +740,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
val transformedGuards = guards.flatMap(transformGuard)
val conditions = if transformedPatterns.isEmpty then transformedGuards else Condition.Patterns(transformedPatterns) :: guards.flatMap(transformGuard)
- Clause(conditions, joinpoint, params.map(p => core.ValueVar(p.id, p.tpe)))
+ Clause(conditions, joinpoint, tparams.map(x => core.ValueType.Var(x)), params.map(p => core.ValueVar(p.id, p.tpe)))
}
/**
@@ -785,7 +806,9 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
DirectApp(BlockVar(f), targs, vargsT, bargsT)
case r: Constructor =>
if (bargs.nonEmpty) Context.abort("Constructors cannot take block arguments.")
- Make(core.ValueType.Data(r.tpe, targs), r, vargsT)
+ 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 =>
diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala
index 171a7befc..bdf591826 100644
--- a/effekt/shared/src/main/scala/effekt/core/Tree.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala
@@ -91,10 +91,11 @@ sealed trait Tree extends Product {
*/
type Id = symbols.Symbol
object Id {
- def apply(n: String): Id = new symbols.Symbol {
- val name = symbols.Name.local(n)
+ def apply(n: symbols.Name): Id = new symbols.Symbol {
+ val name = n
}
- def apply(n: Id): Id = apply(n.name.name)
+ def apply(n: String): Id = apply(symbols.Name.local(n))
+ def apply(n: Id): Id = apply(n.name)
}
/**
@@ -191,7 +192,7 @@ enum Pure extends Expr {
*
* Note: the structure mirrors interface implementation
*/
- case Make(data: ValueType.Data, tag: Id, vargs: List[Pure])
+ case Make(data: ValueType.Data, tag: Id, targs: List[ValueType], vargs: List[Pure])
case Box(b: Block, annotatedCapture: Captures)
}
@@ -431,6 +432,7 @@ object Tree {
case (id, lit) => query(lit)
}
def query(b: ExternBody)(using Ctx): Res = structuralQuery(b, externBody)
+ def query(m: ModuleDecl)(using Ctx) = structuralQuery(m, PartialFunction.empty)
}
class Rewrite extends Structural {
@@ -578,7 +580,7 @@ object Variables {
case Pure.ValueVar(id, annotatedType) => Variables.value(id, annotatedType)
case Pure.Literal(value, annotatedType) => Variables.empty
case Pure.PureApp(b, targs, vargs) => free(b) ++ all(vargs, free)
- case Pure.Make(data, tag, vargs) => all(vargs, free)
+ case Pure.Make(data, tag, targs, vargs) => all(vargs, free)
case Pure.Box(b, annotatedCapture) => free(b)
}
@@ -772,8 +774,8 @@ object substitutions {
case Literal(value, annotatedType) =>
Literal(value, substitute(annotatedType))
- case Make(tpe, tag, vargs) =>
- Make(substitute(tpe).asInstanceOf, tag, vargs.map(substitute))
+ case Make(tpe, tag, targs, vargs) =>
+ Make(substitute(tpe).asInstanceOf, tag, targs.map(substitute), vargs.map(substitute))
case PureApp(f, targs, vargs) => substitute(f) match {
case g : Block.BlockVar => PureApp(g, targs.map(substitute), vargs.map(substitute))
diff --git a/effekt/shared/src/main/scala/effekt/core/Type.scala b/effekt/shared/src/main/scala/effekt/core/Type.scala
index e52bbaaa1..cdba76670 100644
--- a/effekt/shared/src/main/scala/effekt/core/Type.scala
+++ b/effekt/shared/src/main/scala/effekt/core/Type.scala
@@ -252,7 +252,7 @@ object Type {
case Pure.ValueVar(id, tpe) => tpe
case Pure.Literal(value, tpe) => tpe
case Pure.PureApp(callee, targs, args) => instantiate(callee.functionType, targs, Nil).result
- case Pure.Make(tpe, tag, args) => tpe
+ case Pure.Make(tpe, tag, targs, args) => tpe // TODO instantiate?
case Pure.Box(block, capt) => ValueType.Boxed(block.tpe, capt)
}
diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala
index 3b91c76e8..478710e95 100644
--- a/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala
+++ b/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala
@@ -126,8 +126,8 @@ object BindSubexpressions {
case Pure.ValueVar(id, tpe) => pure(ValueVar(transform(id), transform(tpe)))
case Pure.Literal(value, tpe) => pure(Pure.Literal(value, transform(tpe)))
- case Pure.Make(data, tag, vargs) => transformExprs(vargs) { vs =>
- bind(Pure.Make(data, tag, vs))
+ case Pure.Make(data, tag, targs, vargs) => transformExprs(vargs) { vs =>
+ bind(Pure.Make(data, tag, targs, vs))
}
case DirectApp(f, targs, vargs, bargs) => for {
vs <- transformExprs(vargs);
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 6344fb070..b96da7e12 100644
--- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala
+++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala
@@ -125,6 +125,7 @@ object Normalizer { normal =>
* A good testcase to look at for this is:
* examples/pos/capture/regions.effekt
*/
+ @tailrec
private def active[R](b: Block)(using C: Context): NormalizedBlock =
normalize(b) match {
case b: Block.BlockLit => NormalizedBlock.Known(b, None)
@@ -204,10 +205,10 @@ object Normalizer { normal =>
}
case Stmt.Match(scrutinee, clauses, default) => active(scrutinee) match {
- case Pure.Make(data, tag, vargs) if clauses.exists { case (id, _) => id == tag } =>
+ case Pure.Make(data, tag, targs, vargs) if clauses.exists { case (id, _) => id == tag } =>
val clause: BlockLit = clauses.collectFirst { case (id, cl) if id == tag => cl }.get
- normalize(reduce(clause, Nil, vargs.map(normalize), Nil))
- case Pure.Make(data, tag, vargs) if default.isDefined =>
+ normalize(reduce(clause, targs, vargs.map(normalize), Nil))
+ case Pure.Make(data, tag, targs, vargs) if default.isDefined =>
normalize(default.get)
case _ =>
val normalized = normalize(scrutinee)
@@ -355,7 +356,7 @@ object Normalizer { normal =>
// congruences
case Pure.PureApp(f, targs, vargs) => Pure.PureApp(f, targs, vargs.map(normalize))
- case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(normalize))
+ case Pure.Make(data, tag, targs, vargs) => Pure.Make(data, tag, targs, vargs.map(normalize))
case Pure.ValueVar(id, annotatedType) => p
case Pure.Literal(value, annotatedType) => p
}
@@ -400,7 +401,6 @@ object Normalizer { normal =>
renamedIds.foreach(copyUsage)
- val newUsage = usage.collect { case (id, usage) if util.show(id) contains "foreach" => (id, usage) }
// (2) substitute
val body = substitutions.substitute(renamedLit, targs, vargs, bvars)
diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala
index 15cbbceeb..abbd2327a 100644
--- a/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala
+++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala
@@ -109,7 +109,7 @@ class Reachable(
case Pure.ValueVar(id, annotatedType) => process(id)
case Pure.Literal(value, annotatedType) => ()
case Pure.PureApp(b, targs, vargs) => process(b); vargs.foreach(process)
- case Pure.Make(data, tag, vargs) => process(tag); vargs.foreach(process)
+ case Pure.Make(data, tag, targs, vargs) => process(tag); vargs.foreach(process)
case Pure.Box(b, annotatedCapture) => process(b)
}
diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala
index b8d9f69c1..8f023dfa3 100644
--- a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala
+++ b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala
@@ -91,7 +91,8 @@ object StaticArguments {
// the worker now closes over the static block arguments (`c` in the example above):
val newCapture = blockLit.capt ++ selectStatic(staticB, freshCparams).toSet
- val workerVar: Block.BlockVar = BlockVar(Id(id.name.name + "_worker"), workerType, newCapture)
+ val workerId = Id(id.name.rename { original => s"${original}_worker"})
+ val workerVar: Block.BlockVar = BlockVar(workerId, workerType, newCapture)
ctx.workers(id) = workerVar
BlockLit(
@@ -185,7 +186,7 @@ object StaticArguments {
def rewrite(p: Pure)(using StaticArgumentsContext): Pure = p match {
case Pure.PureApp(f, targs, vargs) => Pure.PureApp(f, targs, vargs.map(rewrite))
- case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(rewrite))
+ case Pure.Make(data, tag, targs, vargs) => Pure.Make(data, tag, targs, vargs.map(rewrite))
case x @ Pure.ValueVar(id, annotatedType) => x
// congruences
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 631e779c2..541f5d27b 100644
--- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala
+++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala
@@ -205,6 +205,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) {
stack match {
case Stack.Empty => ???
case Stack.Segment(frames, prompt, rest) =>
+ @tailrec
def go(frames: List[Frame], acc: List[Frame]): Stack =
frames match {
case Nil =>
@@ -331,11 +332,6 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) {
// TODO make the type of Region more precise...
case Stmt.Region(_) => ???
- case Stmt.Alloc(id, init, region, body) if region == symbols.builtins.globalRegion =>
- val value = eval(init, env)
- val address = freshAddress()
- State.Step(body, env.bind(id, Computation.Reference(address)), stack, heap.updated(address, value))
-
case Stmt.Alloc(id, init, region, body) =>
val value = eval(init, env)
@@ -506,7 +502,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) {
case other => other.toString
}.mkString(", ")}" }
}
- case Pure.Make(data, tag, vargs) =>
+ case Pure.Make(data, tag, targs, vargs) =>
val result: Value.Data = Value.Data(data, tag, vargs.map(a => eval(a, env)))
instrumentation.allocate(result)
result
diff --git a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala
index 636787d09..b43c41633 100644
--- a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala
@@ -182,7 +182,7 @@ object Transformer {
case Block.BlockVar(id) => PureApp(id, vargs.map(transform))
case _ => sys error "Should not happen"
}
- case core.Pure.Make(data, tag, vargs) => Make(data, tag, vargs.map(transform))
+ case core.Pure.Make(data, tag, targs, vargs) => Make(data, tag, vargs.map(transform))
case core.Pure.Box(b, annotatedCapture) => Box(transform(b))
}
diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala
index 01ea52559..90882531d 100644
--- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala
@@ -95,9 +95,6 @@ trait Transformer {
case Var(ref, init, capt, body) =>
state(nameDef(ref), toChez(init), toChez(body))
- case Alloc(id, init, region, body) if region == symbols.builtins.globalRegion =>
- chez.Let(List(Binding(nameDef(id), chez.Builtin("box", 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))
@@ -222,7 +219,7 @@ trait Transformer {
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, args) => chez.Call(chez.Variable(nameRef(tag)), args map toChez)
+ case Make(data, tag, targs, args) => chez.Call(chez.Variable(nameRef(tag)), args map toChez)
case Box(b, _) => toChez(b)
}
diff --git a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala
index f0065b61b..0ddb299aa 100644
--- a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala
@@ -174,7 +174,7 @@ trait Transformer {
register(publicDependencySymbols(x), x)
case ValueVar(x, tpe) if publicDependencySymbols.isDefinedAt(x) =>
register(publicDependencySymbols(x), x)
- case Make(tpe, id, args) if publicDependencySymbols.isDefinedAt(id) =>
+ case Make(tpe, id, targs, args) if publicDependencySymbols.isDefinedAt(id) =>
register(publicDependencySymbols(id), id)
args.foreach(go)
}
diff --git a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala
index 259d68a91..db67fabf7 100644
--- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala
@@ -70,12 +70,7 @@ object TransformerCps extends Transformer {
val jsDecls = module.declarations.flatMap(toJS)
val stmts = module.definitions.map(toJS)
- val state = js.Const(
- nameDef(symbols.builtins.globalRegion),
- js.Variable(JSName("global"))
- ) :: Nil
-
- js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ state ++ stmts)
+ js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ stmts)
}
def compileLSP(input: cps.ModuleDecl, coreModule: core.ModuleDecl)(using C: Context): List[js.Stmt] =
@@ -381,10 +376,7 @@ object TransformerCps extends Transformer {
// DEALLOC(ref); body
case cps.Stmt.Dealloc(ref, body) =>
- Binding { k =>
- js.ExprStmt(js.Call(DEALLOC, nameRef(ref))) ::
- toJS(body).run(k)
- }
+ toJS(body)
// const id = ref.value; body
case cps.Stmt.Get(ref, id, body) =>
@@ -393,9 +385,9 @@ object TransformerCps extends Transformer {
toJS(body).run(k)
}
- // ref.value = _value; body
+ // ref.set(value); body
case cps.Stmt.Put(ref, value, body) => Binding { k =>
- js.Assign(js.Member(nameRef(ref), JSName("value")), toJS(value)) ::
+ js.ExprStmt(js.MethodCall(nameRef(ref), JSName("set"), toJS(value))) ::
toJS(body).run(k)
}
diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala
index 778f151d3..68910ee20 100644
--- a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala
@@ -12,9 +12,9 @@ object PrettyPrinter {
definitions.map(show).mkString("\n\n")
def show(definition: Definition)(using C: Context): LLVMString = definition match {
- case Function(callingConvention, returnType, name, parameters, basicBlocks) =>
+ case Function(linkage, callingConvention, returnType, name, parameters, basicBlocks) =>
s"""
-define ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) {
+define ${show(linkage)} ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) {
${indentedLines(basicBlocks.map(show).mkString)}
}
"""
@@ -38,6 +38,11 @@ define ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${comm
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"
diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala
index b16c5f521..ded566b62 100644
--- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala
@@ -7,6 +7,7 @@ import effekt.util.intercalate
import effekt.util.messages.ErrorReporter
import effekt.machine.analysis.*
+import scala.annotation.tailrec
import scala.collection.mutable
object Transformer {
@@ -25,7 +26,7 @@ object Transformer {
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(Ccc(), VoidType(), "effektMain", List(), List(entryBlock))
+ val entryFunction = Function(External(), Ccc(), VoidType(), "effektMain", List(), List(entryBlock))
declarations.map(transform) ++ globals :+ entryFunction
}
@@ -378,8 +379,6 @@ object Transformer {
def transform(value: machine.Variable)(using FunctionContext): Operand =
substitute(value) match {
- // TODO rethink existence of global
- case machine.Variable("global", machine.Type.Prompt()) => ConstantGlobal("global")
case machine.Variable(name, tpe) => LocalReference(transform(tpe), name)
}
@@ -435,7 +434,7 @@ object Transformer {
val instructions = BC.instructions; BC.instructions = null;
val entryBlock = BasicBlock("entry", instructions, terminator);
- val function = Function(Ccc(), VoidType(), name, parameters, entryBlock :: basicBlocks);
+ val function = Function(Private(), Ccc(), VoidType(), name, parameters, entryBlock :: basicBlocks);
emit(function)
}
@@ -450,7 +449,7 @@ object Transformer {
val instructions = BC.instructions; BC.instructions = null;
val entryBlock = BasicBlock("entry", instructions, terminator);
- val function = Function(Tailcc(true), VoidType(), name, parameters :+ Parameter(stackType, "stack"), entryBlock :: basicBlocks);
+ val function = Function(Private(), Tailcc(true), VoidType(), name, parameters :+ Parameter(stackType, "stack"), entryBlock :: basicBlocks);
emit(function)
}
@@ -635,6 +634,7 @@ object Transformer {
}
def shareValues(values: machine.Environment, freeInBody: Set[machine.Variable])(using FunctionContext, BlockContext): Unit = {
+ @tailrec
def loop(values: machine.Environment): Unit = {
values match {
case Nil => ()
diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala
index 6306d1f0e..6bcd5666e 100644
--- a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala
+++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala
@@ -7,13 +7,19 @@ package llvm
* see: https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST.html#t:Definition
*/
enum Definition {
- case Function(callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], basicBlocks: List[BasicBlock])
+ 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)
diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala
index 116e4acfd..90751be3c 100644
--- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala
+++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala
@@ -7,6 +7,7 @@ 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 {
@@ -37,8 +38,11 @@ object Transformer {
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"Toplevel object definitions not yet supported: ${d}")
+ ErrorReporter.abort(s"Other toplevel definitions not yet supported: ${d}")
}
val localDefinitions = BC.definitions
@@ -89,9 +93,7 @@ object Transformer {
// Regions are blocks and can be free, but do not have info.
case core.Variable.Block(id, core.Type.TRegion, capt) =>
- if id == symbols.builtins.globalRegion
- then Set.empty
- else Set(Variable(transform(id), Type.Prompt()))
+ 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)
@@ -120,13 +122,16 @@ object Transformer {
noteParameter(id, block.tpe)
New(Variable(transform(id), transform(impl.interface)), transform(impl), transform(rest))
- case core.Def(id, block @ core.BlockVar(alias, tpe, _), rest) =>
- getDefinition(alias) match {
+ 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))
}
- emitDefinition(transformLabel(id), Jump(transformLabel(alias)))
- transform(rest)
case core.Def(id, block @ core.Unbox(pure), rest) =>
noteParameter(id, block.tpe)
@@ -190,6 +195,10 @@ object Transformer {
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)
@@ -246,6 +255,8 @@ object Transformer {
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)
@@ -254,24 +265,13 @@ object Transformer {
case core.Alloc(id, init, region, body) =>
transform(init).run { value =>
- val tpe = value.tpe;
- val name = transform(id)
- val variable = Variable(name, tpe)
- val reference = Variable(transform(id), Type.Reference(tpe))
+ val reference = Variable(transform(id), Type.Reference(value.tpe))
val prompt = Variable(transform(region), Type.Prompt())
val temporary = Variable(freshName("temporaryStack"), Type.Stack())
- region match {
- case symbols.builtins.globalRegion =>
- val globalPrompt = Variable("global", Type.Prompt())
- Shift(temporary, globalPrompt,
- Var(reference, value, Type.Positive(),
- Resume(temporary, transform(body))))
- case _ =>
- Shift(temporary, prompt,
- Var(reference, value, Type.Positive(),
- Resume(temporary, transform(body))))
- }
+ Shift(temporary, prompt,
+ Var(reference, value, Type.Positive(),
+ Resume(temporary, transform(body))))
}
case core.Var(ref, init, capture, body) =>
@@ -314,12 +314,17 @@ object Transformer {
} 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())
- Binding { k =>
+ shift { k =>
New(variable, List(Clause(parameters,
// conceptually: Substitute(parameters zip parameters, Jump(...)) but the Substitute is a no-op here
Jump(transformLabel(id))
@@ -333,13 +338,13 @@ object Transformer {
noteParameters(bparams)
val parameters = vparams.map(transform) ++ bparams.map(transform);
val variable = Variable(freshName("blockLit"), Negative())
- Binding { k =>
+ shift { k =>
New(variable, List(Clause(parameters, transform(body))), k(variable))
}
case core.New(impl) =>
val variable = Variable(freshName("new"), Negative())
- Binding { k =>
+ shift { k =>
New(variable, transform(impl), k(variable))
}
@@ -351,7 +356,7 @@ object Transformer {
case core.ValueVar(id, tpe) if BC.globals contains id =>
val variable = Variable(freshName("run"), transform(tpe))
- Binding { k =>
+ shift { k =>
// TODO this might introduce too many pushes.
PushFrame(Clause(List(variable), k(variable)),
Substitute(Nil, Jump(BC.globals(id))))
@@ -362,38 +367,38 @@ object Transformer {
case core.Literal((), _) =>
val variable = Variable(freshName("unitLiteral"), Positive());
- Binding { k =>
+ shift { k =>
Construct(variable, builtins.Unit, List(), k(variable))
}
case core.Literal(value: Long, _) =>
val variable = Variable(freshName("longLiteral"), Type.Int());
- Binding { k =>
+ shift { k =>
LiteralInt(variable, value, k(variable))
}
// for characters
case core.Literal(value: Int, _) =>
val variable = Variable(freshName("intLiteral"), Type.Int());
- Binding { k =>
+ shift { k =>
LiteralInt(variable, value, k(variable))
}
case core.Literal(value: Boolean, _) =>
val variable = Variable(freshName("booleanLiteral"), Positive())
- Binding { k =>
+ 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());
- Binding { k =>
+ shift { k =>
LiteralDouble(literal_binding, v, k(literal_binding))
}
case core.Literal(javastring: String, _) =>
val literal_binding = Variable(freshName("utf8StringLiteral"), builtins.StringType);
- Binding { k =>
+ shift { k =>
LiteralUTF8String(literal_binding, javastring.getBytes("utf-8"), k(literal_binding))
}
@@ -402,7 +407,7 @@ object Transformer {
val variable = Variable(freshName("pureApp"), transform(tpe.result))
transform(vargs, Nil).flatMap { (values, blocks) =>
- Binding { k =>
+ shift { k =>
ForeignCall(variable, transform(blockName), values ++ blocks, k(variable))
}
}
@@ -412,24 +417,26 @@ object Transformer {
val variable = Variable(freshName("pureApp"), transform(tpe.result))
transform(vargs, bargs).flatMap { (values, blocks) =>
- Binding { k =>
+ shift { k =>
ForeignCall(variable, transform(blockName), values ++ blocks, k(variable))
}
}
- case core.Make(data, constructor, vargs) =>
+ 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) =>
- Binding { k =>
+ shift { k =>
Construct(variable, tag, values ++ blocks, k(variable))
}
}
case core.Box(block, annot) =>
transformBlockArg(block).flatMap { unboxed =>
- Binding { k =>
+ shift { k =>
val boxed = Variable(freshName(unboxed.name), Type.Positive())
ForeignCall(boxed, "box", List(unboxed), k(boxed))
}
@@ -486,8 +493,9 @@ object Transformer {
case core.BlockType.Interface(symbol, targs) => Negative()
}
- def transformLabel(id: Id)(using BPC: BlocksParamsContext): Label = getDefinition(id) match {
+ 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 =
@@ -512,6 +520,9 @@ object Transformer {
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 => ()
}
@@ -554,18 +565,28 @@ object Transformer {
def getBlockInfo(id: Id)(using BPC: BlocksParamsContext): BlockInfo =
BPC.info.getOrElse(id, sys error s"No block info for ${util.show(id)}")
- def getDefinition(id: Id)(using BPC: BlocksParamsContext): BlockInfo.Definition = getBlockInfo(id) match {
- case d : BlockInfo.Definition => d
- case BlockInfo.Parameter(tpe) => sys error s"Expected a function getDefinition, but got a block parameter: ${id}"
- }
+ def shift[A](body: (A => Statement) => Statement): Binding[A] =
+ Binding { k => Trampoline.Done(body { x => trampoline(k(x)) }) }
- case class Binding[A](run: (A => Statement) => Statement) {
+ case class Binding[A](body: (A => Trampoline[Statement]) => Trampoline[Statement]) {
def flatMap[B](rest: A => Binding[B]): Binding[B] = {
- Binding(k => run(a => rest(a).run(k)))
+ 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)
diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala
index 194b551e2..dd4985350 100644
--- a/effekt/shared/src/main/scala/effekt/source/Tree.scala
+++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala
@@ -4,6 +4,8 @@ package source
import effekt.context.Context
import effekt.symbols.Symbol
+import scala.annotation.tailrec
+
/**
* Data type representing source program trees.
*
@@ -125,6 +127,7 @@ enum FeatureFlag extends Tree {
}
object FeatureFlag {
extension (self: List[ExternBody]) {
+ @tailrec
def supportedByFeatureFlags(names: List[String]): Boolean = names match {
case Nil => false
case name :: other =>
@@ -382,7 +385,7 @@ enum Term extends Tree {
// Control Flow
case If(guards: List[MatchGuard], thn: Stmt, els: Stmt)
case While(guards: List[MatchGuard], block: Stmt, default: Option[Stmt])
- case Match(scrutinee: Term, clauses: List[MatchClause], default: Option[Stmt])
+ case Match(scrutinees: List[Term], clauses: List[MatchClause], default: Option[Stmt])
/**
* Handling effects
@@ -504,6 +507,15 @@ enum MatchPattern extends Tree {
* 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.*
@@ -637,7 +649,7 @@ object Named {
// CallLike
case Do => symbols.Operation
case Select => symbols.Field
- case MethodCall => symbols.Operation | symbols.CallTarget
+ case MethodCall => symbols.Operation | symbols.CallTarget | symbols.BlockParam
case IdTarget => symbols.TermSymbol
// Others
diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala
index d871c3539..ae7dad515 100644
--- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala
+++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala
@@ -4,6 +4,7 @@ package symbols
import effekt.source.IdRef
import effekt.util.messages.ErrorReporter
+import scala.annotation.tailrec
import scala.collection.mutable
/**
@@ -100,6 +101,7 @@ object scopes {
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)
@@ -192,6 +194,13 @@ object scopes {
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}") }
diff --git a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala
index ac659534b..9b2e64966 100644
--- a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala
+++ b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala
@@ -38,7 +38,7 @@ trait Symbol {
case _ => false
}
- def show: String = name.toString + id
+ def show: String = s"${name}_${id}"
override def toString: String = name.toString
}
diff --git a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala
index 627df9997..96a14bfa3 100644
--- a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala
+++ b/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala
@@ -66,13 +66,17 @@ object TypePrinter extends ParenPrettyPrinter {
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 = toDoc(result)
+ 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
diff --git a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala
index d3b33b553..c6e2d3813 100644
--- a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala
+++ b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala
@@ -53,6 +53,9 @@ object builtins {
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))
+
object TState {
val S: TypeParam = TypeParam(Name.local("S"))
val interface: Interface = Interface(Name.local("Ref"), List(S), Nil)
@@ -86,23 +89,16 @@ object builtins {
"Region" -> RegionSymbol
)
- lazy val globalRegion = ExternResource(name("global"), TRegion)
-
- val rootTerms: Map[String, TermSymbol] = Map(
- "global" -> globalRegion
- )
-
val rootCaptures: Map[String, Capture] = Map(
"io" -> IOCapability.capture,
"async" -> AsyncCapability.capture,
- "global" -> globalRegion.capture
+ "global" -> GlobalCapability.capture
)
// captures which are allowed on the toplevel
- val toplevelCaptures: CaptureSet = CaptureSet() // CaptureSet(IOCapability.capture, globalRegion.capture)
+ val toplevelCaptures: CaptureSet = CaptureSet() // CaptureSet(IOCapability.capture, GlobalCapability.capture)
lazy val rootBindings: Bindings =
- Bindings(rootTerms.map { case (k, v) => (k, Set(v)) }, rootTypes, rootCaptures,
- Map("effekt" -> Bindings(rootTerms.map { case (k, v) => (k, Set(v)) }, rootTypes, rootCaptures, Map.empty)))
+ Bindings(Map.empty, rootTypes, rootCaptures, Map("effekt" -> Bindings(Map.empty, rootTypes, rootCaptures, Map.empty)))
}
diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala
index b7feb47b8..5a335df84 100644
--- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala
+++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala
@@ -211,8 +211,8 @@ case class CallTarget(symbols: List[Set[BlockSymbol]]) extends BlockSymbol { val
* 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 + "_" + Symbol.fresh.next()) }
-case class TmpBlock(hint: String = "tmp") extends BlockSymbol { val name = Name.local("b_" + hint + "_" + Symbol.fresh.next()) }
+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
diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala
index 7b0a47daf..f2c4b5e2e 100644
--- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala
@@ -68,8 +68,8 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] {
case While(guards, body, default) =>
While(guards.map(rewrite), rewrite(body), default.map(rewrite))
- case Match(sc, clauses, default) =>
- Match(rewriteAsExpr(sc), clauses.map(rewrite), 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)
@@ -87,20 +87,14 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] {
val vargsTransformed = vargs.map(rewriteAsExpr)
val bargsTransformed = bargs.map(rewriteAsBlock)
- val syms = m.definition match {
+ val hasMethods = m.definition match {
// an overloaded call target
- case symbols.CallTarget(syms) => syms.flatten
- case s => C.panic(s"Not a valid method or function: ${id.name}")
- }
-
- val (funs, methods) = syms.partitionMap {
- case t: symbols.Operation => Right(t)
- case t: symbols.Callable => Left(t)
- case t => C.abort(pp"Not a valid method or function: ${t}")
+ case symbols.CallTarget(syms) => syms.flatten.exists(b => b.isInstanceOf[symbols.Operation])
+ case s => false
}
// we prefer methods over uniform call syntax
- if (methods.nonEmpty) {
+ if (hasMethods) {
MethodCall(rewriteAsBlock(receiver), id, targs, vargsTransformed, bargsTransformed)
} else {
Call(IdTarget(id).inheritPosition(id), targs, rewriteAsExpr(receiver) :: vargsTransformed, bargsTransformed)
diff --git a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala b/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala
index 0ce1165e8..f68e66f04 100644
--- a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala
@@ -52,6 +52,10 @@ object ConcreteEffects {
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
diff --git a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala b/effekt/shared/src/main/scala/effekt/typer/Constraints.scala
index a3425938e..2764d3b3f 100644
--- a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/Constraints.scala
@@ -121,16 +121,25 @@ class Constraints(
* 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 =
- val types = classes.flatMap[TypeVar, ValueType] { case (k, v) => typeSubstitution.get(v).map { k -> _ } }
+ 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(types, captures)
+ Substitutions(_typeSubstitution, captures)
/**
* Should only be called on unification variables where we do not know any types, yet
@@ -308,16 +317,18 @@ class Constraints(
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)
@@ -462,6 +473,7 @@ class Constraints(
*/
private def updateSubstitution(): Unit =
val substitution = subst
+ invalidate()
typeSubstitution = typeSubstitution.map { case (node, tpe) => node -> substitution.substitute(tpe) }
private def getNode(x: UnificationVar): Node =
diff --git a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala b/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala
index 06f8422f3..f7faeaec1 100644
--- a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala
@@ -92,20 +92,28 @@ object ExhaustivityChecker {
// Scrutinees are identified by tracing from the original scrutinee.
enum Trace {
- case Root(scrutinee: source.Term)
+ case Root(scrutinees: source.Term)
case Child(c: Constructor, field: Field, outer: Trace)
}
- def preprocess(root: source.Term, cl: source.MatchClause)(using Context): Clause = cl match {
- case source.MatchClause(pattern, guards, body) =>
+ 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) =>
@@ -121,7 +129,7 @@ object ExhaustivityChecker {
* - 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]) {
+ class Exhaustivity(allClauses: List[source.MatchClause], originalScrutinees: List[source.Term]) {
// Redundancy Information
// ----------------------
@@ -152,7 +160,8 @@ object ExhaustivityChecker {
def reportNonExhaustive()(using C: ErrorReporter): Unit = {
@tailrec
def traceToCase(at: Trace, acc: String): String = at match {
- case Trace.Root(_) => acc
+ 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)
@@ -191,13 +200,23 @@ object ExhaustivityChecker {
}
}
- def checkExhaustive(scrutinee: source.Term, cls: List[source.MatchClause])(using C: Context): Unit = {
- val initialClauses: List[Clause] = cls.map(preprocess(scrutinee, _))
- given E: Exhaustivity = new Exhaustivity(cls)
- checkScrutinee(Trace.Root(scrutinee), Context.inferredTypeOf(scrutinee), initialClauses)
+ 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 = {
diff --git a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala b/effekt/shared/src/main/scala/effekt/typer/Substitution.scala
index 610e935d5..a736de9de 100644
--- a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/Substitution.scala
@@ -31,12 +31,6 @@ case class Substitutions(
}
def get(x: CaptUnificationVar): Option[Captures] = captures.get(x)
- // amounts to first substituting this, then other
- def updateWith(other: Substitutions): Substitutions =
- Substitutions(
- values.view.mapValues { t => other.substitute(t) }.toMap,
- captures.view.mapValues { t => other.substitute(t) }.toMap) ++ other
-
// amounts to parallel substitution
def ++(other: Substitutions): Substitutions = Substitutions(values ++ other.values, captures ++ other.captures)
@@ -95,4 +89,4 @@ object Substitutions {
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)
-}
\ No newline at end of file
+}
diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala
index ecfd7330b..3ab2440b6 100644
--- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala
+++ b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala
@@ -161,12 +161,17 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext]
pp"The return type ${tpe} of the region body is not allowed to refer to region ${reg.capture}."
})
- case tree @ source.Match(scrutinee, clauses, default) => Context.at(tree) {
+ case tree @ source.Match(scrutinees, clauses, default) => Context.at(tree) {
// TODO copy annotations from default to synthesized defaultClause (in particular positions)
- val defaultClause = default.toList.map(body => source.MatchClause(source.IgnorePattern(), Nil, body))
- ExhaustivityChecker.checkExhaustive(scrutinee, clauses ++ defaultClause)
+ 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)
- query(scrutinee)
+ scrutinees foreach { query }
clauses foreach { query }
default foreach query
@@ -310,6 +315,10 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext]
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
diff --git a/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala
index bfa3051fb..97750d950 100644
--- a/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala
+++ b/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala
@@ -16,7 +16,7 @@ object PrettyPrinter extends ParenPrettyPrinter {
def format(value: Any): Document = pretty(toDoc(value), 80)
def toDoc(value: Any): Doc = value match {
- case sym: effekt.symbols.Symbol => string(sym.name.name + "_" + sym.id.toString)
+ case sym: effekt.symbols.Symbol => string(sym.show)
case Nil => "Nil"
case l: List[a] => "List" <> parens(l.map(toDoc))
case l: Map[a, b] => "Map" <> parens(l.map {
diff --git a/effekt/shared/src/main/scala/effekt/util/Structural.scala b/effekt/shared/src/main/scala/effekt/util/Structural.scala
index 5ca31b7a8..a87330e40 100644
--- a/effekt/shared/src/main/scala/effekt/util/Structural.scala
+++ b/effekt/shared/src/main/scala/effekt/util/Structural.scala
@@ -1,6 +1,7 @@
package effekt
package util
+import scala.annotation.tailrec
import scala.quoted.*
/**
@@ -128,6 +129,7 @@ class StructuralMacro[Self: Type, Q <: Quotes](debug: Boolean)(using val q: Q) {
val self = TypeRepr.of[Self].typeSymbol
val rewriteMethod =
+ @tailrec
def findMethod(sym: Symbol): Symbol =
if (sym.isDefDef && !sym.isAnonymousFunction) sym else findMethod(sym.owner)
@@ -145,6 +147,7 @@ class StructuralMacro[Self: Type, Q <: Quotes](debug: Boolean)(using val q: Q) {
val rewrites: List[RewriteMethod] = self.methodMember(rewriteName).map { m =>
TypeRepr.of[Self].memberType(m) match {
case tpe: MethodType =>
+ @tailrec
def findResult(tpe: TypeRepr): TypeRepr = tpe match {
case tpe: LambdaType => findResult(tpe.resType)
case _ => tpe
diff --git a/examples/benchmarks/are_we_fast_yet/permute.effekt b/examples/benchmarks/are_we_fast_yet/permute.effekt
index 4741322dc..04214d496 100644
--- a/examples/benchmarks/are_we_fast_yet/permute.effekt
+++ b/examples/benchmarks/are_we_fast_yet/permute.effekt
@@ -1,5 +1,6 @@
import examples/benchmarks/runner
+import ref
import array
def swap(arr: Array[Int], i: Int, j: Int) = {
@@ -9,10 +10,10 @@ def swap(arr: Array[Int], i: Int, j: Int) = {
}
def run(n: Int) = {
- var count in global = 0;
+ val count = ref(0);
def permute(arr: Array[Int], n: Int): Unit = {
- count = count + 1;
+ count.set(count.get + 1);
if (n != 0) {
val n1 = n - 1;
permute(arr, n1);
@@ -28,7 +29,7 @@ def run(n: Int) = {
array(n, 1).permute(n);
- count
+ count.get
}
def main() = benchmark(6){run}
diff --git a/examples/benchmarks/are_we_fast_yet/storage.effekt b/examples/benchmarks/are_we_fast_yet/storage.effekt
index 17ab62fb2..315a83db7 100644
--- a/examples/benchmarks/are_we_fast_yet/storage.effekt
+++ b/examples/benchmarks/are_we_fast_yet/storage.effekt
@@ -1,5 +1,6 @@
import examples/benchmarks/runner
+import ref
import array
type Tree {
@@ -25,10 +26,10 @@ def withRandom[R]{ program: { Random } => R }: R = {
}
def run(n: Int) = {
- var count in global = 0;
+ val count = ref(0);
def buildTreeDepth(depth: Int) { rand: Random }: Tree = {
- count = count + 1;
+ count.set(count.get + 1);
if (depth == 1) {
Leaf(allocate(mod(rand.next, 10) + 1))
} else {
@@ -44,7 +45,7 @@ def run(n: Int) = {
withRandom { { rand: Random } => buildTreeDepth(7) {rand}; () }
}
- count
+ count.get
}
def main() = benchmark(1){run}
diff --git a/examples/benchmarks/other/binarysearch.check b/examples/benchmarks/other/binarysearch.check
new file mode 100644
index 000000000..cccad55fc
--- /dev/null
+++ b/examples/benchmarks/other/binarysearch.check
@@ -0,0 +1,6 @@
+sqrt of 150 is between:
+12 (^2 = 144)
+13 (^2 = 169)
+sqrt of 9876543210123 is between:
+3142696 (^2 = 9876538148416)
+3142697 (^2 = 9876544433809)
diff --git a/examples/benchmarks/other/binarysearch.effekt b/examples/benchmarks/other/binarysearch.effekt
new file mode 100644
index 000000000..53922e60f
--- /dev/null
+++ b/examples/benchmarks/other/binarysearch.effekt
@@ -0,0 +1,49 @@
+module binarysearch
+
+// Effectful binary search
+// ... inspired by Jules Jacobs https://julesjacobs.com/notes/binarysearch/binarysearch.pdf
+// ... ... and Brent Yorgey https://byorgey.wordpress.com/2023/01/01/competitive-programming-in-haskell-better-binary-search/
+
+effect breakWith[A](value: A): Nothing
+effect mid[A](l: A, r: A): A / breakWith[(A, A)]
+
+def break2[A, B](x: A, y: B) =
+ do breakWith((x, y))
+def boundary[A] { prog: => A / breakWith[A] }: A =
+ try prog() with breakWith[A] { a => a }
+
+def search[A](l: A, r: A) { predicate: A => Bool }: (A, A) / mid[A] = boundary[(A, A)] {
+ def go(l: A, r: A): (A, A) = {
+ val m = do mid(l, r)
+ if (predicate(m)) {
+ go(l, m)
+ } else {
+ go(m, r)
+ }
+ }
+ go(l, r)
+}
+
+def binary[R] { prog: => R / mid[Int] }: R =
+ try prog() with mid[Int] { (l, r) =>
+ resume {
+ if ((r - l) > 1) {
+ (l + r) / 2
+ } else {
+ break2(l, r)
+ }
+ }
+ }
+
+def main() = binary {
+ def findSqrtUpTo(pow2: Int, max: Int) = {
+ val (l, r) = search(0, max) { x => x * x >= pow2 }
+ println("sqrt of " ++ pow2.show ++ " is between:")
+ println(l.show ++ " (^2 = " ++ (l * l).show ++ ")")
+ println(r.show ++ " (^2 = " ++ (r * r).show ++ ")")
+ }
+
+ // Comment out the first call below to get much better JS/Core codegen:
+ findSqrtUpTo(150, 100)
+ findSqrtUpTo(9876543210123, 9000000)
+}
diff --git a/examples/benchmarks/other/nbe.check b/examples/benchmarks/other/nbe.check
new file mode 100644
index 000000000..6c2239814
--- /dev/null
+++ b/examples/benchmarks/other/nbe.check
@@ -0,0 +1,10 @@
+S ~> S:
+λ1.λ2.λ3.((1 3) (2 3)) ~> λ1.λ2.λ3.((1 3) (2 3))
+(ι (ι (ι (ι ι)))) ~> S:
+(λ1.((1 λ2.λ3.λ4.((2 4) (3 4))) λ5.λ6.5) (λ7.((7 λ8.λ9.λ10.((8 10) (9 10))) λ11.λ12.11) (λ13.((13 λ14.λ15.λ16.((14 16) (15 16))) λ17.λ18.17) (λ19.((19 λ20.λ21.λ22.((20 22) (21 22))) λ23.λ24.23) λ25.((25 λ26.λ27.λ28.((26 28) (27 28))) λ29.λ30.29))))) ~> λ1.λ2.λ3.((1 3) (2 3))
+(2^3)%3 == 2 // using Church numerals:
+(((λ1.λ2.λ3.(((3 λ4.λ5.(4 λ6.((5 λ7.λ8.(7 ((6 7) 8))) 6))) (λ9.λ10.(10 9) λ11.λ12.12)) λ13.(((2 1) (((3 λ14.λ15.λ16.(14 λ17.((15 17) 16))) λ18.18) λ19.λ20.(20 19))) (((3 λ21.λ22.21) λ23.23) λ24.24))) λ25.λ26.(25 (25 26))) λ27.λ28.(27 (27 (27 28)))) λ29.λ30.(29 (29 (29 30)))) ~> λ1.λ2.(1 (1 2))
+5! == 120 // using Church numerals:
+(λ1.λ2.(((1 λ3.λ4.(4 (3 λ5.λ6.((4 5) (5 6))))) λ7.2) λ8.8) λ9.λ10.(9 (9 (9 (9 (9 10)))))) ~> λ1.λ2.(1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 2))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
+(prime? 7) == true // using Church numerals and Wilson's Theorem:
+(λ1.(λ2.((2 λ3.λ4.λ5.5) λ6.λ7.6) ((λ8.λ9.λ10.λ11.(((8 (λ12.λ13.(13 12) λ14.λ15.14)) ((8 λ16.(((9 λ17.λ18.λ19.((19 (17 (10 18))) 18)) λ20.16) 11)) λ21.11)) λ22.λ23.23) (λ24.λ25.λ26.(25 ((24 25) 26)) (λ27.λ28.(((27 λ29.λ30.(30 (29 λ31.λ32.((30 31) (31 32))))) λ33.28) λ34.34) (λ35.λ36.λ37.((λ38.λ39.(39 38) λ40.40) ((35 λ41.(λ42.λ43.(43 42) (41 36))) λ44.37)) 1)))) 1)) λ45.λ46.(45 (45 (45 (45 (45 (45 (45 46)))))))) ~> λ1.λ2.1
diff --git a/examples/benchmarks/other/nbe.effekt b/examples/benchmarks/other/nbe.effekt
new file mode 100644
index 000000000..64304ea52
--- /dev/null
+++ b/examples/benchmarks/other/nbe.effekt
@@ -0,0 +1,151 @@
+/// Demo of an effectful Normalization by Evaluation (NbE) implementation for the pure, untyped lambda calculus.
+/// Uses the Call-by-Value reduction order, same as host.
+
+import io
+import map
+import stream
+import process
+import stringbuffer
+
+type Name = Int
+
+effect fresh(): Name
+effect lookup(n: Name): Int
+
+/// lambda term
+type Term {
+ Abs(n: Name, t: Term)
+ App(a: Term, b: Term)
+ Var(n: Name)
+}
+
+/// lambda term in normal domain
+type Value {
+ VNeu(neu: Neutral)
+ VClo(n: Name, t: Term, env: Map[Name, Value])
+}
+
+/// lambda term in neutral domain (not yet reducible)
+type Neutral {
+ NVar(n: Name)
+ NApp(a: Neutral, b: Value)
+}
+
+/// evaluate a single term without going into abstractions
+def eval(env: Map[Name, Value], t: Term): Value = t match {
+ case Abs(n, t) => VClo(n, t, env)
+ case App(a, b) => apply(eval(env, a), eval(env, b))
+ case Var(n) => env.getOrElse(n) { VNeu(NVar(n)) }
+}
+
+/// apply terms in their normal domain
+/// this does the actual substitution via environment lookup
+def apply(a: Value, b: Value): Value = a match {
+ case VNeu(neu) => VNeu(NApp(neu, b))
+ case VClo(n, t, env) => eval(env.put(n, b), t)
+}
+
+/// reflect variable name to the neutral domain
+def reflect(n: Name): Value = VNeu(NVar(n))
+
+/// convert terms to their normal form (in term domain)
+def reify(v: Value): Term / fresh = v match {
+ case VNeu(NVar(n)) => Var(n)
+ case VNeu(NApp(a, b)) => App(reify(VNeu(a)), reify(b))
+ case _ => {
+ val n = do fresh()
+ Abs(n, reify(apply(v, reflect(n))))
+ }
+}
+
+/// strong normalization of the term
+def normalize(t: Term): Term = {
+ var i = 0
+ try reify(eval(empty[Name, Value](compareInt), t))
+ with fresh { i = i + 1; resume(i) }
+}
+
+/// parse named term from BLC stream (de Bruijn)
+def parse(): Term / { read[Bool], stop } = {
+ def go(): Term / { fresh, lookup } =
+ (do read[Bool](), do read[Bool]) match {
+ case (false, false) => {
+ val n = do fresh()
+ Abs(n, try go() with lookup { i =>
+ resume(if (i == 0) n else do lookup(i - 1))
+ })
+ }
+ case (false, true ) => App(go(), go())
+ case (true , false) => Var(do lookup(0))
+ case (true , true ) => {
+ var i = 1
+ while (do read[Bool]()) i = i + 1
+ Var(do lookup(i))
+ }
+ }
+
+ var i = 0
+ try go()
+ with fresh { i = i + 1; resume(i) }
+ with lookup { n =>
+ println("error: free variable " ++ n.show)
+ exit(1)
+ }
+}
+
+/// helper function for pretty string interpolation of terms
+def pretty { s: () => Unit / { literal, splice[Term] } }: String = {
+ with stringBuffer
+
+ try { s(); do flush() }
+ with literal { l => resume(do write(l)) }
+ with splice[Term] { t =>
+ t match {
+ case Abs(n, t) => do write("λ" ++ n.show ++ pretty".${t}")
+ case App(a, b) => do write(pretty"(${a} ${b})")
+ case Var(v) => do write(v.show)
+ }
+ resume(())
+ }
+}
+
+/// convert char stream to bit stream, skipping non-bit chars
+def bits { p: () => Unit / { read[Bool], stop } }: Unit / read[Char] =
+ try exhaustively { p() }
+ with read[Bool] {
+ with exhaustively
+ val c = do read[Char]()
+ if (c == '0') resume { false }
+ if (c == '1') resume { true }
+ }
+
+/// evaluate the input BLC string and prettify it
+def testNormalization(input: String) = {
+ with feed(input)
+ with bits
+ val t = parse()
+ println(pretty"${t} ~> ${normalize(t)}")
+}
+
+def main() = {
+ var t = "00000001011110100111010"
+ println("S ~> S:")
+ testNormalization(t)
+
+ t = "010001011000000001011110100111010000011001000101100000000101111010011101000001100100010110000000010111101001110100000110010001011000000001011110100111010000011000010110000000010111101001110100000110"
+ println("(ι (ι (ι (ι ι)))) ~> S:")
+ testNormalization(t)
+
+ t = "010101000000010101100000011100001011100000011100101111011010100100000110110000010000101011110111100101011100000000111100001011110101100010000001101100101011100000110001000100000011100111010000001110011100111010000001110011100111010"
+ println("(2^3)%3 == 2 // using Church numerals:")
+ testNormalization(t)
+
+ // TODO: is there an off-by-one in the string buffer?
+ t = "010000010101110000001100111000000101111011001110100011000100000011100111001110011100111010"
+ println("5! == 120 // using Church numerals:")
+ testNormalization(t)
+
+ t = "010001000101100000001000001100101000000000101011111001000001101100000110010111110000101011111000000001011001111001111111011011000110110001100000100100000001110010111101101001000001010111000000110011100000010111101100111010001100010010000000101000001101100010010111100001000001101100110111000110101000000111001110011100111001110011100111010"
+ println("(prime? 7) == true // using Church numerals and Wilson's Theorem:")
+ testNormalization(t)
+}
diff --git a/examples/casestudies/naturalisticdsls.effekt.md b/examples/casestudies/naturalisticdsls.effekt.md
index 8bccf749e..3fd0bae90 100644
--- a/examples/casestudies/naturalisticdsls.effekt.md
+++ b/examples/casestudies/naturalisticdsls.effekt.md
@@ -234,11 +234,11 @@ def main() = {
}
```
-[@hudak96building]: https://dl.acm.org/doi/10.1145/242224.242477
-[@lopes2003beyond]: https://dl.acm.org/doi/abs/10.1145/966051.966058
+[@hudak96building]: https://doi.org/10.1145/242224.242477
+[@lopes2003beyond]: https://doi.org/10.1145/966051.966058
[@marvsik2016introducing]: https://hal.archives-ouvertes.fr/hal-01079206/
-[@erdweg11sugarj]: https://dl.acm.org/doi/abs/10.1145/2048066.2048099
-[@shan2005linguistic]: http://homes.sice.indiana.edu/ccshan/dissertation/book.pdf
-[@shan2004delimited]: https://arxiv.org/abs/cs/0404006
-[@barker2004continuations]: https://www.nyu.edu/projects/barker/barker-cw.pdf
-[@yallop2017staged]: https://dl.acm.org/doi/abs/10.1145/2847538.2847546
\ No newline at end of file
+[@erdweg11sugarj]: https://doi.org/10.1145/2048066.2048099
+[@shan2005linguistic]: https://homes.sice.indiana.edu/ccshan/dissertation/book.pdf
+[@shan2004delimited]: https://doi.org/10.48550/arXiv.cs/0404006
+[@barker2014continuations]: https://archive.org/details/continuationsnat0000bark/
+[@yallop2017staged]: https://doi.org/10.1145/2847538.2847546
diff --git a/examples/neg/cyclic_a.check b/examples/neg/cyclic_a.check
new file mode 100644
index 000000000..c3839982d
--- /dev/null
+++ b/examples/neg/cyclic_a.check
@@ -0,0 +1,10 @@
+[error] ./examples/neg/cyclic_a.effekt:3:1: Cyclic import: cyclic_a depends on itself, via:
+ cyclic_a -> cyclic_b -> cyclic_a
+import examples/neg/cyclic_b
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[error] ./examples/neg/cyclic_a.effekt:3:1: Cannot compile dependency: ./examples/neg/cyclic_a
+import examples/neg/cyclic_b
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[error] ./examples/neg/cyclic_a.effekt:3:1: Cannot compile dependency: ./examples/neg/cyclic_b
+import examples/neg/cyclic_b
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/examples/neg/cyclic_a.effekt b/examples/neg/cyclic_a.effekt
new file mode 100644
index 000000000..89158d000
--- /dev/null
+++ b/examples/neg/cyclic_a.effekt
@@ -0,0 +1,5 @@
+module cyclic_a
+
+import examples/neg/cyclic_b
+
+def main() = ()
diff --git a/examples/neg/cyclic_b.effekt b/examples/neg/cyclic_b.effekt
new file mode 100644
index 000000000..0d6287d6e
--- /dev/null
+++ b/examples/neg/cyclic_b.effekt
@@ -0,0 +1,5 @@
+module cyclic_b
+
+import examples/neg/cyclic_a
+
+def main() = ()
diff --git a/examples/neg/issue919.effekt b/examples/neg/issue919.effekt
new file mode 100644
index 000000000..1943f2d38
--- /dev/null
+++ b/examples/neg/issue919.effekt
@@ -0,0 +1,11 @@
+effect tick(): Unit
+
+def main() = {
+ region r {
+ var k in r = box { () } // ERROR tick escapes
+ try {
+ k = box { do tick() }
+ } with tick { () => () }
+ k()
+ }
+}
\ No newline at end of file
diff --git a/examples/neg/lambda_case_exhaustivity.effekt b/examples/neg/lambda_case_exhaustivity.effekt
new file mode 100644
index 000000000..13dc5ddc6
--- /dev/null
+++ b/examples/neg/lambda_case_exhaustivity.effekt
@@ -0,0 +1,7 @@
+def foo[A, B](a: A){ body: A => B }: B = body(a)
+
+def main() = {
+ foo(true){ // ERROR Non-exhaustive
+ case false => ()
+ }
+}
\ No newline at end of file
diff --git a/examples/neg/lambdas/inference.check b/examples/neg/lambdas/inference.check
index e0186fc8a..ea7eebe7e 100644
--- a/examples/neg/lambdas/inference.check
+++ b/examples/neg/lambdas/inference.check
@@ -1,22 +1,24 @@
-[error] Expected type
- Int => Bool at {} => String
+[error] examples/neg/lambdas/inference.effekt:13:8: Expected type
+ (Int => Bool at {}) => String
but got type
- Int => Unit at {} => String
+ (Int => Unit at {}) => String
Type mismatch between Bool and Unit.
comparing the argument types of
- Int => Unit at {} => String (given)
- Int => Bool at {} => String (expected)
+ (Int => Unit at {}) => String (given)
+ (Int => Bool at {}) => String (expected)
when comparing the return type of the function.
+ hof2(fun(f: (Int) => Unit at {}) { "" })
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] examples/neg/lambdas/inference.effekt:13:8: Expected type
- Int => Bool at {} => String at {}
+ (Int => Bool at {}) => String at {}
but got type
- Int => Unit at {} => String at ?C
+ (Int => Unit at {}) => String at ?C
Type mismatch between Bool and Unit.
comparing the argument types of
- Int => Unit at {} => String (given)
- Int => Bool at {} => String (expected)
+ (Int => Unit at {}) => String (given)
+ (Int => Bool at {}) => String (expected)
when comparing the return type of the function.
hof2(fun(f: (Int) => Unit at {}) { "" })
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/examples/neg/multi_arity_match_arity.effekt b/examples/neg/multi_arity_match_arity.effekt
new file mode 100644
index 000000000..1f741b393
--- /dev/null
+++ b/examples/neg/multi_arity_match_arity.effekt
@@ -0,0 +1,10 @@
+def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = {
+ fn(a, b)
+}
+
+def main() = {
+ foo(1, 2){
+ case _, _ => ()
+ case _, _, _ => () // ERROR Number of patterns
+ }
+}
\ No newline at end of file
diff --git a/examples/neg/multi_arity_match_exhaustivity.effekt b/examples/neg/multi_arity_match_exhaustivity.effekt
new file mode 100644
index 000000000..e3aa96836
--- /dev/null
+++ b/examples/neg/multi_arity_match_exhaustivity.effekt
@@ -0,0 +1,9 @@
+def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = {
+ fn(a, b)
+}
+
+def main() = {
+ foo(1, false){ // ERROR Non-exhaustive
+ case _, true => ()
+ }
+}
\ No newline at end of file
diff --git a/examples/neg/namer/issue950/ambiguous_interfaces_overload.check b/examples/neg/namer/issue950/ambiguous_interfaces_overload.check
new file mode 100644
index 000000000..eec95e131
--- /dev/null
+++ b/examples/neg/namer/issue950/ambiguous_interfaces_overload.check
@@ -0,0 +1,9 @@
+[info] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: There is an equally named effect operation sayHello of interface Greet. Use syntax `do sayHello()` to call it.
+ sayHello()
+ ^^^^^^^^
+[info] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: There is an equally named effect operation sayHello of interface Welcome. Use syntax `do sayHello()` to call it.
+ sayHello()
+ ^^^^^^^^
+[error] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: Cannot find a function named `sayHello`.
+ sayHello()
+ ^^^^^^^^
\ No newline at end of file
diff --git a/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt b/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt
new file mode 100644
index 000000000..2aeb884c4
--- /dev/null
+++ b/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt
@@ -0,0 +1,12 @@
+interface Greet { def sayHello(): Unit }
+interface Welcome { def sayHello(): Unit }
+
+def helloWorld() = try {
+ sayHello()
+} with Greet {
+ def sayHello() = { println("Hello from Greet!"); resume(()) }
+}
+
+def main() = {
+ helloWorld()
+}
\ No newline at end of file
diff --git a/examples/neg/namer/issue950/cannot_find_function.effekt b/examples/neg/namer/issue950/cannot_find_function.effekt
new file mode 100644
index 000000000..7ac08fbc1
--- /dev/null
+++ b/examples/neg/namer/issue950/cannot_find_function.effekt
@@ -0,0 +1 @@
+def main() = doesNotExist() // ERROR Cannot find a function named `doesNotExist`.
diff --git a/examples/neg/namer/issue950/effect_operation_with_same_name.check b/examples/neg/namer/issue950/effect_operation_with_same_name.check
new file mode 100644
index 000000000..35c1f3ed6
--- /dev/null
+++ b/examples/neg/namer/issue950/effect_operation_with_same_name.check
@@ -0,0 +1,6 @@
+[info] examples/neg/namer/issue950/effect_operation_with_same_name.effekt:4:3: There is an equally named effect operation sayHello of interface Greet. Use syntax `do sayHello()` to call it.
+ sayHello()
+ ^^^^^^^^
+[error] examples/neg/namer/issue950/effect_operation_with_same_name.effekt:4:3: Cannot find a function named `sayHello`.
+ sayHello()
+ ^^^^^^^^
\ No newline at end of file
diff --git a/examples/neg/namer/issue950/effect_operation_with_same_name.effekt b/examples/neg/namer/issue950/effect_operation_with_same_name.effekt
new file mode 100644
index 000000000..78e615947
--- /dev/null
+++ b/examples/neg/namer/issue950/effect_operation_with_same_name.effekt
@@ -0,0 +1,11 @@
+interface Greet { def sayHello(): Unit }
+
+def helloWorld() = try {
+ sayHello()
+} with Greet {
+ def sayHello() = { println("Hello!"); resume(()) }
+}
+
+def main() = {
+ helloWorld()
+}
\ No newline at end of file
diff --git a/examples/neg/typer/infer-effectful-while.check b/examples/neg/typer/infer-effectful-while.check
new file mode 100644
index 000000000..ce30f309a
--- /dev/null
+++ b/examples/neg/typer/infer-effectful-while.check
@@ -0,0 +1,3 @@
+[error] examples/neg/typer/infer-effectful-while.effekt:14:1: Main cannot have effects, but includes effects: { Foo }
+def main() = {
+^
diff --git a/examples/neg/typer/infer-effectful-while.effekt b/examples/neg/typer/infer-effectful-while.effekt
new file mode 100644
index 000000000..44efff167
--- /dev/null
+++ b/examples/neg/typer/infer-effectful-while.effekt
@@ -0,0 +1,16 @@
+interface Foo { def foo(): Unit }
+
+def bar(): Bool / Foo = {
+ do foo()
+ false
+}
+
+def fooo() = {
+ while(bar() is true) {
+ ()
+ }
+}
+
+def main() = {
+ fooo()
+}
diff --git a/examples/pos/bidirectional/typeparametric.effekt b/examples/pos/bidirectional/typeparametric.effekt
index 61b1e9ed5..f9eaf2da9 100644
--- a/examples/pos/bidirectional/typeparametric.effekt
+++ b/examples/pos/bidirectional/typeparametric.effekt
@@ -3,6 +3,7 @@ extern interface Cap[U, V]
extern pure def cap[U, V](): Cap[U, V] at {} =
js "42"
chez "42"
+ llvm "ret %Pos undef"
interface Foo[S] {
def op[A]() {f: Cap[S, A]}: Cap[S, A] at {f} / { Exception[S] }
diff --git a/examples/pos/capture/selfregion.effekt b/examples/pos/capture/selfregion.effekt
index 165d574b5..b2e14bfb3 100644
--- a/examples/pos/capture/selfregion.effekt
+++ b/examples/pos/capture/selfregion.effekt
@@ -46,8 +46,8 @@ def ex5() =
def ex6() = {
- var x in global = 42;
- println(x + x)
+ val x = ref(42);
+ println(x.get + x.get)
}
def main() = {
diff --git a/examples/pos/int_ufcs.check b/examples/pos/int_ufcs.check
new file mode 100644
index 000000000..7f957ee0a
--- /dev/null
+++ b/examples/pos/int_ufcs.check
@@ -0,0 +1,2 @@
+1234567890123
+1234567890123
diff --git a/examples/pos/int_ufcs.effekt b/examples/pos/int_ufcs.effekt
new file mode 100644
index 000000000..74ac683d6
--- /dev/null
+++ b/examples/pos/int_ufcs.effekt
@@ -0,0 +1,5 @@
+def main() = {
+ 1234567890123.println
+ println(1234567890123)
+}
+
diff --git a/examples/pos/multi_arity_lambda_case.check b/examples/pos/multi_arity_lambda_case.check
new file mode 100644
index 000000000..62835ebbd
--- /dev/null
+++ b/examples/pos/multi_arity_lambda_case.check
@@ -0,0 +1,2 @@
+Case III
+OK
\ No newline at end of file
diff --git a/examples/pos/multi_arity_lambda_case.effekt b/examples/pos/multi_arity_lambda_case.effekt
new file mode 100644
index 000000000..c3d33d304
--- /dev/null
+++ b/examples/pos/multi_arity_lambda_case.effekt
@@ -0,0 +1,16 @@
+def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = {
+ fn(a, b)
+}
+
+def main() = {
+ foo(12, true){
+ case i, false => println("Case I")
+ case 0, true => println("Case II")
+ case _, true => println("Case III")
+ }
+ foo((1,2), [1,2,3]){
+ case (x, y), Cons(1,Cons(2,Cons(3,Nil()))) => println("OK")
+ case (_, _), _ => println("ERR")
+ }
+ ()
+}
\ No newline at end of file
diff --git a/examples/pos/string_interpolation.check b/examples/pos/string_interpolation.check
index 04bcd4f9e..bff0393aa 100644
--- a/examples/pos/string_interpolation.check
+++ b/examples/pos/string_interpolation.check
@@ -1,2 +1,3 @@
GET https://api.effekt-lang.org/users/effekt/resource/42
-Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x
\ No newline at end of file
+Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x
+12
diff --git a/examples/pos/string_interpolation.effekt b/examples/pos/string_interpolation.effekt
index 509b3fc9f..b5fe1e529 100644
--- a/examples/pos/string_interpolation.effekt
+++ b/examples/pos/string_interpolation.effekt
@@ -28,6 +28,15 @@ def pretty { prog: () => Unit / {literal, splice[Expr]} }: String = {
}
}
+def len { prog: () => Unit / {literal} }: Int = {
+ try {
+ prog()
+ 0
+ } with literal { s =>
+ s.length
+ }
+}
+
def main() = {
val domain = "https://api.effekt-lang.org"
val user = "effekt"
@@ -36,4 +45,6 @@ def main() = {
val fixpoint = Abs("f", App(Abs("x", App(Var("f"), App(Var("x"), Var("x")))), Abs("x", App(Var("f"), App(Var("x"), Var("x"))))))
println(pretty"Fix point combinator: ${fixpoint}")
+
+ println(show(len"hello, world"))
}
diff --git a/examples/pos/string_interpolation_literal.check b/examples/pos/string_interpolation_literal.check
new file mode 100644
index 000000000..8d842fe6e
--- /dev/null
+++ b/examples/pos/string_interpolation_literal.check
@@ -0,0 +1,5 @@
+42
+[WARNING] Frobnicators have been jabberwocked!
+-1
+[ERROR] Stuff went wrong!
+[ERROR] Aborting!
diff --git a/examples/pos/string_interpolation_literal.effekt b/examples/pos/string_interpolation_literal.effekt
new file mode 100644
index 000000000..978637456
--- /dev/null
+++ b/examples/pos/string_interpolation_literal.effekt
@@ -0,0 +1,21 @@
+effect log(msg: String): Unit
+
+def error { body: => Unit / literal }: Unit / log =
+ try body() with literal { x => do log("[ERROR] " ++ x); resume(()) }
+
+def warn { body: => Unit / literal }: Unit / log =
+ try body() with literal { x => do log("[WARNING] " ++ x); resume(()) }
+
+def doc { body: => Unit / {literal} }: Unit =
+ try body() with literal { _ => resume(()) }
+
+def main() = try {
+ doc"This is the doc string for my main function!"
+
+ println(42)
+ warn"Frobnicators have been jabberwocked!"
+
+ println(-1)
+ error"Stuff went wrong!"
+ error"Aborting!"
+} with log { msg => println(msg); resume(()) }
diff --git a/examples/pos/ufcs_blockparams.check b/examples/pos/ufcs_blockparams.check
new file mode 100644
index 000000000..27ba77dda
--- /dev/null
+++ b/examples/pos/ufcs_blockparams.check
@@ -0,0 +1 @@
+true
diff --git a/examples/pos/ufcs_blockparams.effekt b/examples/pos/ufcs_blockparams.effekt
new file mode 100644
index 000000000..34b3156af
--- /dev/null
+++ b/examples/pos/ufcs_blockparams.effekt
@@ -0,0 +1,6 @@
+def refl[A](a: A) { eq: (A, A) => Bool }: Bool =
+ a.eq(a) && eq(a, a)
+
+def main() = {
+ println(refl(42) { (x, y) => x == y })
+}
diff --git a/examples/stdlib/acme.effekt b/examples/stdlib/acme.effekt
index 7ae412dfa..cffcd9178 100644
--- a/examples/stdlib/acme.effekt
+++ b/examples/stdlib/acme.effekt
@@ -24,6 +24,7 @@ import map
import option
import process
import queue
+import random
import ref
import regex
import resizable_array
diff --git a/examples/stdlib/queue.check b/examples/stdlib/queue.check
new file mode 100644
index 000000000..e983f7fb2
--- /dev/null
+++ b/examples/stdlib/queue.check
@@ -0,0 +1,12 @@
+true
+false
+Some(3)
+Some(1)
+Some(2)
+Some(4)
+Some(5)
+Some(6)
+Some(7)
+Some(8)
+Some(9)
+None()
diff --git a/examples/stdlib/queue.effekt b/examples/stdlib/queue.effekt
new file mode 100644
index 000000000..a0069a115
--- /dev/null
+++ b/examples/stdlib/queue.effekt
@@ -0,0 +1,3 @@
+import queue
+
+def main() = queue::examples::main()
diff --git a/examples/stdlib/random.check b/examples/stdlib/random.check
new file mode 100644
index 000000000..cd10a502c
--- /dev/null
+++ b/examples/stdlib/random.check
@@ -0,0 +1,65 @@
+prng
+int32s:
+-1472445053
+-935901669
+-1244218020
+492812375
+-894738723
+1372722888
+-1723450959
+2005696606
+536774910
+2111603542
+int32s, part2:
+348632114
+-493311473
+521105902
+-441336655
+1315564179
+-245050234
+1432006216
+-2018660684
+349983049
+-1541851413
+1242068606
+-953174617
+728164170
+-558026150
+812040776
+-225070679
+125608749
+-1547184487
+2026319992
+-627925429
+doubles:
+0.009
+0.758
+0.769
+0.032
+0.15
+0.118
+0.03
+0.946
+0.049
+0.565
+randomInt:
+3 4 3
+343
+0 6 8
+68
+2 3 0
+230
+6 1 0
+610
+4 0 1
+401
+2 3 4
+234
+9 3 2
+932
+8 2 2
+822
+3 3 2
+332
+8 5 1
+851
diff --git a/examples/stdlib/random.effekt b/examples/stdlib/random.effekt
new file mode 100644
index 000000000..c75109486
--- /dev/null
+++ b/examples/stdlib/random.effekt
@@ -0,0 +1,3 @@
+import random
+
+def main() = random::examples::main()
diff --git a/examples/stdlib/stringbuffer.check b/examples/stdlib/stringbuffer.check
new file mode 100644
index 000000000..811f48d9a
--- /dev/null
+++ b/examples/stdlib/stringbuffer.check
@@ -0,0 +1,3 @@
+hello, world
+
+Effekt = Effekt
diff --git a/examples/stdlib/stringbuffer.effekt b/examples/stdlib/stringbuffer.effekt
new file mode 100644
index 000000000..f4572882a
--- /dev/null
+++ b/examples/stdlib/stringbuffer.effekt
@@ -0,0 +1,3 @@
+import stringbuffer
+
+def main() = stringbuffer::examples::main()
diff --git a/examples/stdlib/test/list_examples_pbt.check b/examples/stdlib/test/list_examples_pbt.check
new file mode 100644
index 000000000..615e5f0a9
--- /dev/null
+++ b/examples/stdlib/test/list_examples_pbt.check
@@ -0,0 +1,50 @@
+PBT: List Functions and Other Simple Examples
+✓ 1 + 1 == 2 [0.87ms]
+✕ 2 + 2 == 3 [0.14ms]
+ Expected: 3
+ Obtained: 4
+✓ reverse-singleton, passed 100 tests [3.16ms]
+✓ reverse-singleton, explicitely passing the generator, passed 100 tests [1.59ms]
+✕ reverse-singleton mistake [0.15ms]
+ ! Falsified after 0 passed tests: [0.15ms]
+ Expected: Cons(6, Nil())
+ Obtained: Cons(47, Nil())
+ failed on input:
+ 1. 47
+✓ Is the Integer 2 among the generated values? [0.40ms]
+ Verified after 18 tried inputs
+ example value:
+ 1. 2
+✓ reverse: distributivity over append, passed 100 tests [4.36ms]
+✕ reverse: distributivity mistake - swapped order [0.72ms]
+ ! Falsified after 0 passed tests: [0.72ms]
+ Expected: Cons(-45, Cons(43, Cons(15, Cons(0, Nil()))))
+ Obtained: Cons(15, Cons(0, Cons(-45, Cons(43, Nil()))))
+ failed on inputs:
+ 1. Cons(43, Cons(-45, Nil()))
+ 2. Cons(0, Cons(15, Nil()))
+✓ |zip(xs,ys)| === min(|xs|,|ys|), passed 10 tests [0.83ms]
+✕ intended mistake: |zip(xs,ys)| != max(|xs|,|ys|) [0.39ms]
+ ! Falsified after 0 passed tests: [0.39ms]
+ Expected: 5
+ Obtained: 2
+ failed on inputs:
+ 1. Cons(102, Cons(97, Cons(41, Cons(123, Cons(122, Nil())))))
+ 2. Cons(42, Cons(-46, Nil()))
+✓ unzip-zip-take relation, passed 10 tests [3.92ms]
+✓ drop: concatenation , passed 10 tests [1.94ms]
+✓ drop-slice relation, passed 10 tests [0.64ms]
+✓ reverseOnto-reverse-append relation , passed 10 tests [0.84ms]
+✓ size-foldLeft relation, passed 20 tests [1.14ms]
+✓ take-drop: inversion over concatenation, passed 20 tests [0.77ms]
+✕ even numbers are even and smaller than 4 [0.11ms]
+ ! Falsified after 1 passed test: [0.11ms]
+ Assertion failed
+ failed on input:
+ 1. 12
+
+ 12 pass
+ 5 fail
+ 17 test(s) total [41.5ms]
+
+false
\ No newline at end of file
diff --git a/examples/stdlib/test/list_examples_pbt.effekt b/examples/stdlib/test/list_examples_pbt.effekt
new file mode 100644
index 000000000..b02e263b0
--- /dev/null
+++ b/examples/stdlib/test/list_examples_pbt.effekt
@@ -0,0 +1,173 @@
+import test
+import bench
+import random
+
+def main() = {
+ with minstd(1337);
+
+ // Don't print out times in CI.
+ suite("PBT: List Functions and Other Simple Examples", false) {
+
+ // example unit tests to show that mixed test suites work
+ test("1 + 1 == 2") {
+ assertEqual(1 + 1, 2)
+ }
+
+ test("2 + 2 == 3") {
+ assertEqual(2 + 2, 3)
+ }
+
+ // reverse[x] === [x]
+ with arbitraryInt;
+ forall[Int]("reverse-singleton", 100){ x =>
+ assertEqual(
+ reverse([x]),
+ [x]
+ )
+ }
+
+ // reverse[x] === [x], with explicit passing of the generator
+ with def g = arbitraryInt;
+ forall[Int]("reverse-singleton, explicitely passing the generator", 100){ g }
+ { x =>
+ assertEqual(
+ reverse([x]),
+ [x]
+ )
+ }
+
+ // intented mistake: reverse[x] === [6]
+ forall[Int]("reverse-singleton mistake", 100)
+ { x =>
+ assertEqual(
+ reverse([x]),
+ [6]
+ )
+ }
+
+ // shows that exists prints tried test cases and found examples correctly
+ exists[Int]("Is the Integer 2 among the generated values?", 100)
+ { x =>
+ assertEqual(
+ x,
+ 2
+ )
+ }
+
+ // reverse(xs ++ ys) === reverse(ys) ++ reverse(xs)
+ with arbitraryList[Int](3)
+ forall[List[Int], List[Int]]("reverse: distributivity over append", 100)
+ { (xs, ys) =>
+ assertEqual(
+ reverse(xs.append(ys)),
+ reverse(ys).append(reverse(xs))
+ )
+ }
+
+ // intended mistake: reverse(xs ++ ys) === reverse(xs) ++ reverse(ys)
+ forall[List[Int], List[Int]]("reverse: distributivity mistake - swapped order", 20)
+ { (xs, ys) =>
+ assertEqual(
+ reverse(xs.append(ys)),
+ reverse(xs).append(reverse(ys))
+ )
+ }
+
+ with arbitraryChar;
+ with arbitraryList[Char](4);
+ forall[List[Char], List[Int]]("|zip(xs,ys)| === min(|xs|,|ys|)",10)
+ { (xs, ys) =>
+ assertEqual(
+ zip(xs, ys).size,
+ min(xs.size, ys.size)
+ )
+ }
+
+ with arbitraryChar;
+ with arbitraryList[Char](6);
+ forall[List[Char], List[Int]]("intended mistake: |zip(xs,ys)| != max(|xs|,|ys|)",10)
+ { (xs, ys) =>
+ assertEqual(
+ zip(xs, ys).size,
+ max(xs.size, ys.size)
+ )
+ }
+
+ // unzip(zip(xs,ys)) === (xs.take(m), ys.take(m)) where m = min(|xs|,|ys|)
+ with arbitraryChar;
+ with arbitraryString(4);
+ with arbitraryList[String](2);
+ with arbitraryInt;
+ with arbitraryList[Int](3)
+ forall[List[Int], List[String]]("unzip-zip-take relation", 10)
+ { (xs, ys) =>
+ val m = min(xs.size, ys.size)
+ assertEqual(
+ unzip(zip(xs, ys)),
+ (xs.take(m), ys.take(m))
+ )
+ }
+
+ // Dropping elements from the concatenation of two lists is equivalent to dropping elements from the first list,
+ // and then (if necessary) dropping the remaining count from the second list.
+ // (xs ++ ys).drop(n) === if n <= len(xs) then (xs.drop(n)) ++ ys else ys.drop(n - len(xs))
+ forall[List[Int], List[Int], Int]("drop: concatenation ", 10)
+ { (xs, ys, n) =>
+ val res = if(n <= xs.size) {
+ xs.drop(n).append(ys)
+ }
+ else { ys.drop(n - xs.size) }
+ assertEqual(
+ (xs.append(ys).drop(n)),
+ res
+ )
+ }
+
+ // xs.drop(n) === xs.slice(n, x.size)
+ forall[List[Int], Int]("drop-slice relation", 10)
+ { (xs, n) =>
+ assertEqual(
+ xs.drop(n),
+ xs.slice(n, xs.size)
+ )
+ }
+
+ // reverseOnto(reverse(xs), ys) === append(xs, ys)
+ forall[List[Int], List[Int]]("reverseOnto-reverse-append relation ", 10)
+ { (xs, ys) =>
+ assertEqual(
+ reverseOnto(reverse(xs), ys),
+ append(xs, ys)
+ )
+ }
+
+ // size(xs) === foldLeft(xs, 0) { (acc, _) => acc + 1 }
+ forall[List[Int]]("size-foldLeft relation", 20)
+ { xs =>
+ assertEqual(
+ size(xs),
+ foldLeft(xs, 0){(acc, _) => acc + 1}
+ )
+ }
+
+ //xs.take(n) ++ xs.drop(n) === xs
+ forall[Int, List[Int]]("take-drop: inversion over concatenation", 20)
+ { (n, xs) =>
+ assertEqual(
+ append(xs.take(n), xs.drop(n)),
+ xs
+ )
+ }
+
+ // example for a property-based test with multiple assertions
+ with evenNumbers;
+ forall[Int]("even numbers are even and smaller than 4", 20)
+ { n =>
+ assertTrue(n.mod(2) == 0)
+ assertTrue(n <= 4)
+ }
+ };
+
+ ()
+}
+
\ No newline at end of file
diff --git a/examples/stdlib/test/tree_examples_pbt.check b/examples/stdlib/test/tree_examples_pbt.check
new file mode 100644
index 000000000..fb65e1c93
--- /dev/null
+++ b/examples/stdlib/test/tree_examples_pbt.check
@@ -0,0 +1,9 @@
+PBT: Tree Map Examples
+✓ get-put law, passed 100 tests
+✓ put-put law, passed 100 tests
+✓ put-get law, passed 100 tests
+✓ forget-map relation, passed 100 tests
+
+ 4 pass
+ 0 fail
+ 4 test(s) total
\ No newline at end of file
diff --git a/examples/stdlib/test/tree_examples_pbt.effekt b/examples/stdlib/test/tree_examples_pbt.effekt
new file mode 100644
index 000000000..a3a0069d0
--- /dev/null
+++ b/examples/stdlib/test/tree_examples_pbt.effekt
@@ -0,0 +1,113 @@
+import map
+import stream
+import random
+
+import test
+
+// user-defined generator for arbitrary, unique Integers in ascending order
+def uniqueInt{body: => Unit / Generator[Int]}: Unit / random = {
+ try body() with Generator[Int]{
+ def generate() = resume{
+ var next = randomInt(-100, 100)
+ do emit(next)
+ while(true){
+ next = next + randomInt(1, 5)
+ do emit(next)
+ }
+ }
+ def shrink(v) = <>
+ }
+}
+
+// precondition: provided handler for Generator[K] needs to produce unique keys in ascending order!
+// otherwise the generated trees aren't valid binary search trees
+def arbitraryBinTree[K, V] (numKeys: Int) { body: => Unit / Generator[internal::Tree[K, V]] }: Unit / {Generator[K], Generator[V]} = {
+ def buildTree[K, V](pairs: List[(K, V)]): internal::Tree[K,V] = {
+ if (pairs.isEmpty) {
+ internal::Tip()
+ } else {
+ val midIdx = pairs.size() / 2
+ with on[OutOfBounds].panic;
+ val midEl = pairs.get(midIdx)
+ val leftTree = buildTree(pairs.take(midIdx))
+ val rightTree = buildTree(pairs.drop(midIdx + 1))
+ val size = 1 + internal::size(leftTree) + internal::size(rightTree)
+
+ internal::Bin(size, midEl.first, midEl.second, leftTree, rightTree)
+ }
+ }
+
+ try body() with Generator[internal::Tree[K, V]] {
+ def generate() = resume {
+ while(true) {
+ val l1 = collectList[K]{with boundary; with limit[K](numKeys); do generate[K]}
+ val l2 = collectList[V]{with boundary; with limit[V](numKeys); do generate[V]}
+ val sortedPairs = zip(l1,l2)
+ do emit(buildTree[K, V](sortedPairs))
+ //TODO try to use zip again
+ }
+ }
+ def shrink(v) = <>
+ }
+}
+
+def main()= {
+ with minstd(1337);
+
+ with arbitraryChar;
+ with arbitraryString(5);
+ with uniqueInt;
+ with arbitraryBinTree[Int, String](8);
+
+ // Don't print out times in CI.
+ suite("PBT: Tree Map Examples", false) {
+
+ // get-put law: get(put(M, K, V), K) = V
+ forall[String, internal::Tree[Int, String], Int]("get-put law", 100)
+ { (v, t, k) =>
+ val newT = internal::put(t, compareInt, k, v)
+ internal::get(newT, compareInt, k) match {
+ case Some(value) => assertEqual(value, v)
+ case None() => do assert(false, "Key not in the tree")
+ }
+ }
+
+ // put-put law: get(put(put(M, K, V1), K, V2), K) = V2
+ forall[String, internal::Tree[Int, String], Int]("put-put law", 100)
+ { (v1, t, k) =>
+ val v2 = v1 ++ show(k) ++ v1
+ val newT1 = internal::put(t, compareInt, k, v1)
+ val newT2 = internal::put(newT1, compareInt, k, v2)
+ internal::get(newT2, compareInt, k) match {
+ case Some(v) => assertEqual(v, v2)
+ case None() => do assert(false, "Key in the tree")
+ }
+ }
+
+ // put-get law: put(M, K, get(M, K)) = M
+ forall[internal::Tree[Int, String]]("put-get law", 100)
+ { t =>
+ // put-get law only make sense if K is present in the tree ~> get the keys of the tree
+ var keys = Nil()
+ t.internal::foreach[Int, String] { (k, _v) => keys = Cons(k, keys)}
+
+ with on[OutOfBounds].panic
+ val k = keys.get(randomInt(0, keys.size() - 1)) // only check the property for key's present in the tree (= non trivial test case)
+ internal::get(t, compareInt, k) match {
+ case Some(v) => {
+ val newT = internal::put(t, compareInt, k, v)
+ assertEqual(t, newT)}
+ case None() => do assert(false, "Key not in the tree")
+ }
+ }
+
+ // Law: `m.forget === m.map { (_k, _v) => () }
+ forall[internal::Tree[Int, String]]("forget-map relation", 100){t =>
+ val tForget = internal::forget(t)
+ val tMap = internal::map(t){ (_k, _v) => ()}
+ assertEqual(tForget, tMap)
+ }
+ };
+
+ ()
+}
\ No newline at end of file
diff --git a/examples/tour/captures.effekt.md b/examples/tour/captures.effekt.md
index 53b27309e..3ce9b5a73 100644
--- a/examples/tour/captures.effekt.md
+++ b/examples/tour/captures.effekt.md
@@ -131,4 +131,4 @@ An example of a builtin resource is `io`, which is handled by the runtime. Other
## References
-- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://dl.acm.org/doi/10.1145/3527320)
+- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://doi.org/10.1145/3527320)
diff --git a/examples/tour/objects.effekt.md b/examples/tour/objects.effekt.md
index 137e2bed0..6c452c50c 100644
--- a/examples/tour/objects.effekt.md
+++ b/examples/tour/objects.effekt.md
@@ -85,4 +85,4 @@ def counterAsEffect2() = {
counterAsEffect2()
```
As part of its compilation pipeline, and guided by the type-and-effect system,
-the Effekt compiler performs this translation from implicitly handled effects to explicitly passed capabilities [Brachthäuser et al., 2020](https://dl.acm.org/doi/10.1145/3428194).
+the Effekt compiler performs this translation from implicitly handled effects to explicitly passed capabilities [Brachthäuser et al., 2020](https://doi.org/10.1145/3428194).
diff --git a/examples/tour/regions.effekt.md b/examples/tour/regions.effekt.md
index a0315c4f5..3e209697b 100644
--- a/examples/tour/regions.effekt.md
+++ b/examples/tour/regions.effekt.md
@@ -117,21 +117,24 @@ Running it will give us the same result:
example1Region()
```
-## Global
+## Global Mutable State
-It is also possible to allocate a variable globally by allocating it into the built-in region `global`. With this, it is possible to write a program which is normally not possible:
+It is also possible to allocate a variable globally by using the reference module `ref`. With this, it is possible to write a program which is normally not possible:
```
+import ref
+
def example5() = {
- var x in global = 1
- val closure = box { () => x }
+ val x = ref(1)
+ val closure = box { () => x.get }
closure
}
```
-We can return a closure that closes over a variable. This is only possible because `x` is allocated into the `global` region and therefore has a static lifetime.
+We can return a closure that closes over a mutable reference. This is only possible because `x` is allocated on the heap and subject to garbage collection. Note that in most cases it does not make sense to define mutable references using `var`.
+
## References
-- [Region-based Resource Management and Lexical Exception Handlers in Continuation-Passing Style](https://link.springer.com/chapter/10.1007/978-3-030-99336-8_18)
-- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://dl.acm.org/doi/10.1145/3527320)
+- [Region-based Resource Management and Lexical Exception Handlers in Continuation-Passing Style](https://doi.org/10.1007/978-3-030-99336-8_18)
+- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://doi.org/10.1145/3527320)
diff --git a/kiama b/kiama
index 3abc1e974..668874d4e 160000
--- a/kiama
+++ b/kiama
@@ -1 +1 @@
-Subproject commit 3abc1e97416b867449aaf7ed381a725f4832b1fa
+Subproject commit 668874d4e54c303bb45d0df2ee1abb38eb81a3e1
diff --git a/libraries/common/array.effekt b/libraries/common/array.effekt
index 5cc584902..af9a01c0f 100644
--- a/libraries/common/array.effekt
+++ b/libraries/common/array.effekt
@@ -8,14 +8,54 @@ import option
/// A mutable 0-indexed fixed-sized array.
extern type Array[T]
+/** We represent arrays like positive types.
+ * The tag is 0 and the obj points to memory with the following layout:
+ *
+ * +--[ Header ]--+------+------------+
+ * | Rc | Eraser | Size | Fields ... |
+ * +--------------+------+------------+
+ */
+
+extern llvm """
+declare noalias ptr @calloc(i64, i64)
+
+define void @array_erase_fields(ptr %array_pointer) {
+entry:
+ %data_pointer = getelementptr inbounds i64, ptr %array_pointer, i64 1
+ %size = load i64, ptr %array_pointer, align 8
+ %size_eq_0 = icmp eq i64 %size, 0
+ br i1 %size_eq_0, label %exit, label %loop
+loop:
+ %loop_phi = phi i64 [ %inc, %loop ], [ 0, %entry ]
+
+ %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 %loop_phi
+ %element = load %Pos, ptr %element_pointer
+
+ call void @erasePositive(%Pos %element)
+ %inc = add nuw i64 %loop_phi, 1
+ %cmp = icmp ult i64 %inc, %size
+ br i1 %cmp, label %loop, label %exit
+exit:
+ ret void
+}
+"""
+
/// Allocates a new array of size `size`, keeping its values _undefined_.
/// Prefer using `array` constructor instead to ensure that values are defined.
extern global def allocate[T](size: Int): Array[T] =
js "(new Array(${size}))"
chez "(make-vector ${size})" // creates an array filled with 0s on CS
llvm """
- %z = call %Pos @c_array_new(%Int ${size})
- ret %Pos %z
+ %size0 = shl i64 ${size}, 4
+ %size1 = add i64 %size0, 24
+ %calloc = tail call noalias ptr @calloc(i64 %size1, i64 1)
+ %eraser_pointer = getelementptr ptr, ptr %calloc, i64 1
+ %size_pointer = getelementptr ptr, ptr %calloc, i64 2
+ store i64 0, ptr %calloc
+ store ptr @array_erase_fields, ptr %eraser_pointer
+ store i64 ${size}, ptr %size_pointer
+ %ret_pos = insertvalue %Pos { i64 0, ptr poison }, ptr %calloc, 1
+ ret %Pos %ret_pos
"""
vm "array::allocate(Int)"
@@ -49,8 +89,12 @@ extern pure def size[T](arr: Array[T]): Int =
js "${arr}.length"
chez "(vector-length ${arr})"
llvm """
- %z = call %Int @c_array_size(%Pos ${arr})
- ret %Int %z
+ %pos_data = extractvalue %Pos ${arr}, 1
+ %size_pointer = getelementptr inbounds i64, ptr %pos_data, i64 2
+ %size = load i64, ptr %size_pointer
+
+ tail call void @erasePositive(%Pos ${arr})
+ ret i64 %size
"""
vm "array::size[T](Array[T])"
@@ -62,8 +106,15 @@ extern global def unsafeGet[T](arr: Array[T], index: Int): T =
js "${arr}[${index}]"
chez "(vector-ref ${arr} ${index})"
llvm """
- %z = call %Pos @c_array_get(%Pos ${arr}, %Int ${index})
- ret %Pos %z
+ %pos_data = extractvalue %Pos ${arr}, 1
+
+ %data_pointer = getelementptr inbounds ptr, ptr %pos_data, i64 3
+ %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 ${index}
+ %element = load %Pos, ptr %element_pointer
+
+ tail call void @sharePositive(%Pos %element)
+ tail call void @erasePositive(%Pos ${arr})
+ ret %Pos %element
"""
vm "array::unsafeGet[T](Array[T], Int)"
@@ -82,8 +133,15 @@ extern global def unsafeSet[T](arr: Array[T], index: Int, value: T): Unit =
js "array$set(${arr}, ${index}, ${value})"
chez "(begin (vector-set! ${arr} ${index} ${value}) #f)"
llvm """
- %z = call %Pos @c_array_set(%Pos ${arr}, %Int ${index}, %Pos ${value})
- ret %Pos %z
+ %array_data = extractvalue %Pos ${arr}, 1
+
+ %data_pointer = getelementptr inbounds i64, ptr %array_data, i64 3
+ %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 ${index}
+ %element = load %Pos, ptr %element_pointer, align 8
+ tail call void @erasePositive(%Pos %element)
+ store %Pos ${value}, ptr %element_pointer, align 8
+ tail call void @erasePositive(%Pos ${arr})
+ ret %Pos zeroinitializer
"""
vm "array::unsafeSet[T](Array[T], Int, T)"
diff --git a/libraries/common/buffer.effekt b/libraries/common/buffer.effekt
index 2c9abab28..6002bc2c0 100644
--- a/libraries/common/buffer.effekt
+++ b/libraries/common/buffer.effekt
@@ -3,7 +3,6 @@ module buffer
import array
// TODO (in Effekt compiler)
-// - [ ] fix allocating into actually global region
// - [ ] fix exceptions on objects
// - [X] allow omitting braces after `at` for singleton regions
@@ -35,12 +34,12 @@ def emptyBuffer[T](capacity: Int): Buffer[T] at {global} = {
def arrayBuffer[T](initialCapacity: Int): Buffer[T] at {global} = {
// TODO allocate buffer (and array) into a region r.
val contents = array::allocate[T](initialCapacity)
- var head in global = 0
- var tail in global = 0
+ val head = ref(0)
+ val tail = ref(0)
def size(): Int =
- if (tail >= head) { tail - head }
- else { initialCapacity - head + tail }
+ if (tail.get >= head.get) { tail.get - head.get }
+ else { initialCapacity - head.get + tail.get }
def capacity(): Int = initialCapacity - size()
@@ -51,36 +50,36 @@ def arrayBuffer[T](initialCapacity: Int): Buffer[T] at {global} = {
def read() = {
if (buffer.empty?) None()
else {
- val result: T = contents.unsafeGet(head);
- head = mod(head + 1, initialCapacity)
+ val result: T = contents.unsafeGet(head.get)
+ head.set(mod(head.get + 1, initialCapacity))
Some(result)
}
}
def write(el: T) = {
if (buffer.full?) <> // raise(BufferOverflow())
- contents.unsafeSet(tail, el)
- tail = mod(tail + 1, initialCapacity)
+ contents.unsafeSet(tail.get, el)
+ tail.set(mod(tail.get + 1, initialCapacity))
}
}
buffer
}
def refBuffer[T](): Buffer[T] at {global} = {
- var content: Option[T] in global = None()
+ val content = ref(None())
new Buffer[T] {
- def capacity() = if (content.isEmpty) 1 else 0
- def full?() = content.isDefined
- def empty?() = isEmpty(content)
+ def capacity() = if (content.get.isEmpty) 1 else 0
+ def full?() = content.get.isDefined
+ def empty?() = isEmpty(content.get)
def read() = {
- val res = content
- content = None()
+ val res = content.get
+ content.set(None())
res
}
- def write(el: T) = content match {
+ def write(el: T) = content.get match {
case Some(v) =>
<> // do raise(BufferOverflow(), "Cannot read element from buffer")
case None() =>
- content = Some(el)
+ content.set(Some(el))
}
}
}
@@ -88,24 +87,24 @@ def refBuffer[T](): Buffer[T] at {global} = {
namespace examples {
def main() = ignore[BufferOverflow] {
// Buffer with capacity 1
- def b = emptyBuffer[Int](1);
- println(b.capacity);
- println(b.full?);
+ def b = emptyBuffer[Int](1)
+ println(b.capacity)
+ println(b.full?)
- b.write(17);
- println(b.read());
+ b.write(17)
+ println(b.read())
// buffer with capacity 3
- def ringbuffer = emptyBuffer[Int](3);
- ringbuffer.write(1);
- ringbuffer.write(2);
- println(ringbuffer.read());
- ringbuffer.write(3);
- println(ringbuffer.read());
- println(ringbuffer.read());
- ringbuffer.write(4);
- ringbuffer.write(5);
- println(ringbuffer.read());
- println(ringbuffer.read());
+ def ringbuffer = emptyBuffer[Int](3)
+ ringbuffer.write(1)
+ ringbuffer.write(2)
+ println(ringbuffer.read())
+ ringbuffer.write(3)
+ println(ringbuffer.read())
+ println(ringbuffer.read())
+ ringbuffer.write(4)
+ ringbuffer.write(5)
+ println(ringbuffer.read())
+ println(ringbuffer.read())
}
}
diff --git a/libraries/common/bytearray.effekt b/libraries/common/bytearray.effekt
index 70c757ae2..c14a88f0d 100644
--- a/libraries/common/bytearray.effekt
+++ b/libraries/common/bytearray.effekt
@@ -2,6 +2,17 @@ module bytearray
/**
* A memory managed, mutable, fixed-length array of bytes.
+ *
+ * We represent bytearrays like positive types.
+ *
+ * - The field `tag` contains the size
+ * - The field `obj` points to memory with the following layout:
+ *
+ * +--[ Header ]--+--------------+
+ * | Rc | Eraser | Contents ... |
+ * +--------------+--------------+
+ *
+ * The eraser does nothing.
*/
extern type ByteArray
// = llvm "%Pos"
@@ -12,32 +23,47 @@ extern type ByteArray
extern global def allocate(size: Int): ByteArray =
js "(new Uint8Array(${size}))"
llvm """
- %arr = call %Pos @c_bytearray_new(%Int ${size})
- ret %Pos %arr
+ %object_size = add i64 ${size}, 16
+ %object_alloc = tail call noalias ptr @malloc(i64 noundef %object_size)
+ store i64 0, ptr %object_alloc, align 8
+ %object_data_ptr = getelementptr inbounds i8, ptr %object_alloc, i64 8
+ store ptr @bytearray_erase_noop, ptr %object_data_ptr, align 8
+ %ret_object1 = insertvalue %Pos poison, i64 ${size}, 0
+ %ret_object2 = insertvalue %Pos %ret_object1, ptr %object_alloc, 1
+ ret %Pos %ret_object2
"""
chez "(make-bytevector ${size})"
extern pure def size(arr: ByteArray): Int =
js "${arr}.length"
llvm """
- %size = call %Int @c_bytearray_size(%Pos ${arr})
- ret %Int %size
+ %size = extractvalue %Pos ${arr}, 0
+ tail call void @erasePositive(%Pos ${arr})
+ ret i64 %size
"""
chez "(bytevector-length ${arr})"
extern global def unsafeGet(arr: ByteArray, index: Int): Byte =
js "(${arr})[${index}]"
llvm """
- %byte = call %Byte @c_bytearray_get(%Pos ${arr}, %Int ${index})
- ret %Byte %byte
+ %arr_ptr = extractvalue %Pos ${arr}, 1
+ %arr_data_ptr = getelementptr inbounds i8, ptr %arr_ptr, i64 16
+ %element_ptr = getelementptr inbounds i8, ptr %arr_data_ptr, i64 ${index}
+ %element = load i8, ptr %element_ptr, align 1
+ tail call void @erasePositive(%Pos ${arr})
+ ret i8 %element
"""
chez "(bytevector-u8-ref ${arr} ${index})"
extern global def unsafeSet(arr: ByteArray, index: Int, value: Byte): Unit =
js "bytearray$set(${arr}, ${index}, ${value})"
llvm """
- %z = call %Pos @c_bytearray_set(%Pos ${arr}, %Int ${index}, %Byte ${value})
- ret %Pos %z
+ %arr_ptr = extractvalue %Pos ${arr}, 1
+ %arr_data_ptr = getelementptr inbounds i8, ptr %arr_ptr, i64 16
+ %element_ptr = getelementptr inbounds i8, ptr %arr_data_ptr, i64 ${index}
+ store i8 ${value}, ptr %element_ptr, align 1
+ tail call void @erasePositive(%Pos ${arr})
+ ret %Pos zeroinitializer
"""
chez "(bytevector-u8-set! ${arr} ${index} ${value})"
@@ -121,6 +147,12 @@ extern js """
}
"""
+extern llvm """
+define void @bytearray_erase_noop(ptr readnone %0) {
+ ret void
+}
+"""
+
extern chez """
(define (bytearray$compare b1 b2)
(let ([len1 (bytevector-length b1)]
diff --git a/libraries/common/effekt.effekt b/libraries/common/effekt.effekt
index 06e271db0..e34c6804c 100644
--- a/libraries/common/effekt.effekt
+++ b/libraries/common/effekt.effekt
@@ -344,7 +344,7 @@ extern pure def infixSub(x: Double, y: Double): Double =
extern pure def infixDiv(x: Double, y: Double): Double =
js "(${x} / ${y})"
- chez "(/ ${x} ${y})"
+ chez "(fl/ ${x} ${y})"
llvm "%z = fdiv %Double ${x}, ${y} ret %Double %z"
vm "effekt::infixDiv(Double, Double)"
@@ -442,14 +442,14 @@ extern pure def toInt(d: Double): Int =
extern pure def toDouble(d: Int): Double =
js "${d}"
- chez "${d}"
+ chez "(fixnum->flonum ${d})"
llvm "%z = sitofp i64 ${d} to double ret double %z"
vm "effekt::toDouble(Int)"
extern pure def round(d: Double): Int =
js "Math.round(${d})"
- chez "(round ${d})"
+ chez "(flonum->fixnum (round ${d}))"
llvm """
%i = call %Double @llvm.round.f64(double ${d})
%z = fptosi double %i to %Int ret %Int %z
diff --git a/libraries/common/queue.effekt b/libraries/common/queue.effekt
index d01c28640..8fe07ee9a 100644
--- a/libraries/common/queue.effekt
+++ b/libraries/common/queue.effekt
@@ -25,82 +25,82 @@ def emptyQueue[T](): Queue[T] at {global} =
def emptyQueue[T](initialCapacity: Int): Queue[T] at {global} = {
- var contents in global = array[Option[T]](initialCapacity, None())
- var head in global = 0
- var tail in global = 0
- var size in global = 0
- var capacity in global = initialCapacity
+ val contents = ref(array[Option[T]](initialCapacity, None()))
+ val head = ref(0)
+ val tail = ref(0)
+ val size = ref(0)
+ val capacity = ref(initialCapacity)
def remove(arr: Array[Option[T]], index: Int): Option[T] = {
- with on[OutOfBounds].default { None() };
- val value = arr.get(index);
- arr.set(index, None());
+ with on[OutOfBounds].default { None() }
+ val value = arr.get(index)
+ arr.set(index, None())
value
}
def nonEmpty[T] { p: => Option[T] / Exception[OutOfBounds] }: Option[T] =
- if (size <= 0) None() else on[OutOfBounds].default { None() } { p() }
+ if (size.get <= 0) None() else on[OutOfBounds].default { None() } { p() }
// Exponential back-off
def resizeTo(requiredSize: Int): Unit =
- if (requiredSize <= capacity) () else {
+ if (requiredSize <= capacity.get) () else {
with on[OutOfBounds].ignore // should not happen
- val oldSize = capacity
- val newSize = capacity * 2
- val oldContents = contents
+ val oldSize = capacity.get
+ val newSize = capacity.get * 2
+ val oldContents = contents.get
val newContents = array::allocate[Option[T]](newSize)
- if (head < tail) {
+ if (head.get < tail.get) {
// The queue does not wrap around; direct copy is possible.
- copy(oldContents, head, newContents, 0, size) // changed tail to size
- } else if (size > 0) {
+ copy(oldContents, head.get, newContents, 0, size.get) // changed tail to size
+ } else if (size.get > 0) {
// The queue wraps around; copy in two segments.
- copy(oldContents, head, newContents, 0, oldSize - head) // changed oldSize to oldSize - head
- copy(oldContents, 0, newContents, oldSize - head, tail) // changed oldSize - head to oldSize - head
+ copy(oldContents, head.get, newContents, 0, oldSize - head.get) // changed oldSize to oldSize - head
+ copy(oldContents, 0, newContents, oldSize - head.get, tail.get) // changed oldSize - head to oldSize - head
}
- contents = newContents
- capacity = newSize
- head = 0
- tail = oldSize
+ contents.set(newContents)
+ capacity.set(newSize)
+ head.set(0)
+ tail.set(oldSize)
}
def queue = new Queue[T] {
- def empty?() = size <= 0
+ def empty?() = size.get <= 0
def popFront() =
nonEmpty {
- val result = contents.remove(head)
- head = mod(head + 1, capacity)
- size = size - 1
+ val result = contents.get.remove(head.get)
+ head.set(mod(head.get + 1, capacity.get))
+ size.set(size.get - 1)
result
}
def popBack() =
nonEmpty {
- tail = mod(tail - 1 + capacity, capacity)
- val result = contents.remove(tail)
- size = size - 1
+ tail.set(mod(tail.get - 1 + capacity.get, capacity.get))
+ val result = contents.get.remove(tail.get)
+ size.set(size.get - 1)
result
}
- def peekFront() = nonEmpty { contents.get(head) }
+ def peekFront() = nonEmpty { contents.get.get(head.get) }
- def peekBack() = nonEmpty { contents.get(tail) }
+ def peekBack() = nonEmpty { contents.get.get(tail.get) }
def pushFront(el: T) = {
- resizeTo(size + 1);
- head = mod(head - 1 + capacity, capacity);
- size = size + 1;
- contents.unsafeSet(head, Some(el))
+ resizeTo(size.get + 1)
+ head.set(mod(head.get - 1 + capacity.get, capacity.get))
+ size.set(size.get + 1)
+ contents.get.unsafeSet(head.get, Some(el))
}
def pushBack(el: T) = {
- resizeTo(size + 1);
- contents.unsafeSet(tail, Some(el));
- size = size + 1;
- tail = mod(tail + 1, capacity)
+ resizeTo(size.get + 1)
+ contents.get.unsafeSet(tail.get, Some(el))
+ size.set(size.get + 1)
+ tail.set(mod(tail.get + 1, capacity.get))
}
}
queue
@@ -109,30 +109,30 @@ def emptyQueue[T](initialCapacity: Int): Queue[T] at {global} = {
namespace examples {
def main() = {
// queue with initial capacity 4
- def b = emptyQueue[Int](4);
- println(b.empty?);
- b.pushFront(1);
- b.pushBack(2);
- b.pushFront(3);
- b.pushBack(4);
+ def b = emptyQueue[Int](4)
+ println(b.empty?)
+ b.pushFront(1)
+ b.pushBack(2)
+ b.pushFront(3)
+ b.pushBack(4)
// this will cause resizing:
- b.pushBack(5);
- b.pushBack(6);
- b.pushBack(7);
- b.pushBack(8);
+ b.pushBack(5)
+ b.pushBack(6)
+ b.pushBack(7)
+ b.pushBack(8)
// and again:
- b.pushBack(9);
-
- println(b.empty?);
- println(b.popFront()); // Some(3)
- println(b.popFront()); // Some(1)
- println(b.popFront()); // Some(2)
- println(b.popFront()); // Some(4)
- println(b.popFront()); // Some(5)
- println(b.popFront()); // Some(6)
- println(b.popFront()); // Some(7)
- println(b.popFront()); // Some(8)
- println(b.popFront()); // Some(9)
- println(b.popFront()); // None()
+ b.pushBack(9)
+
+ println(b.empty?)
+ println(b.popFront()) // Some(3)
+ println(b.popFront()) // Some(1)
+ println(b.popFront()) // Some(2)
+ println(b.popFront()) // Some(4)
+ println(b.popFront()) // Some(5)
+ println(b.popFront()) // Some(6)
+ println(b.popFront()) // Some(7)
+ println(b.popFront()) // Some(8)
+ println(b.popFront()) // Some(9)
+ println(b.popFront()) // None()
}
}
diff --git a/libraries/common/random.effekt b/libraries/common/random.effekt
new file mode 100644
index 000000000..b1998aec7
--- /dev/null
+++ b/libraries/common/random.effekt
@@ -0,0 +1,186 @@
+module random
+
+import stream
+import io/error
+
+/// Infinite pull stream of random bytes.
+effect random(): Byte
+
+// ---------------------
+// Sources of randomness
+
+/// A streaming source (push stream) of byte-level randomness
+/// based on Park and Miller's MINSTD with revised parameters.
+///
+/// Deterministic: needs a 32bit `seed` -- you can use `bench::timestamp`.
+def minstd(seed: Int): Unit / emit[Byte] = {
+ // Initialize state with seed, ensuring it's not zero
+ var state = if (seed == 0) 1 else seed
+
+ def nextInt(): Int = {
+ // Uses only at most 32-bit integers internally
+ // (Schrage's method: https://en.wikipedia.org/wiki/Lehmer_random_number_generator#Schrage's_method)
+ val a = 48271
+ val m = 2147483647
+
+ val q = m / a // 44488
+ val r = m.mod(a) // 3399
+
+ val div = state / q // max: M / Q = A = 48271
+ val rem = state.mod(q) // max: Q - 1 = 44487
+
+ val s = rem * a; // max: 44487 * 48271 = 2147431977
+ val t = div * r; // max: 48271 * 3399 = 164073129
+
+ val result = s - t
+ // keep the state positive
+ if (result < 0) result + m else result
+ }
+
+ while (true) {
+ state = nextInt()
+ val b = state.mod(256).toByte
+ do emit(b)
+ }
+}
+
+/// A thin wrapper over `minstd`, handling a reader of random bytes.
+///
+/// Deterministic: needs a 32bit `seed` -- you can use `bench::timestamp`.
+///
+/// Implementation is similar to `stream::source`, specialized for bytes and the `random` effect.
+def minstd(seed: Int) { randomnessReader: () => Unit / random }: Unit = {
+ var next = box { 255.toByte } // sentinel value
+ next = box {
+ try {
+ minstd(seed)
+ <> // safe: randomness generator cannot run out of numbers...
+ } with emit[Byte] { v =>
+ next = box { resume(()) }
+ v
+ }
+ }
+
+ try randomnessReader() with random {
+ resume(next())
+ }
+}
+
+/// CSPRNG from `/dev/urandom`, handling a reader of random bytes.
+/// Only works on Unix-like OSes!
+def devurandom { randomnessReader: () => Unit / random }: Unit / Exception[IOError] =
+ try {
+ with readFile("/dev/urandom")
+ try randomnessReader() with random {
+ resume(do read[Byte]())
+ }
+ } with stop {
+ do raise(io::error::EOF(), "Unexpected EOF when reading /dev/urandom!")
+ }
+
+// ------------------------
+// Functions using `random`
+//
+// Always two variants:
+// - readType(): Type / random
+// - readTypes(): Unit / {emit[Type], random}
+
+def randomByte(): Byte / random = do random()
+def randomBytes(): Unit / {emit[Byte], random} =
+ while (true) do emit(do random())
+
+def randomBool(): Bool / random = {
+ val b = do random()
+ b.toInt.mod(2) == 1
+}
+def randomBools(): Unit / {emit[Bool], random} =
+ while (true) do emit(randomBool())
+
+def randomInt32(): Int / random = {
+ var result = 0
+ repeat(4) {
+ val b = do random()
+ result = result * 256 + b.toInt
+ }
+ val signBit = result.bitwiseShr(31).bitwiseAnd(1) == 0
+ result.mod(1.bitwiseShl(31)).abs * if (signBit) 1 else -1
+}
+def randomInt32s(): Unit / {emit[Int], random} =
+ while (true) do emit(randomInt32())
+
+/// `max` is _inclusive_!
+def randomInt(min: Int, max: Int): Int / random = {
+ if (min > max) {
+ randomInt(max, min)
+ } else {
+ val range = max - min + 1
+ val bytesNeeded = (log(range.toDouble) / log(256.0)).ceil
+
+ var result = 0
+ repeat(bytesNeeded) {
+ val b = do random()
+ result = result * 256 + b.toInt
+ }
+
+ min + (abs(result).mod(range))
+ }
+}
+/// `max` is _inclusive_!
+def randomInts(min: Int, max: Int): Unit / {emit[Int], random} =
+ while (true) do emit(randomInt(min, max))
+
+
+/// Random double between 0.0 and 1.0
+def randomDouble(): Double / random =
+ (randomInt32().toDouble / 1.bitwiseShl(31).toDouble).abs
+ // This is not perfect, but it will do for now.
+def randomDoubles(): Unit / {emit[Double], random} =
+ while (true) do emit(randomDouble())
+
+
+namespace examples {
+def main() = {
+ println("prng")
+ prngRandom()
+ }
+
+ def prngRandom(): Unit = {
+ with minstd(1337);
+
+ println("int32s:")
+ repeat(10) {
+ println(randomInt32())
+ }
+
+ println("int32s, part2:")
+ repeat(10) {
+ println(randomInt(0, 2147483647))
+ println(randomInt(-2147483648, 0))
+ }
+
+ println("doubles:")
+ repeat(10) {
+ println(randomDouble().round(3))
+ }
+
+ println("randomInt:")
+ repeat(10) {
+ val a = randomInt(0, 9)
+ val b = randomInt(0, 9)
+ val c = randomInt(0, 9)
+ println(a.show ++ " " ++ b.show ++ " " ++ c.show)
+ println(a*100 + b*10 + c)
+ }
+ }
+
+ def unixRandom(): Unit = {
+ with on[IOError].report;
+ with devurandom;
+
+ val a = randomInt32()
+ val b = randomInt32()
+
+ // This is just to use the generated numbers :)
+ println((a.show ++ b.show).length != 0)
+ }
+}
\ No newline at end of file
diff --git a/libraries/common/ref.effekt b/libraries/common/ref.effekt
index a38484966..de875dff5 100644
--- a/libraries/common/ref.effekt
+++ b/libraries/common/ref.effekt
@@ -9,17 +9,44 @@ extern js """
}
"""
+extern llvm """
+define void @c_ref_erase_field(ptr %0) {
+ %field = load %Pos, ptr %0, align 8
+ tail call void @erasePositive(%Pos %field)
+ ret void
+}
+"""
+
/// Global, mutable references
extern type Ref[T]
+/** We represent references like positive types in LLVM
+ * The tag is 0 and the obj points to memory with the following layout:
+ *
+ * +--[ Header ]--------------+------------+
+ * | ReferenceCount | Eraser | Field |
+ * +--------------------------+------------+
+ */
+
/// Allocates a new reference, keeping its value _undefined_.
/// Prefer using `ref` constructor instead to ensure that the value is defined.
extern global def allocate[T](): Ref[T] =
js "{ value: undefined }"
chez "(box #f)"
llvm """
- %z = call %Pos @c_ref_fresh(%Pos zeroinitializer)
- ret %Pos %z
+ ; sizeof Header + sizeof Pos = 32
+ %ref = tail call noalias ptr @malloc(i64 noundef 32)
+ %refEraser = getelementptr ptr, ptr %ref, i64 1
+ %fieldTag = getelementptr ptr, ptr %ref, i64 2
+ %fieldData_pointer = getelementptr ptr, ptr %ref, i64 3
+
+ store i64 0, ptr %ref, align 8
+ store ptr @c_ref_erase_field, ptr %refEraser, align 8
+ store i64 0, ptr %fieldTag, align 8
+ store ptr null, ptr %fieldData_pointer, align 8
+
+ %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1
+ ret %Pos %refWrap
"""
/// Creates a new reference with the initial value `init`.
@@ -27,8 +54,20 @@ extern global def ref[T](init: T): Ref[T] =
js "{ value: ${init} }"
chez "(box ${init})"
llvm """
- %z = call %Pos @c_ref_fresh(%Pos ${init})
- ret %Pos %z
+ %initTag = extractvalue %Pos ${init}, 0
+ %initObject_pointer = extractvalue %Pos ${init}, 1
+ ; sizeof Header + sizeof Pos = 32
+ %ref = tail call noalias ptr @malloc(i64 noundef 32)
+ %refEraser = getelementptr ptr, ptr %ref, i64 1
+ %refField = getelementptr ptr, ptr %ref, i64 2
+
+ store i64 0, ptr %ref, align 8
+ store ptr @c_ref_erase_field, ptr %refEraser, align 8
+ store %Pos ${init}, ptr %refField, align 8
+
+ %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1
+
+ ret %Pos %refWrap
"""
vm "ref::ref[T](T)"
@@ -36,9 +75,15 @@ extern global def ref[T](init: T): Ref[T] =
extern global def get[T](ref: Ref[T]): T =
js "${ref}.value"
chez "(unbox ${ref})"
- llvm """
- %z = call %Pos @c_ref_get(%Pos ${ref})
- ret %Pos %z
+llvm """
+ %ref_pointer = extractvalue %Pos ${ref}, 1
+ %refField_pointer = getelementptr inbounds ptr, ptr %ref_pointer, i64 2
+ %refField = load %Pos, ptr %refField_pointer, align 8
+
+ tail call void @sharePositive(%Pos %refField)
+ tail call void @erasePositive(%Pos ${ref})
+
+ ret %Pos %refField
"""
vm "ref::get[T](Ref[T])"
@@ -47,7 +92,15 @@ extern global def set[T](ref: Ref[T], value: T): Unit =
js "set$impl(${ref}, ${value})"
chez "(set-box! ${ref} ${value})"
llvm """
- %z = call %Pos @c_ref_set(%Pos ${ref}, %Pos ${value})
- ret %Pos %z
+ %refField_pointer = extractvalue %Pos ${ref}, 1
+
+ %field_pointer = getelementptr ptr, ptr %refField_pointer, i64 2
+ %field = load %Pos, ptr %field_pointer, align 8
+
+ tail call void @erasePositive(%Pos %field)
+ store %Pos ${value}, ptr %field_pointer
+ tail call void @erasePositive(%Pos ${ref})
+
+ ret %Pos zeroinitializer
"""
vm "ref::set[T](Ref[T], T)"
diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt
index 92b8fa280..dd4daac2d 100644
--- a/libraries/common/stringbuffer.effekt
+++ b/libraries/common/stringbuffer.effekt
@@ -14,7 +14,7 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = {
var pos = 0
def ensureCapacity(sizeToAdd: Int): Unit = {
- val cap = buffer.size - pos + 1
+ val cap = buffer.size - pos
if (sizeToAdd <= cap) ()
else {
// Double the capacity while ensuring the required capacity
@@ -35,10 +35,11 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = {
resume(())
}
def flush() = {
- // resize buffer to strip trailing zeros that otherwise would be converted into 0x00 characters
+ // resize (& copy) buffer to strip trailing zeros that otherwise would be converted into 0x00 characters
val str = bytearray::resize(buffer, pos).toString()
- // after flushing, the stringbuffer should be empty again
- buffer = bytearray::allocate(initialCapacity)
+ // NOTE: Keep the `buffer` as-is (no wipe, no realloc),
+ // just reset the `pos` in case we want to use it again.
+ pos = 0
resume(str)
}
}
@@ -55,11 +56,22 @@ def s { prog: () => Unit / { literal, splice[String] } }: String =
namespace examples {
def main() = {
with stringBuffer
+
do write("hello")
do write(", world")
// prints `hello, world`
println(do flush())
+
// prints the empty string
println(do flush())
+
+ do write("Ef")
+ do write("fe")
+ do write("kt")
+ do write(" = ")
+ do write("")
+ do write("Effekt")
+ // prints `Effekt = Effekt`
+ println(do flush())
}
}
diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt
index d5cafc894..92f5d5858 100644
--- a/libraries/common/test.effekt
+++ b/libraries/common/test.effekt
@@ -1,6 +1,8 @@
import tty
import process
import bench
+import stream
+import random
interface Assertion {
def assert(condition: Bool, msg: String): Unit
@@ -56,10 +58,238 @@ def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show:
// NOTE: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Here's an accidental capture! Can we prevent this somehow nicely?
+/// Represents push stream based generators with manual shrinking
+interface Generator[T] {
+ /// Generates a push stream of values of type `T`
+ def generate(): Unit/emit[T]
+
+ /// Produces a puh stream of simplified (shrunk) versions of a given value of type `T`
+ def shrink(v: T): Unit/emit[T]
+}
+
+def arbitraryInt { body: => Unit / Generator[Int] }: Unit / random =
+ try body() with Generator[Int] {
+ def generate() = resume {
+ //[0, -1, 1, int.minVal, int.maxVal, -2, 2].each // This can reduce the need for shrinking by emitting common edge cases first
+ while (true) {
+ do emit(randomInt(-50, 50))
+ }
+ }
+
+ def shrink(v: Int) = resume {
+ var shrunk: Int = v/2
+ while (shrunk != 0 && shrunk != v) {
+ do emit(shrunk)
+ do emit(neg(shrunk))
+ shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 )
+ }
+ do emit(0)
+ }
+ }
+
+// same as other arbitraryInt but for quantifier version that explicitely takes the generator as input
+def arbitraryInt { body: {Generator[Int]} => Unit }: Unit /random = {
+ try body{g} with g: Generator[Int] {
+ def generate() = resume {
+ while (true) {
+ do emit(randomInt(-50, 50))
+ }
+ }
+
+ def shrink(v: Int) = resume {
+ var shrunk: Int = v/2
+ while (shrunk != 0 && shrunk != v) {
+ do emit(shrunk)
+ do emit(neg(shrunk))
+ shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 )
+ }
+ do emit(0)
+ }
+ }
+}
+
+def chooseInt(minInclusive: Int, maxExclusive: Int) { body: => Unit / Generator[Int] }: Unit / random=
+ try body() with Generator[Int] {
+ def generate() = resume {
+ while (true) {
+ do emit(randomInt(minInclusive, maxExclusive))
+ }
+ }
+
+ def shrink(v: Int) = resume {
+ var shrunk: Int = v/2
+ while (shrunk != 0 && shrunk != v) {
+ do emit(shrunk)
+ do emit(neg(shrunk))
+ shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 )
+ }
+ do emit(0)
+ }
+ }
+
+def arbitraryBool { body: => Unit/Generator[Bool] }: Unit / random=
+ try body() with Generator[Bool]{
+ def generate() = resume {
+ while (true) {
+ do emit(randomBool())
+ }
+ }
+
+ def shrink(v: Bool) = resume {
+ var out = v
+ if(v){
+ out = false
+ }else {
+ out = true
+ }
+ do emit(out)
+ }
+ }
+
+def arbitraryDouble { body: => Unit/Generator[Double] }: Unit / random =
+ try body() with Generator[Double]{
+ def generate() = resume {
+ while (true) {
+ do emit(randomDouble())
+ }
+ }
+
+ def shrink(v: Double) = resume {
+ var shrink: Double = v/2.0
+ while (shrink != 0.0 && shrink != v) {
+ do emit(shrink)
+ do emit(neg(shrink))
+ shrink = shrink / 2.0 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 )
+ }
+ do emit(0.0)
+ }
+ }
+
+
+def arbitraryChar { body: => Unit / Generator[Char] }: Unit / random =
+ try body() with Generator[Char] {
+ def generate() = resume {
+ // ASCII printable characters range from 32 to 126
+ while (true) {
+ do emit(randomInt(32, 127).toChar)
+ }
+ }
+
+ def shrink(v: Char) = resume { <> }
+ }
+
+def arbitraryString { body: => Unit / Generator[String] }: Unit / {Generator[Char], random } = {
+ try body() with Generator[String] {
+ def generate() = resume {
+ while (true) {
+ val string: String = collectString {
+ with boundary;
+ with limit[Char](randomInt(1, 20))
+ do generate[Char]()
+ }
+ do emit(string)
+ }
+ }
+
+ def shrink(v: String) = resume { <> }
+ }
+}
+
+def arbitraryString(len: Int) { body: => Unit / Generator[String] }: Unit / { Generator[Char], random } = {
+ try body() with Generator[String] {
+ def generate() = resume {
+ while (true) {
+ val string: String = collectString {
+ with boundary;
+ with limit[Char](len)
+ do generate[Char]()
+ }
+ do emit(string)
+ }
+ }
+
+ def shrink(v: String) = resume { <> }
+ }
+}
+
+def arbitraryList[T] { body: => Unit / Generator[List[T]]} : Unit / { Generator[T], random } = {
+ try body() with Generator[List[T]]{
+ def generate() = resume {
+ while (true) {
+ val list = collectList[T]{
+ with boundary;
+ with limit[T](randomInt(1, 10))
+ do generate[T]()
+ }
+ do emit(list)
+ }
+ }
+
+ def shrink(v: List[T]) = resume { <> }
+ }
+}
+
+def arbitraryList[T](len: Int) { body: => Unit/Generator[List[T]]} : Unit / { Generator[T], random } = {
+ try body() with Generator[List[T]]{
+ def generate() = resume {
+ while (true) {
+ val list = collectList[T]{
+ with boundary;
+ with limit[T](len)
+ do generate[T]()
+ }
+ do emit(list)
+ }
+ }
+
+ def shrink(v: List[T]) = resume { <> }
+ }
+}
+
+def evenNumbers { body: => Unit / Generator[Int] }: Unit / random =
+ try body() with Generator[Int] {
+ def generate() = resume {
+ while(true) {
+ val nextEmit = randomInt(-50, 50)
+ if(nextEmit.mod(2) == 0)
+ do emit(nextEmit)
+ }
+ }
+
+ def shrink(v: Int) = resume {
+ // we know v is even because the generator only produces even numbers
+ var shrink = v - 2
+ while(shrink != 0 && shrink != v){
+ do emit(shrink)
+ shrink = shrink / 2
+ }
+ do emit(0)
+ }
+ }
+
+def alphabeticChar { body: => Unit / Generator[Char] }: Unit / random = {
+ try body() with Generator[Char] {
+ def generate() = resume {
+ // ASCII printable characters range from 32 to 126
+ val min = 32
+ val max = 127 // max is exclusive in randomInt
+ while (true) {
+ do emit(randomInt(65, 91).toChar)
+ }
+ }
+
+ def shrink(v: Char) = resume { <> }
+ }
+}
+
interface Test {
def success(name: String, duration: Int): Unit
+ def successForall(name: String, passed: Int, duration: Int): Unit
+ def successExists(name: String, tried: Int, msg: String, duration: Int): Unit
def failure(name: String, msg: String, duration: Int): Unit
+ def failureForall(name: String, passed: Int, msg: String, duration: Int): Unit
+ def failureExists(name: String, tried: Int, duration: Int): Unit
}
/// Runs the `body` as a test under the given `name`
@@ -80,6 +310,185 @@ def test(name: String) { body: => Unit / Assertion } = {
}
}
+/// forall quantifier for property-based testing
+/// version that uses the closest generator in scope (fully value-based generation)
+def forall[A](name: String, n: Int)
+ { body: A => Unit / Assertion }: Unit / { Test, Generator[A] } = {
+
+ val startTime = bench::relativeTimestamp()
+ var successCounter = 0
+ with boundary;
+ with val x = for[A] { with limit[A](n + 1); do generate[A]() }
+
+ try {
+ body(x)
+ if(successCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successForall(name, n, duration)
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if (condition) {
+ successCounter = successCounter + 1
+ resume(())}
+ else {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureForall(name, successCounter, msg ++ "\n failed on input:\n 1. " ++ genericShow(x), duration)
+ do stop()
+ }
+ }
+}
+
+/// forall quantifier for property-based testing
+/// version that that explicitely gets the generators as inputs (type-based-style generation)
+def forall[A](name: String, n: Int)
+ { g: Generator[A]}
+ { body: A => Unit / Assertion }: Unit / {Test} = {
+
+ val startTime = bench::relativeTimestamp()
+ var successCounter = 0
+ with boundary
+ with val x = for[A] {with limit[A](n + 1); g.generate[A]()}
+
+ try {
+ body(x)
+ if(successCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successForall(name, n, duration)
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if(condition) {
+ successCounter = successCounter + 1
+ resume(())
+ }
+ else {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureForall(name, successCounter, msg ++ "\n failed on input:\n 1. " ++ genericShow(x), duration)
+ do stop()
+ }
+ }
+}
+
+def forall[A, B](name: String, n: Int)
+ { body: (A, B) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B]} = {
+
+ val startTime = bench::relativeTimestamp()
+ var successCounter = 0
+ with boundary;
+ with val x = for[A] {with limit[A](n + 1); do generate[A]()}
+ with val y = for[B] {with limit[B](n + 1); do generate[B]()}
+
+ try {
+ body(x, y)
+ if(successCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successForall(name, n, duration)
+ do stop()
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if(condition) {
+ successCounter = successCounter + 1
+ resume(())
+ }
+ else {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureForall(name, successCounter, msg ++ "\n failed on inputs:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y), duration)
+ do stop()
+ }
+ }
+}
+
+def forall[A, B, C](name: String, n: Int)
+ { body: (A, B, C) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B], Generator[C]} = {
+
+ val startTime = bench::relativeTimestamp()
+ var successCounter = 0
+ with boundary;
+ with val x = for[A] {with limit[A](n + 1); do generate[A]()}
+ with val y = for[B] {with limit[B](n + 1); do generate[B]()}
+ with val z = for[C] {with limit[C](n + 1); do generate[C]()}
+
+ try {
+ body(x, y, z)
+ if(successCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successForall(name, n, duration)
+ do stop()
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if(condition) {
+ successCounter = successCounter + 1
+ resume(())
+ }
+ else {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureForall(name, successCounter, msg ++ "\n failed on inputs:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y) ++ "\n 3. " ++ genericShow(z), duration)
+ do stop()
+ }
+ }
+}
+
+/// exists quantifier for property-based testing
+def exists[A](name: String, n: Int)
+ { body: A => Unit / Assertion }: Unit / {Test, Generator[A]} = {
+
+ val startTime = bench::relativeTimestamp()
+ var triedCounter = 0
+ with boundary
+ with val x = for[A] {with limit[A](n + 1); do generate[A]()}
+
+ try {
+ body(x)
+ if(triedCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureExists(name, triedCounter, duration)
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if (condition) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successExists(name, triedCounter, " example value:\n 1. " ++ genericShow(x), duration)
+ do stop()
+ }
+ else {
+ triedCounter = triedCounter + 1
+ resume(())
+ }
+ }
+}
+
+def exists[A, B](name: String, n: Int)
+ { body: (A, B) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B]} = {
+
+ val startTime = bench::relativeTimestamp()
+ var triedCounter = 0
+ with boundary
+ with val x = for[A] {with limit[A](n + 1); do generate[A]()}
+ with val y = for[B] {with limit[B](n + 1); do generate[B]()}
+
+ try {
+ body(x, y)
+ if(triedCounter == n) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do failureExists(name, triedCounter, duration)
+ }
+ } with Assertion {
+ def assert(condition, msg) =
+ if(condition) {
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
+ do successExists(name, triedCounter, "\n example values:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y), duration)
+ do stop()
+ }
+ else {
+ triedCounter = triedCounter + 1
+ resume(())
+ }
+ }
+}
+
/// Run a test suite with a given `name`.
/// - If `printTimes` is `true` (or missing), prints out time in milliseconds.
/// - Formats automatically using ANSI escapes.
@@ -91,6 +500,13 @@ def test(name: String) { body: => Unit / Assertion } = {
/// test("1 + 1 == 2") {
/// assertEqual(1 + 1, 2)
/// }
+/// with arbitraryInt;
+/// forall[Int]("reverse-singleton", 100){ x =>
+/// assertEqual(
+/// reverse([x]),
+/// [x]
+/// )
+/// }
/// }
/// ```
def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = {
@@ -107,8 +523,13 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted }
if (n == 0) { dim(s) }
else { colorIfNonZero(s) }
- var failed = 0
- var passed = 0
+ var failedUnit = 0
+ var passedUnit = 0
+ var failedForall = 0
+ var passedForall = 0
+ var failedExists = 0
+ var passedExists = 0
+ // TODO check if computing time works correctly
// 1) Print the name of the test
println(name.bold)
@@ -116,31 +537,71 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted }
// 2) Run the tests, timing them
val totalDuration = timed {
try { body() } with Test {
- // 2a) Handle a passing test on success
+ // 2a) Handle a passing unit test on success
def success(name, duration) = {
- passed = passed + 1
+ passedUnit = passedUnit + 1
println("✓".green ++ " " ++ name ++ duration.ms)
resume(())
}
- // 2b) Handle a failing test on failure, additionally printing its message
+ // 2b) Handle a failing unit test on failure, additionally printing its message
def failure(name, msg, duration) = {
- failed = failed + 1
+ failedUnit = failedUnit + 1
println("✕".red ++ " " ++ name ++ duration.ms)
println(" " ++ msg.red)
resume(())
}
+
+ // 2c) Handle a passing forall test on success
+ def successForall(name, passed, duration) = {
+ passedForall = passedForall + 1
+ println("✓".green ++ " " ++ name ++ ", passed " ++ passed.show ++ " tests " ++ duration.ms)
+ resume(())
+ }
+
+ // 2d) Handle a failing forall test on failure, additionally printing its message
+ def failureForall(name, passed, msg, duration) = {
+ failedForall = failedForall + 1
+ println("✕".red ++ " " ++ name ++ duration.ms)
+ if(passed == 1){
+ println(" ! Falsified after " ++ show(passed).red ++ " passed test:" ++ duration.ms)
+ } else { println(" ! Falsified after " ++ show(passed).red ++ " passed tests:" ++ duration.ms) }
+ println(" " ++ msg.red)
+ resume(())
+ }
+
+ // 2e) Handle a passing exists test on success
+ def successExists(name, tried, msg, duration) = {
+ passedExists = passedExists + 1
+ //println("✓".green ++ " " ++ name ++ ", found after " ++ tried.show ++ " tests " ++ duration.ms)
+ println("✓".green ++ " " ++ name ++ duration.ms)
+ if(tried == 1){
+ println(" Verified after " ++ tried.show ++ " tried input")
+ } else { println(" Verified after " ++ tried.show ++ " tried inputs") }
+ println(" " ++ msg.green)
+ resume(())
+ }
+
+ // 2f) Handle a failing exists test on failure, additionally printing its message
+ def failureExists(name, tried, duration) = {
+ failedExists = failedExists + 1
+ println("✕".red ++ " " ++ name ++ duration.ms)
+ println(" ! Tried " ++ tried.show ++ " different inputs but all failed" ++ duration.ms)
+ resume(())
+ }
}
}
// 3) Format the test results
+ println(" ")
+ println(" " ++ (show(passedUnit + passedForall + passedExists) ++ " pass").dimWhenZeroElse(passedUnit + passedForall + passedExists) { green })
+ println(" " ++ (show(failedUnit + failedForall + failedExists) ++ " fail").dimWhenZeroElse(failedUnit + failedForall + failedExists) { red })
+ println(" " ++ (passedUnit + failedUnit + passedForall + failedForall + passedExists + failedExists).show ++ " test(s) total" ++ totalDuration.ms)
+
println("")
- println(" " ++ (passed.show ++ " pass").dimWhenZeroElse(passed) { green })
- println(" " ++ (failed.show ++ " fail").dimWhenZeroElse(failed) { red })
- println(" " ++ (passed + failed).show ++ " tests total" ++ totalDuration.ms)
// 4) Return true if all tests succeeded, otherwise false
- return failed == 0
+ return failedUnit == 0 && failedForall == 0 && failedExists == 0
}
/// See `suite` above.
@@ -157,4 +618,4 @@ def mainSuite(name: String) { body: => Unit / { Test, Formatted } }: Unit = {
val result = suite(name, true) { body }
val exitCode = if (result) 0 else 1
exit(exitCode)
-}
+}
\ No newline at end of file
diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js
index b1b5ff2e1..bb149178d 100644
--- a/libraries/js/effekt_runtime.js
+++ b/libraries/js/effekt_runtime.js
@@ -1,69 +1,79 @@
-
-// Common Runtime
-// --------------
-function Cell(init, region) {
- const cell = {
- value: init,
- backup: function() {
- const _backup = cell.value;
- // restore function (has a STRONG reference to `this`)
- return () => { cell.value = _backup; return cell }
- }
- }
- return cell;
+// Complexity of state:
+//
+// get: O(1)
+// set: O(1)
+// capture: O(1)
+// restore: O(|write operations since capture|)
+const Mem = null
+
+function Arena() {
+ const s = {
+ root: { value: Mem },
+ generation: 0,
+ fresh: (v) => {
+ const r = {
+ value: v,
+ generation: s.generation,
+ store: s,
+ set: (v) => {
+ const s = r.store
+ const r_gen = r.generation
+ const s_gen = s.generation
+
+ if (r_gen == s_gen) {
+ r.value = v;
+ } else {
+ const root = { value: Mem }
+ // update store
+ s.root.value = { ref: r, value: r.value, generation: r_gen, root: root }
+ s.root = root
+ r.value = v
+ r.generation = s_gen
+ }
+ }
+ };
+ return r
+ },
+ // not implemented
+ newRegion: () => s
+ };
+ return s
}
-const global = {
- fresh: Cell
+function snapshot(s) {
+ const snap = { store: s, root: s.root, generation: s.generation }
+ s.generation = s.generation + 1
+ return snap
}
-function Arena(_region) {
- const region = _region;
- return {
- fresh: function(init) {
- const cell = Cell(init);
- // region keeps track what to backup, but we do not need to backup unreachable cells
- region.push(cell) // new WeakRef(cell))
- return cell;
- },
- region: _region,
- newRegion: function() {
- // a region aggregates weak references
- const nested = Arena([])
- // this doesn't work yet, since Arena.backup doesn't return a thunk
- region.push(nested) //new WeakRef(nested))
- return nested;
- },
- backup: function() {
- const _backup = []
- let nextIndex = 0;
- for (const ref of region) {
- const cell = ref //.deref()
- // only backup live cells
- if (cell) {
- _backup[nextIndex] = cell.backup()
- nextIndex++
- }
- }
- function restore() {
- const region = []
- let nextIndex = 0;
- for (const restoreCell of _backup) {
- region[nextIndex] = restoreCell() // new WeakRef(restoreCell())
- nextIndex++
- }
- return Arena(region)
- }
- return restore;
- }
- }
+function reroot(n) {
+ if (n.value === Mem) return;
+
+ const diff = n.value
+ const r = diff.ref
+ const v = diff.value
+ const g = diff.generation
+ const n2 = diff.root
+ reroot(n2)
+ n.value = Mem
+ n2.value = { ref: r, value: r.value, generation: r.generation, root: n}
+ r.value = v
+ r.generation = g
}
+function restore(store, snap) {
+ // linear in the number of modifications...
+ reroot(snap.root)
+ store.root = snap.root
+ store.generation = snap.generation + 1
+}
+// Common Runtime
+// --------------
let _prompt = 1;
const TOPLEVEL_K = (x, ks) => { throw { computationIsDone: true, result: x } }
-const TOPLEVEL_KS = { prompt: 0, arena: Arena([]), rest: null }
+const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null }
function THUNK(f) {
f.thunk = true
@@ -80,9 +90,6 @@ function CAPTURE(body) {
const RETURN = (x, ks) => ks.rest.stack(x, ks.rest)
-// const r = ks.arena.newRegion(); body
-// const x = r.alloc(init); body
-
// HANDLE(ks, ks, (p, ks, k) => { STMT })
function RESET(prog, ks, k) {
const prompt = _prompt++;
@@ -90,13 +97,6 @@ function RESET(prog, ks, k) {
return prog(prompt, { prompt, arena: Arena([]), rest }, RETURN)
}
-function DEALLOC(ks) {
- const arena = ks.arena
- if (!!arena) {
- arena.length = arena.length - 1
- }
-}
-
function SHIFT(p, body, ks, k) {
// TODO avoid constructing this object
@@ -104,13 +104,15 @@ function SHIFT(p, body, ks, k) {
let cont = null
while (!!meta && meta.prompt !== p) {
- cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont }
+ let store = meta.arena
+ cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont }
meta = meta.rest
}
if (!meta) { throw `Prompt not found ${p}` }
// package the prompt itself
- cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont }
+ let store = meta.arena
+ cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont }
meta = meta.rest
const k1 = meta.stack
@@ -123,7 +125,8 @@ function RESUME(cont, c, ks, k) {
let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest }
let toRewind = cont
while (!!toRewind) {
- meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.backup(), rest: meta }
+ restore(toRewind.arena, toRewind.backup)
+ meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta }
toRewind = toRewind.rest
}
diff --git a/libraries/llvm/array.c b/libraries/llvm/array.c
deleted file mode 100644
index df0950b82..000000000
--- a/libraries/llvm/array.c
+++ /dev/null
@@ -1,57 +0,0 @@
-#ifndef EFFEKT_ARRAY_C
-#define EFFEKT_ARRAY_C
-
-/** We represent arrays like positive types.
- * The tag is 0 and the obj points to memory with the following layout:
- *
- * +--[ Header ]--+------+------------+
- * | Rc | Eraser | Size | Fields ... |
- * +--------------+------+------------+
- */
-
-void c_array_erase_fields(void *envPtr) {
- uint64_t *sizePtr = envPtr;
- struct Pos *dataPtr = envPtr + sizeof(uint64_t);
- uint64_t size = *sizePtr;
- for (uint64_t i = 0; i < size; i++) {
- erasePositive(dataPtr[i]);
- }
-}
-
-struct Pos c_array_new(const Int size) {
- void *objPtr = calloc(sizeof(struct Header) + sizeof(uint64_t) + size * sizeof(struct Pos), 1);
- struct Header *headerPtr = objPtr;
- uint64_t *sizePtr = objPtr + sizeof(struct Header);
- *headerPtr = (struct Header) { .rc = 0, .eraser = c_array_erase_fields, };
- *sizePtr = size;
- return (struct Pos) {
- .tag = 0,
- .obj = objPtr,
- };
-}
-
-Int c_array_size(const struct Pos arr) {
- uint64_t *sizePtr = arr.obj + sizeof(struct Header);
- uint64_t size = *sizePtr;
- erasePositive(arr);
- return size;
-}
-
-struct Pos c_array_get(const struct Pos arr, const Int index) {
- struct Pos *dataPtr = arr.obj + sizeof(struct Header) + sizeof(uint64_t);
- struct Pos element = dataPtr[index];
- sharePositive(element);
- erasePositive(arr);
- return element;
-}
-
-struct Pos c_array_set(const struct Pos arr, const Int index, const struct Pos value) {
- struct Pos *dataPtr = arr.obj + sizeof(struct Header) + sizeof(uint64_t);
- struct Pos element = dataPtr[index];
- erasePositive(element);
- dataPtr[index] = value;
- erasePositive(arr);
- return Unit;
-}
-
-#endif
diff --git a/libraries/llvm/main.c b/libraries/llvm/main.c
index 2f54374c2..38c5f7133 100644
--- a/libraries/llvm/main.c
+++ b/libraries/llvm/main.c
@@ -13,8 +13,6 @@
#include "bytearray.c"
#include "io.c"
#include "panic.c"
-#include "ref.c"
-#include "array.c"
extern void effektMain();
diff --git a/libraries/llvm/ref.c b/libraries/llvm/ref.c
deleted file mode 100644
index 48620a42c..000000000
--- a/libraries/llvm/ref.c
+++ /dev/null
@@ -1,47 +0,0 @@
-#ifndef EFFEKT_REF_C
-#define EFFEKT_REF_C
-
-/** We represent references like positive types.
- * The tag is 0 and the obj points to memory with the following layout:
- *
- * +--[ Header ]--------------+------------+
- * | ReferenceCount | Eraser | Field |
- * +--------------------------+------------+
- */
-
-void c_ref_erase_field(void *envPtr) {
- struct Pos *fieldPtr = envPtr;
- struct Pos element = *fieldPtr;
- erasePositive(element);
-}
-
-struct Pos c_ref_fresh(const struct Pos value) {
- void *objPtr = malloc(sizeof(struct Header) + sizeof(struct Pos));
- struct Header *headerPtr = objPtr;
- struct Pos *fieldPtr = objPtr + sizeof(struct Header);
- *headerPtr = (struct Header) { .rc = 0, .eraser = c_ref_erase_field, };
- *fieldPtr = value;
- return (struct Pos) {
- .tag = 0,
- .obj = objPtr,
- };
-}
-
-struct Pos c_ref_get(const struct Pos ref) {
- struct Pos *fieldPtr = ref.obj + sizeof(struct Header);
- struct Pos element = *fieldPtr;
- sharePositive(element);
- erasePositive(ref);
- return element;
-}
-
-struct Pos c_ref_set(const struct Pos ref, const struct Pos value) {
- struct Pos *fieldPtr = ref.obj + sizeof(struct Header);
- struct Pos element = *fieldPtr;
- erasePositive(element);
- *fieldPtr = value;
- erasePositive(ref);
- return Unit;
-}
-
-#endif
diff --git a/libraries/llvm/rts.ll b/libraries/llvm/rts.ll
index 52dbc9ed6..861665905 100644
--- a/libraries/llvm/rts.ll
+++ b/libraries/llvm/rts.ll
@@ -678,14 +678,7 @@ define private void @freeStack(%StackPointer %stackPointer) alwaysinline {
; RTS initialization
define private tailcc void @topLevel(%Pos %val, %Stack %stack) {
- %rest = call %Stack @underflowStack(%Stack %stack)
- ; rest holds global variables
- call void @resume_Pos(%Stack %rest, %Pos %val)
- ret void
-}
-
-define private tailcc void @globalsReturn(%Pos %val, %Stack %stack) {
- %rest = call %Stack @underflowStack(%Stack %stack)
+ call %Stack @underflowStack(%Stack %stack)
ret void
}
@@ -699,29 +692,8 @@ define private void @topLevelEraser(%Environment %environment) {
ret void
}
-@global = private global { i64, %Stack } { i64 0, %Stack null }
-
define private %Stack @withEmptyStack() {
- %globals = call %Stack @reset(%Stack null)
-
- %globalsStackPointer_pointer = getelementptr %StackValue, %Stack %globals, i64 0, i32 1
- %globalsStackPointer = load %StackPointer, ptr %globalsStackPointer_pointer, !alias.scope !11, !noalias !21
-
- %returnAddressPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 0
- %sharerPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 1
- %eraserPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 2
-
- store ptr @globalsReturn, ptr %returnAddressPointer.0, !alias.scope !12, !noalias !22
- store ptr @topLevelSharer, ptr %sharerPointer.0, !alias.scope !12, !noalias !22
- store ptr @topLevelEraser, ptr %eraserPointer.0, !alias.scope !12, !noalias !22
-
- %globalsStackPointer_2 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 1
- store %StackPointer %globalsStackPointer_2, ptr %globalsStackPointer_pointer, !alias.scope !11, !noalias !21
-
- %stack = call %Stack @reset(%Stack %globals)
-
- %globalStack = getelementptr %PromptValue, %Prompt @global, i64 0, i32 1
- store %Stack %stack, ptr %globalStack, !alias.scope !13, !noalias !23
+ %stack = call %Stack @reset(%Stack null)
%stackStackPointer = getelementptr %StackValue, %Stack %stack, i64 0, i32 1
%stackPointer = load %StackPointer, ptr %stackStackPointer, !alias.scope !11, !noalias !21
diff --git a/package.json b/package.json
index 6be421eb7..ce5b60d60 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@effekt-lang/effekt",
"author": "Jonathan Brachthäuser",
- "version": "0.24.0",
+ "version": "0.28.0",
"repository": {
"type": "git",
"url": "git+https://github.com/effekt-lang/effekt.git"
diff --git a/pom.xml b/pom.xml
index a84b1feb6..3e50f66b7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
de.bstudios
Effekt
Effekt
- 0.24.0
+ 0.28.0
4.0.0
diff --git a/project/EffektVersion.scala b/project/EffektVersion.scala
index 6cb43b385..d36205607 100644
--- a/project/EffektVersion.scala
+++ b/project/EffektVersion.scala
@@ -1,4 +1,4 @@
// Don't change this file without changing the CI too!
import sbt.*
import sbt.Keys.*
-object EffektVersion { lazy val effektVersion = "0.24.0" }
+object EffektVersion { lazy val effektVersion = "0.28.0" }
diff --git a/project/project/metals.sbt b/project/project/metals.sbt
new file mode 100644
index 000000000..ce00deb36
--- /dev/null
+++ b/project/project/metals.sbt
@@ -0,0 +1,8 @@
+// format: off
+// DO NOT EDIT! This file is auto-generated.
+
+// This file enables sbt-bloop to create bloop config files.
+
+addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.9")
+
+// format: on
diff --git a/project/project/project/metals.sbt b/project/project/project/metals.sbt
new file mode 100644
index 000000000..ce00deb36
--- /dev/null
+++ b/project/project/project/metals.sbt
@@ -0,0 +1,8 @@
+// format: off
+// DO NOT EDIT! This file is auto-generated.
+
+// This file enables sbt-bloop to create bloop config files.
+
+addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.9")
+
+// format: on