diff --git a/effekt/js/src/main/scala/effekt/EffektConfig.scala b/effekt/js/src/main/scala/effekt/EffektConfig.scala index 63292b5bc..3ce989cdb 100644 --- a/effekt/js/src/main/scala/effekt/EffektConfig.scala +++ b/effekt/js/src/main/scala/effekt/EffektConfig.scala @@ -41,4 +41,6 @@ trait EffektConfig { def timed() = false def debug() = false + + def useNewNormalizer(): Boolean = false } diff --git a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala index 154b49974..71880dbdf 100644 --- a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala +++ b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala @@ -200,6 +200,19 @@ class EffektConfig(args: Seq[String]) extends REPLConfig(args.takeWhile(_ != "-- group = debugging ) + // Experimental + // ------------ + + lazy val newNormalizer = toggle( + "new-normalizer", + descrYes = "Use the new normalizer implementation", + descrNo = "Use the old normalizer implementation", + default = Some(false), + noshort = true, + prefix = "no-", + group = group("Experimental Features") + ) + /** * Tries to find the path to the standard library. Proceeds in the following * order: @@ -262,6 +275,8 @@ class EffektConfig(args: Seq[String]) extends REPLConfig(args.takeWhile(_ != "-- def documenter(): Boolean = showDocumentation() || writeDocumentation() def timed(): Boolean = time.isSupplied && !server() + + def useNewNormalizer(): Boolean = newNormalizer() validateFilesIsDirectory(includePath) diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index 3525a22fe..ca4c5f0bd 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -28,6 +28,21 @@ trait CoreTests extends munit.FunSuite { |""".stripMargin }) + def shouldBeEqual(obtained: Toplevel, expected: Toplevel, clue: => Any)(using Location) = + assertEquals(obtained, expected, { + s"""${clue} + |===================== + |Got: + |---- + |${effekt.core.ReparsablePrettyPrinter.format(obtained).layout} + | + |Expected: + |--------- + |${effekt.core.ReparsablePrettyPrinter.format(expected).layout} + | + |""".stripMargin + }) + def shouldBeEqual(obtained: Stmt, expected: Stmt, clue: => Any)(using Location) = assertEquals(obtained, expected, { s"""${clue} @@ -55,6 +70,7 @@ trait CoreTests extends munit.FunSuite { assertEquals(obtainedPrinted, expectedPrinted) shouldBeEqual(obtainedRenamed, expectedRenamed, clue) } + def assertAlphaEquivalentStatements(obtained: Stmt, expected: Stmt, clue: => Any = "values are not alpha-equivalent", diff --git a/effekt/jvm/src/test/scala/effekt/core/NewNormalizerTests.scala b/effekt/jvm/src/test/scala/effekt/core/NewNormalizerTests.scala new file mode 100644 index 000000000..8da453aa8 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/core/NewNormalizerTests.scala @@ -0,0 +1,900 @@ +package effekt.core + +import effekt.PhaseResult.CoreTransformed +import effekt.context.{Context, IOModuleDB} +import effekt.core.optimizer.{Deadcode, Normalizer, Optimizer} +import effekt.core.optimizer.normalizer.NewNormalizer +import effekt.util.PlainMessaging +import effekt.* +import kiama.output.PrettyPrinterTypes.Document +import kiama.util.{Source, StringSource} +import munit.Location + +class NewNormalizerTests extends CoreTests { + object plainMessaging extends PlainMessaging + object context extends Context with IOModuleDB { + val messaging = plainMessaging + + object frontend extends NormalizeOnly + + override lazy val compiler = frontend.asInstanceOf + } + + def compileString(content: String): (Id, symbols.Module, ModuleDecl) = + val config = new EffektConfig(Seq("--Koutput", "string")) + config.verify() + context.setup(config) + context.frontend.compile(StringSource(content, "input.effekt"))(using context).map { + case (_, decl) => decl + }.getOrElse { + val errors = plainMessaging.formatMessages(context.messaging.buffer) + sys error errors + } + + def normalize(contents: String): (Id, core.ModuleDecl) = { + val (main, mod, decl) = compileString(contents) + (main, decl) + } + + def assertAlphaEquivalentToplevels( + actual: ModuleDecl, + expected: ModuleDecl, + defNames: List[String], + externNames: List[String] = List(), + declNames: List[String] = List(), + ctorNames: List[(String, String)] = List() + )(using Location): Unit = { + + def findDef(mod: ModuleDecl, name: String) = + mod.definitions.find(_.id.name.name == name) + .getOrElse(throw new NoSuchElementException(s"Definition '$name' not found")) + + def findDecl(mod: ModuleDecl, name: String)= + mod.declarations.find(_.id.name.name == name) + .getOrElse(throw new NoSuchElementException(s"Declaration '$name' not found")) + + def findCtor(data: Data, name: String) = + data.constructors.find(_.id.name.name == name) + .getOrElse(throw new NoSuchElementException( + s"Constructor '$name' not found in data '${data.id.name.name}'" + )) + + def findExternDef(mod: ModuleDecl, name: String) = + mod.externs.collect { case d: Extern.Def => d } + .find(_.id.name.name == name) + .getOrElse(throw new NoSuchElementException(s"Extern def '$name' not found")) + + val externPairs: List[(Id, Id)] = + externNames.flatMap { name => + val canon = Id(name) + List( + findExternDef(actual, name).id -> canon, + findExternDef(expected, name).id -> canon + ) + } + + val declPairs: List[(Id, Id)] = + declNames.flatMap { name => + val canon = Id(name) + List( + findDecl(actual, name).id -> canon, + findDecl(expected, name).id -> canon + ) + } + + val ctorPairs: List[(Id, Id)] = + ctorNames.flatMap { case (dataName, ctorName) => + val canon = Id(ctorName) + val actualData = findDecl(actual, dataName) match { + case d: Data => d + case _: Interface => throw new IllegalArgumentException( + s"Expected data declaration for '$dataName', found interface" + ) + } + val expectedData = findDecl(expected, dataName) match { + case d: Data => d + case _: Interface => throw new IllegalArgumentException( + s"Expected data declaration for '$dataName', found interface" + ) + } + List( + findCtor(actualData, ctorName).id -> canon, + findCtor(expectedData, ctorName).id -> canon + ) + } + + def compareOneDef(name: String): Unit = { + val aDef = findDef(actual, name) + val eDef = findDef(expected, name) + val renamer = TestRenamer(Names(defaultNames), "$", preserveUserAnnotatedPrefix = false) + val obtainedRenamed = renamer(aDef) + val expectedRenamed = renamer(eDef) + val obtainedPrinted = effekt.core.ReparsablePrettyPrinter.format(obtainedRenamed).layout + val expectedPrinted = effekt.core.ReparsablePrettyPrinter.format(expectedRenamed).layout + assertEquals(obtainedPrinted, expectedPrinted) + } + + defNames.foreach(compareOneDef) + } + + // This example shows a box that contains an extern reference. + // The normalizer is able to unbox this indirection away. + test("extern in box") { + val input = """ + |extern def foo: Int = vm"42" + | + |def run(): Int = { + | val f = box { + | box foo + | } at { io } + | + | val x = /* unbox */ f()() + | return x + |} + | + |def main() = println(run()) + |""".stripMargin + + val expected = parse( + """ + |module input + | + |extern {io} def foo(): Int = vm"42" + | + |def run() = { + | def f1() = { + | def f2() = { + | let ! x = foo: () => Int @ {io}() + | return x: Int + | } + | let y = box {io} f2: () => Int @ {io} + | return y: () => Int at {io} + | } + | val z: () => Int at {io} = { + | f1: () => (() => Int at {io}) @ {}() + | }; + | def r = unbox z: () => Int at {io} + | r: () => Int @ {io}() + |} + | + |""".stripMargin) + + + val (mainId, actual) = normalize(input) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), List("foo")) + } + + // This example shows a box that cannot be normalized away. + // This is because the box is passed to an extern definition. + test("box passed to extern") { + val input = + """ + |extern def foo(f: => Int at {}) at {io}: Int = vm"42" + | + |def run(): Int = { + | val f = box { + | 42 + | } at {} + | + | val x = foo(f) + | return x + |} + | + |def main() = println(run()) + |""".stripMargin + + val expected = + parse(""" + |module input + | + |extern {io} def foo(): Int = vm"42" + | + |def run() = { + | def f() = { + | let x = 42 + | return x: Int + | } + | let fBox = box {} f: () => Int @ {} + | let ! r = foo: (() => Int at {}) => Int @ {io}(fBox: () => Int at {}) + | return r: Int + |} + |""".stripMargin + ) + + val (mainId, actual) = normalize(input) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), List("foo")) + } + + // This example shows an unbox that cannot be normalized away. + // This is because the box is retrieved from an extern definition. + test("unbox blocked by extern") { + val input = + """ + |extern def foo() at {}: => Int at {} = vm"42" + | + |def run(): Int = { + | val x = foo()() + | return x + |} + | + |def main() = println(run()) + |""".stripMargin + + val expected = + parse(""" + |module input + | + |extern {} def foo(): => Int at {} = vm"42" + | + |def run() = { + | let x1 = (foo: () => (() => Int at {}) @ {})() + | def x2 = (unbox x1: () => Int at {}) + | x2: () => Int @ {}() + |} + |""".stripMargin + ) + + val (mainId, actual) = normalize(input) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), List("foo")) + } + + test("Mutable variable with infixPlus is optimized to a single constant let-binding") { + val input = + """ + |def run(): Int = { + | var x = 41 + | x = x + 1 + | return x + |} + | + |def main() = println(run()) + |""".stripMargin + + val expected = + parse(""" + |module input + | + |extern {} def infixPlus(x: Int, y: Int): Int = vm "" + | + |def run() = { + | let x = 42 + | return x: Int + |} + |""".stripMargin + ) + + val (mainId, actual) = normalize(input) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), List("infixPlus")) + } + + // This test case shows mutable variable assignments turning into static let-bindings + test("Mutable Peano Nats turn into let-bindings") { + val input = + """ + |type Nat { + | Z() + | S(pred: Nat) + |} + | + |def toInt(n: Nat): Int = n match { + | case Z() => 0 + | case S(pred) => 1 + toInt(pred) + |} + | + |def run(): Nat = { + | var x = Z() + | x = S(x) + | return x + |} + | + |def main() = println(run().toInt()) + |""".stripMargin + + val expected = + parse(""" + |module input + | + |type Nat { + | Z() + | S(pred: Nat) + |} + | + |def run() = { + | let x1 = make Nat Z() + | let x2 = make Nat S(x1: Nat) + | return x2: Nat + |} + |""".stripMargin + ) + + val (mainId, actual) = normalize(input) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), List(), List("Nat"), List(("Nat", "Z"), ("Nat", "S"))) + } + + // This test case shows a mutable variable that is captured in an effect handler. + // The resulting core code shows how the reference is passed to the handler block. + // Even though the variable is not mutated, the normalizer currently cannot eliminate the reference. + // This is because the stack used to normalize the handler is currently treated as "unknown". + test("Mutable variable read in handler") { + val input = + """ + |effect bar: Unit + | + |extern def foo(x: Int) at {io}: Unit = vm"" + | + |def run() = { + | var x = 1 + | try { + | do bar() + | } with bar { + | foo(x) + | } + |} + | + |def main() = println(run()) + | + |""".stripMargin + + val (mainId, actual) = normalize(input) + + val expected = + parse(""" + |module input + | + |interface bar { + | bar: => Unit + |} + | + |extern {io} def foo(x: Int): Unit = vm"" + | + |def run() = { + | let xv = 1 + | def handler(){r @ r: Ref[Int]} {p @ p: Prompt[Unit]} = { + | shift(p: Prompt[Unit] @ {p}) {{k: Resume[Unit, Unit]} => + | get v : Int = ! r @ r; + | let ! o = foo: (Int) => Unit @ {io}(v: Int) + | return o: Unit + | } + | } + | var z @ z = xv: Int; + | reset { (){p @ p: Prompt[Unit]} => + | handler: (){z: Ref[Int]} {p: Prompt[Unit]} => Unit @ {io}(){z: Ref[Int] @ {z}} {p: Prompt[Unit] @ {p}} + | } + |} + |""".stripMargin + ) + + assertAlphaEquivalentToplevels(actual, expected, List("run"), declNames=List("bar"), externNames=List("foo")) + } + + // This test case shows a mutable variable passed to the identity function. + // Currently, the normalizer is not able to see through the identity function, + // but it does ignore the mutable variable and just passes the initial value. + // Inlining is performed by a separate inlining phase. + test("Pass mutable variable to identity function uses let binding") { + val input = + """ + |def run(): Int = { + | def f(x: Int) = x + | var x = 42 + | f(x) + |} + | + |def main() = println(run()) + | + |""".stripMargin + + val (mainId, actual) = normalize(input) + + val expected = + parse( + """ + |module input + | + |def run() = { + | def f(x: Int) = { + | return x: Int + | } + | let y = 42 + | f: (Int) => Int @ {}(y: Int) + |} + |""".stripMargin + ) + + assertAlphaEquivalentToplevels(actual, expected, List("run")) + } + + // This test shows that when the value of a mutable variable is known, we can directly pass the let-bound value. + test("Mutate mutable variable before passing it to identity function") { + val input = + """ + |def run(): Int = { + | def f(x: Int) = x + | var x = 42 + | x = 43 + | f(x) + |} + | + |def main() = println(run()) + | + |""".stripMargin + + val (mainId, actual) = normalize(input) + + val expected = + parse( + """ + |module input + | + |def run() = { + | def f(x: Int) = { + | return x: Int + | } + | let x = 43 + | f: (Int) => Int @ {}(x: Int) + |} + |""".stripMargin + ) + + assertAlphaEquivalentToplevels(actual, expected, List("run")) + } + + // This test shows a mutable reference captured by a block parameter. + // During normalization, this block parameter gets lifted to a `def`. + // One might hope for this mutable variable to be eliminated entirely, + // but currently the normalizer does not inline definitions. + test("Block param capturing mutable reference can be lifted") { + val input = + """ + |def run(): Int = { + | def modifyProg { setter: Int => Unit }: Unit = { + | setter(2) + | () + | } + | var x = 1 + | modifyProg { y => x = y } + | x + |} + | + |def main() = println(run()) + |""".stripMargin + + val (mainId, actual) = normalize(input) + + val expected = + parse( + """ + |module input + | + |def run() = { + | def modifyProg(){setter @ sc: (Int) => Unit} = { + | let v = 2 + | val o: Unit = { + | setter: (Int) => Unit @ {sc}(v: Int) + | }; + | let u = () + | return u: Unit + | } + | let v = 1 + | def setter(v: Int){xr @ xr: Ref[Int]} = { + | put xr @ xr = v: Int; + | let u = () + | return u: Unit + | } + | var xvar @ xvar = v: Int; + | val r: Unit = { + | modifyProg: (){setter: (Int) => Unit} => Unit @ {}(){ (v: Int) => + | setter: (Int){x: Ref[Int]} => Unit @ {}(v: Int){xvar: Ref[Int] @ {xvar}} + | } + | }; + | get o : Int = ! xvar @ xvar; + | return o: Int + |} + |""".stripMargin + ) + + assertAlphaEquivalentToplevels(actual, expected, List("run")) + } + + // This test case shows that we can normalize a potentially recursive block that never actually recurses. + // The while loop in the surface language is translated to a recursive block in core. + // Note that the variable `v` and its capture need to be correctly passed around. + test("Can normalize while loop with non-satisfiable condition") { + val input = + """ + |def run() = { + | var v = 2; + | while (v <= 1) {} + | 0 + |} + | + |def main() = println(run()) + |""".stripMargin + + // Does not throw + normalize(input) + } + + test("Mutable variables are added as an extra parameter") { + val input = + """ + |def main() = { + | var x = 1 + | def update() = { x = 2 } + | update() + | println(x) + |} + """.stripMargin + + normalize(input) + } + + test("Reset/Shift with mutable variable") { + val input = + """ + |effect Eff(): Unit + |def main() = { + | var x = 0 + | try { + | x = 1 + | do Eff() + | } with Eff { resume(()) } + | println(x) + |} + """.stripMargin + + normalize(input) + } + + test("Mutable variable and recursive function") { + val input = + """ + |def main() = { + | var x = 0 + | def loop(n: Int): Unit = { + | if (n == 0) () + | else { + | x = x + 1 + | loop(n - 1) + | } + | } + | loop(5) + | println(x) + |} + """.stripMargin + + normalize(input) + } + + test("basic region usage") { + val input = + """ + |def main() = { + | val y = region r { + | var x in r = 42 + | x = x + 1 + | x + | } + | println(y) + |} + |""".stripMargin + + normalize(input) + } + + test("region parameter") { + val input = + """ + |def main() = { + | val y = region reg { + | def foo(init: Int) {r: Region} = { + | var x in r = init + | x = x + 1 + | x + | } + | foo(42) {reg} + | } + | println(y) + |} + |""".stripMargin + + normalize(input) + } + + test("Can lookup top-level def") { + val input = + """ + |def top(): Int = 43 + | + |def main() = { + | val x = top() + top() + | println(x.show) + |} + |""".stripMargin + + // Does not throw + normalize(input) + } + + test("Can lookup top-level val") { + val input = + """ + |val top: Int = 43 + | + |def main() = { + | val x = top + top + | println(x.show) + |} + |""".stripMargin + + // Does not throw + normalize(input) + } + + // This case tests a subtle aspect of normalizing with block parameters. + // Consider the provided input program. + // The normalized program looks as follows, with irrelevant details elided: + // ```scala + // () { + // def break = ... + // def run = ... + // let x = 1 + // def f = (v: Int){f} {y} {p} { + // let x = 2 + // y := x + // jump break(){p} + // } + // val o = reset {{p} => + // var y = x + // val tmp = jump run(){f @ [y, p]} + // let z = () + // return z + // } + // jump println(o) + // } + // ``` + // As you can see, the block argument supplied to block parameter `prog` of `run` is lifted to a `def f`. + // This definition needs to abstract over all free captures and variables in the body. + // In particular, the call side of `f` needs to supply the correct closure. + // Therefore, it is not possible to simply call `run` as follows: + // ```scala + // jump run(){f} + // ``` + // where `f` would be passed as a block variable. + // Instead, we need to "eta-expand" this parameter to a block that calls f with the correct captures: + // ```scala + // jump run(){f @ [y, p]} + // ``` + // where `y` and `p` are the correct captures from the reset body. + test("Block parameters get lifted and captures are passed correctly") { + val input: String = + """ + |effect break(): Unit + | + |def main() = { + | val x = try { + | def run { prog: (Int) {() => Unit} => Unit }: Unit = prog(42) { () => () } + | var y = 1 + | run { (v) { f } => + | y = 2 + | do break() + | } + | () + | } with break { + | () + | } + | println(x) + |} + |""".stripMargin + + // Does not throw + normalize(input) + } + + test("Compile-time string concatenation with neutral calls in between") { + val input = + """ + |extern def foo: String = vm"" + | + |def run(): String = { + | "a" ++ "b" ++ foo() ++ "c" ++ "d" + |} + | + |def main() = println(run()) + |""".stripMargin + + val expected = + """module input + |extern {io} def foo(): String = vm"42" + |def run() = { + | let ! s2 = foo: () => String @ {io}() + | let s1 = "ab" + | let r = (infixPlusPlus: (String, String) => String @ {})((infixPlusPlus: (String, String) => String @ {})(s1: String, s2: String), "cd") + | return r: String + |} + |""".stripMargin + + val (mainId, actual) = normalize(input) + assertAlphaEquivalentToplevels(actual, parse(expected), List("run"), List("foo")) + } + + // Equality checks on neutral integer expressions where both sides have the same normal form can be evaluated at compile time. + test("Compile-time integer equality on certain neutral expressions") { + val input = + """ + |extern def z: Int = vm"0" + | + |def run(x: Int): Bool = { + | 42 + 2 * x == x + 42 + x + |} + | + |def main() = { + | val x = z() + | println(run(x)) + |} + |""".stripMargin + + val expected = + """module input + |extern {} def infixEq(x: Int, y: Int): Bool = vm "" + |def run(x: Int) = { + | let x = true + | return x: Bool + |} + |""".stripMargin + + val (mainId, actual) = normalize(input) + assertAlphaEquivalentToplevels(actual, parse(expected), List("run"), List("infixEq")) + } + + // Even for impure extern calls, we do not assume that different calls return the same value. + test("Reflexivity does not hold for impure neutral extern defs") { + val input = + """ + |extern def z at {io}: Int = vm"0" + | + |def run(x: Int): Bool = { + | z() == z() + |} + | + |def main() = { + | val x = z() + | println(run(x)) + |} + |""".stripMargin + + val expected = + """module input + |extern {io} def infixEq(x: Int, y: Int): Bool = vm "" + |def run(x: Int) = { + | let ! x1 = z: () => Int @ {io}() + | let ! x2 = z: () => Int @ {io}() + | let x = (infixEq: (Int, Int) => Bool @ {})(x1: Int, x2: Int) + | return x: Bool + |} + |""".stripMargin + + val (mainId, actual) = normalize(input) + assertAlphaEquivalentToplevels(actual, parse(expected), List("run"), List("infixEq")) + } + + test("Reflexivity holds for pure neutral extern defs that return integers") { + val input = + """ + |extern def z at {}: Int = vm"0" + | + |def run(): Bool = { + | z() == z() + |} + | + |def main() = { + | println(run()) + |} + |""".stripMargin + + val expected = + """module input + |extern {} def infixEq(x: Int, y: Int): Bool = vm "" + |def run() = { + | let x = true + | return x: Bool + |} + |""".stripMargin + + val (mainId, actual) = normalize(input) + assertAlphaEquivalentToplevels(actual, parse(expected), List("run"), List("infixEq")) + } + + test("infixEq on strings with pure neutral extern defs") { + val input = + """ + |extern def s at {}: String = vm"hello" + | + |def run(): Bool = { + | val x = s() + | ("a" ++ x) ++ "b" == "a" ++ (x ++ "b") + |} + | + |def main() = { + | println(run()) + |} + |""".stripMargin + + val expected = + """module input + |extern {} def infixEq(x: String, y: String): Bool = vm "" + |def run() = { + | let x = true + | return x: Bool + |} + |""".stripMargin + + val (mainId, actual) = normalize(input) + assertAlphaEquivalentToplevels(actual, parse(expected), List("run"), List("infixEq")) + } + + test("Mutable variable being boxed") { + val input = + """ + |def main() = { + | var x = 0 + | def makeInc() = { + | box { x = x + 1} + | } + | val inc = makeInc() + | (unbox inc)() + | println(x) + |} + |""".stripMargin + normalize(input) // does not throw + } + + test("Single static capture turns into multiple runtime captures") { + val input = + """ + |def main() = { + | def foo{f: () => Unit}: () => Unit at {f} = box f + | var x = 1; + | var y = 2; + | foo { => println(x + y) } () + |} + |""".stripMargin + normalize(input) // does not throw + } +} + +/** + * A "backend" that simply outputs the normalized core module. + */ +class NormalizeOnly 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) + var tree = Deadcode.remove(mainSymbol, core) + val normalizer = NewNormalizer() + tree = normalizer.run(tree) + Normalizer.assertNormal(tree) + (mainSymbol, mod, tree) + } +} diff --git a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala index 653d8ff6e..c216ebe1d 100644 --- a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala @@ -102,7 +102,7 @@ class RenamerTests extends CoreTests { """module main | |def foo = { () => - | var x @ global = (foo:(Int)=>Int@{})(4) ; + | var x @ x = (foo:(Int)=>Int@{})(4) ; | return x: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 9344a3250..5aeec6907 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -307,7 +307,8 @@ class CoreParsers(names: Names) extends EffektLexers { case captures ~ (id, tparams, cparams, vparams, bparams, result) ~ (ff ~ templ) => Extern.Def( id, tparams, cparams, vparams, bparams, result, captures, - ExternBody.StringExternBody(ff, templ) + ExternBody.StringExternBody(ff, templ), + None ) } | `extern` ~> `type` ~> id ~ typeParams.? ^^ { diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 464035159..1b1594c6f 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -16,6 +16,9 @@ class PrettyPrinter(printDetails: Boolean, printInternalIds: Boolean = true) ext def format(t: ModuleDecl): Document = pretty(toDoc(t), 4) + def format(t: Toplevel): Document = + pretty(toDoc(t), 4) + def format(defs: List[Toplevel]): String = pretty(toDoc(defs), 60).layout @@ -75,7 +78,7 @@ class PrettyPrinter(printDetails: Boolean, printInternalIds: Boolean = true) ext vsep(definitions map toDoc) def toDoc(e: Extern): Doc = e match { - case Extern.Def(id, tps, cps, vps, bps, ret, capt, bodies) => + case Extern.Def(id, tps, cps, vps, bps, ret, capt, bodies, _) => "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 @@ -209,7 +212,7 @@ class PrettyPrinter(printDetails: Boolean, printInternalIds: Boolean = true) ext def toDoc(s: Stmt): Doc = s match { // requires a block to be readable: - case _ : (Stmt.Def | Stmt.Let | Stmt.Val | Stmt.Alloc | Stmt.Var | Stmt.Get | Stmt.Put) => block(toDocStmts(s)) + case _ : (Stmt.Def | Stmt.Let | Stmt.ImpureApp | Stmt.Val | Stmt.Alloc | Stmt.Var | Stmt.Get | Stmt.Put) => block(toDocStmts(s)) case other => toDocStmts(s) } diff --git a/effekt/shared/src/main/scala/effekt/core/Renamer.scala b/effekt/shared/src/main/scala/effekt/core/Renamer.scala index 40e6d4003..45dd04692 100644 --- a/effekt/shared/src/main/scala/effekt/core/Renamer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Renamer.scala @@ -73,8 +73,7 @@ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core case core.Var(ref, init, capt, body) => val resolvedInit = rewrite(init) - val resolvedCapt = rewrite(capt) - withBinding(ref) { core.Var(rewrite(ref), resolvedInit, resolvedCapt, rewrite(body)) } + withBindings(List(ref, capt)) { core.Var(rewrite(ref), resolvedInit, rewrite(capt), rewrite(body)) } case core.Get(id, tpe, ref, capt, body) => val resolvedRef = rewrite(ref) diff --git a/effekt/shared/src/main/scala/effekt/core/Show.scala b/effekt/shared/src/main/scala/effekt/core/Show.scala index 5d4ffc538..c0bd7bc66 100644 --- a/effekt/shared/src/main/scala/effekt/core/Show.scala +++ b/effekt/shared/src/main/scala/effekt/core/Show.scala @@ -275,7 +275,7 @@ object Show extends Phase[CoreTransformed, CoreTransformed] { def findExternDef(name: String, vts: List[ValueType])(using dctx: DeclarationContext)(using Context): Block.BlockVar = dctx.findExternDef(name, vts) match case None => Context.abort(pretty"Could not find show definition for ${vts}") - case Some(Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body)) => + case Some(Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body, vmBody)) => Block.BlockVar(id, BlockType.Function(tparams, cparams, vparams map (_.tpe), bparams map (_.tpe), ret), annotatedCapture) def generateShowInstance(decl: Declaration, targs: List[ValueType])(using ctx: ShowContext, dctx: DeclarationContext)(using Context): Option[Toplevel.Def] = decl match { diff --git a/effekt/shared/src/main/scala/effekt/core/TestRenamer.scala b/effekt/shared/src/main/scala/effekt/core/TestRenamer.scala index b85511a0d..47642e40a 100644 --- a/effekt/shared/src/main/scala/effekt/core/TestRenamer.scala +++ b/effekt/shared/src/main/scala/effekt/core/TestRenamer.scala @@ -116,8 +116,7 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "$", preserv case core.Var(ref, init, capt, body) => val resolvedInit = rewrite(init) - val resolvedCapt = rewrite(capt) - withBinding(ref) { core.Var(rewrite(ref), resolvedInit, resolvedCapt, rewrite(body)) } + withBindings(List(ref, capt)) { core.Var(rewrite(ref), resolvedInit, rewrite(capt), rewrite(body)) } case core.Get(id, tpe, ref, capt, body) => val resolvedRef = rewrite(ref) @@ -183,7 +182,7 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "$", preserv } override def rewrite(e: Extern) = e match { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => { + case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body, vmBody) => { // 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( @@ -194,7 +193,8 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "$", preserv bparams map rewrite, rewrite(ret), rewrite(annotatedCapture), - rewrite(body) + rewrite(body), + vmBody ) } } @@ -272,6 +272,12 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "$", preserv core.ModuleDecl(path, includes, declarations map rewrite, externs map rewrite, definitions map rewrite, exports map rewrite) } + def apply(t: core.Toplevel): core.Toplevel = + suffix = 0 + scopes = List.empty + toplevelScope = Map.empty + rewrite(t) + def apply(s: Stmt): Stmt = { suffix = 0 toplevelScope = Map.empty @@ -286,7 +292,7 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "$", preserv 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.Def(id, _, _, _, _, _, _, _, _) => Some(id) case Extern.Include(_, _) => None case Extern.Data(id, _) => Some(id) } diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala index a45ab3e6e..5e5a2c7e2 100644 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala @@ -92,15 +92,31 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { val sym@ExternFunction(name, tps, _, _, ret, effects, capt, _, _) = f.symbol assert(effects.isEmpty) val cps = bps.map(b => b.symbol.capture) - val tBody = bodies match { - case source.ExternBody.StringExternBody(ff, body, span) :: Nil => + + val vmBody: Option[ExternBody.StringExternBody] = bodies.collectFirst { + case ext @ source.ExternBody.StringExternBody(ff, body, span) if ff.matches("vm", matchDefault = false) => + ExternBody.StringExternBody(ff, Template(body.strings, body.args.map(transformAsExpr))) + } + // TODO after changing vmBody: change `:: _` to `:: Nil` again to check uniqueness of extern resolution. + val targetBody = bodies match { + case source.ExternBody.StringExternBody(ff, body, span) :: _ => ExternBody.StringExternBody(ff, Template(body.strings, body.args.map(transformAsExpr))) - case source.ExternBody.Unsupported(err) :: Nil => + case source.ExternBody.Unsupported(err) :: _ => ExternBody.Unsupported(err) case _ => - Context.abort("Externs should be resolved and desugared before core.Transformer") + Context.abort(s"Externs should be resolved and desugared before core.Transformer") } - List(Extern.Def(sym, tps, cps.unspan, vps.unspan map transform, bps.unspan map transform, transform(ret), transform(capt), tBody)) + List(Extern.Def( + sym, + tps, + cps.unspan, + vps.unspan map transform, + bps.unspan map transform, + transform(ret), + transform(capt), + targetBody, + vmBody, + )) case e @ source.ExternInclude(ff, path, contents, _, doc, span) => List(Extern.Include(ff, contents.get)) diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 4a13d1445..8222ff43a 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -1,6 +1,7 @@ package effekt package core +import effekt.core.ExternBody.StringExternBody import effekt.source.FeatureFlag import effekt.util.{ Structural, Trampoline } import effekt.util.messages.INTERNAL_ERROR @@ -133,7 +134,19 @@ case class Property(id: Id, tpe: BlockType) extends Tree */ enum Extern extends Tree { case Data(id: Id, tparams: List[Id]) - case Def(id: Id, tparams: List[Id], cparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: ValueType, annotatedCapture: Captures, body: ExternBody) + case Def( + id: Id, + tparams: List[Id], + cparams: List[Id], + vparams: List[ValueParam], + bparams: List[BlockParam], + ret: ValueType, + annotatedCapture: Captures, + /* Extern body for the chosen compilation target */ + targetBody: ExternBody, + /* Extern body for the vm target, if any */ + vmBody: Option[StringExternBody] + ) case Include(featureFlag: FeatureFlag, contents: String) } sealed trait ExternBody extends Tree @@ -221,6 +234,13 @@ enum Block extends Tree { lazy val free: Free = typing.free def show: String = util.show(this) + + this match { + case Block.BlockVar(id, annotatedTpe, annotatedCapt) => () + case Block.BlockLit(tparams, cparams, vparams, bparams, body) => assert(cparams.size == bparams.size) + case Block.Unbox(pure) => () + case Block.New(impl) => () + } } export Block.* @@ -481,8 +501,9 @@ 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= rewriteStructurally(b) - def rewrite(e: Extern): Extern= rewriteStructurally(e) + def rewrite(b: ExternBody): ExternBody = rewriteStructurally(b) + def rewrite(e: Extern): Extern = rewriteStructurally(e) + def rewrite(s: StringExternBody): StringExternBody = s def rewrite(d: Declaration): Declaration = rewriteStructurally(d) def rewrite(c: Constructor): Constructor = rewriteStructurally(c) def rewrite(f: Field): Field = rewriteStructurally(f) @@ -706,9 +727,9 @@ object Tree { } def rewrite(e: Extern): Extern = e match { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => + case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body, vmBody) => Extern.Def(rewrite(id), tparams.map(rewrite), cparams.map(rewrite), vparams.map(rewrite), bparams.map(rewrite), - rewrite(ret), rewrite(annotatedCapture), rewrite(body).run()) + rewrite(ret), rewrite(annotatedCapture), rewrite(body).run(), vmBody) case Extern.Include(featureFlag, contents) => e case Extern.Data(id, tparams) => Extern.Data(rewrite(id), tparams.map(rewrite)) } diff --git a/effekt/shared/src/main/scala/effekt/core/Type.scala b/effekt/shared/src/main/scala/effekt/core/Type.scala index 64c331333..3befd602d 100644 --- a/effekt/shared/src/main/scala/effekt/core/Type.scala +++ b/effekt/shared/src/main/scala/effekt/core/Type.scala @@ -323,6 +323,9 @@ object Type { Type.instantiate(callee.tpe.asInstanceOf[core.BlockType.Function], targs, bargs.map(_.capt)).result } + def bindingType(callee: BlockVar, targs: List[ValueType], vargs: List[Expr], bargs: List[Block]): ValueType = + Type.instantiate(callee.tpe.asInstanceOf[core.BlockType.Function], targs, bargs.map(_.capt)).result + extension (block: Block) { def returnType: ValueType = block.functionType.result def functionType: BlockType.Function = block.tpe.asInstanceOf @@ -344,7 +347,7 @@ object Type { val boundBlocks = definitions.collect { case Toplevel.Def(id, block) => BlockParam(id, block.tpe, block.capt) } ++ externs.collect { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, capt, _) => + case Extern.Def(id, tparams, cparams, vparams, bparams, ret, capt, _, _) => BlockParam(id, BlockType.Function(tparams, cparams, vparams.map(_.tpe), bparams.map(_.tpe), ret), capt) } @@ -369,7 +372,7 @@ object Type { } externs.foreach { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => + case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body, _) => val splices = body match { case ExternBody.StringExternBody(featureFlag, contents) => contents.args case ExternBody.Unsupported(err) => Nil @@ -466,17 +469,18 @@ object Type { Typing(ValueType.Boxed(bTpe, annotatedCapture), Set.empty, bFree) } - def typecheck(block: Block): Typing[BlockType] = checking(block) { - case Block.BlockVar(id, annotatedTpe, annotatedCapt) => Typing(annotatedTpe, annotatedCapt, Free.block(id, annotatedTpe, annotatedCapt)) - case Block.Unbox(pure) => - val Typing(tpe, capt, free) = pure.typing - tpe match { - case ValueType.Boxed(tpe2, capt2) => Typing(tpe2, capt2, free) - case other => typeError(s"Expected a boxed type, but got: ${util.show(other)}") - } - case b : Block.BlockLit => typecheck(b) - case Block.New(impl) => impl.typing - } + def typecheck(block: Block): Typing[BlockType] = + checking(block) { + case Block.BlockVar(id, annotatedTpe, annotatedCapt) => Typing(annotatedTpe, annotatedCapt, Free.block(id, annotatedTpe, annotatedCapt)) + case Block.Unbox(pure) => + val Typing(tpe, capt, free) = pure.typing + tpe match { + case ValueType.Boxed(tpe2, capt2) => Typing(tpe2, capt2, free) + case other => typeError(s"Expected a boxed type, but got: ${util.show(other)}") + } + case b : Block.BlockLit => typecheck(b) + case Block.New(impl) => impl.typing + } def typecheck(blocklit: BlockLit): Typing[BlockType.Function] = checking(blocklit) { case BlockLit(tparams, cparams, vparams, bparams, body) => diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Inliner.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Inliner.scala new file mode 100644 index 000000000..aab9a98c2 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Inliner.scala @@ -0,0 +1,180 @@ +package effekt.core.optimizer + +import effekt.core.* +import effekt.core + +import scala.collection.mutable + +sealed trait InliningPolicy { + def apply(id: Id)(using Context): Boolean +} + +class Unique(maxInlineSize: Int) extends InliningPolicy { + override def apply(id: Id)(using ctx: Context): Boolean = + ctx.usages.get(id).contains(Usage.Once) && + !ctx.usages.get(id).contains(Usage.Recursive) && + ctx.blocks.get(id).exists(_.size <= maxInlineSize) +} + +class UniqueJumpSimple(maxInlineSize: Int) extends InliningPolicy { + def isSimple(s: Stmt): Boolean = s match { + case Stmt.Def(id, block, body) => isSimple(body) + case Stmt.Let(id, binding, body) => isSimple(body) + case Stmt.ImpureApp(id, callee, targs, vargs, bargs, body) => isSimple(body) + + case Stmt.Alloc(id, init, region, body) => true + case Stmt.Get(id, annotatedTpe, ref, annotatedCapt, body) => true + case Stmt.Put(ref, annotatedCapt, value, body) => true + case Stmt.Var(ref, init, capture, body) => true + + case Stmt.Return(expr) => true + case Stmt.App(callee, targs, vargs, bargs) => true + case Stmt.Invoke(callee, method, methodTpe, targs, vargs, bargs) => true + + case Stmt.Val(id, binding, body) => false + case Stmt.If(cond, thn, els) => false + case Stmt.Match(scrutinee, annotatedType, clauses, default) => false + case Stmt.Region(body) => false + case Stmt.Reset(body) => false + case Stmt.Shift(prompt, k, body) => false + case Stmt.Resume(k, body) => false + case Stmt.Hole(annotatedTpe, span) => false + } + override def apply(id: Id)(using ctx: Context): Boolean = { + val use = ctx.usages.get(id) + val block = ctx.blocks.get(id) + var doInline = !ctx.usages.get(id).contains(Usage.Recursive) + doInline &&= use.contains(Usage.Once) || (block.collect { + case Block.BlockLit(_, _, _, _, stmt) => isSimple(stmt) + case Block.New(_) => true + case Block.BlockVar(_, _, _) => true + }.getOrElse(false) && block.exists(_.size <= maxInlineSize)) + doInline + } +} + +case class Context( + blocks: Map[Id, Block], + exprs: Map[Id, Expr], + usages: mutable.Map[Id, Usage] +) { + def bind(id: Id, expr: Expr): Context = copy(exprs = exprs + (id -> expr)) + def bind(id: Id, block: Block): Context = copy(blocks = blocks + (id -> block)) +} + +object Context { + def empty(usages: Map[Id, Usage]): Context = Context(Map.empty, Map.empty, mutable.Map.from(usages)) +} + +class Inliner(shouldInline: InliningPolicy, usages: Map[Id, Usage]) extends Tree.RewriteWithContext[Context] { + + def run(mod: ModuleDecl): ModuleDecl = { + mod match { + case ModuleDecl(path, includes, declarations, externs, definitions, exports) => + var ctx = Context.empty(usages) + val d = definitions.map { + case Toplevel.Def(id, block) => + val b = rewrite(block)(using ctx) + ctx = ctx.bind(id, b) + Toplevel.Def(id, b) + case v@Toplevel.Val(id, binding) => v + } + ModuleDecl(path, includes, declarations, externs, d, exports) + } + } + + private def blockFor(id: Id)(using ctx: Context): Option[Block] = + ctx.blocks.get(id) + + private def exprFor(id: Id)(using ctx: Context): Option[Expr] = + ctx.exprs.get(id) + + def bindBlock(body: Stmt, bparam: BlockParam, barg: Block): Stmt = + Stmt.Def(bparam.id, barg, body) + + def bindBlocks(body: Stmt, blocks: List[(BlockParam, Block)]): Stmt = + blocks.foldRight(body) { case ((bp, barg), acc) => + bindBlock(acc, bp, barg) + } + + def bindValue(body: Stmt, vparam: ValueParam, varg: Expr): Stmt = + Stmt.Let(vparam.id, varg, body) + + def bindValues(body: Stmt, values: List[(ValueParam, Expr)]): Stmt = + values.foldRight(body) { case ((vp, varg), acc) => + bindValue(acc, vp, varg) + } + + def inlineApp(b: Block.BlockLit, targs: List[ValueType], vargs: List[Expr], bargs: List[Block])(using ctx: Context): Stmt = { + // (1) Rename definition's blocklit to keep IDs globally unique after inlining + val (renamedBlock @ Block.BlockLit(tparams, cparams, vparams, bparams, body), renamedIds) = Renamer.rename(b) + // (2) Copy usage information for renamed IDs + renamedIds.foreach { (from, to) => + ctx.usages.get(from).foreach { info => ctx.usages.update(to, info) } + } + // (3) We only need to bind block arguments that are _not_ block variables. Thus, separate block var args from the rest + val (bvars, other) = bparams.zip(bargs).partition { + case (_, _: Block.BlockVar) => true + case _ => false + } + // (4) Substitute. Only substitute block var args, other block args are bound before the inlinee's body + val substBody = substitutions.substitute(renamedBlock.body)(using substitutions.Substitution( + (tparams zip targs).toMap, + (cparams zip bargs.map(_.capt)).toMap, + (vparams.map(_.id) zip vargs).toMap, + bvars.map { (bp, bv) => bp.id -> bv }.toMap + )) + // (5) Bind all block arguments that are not block variables + bindBlocks(substBody, other) + } + + override def stmt(using ctx: Context): PartialFunction[Stmt, Stmt] = { + case app @ Stmt.App(v: BlockVar, targs, vargs, bargs) if shouldInline(v.id) => + val vas = vargs.map(rewrite) + val bas = bargs.map(rewrite) + blockFor(v.id).map { + case block: Block.BlockLit => + inlineApp(block, targs, vargs, bargs) + case b => + Stmt.App(b, targs, vargs, bargs) + }.getOrElse(Stmt.App(v, targs, vas, bas)) + case Stmt.Invoke(v: BlockVar, method, methodTpe, targs, vargs, bargs) if shouldInline(v.id) => + val vas = vargs.map(rewrite) + val bas = bargs.map(rewrite) + blockFor(v.id).collect { + case b@Block.New(Implementation(interface, operations)) => + val op = operations.find { op => op.name == method }.get + val b: Block.BlockLit = Block.BlockLit(op.tparams, op.cparams, op.vparams, op.bparams, op.body) + inlineApp(b, targs, vas, bas) + }.getOrElse(Stmt.Invoke(v, method, methodTpe, targs, vas, bas)) + case Stmt.Def(id, block, body) => + val b = rewrite(block)(using ctx) + given Context = ctx.bind(id, b) + Stmt.Def(id, b, rewrite(body)) + case Stmt.Let(id, binding, body) => + val expr = rewrite(binding)(using ctx) + given Context = ctx.bind(id, expr) + Stmt.Let(id, expr, rewrite(body)) + } + + override def expr(using Context): PartialFunction[Expr, Expr] = { + case v@Expr.ValueVar(id, tpe) if shouldInline(id) => + val e = exprFor(id) + //util.trace("inlining", id) + e match { + case Some(p: Expr.Make) => p + case Some(p: Expr.Literal) => p + case Some(p: Expr.Box) => p + case Some(other) if other.capt.isEmpty => other + case _ => v + } + } + + override def toplevel(using ctx: Context): PartialFunction[Toplevel, Toplevel] = { + case Toplevel.Def(id, block) => + given Context = ctx.bind(id, block) + Toplevel.Def(id, rewrite(block)) + case Toplevel.Val(id, binding) => + Toplevel.Val(id, rewrite(binding)) + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala index ac7749301..e238c65e5 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala @@ -2,7 +2,7 @@ package effekt package core package optimizer -import effekt.util.messages.INTERNAL_ERROR +import effekt.util.messages.{ ErrorReporter, INTERNAL_ERROR } import scala.annotation.{ tailrec, targetName } import scala.collection.mutable @@ -30,6 +30,30 @@ import scala.collection.mutable */ object Normalizer { normal => + def assertNormal(t: Tree)(using E: ErrorReporter): Unit = Tree.visit(t) { + // The only allowed forms are the following. + // In the future, Stmt.Shift should also be performed statically. + case Stmt.Val(_, binding: (Stmt.Reset | Stmt.Var | Stmt.App | Stmt.Invoke | Stmt.Region | Stmt.Shift | Stmt.Resume), body) => + assertNormal(binding); assertNormal(body) + /* + val x = if (...) { return 1 } else { return 2 }; s + is always normalized to + def joinpoint(x: Int) = s + if (...) { joinpoint(1) } else { joinpoint(2) } + */ + case t @ Stmt.Val(_, binding, body) => + E.warning(s"Not allowed as binding of Val: ${util.show(t)}") + case t @ Stmt.App(b: BlockLit, targs, vargs, bargs) => + E.warning(s"Unreduced beta-redex: ${util.show(t)}") + case t @ Stmt.Invoke(b: New, method, tpe, targs, vargs, bargs) => + E.warning(s"Unreduced beta-redex: ${util.show(t)}") + case t @ Stmt.If(cond: Literal, thn, els) => + E.warning(s"Unreduced if: ${util.show(t)}") + case t @ Stmt.Match(sc: Make, _, clauses, default) => + E.warning(s"Unreduced match: ${util.show(t)}") + } + + case class Context( blocks: Map[Id, Block], exprs: Map[Id, Expr], @@ -79,10 +103,11 @@ object Normalizer { normal => val context = Context(defs, Map.empty, DeclarationContext(m.declarations, m.externs), mutable.Map.from(usage), maxInlineSize, true) val (normalizedDefs, _) = normalizeToplevel(m.definitions)(using context) + m.copy(definitions = normalizedDefs) } - def normalizeToplevel(definitions: List[Toplevel])(using ctx: Context): (List[Toplevel], Context) = + private def normalizeToplevel(definitions: List[Toplevel])(using ctx: Context): (List[Toplevel], Context) = var contextSoFar = ctx val defs = definitions.map { case Toplevel.Def(id, block) => diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala index c72f3f765..6b77df971 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Optimizer.scala @@ -4,7 +4,8 @@ package optimizer import effekt.PhaseResult.CoreTransformed import effekt.context.Context - +import effekt.core.optimizer.Usage.{ Once, Recursive } +import effekt.core.optimizer.normalizer.NewNormalizer import kiama.util.Source object Optimizer extends Phase[CoreTransformed, CoreTransformed] { @@ -15,7 +16,12 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] { input match { case CoreTransformed(source, tree, mod, core) => val term = Context.ensureMainExists(mod) - val optimized = Context.timed("optimize", source.name) { optimize(source, term, core) } + val optimizer = if (Context.config.useNewNormalizer()) { + optimizeWithNewNormalizer + } else { + optimize + } + val optimized = Context.timed("optimize", source.name) { optimizer(source, term, core) } if Context.config.debug() then optimized.typecheck() Some(CoreTransformed(source, tree, mod, optimized)) } @@ -24,7 +30,7 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] { var tree = core - // (1) first thing we do is simply remove unused definitions (this speeds up all following analysis and rewrites) + // (1) first thing we do is simply remove unused definitions (this speeds up all following analysis and rewrites) tree = Context.timed("deadcode-elimination", source.name) { Deadcode.remove(mainSymbol, tree) } @@ -51,4 +57,37 @@ object Optimizer extends Phase[CoreTransformed, CoreTransformed] { tree = Context.timed("normalize-3", source.name) { normalize(tree) } tree + + def optimizeWithNewNormalizer(source: Source, mainSymbol: symbols.Symbol, core: ModuleDecl)(using Context): ModuleDecl = { + var tree = core + + // (1) first thing we do is simply remove unused definitions (this speeds up all following analysis and rewrites) + tree = Context.timed("deadcode-elimination", source.name) { + Deadcode.remove(mainSymbol, tree) + } + + if !Context.config.optimize() then return tree; + + val inliningPolicy = UniqueJumpSimple( + maxInlineSize = 15 + ) + + def normalize(m: ModuleDecl) = Context.timed("new-normalizer", source.name) { + val staticArgs = StaticArguments.transform(mainSymbol, m) + val normalized = NewNormalizer().run(staticArgs) + val reachability = Reachable(Set(mainSymbol), normalized) + val inlined = Inliner(inliningPolicy, reachability).run(normalized) + val live = Deadcode.remove(mainSymbol, inlined) + val tailRemoved = RemoveTailResumptions(live) + //val contified = DirectStyle.rewrite(tailRemoved) + tailRemoved + } + + tree = normalize(tree) + tree = normalize(tree) + tree = normalize(tree) + tree = normalize(tree) + //util.trace(tree) + tree + } } diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/NewNormalizer.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/NewNormalizer.scala new file mode 100644 index 000000000..e5f387d75 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/NewNormalizer.scala @@ -0,0 +1,833 @@ +package effekt +package core +package optimizer +package normalizer + +import effekt.core.ValueType.Boxed + +// TODO +// - change story of how inlining is implemented. We need to also support toplevel functions that potentially +// inline each other. Do we need to sort them topologically? How do we deal with (mutually) recursive definitions? +// +// +// plan: only introduce parameters for free things inside a block that are bound in the **stack** +// that is in +// +// only abstract over p, but not n: +// +// def outer(n: Int) = +// def foo(p) = shift(p) { ... n ... } +// reset { p => +// ... +// } +// +// Same actually for stack allocated mutable state, we should abstract over those (but only those) +// and keep the function in its original location. +// This means we only need to abstract over blocks, no values, no types. +// +// TODO Region desugaring +// region r { +// reset { p => +// var x in r = 42 +// x = !x + 1 +// println(!x) +// } +// } +// +// reset { r => +// reset { p => +// //var x in r = 42 +// shift(r) { k => +// var x = 42 +// resume(k) { +// x = !x + 1 +// println(!x) +// } +// } +// } +// } +// +// - Typeability preservation: {r: Region} becomes {r: Prompt[T]} +// [[ def f() {r: Region} = s ]] = def f[T]() {r: Prompt[T]} = ... +// - Continuation capture is _not_ constant time in JS backend, so we expect a (drastic) slowdown when desugaring + +/** + * A new normalizer that is conservative (avoids code bloat) + */ +class NewNormalizer { + + import semantics.* + + // used for potentially recursive definitions + def evaluateRecursive(id: Id, block: core.BlockLit, bound: List[Static])(using env: Env, scope: Scope): Computation = + block match { + case core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => + val freshened = Id(id) + + // we keep the params as they are for now... + given localEnv: Env = env + .bindValue(vparams.map { p => p.id -> p.id }) + .bindComputation(bparams.map(p => p.id -> Computation.Unknown(p.id))) + // Assume that we capture nothing + .bindComputation(id, Computation.Def(Closure(freshened, Nil))) + + val normalizedBlock = scope.local { + Block(tparams, vparams, bparams, nested { + evaluate(body, Frame.Return, Stack.Unknown)(using localEnv) + }) + } + + val dynamicCapt = normalizedBlock.dynamicCapture.toList + // HACK: This is a little subtle. + // We want to distinguish between static identifiers as written in the source program and + // identifiers for the corresponding dynamic references during normalization. + // Thereby, the `env` maps static ids to computations that contain the renamed dynamics ids. + // For this reason, we have to look up the dynamics id in the values of `env.computations` + // rather than in the keys. + // util.trace(env.computations.collect { + // case (id, Computation.Known(s)) => id.show ++ "->" ++ s.id.id.show + //}, env.computations.mkString(", ")) + val closureParams = dynamicCapt.map { c => + //util.trace(c.show) + env.computations.values.collectFirst { + case Computation.Known(bp) if bp.id.id == c => bp: Static + }.get + } + + // Only normalize again if we actually we wrong in our assumption that we capture nothing + // We might run into exponential complexity for nested recursive functions + if (closureParams.isEmpty) { + scope.defineRecursive(freshened, normalizedBlock.copy(bparams = normalizedBlock.bparams), block.tpe, block.capt) + Computation.Def(Closure(freshened, Nil)) + } else { + val captures = closureParams.map { bp => Computation.Known(bp): Computation.Known }.toList + given localEnv1: Env = env + .bindValue(vparams.map(p => p.id -> p.id)) + .bindComputation(bparams.map(p => p.id -> Computation.Unknown(p.id))) + .bindComputation(id, Computation.Def(Closure(freshened, captures))) + + val normalizedBlock1 = Block(tparams, vparams, bparams, nested { + evaluate(body, Frame.Return, Stack.Unknown)(using localEnv1) + }) + + val tpe: BlockType.Function = block.tpe match { + case _: BlockType.Interface => ??? + case ftpe: BlockType.Function => ftpe + } + + scope.defineRecursive( + freshened, + normalizedBlock1.copy(bparams = normalizedBlock1.bparams ++ closureParams.map(_.id)), + // here we update the argument types and captures to account for the closed over block parameters + tpe.copy( + bparams = tpe.bparams ++ closureParams.map { bp => bp.id.tpe }, + cparams = tpe.cparams ++ closureParams.map { bp => bp.id.id } + ), + block.capt + ) + Computation.Def(Closure(freshened, captures)) + } + + } + + // the stack here is not the one this is run in, but the one the definition potentially escapes + // Block, String, List[BlockParam], Env, Scope => Computation + def evaluate(block: core.Block, hint: String, bound: List[Static])(using env: Env, scope: Scope): Computation = block match { + case core.Block.BlockVar(id, annotatedTpe, annotatedCapt) => + env.lookupComputation(id) + case core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => + // we keep the params as they are for now... + given localEnv: Env = env + .bindValue(vparams.map(p => p.id -> p.id)) + .bindComputation(bparams.map(p => p.id -> Computation.Unknown(p.id))) + + val normalizedBlock = Block(tparams, vparams, bparams, nested { + evaluate(body, Frame.Return, Stack.Unknown) + }) + + val dynamicCapt = normalizedBlock.dynamicCapture.toList + // HACK: This is a little subtle. + // We want to distinguish between static identifiers as written in the source program and + // identifiers for the corresponding dynamic references during normalization. + // Thereby, the `env` maps static ids to computations that contain the renamed dynamics ids. + // For this reason, we have to look up the dynamics id in the values of `env.computations` + // rather than in the keys. + val closureParams = dynamicCapt.map { c => + env.computations.values.collectFirst { + case Computation.Known(bp) if bp.id.id == c => bp: Static + }.get + } + val f = Id(hint) + scope.define(f, normalizedBlock.copy(bparams = normalizedBlock.bparams ++ closureParams.map(_.id))) + Computation.Def(Closure(f, closureParams.map { bp => Computation.Known(bp) })) + + case core.Block.Unbox(pure) => + val addr = evaluate(pure, bound) + scope.lookupValue(addr) match { + case Some(Value.Box(body, _)) => body + case Some(_) | None => { + val (tpe, capt) = pure.tpe match { + case Boxed(tpe, capt) => (tpe, capt) + case _ => sys error "should not happen" + } + //util.trace(capt.map(_.show).mkString(", ")) + //env.captures.foreach { (k, v) => + // util.trace(k.show, v.map { + // case semantics.RuntimeCapture.Known(id) => id.id.id.show + // case semantics.RuntimeCapture.Unknown(id) => id.show + // }.mkString(", ")) + //} + // TODO translate static capture set capt to a dynamic capture set (e.g. {exc} -> {@p_17}) + val unboxAddr = scope.unbox(addr, tpe, capt.flatMap { env.lookupCapture }) + Computation.Unknown(unboxAddr) + } + } + + // TODO this does not work for recursive objects currently + case core.Block.New(Implementation(interface, operations)) => + val ops = operations.map { + case Operation(name, tparams, cparams, vparams, bparams, body) => + // Check whether the operation is already "just" an eta expansion and then use the identifier... + // no need to create a fresh block literal + val eta: Option[Closure] = + body match { + case Stmt.App(BlockVar(id, _, _), targs, vargs, bargs) => + def sameTargs = targs == tparams.map(t => ValueType.Var(t)) + def sameVargs = vargs == vparams.map(p => ValueVar(p.id, p.tpe)) + def sameBargs = bargs == bparams.map(p => BlockVar(p.id, p.tpe, p.capt)) + def isEta = sameTargs && sameVargs && sameBargs + + env.lookupComputation(id) match { + // TODO what to do with closure environment + case Computation.Def(closure) if isEta => Some(closure) + case _ => None + } + case _ => None + } + + val closure = eta.getOrElse { + evaluate(core.Block.BlockLit(tparams, cparams, vparams, bparams, body), name.name.name, bound) match { + case Computation.Def(closure) => closure + case _ => sys error "Should not happen" + } + } + (name, closure) + } + Computation.New(interface, ops) + } + + def evaluate(expr: Expr, bound: List[Static])(using env: Env, scope: Scope): Addr = expr match { + case Expr.ValueVar(id, annotatedType) => + env.lookupValue(id) + + case core.Expr.Literal(value, annotatedType) => value match { + case As.IntRep(x) => scope.allocate("x", Value.Integer(x)) + case As.StringRep(x) => scope.allocate("x", Value.String(x)) + case _ => scope.allocate("x", Value.Literal(value, annotatedType)) + } + + case core.Expr.PureApp(f, targs, vargs) => + val externDef = env.lookupComputation(f.id) + val vargsEvaluated = vargs.map(evaluate(_, bound)) + val valuesOpt: Option[List[semantics.Value]] = + vargsEvaluated.foldLeft(Option(List.empty[semantics.Value])) { (acc, addr) => + for { + xs <- acc + x <- scope.lookupValue(addr) + } yield x :: xs + }.map(_.reverse) + (valuesOpt, externDef) match { + case (Some(values), Computation.BuiltinExtern(id, name)) if supportedBuiltins(name).isDefinedAt(values) => + val impl = supportedBuiltins(name) + val res = impl(values) + scope.allocate("x", res) + case _ => scope.allocate("x", Value.PureExtern(f, targs, vargs.map(evaluate(_, bound)))) + } + + case core.Expr.Make(data, tag, targs, vargs) => + scope.allocate("x", Value.Make(data, tag, targs, vargs.map(evaluate(_, bound)))) + + case core.Expr.Box(b, annotatedCapture) => + /* + var counter = 22; + val p : Borrowed[Int] at counter = box new Borrowed[Int] { + def dereference() = counter + }; + counter = counter + 1; + println(p.dereference) + */ + // should capture `counter` but does not since the stack is Stack.Unknown + // (effekt.JavaScriptTests.examples/pos/capture/borrows.effekt (js)) + // TLDR we need to pass an escaping stack to do a proper escape analysis. Stack.Unkown is insufficient + val comp = evaluate(b, "boxed", bound) + scope.allocate("box", Value.Box(comp, annotatedCapture)) + } + + // TODO make evaluate(stmt) return BasicBlock (won't work for shift or reset, though) + def evaluate(stmt: Stmt, k: Frame, ks: Stack)(using env: Env, scope: Scope): NeutralStmt = stmt match { + + case Stmt.Return(expr) => + k.ret(ks, evaluate(expr, ks.bound)) + + case Stmt.Val(id, binding, body) => + evaluate(binding, k.push(body.tpe) { scope => res => k => ks => + given Scope = scope + bind(id, res) { evaluate(body, k, ks) } + }, ks) + + case Stmt.ImpureApp(id, f, targs, vargs, bargs, body) => + assert(bargs.isEmpty) + val addr = scope.run("x", f, targs, vargs.map(evaluate(_, ks.bound)), bargs.map(evaluate(_, "f", Stack.Unknown.bound))) + evaluate(body, k, ks)(using env.bindValue(id, addr), scope) + + case Stmt.Let(id, binding, body) => + bind(id, evaluate(binding, ks.bound)) { evaluate(body, k, ks) } + + // can be recursive + case Stmt.Def(id, block: core.BlockLit, body) => + bind(id, evaluateRecursive(id, block, ks.bound)) { evaluate(body, k, ks) } + + case Stmt.Def(id, block, body) => + bind(id, evaluate(block, id.name.name, ks.bound)) { evaluate(body, k, ks) } + + case Stmt.App(core.Block.BlockLit(tparams, cparams, vparams, bparams, body), targs, vargs, bargs) => + // TODO also bind type arguments in environment + // TODO substitute cparams??? + val newEnv = env + .bindValue(vparams.zip(vargs).map { case (p, a) => p.id -> evaluate(a, ks.bound) }) + .bindComputation(bparams.zip(bargs).map { case (p, a) => p.id -> evaluate(a, "f", ks.bound) }) + + evaluate(body, k, ks)(using newEnv, scope) + + case Stmt.App(callee, targs, vargs, bargs) => + evaluate(callee, "f", ks.bound) match { + case Computation.Unknown(id) => + reify(k, ks) { NeutralStmt.App(id, targs, vargs.map(evaluate(_, ks.bound)), bargs.map(evaluate(_, "f", ks.bound))) } + case Computation.Def(Closure(label, environment)) => + val args = vargs.map(evaluate(_, ks.bound)) + /* + try { + prog { + do Eff() + } + } with Eff { ... } + --- + val captures = stack.bound.filter { block.free } + is incorrect as the result is always the empty capture set since Stack.Unkown.bound = Set() + */ + val blockargs = bargs.map(evaluate(_, "f", ks.bound)) + // if stmt doesn't capture anything, it can not make any changes to the stack (ks) and we don't have to pretend it is unknown as an over-approximation + // compute dynamic captures of the whole statement (App node) + val dynCaptures = blockargs.flatMap(_.dynamicCapture) ++ environment + if (dynCaptures.isEmpty) { + reifyKnown(k, ks) { + NeutralStmt.Jump(label, targs, args, blockargs) + } + } else { + reify(k, ks) { + NeutralStmt.Jump(label, targs, args, blockargs ++ environment) + } + } + case _: (Computation.New | Computation.Known | Computation.Continuation | Computation.BuiltinExtern) => sys error "Should not happen" + } + + // case Stmt.Invoke(New) + + case Stmt.Invoke(callee, method, methodTpe, targs, vargs, bargs) => + val bound = Stack.Unknown.bound + evaluate(callee, "o", bound) match { + case Computation.Unknown(id) => + reify(k, ks) { NeutralStmt.Invoke(id, method, methodTpe, targs, vargs.map(evaluate(_, ks.bound)), bargs.map(evaluate(_, "f", bound))) } + case Computation.New(interface, operations) => + operations.collectFirst { case (id, Closure(label, environment)) if id == method => + reify(k, ks) { NeutralStmt.Jump(label, targs, vargs.map(evaluate(_, ks.bound)), bargs.map(evaluate(_, "f", bound)) ++ environment) } + }.get + case _: (Computation.Def | Computation.Known | Computation.Continuation | Computation.BuiltinExtern) => sys error s"Should not happen" + } + + case Stmt.If(cond, thn, els) => + val sc = evaluate(cond, ks.bound) + scope.lookupValue(sc) match { + case Some(Value.Literal(true, _)) => evaluate(thn, k, ks) + case Some(Value.Literal(false, _)) => evaluate(els, k, ks) + case _ => + // joinpoint(k, ks, List(thn, els)) { (A, Frame, Stack) => (NeutralStmt, Variables) } { case thn1 :: els1 :: Nil => NeutralStmt.If(sc, thn1, els1) } + joinpoint(k, ks) { (k, ks) => + NeutralStmt.If(sc, nested { + evaluate(thn, k, ks) + }, nested { + evaluate(els, k, ks) + }) + } + } + case Stmt.Match(scrutinee, annotatedType, clauses, default) => + val sc = evaluate(scrutinee, ks.bound) + scope.lookupValue(sc) match { + case Some(Value.Make(data, tag, targs, vargs)) => + // TODO substitute types (or bind them in the env)! + clauses.collectFirst { + case (tpe, core.Block.BlockLit(tparams, cparams, vparams, bparams, body)) if tpe == tag => + bind(vparams.map(_.id).zip(vargs)) { evaluate(body, k, ks) } + }.getOrElse { + evaluate(default.getOrElse { sys.error("Non-exhaustive pattern match.") }, k, ks) + } + // case _ if (clauses.size + default.size) <= 1 => + // NeutralStmt.Match(sc, + // clauses.map { case (id, BlockLit(tparams, cparams, vparams, bparams, body)) => + // given localEnv: Env = env.bindValue(vparams.map(p => p.id -> p.id)) + // val block = Block(tparams, vparams, bparams, nested { + // evaluate(body, k, ks) + // }) + // (id, block) + // }, + // default.map { stmt => nested { evaluate(stmt, k, ks) } }) + case _ => + def neutralMatch(k: Frame, ks: Stack) = + NeutralStmt.Match(sc, + annotatedType, + // This is ALMOST like evaluate(BlockLit), but keeps the current continuation + clauses.map { case (id, core.Block.BlockLit(tparams, cparams, vparams, bparams, body)) => + given localEnv: Env = env.bindValue(vparams.map(p => p.id -> p.id)) + val block = Block(tparams, vparams, bparams, nested { scope ?=> + // here we now know that our scrutinee sc has the shape id(vparams, ...) + + val datatype = scrutinee.tpe match { + case tpe @ ValueType.Data(name, targs) => tpe + case tpe => sys error s"Should not happen: pattern matching on a non-datatype: ${tpe}" + } + val eta = Value.Make(datatype, id, tparams.map(t => ValueType.Var(t)), vparams.map(p => p.id)) + scope.bindings = scope.bindings.updated(sc, Binding.Let(eta)) + evaluate(body, k, ks) + }) + (id, block) + }, + default.map { stmt => nested { evaluate(stmt, k, ks) } }) + // linear usage of the continuation: do not create a joinpoint. + // This is a simple optimization for record access since r.x is always desugared into a match + if (default.size + clauses.size > 1) { + joinpoint(k, ks) { (k, ks) => neutralMatch(k, ks) } + } else { + neutralMatch(k, ks) + } + } + + case Stmt.Hole(tpe, span) => NeutralStmt.Hole(tpe, span) + + // State + case Stmt.Region(BlockLit(Nil, List(capture), Nil, List(cap), body)) => + val reg = Id(cap.id) + val bp = BlockParam(reg, cap.tpe, cap.capt) + val captures1 = cap.capt + bind(cap.id, Computation.Known(Static.Region(bp))) { + evaluate(body, Frame.Return, Stack.Region(bp, Map.empty, k, ks)) + } + case Stmt.Region(_) => ??? + case Stmt.Alloc(ref, init, region, body) => + val ref1 = Id(ref) + val region1 = env.subst(region) + val addr = evaluate(init, ks.bound) + val bp = BlockParam(ref1, Type.TState(init.tpe), Set(region1)) + bind(ref, Computation.Known(Static.Reference(bp))) { + alloc(bp, region1, addr, ks) match { + case Some(ks1) => evaluate(body, k, ks1) + case None => NeutralStmt.Alloc(bp, addr, region1, nested { + evaluate(body, k, ks) + }) + } + } + + case Stmt.Var(ref, init, capture, body) => + val label = Id(ref) + val runtimeCaptureRegion = label + val addr = evaluate(init, ks.bound)(using env) + val bp = BlockParam(label, Type.TState(init.tpe), Set(runtimeCaptureRegion)) + given env1: Env = env.bindComputation(ref, Computation.Known(Static.Reference(bp))) + .bindCapture(capture, RuntimeCapture.Known(Static.Reference(bp))) + val env2 = env1 + evaluate(body, Frame.Return, Stack.Var(bp, addr, k, ks)) + case Stmt.Get(id, annotatedTpe, ref, annotatedCapt, body) => + val ref1 = env.subst(ref) + val capt = annotatedCapt.flatMap(env.lookupCapture).collect { + case RuntimeCapture.Known(s) => s.id.id + } + get(ref1, ks) match { + case Some(addr) => bind(id, addr) { evaluate(body, k, ks) } + case None => bind(id, scope.allocateGet(ref1, annotatedTpe, capt)) { evaluate(body, k, ks) } + } + case Stmt.Put(ref, annotatedCapt, value, body) => + val addr = evaluate(value, ks.bound) + val ref1 = env.subst(ref) + val capt = annotatedCapt.flatMap(env.lookupCapture).collect { + case RuntimeCapture.Known(s) => s.id.id + } + put(ref1, addr, ks) match { + case Some(stack) => evaluate(body, k, stack) + case None => + NeutralStmt.Put(ref1, value.tpe, capt, addr, nested { evaluate(body, k, ks) }) + } + + // Control Effects + case Stmt.Shift(prompt, k2, body) => + val p = env.subst(prompt.id) + + if (ks.bound.exists { other => other.id == p }) { + val (cont, frame, stack) = shift(p, k, ks) + given Env = env.bindComputation(k2.id -> Computation.Continuation(cont) :: Nil) + evaluate(body, frame, stack) + } else { + val neutralBody = { + given Env = env.bindComputation(k2.id -> Computation.Unknown(k2.id) :: Nil) + nested { + evaluate(body, Frame.Return, Stack.Unknown) + } + } + reify(k, ks) { NeutralStmt.Shift(p, k2, neutralBody) } + } + case Stmt.Reset(core.Block.BlockLit(Nil, cparams, Nil, prompt :: Nil, body)) => + val p = Id(prompt.id) + val captures = p + val bp = BlockParam(p, prompt.tpe, Set(captures)) + given Env = env.bindComputation(prompt.id, Computation.Known(Static.Prompt(bp))) + // only ever one capture assumed here + .bindCapture(prompt.capt.head, RuntimeCapture.Known(Static.Prompt(bp))) + evaluate(body, Frame.Return, Stack.Reset(bp, k, ks)) + + case Stmt.Reset(_) => ??? + case Stmt.Resume(k2, body) => + env.lookupComputation(k2.id) match { + case Computation.Unknown(r) => + reify(k, ks) { + NeutralStmt.Resume(r, nested { + evaluate(body, Frame.Return, Stack.Unknown) + }) + } + case Computation.Continuation(k3) => + val (k4, ks4) = resume(k3, k, ks) + evaluate(body, k4, ks4) + case _ => ??? + } + } + + def run(mod: ModuleDecl): ModuleDecl = { + //util.trace(mod) + // TODO deal with async externs properly (see examples/benchmarks/input_output/dyck_one.effekt) + val externTypes = mod.externs.collect { case d: Extern.Def => + d.id -> (BlockType.Function(d.tparams, d.cparams, d.vparams.map { _.tpe }, d.bparams.map { bp => bp.tpe }, d.ret), d.annotatedCapture) + } + + val (builtinExterns, otherExterns) = mod.externs.collect { case d: Extern.Def => d }.partition { + case Extern.Def(id, tps, cps, vps, bps, ret, capt, targetBody, Some(vmBody)) => + val builtinName = vmBody.contents.strings.head + supportedBuiltins.contains(builtinName) + case _ => false + } + + val builtinNameToBlockVar: Map[String, BlockVar] = builtinExterns.collect { + case Extern.Def(id, tps, cps, vps, bps, ret, capt, targetBody, Some(vmBody)) => + val builtinName = vmBody.contents.strings.head + val bv: BlockVar = BlockVar(id, BlockType.Function(tps, cps, vps.map { _.tpe }, bps.map { bp => bp.tpe }, ret), capt) + builtinName -> bv + }.toMap + + val toplevelEnv = Env.empty + // user-defined functions + .bindComputation(mod.definitions.collect { + case Toplevel.Def(id, b) => id -> (b match { + case core.Block.BlockLit(tparams, cparams, vparams, bparams, body) => Computation.Def(Closure(id, Nil)) + case core.Block.BlockVar(idd, annotatedTpe, annotatedCapt) => Computation.Unknown(id) + case core.Block.Unbox(pure) => Computation.Unknown(id) + case core.Block.New(impl) => Computation.Unknown(id) + }) + }) + // user-defined values + .bindValue(mod.definitions.collect { + case Toplevel.Val(id, _) => id -> id + }) + // async extern functions + .bindComputation(otherExterns.map(defn => defn.id -> Computation.Unknown(defn.id))) + // pure extern functions + .bindComputation(builtinExterns.flatMap(defn => defn.vmBody.map(vmBody => defn.id -> Computation.BuiltinExtern(defn.id, vmBody.contents.strings.head)))) + + val typingContext = TypingContext( + mod.definitions.collect { + case Toplevel.Val(id, binding) => id -> binding.tpe + }.toMap, + mod.definitions.collect { + case Toplevel.Def(id, b) => id -> (b.tpe, b.capt) + }.toMap ++ externTypes, + builtinNameToBlockVar + ) + + val newDefinitions = mod.definitions.map(d => run(d)(using toplevelEnv, typingContext)) + mod.copy(definitions = newDefinitions) + } + + val showDebugInfo = true + inline def debug(inline msg: => Any) = if (showDebugInfo) println(msg) else () + + def run(defn: Toplevel)(using env: Env, G: TypingContext): Toplevel = defn match { + case Toplevel.Def(id, core.Block.BlockLit(tparams, cparams, vparams, bparams, body)) => + debug(s"------- ${util.show(id)} -------") + debug(util.show(body)) + + val scope = Scope.empty + val localEnv: Env = env + .bindValue(vparams.map(p => p.id -> scope.allocate("p", Value.Var(p.id, p.tpe)))) + .bindComputation(bparams.map(p => p.id -> Computation.Unknown(p.id))) + + val result = evaluate(body, Frame.Return, Stack.Empty)(using localEnv, scope) + + debug(s"----------normalized-----------") + val block = Block(tparams, vparams, bparams, reifyBindings(scope, result)) + debug(PrettyPrinter.show(block)) + + debug(s"----------embedded-----------") + val embedded = embedBlockLit(block) + debug(util.show(embedded)) + + Toplevel.Def(id, embedded) + case other => other + } + + case class TypingContext(values: Map[Addr, ValueType], blocks: Map[Label, (BlockType, Captures)], builtinBlockVars: Map[String, BlockVar]) { + def bind(id: Id, tpe: ValueType): TypingContext = this.copy(values = values + (id -> tpe)) + def bind(id: Id, tpe: BlockType, capt: Captures): TypingContext = this.copy(blocks = blocks + (id -> (tpe, capt))) + def bindValues(vparams: List[ValueParam]): TypingContext = this.copy(values = values ++ vparams.map(p => p.id -> p.tpe)) + def lookupValue(id: Id): ValueType = values.getOrElse(id, sys.error(s"Unknown value: ${util.show(id)}")) + def bindComputations(bparams: List[BlockParam]): TypingContext = this.copy(blocks = blocks ++ bparams.map(p => p.id -> (p.tpe, p.capt))) + def bindComputation(bparam: BlockParam): TypingContext = this.copy(blocks = blocks + (bparam.id -> (bparam.tpe, bparam.capt))) + } + + def embedStmt(neutral: NeutralStmt)(using G: TypingContext): core.Stmt = neutral match { + case NeutralStmt.Return(result) => + Stmt.Return(embedExpr(result)) + case NeutralStmt.Jump(label, targs, vargs, bargs) => + Stmt.App(embedBlockVar(label), targs, vargs.map(embedExpr), bargs.map(embedBlock)) + case NeutralStmt.App(label, targs, vargs, bargs) => + Stmt.App(embedBlockVar(label), targs, vargs.map(embedExpr), bargs.map(embedBlock)) + case NeutralStmt.Invoke(label, method, tpe, targs, vargs, bargs) => + Stmt.Invoke(embedBlockVar(label), method, tpe, targs, vargs.map(embedExpr), bargs.map(embedBlock)) + case NeutralStmt.If(cond, thn, els) => + Stmt.If(embedExpr(cond), embedStmt(thn), embedStmt(els)) + case NeutralStmt.Match(scrutinee, tpe, clauses, default) => + Stmt.Match(embedExpr(scrutinee), + tpe, + clauses.map { case (id, block) => id -> embedBlockLit(block) }, + default.map(embedStmt)) + case NeutralStmt.Reset(prompt, body) => + val capture = prompt.capt match { + case set if set.size == 1 => set.head + case _ => sys error "Prompt needs to have a single capture" + } + Stmt.Reset(core.BlockLit(Nil, capture :: Nil, Nil, prompt :: Nil, embedStmt(body)(using G.bindComputation(prompt)))) + case NeutralStmt.Shift(prompt, k, body) => + Stmt.Shift(embedBlockVar(prompt), k, embedStmt(body)(using G.bindComputation(k))) + case NeutralStmt.Resume(k, body) => + Stmt.Resume(embedBlockVar(k), embedStmt(body)) + case NeutralStmt.Var(blockParam, init, body) => + val capt = blockParam.capt match { + case cs if cs.size == 1 => cs.head + case _ => sys error "Variable needs to have a single capture" + } + Stmt.Var(blockParam.id, embedExpr(init), capt, embedStmt(body)(using G.bind(blockParam.id, blockParam.tpe, blockParam.capt))) + case NeutralStmt.Put(ref, annotatedTpe, annotatedCapt, value, body) => + Stmt.Put(ref, annotatedCapt, embedExpr(value), embedStmt(body)) + case NeutralStmt.Region(id, body) => + Stmt.Region(BlockLit(Nil, List(id.id), Nil, List(id), embedStmt(body)(using G.bindComputation(id)))) + case NeutralStmt.Alloc(blockparam, init, region, body) => + Stmt.Alloc(blockparam.id, embedExpr(init), region, embedStmt(body)(using G.bind(blockparam.id, blockparam.tpe, blockparam.capt))) + case NeutralStmt.Hole(tpe, span) => + Stmt.Hole(tpe, span) + } + + def embedStmt(basicBlock: BasicBlock)(using G: TypingContext): core.Stmt = basicBlock match { + case BasicBlock(bindings, stmt) => + bindings.foldRight((G: TypingContext) => embedStmt(stmt)(using G)) { + case ((id, Binding.Let(value)), rest) => G => + val coreExpr = embedExpr(value)(using G) + Stmt.Let(id,coreExpr, rest(G.bind(id, coreExpr.tpe))) + case ((id, Binding.Def(block)), rest) => G => + val coreBlock = embedBlock(block)(using G) + Stmt.Def(id, coreBlock, rest(G.bind(id, coreBlock.tpe, coreBlock.capt))) + case ((id, Binding.Rec(block, tpe, capt)), rest) => G => + val coreBlock = embedBlock(block)(using G.bind(id, tpe, capt)) + Stmt.Def(id, coreBlock, rest(G.bind(id, coreBlock.tpe, coreBlock.capt))) + case ((id, Binding.Val(stmt)), rest) => G => + val coreStmt = embedStmt(stmt)(using G) + Stmt.Val(id, coreStmt, rest(G.bind(id, coreStmt.tpe))) + case ((id, Binding.Run(callee, targs, vargs, bargs)), rest) => G => + val vargs1 = vargs.map(arg => embedExpr(arg)(using G)) + val bargs1 = bargs.map(arg => embedBlock(arg)(using G)) + val tpe = Type.bindingType(callee, targs, vargs1, bargs1) + core.ImpureApp(id, callee, targs, vargs1, bargs1, rest(G.bind(id, tpe))) + case ((id, Binding.Unbox(addr, tpe, capt)), rest) => G => + val pureValue = embedExpr(addr)(using G) + val c = capt.collect { + case RuntimeCapture.Unknown(id) => id + } + Stmt.Def(id, core.Block.Unbox(pureValue), rest(G.bind(id, tpe, c))) + case ((id, Binding.Get(ref, tpe, cap)), rest) => G => + Stmt.Get(id, tpe, ref, cap, rest(G.bind(id, tpe)) ) + }(G) + } + + def embedExpr(value: Value)(using cx: TypingContext): core.Expr = value match { + case Value.PureExtern(callee, targs, vargs) => Expr.PureApp(callee, targs, vargs.map(embedExpr)) + case Value.Literal(value, annotatedType) => Expr.Literal(value, annotatedType) + case Value.Make(data, tag, targs, vargs) => Expr.Make(data, tag, targs, vargs.map(embedExpr)) + case Value.Box(body, annotatedCapture) => Expr.Box(embedBlock(body), annotatedCapture) + case Value.Var(id, annotatedType) => Expr.ValueVar(id, annotatedType) + case Value.Integer(value) => theories.integers.reify(value, cx.builtinBlockVars, embedNeutral) + case Value.String(value) => theories.strings.reify(value, cx.builtinBlockVars, embedNeutral) + } + + def embedNeutral(neutral: Neutral)(using G: TypingContext): core.Expr = neutral match { + case Value.Var(id, annotatedType) => Expr.ValueVar(id, annotatedType) + case Value.PureExtern(callee, targs, vargs) => Expr.PureApp(callee, targs, vargs.map(embedExpr)) + } + + def embedExpr(addr: Addr)(using G: TypingContext): core.Expr = Expr.ValueVar(addr, G.lookupValue(addr)) + + def embedBlock(comp: Computation)(using G: TypingContext): core.Block = comp match { + case Computation.Unknown(id) => + embedBlockVar(id) + case Computation.Def(Closure(label, Nil)) => + embedBlockVar(label) + case Computation.Def(closure) => + etaExpandToBlockLit(closure) + case Computation.Known(id) => + embedBlockVar(id.id.id) + case Computation.Continuation(k) => ??? + case Computation.BuiltinExtern(_, _) => ??? + case Computation.New(interface, operations) => + val ops = operations.map { etaExpandToOperation.tupled } + core.Block.New(Implementation(interface, ops)) + } + + /** + * Embed `Computation.Def` to a `core.BlockLit` + * This eta-expands the block var that stands for the `Computation.Def` to a full block literal + * so that we can supply the correct capture arguments from the environment. + */ + def etaExpandToBlockLit(closure: Closure)(using G: TypingContext): core.BlockLit = { + val Closure(label, environment) = closure + val blockvar = embedBlockVar(label) + G.blocks(label) match { + // TODO why is `captures` unused? + case (BlockType.Function(tparams, cparams, vparams, bparams, result), captures) => + val vps = vparams.map { p => core.ValueParam(Id("x"), p) } + val vargs = vps.map { vp => core.Expr.ValueVar(vp.id, vp.tpe) } + + // this uses the invariant that we _append_ all environment captures to the bparams + val (origCapts, synthCapts) = cparams.splitAt(bparams.length - environment.length) + val (origBparams, synthBparams) = bparams.splitAt(bparams.length - environment.length) + val origBps = origBparams.zip(origCapts).map { case (bp, c) => core.BlockParam(Id("f"), bp, Set(c)) } + val origBargs = origBps.map { bp => core.BlockVar(bp.id, bp.tpe, bp.capt) } + val synthBargs = environment.zip(synthBparams).zip(synthCapts).map { + case ((Computation.Known(s), bp), c) => core.BlockVar(s.id.id, bp, Set(c)) + } + val bargs = origBargs ++ synthBargs + + val targs = tparams.map { core.ValueType.Var.apply } + + core.Block.BlockLit( + tparams, + origCapts, + vps, + origBps, + Stmt.App( + blockvar, + targs, + vargs, + bargs + ) + ) + case _ => sys.error("Unexpected block type for a closure") + } + } + + /** + * Embed an operation as part of a `Computation.New`. + * This eta-expands the block var that stands for the operation body to a full operation + * so that we can supply the correct capture arguments from the environment. + */ + def etaExpandToOperation(id: Id, closure: Closure)(using G: TypingContext): core.Operation = { + val Closure(label, environment) = closure + G.blocks(label) match { + case (BlockType.Function(tparams, cparams, vparams, bparams, result), captures) => + val tparams2 = tparams.map(t => Id(t)) + // TODO if we freshen cparams, then we also need to substitute them in the result AND the parameters + val cparams2 = cparams //.map(c => Id(c)) + val vparams2 = vparams.map(t => ValueParam(Id("x"), t)) + val bparams2 = (bparams zip cparams).map { case (t, c) => BlockParam(Id("f"), t, Set(c)) } + // In the following section, we create a new instance of the interface. + // All operation bodies were lifted to block literals in an earlier stage. + // While doing so, their block parameters (bparams) were concatenated with their capture parameters (cparams). + // When we embed back to core, we need to "eta-expand" the operation body to supply the correct captures from the environment. + // To see why this "eta-expansion" is necessary to achieve this, consider the following example: + // ```scala + // effect Eff(): Unit + // def use = { do Eff() } + // def main() = { + // val r = try { + // use() + // } with Eff { + // resume(()) + // } + // } + // ``` + // the handler body normalizes to the following: + // ```scala + // reset {{p} => + // jump use(){new Eff {def Eff = Eff @ [p]}} + // } + // ``` + // where + // ``` + // def Eff = (){p} { ... } + // ``` + // In particular, the prompt `p` needs to be passed to the lifted operation body. + // ``` + val (origBparams, synthBparams) = bparams2.splitAt(bparams2.length - environment.length) + val bargs = + // TODO: Fix captures + origBparams.map { case bp => BlockVar(bp.id, bp.tpe, Set()) } ++ + synthBparams.zip(environment).map { + // TODO: Fix captures + case (bp, Computation.Known(s)) => BlockVar(s.id.id, bp.tpe, Set()) + } + + core.Operation( + id, + tparams2, + cparams.take(cparams.length - environment.length), + vparams2, + origBparams, + Stmt.App( + embedBlockVar(label), + tparams2.map(ValueType.Var.apply), + vparams2.map(p => ValueVar(p.id, p.tpe)), + bargs + ) + ) + case _ => sys error "Unexpected block type" + } + } + + def embedBlock(block: Block)(using G: TypingContext): core.Block = block match { + case Block(tparams, vparams, bparams, b) => + val cparams = bparams.map { + case BlockParam(id, tpe, captures) => + assert(captures.size == 1) + captures.head + } + core.Block.BlockLit(tparams, cparams, vparams, bparams, + embedStmt(b)(using G.bindValues(vparams).bindComputations(bparams))) + } + + def embedBlockLit(block: Block)(using G: TypingContext): core.BlockLit = embedBlock(block).asInstanceOf[core.BlockLit] + + def embedBlockVar(label: Label)(using G: TypingContext): core.BlockVar = + val (tpe, capt) = G.blocks.getOrElse(label, sys error s"Unknown block: ${util.show(label)}. ${G.blocks.keys.map(util.show).mkString(", ")}") + core.BlockVar(label, tpe, capt) +} diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/builtins.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/builtins.scala new file mode 100644 index 000000000..bd83a3f48 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/builtins.scala @@ -0,0 +1,300 @@ +package effekt +package core +package optimizer +package normalizer + +type ~>[-A, +B] = PartialFunction[A, B] + +type BuiltinImpl = List[semantics.Value] ~> semantics.Value + +def builtin(name: String)(impl: List[semantics.Value] ~> semantics.Value): (String, BuiltinImpl) = name -> impl + +type Builtins = Map[String, BuiltinImpl] + +given Conversion[Long, semantics.Value] with + def apply(n: Long): semantics.Value = + semantics.Value.Integer(theories.integers.embed(n)) + +given Conversion[Boolean, semantics.Value] with + def apply(b: Boolean): semantics.Value = + semantics.Value.Literal(b, Type.TBoolean) + +given Conversion[String, semantics.Value] with + def apply(s: String): semantics.Value = + semantics.Value.Literal(s, Type.TString) + +given Conversion[Double, semantics.Value] with + def apply(d: Double): semantics.Value = + semantics.Value.Literal(d, Type.TDouble) + +lazy val supportedBuiltins: Builtins = integers ++ doubles ++ booleans ++ strings ++ chars + +lazy val integers: Builtins = Map( + // Integer arithmetic operations with symbolic simplification support + // ---------- + builtin("effekt::infixPlus(Int, Int)") { + case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.add(x, y)) + }, + builtin("effekt::InfixMinus(Int, Int)") { + case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.sub(x, y)) + }, + builtin("effekt::infixStar(Int, Int)") { + case As.IntRep(x) :: As.IntRep(y) :: Nil => semantics.Value.Integer(theories.integers.mul(x, y)) + }, + builtin("effekt::infixEq(Int, Int)") { + case As.IntRep(x) :: As.IntRep(y) :: Nil if x == y => true + case As.Int(x) :: As.Int(y) :: Nil => x == y + }, + // Integer arithmetic operations only evaluated for literals + // ---------- + builtin("effekt::infixDiv(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil if y != 0 => x / y + }, + builtin("effekt::mod(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil if y != 0 => x % y + }, + builtin("effekt::bitwiseShl(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x << y + }, + builtin("effekt::bitwiseShr(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x >> y + }, + builtin("effekt::bitwiseAnd(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x & y + }, + builtin("effekt::bitwiseOr(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x | y + }, + builtin("effekt::bitwiseXor(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x ^ y + }, + // Comparison + // ---------- + builtin("effekt::infixNeq(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x != y + }, + builtin("effekt::infixLt(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x < y + }, + builtin("effekt::infixGt(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x > y + }, + builtin("effekt::infixLte(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x <= y + }, + builtin("effekt::infixGte(Int, Int)") { + case As.Int(x) :: As.Int(y) :: Nil => x >= y + }, + // Conversion + // ---------- + builtin("effekt::toDouble(Int)") { + case As.Int(x) :: Nil => x.toDouble + }, + + builtin("effekt::show(Int)") { + case As.Int(n) :: Nil => n.toString + }, +) + +lazy val doubles: Builtins = Map( + // Arithmetic + // ---------- + builtin("effekt::infixPlus(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x + y + }, + builtin("effekt::InfixMinus(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x - y + }, + builtin("effekt::infixStar(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x * y + }, + builtin("effekt::infixDiv(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x / y + }, + builtin("effekt::sqrt(Double)") { + case As.Double(x) :: Nil => Math.sqrt(x) + }, + builtin("effekt::exp(Double)") { + case As.Double(x) :: Nil => Math.exp(x) + }, + builtin("effekt::log(Double)") { + case As.Double(x) :: Nil => Math.log(x) + }, + builtin("effekt::cos(Double)") { + case As.Double(x) :: Nil => Math.cos(x) + }, + builtin("effekt::sin(Double)") { + case As.Double(x) :: Nil => Math.sin(x) + }, + builtin("effekt::tan(Double)") { + case As.Double(x) :: Nil => Math.tan(x) + }, + builtin("effekt::atan(Double)") { + case As.Double(x) :: Nil => Math.atan(x) + }, + builtin("effekt::round(Double)") { + case As.Double(x) :: Nil => Math.round(x) + }, + builtin("effekt::pow(Double, Double)") { + case As.Double(base) :: As.Double(exp) :: Nil => Math.pow(base, exp) + }, + builtin("effekt::pi()") { + case Nil => Math.PI + }, + // Comparison + // ---------- + builtin("effekt::infixEq(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x == y + }, + builtin("effekt::infixNeq(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x != y + }, + builtin("effekt::infixLt(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x < y + }, + builtin("effekt::infixGt(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x > y + }, + builtin("effekt::infixLte(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x <= y + }, + builtin("effekt::infixGte(Double, Double)") { + case As.Double(x) :: As.Double(y) :: Nil => x >= y + }, + + // Conversion + // ---------- + builtin("effekt::toInt(Double)") { + case As.Double(x) :: Nil => x.toLong + }, + builtin("effekt::show(Double)") { + // TODO globally define show on double in a decent way... this mimicks JS + case As.Double(n) :: Nil => + if (n == n.toInt.toDouble) n.toInt.toString // Handle integers like 15.0 → 15 + else { + val formatted = BigDecimal(n) + .setScale(15, BigDecimal.RoundingMode.DOWN) // Truncate to 15 decimal places without rounding up + .bigDecimal + .stripTrailingZeros() + .toPlainString + + formatted + } + }, +) + +lazy val booleans: Builtins = Map( + builtin("effekt::not(Bool)") { + case As.Bool(x) :: Nil => !x + }, +) + +lazy val strings: Builtins = Map( + builtin("effekt::infixPlusPlus(String, String)") { + case As.StringRep(x) :: As.StringRep(y) :: Nil => semantics.Value.String(theories.strings.concat(x, y)) + }, + + builtin("effekt::infixEq(String, String)") { + case As.StringRep(x) :: As.StringRep(y) :: Nil if x == y => true + case As.String(x) :: As.String(y) :: Nil => x == y + }, + + builtin("effekt::length(String)") { + case As.String(x) :: Nil => x.length.toLong + }, + + builtin("effekt::substring(String, Int, Int)") { + case As.String(x) :: As.Int(from) :: As.Int(to) :: Nil => x.substring(from.toInt, to.toInt) + }, + + builtin("string::unsafeCharAt(String, Int)") { + case As.String(x) :: As.Int(at) :: Nil => x.charAt(at.toInt).toLong + }, + + builtin("string::toInt(Char)") { + case As.Int(n) :: Nil => n + }, + + builtin("string::toChar(Int)") { + case As.Int(n) :: Nil => n + }, + + builtin("string::infixLte(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => x <= y + }, + + builtin("string::infixLt(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => x < y + }, + + builtin("string::infixGt(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => x > y + }, + + builtin("string::infixGte(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => x >= y + }, +) + +lazy val chars: Builtins = Map( + builtin("effekt::infixEq(Char, Char)") { + case As.Int(x) :: As.Int(y) :: Nil => x == y + }, +) + +protected object As { + object Int { + def unapply(v: semantics.Value): Option[scala.Long] = v match { + case semantics.Value.Literal(value: scala.Long, _) => Some(value) + case semantics.Value.Literal(value: scala.Int, _) => Some(value.toLong) + case semantics.Value.Literal(value: java.lang.Integer, _) => Some(value.toLong) + case semantics.Value.Integer(value) if value.isLiteral => Some(value.value) + case _ => None + } + } + + object IntRep { + def unapply(v: semantics.Value): Option[theories.integers.IntegerRep] = v match { + // Integer literals not yet embedded into the theory of integers + case semantics.Value.Literal(value: scala.Long, _) => Some(theories.integers.embed(value)) + case semantics.Value.Literal(value: scala.Int, _) => Some(theories.integers.embed(value.toLong)) + case semantics.Value.Literal(value: java.lang.Integer, _) => Some(theories.integers.embed(value.toLong)) + // Neutrals (e.g. variables or extern calls) + case n: semantics.Neutral => Some(theories.integers.embed(n)) + // Already embedded integers + case semantics.Value.Integer(value) => Some(value) + case _ => None + } + } + + object Double { + def unapply(v: semantics.Value): Option[scala.Double] = v match { + case semantics.Value.Literal(value: scala.Double, _) => Some(value) + case _ => None + } + } + + object String { + def unapply(v: semantics.Value): Option[java.lang.String] = v match { + case semantics.Value.Literal(value: java.lang.String, _) => Some(value) + case semantics.Value.String(value) if value.isLiteral => Some(value.value.head.asInstanceOf[java.lang.String]) + case _ => None + } + } + + object StringRep { + def unapply(v: semantics.Value): Option[theories.strings.StringRep] = v match { + case semantics.Value.Literal(value: java.lang.String, _) => Some(theories.strings.embed(value)) + case n: semantics.Neutral => Some(theories.strings.embed(n)) + case semantics.Value.String(value) => Some(value) + case _ => None + } + } + + object Bool { + def unapply(v: semantics.Value): Option[scala.Boolean] = v match { + case semantics.Value.Literal(value: scala.Boolean, _) => Some(value) + case _ => None + } + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/semantics.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/semantics.scala new file mode 100644 index 000000000..6693e6b8a --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/semantics.scala @@ -0,0 +1,802 @@ +package effekt +package core +package optimizer +package normalizer + +import effekt.core.optimizer.normalizer.semantics.PrettyPrinter.{braces, brackets, comma, emptyDoc, hcat, hsep, line, nest, parens, pretty} +import effekt.core.{BlockVar, Capture, Captures, Id} +import effekt.source.Span +import kiama.output.ParenPrettyPrinter + +import scala.annotation.tailrec +import scala.collection.immutable.ListMap + +object semantics { + + // Values + // ------ + + type Addr = Id + type Label = Id + type Prompt = Id + + // this could not only compute free variables, but also usage information to guide the inliner (see "secrets of the ghc inliner") + type Variables = Set[Id] + def all[A](ts: List[A], f: A => Variables): Variables = ts.flatMap(f).toSet + + type Neutral = Value.Var | Value.PureExtern + + enum Value { + // Stuck (neutrals) + case Var(id: Id, annotatedType: ValueType) + case PureExtern(f: BlockVar, targs: List[ValueType], vargs: List[Addr]) + + // Values with specialized representation for algebraic simplification + case Integer(value: theories.integers.IntegerRep) + case String(value: theories.strings.StringRep) + + // Fallback literal for other values types without special representation + case Literal(value: Any, annotatedType: ValueType) + + case Make(data: ValueType.Data, tag: Id, targs: List[ValueType], vargs: List[Addr]) + + // TODO use dynamic captures + case Box(body: Computation, annotatedCaptures: Set[effekt.symbols.Symbol]) + + val dynamicCapture: Variables = Set.empty + + val free: Variables = this match { + case Value.Var(id, annotatedType) => Set(id) + case Value.PureExtern(id, targs, vargs) => vargs.toSet + case Value.Literal(value, annotatedType) => Set.empty + case Value.Integer(value) => value.free + case Value.String(value) => value.free + case Value.Make(data, tag, targs, vargs) => vargs.toSet + // Box abstracts over all free computation variables, only when unboxing, they occur free again + case Value.Box(body, tpe) => body.free + } + } + + // TODO find better name for this + enum Binding { + case Let(value: Value) + case Def(block: Block) + case Rec(block: Block, tpe: BlockType, capt: Captures) + case Val(stmt: NeutralStmt) + case Run(f: BlockVar, targs: List[ValueType], vargs: List[Addr], bargs: List[Computation]) + case Unbox(addr: Addr, tpe: BlockType, capt: Set[RuntimeCapture]) + case Get(ref: Id, tpe: ValueType, cap: Captures) + + val free: Variables = this match { + case Binding.Let(value) => value.free + case Binding.Def(block) => block.free + case Binding.Rec(block, tpe, capt) => block.free + case Binding.Val(stmt) => stmt.free + // TODO block args for externs are not supported (for now?) + case Binding.Run(f, targs, vargs, bargs) => vargs.toSet + case Binding.Unbox(addr, tpe: BlockType, capt) => Set(addr) + case Binding.Get(ref, tpe, cap) => Set(ref) + } + + val dynamicCapture: Variables = this match { + case Binding.Let(value) => value.dynamicCapture + case Binding.Def(block) => block.dynamicCapture + case Binding.Rec(block, tpe, capt) => block.dynamicCapture + case Binding.Val(stmt) => stmt.dynamicCapture + // TODO block args for externs are not supported (for now?) + case Binding.Run(f, targs, vargs, bargs) => Set.empty + case Binding.Unbox(addr, tpe: BlockType, capt) => + capt.collect { + case RuntimeCapture.Known(id) => id.id.id + } + case Binding.Get(ref, tpe, cap) => Set(ref) + } + } + + type Bindings = List[(Id, Binding)] + object Bindings { + def empty: Bindings = Nil + } + + /** + * A Scope is a bit like a basic block, but without the terminator + */ + class Scope( + var bindings: ListMap[Id, Binding], + var inverse: Map[Value, Addr], + val outer: Option[Scope] + ) { + // Backtrack the internal state of Scope after running `prog` + def local[A](prog: => A): A = { + val scopeBefore = Scope(this.bindings, this.inverse, this.outer) + val res = prog + this.bindings = scopeBefore.bindings + this.inverse = scopeBefore.inverse + res + } + + // floating values to the top is not always beneficial. For example + // def foo() = COMPUTATION + // vs + // let x = COMPUTATION + // def foo() = x + def getDefinition(value: Value): Option[Addr] = + inverse.get(value) orElse outer.flatMap(_.getDefinition(value)) + + def allocate(hint: String, value: Value): Addr = + getDefinition(value) match { + case Some(value) => value + case None => + val addr = Id(hint) + bindings = bindings.updated(addr, Binding.Let(value)) + inverse = inverse.updated(value, addr) + addr + } + + def allocateGet(ref: Id, tpe: ValueType, cap: Captures): Addr = { + val addr = Id("get") + bindings = bindings.updated(addr, Binding.Get(ref, tpe, cap)) + addr + } + + def run(hint: String, callee: BlockVar, targs: List[ValueType], vargs: List[Addr], bargs: List[Computation]): Addr = { + val addr = Id(hint) + bindings = bindings.updated(addr, Binding.Run(callee, targs, vargs, bargs)) + addr + } + + def unbox(innerAddr: Addr, tpe: BlockType, capt: Set[RuntimeCapture]): Addr = { + val unboxAddr = Id("unbox") + bindings = bindings.updated(unboxAddr, Binding.Unbox(innerAddr, tpe, capt)) + unboxAddr + } + + // TODO Option[Value] or Var(id) in Value? + def lookupValue(addr: Addr): Option[Value] = bindings.get(addr) match { + case Some(Binding.Let(value)) => Some(value) + case _ => outer.flatMap(_.lookupValue(addr)) + } + + def define(label: Label, block: Block): Unit = + bindings = bindings.updated(label, Binding.Def(block)) + + def defineRecursive(label: Label, block: Block, tpe: BlockType, capt: Captures): Unit = + bindings = bindings.updated(label, Binding.Rec(block, tpe, capt)) + + def push(id: Id, stmt: NeutralStmt): Unit = + bindings = bindings.updated(id, Binding.Val(stmt)) + } + object Scope { + def empty: Scope = new Scope(ListMap.empty, Map.empty, None) + } + + def reifyBindings(scope: Scope, body: NeutralStmt): BasicBlock = { + var used = body.free + var filtered = Bindings.empty + // TODO implement properly + scope.bindings.toSeq.reverse.foreach { + // TODO for now we keep ALL definitions + case (addr, b: Binding.Def) => + used = used ++ b.free + filtered = (addr, b) :: filtered + case (addr, b: Binding.Rec) => + used = used ++ b.free + filtered = (addr, b) :: filtered + case (addr, s: Binding.Val) => + used = used ++ s.free + filtered = (addr, s) :: filtered + case (addr, v: Binding.Run) => + used = used ++ v.free + filtered = (addr, v) :: filtered + + // TODO if type is unit like, we can potentially drop this binding (but then we need to make up a "fresh" unit at use site) + case (addr, v: Binding.Let) if used.contains(addr) => + used = used ++ v.free + filtered = (addr, v) :: filtered + case (addr, v: Binding.Let) => () + case (addr, b: Binding.Unbox) => + used = used ++ b.free + filtered = (addr, b):: filtered + case (addr, g: Binding.Get) => + used = used ++ g.free + filtered = (addr, g) :: filtered + } + + // we want to avoid turning tailcalls into non tail calls like + // + // val x = app(x) + // return x + // + // so we eta-reduce here. Can we achieve this by construction? + // TODO lastOption will go through the list AGAIN, let's see whether this causes performance problems + (filtered.lastOption, body) match { + case (Some((id1, Binding.Val(stmt))), NeutralStmt.Return(id2)) if id1 == id2 => + BasicBlock(filtered.init, stmt) + case (_, _) => + BasicBlock(filtered, body) + } + } + + def nested(prog: Scope ?=> NeutralStmt)(using scope: Scope): BasicBlock = { + // TODO parent code and parent store + val local = Scope(ListMap.empty, Map.empty, Some(scope)) + val result = prog(using local) + reifyBindings(local, result) + } + + enum RuntimeCapture { + case Known(id: Static) + case Unknown(id: Id) + } + + /** + * Environment during normalization + * + * @param values + * @param computations + * @param captures: Mapping of static to (dyanmic) runtime captures. Here, it can happen that a single static capture + * maps to multiple runtime captures. For example: + * ``` + * def foo{f: () => Unit}: () => Unit at {f} = box f + * var x = 1; + * var y = 2; + * foo { => println(x + y) } () + * ``` + * Here we need to substitute the static capture f with runtime captures corresponding to {x, y}. + */ + case class Env(values: Map[Id, Addr], computations: Map[Id, Computation], captures: Map[Id, Set[RuntimeCapture]]) { + def lookupValue(id: Id): Addr = values(id) + def bindValue(id: Id, value: Addr): Env = Env(values + (id -> value), computations, captures) + def bindValue(newValues: List[(Id, Addr)]): Env = Env(values ++ newValues, computations, captures) + + def bindCapture(id: Id, c: RuntimeCapture): Env = Env(values, computations, captures + (id -> Set(c))) + def bindCapture(cs: Set[(Id, Set[RuntimeCapture])]): Env = Env(values, computations, captures ++ cs.toMap) + def lookupCapture(id: Id): Set[RuntimeCapture] = captures.get(id).getOrElse(Set(RuntimeCapture.Unknown(id))) + + def lookupComputation(id: Id): Computation = computations.getOrElse(id, sys error s"Unknown computation: ${util.show(id)} -- env: ${computations.map { case (id, comp) => s"${util.show(id)}: $comp" }.mkString("\n") }") + def bindComputation(id: Id, computation: Computation): Env = Env(values, computations + (id -> computation), captures) + def bindComputation(newComputations: List[(Id, Computation)]): Env = Env(values, computations ++ newComputations, captures) + def subst(ids: List[Id]): List[Id] = ids.map(subst) + def subst(id: Id): Id = computations.get(id) match { + case Some(Computation.Known(inner)) => inner.id.id + case Some(Computation.Unknown(id)) => id + case _ => id + } + } + object Env { + def empty: Env = Env(Map.empty, Map.empty, Map.empty) + } + // "handlers" + def bind[R](id: Id, addr: Addr)(prog: Env ?=> R)(using env: Env): R = + prog(using env.bindValue(id, addr)) + + def bind[R](id: Id, computation: Computation)(prog: Env ?=> R)(using env: Env): R = + prog(using env.bindComputation(id, computation)) + + def bind[R](values: List[(Id, Addr)])(prog: Env ?=> R)(using env: Env): R = + prog(using env.bindValue(values)) + + case class Block(tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], body: BasicBlock) { + val free: Variables = body.free -- vparams.map(_.id) -- bparams.map(_.id) + val dynamicCapture: Variables = body.dynamicCapture -- bparams.map(_.id) + } + + case class BasicBlock(bindings: Bindings, body: NeutralStmt) { + val free: Variables = { + var free = body.free + bindings.reverse.foreach { + case (id, b: Binding.Let) => free = (free - id) ++ b.free + case (id, b: Binding.Def) => free = (free - id) ++ b.free + case (id, b: Binding.Rec) => free = (free - id) ++ (b.free - id) + case (id, b: Binding.Val) => free = (free - id) ++ b.free + case (id, b: Binding.Run) => free = (free - id) ++ b.free + case (id, b: Binding.Unbox) => free = (free - id) ++ b.free + case (id, b: Binding.Get) => free = (free - id) ++ b.free + } + free + } + + val dynamicCapture: Variables = { + body.dynamicCapture ++ bindings.flatMap(_._2.dynamicCapture) + } + } + + enum Computation { + // Unknown identifiers -- stuck + case Unknown(id: Id) + // Known function + case Def(closure: Closure) + + // known identifiers introduced by reset, var and region + case Known(inner: Static) + + case Continuation(k: Cont) + + case BuiltinExtern(id: Id, builtinName: String) + + // Known object + case New(interface: BlockType.Interface, operations: List[(Id, Closure)]) + + val free: Variables = this match { + case Computation.Unknown(id) => Set(id) + case Computation.Known(inner) => Set(inner.id.id) + case Computation.Def(closure) => closure.free + case Computation.Continuation(k) => Set.empty // TODO ??? + case Computation.New(interface, operations) => operations.flatMap(_._2.free).toSet + case Computation.BuiltinExtern(id, vmSymbol) => Set.empty + } + + val dynamicCapture: Variables = this match { + case Computation.Unknown(id) => Set(id) + case Computation.Known(inner) => Set(inner.id.id) + case Computation.Def(closure) => closure.dynamicCapture + case Computation.Continuation(k) => Set.empty // TODO ??? + case Computation.New(interface, operations) => operations.flatMap(_._2.dynamicCapture).toSet + case Computation.BuiltinExtern(id, vmSymbol) => Set.empty + } + } + + enum Static { + case Prompt(id: BlockParam) + case Reference(id: BlockParam) + case Region(id: BlockParam) + val id: BlockParam + } + + // TODO add escaping mutable variables + case class Closure(label: Label, environment: List[Computation.Known]) { + val free: Variables = Set(label) ++ environment.flatMap(_.free).toSet + val dynamicCapture: Variables = environment.map(_.inner.id.id).toSet + } + + // Statements + // ---------- + enum NeutralStmt { + // context (continuation) is unknown + case Return(result: Id) + // callee is unknown + case App(callee: Id, targs: List[ValueType], vargs: List[Id], bargs: List[Computation]) + // Known jump, but we do not want to inline + case Jump(label: Id, targs: List[ValueType], vargs: List[Id], bargs: List[Computation]) + // callee is unknown + case Invoke(id: Id, method: Id, methodTpe: BlockType, targs: List[ValueType], vargs: List[Id], bargs: List[Computation]) + // cond is unknown + case If(cond: Id, thn: BasicBlock, els: BasicBlock) + // scrutinee is unknown + case Match(scrutinee: Id, tpe: ValueType, clauses: List[(Id, Block)], default: Option[BasicBlock]) + + // body is stuck + case Reset(prompt: BlockParam, body: BasicBlock) + // prompt / context is unknown + case Shift(prompt: Prompt, k: BlockParam, body: BasicBlock) + // continuation is unknown + case Resume(k: Id, body: BasicBlock) + + case Var(id: BlockParam, init: Addr, body: BasicBlock) + case Put(ref: Id, tpe: ValueType, cap: Captures, value: Addr, body: BasicBlock) + + case Region(id: BlockParam, body: BasicBlock) + case Alloc(id: BlockParam, init: Addr, region: Id, body: BasicBlock) + + // aborts at runtime + case Hole(tpe: ValueType, span: Span) + + val free: Variables = this match { + case NeutralStmt.Jump(label, targs, vargs, bargs) => Set(label) ++ vargs.toSet ++ all(bargs, _.free) + case NeutralStmt.App(label, targs, vargs, bargs) => Set(label) ++ vargs.toSet ++ all(bargs, _.free) + case NeutralStmt.Invoke(label, method, tpe, targs, vargs, bargs) => Set(label) ++ vargs.toSet ++ all(bargs, _.free) + case NeutralStmt.If(cond, thn, els) => Set(cond) ++ thn.free ++ els.free + case NeutralStmt.Match(scrutinee, tpe, clauses, default) => Set(scrutinee) ++ clauses.flatMap(_._2.free).toSet ++ default.map(_.free).getOrElse(Set.empty) + case NeutralStmt.Return(result) => Set(result) + case NeutralStmt.Reset(prompt, body) => body.free - prompt.id + case NeutralStmt.Shift(prompt, k, body) => (body.free - k.id) + prompt + case NeutralStmt.Resume(k, body) => Set(k) ++ body.free + case NeutralStmt.Var(id, init, body) => Set(init) ++ body.free - id.id + case NeutralStmt.Put(ref, tpe, cap, value, body) => Set(ref, value) ++ body.free + case NeutralStmt.Region(id, body) => body.free - id.id + case NeutralStmt.Alloc(id, init, region, body) => Set(init, region) ++ body.free - id.id + case NeutralStmt.Hole(tpe, span) => Set.empty + } + + val dynamicCapture: Variables = this match { + case NeutralStmt.Return(result) => Set.empty + case NeutralStmt.Hole(tpe, span) => Set.empty + + case NeutralStmt.Jump(label, targs, vargs, bargs) => all(bargs, _.dynamicCapture) + case NeutralStmt.App(label, targs, vargs, bargs) => all(bargs, _.dynamicCapture) + case NeutralStmt.Invoke(label, method, tpe, targs, vargs, bargs) => all(bargs, _.dynamicCapture) + case NeutralStmt.If(cond, thn, els) => thn.dynamicCapture ++ els.dynamicCapture + case NeutralStmt.Match(scrutinee, tpe, clauses, default) => clauses.flatMap(_._2.dynamicCapture).toSet ++ default.map(_.dynamicCapture).getOrElse(Set.empty) + case NeutralStmt.Reset(prompt, body) => body.dynamicCapture - prompt.id + case NeutralStmt.Shift(prompt, k, body) => body.dynamicCapture + prompt + case NeutralStmt.Resume(k, body) => body.dynamicCapture + case NeutralStmt.Var(id, init, body) => body.dynamicCapture - id.id + case NeutralStmt.Put(ref, tpe, cap, value, body) => Set(ref) ++ body.dynamicCapture + case NeutralStmt.Region(id, body) => body.dynamicCapture - id.id + case NeutralStmt.Alloc(id, init, region, body) => Set(region) ++ body.dynamicCapture - id.id + } + } + + // Stacks + // ------ + enum Frame { + case Return + case Static(tpe: ValueType, apply: Scope => Addr => Stack => NeutralStmt) + case Dynamic(closure: Closure) + + /* Return an argument `arg` through this frame and the rest of the stack `ks` + */ + def ret(ks: Stack, arg: Addr)(using scope: Scope): NeutralStmt = this match { + case Frame.Return => ks match { + case Stack.Empty => NeutralStmt.Return(arg) + case Stack.Unknown => NeutralStmt.Return(arg) + case Stack.Reset(p, k, ks) => k.ret(ks, arg) + case Stack.Var(id, curr, k, ks) => k.ret(ks, arg) + case Stack.Region(id, bindings, k, ks) => k.ret(ks, arg) + } + case Frame.Static(tpe, apply) => apply(scope)(arg)(ks) + case Frame.Dynamic(Closure(label, environment)) => reify(ks) { NeutralStmt.Jump(label, Nil, List(arg), environment) } + } + + // pushing purposefully does not abstract over env (it closes over it!) + def push(tpe: ValueType)(f: Scope => Addr => Frame => Stack => NeutralStmt): Frame = + Frame.Static(tpe, scope => arg => ks => f(scope)(arg)(this)(ks)) + } + + // maybe, for once it is simpler to decompose stacks like + // + // f, (p, f) :: (p, f) :: Nil + // + // where the frame on the reset is the one AFTER the prompt NOT BEFORE! + enum Stack { + /** + * Statically known to be empty + * This only occurs at the entrypoint of normalization. + * In other cases, where the stack is not known, you should use Unknown instead. + */ + case Empty + /** Dynamic tail (we do not know the shape of the remaining stack) + */ + case Unknown + case Reset(prompt: BlockParam, frame: Frame, next: Stack) + case Var(id: BlockParam, curr: Addr, frame: Frame, next: Stack) + // TODO desugar regions into var? + case Region(id: BlockParam, bindings: Map[BlockParam, Addr], frame: Frame, next: Stack) + + lazy val bound: List[Static] = this match { + case Stack.Empty => List.empty + case Stack.Unknown => List.empty + case Stack.Reset(prompt, frame, next) => Static.Prompt(prompt) :: next.bound + case Stack.Var(id, curr, frame, next) => Static.Reference(id) :: next.bound + case Stack.Region(id, bindings, frame, next) => Static.Region(id) :: (bindings.keys.map { k => Static.Reference(k) }.toList ++ next.bound) + } + } + + @tailrec + def get(ref: Id, ks: Stack): Option[Addr] = ks match { + case Stack.Empty => sys error s"Should not happen: trying to lookup ${util.show(ref)} in empty stack" + // We have reached the end of the known stack, so the variable must be in the unknown part. + case Stack.Unknown => None + case Stack.Reset(prompt, frame, next) => get(ref, next) + case Stack.Var(id1, curr, frame, next) if ref == id1.id => Some(curr) + case Stack.Var(id1, curr, frame, next) => get(ref, next) + case Stack.Region(id, bindings, frame, next) => + val containsRef = bindings.keys.find(bp => bp.id == ref) + containsRef match { + case Some(bparam) => Some(bindings(bparam)) + case None => get(ref, next) + } + } + + def put(ref: Id, value: Addr, ks: Stack): Option[Stack] = ks match { + case Stack.Empty => sys error s"Should not happen: trying to put ${util.show(ref)} in empty stack" + // We have reached the end of the known stack, so the variable must be in the unknown part. + case Stack.Unknown => None + case Stack.Reset(prompt, frame, next) => put(ref, value, next).map(Stack.Reset(prompt, frame, _)) + case Stack.Var(id, curr, frame, next) if ref == id.id => Some(Stack.Var(id, value, frame, next)) + case Stack.Var(id, curr, frame, next) => put(ref, value, next).map(Stack.Var(id, curr, frame, _)) + case Stack.Region(id, bindings, frame, next) => + val containsRef = bindings.keys.find(bp => bp.id == ref) + containsRef match { + case Some(bparam) => Some(Stack.Region(id, bindings.updated(bparam, value), frame, next)) + case None => put(ref, value, next).map(Stack.Region(id, bindings, frame, _)) + } + } + + def alloc(ref: BlockParam, reg: Id, value: Addr, ks: Stack): Option[Stack] = ks match { + // This case can occur if we normalize a function that abstracts over a region as a parameter + // We return None and force the reification of the allocation + case Stack.Empty => None + // We have reached the end of the known stack, so the variable must be in the unknown part. + case Stack.Unknown => None + case Stack.Reset(prompt, frame, next) => + alloc(ref, reg, value, next).map(Stack.Reset(prompt, frame, _)) + case Stack.Var(id, curr, frame, next) => + alloc(ref, reg, value, next).map(Stack.Var(id, curr, frame, _)) + case Stack.Region(id, bindings, frame, next) => + if (reg == id.id){ + Some(Stack.Region(id, bindings.updated(ref, value), frame, next)) + } else { + alloc(ref, reg, value, next).map(Stack.Region(id, bindings, frame, _)) + } + } + + enum Cont { + case Empty + case Reset(frame: Frame, prompt: BlockParam, rest: Cont) + case Var(frame: Frame, id: BlockParam, curr: Addr, rest: Cont) + case Region(frame: Frame, id: BlockParam, bindings: Map[BlockParam, Addr], rest: Cont) + } + + def shift(p: Id, k: Frame, ks: Stack): (Cont, Frame, Stack) = ks match { + case Stack.Empty => sys error s"Should not happen: cannot find prompt ${util.show(p)}" + case Stack.Unknown => sys error s"Cannot find prompt ${util.show(p)} in unknown stack" + case Stack.Reset(prompt, frame, next) if prompt.id == p => + (Cont.Reset(k, prompt, Cont.Empty), frame, next) + case Stack.Reset(prompt, frame, next) => + val (c, frame2, stack) = shift(p, frame, next) + (Cont.Reset(k, prompt, c), frame2, stack) + case Stack.Var(id, curr, frame, next) => + val (c, frame2, stack) = shift(p, frame, next) + (Cont.Var(k, id, curr, c), frame2, stack) + case Stack.Region(id, bindings, frame, next) => + val (c, frame2, stack) = shift(p, frame, next) + (Cont.Region(k, id, bindings, c), frame2, stack) + } + + def resume(c: Cont, k: Frame, ks: Stack): (Frame, Stack) = c match { + case Cont.Empty => + (k, ks) + case Cont.Reset(frame, prompt, rest) => + val (k1, ks1) = resume(rest, k, ks) + (frame, Stack.Reset(prompt, k1, ks1)) + case Cont.Var(frame, id, curr, rest) => + val (k1, ks1) = resume(rest, k, ks) + (frame, Stack.Var(id, curr, k1, ks1)) + case Cont.Region(frame, id, bindings, rest) => + val (k1, ks1) = resume(rest, k, ks) + (frame, Stack.Region(id, bindings, k1, ks1)) + } + + def joinpoint(k: Frame, ks: Stack)(f: (Frame, Stack) => NeutralStmt)(using scope: Scope): NeutralStmt = { + def reifyFrame(k: Frame, escaping: Stack)(using scope: Scope): Frame = k match { + case Frame.Static(tpe, apply) => + val x = Id("x") + nested { scope ?=> apply(scope)(x)(Stack.Unknown) } match { + // Avoid trivial continuations like + // def k_6268 = (x_6267: Int_3) { + // return x_6267 + // } + case BasicBlock(Nil, _: (NeutralStmt.Return | NeutralStmt.App | NeutralStmt.Jump)) => + k + case body => + val k = Id("k") + val closureParams = escaping.bound.collect { case bp if body.dynamicCapture contains bp.id.id => bp }.toList + scope.define(k, Block(Nil, ValueParam(x, tpe) :: Nil, closureParams.map(_.id), body)) + Frame.Dynamic(Closure(k, closureParams.map { bp => Computation.Known(bp) })) + } + case Frame.Return => k + case Frame.Dynamic(label) => k + } + + def reifyStack(ks: Stack): Stack = ks match { + case Stack.Empty => Stack.Empty + case Stack.Unknown => Stack.Unknown + case Stack.Reset(prompt, frame, next) => + Stack.Reset(prompt, reifyFrame(frame, next), reifyStack(next)) + case Stack.Var(id, curr, frame, next) => + Stack.Var(id, curr, reifyFrame(frame, next), reifyStack(next)) + case Stack.Region(id, bindings, frame, next) => + Stack.Region(id, bindings, reifyFrame(frame, next), reifyStack(next)) + } + f(reifyFrame(k, ks), reifyStack(ks)) + } + + def reify(k: Frame, ks: Stack)(stmt: Scope ?=> NeutralStmt)(using Scope): NeutralStmt = + reify(ks) { reify(k) { stmt } } + + def reify(k: Frame)(stmt: Scope ?=> NeutralStmt)(using scope: Scope): NeutralStmt = + k match { + case Frame.Return => stmt + case Frame.Static(tpe, apply) => + val tmp = Id("tmp") + scope.push(tmp, stmt) + // TODO Over-approximation + // Don't pass Stack.Unknown but rather the stack until the next reset? + /* + |----------| |----------| |---------| + | | ---> ... ---> | | ---> ... ---> | | ---> ... + |----------| |----------| |---------| + r1 r2 first next prompt + + Pass r1 :: ... :: r2 :: ... :: prompt :: UNKNOWN + */ + apply(scope)(tmp)(Stack.Unknown) + case Frame.Dynamic(Closure(label, closure)) => + val tmp = Id("tmp") + scope.push(tmp, stmt) + NeutralStmt.Jump(label, Nil, List(tmp), closure) + } + + def reifyKnown(k: Frame, ks: Stack)(stmt: Scope ?=> NeutralStmt)(using scope: Scope): NeutralStmt = + k match { + case Frame.Return => reify(ks) { stmt } + case Frame.Static(tpe, apply) => + val tmp = Id("tmp") + scope.push(tmp, stmt) + apply(scope)(tmp)(ks) + case Frame.Dynamic(Closure(label, closure)) => reify(ks) { sc ?=> + val tmp = Id("tmp") + sc.push(tmp, stmt) + NeutralStmt.Jump(label, Nil, List(tmp), closure) + } + } + + @tailrec + final def reify(ks: Stack)(stmt: Scope ?=> NeutralStmt)(using scope: Scope): NeutralStmt = { + ks match { + case Stack.Empty => stmt + case Stack.Unknown => stmt + case Stack.Reset(prompt, frame, next) => + reify(next) { reify(frame) { + val body = nested { stmt } + if (body.dynamicCapture contains prompt.id) NeutralStmt.Reset(prompt, body) + else stmt // TODO this runs normalization a second time in the outer scope! + }} + case Stack.Var(id, curr, frame, next) => + reify(next) { reify(frame) { + val body = nested { stmt } + if (body.dynamicCapture contains id.id) NeutralStmt.Var(id, curr, body) + else stmt + }} + case Stack.Region(id, bindings, frame, next) => + reify(next) { reify(frame) { + val body = nested { stmt } + val bodyUsesBinding = body.dynamicCapture.exists(bindings.map { b => b._1.id }.toSet.contains(_)) + if (body.dynamicCapture.contains(id.id) || bodyUsesBinding) { + // we need to reify all bindings in this region as allocs using their current value + val reifiedAllocs = bindings.foldLeft(body) { case (acc, (bp, addr)) => + nested { NeutralStmt.Alloc(bp, addr, id.id, acc) } + } + NeutralStmt.Region(id, reifiedAllocs) + } + else stmt + }} + } + } + + object PrettyPrinter extends ParenPrettyPrinter { + + override val defaultIndent = 2 + + def toDoc(s: NeutralStmt): Doc = s match { + case NeutralStmt.Return(result) => + "return" <+> toDoc(result) + case NeutralStmt.If(cond, thn, els) => + "if" <+> parens(toDoc(cond)) <+> toDoc(thn) <+> "else" <+> toDoc(els) + case NeutralStmt.Match(scrutinee, tpe, clauses, default) => + "match" <+> parens(toDoc(scrutinee)) <+> braces(hcat(clauses.map { case (id, block) => toDoc(id) <> ":" <+> toDoc(block) })) <> + (if (default.isDefined) "else" <+> toDoc(default.get) else emptyDoc) + case NeutralStmt.Jump(label, targs, vargs, bargs) => + // Format as: l1[T1, T2](r1, r2) + "jump" <+> toDoc(label) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) <> hsep(bargs.map(b => braces(toDoc(b)))) + case NeutralStmt.App(label, targs, vargs, bargs) => + // Format as: l1[T1, T2](r1, r2) + toDoc(label) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) <> hsep(bargs.map(b => braces(toDoc(b)))) + + case NeutralStmt.Invoke(label, method, tpe, targs, vargs, bargs) => + // Format as: l1[T1, T2](r1, r2) + toDoc(label) <> "." <> toDoc(method) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) <> hsep(bargs.map(b => braces(toDoc(b)))) + + case NeutralStmt.Reset(prompt, body) => + "reset" <+> braces(toDoc(prompt) <+> "=>" <+> nest(line <> toDoc(body.bindings) <> toDoc(body.body)) <> line) + + case NeutralStmt.Shift(prompt, k, body) => + "shift" <> parens(toDoc(prompt)) <+> braces(toDoc(k) <+> "=>" <+> nest(line <> toDoc(body.bindings) <> toDoc(body.body)) <> line) + + case NeutralStmt.Resume(k, body) => + "resume" <> parens(toDoc(k)) <+> toDoc(body) + + case NeutralStmt.Var(id, init, body) => + "var" <+> toDoc(id.id) <+> "=" <+> toDoc(init) <> line <> toDoc(body.bindings) <> toDoc(body.body) + + case NeutralStmt.Put(ref, tpe, cap, value, body) => + toDoc(ref) <+> ":=" <+> toDoc(value) <> line <> toDoc(body.bindings) <> toDoc(body.body) + + case NeutralStmt.Region(id, body) => + "region" <+> toDoc(id) <+> toDoc(body) + + case NeutralStmt.Alloc(id, init, region, body) => + "var" <+> toDoc(id) <+> "in" <+> toDoc(region) <+> "=" <+> toDoc(init) <> line <> toDoc(body.bindings) <> toDoc(body.body) + + case NeutralStmt.Hole(tpe, span) => "hole()" + } + + def toDoc(id: Id): Doc = id.show + + def toDoc(value: Value): Doc = value match { + // case Value.Var(id, tpe) => toDoc(id) + + case Value.PureExtern(callee, targs, vargs) => + toDoc(callee.id) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) + + case Value.Literal(value, _) => util.show(value) + case Value.Integer(value) => value.show + case Value.String(value) => value.show + + case Value.Make(data, tag, targs, vargs) => + "make" <+> toDoc(data) <+> toDoc(tag) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) + + case Value.Box(body, tpe) => + "box" <+> braces(nest(line <> toDoc(body) <> line)) + + case Value.Var(id, tpe) => toDoc(id) + } + + def toDoc(block: Block): Doc = block match { + case Block(tparams, vparams, bparams, body) => + (if (tparams.isEmpty) emptyDoc else brackets(hsep(tparams.map(toDoc), comma))) <> + parens(hsep(vparams.map(toDoc), comma)) <> hsep(bparams.map(toDoc)) <+> toDoc(body) + } + + def toDoc(comp: Computation): Doc = comp match { + case Computation.Unknown(id) => toDoc(id) + case Computation.Def(closure) => toDoc(closure) + case Computation.Known(s) => toDoc(s) + case Computation.Continuation(k) => ??? + case Computation.New(interface, operations) => "new" <+> toDoc(interface) <+> braces { + hsep(operations.map { case (id, impl) => "def" <+> toDoc(id) <+> "=" <+> toDoc(impl) }, ",") + } + case Computation.BuiltinExtern(id, vmSymbol) => "extern" <+> toDoc(id) <+> "=" <+> vmSymbol + } + def toDoc(closure: Closure): Doc = closure match { + case Closure(label, env) => toDoc(label) <+> "@" <+> brackets(hsep(env.map(toDoc), comma)) + } + + def toDoc(s: Static): Doc = s match { + case Static.Reference(id) => "ref@" <> toDoc(id.id) + case Static.Region(id) => "reg@" <> toDoc(id.id) + case Static.Prompt(id) => "p@" <> toDoc(id.id) + } + + def toDoc(bindings: Bindings): Doc = + hcat(bindings.map { + case (addr, Binding.Let(value)) => "let" <+> toDoc(addr) <+> "=" <+> toDoc(value) <> line + case (addr, Binding.Def(block)) => "def" <+> toDoc(addr) <+> "=" <+> toDoc(block) <> line + case (addr, Binding.Rec(block, tpe, capt)) => "def" <+> toDoc(addr) <+> "=" <+> toDoc(block) <> line + case (addr, Binding.Val(stmt)) => "val" <+> toDoc(addr) <+> "=" <+> toDoc(stmt) <> line + case (addr, Binding.Run(callee, targs, vargs, bargs)) => "let !" <+> toDoc(addr) <+> "=" <+> toDoc(callee.id) <> + (if (targs.isEmpty) emptyDoc else brackets(hsep(targs.map(toDoc), comma))) <> + parens(hsep(vargs.map(toDoc), comma)) <> hcat(bargs.map(b => braces { toDoc(b) })) <> line + case (addr, Binding.Unbox(innerAddr, tpe, capt)) => "def" <+> toDoc(addr) <+> "=" <+> "unbox" <+> toDoc(innerAddr) <+> "@" <+> brackets(hsep(capt.map { + case RuntimeCapture.Known(id) => toDoc(id) + case RuntimeCapture.Unknown(id) => toDoc(id) + }.toSeq, ", ")) <> line + case (addr, Binding.Get(ref, tpe, cap)) => "let" <+> toDoc(addr) <+> "=" <+> "!" <> toDoc(ref) <> line + }) + + def toDoc(block: BasicBlock): Doc = + braces(nest(line <> toDoc(block.bindings) <> toDoc(block.body)) <> line) + + def toDoc(p: ValueParam): Doc = toDoc(p.id) <> ":" <+> toDoc(p.tpe) + def toDoc(p: BlockParam): Doc = braces(toDoc(p.id)) + + def toDoc(t: ValueType): Doc = util.show(t) + def toDoc(t: BlockType): Doc = util.show(t) + + def show(stmt: NeutralStmt): String = pretty(toDoc(stmt), 80).layout + def show(value: Value): String = pretty(toDoc(value), 80).layout + def show(block: Block): String = pretty(toDoc(block), 80).layout + def show(bindings: Bindings): String = pretty(toDoc(bindings), 80).layout + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/integers.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/integers.scala new file mode 100644 index 000000000..5f3593e46 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/integers.scala @@ -0,0 +1,179 @@ +package effekt.core.optimizer.normalizer.theories + +import effekt.core.Block.BlockVar +import effekt.core.{Expr, Id, Type} +import effekt.core.optimizer.normalizer.semantics.Neutral + +/** + * Theory for integers with neutral variables: multivariate Laurent polynomials with 64-bit signed integer coefficients. + * Compare https://en.wikipedia.org/wiki/Laurent_polynomial + * + * KNOWN LIMITATION: This implementation assumes 64-bit signed integers. + * Unfortunately, this is unsound for the JavaScript backend, which uses JavaScript numbers that are IEEE-754 doubles. + */ +object integers { + case class IntegerRep(value: Long, addends: Addends) { + val free: Set[Id] = addends.flatMap { case (factors, _) => factors.keys.flatMap(_.free) }.toSet + def isLiteral: Boolean = addends.isEmpty + def show: String = { + val IntegerRep(v, a) = this + val terms = a.map { case (factors, n) => + val factorStr = if (factors.isEmpty) "1" else { + factors.map { case (n, exp) => + if (exp == 1) "" + else s"^$exp" + }.mkString("*") + } + if (n == 1) s"$factorStr" + else s"$n*$factorStr" + }.toList + + val constPart = if (v != 0) List(v.toString) else Nil + (constPart ++ terms).mkString(" + ") + } + } + + enum Operation { + case Add, Sub, Mul, Div + } + import Operation._ + + def embed(value: Long): integers.IntegerRep = IntegerRep(value, Map.empty) + def embed(n: Neutral): integers.IntegerRep = IntegerRep(0, Map(Map(n -> 1) -> 1)) + + def reify(value: IntegerRep, embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr): Expr = + Reify(embedBuiltinName, embedNeutral).reify(value) + + // 3 * x * x / y = Addend(3, Map(x -> 2, y -> -1)) + type Addends = Map[Factors, Long] + type Factors = Map[Neutral, Int] + + def normalize(n: IntegerRep): IntegerRep = normalized(n.value, n.addends) + + def normalized(value: Long, addends: Addends): IntegerRep = + val (const, norm) = normalizeAddends(addends) + IntegerRep(value + const, norm) + + def add(l: IntegerRep, r: IntegerRep): IntegerRep = (l, r) match { + // 2 + (3 * x) + 4 + (5 * y) = 6 + (3 * x) + (5 * y) + case (IntegerRep(x, xs), IntegerRep(y, ys)) => + normalized(x + y, add(xs, ys)) + } + + def add(xs: Addends, ys: Addends): Addends = { + var addends = xs + ys.foreach { case (factors, n) => + val m: Long = addends.getOrElse(factors, 0) + addends = addends.updated(factors, n + m) + } + addends + } + + // 3 * x1^2 + 2 * EMPTY + 0 * x2^3 = 2 + 3 * x1^2 + def normalizeAddends(xs: Addends): (Long, Addends) = { + var constant: Long = 0 + var filtered: Addends = Map.empty + xs.foreach { case (factors, n) => + if (factors.isEmpty) { + constant += n + } + if (n != 0) { + filtered = filtered.updated(factors, n) + } + } + (constant, filtered) + } + + def neg(l: IntegerRep): IntegerRep = mul(l, -1) + + // (42 + 3*x + y) - (42 + 3*x + y) = (42 + 3*x + y) + (-1*42 + -1*3*x + -1*y) + def sub(l: IntegerRep, r: IntegerRep): IntegerRep = + add(l, neg(r)) + + def mul(l: IntegerRep, factor: Long): IntegerRep = l match { + case IntegerRep(value, addends) => + IntegerRep(value * factor, addends.map { case (f, n) => f -> n * factor }) + } + + def mul(l: IntegerRep, factor: Factors): IntegerRep = l match { + case IntegerRep(value, addends) => + IntegerRep(0, Map(factor -> value) ++ addends.map { case (f, n) => + mul(f, factor) -> n + }) + } + + // (x * x * y) * (x * y * z) = x^3 + y^2 + z^1 + def mul(l: Factors, r: Factors): Factors = { + var factors = l + r.foreach { case (f, n) => + val m = factors.getOrElse(f, 0) + factors = factors.updated(f, n + m) + } + normalizeFactors(factors) + } + + // x1^2 * x2^0 * x3^3 = x1^2 * x3^3 + def normalizeFactors(f: Factors): Factors = + f.filterNot { case (n, exp) => exp == 0 } + + // (42 + 3*x + y) * (42 + 3*x + y) + // = + // (42 + 3*x + y) * 42 + (42 + 3*x + y) * 3*x + (42 + 3*x + y) * y + def mul(l: IntegerRep, r: IntegerRep): IntegerRep = r match { + case IntegerRep(y, ys) => + var sum: IntegerRep = mul(l, y) + ys.foreach { case (f, n) => sum = add(sum, mul(mul(l, n), f)) } + normalize(sum) + } + + case class Reify(embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr) { + def reifyVar(n: Neutral): Expr = embedNeutral(n) + + def reifyInt(v: Long): Expr = Expr.Literal(v, Type.TInt) + + def reifyOp(l: Expr, op: Operation, r: Expr): Expr = op match { + case Add => Expr.PureApp(embedBuiltinName("effekt::infixPlus(Int, Int)"), List(), List(l, r)) + case Sub => Expr.PureApp(embedBuiltinName("effekt::InfixMinus(Int, Int)"), List(), List(l, r)) + case Mul => Expr.PureApp(embedBuiltinName("effekt::infixStar(Int, Int)"), List(), List(l, r)) + case Div => Expr.PureApp(embedBuiltinName("effekt::infixDiv(Int, Int)"), List(), List(l, r)) + } + + def reify(v: IntegerRep): Expr = + val IntegerRep(const, addends) = normalize(v) + + val adds = addends.toList.map { case (factors, n) => + if (n == 1) reifyFactors(factors) + else reifyOp(reifyInt(n), Mul, reifyFactors(factors)) + }.reduceOption { case (l, r) => reifyOp(l, Add, r) } + + adds.map { a => + if (const != 0) reifyOp(reifyInt(const), Add, a) + else a + }.getOrElse { + reifyInt(const) + } + + def reifyFactor(x: Neutral, n: Int): Expr = + if (n <= 0) sys error "Should not happen" + else if (n == 1) reifyVar(x) + else reifyOp(reifyVar(x), Mul, reifyFactor(x, n - 1)) + + def reifyFactors(ys: Factors): Expr = { + val factors = ys.toList.filterNot { case (_, n) => n == 0 } + val positive = factors.filter { case (_, n) => n > 0 } + val negative = factors.filter { case (_, n) => n < 0 } + + val pos = positive.map { case (x, n) => reifyFactor(x, n) }.reduceOption { case (l, r) => reifyOp(l, Mul, r) } + val neg = negative.map { case (x, n) => reifyFactor(x, n * -1) }.reduceOption { case (l, r) => reifyOp(l, Mul, r) } + val numerator = pos.getOrElse { + reifyInt(1) + } + + neg.map { denominator => + reifyOp(numerator, Div, denominator) + }.getOrElse { + numerator + } + } + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/strings.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/strings.scala new file mode 100644 index 000000000..0aa50cf4d --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/normalizer/theories/strings.scala @@ -0,0 +1,50 @@ +package effekt.core.optimizer.normalizer.theories + +import effekt.core.Block.BlockVar +import effekt.core.{Expr, Id, Type} +import effekt.core.optimizer.normalizer.semantics.Neutral + +/** + * Theory for strings: free extension of the string monoid + * Compare https://arxiv.org/pdf/2306.15375 + * + * Invariant: there are no adjacent literals in a StringRep + */ +object strings { + case class StringRep(value: List[String | Neutral]) { + val free: Set[Id] = value.collect { case n: Neutral => n.free }.flatten.toSet + def isLiteral: Boolean = value.length == 1 && value.head.isInstanceOf[String] + def show: String = { + val terms = value.map { + case s: String => s""""$s"""" + case n: Neutral => "" + } + terms.mkString(" ++ ") + } + } + + def embed(value: String): StringRep = StringRep(List(value)) + def embed(value: Neutral): StringRep = StringRep(List(value)) + + def reify(value: StringRep, embedBuiltinName: String => BlockVar, embedNeutral: Neutral => Expr): Expr = value match { + case StringRep(parts) => + parts.map { + case s: String => Expr.Literal(s, Type.TString) + case n: Neutral => embedNeutral(n) + }.reduceLeft { (l, r) => + Expr.PureApp(embedBuiltinName("effekt::infixPlusPlus(String, String)"), List(), List(l, r)) + } + } + + def concat(l: StringRep, r: StringRep): StringRep = (l, r) match { + case (StringRep(xs), StringRep(ys)) => + (xs, ys) match { + // fuse trailing / leading string literals at the boundary (if any) + case (init :+ (s1: String), (s2: String) :: tail) => + val concatenated: String | Neutral = s1 + s2 + StringRep((init :+ concatenated) ::: tail) + case _ => + StringRep(xs ::: ys) + } + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala b/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala index c958660c7..665793b39 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala @@ -7,6 +7,7 @@ import effekt.util.UByte import java.io.PrintStream import scala.util.matching as regex import scala.util.matching.Regex +import scala.Conversion trait Runtime { def out: PrintStream diff --git a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala index 9b08e63d6..510bd0420 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -522,8 +522,10 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { val functions = m.definitions.collect { case Toplevel.Def(id, b: Block.BlockLit) => id -> b }.toMap val builtinFunctions = m.externs.collect { - case Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, - ExternBody.StringExternBody(FeatureFlag.NamedFeatureFlag("vm", _), Template(name :: Nil, Nil))) => + case Extern.Def( + id, tps, cps, vps, bps, ret, annotatedCapture, _, + Some(ExternBody.StringExternBody(FeatureFlag.NamedFeatureFlag("vm", _), Template(name :: Nil, Nil))) + ) => id -> builtins.getOrElse(name, throw VMError.MissingBuiltin(name)) }.toMap diff --git a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala index 16cf3f099..85cff01b6 100644 --- a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala @@ -35,7 +35,7 @@ object Transformer { } def transform(extern: core.Extern)(using TransformationContext): Option[Extern] = extern match { - case core.Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body) => + case core.Extern.Def(id, tparams, cparams, vparams, bparams, ret, annotatedCapture, body, vmBody) => Some(Extern.Def(id, vparams.map(_.id), bparams.map(_.id), annotatedCapture.contains(symbols.builtins.AsyncCapability.capture), transform(body))) case core.Extern.Include(featureFlag, contents) => Some(Extern.Include(featureFlag, contents)) case core.Extern.Data(id, tparams) => None diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala index fd03c3b49..34bfb76d6 100644 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala @@ -124,7 +124,7 @@ trait Transformer { } def toChez(decl: core.Extern)(using ErrorReporter): Option[chez.Def] = decl match { - case Extern.Def(id, tpe, cps, vps, bps, ret, capt, body) => + case Extern.Def(id, tpe, cps, vps, bps, ret, capt, body, vmBody) => val tBody = body match { case ExternBody.StringExternBody(featureFlag, contents) => toChez(contents) case u: ExternBody.Unsupported => diff --git a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala index b6624bed1..19398ecf2 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala @@ -244,6 +244,8 @@ object TransformerCps extends Transformer { js.Const(nameDef(id), toJS(binding)) :: toJS(body).run(k) } + // Note: currently we only perform this translation if there isn't already a direct-style continuation + // // [[ let k(x, ks) = ...; if (...) jump k(42, ks2) else jump k(10, ks3) ]] = // let x; if (...) { x = 42; ks = ks2 } else { x = 10; ks = ks3 } ... case cps.Stmt.LetCont(id, Cont.ContLam(params, ks, body), body2) if canBeDirect(id, body2) && diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 332518a69..6f7e2e585 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -55,7 +55,7 @@ object Transformer { } def transform(extern: core.Extern)(using BlocksParamsContext, ErrorReporter): Option[Declaration] = extern match { - case core.Extern.Def(name, tps, cparams, vparams, bparams, ret, capture, body) => + case core.Extern.Def(name, tps, cparams, vparams, bparams, ret, capture, body, vmBody) => // TODO delete, and/or enforce at call site (ImpureApp) if bparams.nonEmpty then ErrorReporter.abort("Foreign functions currently cannot take block arguments.") @@ -69,7 +69,7 @@ object Transformer { case core.Extern.Include(ff, contents) => Some(Include(ff, contents)) - + case core.Extern.Data(id, tparams) => None } @@ -634,7 +634,7 @@ object Transformer { noteDefinition(id, params, free, false) def noteParameter(id: Id, tpe: core.BlockType)(using BC: BlocksParamsContext): Unit = - assert(!BC.info.isDefinedAt(id), s"Registering info twice for ${id} (was: ${BC.info(id)}, now: Parameter)") + assert(!BC.info.isDefinedAt(id), s"Registering info twice for ${id} (was: ${BC.info(id)}, now: Parameter(${tpe})") BC.info += (id -> BlockInfo.Parameter(tpe)) def noteParameters(ps: List[core.BlockParam])(using BC: BlocksParamsContext): Unit = diff --git a/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala b/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala index 1c179e56f..fa356c11a 100644 --- a/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala +++ b/effekt/shared/src/main/scala/effekt/source/ResolveExternDefs.scala @@ -13,6 +13,7 @@ object ResolveExternDefs extends Phase[Typechecked, Typechecked] { case Typechecked(source, tree, mod) => Some(Typechecked(source, rewrite(tree), mod)) } + // The list of supported feature flags for this backend def supported(using Context): List[String] = Context.compiler.supportedFeatureFlags def defaultExternBody(warning: String)(using Context): ExternBody = @@ -40,6 +41,7 @@ object ResolveExternDefs extends Phase[Typechecked, Typechecked] { def rewrite(defn: Def)(using Context): Option[Def] = Context.focusing(defn) { case Def.ExternDef(id, tparams, vparams, bparams, capture, ret, bodies, doc, span) => + val vmBody = bodies.find(_.featureFlag.matches("vm", matchDefault = false)) findPreferred(bodies) match { case body@ExternBody.StringExternBody(featureFlag, template, span) => if (featureFlag.isDefault) { @@ -47,7 +49,7 @@ object ResolveExternDefs extends Phase[Typechecked, Typechecked] { + s"please annotate it with a feature flag (Supported by the current backend: ${Context.compiler.supportedFeatureFlags.mkString(", ")})") } - val d = Def.ExternDef(id, tparams, vparams, bparams, capture, ret, List(body), doc, span) + val d = Def.ExternDef(id, tparams, vparams, bparams, capture, ret, List(body) ++ vmBody.toList, doc, span) Context.copyAnnotations(defn, d) Some(d) case ExternBody.EffektExternBody(featureFlag, body, span) => diff --git a/examples/pos/raytracer.effekt b/examples/pos/raytracer.effekt index 4d95e0d3e..43f38baf6 100644 --- a/examples/pos/raytracer.effekt +++ b/examples/pos/raytracer.effekt @@ -35,7 +35,7 @@ def sub(a: Vector, b: Vector): Vector = Vector(a.x - b.x, a.y - b.y, a.z - b.z) -// def infixMul(a: Vector, b: Vector): Double = dot(a, b) +// def infixStar(a: Vector, b: Vector): Double = dot(a, b) def infixStar(a: Vector, t: Double): Vector = a.scale(t) def infixStar(t: Double, a: Vector): Vector = a.scale(t) def infixPlus(a: Vector, b: Vector): Vector = add(a, b)