diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 8b99f0a2f7..3202096ec5 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -61,6 +61,24 @@ jobs: - name: Test effekt binary run: effekt.sh --help + # These are currently separated as they take a long time to run. + core-reparse-tests: + name: "Core Reparse Tests" + needs: build-and-compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: 'true' + + - uses: ./.github/actions/setup-effekt + + - uses: ./.github/actions/restore-build-cache + + - name: Run core reparse tests + run: | + sbt effektJVM/testCoreReparsing + windows-tests: name: "Windows Smoke Test" needs: build-and-compile diff --git a/build.sbt b/build.sbt index 535a5a30c4..818bd80fdc 100644 --- a/build.sbt +++ b/build.sbt @@ -17,6 +17,7 @@ lazy val bumpMinorVersion = taskKey[Unit]("Bumps the minor version number (used lazy val testBackendJS = taskKey[Unit]("Run JavaScript backend tests") lazy val testBackendChez = taskKey[Unit]("Run Chez Scheme backend tests") lazy val testBackendLLVM = taskKey[Unit]("Run LLVM backend tests") +lazy val testCoreReparsing = taskKey[Unit]("Run core reparsing tests") lazy val testRemaining = taskKey[Unit]("Run all non-backend tests (internal tests) on effektJVM") lazy val noPublishSettings = Seq( @@ -230,13 +231,19 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e ).value }, + testCoreReparsing := { + (Test / testOnly).toTask( + " effekt.core.ReparseTests" + ).value + }, + testRemaining := Def.taskDyn { val log = streams.value.log val allTests = (Test / definedTestNames).value.toSet - // Explicit list of backend tests (union of all testBackend targets) - val backendTests = Set( + // Explicit list of tests run separately (union of all testBackend targets) + val separatedTests = Set( "effekt.JavaScriptTests", "effekt.StdlibJavaScriptTests", "effekt.ChezSchemeMonadicTests", @@ -245,10 +252,11 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e "effekt.StdlibChezSchemeCallCCTests", "effekt.LLVMTests", "effekt.LLVMNoValgrindTests", - "effekt.StdlibLLVMTests" + "effekt.StdlibLLVMTests", + "effekt.core.ReparseTests", ) - val remaining = allTests -- backendTests + val remaining = allTests -- separatedTests if (remaining.isEmpty) { log.info("No remaining tests") diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index 95e4bba117..144223bcd0 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -47,14 +47,19 @@ trait CoreTests extends munit.FunSuite { expected: ModuleDecl, clue: => Any = "values are not alpha-equivalent", names: Names = Names(defaultNames))(using Location): Unit = { - val renamer = TestRenamer(names, "$") - shouldBeEqual(renamer(obtained), renamer(expected), clue) + val renamer = TestRenamer(names) + val obtainedRenamed = renamer(obtained) + val expectedRenamed = renamer(expected) + val obtainedPrinted = effekt.core.PrettyPrinter.format(obtainedRenamed).layout + val expectedPrinted = effekt.core.PrettyPrinter.format(expectedRenamed).layout + assertEquals(obtainedPrinted, expectedPrinted) + shouldBeEqual(obtainedRenamed, expectedRenamed, clue) } def assertAlphaEquivalentStatements(obtained: Stmt, expected: Stmt, clue: => Any = "values are not alpha-equivalent", names: Names = Names(defaultNames))(using Location): Unit = { - val renamer = TestRenamer(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 69a4df793f..f31c37d066 100644 --- a/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala @@ -22,7 +22,8 @@ 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 = TestRenamer(names).rewrite(pInput) + val renamer = TestRenamer(names) + val renamed = renamer(pInput) val obtained = transform(renamed) assertAlphaEquivalent(obtained, pExpected, "Not transformed to") diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala new file mode 100644 index 0000000000..e3aef4a6f0 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -0,0 +1,123 @@ +package effekt.core + +import effekt.* +import effekt.PhaseResult.CoreTransformed +import effekt.context.{Context, IOModuleDB} +import effekt.util.PlainMessaging +import kiama.output.PrettyPrinterTypes.Document +import kiama.util.{Source, StringSource} +import munit.Location +import sbt.io.* +import sbt.io.syntax.* + +import java.io.File + +/* +// * This test suite ensures that the core pretty-printer always produces reparsable code. + */ +class ReparseTests extends CoreTests { + object plainMessaging extends PlainMessaging + object context extends Context with IOModuleDB { + val messaging = plainMessaging + + object frontend extends CompileToCore + + override lazy val compiler = frontend.asInstanceOf + } + + // The sources of all test files are stored here: + def examplesDir = new File("examples") + + // Test files which are to be ignored (since features are missing or known bugs exist) + def ignored: Set[File] = Set( + // Missing include: text/pregexp.scm + File("examples/pos/simpleparser.effekt"), + // Cannot find source for unsafe/cont + File("examples/pos/propagators.effekt"), + // Bidirectional effects that mention the same effect recursively are not (yet) supported. + File("examples/pos/bidirectional/selfrecursion.effekt"), + // FIXME: Wrong number of type arguments + File("examples/pos/type_omission_op.effekt"), + // FIXME: There is currently a limitation in TestRenamer in that it does not rename captures. + // This means, that in this example, captures for State[Int] and State[String] are both printed as "State", + // leading to a collapse of the capture set {State_1, State_2} to just {State}. + File("examples/pos/parametrized.effekt") + ) + + def positives: Set[File] = Set( + examplesDir / "pos", + examplesDir / "casestudies", + examplesDir / "benchmarks", + ) + + def runTests() = positives.foreach(runPositiveTestsIn) + + def runPositiveTestsIn(dir: File): Unit = + foreachFileIn(dir) { + // We don't currently test *.effekt.md files + f => if (!ignored.contains(f) && !f.getName.endsWith(".md")) { + test(s"${f.getPath}") { + toCoreThenReparse(f) + } + } + } + + def toCoreThenReparse(input: File) = { + val content = IO.read(input) + val config = new EffektConfig(Seq("--Koutput", "string")) + config.verify() + context.setup(config) + val (_, _, coreMod: ModuleDecl) = context.frontend.compile(StringSource(content, "input.effekt"))(using context).map { + case (_, decl) => decl + }.getOrElse { + val errors = plainMessaging.formatMessages(context.messaging.buffer) + sys error errors + } + val renamer = TestRenamer(Names(defaultNames)) + val expectedRenamed = renamer(coreMod) + val printed = core.PrettyPrinter.format(expectedRenamed).layout + val reparsed: ModuleDecl = parse(printed)(using Location.empty) + val reparsedRenamed = renamer(reparsed) + val reparsedPrinted = core.PrettyPrinter.format(reparsedRenamed).layout + val expectedPrinted = core.PrettyPrinter.format(expectedRenamed).layout + assertEquals(reparsedPrinted, expectedPrinted) + } + + def foreachFileIn(file: File)(test: File => Unit): Unit = + file match { + case f if f.isDirectory => + f.listFiles.foreach(foreachFileIn(_)(test)) + case f if f.getName.endsWith(".effekt") || f.getName.endsWith(".effekt.md") => + test(f) + case _ => () + } + + runTests() +} + +/** + * A "backend" that simply outputs the core module for a given Effekt source program. + */ +class CompileToCore extends Compiler[(Id, symbols.Module, ModuleDecl)] { + def extension = ".effekt-core.ir" + + // Support all the feature flags so that we can test all extern declarations + override def supportedFeatureFlags: List[String] = List("vm", "js", "jsNode", "chez", "llvm") + + override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = None + + override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = None + + override def compile(source: Source)(using C: Context): Option[(Map[String, String], (Id, symbols.Module, ModuleDecl))] = + Optimized.run(source).map { res => (Map.empty, res) } + + lazy val Core = Phase.cached("core") { + Frontend andThen Middleend + } + + lazy val Optimized = allToCore(Core) andThen Aggregate map { + case input @ CoreTransformed(source, tree, mod, core) => + val mainSymbol = Context.ensureMainExists(mod) + (mainSymbol, mod, core) + } +} diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index fdcb58b351..0d44dde011 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -1,7 +1,7 @@ package effekt.core -import effekt.{ core, symbols } -import effekt.context.Context +import effekt.{Template, core, symbols} +import effekt.symbols.builtins /** * Freshens bound names in a given term for tests. @@ -14,10 +14,12 @@ import effekt.context.Context * * @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 { +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 + // Top-level items in the current module. Collected to check for free variables. + private var toplevelScope: Map[Id, Id] = Map.empty // Here we track ALL renamings var renamed: Map[Id, Id] = Map.empty @@ -25,8 +27,19 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends private var suffix: Int = 0 def freshIdFor(id: Id): Id = + // For pre-registered names, like `main` in certain test cases, we use the registered symbol. + if (names.isKnown(id)) { + return names.getKnown(id).get + } + // HACK: This is an unfortunate hack. + // TestRenamer is often used to check for alpha-equivalence by renaming both sides of a comparison. + // However, Effekt requires globally unique Barendregt indices for all symbols, so just creating fresh + // Ids is not sufficient. We also need to cache these fresh Ids, so that both sides of an alpha-equivalence + // comparison get the same fresh Id for a given original Id. + // This is achieved by generating a deterministic string `uniqueName` on both sides, and looking it up in `names`, + // which generates a unique Id for it once and reuses it on subsequent lookups. + val uniqueName = prefix + suffix.toString 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 = @@ -41,11 +54,27 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends /** Alias for withBindings(List(id)){...} */ def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f) - // free variables are left untouched + // Top-level items may be mutually recursive. This means that a bound occurrence may precede its binding. + // We use a separate pass to collect all top-level ids, so that we can distinguish them from free variables. override def id: PartialFunction[core.Id, core.Id] = { - case id => scopes.collectFirst { - case bnds if bnds.contains(id) => bnds(id) - }.getOrElse(id) + case id => + if (builtins.isCoreBuiltin(id)) { + // builtin, do not rename + id + } else { + scopes.collectFirst { + // locally bound variable + case bnds if bnds.contains(id) => bnds(id) + }.getOrElse { + if (toplevelScope.contains(id)) { + // id references a top-level item + toplevelScope(id) + } else { + // free variable, do not rename + id + } + } + } } override def stmt: PartialFunction[Stmt, Stmt] = { @@ -96,7 +125,7 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends 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, + Operation(rewrite(name), tparams map rewrite, cparams map rewrite, vparams map rewrite, @@ -105,15 +134,140 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends } } + override def rewrite(toplevel: Toplevel): Toplevel = toplevel match { + case Toplevel.Def(id, block) => + // We don't use withBinding here, because top-level ids are pre-collected. + Toplevel.Def(rewrite(id), rewrite(block)) + case Toplevel.Val(id, tpe, binding) => + val resolvedBinding = rewrite(binding) + // We don't use withBinding here, because top-level ids are pre-collected. + Toplevel.Val(rewrite(id), rewrite(tpe), resolvedBinding) + } + + override def rewrite(d: Declaration): Declaration = d match { + case Declaration.Data(id: Id, tparams: List[Id], constructors: List[Constructor]) => + // We don't use withBinding(id) here, because top-level ids are pre-collected. + withBindings(tparams) { + Declaration.Data(rewrite(id), tparams map rewrite, constructors map rewrite) + } + case Declaration.Interface(id: Id, tparams: List[Id], properties: List[Property]) => + // We don't use withBinding(id) here, because top-level ids are pre-collected. + withBindings(tparams) { + Declaration.Interface(rewrite(id), tparams map rewrite, properties map rewrite) + } + } + + override def rewrite(e: ExternBody) = e match { + case ExternBody.StringExternBody(featureFlag, contents) => + ExternBody.StringExternBody(featureFlag, rewriteTemplate(contents)) + case ExternBody.Unsupported(err) => ??? + } + + def rewriteTemplate(t: Template[Expr]) = t match { + case Template(strings, args) => Template(strings, args map rewrite) + } + + override def rewrite(e: Extern) = e match { + case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => { + // We don't use withBinding(id) here, because top-level ids are pre-collected. + withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) { + Extern.Def( + rewrite(id), + tparams map rewrite, + cparams map rewrite, + vparams map rewrite, + bparams map rewrite, + rewrite(ret), + rewrite(annotatedCapture), + rewrite(body) + ) + } + } + case Extern.Include(featureFlag, contents) => { + Extern.Include(featureFlag, contents) + } + } + + override def rewrite(c: Constructor) = c match { + case Constructor(id, tparams, fields) => + // We don't use withBinding(id) here, because top-level ids are pre-collected. + withBindings(tparams) { + Constructor(rewrite(id), tparams map rewrite, fields map rewrite) + } + } + + override def rewrite(p: Property) = p match { + // We don't use withBinding here, because top-level ids are pre-collected. + case Property(id: Id, tpe: BlockType) => Property(rewrite(id), rewrite(tpe)) + } + + override def rewrite(f: Field) = f match { + // We don't use withBinding here, because top-level ids are pre-collected. + case Field(id, tpe) => Field(rewrite(id), rewrite(tpe)) + } + + override def rewrite(b: BlockType): BlockType = b match { + case BlockType.Function(tparams, cparams, vparams, bparams, result) => + withBindings(tparams ++ cparams) { + BlockType.Function(tparams map rewrite, cparams map rewrite, vparams map rewrite, bparams map rewrite, rewrite(result)) + } + case BlockType.Interface(name, targs) => + BlockType.Interface(rewrite(name), targs map rewrite) + } + + override def rewrite(b: BlockType.Interface) = b match { + case BlockType.Interface(name, targs) => + BlockType.Interface(rewrite(name), targs map rewrite) + } + + override def rewrite(t: ValueType): ValueType = t match { + case ValueType.Var(name) => ValueType.Var(rewrite(name)) + case ValueType.Data(name, targs) => { + val newName = rewrite(name) + ValueType.Data(rewrite(name), targs map rewrite) + } + case ValueType.Boxed(tpe, capt) => ValueType.Boxed(rewrite(tpe), rewrite(capt)) + } + + override def rewrite(t: ValueType.Data): ValueType.Data = t match { + case ValueType.Data(name: Id, targs: List[ValueType]) => ValueType.Data(rewrite(name), targs map rewrite) + } + + override def rewrite(b: BlockParam): BlockParam = b match { + case BlockParam(id, tpe, capt) => BlockParam(rewrite(id), rewrite(tpe), rewrite(capt)) + } + + override def rewrite(v: ValueParam): ValueParam = v match { + case ValueParam(id, tpe) => ValueParam(rewrite(id), rewrite(tpe)) + } + def apply(m: core.ModuleDecl): core.ModuleDecl = suffix = 0 + scopes = List.empty m match { case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) => - core.ModuleDecl(path, includes, declarations, externs, definitions map rewrite, exports) + // Collect toplevel ids, so that we can distinguish a top-level definition bound later + // from a free variable when deciding on whether to freshen an id or not. + toplevelScope = collectToplevelIds(m).map(id => id -> freshIdFor(id)).toMap + core.ModuleDecl(path, includes, declarations map rewrite, externs map rewrite, definitions map rewrite, exports map rewrite) } def apply(s: Stmt): Stmt = { suffix = 0 + toplevelScope = Map.empty + scopes = List.empty rewrite(s) } + + def collectToplevelIds(m: core.ModuleDecl): Iterable[Id] = + m match { + case core.ModuleDecl (path, includes, declarations, externs, definitions, exports) => + declarations.flatMap { + case Declaration.Data(id, tparams, constructors) => constructors.map(_.id) :+ id + case Interface(id, tparams, properties) => properties.map(_.id) :+ id + } ++ definitions.map(_.id) ++ externs.flatMap { + case Extern.Def(id, _, _, _, _, _, _, _) => Some(id) + case Extern.Include(_, _) => None + } + } } \ 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 index 4f1a95113d..8e4eaeafdb 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -14,18 +14,29 @@ class TestRenamerTests extends CoreTests { 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) + val obtainedPrinted = effekt.core.PrettyPrinter.format(obtained).layout + val expectedPrinted = effekt.core.PrettyPrinter.format(pExpected).layout + assertEquals(obtainedPrinted, expectedPrinted) shouldBeEqual(obtained, pExpected, clue) } test("No bound local variables"){ - val code = + val input = """module main | |def foo = { () => | return (bar: (Int) => Int @ {})(baz:Int) |} |""".stripMargin - assertRenamedTo(code, code) + val expected = + """module main + | + |def renamed0() = { + | return (bar: (Int) => Int @ {})(baz: Int) + |} + |""".stripMargin + + assertRenamedTo(input, expected) } test("val binding"){ @@ -40,9 +51,11 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |def foo = { () => - | val renamed1 = (foo:(Int)=>Int@{})(4); - | return renamed1:Int + |def renamed0() = { + | val renamed1: Int = { + | renamed0: (Int) => Int @ {}(4) + | }; + | return renamed1: Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -60,9 +73,9 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |def foo = { () => - | var renamed1 @ global = (foo:(Int)=>Int@{})(4); - | return renamed1:Int + |def renamed0() = { + | var renamed1 @ global = (renamed0: (Int) => Int @ {})(4); + | return renamed1: Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -72,15 +85,15 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo = { (x:Int) => - | return x:Int + |def renamed0(renamed1: Int) = { + | return renamed1: Int |} |""".stripMargin val expected = """module main | - |def foo = { (renamed1:Int) => - | return renamed1:Int + |def renamed0(renamed1: Int) = { + | return renamed1: Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -98,15 +111,20 @@ class TestRenamerTests 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 + """module main + | + |type renamed1 { + | renamed0(a: Int, b: Int) + |} + | + |def renamed2() = { + | 12 match { + | X : { (renamed3: Int, renamed4: Int) => + | return renamed3: Int + | } + | } + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -121,8 +139,8 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |def foo = { ['renamed1](renamed2: renamed1) => - | return renamed2:Identity[renamed1] + |def renamed0['renamed1](renamed2: renamed1) = { + | return renamed2: Identity[renamed1] |} |""".stripMargin assertRenamedTo(input, expected) @@ -141,14 +159,20 @@ class TestRenamerTests extends CoreTests { |""".stripMargin val expected = - """ module main + """module main | - | def bar = { () => return 1 } - | def main = { () => - | def renamed1 = { () => (bar : () => Unit @ {})() } - | def renamed2 = { () => return 2 } - | (renamed1 : () => Unit @ {})() - | } + |def renamed0() = { + | return 1 + |} + |def renamed1() = { + | def renamed2() = { + | renamed0: () => Unit @ {}() + | } + | def renamed3() = { + | return 2 + | } + | renamed2: () => Unit @ {}() + |} |""".stripMargin assertRenamedTo(input, expected) @@ -165,13 +189,13 @@ class TestRenamerTests extends CoreTests { |""".stripMargin val expected = - """ module main + """module main | - | def main = { () => - | let renamed1 = 1 - | let renamed2 = 2 - | return renamed2:Int - | } + |def renamed0() = { + | let renamed1 = 1 + | let renamed2 = 2 + | return renamed2: Int + |} |""".stripMargin assertRenamedTo(input, expected) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index ca376d51ce..cc0cec6915 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -1,17 +1,32 @@ package effekt package core +import effekt.core.Type.{PromptSymbol, ResumeSymbol} import effekt.source.{FeatureFlag, Span} +import effekt.symbols.builtins import effekt.util.messages.{ErrorReporter, ParseError} import kiama.parsing.{NoSuccess, ParseResult, Parsers, Success} import kiama.util.{Position, Range, Source, StringSource} -class Names(var knownNames: Map[String, Id]) { - def idFor(name: String): Id = knownNames.getOrElse(name, { - val id = Id(name) - knownNames = knownNames.updated(name, id) - id - }) +class Names(private var knownNames: Map[String, Id]) { + private val Suffix = """^(.*)\$(\d+)$""".r + private var renamed = knownNames + + def isKnown(id: Id): Boolean = + builtins.coreBuiltinSymbolFromString(id.name.name).isDefined || knownNames.contains(id.name.name) + + def getKnown(id: Id): Option[Id] = + builtins.coreBuiltinSymbolFromString(id.name.name).orElse(knownNames.get(id.name.name)) + + def idFor(name: String): Id = { + builtins.coreBuiltinSymbolFromString(name).getOrElse( + renamed.getOrElse(name, { + val id = Id(name) + renamed = renamed.updated(name, id) + id + }) + ) + } } @@ -41,6 +56,7 @@ class EffektLexers extends Parsers { lazy val `=` = literal("=") lazy val `:` = literal(":") lazy val `@` = literal("@") + lazy val `${` = literal("${") lazy val `{` = literal("{") lazy val `}` = literal("}") lazy val `(` = literal("(") @@ -57,7 +73,10 @@ class EffektLexers extends Parsers { lazy val `}>` = literal("}>") lazy val `!` = literal("!") lazy val `|` = literal("|") + lazy val `++` = literal("++") + lazy val `get` = keyword("get") + lazy val `put` = keyword("put") lazy val `let` = keyword("let") lazy val `true` = keyword("true") lazy val `false` = keyword("false") @@ -74,6 +93,8 @@ class EffektLexers extends Parsers { lazy val `case` = keyword("case") lazy val `do` = keyword("do") lazy val `resume` = keyword("resume") + lazy val `reset` = keyword("reset") + lazy val `shift` = keyword("shift") lazy val `match` = keyword("match") lazy val `def` = keyword("def") lazy val `module` = keyword("module") @@ -98,14 +119,15 @@ class EffektLexers extends Parsers { "def", "let", "val", "var", "true", "false", "else", "type", "effect", "interface", "try", "with", "case", "do", "if", "while", "match", "module", "import", "extern", "fun", - "at", "box", "unbox", "return", "region", "new", "resource", "and", "is", "namespace" + "at", "box", "unbox", "return", "region", "new", "resource", "and", "is", "namespace", + "reset", "shift", "make" ) def keyword(kw: String): Parser[String] = regex((s"$kw(?!$nameRest)").r, kw) lazy val anyKeyword = - keywords("[^a-zA-Z0-9]".r, keywordStrings) + keywords("[^a-zA-Z0-9_!?$]".r, keywordStrings) /** * Whitespace Handling @@ -121,13 +143,44 @@ class EffektLexers extends Parsers { * Literals */ lazy val integerLiteral = regex("([-+])?(0|[1-9][0-9]*)".r, s"Integer literal") - lazy val doubleLiteral = regex("([-+])?(0|[1-9][0-9]*)[.]([0-9]+)".r, "Double literal") - lazy val stringLiteral = regex("""\"(\\.|\\[\r?\n]|[^\r\n\"])*+\"""".r, "String literal") + lazy val doubleLiteral = + regex("([-+])?(0|[1-9][0-9]*)[.]([0-9]+)([eE][+-]?[0-9]+)?".r, "Double literal") + lazy val stringLiteral = + regex("""\"(\\.|\\[\r?\n]|[^\r\n\"])*+\"""".r, "String literal") ^^ { s => + val contents = s.substring(1, s.length - 1) + unescapeString(contents) + } lazy val charLiteral = regex("""'.'""".r, "Character literal") ^^ { s => s.codePointAt(1) } lazy val unicodeChar = regex("""\\u\{[0-9A-Fa-f]{1,6}\}""".r, "Unicode character literal") ^^ { case contents => Integer.parseInt(contents.stripPrefix("\\u{").stripSuffix("}"), 16) } + /** Inverse of PrettyPrinter.escapeString */ + private def unescapeString(s: String): String = { + val sb = new StringBuilder + var i = 0 + + while (i < s.length) { + val c = s.charAt(i) + if (c == '\\' && i + 1 < s.length) { + s.charAt(i + 1) match { + case '\\' => sb.append('\\'); i += 2 + case '"' => sb.append('"'); i += 2 + case 'r' => sb.append('\r'); i += 2 + case 't' => sb.append('\t'); i += 2 + case other => + sb.append(other) + i += 2 + } + } else { + sb.append(c) + i += 1 + } + } + + sb.toString + } + // Delimiter for multiline strings val multi = "\"\"\"" @@ -207,17 +260,16 @@ class CoreParsers(names: Names) extends EffektLexers { /** * Literals */ - lazy val int = integerLiteral ^^ { n => Literal(n.toInt, Type.TInt) } + lazy val int = integerLiteral ^^ { n => Literal(n.toLong, Type.TInt) } lazy val bool = `true` ^^^ Literal(true, Type.TBoolean) | `false` ^^^ Literal(false, Type.TBoolean) lazy val unit = literal("()") ^^^ Literal((), Type.TUnit) lazy val double = doubleLiteral ^^ { n => Literal(n.toDouble, Type.TDouble) } - lazy val string = stringLiteral ^^ { s => Literal(s.substring(1, s.size - 1), Type.TString) } + lazy val string: P[Expr] = (multilineString | stringLiteral) ^^ { s => Literal(s, Type.TString) } /** * Names */ lazy val id = ident ^^ { name => names.idFor(name) } - lazy val wildcard = success(names.idFor("_")) /** * Main Entry @@ -238,21 +290,37 @@ class CoreParsers(names: Names) extends EffektLexers { // ------- lazy val externDecl: P[Extern] = ( `extern` ~> featureFlag ~ externBody ^^ Extern.Include.apply - | `extern` ~> (captures <~ `def`) ~ signature ~ (`=` ~> (featureFlag ~ externBody)) ^^ { - case captures ~ (id, tparams, cparams, vparams, bparams, result) ~ body => - Extern.Def(id, tparams, cparams, vparams, bparams, result, captures, body match { - case ff ~ (body: String) => - ExternBody.StringExternBody(ff, Template(List(body), Nil)) - }) + | `extern` ~> (captures <~ `def`) ~ signature ~ (`=` ~> (featureFlag ~ externBodyTemplate)) ^^ { + case captures ~ (id, tparams, cparams, vparams, bparams, result) ~ (ff ~ templ) => + Extern.Def( + id, tparams, cparams, vparams, bparams, result, captures, + ExternBody.StringExternBody(ff, templ) + ) }) + lazy val externBodyTemplate: P[Template[Expr]] = + ( + (multilineString | stringLiteral) ~ + rep((`++` ~> expr) ~ (`++` ~> (multilineString | stringLiteral))) + ) ^^ { + case firstStr ~ pairs => + val strings = List.newBuilder[String] + val args = List.newBuilder[Expr] + strings += firstStr + pairs.foreach { case e ~ s => + args += e + strings += s + } + Template(strings.result(), args.result()) + } + lazy val featureFlag: P[FeatureFlag] = ("else" ^^ { _ => FeatureFlag.Default(Span.missing) } | ident ^^ (id => FeatureFlag.NamedFeatureFlag(id, Span.missing)) ) - lazy val externBody = stringLiteral | multilineString + lazy val externBody = multilineString | stringLiteral // Declarations @@ -291,12 +359,26 @@ class CoreParsers(names: Names) extends EffektLexers { lazy val stmt: P[Stmt] = ( `{` ~/> stmts <~ `}` | `return` ~> expr ^^ Stmt.Return.apply + | `reset` ~> blockLit ^^ Stmt.Reset.apply + | `shift` ~> maybeParens(blockVar) ~ blockLit ^^ Stmt.Shift.apply + | `resume` ~> maybeParens(blockVar) ~ stmt ^^ Stmt.Resume.apply | block ~ (`.` ~> id ~ (`:` ~> blockType)).? ~ maybeTypeArgs ~ valueArgs ~ blockArgs ^^ { - case (recv ~ Some(method ~ tpe) ~ targs ~ vargs ~ bargs) => Invoke(recv, method, tpe, targs, vargs, bargs) - case (recv ~ None ~ targs ~ vargs ~ bargs) => App(recv, targs, vargs, bargs) - } + case (recv ~ Some(method ~ tpe) ~ targs ~ vargs ~ bargs) => Invoke(recv, method, tpe, targs, vargs, bargs) + case (recv ~ None ~ targs ~ vargs ~ bargs) => App(recv, targs, vargs, bargs) + } | (`if` ~> `(` ~/> expr <~ `)`) ~ stmt ~ (`else` ~> stmt) ^^ Stmt.If.apply | `region` ~> blockLit ^^ Stmt.Region.apply + | `<>` ~> `@` ~> (stringLiteral <~ `:`) ~ (integerLiteral <~ `:`) ~ integerLiteral ^^ { + case (name ~ from ~ to) => + val source = if (name.startsWith("file://")) { + kiama.util.FileSource(name.stripPrefix("file://")) + } else if (name.startsWith("string://")) { + kiama.util.StringSource("", name.stripPrefix("string://")) + } else { + sys error s"Unsupported source scheme in hole source name: $name" + } + Hole(effekt.source.Span(source, from.toInt, to.toInt)) + } | `<>` ^^^ Hole(effekt.source.Span.missing) | (expr <~ `match`) ~/ (`{` ~> many(clause) <~ `}`) ~ (`else` ~> stmt).? ^^ Stmt.Match.apply ) @@ -307,21 +389,27 @@ class CoreParsers(names: Names) extends EffektLexers { ImpureApp(name, callee, targs, vargs, bargs, body) } | `let` ~/> id ~ maybeTypeAnnotation ~ (`=` ~/> expr) ~ stmts ^^ { - case (name ~ tpe ~ binding ~ body) => - Let(name, tpe.getOrElse(binding.tpe), binding, body) - } - | `def` ~> id ~ (`=` ~/> block) ~ stmts ^^ Stmt.Def.apply - | `def` ~> id ~ parameters ~ (`=` ~/> stmt) ~ stmts ^^ { - case name ~ (tparams, cparams, vparams, bparams) ~ body ~ rest => - Stmt.Def(name, BlockLit(tparams, cparams, vparams, bparams, body), rest) - } - | `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~> stmt) ~ (`;` ~> stmts) ^^ { - case id ~ tpe ~ binding ~ body => Val(id, tpe.getOrElse(binding.tpe), binding, body) - } - | `var` ~> id ~ (`in` ~> id) ~ (`=` ~> expr) ~ (`;` ~> stmts) ^^ { case id ~ region ~ init ~ body => Alloc(id, init, region, body) } - | `var` ~> id ~ (`@` ~> id) ~ (`=` ~> expr) ~ (`;` ~> stmts) ^^ { case id ~ cap ~ init ~ body => Var(id, init, cap, body) } - | stmt - ) + case (name ~ tpe ~ binding ~ body) => + Let(name, tpe.getOrElse(binding.tpe), binding, body) + } + | `get` ~> id ~ (`:` ~> valueType) ~ (`=` ~> `!` ~> id) ~ (`@` ~> id) ~ (`;` ~> stmts) ^^ { + case name ~ tpe ~ ref ~ cap ~ body => Get(name, tpe, ref, Set(cap), body) + } + | `put` ~> id ~ (`@` ~> id) ~ (`=` ~> expr) ~ (`;` ~> stmts) ^^ { + case ref ~ capt ~ value ~ body => Put(ref, Set(capt), value, body) + } + | `def` ~> id ~ (`=` ~/> block) ~ stmts ^^ Stmt.Def.apply + | `def` ~> id ~ parameters ~ (`=` ~/> stmt) ~ stmts ^^ { + case name ~ (tparams, cparams, vparams, bparams) ~ body ~ rest => + Stmt.Def(name, BlockLit(tparams, cparams, vparams, bparams, body), rest) + } + | `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~> stmt) ~ (`;` ~> stmts) ^^ { + case id ~ tpe ~ binding ~ body => Val(id, tpe.getOrElse(binding.tpe), binding, body) + } + | `var` ~> id ~ (`in` ~> id) ~ (`=` ~> expr) ~ (`;` ~> stmts) ^^ { case id ~ region ~ init ~ body => Alloc(id, init, region, body) } + | `var` ~> id ~ (`@` ~> id) ~ (`=` ~> expr) ~ (`;` ~> stmts) ^^ { case id ~ cap ~ init ~ body => Var(id, init, cap, body) } + | stmt + ) lazy val clause: P[(Id, BlockLit)] = (id <~ `:`) ~ blockLit ^^ { case id ~ cl => id -> cl } @@ -341,14 +429,14 @@ class CoreParsers(names: Names) extends EffektLexers { // ---------------- lazy val expr: P[Expr] = ( literal - | id ~ (`:` ~> valueType) ^^ Expr.ValueVar.apply | `box` ~> captures ~ block ^^ { case capt ~ block => Expr.Box(block, capt) } | `make` ~> dataType ~ id ~ maybeTypeArgs ~ valueArgs ^^ Expr.Make.apply + | id ~ (`:` ~> valueType) ^^ Expr.ValueVar.apply | maybeParens(blockVar) ~ maybeTypeArgs ~ valueArgs ^^ Expr.PureApp.apply | failure("Expected a pure expression.") ) - lazy val literal: P[Expr] = int | bool | string | unit | double + lazy val literal: P[Expr] = double | int | bool | string | unit // Calls @@ -469,12 +557,15 @@ class CoreParsers(names: Names) extends EffektLexers { // { f : S } // abbreviation { S } .= { _: S } lazy val blockTypeParam: P[(Id, BlockType)] = - `{` ~> (id <~ `:` | wildcard) ~ blockType <~ `}` ^^ { case id ~ tpe => id -> tpe } + `{` ~> (id <~ `:`) ~ blockType <~ `}` ^^ { case id ~ tpe => id -> tpe } lazy val interfaceType: P[BlockType.Interface] = - ( id ~ maybeTypeArgs ^^ { case id ~ tpe => BlockType.Interface(id, tpe) : BlockType.Interface } - | failure("Expected an interface") - ) + ( + id ~ maybeTypeArgs ^^ { + case id ~ targs => BlockType.Interface(id, targs): BlockType.Interface + } + | failure("Expected an interface") + ) lazy val typeArgs: P[List[ValueType]] = `[` ~/> manySep(valueType, `,`) <~ `]` diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 6fba2061b0..14f2e1e838 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -1,6 +1,7 @@ package effekt package core +import effekt.core.Type.{PromptSymbol, ResumeSymbol} import effekt.source.FeatureFlag import kiama.output.ParenPrettyPrinter @@ -48,24 +49,36 @@ object PrettyPrinter extends ParenPrettyPrinter { val emptyline: Doc = line <> line def toDoc(m: ModuleDecl): Doc = { - "module" <+> m.path <> emptyline <> vsep(m.includes.map { im => "import" <+> im }) <> emptyline <> - vsep(m.externs.map(toDoc)) <> + // The order of toplevel items must match the parser (where the order is currently fixed). + val includes = vsep(m.includes.map { im => "import" <+> im }) + val decls = vsep(m.declarations.map(toDoc)) + val externs = vsep(m.externs.map(toDoc)) + val defs = toDoc(m.definitions) + val exports = vsep(m.exports.map { id => "export" <+> toDoc(id) }) + + "module" <+> m.path <> emptyline <> - vsep(m.declarations.map(toDoc)) <> + includes <> emptyline <> - toDoc(m.definitions) + decls <> + emptyline <> + externs <> + emptyline <> + defs <> + (if m.exports.isEmpty then emptyDoc else emptyline <> exports) } def toDoc(definitions: List[Toplevel]): Doc = - vsep(definitions map toDoc, semi) + vsep(definitions map toDoc) def toDoc(e: Extern): Doc = e match { case Extern.Def(id, tps, cps, vps, bps, ret, capt, bodies) => - "extern" <+> toDoc(capt) <+> "def" <+> toDoc(id) <+> "=" <+> paramsToDoc(tps, vps, bps) <> ":" <+> toDoc(ret) <+> "=" <+> (bodies match { + "extern" <+> toDoc(capt) <+> "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <> ":" <+> toDoc(ret) <+> "=" <+> (bodies match { case ExternBody.StringExternBody(ff, body) => toDoc(ff) <+> toDoc(body) - case ExternBody.Unsupported(err) => s"unsupported(${err.toString})" + // The unsupported case is not currently supported by the core parser + case ExternBody.Unsupported(err) => ??? }) - case Extern.Include(ff, contents) => emptyDoc // right now, do not print includes. + case Extern.Include(ff, contents) => "extern" <+> toDoc(ff) <+> stringLiteral(contents) } def toDoc(ff: FeatureFlag): Doc = ff match { @@ -74,50 +87,63 @@ object PrettyPrinter extends ParenPrettyPrinter { } def toDoc(t: Template[Expr]): Doc = - /// TODO - hsep(t.args.map(toDoc), comma) + val Template(strings, args) = t + val head = stringLiteral(strings.headOption.get) + val rest: List[Doc] = + (args zip strings.drop(1)).map { case (e, s) => + space <> "++" <+> toDoc(e) <+> "++" <+> stringLiteral(s) + } + rest.foldLeft(head)(_ <> _) def toDoc(b: Block, preventBraces: Boolean = false): Doc = b match { - case BlockVar(id, _, _) => toDoc(id) + case BlockVar(id, tpe, capt) => + toDoc(id) <> ":" <+> toDoc(tpe) <+> "@" <+> toDoc(capt) case BlockLit(tps, cps, vps, bps, body) => - val doc = space <> paramsToDoc(tps, vps, bps) <+> "=>" <+> nest(line <> toDocStmts(body)) <> line + val doc = space <> paramsToDoc(tps, cps, vps, bps) <+> "=>" <+> nest(line <> toDocStmts(body)) <> line if preventBraces then doc else braces { doc } - case Unbox(e) => parens("unbox" <+> toDoc(e)) + case Unbox(e) => "unbox" <+> toDoc(e) case New(handler) => "new" <+> toDoc(handler) } def toDoc(p: ValueParam): Doc = toDoc(p.id) <> ":" <+> toDoc(p.tpe) - def toDoc(p: BlockParam): Doc = braces(toDoc(p.id)) + def toDoc(p: BlockParam): Doc = braces(toDoc(p.id) <> ":" <+> toDoc(p.tpe)) + def toDoc(cparam: Id, bparam: BlockParam): Doc = braces(toDoc(bparam.id) <+> "@" <+> toDoc(cparam) <> ":" <+> toDoc(bparam.tpe)) //def toDoc(n: Name): Doc = n.toString - def toDoc(s: symbols.Symbol): Doc = s.show + def toDoc(s: symbols.Symbol): Doc = { + builtins.coreBuiltinSymbolToString(s).getOrElse(s.name.name) + } def toDoc(e: Expr): Doc = e match { case Literal((), _) => "()" - case Literal(s: String, _) => "\"" + s + "\"" + case Literal(s: String, _) => stringLiteral(s) case Literal(value, _) => value.toString - case ValueVar(id, _) => toDoc(id) + case ValueVar(id, tpe) => toDoc(id) <> ":" <+> toDoc(tpe) - case PureApp(b, targs, vargs) => toDoc(b) <> argsToDoc(targs, vargs, Nil) + case PureApp(b, targs, vargs) => parens(toDoc(b)) <> argsToDoc(targs, vargs, Nil) case Make(data, tag, targs, vargs) => "make" <+> toDoc(data) <+> toDoc(tag) <> argsToDoc(targs, vargs, Nil) - case Box(b, capt) => parens("box" <+> toDoc(b)) + case Box(b, capt) => "box" <+> toDoc(capt) <+> toDoc(b) } def argsToDoc(targs: List[core.ValueType], vargs: List[core.Expr], bargs: List[core.Block]): Doc = val targsDoc = if targs.isEmpty then emptyDoc else brackets(targs.map(toDoc)) - //val cargsDoc = if cargs.isEmpty then emptyDoc else brackets(cargs.map(toDoc)) - val vargsDoc = if vargs.isEmpty && !bargs.isEmpty then emptyDoc else parens(vargs.map(toDoc)) + val vargsDoc = parens(vargs.map(toDoc)) // Wrap in braces individually, then concat with a space between. Force BlockLits to not add a layer of braces on top. - val bargsDoc = if bargs.isEmpty then emptyDoc else hcat { bargs.map { b => braces(toDoc(b, preventBraces = true)) } } + val bargsDoc = + if bargs.isEmpty then emptyDoc + else hsep { bargs.map { b => braces(toDoc(b, preventBraces = true)) } } targsDoc <> vargsDoc <> bargsDoc - def paramsToDoc(tps: List[symbols.Symbol], vps: List[ValueParam], bps: List[BlockParam]): Doc = { - val tpsDoc = if tps.isEmpty then emptyDoc else brackets(tps.map(toDoc)) - val vpsDoc = if vps.isEmpty && !bps.isEmpty then emptyDoc else parens(vps.map(toDoc)) - val bpsDoc = if bps.isEmpty then emptyDoc else hcat(bps.map(toDoc)) // already are in braces! + private def typeParamsDoc(tps: List[symbols.Symbol]): Doc = + if tps.isEmpty then emptyDoc else brackets(tps.map(tp => string("'") <> toDoc(tp))) + + def paramsToDoc(tps: List[symbols.Symbol], cps: List[Id], vps: List[ValueParam], bps: List[BlockParam]): Doc = { + val tpsDoc = typeParamsDoc(tps) + val vpsDoc = parens(vps.map(toDoc)) + val bpsDoc = if bps.isEmpty then emptyDoc else hsep(cps.zip(bps).map(toDoc(_, _))) tpsDoc <> vpsDoc <> bpsDoc } @@ -125,13 +151,13 @@ object PrettyPrinter extends ParenPrettyPrinter { val handlerName = toDoc(instance.interface) val clauses = instance.operations.map { case Operation(id, tps, cps, vps, bps, body) => - "def" <+> toDoc(id) <> paramsToDoc(tps, vps, bps) <+> "=" <+> nested(toDoc(body)) + "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <+> "=" <+> block(toDocStmts(body)) } handlerName <+> block(vsep(clauses)) } def typeTemplate(kind: Doc, id: symbols.Symbol, tparams: List[symbols.Symbol], decls: List[Doc]): Doc = - val tps = if tparams.isEmpty then emptyDoc else brackets(tparams.map(toDoc)) + val tps = typeParamsDoc(tparams) val body = if decls.isEmpty then string("{}") else block(vsep(decls)) kind <+> toDoc(id) <> tps <+> body @@ -144,7 +170,7 @@ object PrettyPrinter extends ParenPrettyPrinter { } def toDoc(c: Constructor): Doc = c match { - case Constructor(id, tparams, fields) => toDoc(id) <> brackets(tparams.map(toDoc)) <> parens(fields.map(toDoc)) + case Constructor(id, tparams, fields) => toDoc(id) <> typeParamsDoc(tparams) <> parens(fields.map(toDoc)) } def toDoc(f: Field): Doc = f match { case Field(name, tpe) => toDoc(name) <> ":" <+> toDoc(tpe) @@ -155,11 +181,11 @@ object PrettyPrinter extends ParenPrettyPrinter { def toDoc(d: Toplevel): Doc = d match { case Toplevel.Def(id, BlockLit(tps, cps, vps, bps, body)) => - "def" <+> toDoc(id) <> paramsToDoc(tps, vps, bps) <+> "=" <+> toDoc(body) - case Toplevel.Def(id, block) => - "def" <+> toDoc(id) <+> "=" <+> toDoc(block) - case Toplevel.Val(id, _, binding) => - "let" <+> toDoc(id) <+> "=" <+> toDoc(binding) + "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <+> "=" <+> block(toDocStmts(body)) + case Toplevel.Def(id, blockv) => + "def" <+> toDoc(id) <+> "=" <+> toDoc(blockv) + case Toplevel.Val(id, tpe, binding) => + "val" <+> toDoc(id) <> ":" <+> toDoc(tpe) <+> "=" <+> toDoc(binding) } def toDoc(s: Stmt): Doc = s match { @@ -168,9 +194,16 @@ object PrettyPrinter extends ParenPrettyPrinter { case other => toDocStmts(s) } + private def toDocSingleCapture(capt: core.Captures): Doc = + capt.toList match { + case x :: Nil => toDoc(x) + case _ => toDoc(capt) + } + def toDocStmts(s: Stmt): Doc = s match { case Def(id, BlockLit(tps, cps, vps, bps, body), rest) => - "def" <+> toDoc(id) <> paramsToDoc(tps, vps, bps) <+> "=" <+> toDoc(body) <> line <> + // RHS must be a single `stmt`, so we have to wrap it in a block. + "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <+> "=" <+> block(toDocStmts(body)) <> line <> toDocStmts(rest) case Def(id, block, rest) => @@ -188,19 +221,16 @@ object PrettyPrinter extends ParenPrettyPrinter { case Return(e) => "return" <+> toDoc(e) - case Val(Wildcard(), _, binding, body) => - toDoc(binding) <> ";" <> line <> - toDocStmts(body) - case Val(id, tpe, binding, body) => - "val" <+> toDoc(id) <> ":" <+> toDoc(tpe) <+> "=" <+> toDoc(binding) <> ";" <> line <> + // RHS must be a single `stmt`, so we have to wrap it in a block. + "val" <+> toDoc(id) <> ":" <+> toDoc(tpe) <+> "=" <+> block(toDocStmts(binding)) <> ";" <> line <> toDocStmts(body) case App(b, targs, vargs, bargs) => toDoc(b) <> argsToDoc(targs, vargs, bargs) case Invoke(b, method, methodTpe, targs, vargs, bargs) => - toDoc(b) <> "." <> method.name.toString <> argsToDoc(targs, vargs, bargs) + toDoc(b) <> "." <> toDoc(method) <> ":" <+> toDoc(methodTpe) <> argsToDoc(targs, vargs, bargs) case If(cond, thn, els) => "if" <+> parens(toDoc(cond)) <+> block(toDocStmts(thn)) <+> "else" <+> block(toDocStmts(els)) @@ -219,42 +249,56 @@ object PrettyPrinter extends ParenPrettyPrinter { toDocStmts(body) case Var(ref, init, cap, body) => - "var" <+> toDoc(ref) <+> "=" <+> toDoc(init) <> ";" <> line <> + "var" <+> toDoc(ref) <+> "@" <+> toDoc(cap) <+> "=" <+> toDoc(init) <> ";" <> line <> toDocStmts(body) case Get(id, tpe, ref, capt, body) => - "let" <+> toDoc(id) <+> "=" <+> "!" <> toDoc(ref) <> ";" <> line <> + "get" <+> toDoc(id) <+> ":" <+> toDoc(tpe) <+> "=" <+> "!" <+> toDoc(ref) <+> "@" <+> toDocSingleCapture(capt) <> ";" <> line <> toDocStmts(body) case Put(ref, capt, value, body) => - toDoc(ref) <+> ":=" <+> toDoc(value) <> ";" <> line <> + "put" <+> toDoc(ref) <+> "@" <+> toDocSingleCapture(capt) <+> "=" <+> toDoc(value) <> ";" <> line <> toDocStmts(body) case Region(body) => "region" <+> toDoc(body) case Match(sc, clauses, default) => - val cs = braces(nest(line <> vsep(clauses map { case (p, b) => "case" <+> toDoc(p) <+> toDoc(b) })) <> line) + val cs = braces(nest(line <> vsep(clauses map { case (p, b) => toDoc(p) <+> ":" <+> toDoc(b) })) <> line) val d = default.map { body => space <> "else" <+> braces(nest(line <> toDocStmts(body))) }.getOrElse { emptyDoc } toDoc(sc) <+> "match" <+> cs <> d case Hole(span) => - "<>" <+> s"// @ ${span.range.from.format}" + val from = span.from + val to = span.to + val name = span.source.name + val scheme = span.source match { + case _: kiama.util.FileSource => "file" + case _: kiama.util.StringSource => "string" + case _ => "source" + } + "<>" <+> s"@ \"$scheme://$name\":$from:$to" } def toDoc(tpe: core.BlockType): Doc = tpe match { case core.BlockType.Function(tparams, cparams, vparams, bparams, result) => - val tps = if tparams.isEmpty then emptyDoc else brackets(tparams.map(toDoc)) + val tps = typeParamsDoc(tparams) val vps = parens(vparams.map(toDoc)) - val bps = hcat((cparams zip bparams).map { case (id, tpe) => braces(toDoc(id) <> ":" <+> toDoc(tpe)) }) - val res = toDoc(result) + val bps = hsep((cparams zip bparams).map { case (id, tpe) => braces(toDoc(id) <> ":" <+> toDoc(tpe)) }) + // After `=>` the grammar expects a primValueType. If the result is a boxed value type + // (i.e., contains `at { ... }`), we must parenthesize it so it parses via `( valueType )`. + val res = + result match { + case core.ValueType.Boxed(_, _) => parens(toDoc(result)) + case _ => toDoc(result) + } tps <> vps <> bps <+> "=>" <+> res case core.BlockType.Interface(symbol, Nil) => toDoc(symbol) case core.BlockType.Interface(symbol, targs) => toDoc(symbol) <> brackets(targs.map(toDoc)) } def toDoc(tpe: core.ValueType): Doc = tpe match { - case ValueType.Var(name) => toDoc(name) + case ValueType.Var(name) => "'" <> toDoc(name) case ValueType.Data(symbol, targs) => toDoc(symbol, targs) case ValueType.Boxed(tpe, capt) => toDoc(tpe) <+> "at" <+> toDoc(capt) } @@ -263,7 +307,7 @@ object PrettyPrinter extends ParenPrettyPrinter { if (targs.isEmpty) then toDoc(tpeConstructor) else toDoc(tpeConstructor) <> brackets(targs.map(toDoc)) - def toDoc(capt: core.Captures): Doc = braces(hsep(capt.toList.map(toDoc), comma)) + def toDoc(capt: core.Captures): Doc = braces(hsep(capt.toList.sortBy(_.name.name).map(toDoc), comma)) def nested(content: Doc): Doc = group(nest(line <> content)) @@ -276,4 +320,24 @@ object PrettyPrinter extends ParenPrettyPrinter { def block(content: Doc): Doc = braces(nest(line <> content) <> line) def block(docs: List[Doc]): Doc = block(vsep(docs, line)) + + private def escapeString(s: String): String = { + // TODO: Unicode escapes? + s.flatMap { + case '\\' => "\\\\" + case '"' => "\\\"" + case '\r' => "\\r" + case '\t' => "\\t" + case c => c.toString + } + } + + def stringLiteral(s: String): Doc = + if s.contains("\n") then multilineStringLiteral(s) + else "\"" <> escapeString(s) <> "\"" + + def multilineStringLiteral(s: String): Doc = { + val multi = "\"\"\"" + multi <> s <> multi + } } diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 414eda99f2..b3419f3cb7 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -453,7 +453,11 @@ object Tree { def rewrite(o: Operation): Operation = rewriteStructurally(o) def rewrite(p: ValueParam): ValueParam = rewriteStructurally(p) def rewrite(p: BlockParam): BlockParam = rewriteStructurally(p) - def rewrite(b: ExternBody): ExternBody= rewrite(b) + def rewrite(b: ExternBody): ExternBody= rewriteStructurally(b) + def rewrite(e: Extern): Extern= rewriteStructurally(e) + def rewrite(d: Declaration): Declaration = rewriteStructurally(d) + def rewrite(c: Constructor): Constructor = rewriteStructurally(c) + def rewrite(f: Field): Field = rewriteStructurally(f) def rewrite(b: BlockLit): BlockLit = if block.isDefinedAt(b) then block(b).asInstanceOf else b match { case BlockLit(tparams, cparams, vparams, bparams, body) => @@ -469,6 +473,7 @@ object Tree { def rewrite(t: BlockType): BlockType = rewriteStructurally(t) def rewrite(t: BlockType.Interface): BlockType.Interface = rewriteStructurally(t) def rewrite(capt: Captures): Captures = capt.map(rewrite) + def rewrite(p: Property): Property = rewriteStructurally(p) def rewrite(m: ModuleDecl): ModuleDecl = m match { diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Deadcode.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Deadcode.scala index 776c2c0116..d12104018a 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Deadcode.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Deadcode.scala @@ -47,7 +47,7 @@ class Deadcode(reachable: Map[Id, Usage]) extends core.Tree.Rewrite { exports) } - def rewrite(d: Declaration): Declaration = d match { + override def rewrite(d: Declaration): Declaration = d match { case Declaration.Data(id, tparams, constructors) => Declaration.Data(id, tparams, constructors.collect { case c if used(c.id) => c diff --git a/effekt/shared/src/main/scala/effekt/symbols/Name.scala b/effekt/shared/src/main/scala/effekt/symbols/Name.scala index 5f0b0de8ce..f5100d6f61 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Name.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Name.scala @@ -18,7 +18,7 @@ sealed trait Name { } case object NoName extends Name { - def name = "" + def name = "__anon" def rename(f: String => String): NoName.type = this } diff --git a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala index 98537b5139..0d740375de 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala @@ -3,6 +3,7 @@ package symbols import effekt.source.{Many, ModuleDecl, NoSource, Span} import effekt.context.Context +import effekt.core.Type.{PromptSymbol, ResumeSymbol} import effekt.symbols.ErrorMessageInterpolator import effekt.util.messages.ErrorMessageReifier import kiama.util.StringSource @@ -101,4 +102,20 @@ object builtins { lazy val rootBindings: Bindings = Bindings(Map.empty, rootTypes, rootCaptures, Map("effekt" -> Bindings(Map.empty, rootTypes, rootCaptures, Map.empty))) + // All built-in symbols that can occur in core programs + val coreBuiltins: Map[String, symbols.Symbol] = { + symbols.builtins.rootTypes + ++ symbols.builtins.rootCaptures + + ("Resume" -> ResumeSymbol) + + ("Prompt" -> PromptSymbol) + + ("Ref" -> effekt.symbols.builtins.TState.interface) + } + + def isCoreBuiltin(s: symbols.Symbol): Boolean = + coreBuiltins.contains(s.name.name) && coreBuiltins(s.name.name) == s + + def coreBuiltinSymbolToString(s: symbols.Symbol): Option[String] = + if isCoreBuiltin(s) then Some(s.name.name) else None + + def coreBuiltinSymbolFromString(s: String): Option[symbols.Symbol] = coreBuiltins.get(s) }