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