Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Direct-style (DS) JS Backend #316

Merged
merged 49 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
59496d2
First draft of direct-style (DS) version of the JS backend
b-studios Nov 24, 2023
6fe7bf4
Support run
b-studios Nov 24, 2023
99e0009
Use bind monad to work around JS limitations
b-studios Nov 24, 2023
6eef4de
Add try-catch around entrypoints
b-studios Nov 24, 2023
9a6f349
Implement free variables, once more...
b-studios Nov 24, 2023
c6923f7
debug free variables
b-studios Nov 24, 2023
8c16835
Refactor rename
b-studios Nov 24, 2023
92e52ea
Track locals explicitly
b-studios Nov 24, 2023
7dcd981
Emit continuations
b-studios Nov 24, 2023
73d390f
Start translating handler
b-studios Nov 24, 2023
1ea3705
Support discarding the continuation
b-studios Nov 25, 2023
9e10001
First support of resuming continuation
b-studios Nov 25, 2023
4871641
Minor improvements
b-studios Nov 25, 2023
8389346
Start working on lambda lifting (closures still need to be eta-expanded)
b-studios Nov 25, 2023
6e2d08d
Implement lambda lifting
b-studios Nov 27, 2023
c803500
Start to support pattern matching
b-studios Nov 27, 2023
8da634b
Add some tests
b-studios Nov 27, 2023
22b1715
Mark tests that rely on local mutable state, regions, or bidirectiona…
b-studios Nov 27, 2023
cd357da
Implement local backtrackable mutable state
b-studios Nov 27, 2023
9d4c748
Support regions
b-studios Nov 28, 2023
dbff4c6
Support bidirectional effects
b-studios Nov 28, 2023
86a448e
Also rename types when testing for alpha equivalence
b-studios Nov 30, 2023
bdb5c91
Move LambdaLifting to separate file
b-studios Nov 30, 2023
1d113ea
Adapt Renamer to new structure of BlockParam
b-studios Dec 1, 2023
ee17b9a
Stub compileSeparate and implement it later
b-studios Dec 1, 2023
eaf9165
Make DS backend work with the website (note that these changes need t…
b-studios Dec 2, 2023
5dbcab6
Add two simple lambda lifting tests
b-studios Dec 2, 2023
2af69d7
Fix some bugs in Renamer and simplify Optimizer
b-studios Dec 3, 2023
42124ac
fix local non-recursive definitions in renamer
b-studios Dec 3, 2023
1117134
Transform binder in its scope, even though this is wrong
b-studios Dec 3, 2023
99440a7
Add some optimizer tests
b-studios Dec 3, 2023
0e5745b
Fix one bug where inferred capture was not propagated
b-studios Dec 3, 2023
675ae60
Also fix same bug for regions
b-studios Dec 3, 2023
dcfa4ba
Perform inlining only once and then iterate
b-studios Dec 3, 2023
0ffeea3
Add macro based tracing tool
b-studios Dec 3, 2023
ce93739
Forgot file
b-studios Dec 3, 2023
5eff913
Slightly refactor optimizer
b-studios Dec 4, 2023
beb1124
Fix inline full to not drop count
b-studios Dec 4, 2023
cf05dbd
Fix annotated free variables and avoid passing result twice
b-studios Dec 4, 2023
f86c82b
Share common code between JS backends
b-studios Dec 4, 2023
cf73a03
Prepare runtime to deal with tailcall trampolining
b-studios Dec 4, 2023
cb22749
Reify continuation
b-studios Dec 4, 2023
ac865aa
Use effects to express tail calls (UNSOUND)
b-studios Dec 4, 2023
86b295a
Deprecate tail calls for now
b-studios Dec 4, 2023
a584793
Add while and continue
b-studios Dec 4, 2023
c58a5da
Support obvious tail calls manually in JS
b-studios Dec 4, 2023
eea5266
Fix Renamer to conform to contract given in docs
marzipankaiser Dec 4, 2023
e9d56de
Drop unbind
b-studios Dec 4, 2023
95d8d32
Reactive last failing test
b-studios Dec 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions effekt/js/src/main/scala/effekt/LanguageServer.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package effekt

import effekt.context.{ Context, VirtualFileSource, VirtualModuleDB }
import effekt.generator.js.Transformer
import effekt.generator.js.TransformerMonadic
import effekt.lifted.LiftInference
import effekt.util.{ PlainMessaging, getOrElseAborting }
import effekt.util.messages.{ BufferedMessaging, EffektError, EffektMessaging, FatalPhaseError }
Expand Down Expand Up @@ -190,7 +190,7 @@ class LanguageServer extends Intelligence {
}

private def path(m: symbols.Module)(using C: Context): String =
(C.config.outputPath() / Transformer.jsModuleFile(m.path)).unixPath
(C.config.outputPath() / TransformerMonadic.jsModuleFile(m.path)).unixPath
}

