Skip to content

Commit bd89bbc

Browse files
Reach reparsability of core pretty-printer (#1181)
Currently, it is very hard to write tests for core. There are two main reasons for this: 1. The core parser is currently very rudimentary and it is not easy to write core programs. This would be addressed by a new core frontend (#1152). 2. The core pretty-printer is currently unable to produce code that can be re-parsed with the core parser. I would like to address this with this PR. While a new core frontend would subsume both of these problems, it is a lot more effort than adding a re-parsable mode to the current core pretty-printer. This would allow us to compile source programs to core and pretty-print them to use them as golden output in core tests. In particular, when developing the new normalizer (#1127), I would like to track its behavior in unit tests, but they are currently very time-consuming to write due to the fact I cannot just use pretty-printed core for comparison.
1 parent 4946a1c commit bd89bbc

13 files changed

Lines changed: 662 additions & 152 deletions

File tree

.github/workflows/ci-pr.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ jobs:
6161
- name: Test effekt binary
6262
run: effekt.sh --help
6363

64+
# These are currently separated as they take a long time to run.
65+
core-reparse-tests:
66+
name: "Core Reparse Tests"
67+
needs: build-and-compile
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v5
71+
with:
72+
submodules: 'true'
73+
74+
- uses: ./.github/actions/setup-effekt
75+
76+
- uses: ./.github/actions/restore-build-cache
77+
78+
- name: Run core reparse tests
79+
run: |
80+
sbt effektJVM/testCoreReparsing
81+
6482
windows-tests:
6583
name: "Windows Smoke Test"
6684
needs: build-and-compile

build.sbt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ lazy val bumpMinorVersion = taskKey[Unit]("Bumps the minor version number (used
1717
lazy val testBackendJS = taskKey[Unit]("Run JavaScript backend tests")
1818
lazy val testBackendChez = taskKey[Unit]("Run Chez Scheme backend tests")
1919
lazy val testBackendLLVM = taskKey[Unit]("Run LLVM backend tests")
20+
lazy val testCoreReparsing = taskKey[Unit]("Run core reparsing tests")
2021
lazy val testRemaining = taskKey[Unit]("Run all non-backend tests (internal tests) on effektJVM")
2122

2223
lazy val noPublishSettings = Seq(
@@ -230,13 +231,19 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e
230231
).value
231232
},
232233

234+
testCoreReparsing := {
235+
(Test / testOnly).toTask(
236+
" effekt.core.ReparseTests"
237+
).value
238+
},
239+
233240
testRemaining := Def.taskDyn {
234241
val log = streams.value.log
235242

236243
val allTests = (Test / definedTestNames).value.toSet
237244

238-
// Explicit list of backend tests (union of all testBackend targets)
239-
val backendTests = Set(
245+
// Explicit list of tests run separately (union of all testBackend targets)
246+
val separatedTests = Set(
240247
"effekt.JavaScriptTests",
241248
"effekt.StdlibJavaScriptTests",
242249
"effekt.ChezSchemeMonadicTests",
@@ -247,10 +254,11 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e
247254
"effekt.StdlibChezCPSTests",
248255
"effekt.LLVMTests",
249256
"effekt.LLVMNoValgrindTests",
250-
"effekt.StdlibLLVMTests"
257+
"effekt.StdlibLLVMTests",
258+
"effekt.core.ReparseTests",
251259
)
252260

253-
val remaining = allTests -- backendTests
261+
val remaining = allTests -- separatedTests
254262

255263
if (remaining.isEmpty) {
256264
log.info("No remaining tests")

effekt/jvm/src/test/scala/effekt/core/CoreTests.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,19 @@ trait CoreTests extends munit.FunSuite {
4747
expected: ModuleDecl,
4848
clue: => Any = "values are not alpha-equivalent",
4949
names: Names = Names(defaultNames))(using Location): Unit = {
50-
val renamer = TestRenamer(names, "$")
51-
shouldBeEqual(renamer(obtained), renamer(expected), clue)
50+
val renamer = TestRenamer(names)
51+
val obtainedRenamed = renamer(obtained)
52+
val expectedRenamed = renamer(expected)
53+
val obtainedPrinted = effekt.core.PrettyPrinter.format(obtainedRenamed).layout
54+
val expectedPrinted = effekt.core.PrettyPrinter.format(expectedRenamed).layout
55+
assertEquals(obtainedPrinted, expectedPrinted)
56+
shouldBeEqual(obtainedRenamed, expectedRenamed, clue)
5257
}
5358
def assertAlphaEquivalentStatements(obtained: Stmt,
5459
expected: Stmt,
5560
clue: => Any = "values are not alpha-equivalent",
5661
names: Names = Names(defaultNames))(using Location): Unit = {
57-
val renamer = TestRenamer(names, "$")
62+
val renamer = TestRenamer(names)
5863
shouldBeEqual(renamer(obtained), renamer(expected), clue)
5964
}
6065
def parse(input: String,

effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class OptimizerTests extends CoreTests {
2222
val pExpected = parse(moduleHeader + transformed, "expected", names)
2323

2424
// the parser is not assigning symbols correctly, so we need to run renamer first
25-
val renamed = TestRenamer(names).rewrite(pInput)
25+
val renamer = TestRenamer(names)
26+
val renamed = renamer(pInput)
2627

2728
val obtained = transform(renamed)
2829
assertAlphaEquivalent(obtained, pExpected, "Not transformed to")
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package effekt.core
2+
3+
import effekt.*
4+
import effekt.PhaseResult.CoreTransformed
5+
import effekt.context.{Context, IOModuleDB}
6+
import effekt.util.PlainMessaging
7+
import kiama.output.PrettyPrinterTypes.Document
8+
import kiama.util.{Source, StringSource}
9+
import munit.Location
10+
import sbt.io.*
11+
import sbt.io.syntax.*
12+
13+
import java.io.File
14+
15+
/*
16+
// * This test suite ensures that the core pretty-printer always produces reparsable code.
17+
*/
18+
class ReparseTests extends CoreTests {
19+
object plainMessaging extends PlainMessaging
20+
object context extends Context with IOModuleDB {
21+
val messaging = plainMessaging
22+
23+
object frontend extends CompileToCore
24+
25+
override lazy val compiler = frontend.asInstanceOf
26+
}
27+
28+
// The sources of all test files are stored here:
29+
def examplesDir = new File("examples")
30+
31+
// Test files which are to be ignored (since features are missing or known bugs exist)
32+
def ignored: Set[File] = Set(
33+
// Missing include: text/pregexp.scm
34+
File("examples/pos/simpleparser.effekt"),
35+
// Cannot find source for unsafe/cont
36+
File("examples/pos/propagators.effekt"),
37+
// Bidirectional effects that mention the same effect recursively are not (yet) supported.
38+
File("examples/pos/bidirectional/selfrecursion.effekt"),
39+
// FIXME: Wrong number of type arguments
40+
File("examples/pos/type_omission_op.effekt"),
41+
// FIXME: There is currently a limitation in TestRenamer in that it does not rename captures.
42+
// This means, that in this example, captures for State[Int] and State[String] are both printed as "State",
43+
// leading to a collapse of the capture set {State_1, State_2} to just {State}.
44+
File("examples/pos/parametrized.effekt")
45+
)
46+
47+
def positives: Set[File] = Set(
48+
examplesDir / "pos",
49+
examplesDir / "casestudies",
50+
examplesDir / "benchmarks",
51+
)
52+
53+
def runTests() = positives.foreach(runPositiveTestsIn)
54+
55+
def runPositiveTestsIn(dir: File): Unit =
56+
foreachFileIn(dir) {
57+
// We don't currently test *.effekt.md files
58+
f => if (!ignored.contains(f) && !f.getName.endsWith(".md")) {
59+
test(s"${f.getPath}") {
60+
toCoreThenReparse(f)
61+
}
62+
}
63+
}
64+
65+
def toCoreThenReparse(input: File) = {
66+
val content = IO.read(input)
67+
val config = new EffektConfig(Seq("--Koutput", "string"))
68+
config.verify()
69+
context.setup(config)
70+
val (_, _, coreMod: ModuleDecl) = context.frontend.compile(StringSource(content, "input.effekt"))(using context).map {
71+
case (_, decl) => decl
72+
}.getOrElse {
73+
val errors = plainMessaging.formatMessages(context.messaging.buffer)
74+
sys error errors
75+
}
76+
val renamer = TestRenamer(Names(defaultNames))
77+
val expectedRenamed = renamer(coreMod)
78+
val printed = core.PrettyPrinter.format(expectedRenamed).layout
79+
val reparsed: ModuleDecl = parse(printed)(using Location.empty)
80+
val reparsedRenamed = renamer(reparsed)
81+
val reparsedPrinted = core.PrettyPrinter.format(reparsedRenamed).layout
82+
val expectedPrinted = core.PrettyPrinter.format(expectedRenamed).layout
83+
assertEquals(reparsedPrinted, expectedPrinted)
84+
}
85+
86+
def foreachFileIn(file: File)(test: File => Unit): Unit =
87+
file match {
88+
case f if f.isDirectory =>
89+
f.listFiles.foreach(foreachFileIn(_)(test))
90+
case f if f.getName.endsWith(".effekt") || f.getName.endsWith(".effekt.md") =>
91+
test(f)
92+
case _ => ()
93+
}
94+
95+
runTests()
96+
}
97+
98+
/**
99+
* A "backend" that simply outputs the core module for a given Effekt source program.
100+
*/
101+
class CompileToCore extends Compiler[(Id, symbols.Module, ModuleDecl)] {
102+
def extension = ".effekt-core.ir"
103+
104+
// Support all the feature flags so that we can test all extern declarations
105+
override def supportedFeatureFlags: List[String] = List("vm", "js", "jsNode", "chez", "llvm")
106+
107+
override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = None
108+
109+
override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = None
110+
111+
override def compile(source: Source)(using C: Context): Option[(Map[String, String], (Id, symbols.Module, ModuleDecl))] =
112+
Optimized.run(source).map { res => (Map.empty, res) }
113+
114+
lazy val Core = Phase.cached("core") {
115+
Frontend andThen Middleend
116+
}
117+
118+
lazy val Optimized = allToCore(Core) andThen Aggregate map {
119+
case input @ CoreTransformed(source, tree, mod, core) =>
120+
val mainSymbol = Context.ensureMainExists(mod)
121+
(mainSymbol, mod, core)
122+
}
123+
}

0 commit comments

Comments
 (0)