From 21fbb0de2797e428f8bebd427e0c2a36b30fe359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 5 Nov 2025 09:47:30 +0100 Subject: [PATCH 01/26] Add initial ReparseTests --- .../test/scala/effekt/core/ReparseTests.scala | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala 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..907e4a8312 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -0,0 +1,97 @@ +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") + + def positives: Set[File] = Set( + examplesDir / "pos", + examplesDir / "casestudies", + examplesDir / "benchmarks", + ) + + def runTests() = positives.foreach(runPositiveTestsIn) + + def runPositiveTestsIn(dir: File): Unit = + foreachFileIn(dir) { + f => 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 printed = core.PrettyPrinter.format(coreMod).toString + println(printed) + val reparsed: ModuleDecl = parse(printed)(using Location.empty) + assertAlphaEquivalent(reparsed, coreMod, s"Reparsing the pretty-printed core module of ${input.getPath} did not yield an alpha-equivalent module.") + } + + 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" + + override def supportedFeatureFlags: List[String] = List("vm") + + override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = None + + override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = None + + override def compile(source: Source)(using C: Context): Option[(Map[String, String], (Id, symbols.Module, ModuleDecl))] = + Optimized.run(source).map { res => (Map.empty, res) } + + 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) + } +} From 168642d6bf7452654cc160c31a21569f5badb450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 5 Nov 2025 17:06:47 +0100 Subject: [PATCH 02/26] Close to reaching core printer-parser parity --- .../test/scala/effekt/core/ReparseTests.scala | 13 +- .../src/main/scala/effekt/core/Parser.scala | 169 ++++++++++++------ .../scala/effekt/core/PrettyPrinter.scala | 133 +++++++++----- .../src/main/scala/effekt/symbols/Name.scala | 2 +- 4 files changed, 213 insertions(+), 104 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 907e4a8312..34627738df 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -52,10 +52,14 @@ class ReparseTests extends CoreTests { val errors = plainMessaging.formatMessages(context.messaging.buffer) sys error errors } - val printed = core.PrettyPrinter.format(coreMod).toString - println(printed) + val printed = core.PrettyPrinter.format(coreMod).layout val reparsed: ModuleDecl = parse(printed)(using Location.empty) - assertAlphaEquivalent(reparsed, coreMod, s"Reparsing the pretty-printed core module of ${input.getPath} did not yield an alpha-equivalent module.") + val renamer = TestRenamer(Names(Map.empty), "$") + val lhs = renamer(reparsed) + val rhs = renamer(coreMod) + val lhsPrinted = core.PrettyPrinter.format(lhs).layout + val rhsPrinted = core.PrettyPrinter.format(rhs).layout + assertEquals(lhsPrinted, rhsPrinted) } def foreachFileIn(file: File)(test: File => Unit): Unit = @@ -76,7 +80,8 @@ class ReparseTests extends CoreTests { class CompileToCore extends Compiler[(Id, symbols.Module, ModuleDecl)] { def extension = ".effekt-core.ir" - override def supportedFeatureFlags: List[String] = List("vm") + // 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 diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index ca376d51ce..9177eb8bac 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -1,17 +1,29 @@ package effekt package core +import effekt.core.Type.{PromptSymbol, ResumeSymbol} import effekt.source.{FeatureFlag, Span} 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 - }) + def idFor(name: String): Id = { + // When the name ends with a `$` symbol followed by an integer, + // we assume that the latter part is the Barendregt id of this name. + knownNames.getOrElse(name, { + val i = name.lastIndexOf('$') + val prefix = + if (i >= 0 && i < name.length - 1) { + val suf = name.substring(i + 1) + if (suf.toIntOption.isDefined) name.substring(0, i) else name + } else name + + val id = Id(prefix) + knownNames = knownNames.updated(name, id) + id + }) + } } @@ -30,17 +42,18 @@ class EffektLexers extends Parsers { lazy val ident = (not(anyKeyword) ~> name - | failure("Expected an identifier") - ) + | failure("Expected an identifier") + ) lazy val identRef = (not(anyKeyword) ~> qualifiedName - | failure("Expected an identifier") - ) + | failure("Expected an identifier") + ) lazy val `=` = literal("=") lazy val `:` = literal(":") lazy val `@` = literal("@") + lazy val `${` = literal("${") lazy val `{` = literal("{") lazy val `}` = literal("}") lazy val `(` = literal("(") @@ -57,7 +70,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 +90,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,7 +116,8 @@ 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" ) def keyword(kw: String): Parser[String] = @@ -123,6 +142,7 @@ class EffektLexers extends Parsers { 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") + ^^ { s => s.substring(1, s.size - 1)} 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) @@ -211,7 +231,7 @@ class CoreParsers(names: Names) extends EffektLexers { 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 = stringLiteral ^^ { s => Literal(s, Type.TString) } /** * Names @@ -238,21 +258,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)) - ) + | ident ^^ (id => FeatureFlag.NamedFeatureFlag(id, Span.missing)) + ) - lazy val externBody = stringLiteral | multilineString + lazy val externBody = multilineString | stringLiteral // Declarations @@ -274,32 +310,35 @@ class CoreParsers(names: Names) extends EffektLexers { // Definitions // ----------- lazy val toplevel: P[Toplevel] = - ( `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~/> stmt) ^^ { - case (name ~ tpe ~ binding) => Toplevel.Val(name, tpe.getOrElse(binding.tpe), binding) - } - | `def` ~> id ~ (`=` ~/> block) ^^ Toplevel.Def.apply - | `def` ~> id ~ parameters ~ (`=` ~> stmt) ^^ { + ( `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~/> stmt) ^^ { + case (name ~ tpe ~ binding) => Toplevel.Val(name, tpe.getOrElse(binding.tpe), binding) + } + | `def` ~> id ~ (`=` ~/> block) ^^ Toplevel.Def.apply + | `def` ~> id ~ parameters ~ (`=` ~> stmt) ^^ { case name ~ (tparams, cparams, vparams, bparams) ~ body => Toplevel.Def(name, BlockLit(tparams, cparams, vparams, bparams, body)) } - | failure("Expected a definition.") - ) + | failure("Expected a definition.") + ) // Statements // ---------- lazy val stmt: P[Stmt] = ( `{` ~/> stmts <~ `}` - | `return` ~> expr ^^ Stmt.Return.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) - } - | (`if` ~> `(` ~/> expr <~ `)`) ~ stmt ~ (`else` ~> stmt) ^^ Stmt.If.apply - | `region` ~> blockLit ^^ Stmt.Region.apply - | `<>` ^^^ Hole(effekt.source.Span.missing) - | (expr <~ `match`) ~/ (`{` ~> many(clause) <~ `}`) ~ (`else` ~> stmt).? ^^ Stmt.Match.apply - ) + | `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) + } + | (`if` ~> `(` ~/> expr <~ `)`) ~ stmt ~ (`else` ~> stmt) ^^ Stmt.If.apply + | `region` ~> blockLit ^^ Stmt.Region.apply + | `<>` ^^^ Hole(effekt.source.Span.missing) + | (expr <~ `match`) ~/ (`{` ~> many(clause) <~ `}`) ~ (`else` ~> stmt).? ^^ Stmt.Match.apply + ) lazy val stmts: P[Stmt] = ( (`let` ~ `!` ~/> id) ~ (`=` ~/> maybeParens(blockVar)) ~ maybeTypeArgs ~ valueArgs ~ blockArgs ~ stmts ^^ { @@ -307,21 +346,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 } @@ -348,7 +393,7 @@ class CoreParsers(names: Names) extends EffektLexers { | 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 @@ -472,9 +517,21 @@ class CoreParsers(names: Names) extends EffektLexers { `{` ~> (id <~ `:` | wildcard) ~ 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 => + // Special-case Resume[result, answer] to use the canonical ResumeSymbol + if (id.name.toString.startsWith("Resume")) + BlockType.Interface(ResumeSymbol, targs): BlockType.Interface + else if (id.name.toString.startsWith("Prompt")) + BlockType.Interface(PromptSymbol, targs): BlockType.Interface + else if (id.name.toString.startsWith("Ref")) + BlockType.Interface(effekt.symbols.builtins.TState.interface, targs): BlockType.Interface + else + 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..918ef59adc 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -48,24 +48,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, 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 +86,60 @@ 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 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(n: Name): Doc = n.toString - def toDoc(s: symbols.Symbol): Doc = s.show + def toDoc(s: symbols.Symbol): Doc = s.name.name ++ "$" ++ s.id.toString def toDoc(e: Expr): Doc = e match { case Literal((), _) => "()" case Literal(s: String, _) => "\"" + 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 + 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], 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! + val tpsDoc = typeParamsDoc(tps) + val vpsDoc = parens(vps.map(toDoc)) + val bpsDoc = if bps.isEmpty then emptyDoc else hsep(bps.map(toDoc)) tpsDoc <> vpsDoc <> bpsDoc } @@ -131,7 +153,7 @@ object PrettyPrinter extends ParenPrettyPrinter { } 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 +166,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 +177,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, 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 +190,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, vps, bps) <+> "=" <+> block(toDocStmts(body)) <> line <> toDocStmts(rest) case Def(id, block, rest) => @@ -188,19 +217,21 @@ object PrettyPrinter extends ParenPrettyPrinter { case Return(e) => "return" <+> toDoc(e) - case Val(Wildcard(), _, binding, body) => - toDoc(binding) <> ";" <> line <> + case Val(Wildcard(), tpe, binding, body) => + // RHS must be a single `stmt`, so we have to wrap it in a block. + "val" <+> "_" <> ":" <+> toDoc(tpe) <+> "=" <+> block(toDocStmts(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,22 +250,22 @@ 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 @@ -244,17 +275,23 @@ object PrettyPrinter extends ParenPrettyPrinter { 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) } @@ -276,4 +313,14 @@ object PrettyPrinter extends ParenPrettyPrinter { def block(content: Doc): Doc = braces(nest(line <> content) <> line) def block(docs: List[Doc]): Doc = block(vsep(docs, line)) + + // TODO: Escaping? + def stringLiteral(s: String): Doc = { + if s.contains("\n") then multilineStringLiteral(s) + else "\"" <> string(s) <> "\"" } + + def multilineStringLiteral(s: String): Doc = { + val multi = "\"\"\"" + multi <> string(s) <> multi + } } 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 } From 6c12f7d6b851e35bae8671b6227d53e4fb45d727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Tue, 11 Nov 2025 14:43:42 +0100 Subject: [PATCH 03/26] Towards harmonizing id handling between parser and pretty-printer --- .../test/scala/effekt/core/CoreTests.scala | 4 +- .../test/scala/effekt/core/ReparseTests.scala | 2 +- .../test/scala/effekt/core/TestRenamer.scala | 153 ++++++++++++++++-- .../scala/effekt/core/TestRenamerTests.scala | 32 ++-- .../src/main/scala/effekt/core/Parser.scala | 19 ++- .../scala/effekt/core/PrettyPrinter.scala | 32 +++- .../src/main/scala/effekt/core/Tree.scala | 11 +- .../effekt/core/optimizer/Deadcode.scala | 2 +- 8 files changed, 210 insertions(+), 45 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index 95e4bba117..e7d2460df4 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -47,14 +47,14 @@ 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, "$") + 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 = 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/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 34627738df..526dfba313 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -54,7 +54,7 @@ class ReparseTests extends CoreTests { } val printed = core.PrettyPrinter.format(coreMod).layout val reparsed: ModuleDecl = parse(printed)(using Location.empty) - val renamer = TestRenamer(Names(Map.empty), "$") + val renamer = TestRenamer(Names(defaultNames)) val lhs = renamer(reparsed) val rhs = renamer(coreMod) val lhsPrinted = core.PrettyPrinter.format(lhs).layout diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index fdcb58b351..7cfd29ecae 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -1,7 +1,8 @@ package effekt.core -import effekt.{ core, symbols } +import effekt.{Template, core, symbols} import effekt.context.Context +import effekt.core.Type.{PromptSymbol, ResumeSymbol} /** * Freshens bound names in a given term for tests. @@ -14,7 +15,7 @@ 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)) extends core.Tree.Rewrite { // list of scopes that map bound symbols to their renamed variants. private var scopes: List[Map[Id, Id]] = List.empty @@ -26,8 +27,7 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends 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) + Id(id.name.name, suffix) def withBindings[R](ids: List[Id])(f: => R): R = val before = scopes @@ -41,11 +41,28 @@ 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 + // free variables cannot be left untouched, because top-level items may be mutually recursive. + // This means that a bound occurrence may precede its binding. 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 (isBuiltin(id)) { + id + } else { + scopes.collectFirst { + case bnds if bnds.contains(id) => bnds(id) + }.getOrElse { + scopes match { + case Nil => + id + case head :: tail => { + val freshId = freshIdFor(id) + scopes = (head + (id -> freshId)) :: tail + freshId + } + } + } + } + } } override def stmt: PartialFunction[Stmt, Stmt] = { @@ -105,11 +122,129 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends } } + override def rewrite(toplevel: Toplevel): Toplevel = toplevel match { + case Toplevel.Def(id, block) => + withBinding(id) { + Toplevel.Def(rewrite(id), rewrite(block)) + } + case Toplevel.Val(id, tpe, binding) => + val resolvedBinding = rewrite(binding) + withBinding(id) { + 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]) => + withBinding(id) { + withBindings(tparams) { + Declaration.Data(rewrite(id), tparams map rewrite, constructors map rewrite) + }} + case Declaration.Interface(id: Id, tparams: List[Id], properties: List[Property]) => + withBinding(id) { + 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) => { + withBinding(id) { + 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) => + withBinding(id) { + withBindings(tparams) { + Constructor(rewrite(id), tparams map rewrite, fields map rewrite) + }} + } + + override def rewrite(p: Property) = p match { + case Property(id: Id, tpe: BlockType) => + withBinding(id) { + Property(rewrite(id), rewrite(tpe)) + } + } + + override def rewrite(f: Field) = f match { + case Field(id, tpe) => + withBinding(id) { + 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) => withBinding(id) { + BlockParam(rewrite(id), rewrite(tpe), rewrite(capt)) + } + } + + override def rewrite(v: ValueParam): ValueParam = v match { + case ValueParam(id, tpe) => withBinding(id) { + ValueParam(rewrite(id), rewrite(tpe)) + } + } + 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) + core.ModuleDecl(path, includes, declarations map rewrite, externs map rewrite, definitions map rewrite, exports map rewrite) } def apply(s: Stmt): Stmt = { diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala index 4f1a95113d..bc67f580b5 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -12,7 +12,7 @@ class TestRenamerTests extends CoreTests { 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 renamer = new TestRenamer(names) val obtained = renamer(pInput) shouldBeEqual(obtained, pExpected, clue) } @@ -41,8 +41,8 @@ class TestRenamerTests extends CoreTests { """module main | |def foo = { () => - | val renamed1 = (foo:(Int)=>Int@{})(4); - | return renamed1:Int + | val $1 = (foo:(Int)=>Int@{})(4); + | return $1:Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -61,8 +61,8 @@ class TestRenamerTests extends CoreTests { """module main | |def foo = { () => - | var renamed1 @ global = (foo:(Int)=>Int@{})(4); - | return renamed1:Int + | var $1 @ global = (foo:(Int)=>Int@{})(4); + | return $1:Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -79,8 +79,8 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |def foo = { (renamed1:Int) => - | return renamed1:Int + |def foo = { ($1:Int) => + | return $1:Int |} |""".stripMargin assertRenamedTo(input, expected) @@ -103,7 +103,7 @@ class TestRenamerTests extends CoreTests { |type Data { X(a:Int, b:Int) } |def foo = { () => | 12 match { - | X : {(renamed1:Int, renamed2:Int) => return renamed1:Int } + | X : {($1:Int, $2:Int) => return $1:Int } | } |} |""".stripMargin @@ -121,8 +121,8 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |def foo = { ['renamed1](renamed2: renamed1) => - | return renamed2:Identity[renamed1] + |def foo = { ['$1]($2: $1) => + | return $2:Identity[$1] |} |""".stripMargin assertRenamedTo(input, expected) @@ -145,9 +145,9 @@ class TestRenamerTests extends CoreTests { | | def bar = { () => return 1 } | def main = { () => - | def renamed1 = { () => (bar : () => Unit @ {})() } - | def renamed2 = { () => return 2 } - | (renamed1 : () => Unit @ {})() + | def $1 = { () => (bar : () => Unit @ {})() } + | def $2 = { () => return 2 } + | ($1 : () => Unit @ {})() | } |""".stripMargin @@ -168,9 +168,9 @@ class TestRenamerTests extends CoreTests { """ module main | | def main = { () => - | let renamed1 = 1 - | let renamed2 = 2 - | return renamed2:Int + | let $1 = 1 + | let $2 = 2 + | return $2:Int | } |""".stripMargin diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index 9177eb8bac..b5a9de14f8 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -11,15 +11,20 @@ class Names(var knownNames: Map[String, Id]) { def idFor(name: String): Id = { // When the name ends with a `$` symbol followed by an integer, // we assume that the latter part is the Barendregt id of this name. - knownNames.getOrElse(name, { + val (strippedName, suffix) = { val i = name.lastIndexOf('$') - val prefix = if (i >= 0 && i < name.length - 1) { val suf = name.substring(i + 1) - if (suf.toIntOption.isDefined) name.substring(0, i) else name - } else name - - val id = Id(prefix) + if (suf.toIntOption.isDefined) + (name.substring(0, i), Some(suf.toInt)) + else (name, None) + } else (name, None) + } + knownNames.getOrElse(name, { + val id = suffix match { + case Some(i) => Id(strippedName, i) + case None => Id(strippedName) + } knownNames = knownNames.updated(name, id) id }) @@ -514,7 +519,7 @@ 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 | wildcard) <~ `:`) ~ blockType <~ `}` ^^ { case id ~ tpe => id -> tpe } lazy val interfaceType: P[BlockType.Interface] = ( diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 918ef59adc..a2209f0651 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 @@ -72,7 +73,7 @@ object PrettyPrinter extends ParenPrettyPrinter { 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) // The unsupported case is not currently supported by the core parser case ExternBody.Unsupported(err) => ??? @@ -98,7 +99,7 @@ object PrettyPrinter extends ParenPrettyPrinter { 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) => "unbox" <+> toDoc(e) case New(handler) => "new" <+> toDoc(handler) @@ -106,10 +107,15 @@ object PrettyPrinter extends ParenPrettyPrinter { def toDoc(p: ValueParam): Doc = toDoc(p.id) <> ":" <+> toDoc(p.tpe) 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.name.name ++ "$" ++ s.id.toString + def toDoc(s: symbols.Symbol): Doc = { + if isBuiltin(s) + then s.name.name + else s.name.name ++ "$" ++ s.id.toString + } def toDoc(e: Expr): Doc = e match { case Literal((), _) => "()" @@ -136,10 +142,10 @@ object PrettyPrinter extends ParenPrettyPrinter { 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], vps: List[ValueParam], bps: List[BlockParam]): Doc = { + 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(bps.map(toDoc)) + val bpsDoc = if bps.isEmpty then emptyDoc else hsep(cps.zip(bps).map(toDoc(_, _))) tpsDoc <> vpsDoc <> bpsDoc } @@ -147,7 +153,7 @@ 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) <+> "=" <+> nested(toDoc(body)) } handlerName <+> block(vsep(clauses)) } @@ -177,7 +183,7 @@ 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) <+> "=" <+> block(toDocStmts(body)) + "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) => @@ -199,7 +205,7 @@ object PrettyPrinter extends ParenPrettyPrinter { def toDocStmts(s: Stmt): Doc = s match { case Def(id, BlockLit(tps, cps, vps, bps, body), rest) => // RHS must be a single `stmt`, so we have to wrap it in a block. - "def" <+> toDoc(id) <> paramsToDoc(tps, vps, bps) <+> "=" <+> block(toDocStmts(body)) <> line <> + "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <+> "=" <+> block(toDocStmts(body)) <> line <> toDocStmts(rest) case Def(id, block, rest) => @@ -324,3 +330,13 @@ object PrettyPrinter extends ParenPrettyPrinter { multi <> string(s) <> multi } } + +def isBuiltin(s: symbols.Symbol): Boolean = + val builtins = { + symbols.builtins.rootTypes + ++ symbols.builtins.rootCaptures + + ("Resume" -> ResumeSymbol) + + ("Prompt" -> PromptSymbol) + + ("Ref" -> effekt.symbols.builtins.TState.interface.name) + } + builtins.contains(s.name.name) && builtins(s.name.name) == s diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 414eda99f2..f416af24ac 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -91,6 +91,10 @@ object Id { val name = n } def apply(n: String): Id = apply(symbols.Name.local(n)) + def apply(n: String, theId: Int): Id = new symbols.Symbol { + val name = symbols.Name.local(n) + override lazy val id = theId + } def apply(n: Id): Id = apply(n.name) } @@ -453,7 +457,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 +477,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 From 8654c3e65e6e0d14a273163fef022d5513cb8f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 12 Nov 2025 10:22:55 +0100 Subject: [PATCH 04/26] Fix wrong scope handling --- .../test/scala/effekt/core/TestRenamer.scala | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index 7cfd29ecae..3b26f283be 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -1,8 +1,6 @@ package effekt.core import effekt.{Template, core, symbols} -import effekt.context.Context -import effekt.core.Type.{PromptSymbol, ResumeSymbol} /** * Freshens bound names in a given term for tests. @@ -52,13 +50,12 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { case bnds if bnds.contains(id) => bnds(id) }.getOrElse { scopes match { - case Nil => - id - case head :: tail => { + case Nil => id + case _ => val freshId = freshIdFor(id) - scopes = (head + (id -> freshId)) :: tail + val updatedLast = scopes.last + (id -> freshId) + scopes = scopes.init :+ updatedLast freshId - } } } } @@ -188,17 +185,11 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { } override def rewrite(p: Property) = p match { - case Property(id: Id, tpe: BlockType) => - withBinding(id) { - Property(rewrite(id), rewrite(tpe)) - } + case Property(id: Id, tpe: BlockType) => Property(rewrite(id), rewrite(tpe)) } override def rewrite(f: Field) = f match { - case Field(id, tpe) => - withBinding(id) { - Field(rewrite(id), rewrite(tpe)) - } + case Field(id, tpe) => Field(rewrite(id), rewrite(tpe)) } override def rewrite(b: BlockType): BlockType = b match { @@ -229,15 +220,11 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { } override def rewrite(b: BlockParam): BlockParam = b match { - case BlockParam(id, tpe, capt) => withBinding(id) { - BlockParam(rewrite(id), rewrite(tpe), rewrite(capt)) - } + case BlockParam(id, tpe, capt) => BlockParam(rewrite(id), rewrite(tpe), rewrite(capt)) } override def rewrite(v: ValueParam): ValueParam = v match { - case ValueParam(id, tpe) => withBinding(id) { - ValueParam(rewrite(id), rewrite(tpe)) - } + case ValueParam(id, tpe) => ValueParam(rewrite(id), rewrite(tpe)) } def apply(m: core.ModuleDecl): core.ModuleDecl = From 39ba213e6f7565e4855e4f9a3d9f34499edb3e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 12 Nov 2025 16:30:42 +0100 Subject: [PATCH 05/26] Fix handling of top-level and builtin names --- .../test/scala/effekt/core/TestRenamer.scala | 42 ++-- .../scala/effekt/core/TestRenamerTests.scala | 197 ++++++++++++------ .../src/main/scala/effekt/core/Parser.scala | 52 ++--- .../scala/effekt/core/PrettyPrinter.scala | 29 ++- 4 files changed, 192 insertions(+), 128 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index 3b26f283be..55809c162b 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -17,6 +17,8 @@ class TestRenamer(names: Names = Names(Map.empty)) 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 @@ -39,27 +41,25 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { /** Alias for withBindings(List(id)){...} */ def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f) - // free variables cannot be left untouched, because top-level items may be mutually recursive. - // This means that a bound occurrence may precede its binding. + // 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 => { + case id => if (isBuiltin(id)) { + // builtin, do not rename id + } else if (toplevelScope.contains(id)) { + // id references a top-level item + toplevelScope(id) } else { scopes.collectFirst { + // locally bound variable case bnds if bnds.contains(id) => bnds(id) }.getOrElse { - scopes match { - case Nil => id - case _ => - val freshId = freshIdFor(id) - val updatedLast = scopes.last + (id -> freshId) - scopes = scopes.init :+ updatedLast - freshId - } + // free variable, do not rename + id } } - } } override def stmt: PartialFunction[Stmt, Stmt] = { @@ -229,13 +229,31 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { def apply(m: core.ModuleDecl): core.ModuleDecl = suffix = 0 + scopes = List.empty m match { case core.ModuleDecl(path, includes, declarations, externs, definitions, 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 bc67f580b5..586724d059 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -14,37 +14,60 @@ class TestRenamerTests extends CoreTests { val pExpected = parse(renamed, "expected", names) val renamer = new TestRenamer(names) val obtained = renamer(pInput) - shouldBeEqual(obtained, pExpected, clue) + val prettyInput = effekt.core.PrettyPrinter.format(pInput).layout + assertEquals(prettyInput, renamed) + assertEquals(effekt.util.PrettyPrinter.format(pInput).layout, effekt.util.PrettyPrinter.format(pExpected).layout) + assertAlphaEquivalent(obtained, pExpected, clue) } test("No bound local variables"){ - val code = + val input = """module main | - |def foo = { () => - | return (bar: (Int) => Int @ {})(baz:Int) + |def foo$1 = { () => + | return (bar$2: (Int) => Int @ {})(baz$3:Int) |} |""".stripMargin - assertRenamedTo(code, code) + + val expected = + """module main + | + | + | + | + | + | + | + |def foo$1() = { + | return (bar$2: (Int) => Int @ {})(baz$3: Int) + |}""".stripMargin + assertRenamedTo(input, expected) } test("val binding"){ val input = """module main | - |def foo = { () => - | val x = (foo:(Int)=>Int@{})(4) ; - | return x:Int + |def foo$1 = { () => + | val x$2 = (foo$1:(Int)=>Int@{})(4) ; + | return x$2:Int |} |""".stripMargin val expected = """module main | - |def foo = { () => - | val $1 = (foo:(Int)=>Int@{})(4); - | return $1:Int - |} - |""".stripMargin + | + | + | + | + | + | + |def foo$1() = { + | val x$2: Int = { + | foo$1: (Int) => Int @ {}(4) + | }; + | return x$2: Int + |}""".stripMargin assertRenamedTo(input, expected) } @@ -52,19 +75,24 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo = { () => - | var x @ global = (foo:(Int)=>Int@{})(4) ; - | return x:Int + |def foo$1 = { () => + | var x$2 @ global = (foo$1:(Int)=>Int@{})(4) ; + | return x$2:Int |} |""".stripMargin val expected = """module main | - |def foo = { () => - | var $1 @ global = (foo:(Int)=>Int@{})(4); - | return $1:Int - |} - |""".stripMargin + | + | + | + | + | + | + |def foo$1() = { + | var x$2 @ global = (foo$1: (Int) => Int @ {})(4); + | return x$2: Int + |}""".stripMargin assertRenamedTo(input, expected) } @@ -72,17 +100,22 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo = { (x:Int) => - | return x:Int + |def foo$1 = { (x$2:Int) => + | return x$2:Int |} |""".stripMargin val expected = """module main | - |def foo = { ($1:Int) => - | return $1:Int - |} - |""".stripMargin + | + | + | + | + | + | + |def foo$1(x$2: Int) = { + | return x$2: Int + |}""".stripMargin assertRenamedTo(input, expected) } @@ -90,23 +123,31 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |type Data { X(a:Int, b:Int) } - |def foo = { () => + |type Data$1 { X$2(a$3:Int, b$3:Int) } + |def foo$4 = { () => | 12 match { - | X : {(aa:Int, bb:Int) => return aa:Int } + | X$2 : {(aa$5:Int, bb$6:Int) => return aa$5:Int } | } |} |""".stripMargin val expected = """module main | - |type Data { X(a:Int, b:Int) } - |def foo = { () => + | + | + |type Data$1 { + | X$2(a$3: Int, b$3: Int) + |} + | + | + | + |def foo$4() = { | 12 match { - | X : {($1:Int, $2:Int) => return $1:Int } + | X$2 : { (aa$5: Int, bb$6: Int) => + | return aa$5: Int + | } | } - |} - |""".stripMargin + |}""".stripMargin assertRenamedTo(input, expected) } @@ -114,17 +155,22 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo = { ['A](a: A) => - | return a:Identity[A] + |def foo$1 = { ['A$2](a$3: A$2) => + | return a$3: Identity$4[A$2] |} |""".stripMargin val expected = """module main | - |def foo = { ['$1]($2: $1) => - | return $2:Identity[$1] - |} - |""".stripMargin + | + | + | + | + | + | + |def foo$1['A$2](a$3: A$2) = { + | return a$3: Identity$4[A$2] + |}""".stripMargin assertRenamedTo(input, expected) } @@ -132,48 +178,65 @@ class TestRenamerTests extends CoreTests { val input = """ module main | - | def bar = { () => return 1 } - | def main = { () => - | def foo = { () => (bar : () => Unit @ {})() } - | def bar = { () => return 2 } - | (foo : () => Unit @ {})() + | def bar$1 = { () => return 1 } + | def main$2 = { () => + | def foo$3 = { () => (bar$1 : () => Unit @ {})() } + | def bar$4 = { () => return 2 } + | (foo$3 : () => Unit @ {})() | } |""".stripMargin val expected = - """ module main + """module main | - | def bar = { () => return 1 } - | def main = { () => - | def $1 = { () => (bar : () => Unit @ {})() } - | def $2 = { () => return 2 } - | ($1 : () => Unit @ {})() - | } - |""".stripMargin + | + | + | + | + | + | + |def bar$1() = { + | return 1 + |} + |def main$2() = { + | def foo$3() = { + | bar$1: () => Unit @ {}() + | } + | def bar$4() = { + | return 2 + | } + | foo$3: () => Unit @ {}() + |}""".stripMargin assertRenamedTo(input, expected) } + test("shadowing let bindings"){ - val input = + val code = """ module main | - | def main = { () => - | let x = 1 - | let x = 2 - | return x:Int + | def main$1 = { () => + | let x$2 = 1 + | let x$3 = 2 + | return x$3:Int | } |""".stripMargin val expected = - """ module main + """module main | - | def main = { () => - | let $1 = 1 - | let $2 = 2 - | return $2:Int - | } - |""".stripMargin + | + | + | + | + | + | + |def main$1() = { + | let x$2 = 1 + | let x$3 = 2 + | return x$3: Int + |}""".stripMargin - assertRenamedTo(input, expected) + assertRenamedTo(code, expected) } } diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index b5a9de14f8..b3a3a604f5 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -7,28 +7,22 @@ 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 = { - // When the name ends with a `$` symbol followed by an integer, - // we assume that the latter part is the Barendregt id of this name. - val (strippedName, suffix) = { - val i = name.lastIndexOf('$') - if (i >= 0 && i < name.length - 1) { - val suf = name.substring(i + 1) - if (suf.toIntOption.isDefined) - (name.substring(0, i), Some(suf.toInt)) - else (name, None) - } else (name, None) +class Names(private var knownNames: Map[String, Id]) { + private val Suffix = """^(.*)\$(\d+)$""".r + + def idFor(name: String): Id = + builtinSymbolFromString(name).getOrElse { + knownNames.getOrElse(name, { + val id = name match { + case Suffix(base, n) => Id(base, n.toInt) + case _ => + // Only pre-known and builtin names can be written without Barendregt ids. + sys error s"Name $name not known and has no id suffix. Did you mean to write ${name}$$ instead?" + } + knownNames = knownNames.updated(name, id) + id + }) } - knownNames.getOrElse(name, { - val id = suffix match { - case Some(i) => Id(strippedName, i) - case None => Id(strippedName) - } - knownNames = knownNames.updated(name, id) - id - }) - } } @@ -242,7 +236,6 @@ class CoreParsers(names: Names) extends EffektLexers { * Names */ lazy val id = ident ^^ { name => names.idFor(name) } - lazy val wildcard = success(names.idFor("_")) /** * Main Entry @@ -391,9 +384,9 @@ 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.") ) @@ -519,21 +512,12 @@ 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 ~ targs => - // Special-case Resume[result, answer] to use the canonical ResumeSymbol - if (id.name.toString.startsWith("Resume")) - BlockType.Interface(ResumeSymbol, targs): BlockType.Interface - else if (id.name.toString.startsWith("Prompt")) - BlockType.Interface(PromptSymbol, targs): BlockType.Interface - else if (id.name.toString.startsWith("Ref")) - BlockType.Interface(effekt.symbols.builtins.TState.interface, targs): BlockType.Interface - else - BlockType.Interface(id, targs): BlockType.Interface + case id ~ targs => BlockType.Interface(id, targs): BlockType.Interface } | failure("Expected an interface") ) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index a2209f0651..b579955cc1 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -112,9 +112,7 @@ object PrettyPrinter extends ParenPrettyPrinter { //def toDoc(n: Name): Doc = n.toString def toDoc(s: symbols.Symbol): Doc = { - if isBuiltin(s) - then s.name.name - else s.name.name ++ "$" ++ s.id.toString + builtinSymbolToString(s).getOrElse(s.name.name ++ "$" ++ s.id.toString) } def toDoc(e: Expr): Doc = e match { @@ -223,11 +221,6 @@ object PrettyPrinter extends ParenPrettyPrinter { case Return(e) => "return" <+> toDoc(e) - case Val(Wildcard(), tpe, binding, body) => - // RHS must be a single `stmt`, so we have to wrap it in a block. - "val" <+> "_" <> ":" <+> toDoc(tpe) <+> "=" <+> block(toDocStmts(binding)) <> ";" <> line <> - toDocStmts(body) - case Val(id, tpe, binding, body) => // RHS must be a single `stmt`, so we have to wrap it in a block. "val" <+> toDoc(id) <> ":" <+> toDoc(tpe) <+> "=" <+> block(toDocStmts(binding)) <> ";" <> line <> @@ -331,12 +324,18 @@ object PrettyPrinter extends ParenPrettyPrinter { } } +val builtins: Map[String, symbols.Symbol] = { + symbols.builtins.rootTypes + ++ symbols.builtins.rootCaptures + + ("Resume" -> ResumeSymbol) + + ("Prompt" -> PromptSymbol) + + ("Ref" -> effekt.symbols.builtins.TState.interface) +} + def isBuiltin(s: symbols.Symbol): Boolean = - val builtins = { - symbols.builtins.rootTypes - ++ symbols.builtins.rootCaptures - + ("Resume" -> ResumeSymbol) - + ("Prompt" -> PromptSymbol) - + ("Ref" -> effekt.symbols.builtins.TState.interface.name) - } builtins.contains(s.name.name) && builtins(s.name.name) == s + +def builtinSymbolToString(s: symbols.Symbol): Option[String] = + if isBuiltin(s) then Some(s.name.name) else None + +def builtinSymbolFromString(s: String): Option[symbols.Symbol] = builtins.get(s) From 502025581ae702fb0b78a6ac82053eba168af906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 12 Nov 2025 16:37:56 +0100 Subject: [PATCH 06/26] Move builtins resolution to effekt.symbols.builtins --- .../test/scala/effekt/core/TestRenamer.scala | 3 ++- .../src/main/scala/effekt/core/Parser.scala | 3 ++- .../main/scala/effekt/core/PrettyPrinter.scala | 18 +----------------- .../main/scala/effekt/symbols/builtins.scala | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index 55809c162b..4ac49b4322 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -1,6 +1,7 @@ package effekt.core import effekt.{Template, core, symbols} +import effekt.symbols.builtins /** * Freshens bound names in a given term for tests. @@ -45,7 +46,7 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { // 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 => - if (isBuiltin(id)) { + if (builtins.isCoreBuiltin(id)) { // builtin, do not rename id } else if (toplevelScope.contains(id)) { diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index b3a3a604f5..adb59eba18 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -3,6 +3,7 @@ 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} @@ -11,7 +12,7 @@ class Names(private var knownNames: Map[String, Id]) { private val Suffix = """^(.*)\$(\d+)$""".r def idFor(name: String): Id = - builtinSymbolFromString(name).getOrElse { + builtins.coreBuiltinSymbolFromString(name).getOrElse { knownNames.getOrElse(name, { val id = name match { case Suffix(base, n) => Id(base, n.toInt) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index b579955cc1..60b8a2218f 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -112,7 +112,7 @@ object PrettyPrinter extends ParenPrettyPrinter { //def toDoc(n: Name): Doc = n.toString def toDoc(s: symbols.Symbol): Doc = { - builtinSymbolToString(s).getOrElse(s.name.name ++ "$" ++ s.id.toString) + builtins.coreBuiltinSymbolToString(s).getOrElse(s.name.name ++ "$" ++ s.id.toString) } def toDoc(e: Expr): Doc = e match { @@ -323,19 +323,3 @@ object PrettyPrinter extends ParenPrettyPrinter { multi <> string(s) <> multi } } - -val builtins: Map[String, symbols.Symbol] = { - symbols.builtins.rootTypes - ++ symbols.builtins.rootCaptures - + ("Resume" -> ResumeSymbol) - + ("Prompt" -> PromptSymbol) - + ("Ref" -> effekt.symbols.builtins.TState.interface) -} - -def isBuiltin(s: symbols.Symbol): Boolean = - builtins.contains(s.name.name) && builtins(s.name.name) == s - -def builtinSymbolToString(s: symbols.Symbol): Option[String] = - if isBuiltin(s) then Some(s.name.name) else None - -def builtinSymbolFromString(s: String): Option[symbols.Symbol] = builtins.get(s) 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) } From dae88ea04f593250230d1522873b2a34dc1c70db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 10:34:37 +0100 Subject: [PATCH 07/26] Parity of parsing/printing hole spans --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 11 +++++++++++ .../src/main/scala/effekt/core/PrettyPrinter.scala | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index adb59eba18..5037748420 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -335,6 +335,17 @@ class CoreParsers(names: Names) extends EffektLexers { } | (`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 ) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 60b8a2218f..8829212319 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -269,7 +269,15 @@ object PrettyPrinter extends ParenPrettyPrinter { 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 { From 03347e18fb6aab73f0748f01f4ddfd4af1466970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 10:34:51 +0100 Subject: [PATCH 08/26] Add make to list of keywords --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index 5037748420..2bb437d356 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -117,7 +117,7 @@ class EffektLexers extends Parsers { "effect", "interface", "try", "with", "case", "do", "if", "while", "match", "module", "import", "extern", "fun", "at", "box", "unbox", "return", "region", "new", "resource", "and", "is", "namespace", - "reset", "shift" + "reset", "shift", "make" ) def keyword(kw: String): Parser[String] = From 0c04d08a4ec874c39d28e456170467cbfe837765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 10:40:50 +0100 Subject: [PATCH 09/26] Integer literals can be 64-bit (Long) --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index 2bb437d356..a5b1fc9feb 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -227,7 +227,7 @@ 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) } From 195c80c8729bacaf9b3d17c48c6dce6bd9432b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 10:56:55 +0100 Subject: [PATCH 10/26] Print braces around operation bodies --- effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 8829212319..9151242a4f 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -151,7 +151,7 @@ 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, cps, vps, bps) <+> "=" <+> nested(toDoc(body)) + "def" <+> toDoc(id) <> paramsToDoc(tps, cps, vps, bps) <+> "=" <+> block(toDocStmts(body)) } handlerName <+> block(vsep(clauses)) } From 17166446841369bb68796bd1a8f30d61d3513aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 11:08:06 +0100 Subject: [PATCH 11/26] Ignore some feature-dependent errors --- .../test/scala/effekt/core/ReparseTests.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 526dfba313..23e724bf91 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -28,6 +28,16 @@ class ReparseTests extends CoreTests { // 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") + ) + def positives: Set[File] = Set( examplesDir / "pos", examplesDir / "casestudies", @@ -38,7 +48,11 @@ class ReparseTests extends CoreTests { def runPositiveTestsIn(dir: File): Unit = foreachFileIn(dir) { - f => test(s"${f.getPath}") { toCoreThenReparse(f) } + f => if (!ignored.contains(f)) { + test(s"${f.getPath}") { + toCoreThenReparse(f) + } + } } def toCoreThenReparse(input: File) = { From 5d11df011e1bfadaa2fa4dcb47e4e587228ba08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 11:25:21 +0100 Subject: [PATCH 12/26] Fix parsing and printing of string literals --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 2 +- .../shared/src/main/scala/effekt/core/PrettyPrinter.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index a5b1fc9feb..a3e3fdd8af 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -231,7 +231,7 @@ class CoreParsers(names: Names) extends EffektLexers { 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, Type.TString) } + lazy val string: P[Expr] = (multilineString | stringLiteral) ^^ { s => Literal(s, Type.TString) } /** * Names diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 9151242a4f..9704e157e7 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -117,7 +117,7 @@ object PrettyPrinter extends ParenPrettyPrinter { 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, tpe) => toDoc(id) <> ":" <+> toDoc(tpe) @@ -324,10 +324,10 @@ object PrettyPrinter extends ParenPrettyPrinter { // TODO: Escaping? def stringLiteral(s: String): Doc = { if s.contains("\n") then multilineStringLiteral(s) - else "\"" <> string(s) <> "\"" } + else "\"" <> s <> "\"" } def multilineStringLiteral(s: String): Doc = { val multi = "\"\"\"" - multi <> string(s) <> multi + multi <> s <> multi } } From 684989c0d61277d54ad76e77554231f98670c103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 11:44:01 +0100 Subject: [PATCH 13/26] Add simple string (un)escaping --- .../src/main/scala/effekt/core/Parser.scala | 33 +++++++++++++++++-- .../scala/effekt/core/PrettyPrinter.scala | 16 +++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index a3e3fdd8af..fa23abe256 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -141,13 +141,42 @@ class EffektLexers extends Parsers { */ 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") - ^^ { s => s.substring(1, s.size - 1)} + 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 = "\"\"\"" diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 9704e157e7..0955e4f40b 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -321,10 +321,20 @@ object PrettyPrinter extends ParenPrettyPrinter { def block(docs: List[Doc]): Doc = block(vsep(docs, line)) - // TODO: Escaping? - def stringLiteral(s: String): Doc = { + 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 "\"" <> s <> "\"" } + else "\"" <> escapeString(s) <> "\"" def multilineStringLiteral(s: String): Doc = { val multi = "\"\"\"" From b2d10300b51f9c9e2112034af5b5ae6c46fc4bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 11:53:16 +0100 Subject: [PATCH 14/26] Forbid keywords that contain $ to disambiguate from identifiers --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index fa23abe256..fff435f1a6 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -124,7 +124,7 @@ class EffektLexers extends Parsers { 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 From 0079a32b342781c2f2b1af63af380ee6bc78509f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 12:01:07 +0100 Subject: [PATCH 15/26] Ignore some more tests --- effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 23e724bf91..401ff9d413 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -35,7 +35,9 @@ class ReparseTests extends CoreTests { // 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") + File("examples/pos/bidirectional/selfrecursion.effekt"), + // FIXME: Wrong number of type arguments + File("examples/pos/type_omission_op.effekt"), ) def positives: Set[File] = Set( @@ -48,7 +50,8 @@ class ReparseTests extends CoreTests { def runPositiveTestsIn(dir: File): Unit = foreachFileIn(dir) { - f => if (!ignored.contains(f)) { + // We don't currently test *.effekt.md files + f => if (!ignored.contains(f) && !f.getName.endsWith(".md")) { test(s"${f.getPath}") { toCoreThenReparse(f) } From 38684683a9be12b6fad978d39858a632a32422ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 12:21:15 +0100 Subject: [PATCH 16/26] Parse exponential notation in double literals --- effekt/shared/src/main/scala/effekt/core/Parser.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index fff435f1a6..c47f3abe33 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -140,7 +140,8 @@ 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 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) From 67cc52835075b3fc28a60e2d8ecd431274f75d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Mon, 17 Nov 2025 12:24:19 +0100 Subject: [PATCH 17/26] Print capture sets in alphabetic (and hence deterministic) order --- effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 0955e4f40b..368f12fa9a 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -307,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)) From 06eeed5de3b15114bca62f929456ec92932d24c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 10:56:33 +0100 Subject: [PATCH 18/26] Improve assertAlphaEquivalent output --- effekt/jvm/src/test/scala/effekt/core/CoreTests.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index e7d2460df4..144223bcd0 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -48,7 +48,12 @@ trait CoreTests extends munit.FunSuite { 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 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, From f31261687d023fbbd2f381a493d74592b360f6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 10:57:45 +0100 Subject: [PATCH 19/26] Improve toCoreThenReparse output --- .../src/test/scala/effekt/core/ReparseTests.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 401ff9d413..346960cb32 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -69,14 +69,14 @@ class ReparseTests extends CoreTests { val errors = plainMessaging.formatMessages(context.messaging.buffer) sys error errors } - val printed = core.PrettyPrinter.format(coreMod).layout - val reparsed: ModuleDecl = parse(printed)(using Location.empty) val renamer = TestRenamer(Names(defaultNames)) - val lhs = renamer(reparsed) - val rhs = renamer(coreMod) - val lhsPrinted = core.PrettyPrinter.format(lhs).layout - val rhsPrinted = core.PrettyPrinter.format(rhs).layout - assertEquals(lhsPrinted, rhsPrinted) + 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 = From b8eece9014e9a44c712616fd2369b8f4b4642458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 10:58:00 +0100 Subject: [PATCH 20/26] Fix wrong usage of TestRenamer --- effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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") From 036773376fd4e16604ec03473f85de5df66ebce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 10:58:31 +0100 Subject: [PATCH 21/26] Fix Barendregt id handling --- .../test/scala/effekt/core/TestRenamer.scala | 59 +++-- .../scala/effekt/core/TestRenamerTests.scala | 205 +++++++----------- .../src/main/scala/effekt/core/Parser.scala | 25 ++- .../scala/effekt/core/PrettyPrinter.scala | 2 +- .../src/main/scala/effekt/core/Tree.scala | 4 - 5 files changed, 134 insertions(+), 161 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index 4ac49b4322..0d44dde011 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -14,7 +14,7 @@ import effekt.symbols.builtins * * @param C the context is used to copy annotations from old symbols to fresh symbols */ -class TestRenamer(names: Names = Names(Map.empty)) 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 @@ -27,8 +27,20 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { 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 - Id(id.name.name, suffix) + names.idFor(uniqueName) def withBindings[R](ids: List[Id])(f: => R): R = val before = scopes @@ -49,16 +61,18 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { if (builtins.isCoreBuiltin(id)) { // builtin, do not rename id - } else if (toplevelScope.contains(id)) { - // id references a top-level item - toplevelScope(id) } else { scopes.collectFirst { // locally bound variable case bnds if bnds.contains(id) => bnds(id) }.getOrElse { - // free variable, do not rename - id + if (toplevelScope.contains(id)) { + // id references a top-level item + toplevelScope(id) + } else { + // free variable, do not rename + id + } } } } @@ -111,7 +125,7 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { 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, @@ -122,27 +136,25 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { override def rewrite(toplevel: Toplevel): Toplevel = toplevel match { case Toplevel.Def(id, block) => - withBinding(id) { - Toplevel.Def(rewrite(id), rewrite(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) - withBinding(id) { - Toplevel.Val(rewrite(id), rewrite(tpe), resolvedBinding) - } + // 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]) => - withBinding(id) { + // 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]) => - withBinding(id) { + // 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 { @@ -157,8 +169,8 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { override def rewrite(e: Extern) = e match { case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => { - withBinding(id) { - withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) { + // 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, @@ -170,7 +182,6 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { rewrite(body) ) } - } } case Extern.Include(featureFlag, contents) => { Extern.Include(featureFlag, contents) @@ -179,17 +190,19 @@ class TestRenamer(names: Names = Names(Map.empty)) extends core.Tree.Rewrite { override def rewrite(c: Constructor) = c match { case Constructor(id, tparams, fields) => - withBinding(id) { + // 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)) } diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala index 586724d059..56607238ba 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -12,35 +12,30 @@ class TestRenamerTests extends CoreTests { names: Names = Names(defaultNames))(using munit.Location) = { val pInput = parse(input, "input", names) val pExpected = parse(renamed, "expected", names) - val renamer = new TestRenamer(names) + val renamer = new TestRenamer(names, "renamed") // use "renamed" as prefix so we can refer to it val obtained = renamer(pInput) - val prettyInput = effekt.core.PrettyPrinter.format(pInput).layout - assertEquals(prettyInput, renamed) - assertEquals(effekt.util.PrettyPrinter.format(pInput).layout, effekt.util.PrettyPrinter.format(pExpected).layout) - assertAlphaEquivalent(obtained, pExpected, clue) + 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 input = """module main | - |def foo$1 = { () => - | return (bar$2: (Int) => Int @ {})(baz$3:Int) + |def foo = { () => + | return (bar: (Int) => Int @ {})(baz:Int) |} |""".stripMargin - val expected = """module main | - | - | - | - | - | - | - |def foo$1() = { - | return (bar$2: (Int) => Int @ {})(baz$3: Int) - |}""".stripMargin + |def renamed0() = { + | return (bar: (Int) => Int @ {})(baz: Int) + |} + |""".stripMargin + assertRenamedTo(input, expected) } @@ -48,26 +43,21 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo$1 = { () => - | val x$2 = (foo$1:(Int)=>Int@{})(4) ; - | return x$2:Int + |def foo = { () => + | val x = (foo:(Int)=>Int@{})(4) ; + | return x:Int |} |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def foo$1() = { - | val x$2: Int = { - | foo$1: (Int) => Int @ {}(4) + |def renamed0() = { + | val renamed1: Int = { + | renamed0: (Int) => Int @ {}(4) | }; - | return x$2: Int - |}""".stripMargin + | return renamed1: Int + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -75,24 +65,19 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo$1 = { () => - | var x$2 @ global = (foo$1:(Int)=>Int@{})(4) ; - | return x$2:Int + |def foo = { () => + | var x @ global = (foo:(Int)=>Int@{})(4) ; + | return x:Int |} |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def foo$1() = { - | var x$2 @ global = (foo$1: (Int) => Int @ {})(4); - | return x$2: Int - |}""".stripMargin + |def renamed0() = { + | var renamed1 @ global = (renamed0: (Int) => Int @ {})(4); + | return renamed1: Int + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -100,22 +85,17 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo$1 = { (x$2:Int) => - | return x$2:Int + |def renamed0(renamed1: Int) = { + | return renamed1: Int |} |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def foo$1(x$2: Int) = { - | return x$2: Int - |}""".stripMargin + |def renamed0(renamed1: Int) = { + | return renamed1: Int + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -123,31 +103,28 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |type Data$1 { X$2(a$3:Int, b$3:Int) } - |def foo$4 = { () => + |type Data { X(a:Int, b:Int) } + |def foo = { () => | 12 match { - | X$2 : {(aa$5:Int, bb$6:Int) => return aa$5:Int } + | X : {(aa:Int, bb:Int) => return aa:Int } | } |} |""".stripMargin val expected = - """module main - | - | - | - |type Data$1 { - | X$2(a$3: Int, b$3: Int) - |} - | - | - | - |def foo$4() = { - | 12 match { - | X$2 : { (aa$5: Int, bb$6: Int) => - | return aa$5: Int - | } - | } - |}""".stripMargin + """module main + | + |type renamed3 { + | renamed4(a: Int, b: Int) + |} + | + |def renamed2() = { + | 12 match { + | X : { (renamed5: Int, renamed6: Int) => + | return renamed5: Int + | } + | } + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -155,22 +132,17 @@ class TestRenamerTests extends CoreTests { val input = """module main | - |def foo$1 = { ['A$2](a$3: A$2) => - | return a$3: Identity$4[A$2] + |def foo = { ['A](a: A) => + | return a:Identity[A] |} |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def foo$1['A$2](a$3: A$2) = { - | return a$3: Identity$4[A$2] - |}""".stripMargin + |def renamed0['renamed1](renamed2: renamed1) = { + | return renamed2: Identity[renamed1] + |} + |""".stripMargin assertRenamedTo(input, expected) } @@ -178,65 +150,54 @@ class TestRenamerTests extends CoreTests { val input = """ module main | - | def bar$1 = { () => return 1 } - | def main$2 = { () => - | def foo$3 = { () => (bar$1 : () => Unit @ {})() } - | def bar$4 = { () => return 2 } - | (foo$3 : () => Unit @ {})() + | def bar = { () => return 1 } + | def main = { () => + | def foo = { () => (bar : () => Unit @ {})() } + | def bar = { () => return 2 } + | (foo : () => Unit @ {})() | } |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def bar$1() = { + |def renamed0() = { | return 1 |} - |def main$2() = { - | def foo$3() = { - | bar$1: () => Unit @ {}() + |def renamed1() = { + | def renamed2() = { + | renamed0: () => Unit @ {}() | } - | def bar$4() = { + | def renamed3() = { | return 2 | } - | foo$3: () => Unit @ {}() - |}""".stripMargin + | renamed2: () => Unit @ {}() + |} + |""".stripMargin assertRenamedTo(input, expected) } - test("shadowing let bindings"){ - val code = + val input = """ module main | - | def main$1 = { () => - | let x$2 = 1 - | let x$3 = 2 - | return x$3:Int + | def main = { () => + | let x = 1 + | let x = 2 + | return x:Int | } |""".stripMargin val expected = """module main | - | - | - | - | - | - | - |def main$1() = { - | let x$2 = 1 - | let x$3 = 2 - | return x$3: Int - |}""".stripMargin + |def renamed0() = { + | let renamed1 = 1 + | let renamed2 = 2 + | return renamed2: Int + |} + |""".stripMargin - assertRenamedTo(code, expected) + 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 c47f3abe33..3df10fb469 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -10,20 +10,23 @@ import kiama.util.{Position, Range, Source, StringSource} class Names(private var knownNames: Map[String, Id]) { private val Suffix = """^(.*)\$(\d+)$""".r + private var renamed = knownNames - def idFor(name: String): Id = - builtins.coreBuiltinSymbolFromString(name).getOrElse { - knownNames.getOrElse(name, { - val id = name match { - case Suffix(base, n) => Id(base, n.toInt) - case _ => - // Only pre-known and builtin names can be written without Barendregt ids. - sys error s"Name $name not known and has no id suffix. Did you mean to write ${name}$$ instead?" - } - knownNames = knownNames.updated(name, id) + 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 }) - } + ) + } } diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 368f12fa9a..14f2e1e838 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -112,7 +112,7 @@ object PrettyPrinter extends ParenPrettyPrinter { //def toDoc(n: Name): Doc = n.toString def toDoc(s: symbols.Symbol): Doc = { - builtins.coreBuiltinSymbolToString(s).getOrElse(s.name.name ++ "$" ++ s.id.toString) + builtins.coreBuiltinSymbolToString(s).getOrElse(s.name.name) } def toDoc(e: Expr): Doc = e match { diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index f416af24ac..b3419f3cb7 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -91,10 +91,6 @@ object Id { val name = n } def apply(n: String): Id = apply(symbols.Name.local(n)) - def apply(n: String, theId: Int): Id = new symbols.Symbol { - val name = symbols.Name.local(n) - override lazy val id = theId - } def apply(n: Id): Id = apply(n.name) } From bf50d1a06cc063a5a76fade9075ee9501b92f4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 11:09:35 +0100 Subject: [PATCH 22/26] Undo some whitespace changes to reduce diff --- .../src/main/scala/effekt/core/Parser.scala | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index 3df10fb469..cc0cec6915 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -45,13 +45,13 @@ class EffektLexers extends Parsers { lazy val ident = (not(anyKeyword) ~> name - | failure("Expected an identifier") - ) + | failure("Expected an identifier") + ) lazy val identRef = (not(anyKeyword) ~> qualifiedName - | failure("Expected an identifier") - ) + | failure("Expected an identifier") + ) lazy val `=` = literal("=") lazy val `:` = literal(":") @@ -290,7 +290,7 @@ class CoreParsers(names: Names) extends EffektLexers { // ------- lazy val externDecl: P[Extern] = ( `extern` ~> featureFlag ~ externBody ^^ Extern.Include.apply - | `extern` ~> (captures <~ `def`) ~ signature ~ (`=` ~> (featureFlag ~ externBodyTemplate)) ^^ { + | `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, @@ -316,8 +316,8 @@ class CoreParsers(names: Names) extends EffektLexers { lazy val featureFlag: P[FeatureFlag] = ("else" ^^ { _ => FeatureFlag.Default(Span.missing) } - | ident ^^ (id => FeatureFlag.NamedFeatureFlag(id, Span.missing)) - ) + | ident ^^ (id => FeatureFlag.NamedFeatureFlag(id, Span.missing)) + ) lazy val externBody = multilineString | stringLiteral @@ -342,33 +342,33 @@ class CoreParsers(names: Names) extends EffektLexers { // Definitions // ----------- lazy val toplevel: P[Toplevel] = - ( `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~/> stmt) ^^ { - case (name ~ tpe ~ binding) => Toplevel.Val(name, tpe.getOrElse(binding.tpe), binding) - } - | `def` ~> id ~ (`=` ~/> block) ^^ Toplevel.Def.apply - | `def` ~> id ~ parameters ~ (`=` ~> stmt) ^^ { + ( `val` ~> id ~ maybeTypeAnnotation ~ (`=` ~/> stmt) ^^ { + case (name ~ tpe ~ binding) => Toplevel.Val(name, tpe.getOrElse(binding.tpe), binding) + } + | `def` ~> id ~ (`=` ~/> block) ^^ Toplevel.Def.apply + | `def` ~> id ~ parameters ~ (`=` ~> stmt) ^^ { case name ~ (tparams, cparams, vparams, bparams) ~ body => Toplevel.Def(name, BlockLit(tparams, cparams, vparams, bparams, body)) } - | failure("Expected a definition.") - ) + | failure("Expected a definition.") + ) // Statements // ---------- 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 ^^ { + | `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) } - | (`if` ~> `(` ~/> expr <~ `)`) ~ stmt ~ (`else` ~> stmt) ^^ Stmt.If.apply - | `region` ~> blockLit ^^ Stmt.Region.apply - | `<>` ~> `@` ~> (stringLiteral <~ `:`) ~ (integerLiteral <~ `:`) ~ integerLiteral ^^ { + | (`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://")) @@ -379,9 +379,9 @@ class CoreParsers(names: Names) extends EffektLexers { } Hole(effekt.source.Span(source, from.toInt, to.toInt)) } - | `<>` ^^^ Hole(effekt.source.Span.missing) - | (expr <~ `match`) ~/ (`{` ~> many(clause) <~ `}`) ~ (`else` ~> stmt).? ^^ Stmt.Match.apply - ) + | `<>` ^^^ Hole(effekt.source.Span.missing) + | (expr <~ `match`) ~/ (`{` ~> many(clause) <~ `}`) ~ (`else` ~> stmt).? ^^ Stmt.Match.apply + ) lazy val stmts: P[Stmt] = ( (`let` ~ `!` ~/> id) ~ (`=` ~/> maybeParens(blockVar)) ~ maybeTypeArgs ~ valueArgs ~ blockArgs ~ stmts ^^ { From 81c0052c073f0f54930fa37d34f9d49f5f7716ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 11:22:07 +0100 Subject: [PATCH 23/26] Ignore limitation of TestRenamer wrt renaming captures --- effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala index 346960cb32..e3aef4a6f0 100644 --- a/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala @@ -38,6 +38,10 @@ class ReparseTests extends CoreTests { 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( From 1aec2d46b31896007cce9f3444a8c3786b922edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 11:36:29 +0100 Subject: [PATCH 24/26] Fix TestRenamerTests --- .../jvm/src/test/scala/effekt/core/TestRenamerTests.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala index 56607238ba..8e4eaeafdb 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -113,14 +113,14 @@ class TestRenamerTests extends CoreTests { val expected = """module main | - |type renamed3 { - | renamed4(a: Int, b: Int) + |type renamed1 { + | renamed0(a: Int, b: Int) |} | |def renamed2() = { | 12 match { - | X : { (renamed5: Int, renamed6: Int) => - | return renamed5: Int + | X : { (renamed3: Int, renamed4: Int) => + | return renamed3: Int | } | } |} From 9755de4a281cd1ce540f150256dd03ab8b0bd775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Wed, 19 Nov 2025 11:52:30 +0100 Subject: [PATCH 25/26] Separate core reparse tests --- .github/workflows/ci-pr.yml | 18 ++++++++++++++++++ build.sbt | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) 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..542d1507be 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,6 +231,12 @@ 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 @@ -245,7 +252,8 @@ 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 From 6ced795a1b371dc6c7af4e84a625ee0208e504f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Thu, 27 Nov 2025 14:40:28 +0100 Subject: [PATCH 26/26] Improve name in build.sbt --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 542d1507be..818bd80fdc 100644 --- a/build.sbt +++ b/build.sbt @@ -242,8 +242,8 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e 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", @@ -256,7 +256,7 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e "effekt.core.ReparseTests", ) - val remaining = allTests -- backendTests + val remaining = allTests -- separatedTests if (remaining.isEmpty) { log.info("No remaining tests")