@JSExportTopLevel("effekt")
Expand Down
2 changes: 1 addition & 1 deletion effekt/js/src/test/scala/effekt/WebTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object WebTests extends TestSuite {
def evaluate[A](imports: List[String], content: String) = {
server.writeFile("interactive.effekt", imports.map(i => s"import $i").mkString("\n") + s"\n\ndef main() = ${content}")
val mainFile = server.compileFile("interactive.effekt")
load(mainFile.replace("out/", "")).main().run().asInstanceOf[A]
load(mainFile.replace("out/", "")).main().asInstanceOf[A]
}

test("Evaluate simple expressions in REPL") {
Expand Down
4 changes: 2 additions & 2 deletions effekt/jvm/src/main/scala/effekt/Runner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ object JSRunner extends Runner[String] {
val jsFilePath = (out / path).unixPath
// create "executable" using shebang besides the .js file
val jsScriptFilePath = jsFilePath.stripSuffix(s".$extension")
val jsScript = s"require('${jsFilePath}').main().run()"
val jsScript = s"require('${jsFilePath}').main()"
val shebang = "#!/usr/bin/env node"
IO.createFile(jsScriptFilePath, s"$shebang\n$jsScript", true)
jsScriptFilePath
Expand Down Expand Up @@ -213,7 +213,7 @@ object MLRunner extends Runner[String] {
* Requires the MLton compiler to be installed on the machine.
* Assumes [[path]] has the format "SOMEPATH.sml".
*/
override def build(path: String)(using C: Context): String =
override def build(path: String)(using C: Context): String =
val out = C.config.outputPath()
val buildFile = (out / "main.mlb").canonicalPath
val executable = (out / "mlton-main").canonicalPath
Expand Down
33 changes: 25 additions & 8 deletions effekt/jvm/src/test/scala/effekt/core/CoreTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,40 @@ import effekt.PhaseResult.CoreTransformed
*/
trait CoreTests extends munit.FunSuite {

protected val defaultNames = new Names(symbols.builtins.rootTypes ++ symbols.builtins.rootTerms ++ symbols.builtins.rootCaptures)
protected def defaultNames = symbols.builtins.rootTypes ++ symbols.builtins.rootTerms ++ symbols.builtins.rootCaptures

def shouldBeEqual(obtained: ModuleDecl, expected: ModuleDecl, clue: => Any)(using Location) =
assertEquals(obtained, expected, {
s"""${clue}
|=====================
|Got:
|----
|${effekt.core.PrettyPrinter.format(obtained).layout}
|
|Expected:
|---------
|${effekt.core.PrettyPrinter.format(expected).layout}
|
|""".stripMargin
})

def assertAlphaEquivalent(obtained: ModuleDecl,
expected: ModuleDecl,
clue: => Any = "values are not alpha-equivalent",
names: Names = defaultNames)(using Location): Unit = {
val renamer = Renamer(names)
assertEquals(renamer(obtained), renamer(expected), clue=clue)
names: Names = Names(defaultNames))(using Location): Unit = {
val renamer = Renamer(names, "$")
shouldBeEqual(renamer(obtained), renamer(expected), clue)
}
def parse(input: String,
nickname: String = "input",
names: Names = defaultNames): ModuleDecl = {
names: Names = Names(defaultNames))(using Location): ModuleDecl = {
CoreParsers.module(input, names) match {
case Success(result, next) if next.atEnd => result
case Success(result, next) => fail(s"Parsing ${nickname} had trailing garbage: " +
s"${next.source.toString.substring(next.offset)}")
case err: NoSuccess => fail(s"Parsing ${nickname} failed: ${err.message}")
case err: NoSuccess =>
val pos = err.next.position
fail(s"Parsing ${nickname} failed\n[${pos.line}:${pos.column}] ${err.message}")
}
}

Expand All @@ -38,7 +55,7 @@ trait CoreTransformationTests extends CoreTests {

def assertTransformsTo(input: String, expected: String,
clue: => Any = "transformation result is not the expected one",
names: Names = defaultNames)(using Location): Unit = {
names: Names = Names(defaultNames))(using Location): Unit = {
val pInput = parse(input, "input", names = names)
val pExpected = parse(expected, "expected result", names = names)
val obtained = transform(pInput)
Expand Down Expand Up @@ -68,4 +85,4 @@ trait CorePhaseTests[P <: Phase[CoreTransformed, CoreTransformed]](phase: P) ext
}
}
}
}
}
39 changes: 39 additions & 0 deletions effekt/jvm/src/test/scala/effekt/core/LambdaLiftingTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package effekt.core


class LambdaLiftingTests extends CorePhaseTests(LambdaLifting) {

test("toplevel functions stay unchanged"){
val from =
"""module main
|
|def id = { ['A](a: 'A) => return a: 'A }
|""".stripMargin
assertTransformsTo(from, from)
}

test("local functions are lifted out"){
val from =
"""module main
|
|def outer = { () =>
| def local = { () => return 42 }
| val res = (local: () => Int @ {})();
| return res:Int
|}
|""".stripMargin

val to =
"""module main
|
|def outer = { () =>
| val res = (local: () => Int @ {})();
| return res:Int
|}
|
|def local = { () => return 42 }
|""".stripMargin
assertTransformsTo(from, to)
}

}
222 changes: 222 additions & 0 deletions effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package effekt
package core
import effekt.symbols

class OptimizerTests extends CoreTests {

val mainSymbol = Id("main")

def assertTransformsTo(
input: String,
transformed: String,
names: Names = Names(defaultNames + ("main" -> mainSymbol))
)(transform: ModuleDecl => ModuleDecl)(using munit.Location) = {
val moduleHeader =
"""module test
|
|""".stripMargin
val pInput = parse(moduleHeader + input, "input", names)
val pExpected = parse(moduleHeader + transformed, "expected", names)

// the parser is not assigning symbols correctly, so we need to run renamer first
val renamed = Renamer(names).rewrite(pInput)

val obtained = transform(renamed)
assertAlphaEquivalent(obtained, pExpected, "Not transformed to")
}

def removeUnused(input: String, expected: String)(using munit.Location) =
assertTransformsTo(input, expected) { tree =>
RemoveUnusedDefinitions.remove(Set(mainSymbol), tree)
}

def inlineOnce(input: String, expected: String)(using munit.Location) =
assertTransformsTo(input, expected) { tree =>
val (result, count) = InlineUnique.once(Set(mainSymbol), tree)
result
}

def inlineFull(input: String, expected: String)(using munit.Location) =
assertTransformsTo(input, expected) { tree =>
InlineUnique.full(Set(mainSymbol), tree)
}

test("toplevel"){
val input =
""" def foo = { () => return 42 }
| def main = { () => return 42 }
|""".stripMargin

val expected =
""" def main = { () => return 42 }
|""".stripMargin

removeUnused(input, expected)
}

test("transitive (length 3)"){
val input =
""" def foo = { () => return 42 }
| def bar = { () => (foo : () => Unit @ {})() }
| def baz = { () => (bar : () => Unit @ {})() }
| def bam = { () => (baz : () => Unit @ {})() }
| def main = { () => (bam : () => Unit @ {})() }
|""".stripMargin

removeUnused(input, input)
}

test("recursive (unused)"){
val input =
""" def foo = { () => (bar : () => Unit @ {})() }
| def bar = { () => (foo : () => Unit @ {})() }
| def main = { () => return 42 }
|""".stripMargin

val expected =
""" def main = { () => return 42 }
|""".stripMargin

removeUnused(input, expected)
}

test("recursive used"){
val input =
""" def foo = { () => (bar : () => Unit @ {})() }
| def bar = { () => (foo : () => Unit @ {})() }
| def main = { () => (foo : () => Unit @ {})() }
|""".stripMargin

removeUnused(input, input)
}

test("nested all removed"){
val input =
""" def main = { () =>
| def foo = { () => return 1 }
| return 2
| }
|""".stripMargin

val expected =
""" def main = { () => return 2 }
|""".stripMargin

removeUnused(input, expected)
}
// let y = !(println: (String) => Unit @ {io})("hello")
test("drop pure let expressions"){
val input =
""" def main = { () =>
| let x = (add : (Int, Int) => Int @ {})(1, 2)
| let y = !(println: (String) => Unit @ {io})("hello")
| let z = 7
| return z:Int
| }
|""".stripMargin

val expected =
""" def main = { () =>
| let y = !(println: (String) => Unit @ {io})("hello")
| let z = 7
| return z:Int
| }
|""".stripMargin

removeUnused(input, expected)
}

test("pseudo recursive"){
val input =
""" def bar = { () => return 1 }
| def main = { () =>
| def foo = { () => (bar : () => Unit @ {})() }
| def bar = { () => return 2 }
| (foo : () => Unit @ {})()
| }
|""".stripMargin


val expected =
""" def bar = { () => return 1 }
| def main = { () =>
| def foo = { () => (bar : () => Unit @ {})() }
| (foo : () => Unit @ {})()
| }
|""".stripMargin

removeUnused(input, expected)
}

test("inline toplevel"){
val input =
""" def foo = { () => return 42 }
| def main = { () => (foo : () => Unit @ {})() }
|""".stripMargin

val expected =
""" def foo = { () => return 42 }
| def main = { () => return 42 }
|""".stripMargin

inlineOnce(input, expected)
}

test("inline with argument"){
val input =
""" def foo = { (n: Int) => return n:Int }
| def main = { () => (foo : (Int) => Unit @ {})(42) }
|""".stripMargin

val expected =
""" def foo = { (n: Int) => return n:Int }
| def main = { () => return 42 }
|""".stripMargin

inlineOnce(input, expected)
}

test("inline higher order function"){
val input =
""" def foo = { (n: Int) => return n:Int }
| def hof = { (){f : (Int) => Int} =>
| (f : (Int) => Int @ {f})(1)
| }
| def main = { () =>
| (hof : (){f : (Int) => Int} => Int @ {})(){ (foo : (Int) => Unit @ {}) }
| }
|""".stripMargin

val expected =
""" def foo = { (n: Int) => return n:Int }
| def hof = { (){f : (Int) => Int} =>
| (f : (Int) => Int @ {f})(1)
| }
| def main = { () =>
| def local(n: Int) = return n:Int
| (local : (Int) => Int @ {})(1)
| }
|""".stripMargin

inlineOnce(input, expected)
}

test("fully inline higher order function"){
val input =
""" def foo = { (n: Int) => return n:Int }
| def hof = { (){f : (Int) => Int} =>
| (f : (Int) => Int @ {f})(1)
| }
| def main = { () =>
| (hof : (){f : (Int) => Int} => Int @ {})(){ (foo : (Int) => Unit @ {}) }
| }
|""".stripMargin

val expected =
""" def main = { () => return 1 }
|""".stripMargin

inlineFull(input, expected)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@ abstract class AbstractPolymorphismBoxingTests extends CorePhaseTests(Polymorphi
}
}

override protected val defaultNames = new Names(boxtpes ++
override protected val defaultNames = boxtpes ++
symbols.builtins.rootTypes ++ Map(
// TODO maybe add used names
))

)
}
class PolymorphismBoxingTests extends AbstractPolymorphismBoxingTests {

Expand Down
Loading