Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
21fbb0d
Add initial ReparseTests
timsueberkrueb Nov 5, 2025
168642d
Close to reaching core printer-parser parity
timsueberkrueb Nov 5, 2025
6c12f7d
Towards harmonizing id handling between parser and pretty-printer
timsueberkrueb Nov 11, 2025
8654c3e
Fix wrong scope handling
timsueberkrueb Nov 12, 2025
39ba213
Fix handling of top-level and builtin names
timsueberkrueb Nov 12, 2025
5020255
Move builtins resolution to effekt.symbols.builtins
timsueberkrueb Nov 12, 2025
dae88ea
Parity of parsing/printing hole spans
timsueberkrueb Nov 17, 2025
03347e1
Add make to list of keywords
timsueberkrueb Nov 17, 2025
0c04d08
Integer literals can be 64-bit (Long)
timsueberkrueb Nov 17, 2025
195c80c
Print braces around operation bodies
timsueberkrueb Nov 17, 2025
1716644
Ignore some feature-dependent errors
timsueberkrueb Nov 17, 2025
5d11df0
Fix parsing and printing of string literals
timsueberkrueb Nov 17, 2025
684989c
Add simple string (un)escaping
timsueberkrueb Nov 17, 2025
b2d1030
Forbid keywords that contain $ to disambiguate from identifiers
timsueberkrueb Nov 17, 2025
0079a32
Ignore some more tests
timsueberkrueb Nov 17, 2025
3868468
Parse exponential notation in double literals
timsueberkrueb Nov 17, 2025
67cc528
Print capture sets in alphabetic (and hence deterministic) order
timsueberkrueb Nov 17, 2025
06eeed5
Improve assertAlphaEquivalent output
timsueberkrueb Nov 19, 2025
f312616
Improve toCoreThenReparse output
timsueberkrueb Nov 19, 2025
b8eece9
Fix wrong usage of TestRenamer
timsueberkrueb Nov 19, 2025
0367733
Fix Barendregt id handling
timsueberkrueb Nov 19, 2025
bf50d1a
Undo some whitespace changes to reduce diff
timsueberkrueb Nov 19, 2025
81c0052
Ignore limitation of TestRenamer wrt renaming captures
timsueberkrueb Nov 19, 2025
1aec2d4
Fix TestRenamerTests
timsueberkrueb Nov 19, 2025
9755de4
Separate core reparse tests
timsueberkrueb Nov 19, 2025
6ced795
Improve name in build.sbt
timsueberkrueb Nov 27, 2025
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
18 changes: 18 additions & 0 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ jobs:
- name: Test effekt binary
run: effekt.sh --help

# These are currently separated as they take a long time to run.
core-reparse-tests:
name: "Core Reparse Tests"
needs: build-and-compile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: 'true'

- uses: ./.github/actions/setup-effekt

- uses: ./.github/actions/restore-build-cache

- name: Run core reparse tests
run: |
sbt effektJVM/testCoreReparsing

windows-tests:
name: "Windows Smoke Test"
needs: build-and-compile
Expand Down
16 changes: 12 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lazy val bumpMinorVersion = taskKey[Unit]("Bumps the minor version number (used
lazy val testBackendJS = taskKey[Unit]("Run JavaScript backend tests")
lazy val testBackendChez = taskKey[Unit]("Run Chez Scheme backend tests")
lazy val testBackendLLVM = taskKey[Unit]("Run LLVM backend tests")
lazy val testCoreReparsing = taskKey[Unit]("Run core reparsing tests")
lazy val testRemaining = taskKey[Unit]("Run all non-backend tests (internal tests) on effektJVM")

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

testCoreReparsing := {
(Test / testOnly).toTask(
" effekt.core.ReparseTests"
).value
},

testRemaining := Def.taskDyn {
val log = streams.value.log

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

// Explicit list of backend tests (union of all testBackend targets)
val backendTests = Set(
// Explicit list of tests run separately (union of all testBackend targets)
val separatedTests = Set(
"effekt.JavaScriptTests",
"effekt.StdlibJavaScriptTests",
"effekt.ChezSchemeMonadicTests",
Expand All @@ -245,10 +252,11 @@ lazy val effekt: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("e
"effekt.StdlibChezSchemeCallCCTests",
"effekt.LLVMTests",
"effekt.LLVMNoValgrindTests",
"effekt.StdlibLLVMTests"
"effekt.StdlibLLVMTests",
"effekt.core.ReparseTests",
)

val remaining = allTests -- backendTests
val remaining = allTests -- separatedTests

if (remaining.isEmpty) {
log.info("No remaining tests")
Expand Down
11 changes: 8 additions & 3 deletions effekt/jvm/src/test/scala/effekt/core/CoreTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,19 @@ trait CoreTests extends munit.FunSuite {
expected: ModuleDecl,
clue: => Any = "values are not alpha-equivalent",
names: Names = Names(defaultNames))(using Location): Unit = {
val renamer = TestRenamer(names, "$")
shouldBeEqual(renamer(obtained), renamer(expected), clue)
val renamer = TestRenamer(names)
val obtainedRenamed = renamer(obtained)
val expectedRenamed = renamer(expected)
val obtainedPrinted = effekt.core.PrettyPrinter.format(obtainedRenamed).layout
val expectedPrinted = effekt.core.PrettyPrinter.format(expectedRenamed).layout
assertEquals(obtainedPrinted, expectedPrinted)
shouldBeEqual(obtainedRenamed, expectedRenamed, clue)
}
def assertAlphaEquivalentStatements(obtained: Stmt,
expected: Stmt,
clue: => Any = "values are not alpha-equivalent",
names: Names = Names(defaultNames))(using Location): Unit = {
val renamer = TestRenamer(names, "$")
val renamer = TestRenamer(names)
shouldBeEqual(renamer(obtained), renamer(expected), clue)
}
def parse(input: String,
Expand Down
3 changes: 2 additions & 1 deletion effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class OptimizerTests extends CoreTests {
val pExpected = parse(moduleHeader + transformed, "expected", names)

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

val obtained = transform(renamed)
assertAlphaEquivalent(obtained, pExpected, "Not transformed to")
Expand Down
123 changes: 123 additions & 0 deletions effekt/jvm/src/test/scala/effekt/core/ReparseTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package effekt.core

import effekt.*
import effekt.PhaseResult.CoreTransformed
import effekt.context.{Context, IOModuleDB}
import effekt.util.PlainMessaging
import kiama.output.PrettyPrinterTypes.Document
import kiama.util.{Source, StringSource}
import munit.Location
import sbt.io.*
import sbt.io.syntax.*

import java.io.File

/*
// * This test suite ensures that the core pretty-printer always produces reparsable code.
*/
class ReparseTests extends CoreTests {
object plainMessaging extends PlainMessaging
object context extends Context with IOModuleDB {
val messaging = plainMessaging

object frontend extends CompileToCore

override lazy val compiler = frontend.asInstanceOf
}

// The sources of all test files are stored here:
def examplesDir = new File("examples")

// Test files which are to be ignored (since features are missing or known bugs exist)
def ignored: Set[File] = Set(
// Missing include: text/pregexp.scm
File("examples/pos/simpleparser.effekt"),
// Cannot find source for unsafe/cont
File("examples/pos/propagators.effekt"),
// Bidirectional effects that mention the same effect recursively are not (yet) supported.
File("examples/pos/bidirectional/selfrecursion.effekt"),
// FIXME: Wrong number of type arguments
File("examples/pos/type_omission_op.effekt"),
// FIXME: There is currently a limitation in TestRenamer in that it does not rename captures.
// This means, that in this example, captures for State[Int] and State[String] are both printed as "State",
// leading to a collapse of the capture set {State_1, State_2} to just {State}.
File("examples/pos/parametrized.effekt")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marzipankaiser this is what I just mentioned in person.

)

def positives: Set[File] = Set(
examplesDir / "pos",
examplesDir / "casestudies",
examplesDir / "benchmarks",
)

def runTests() = positives.foreach(runPositiveTestsIn)

def runPositiveTestsIn(dir: File): Unit =
foreachFileIn(dir) {
// We don't currently test *.effekt.md files
f => if (!ignored.contains(f) && !f.getName.endsWith(".md")) {
test(s"${f.getPath}") {
toCoreThenReparse(f)
}
}
}

def toCoreThenReparse(input: File) = {
val content = IO.read(input)
val config = new EffektConfig(Seq("--Koutput", "string"))
config.verify()
context.setup(config)
val (_, _, coreMod: ModuleDecl) = context.frontend.compile(StringSource(content, "input.effekt"))(using context).map {
case (_, decl) => decl
}.getOrElse {
val errors = plainMessaging.formatMessages(context.messaging.buffer)
sys error errors
}
val renamer = TestRenamer(Names(defaultNames))
val expectedRenamed = renamer(coreMod)
val printed = core.PrettyPrinter.format(expectedRenamed).layout
val reparsed: ModuleDecl = parse(printed)(using Location.empty)
val reparsedRenamed = renamer(reparsed)
val reparsedPrinted = core.PrettyPrinter.format(reparsedRenamed).layout
val expectedPrinted = core.PrettyPrinter.format(expectedRenamed).layout
assertEquals(reparsedPrinted, expectedPrinted)
}

def foreachFileIn(file: File)(test: File => Unit): Unit =
file match {
case f if f.isDirectory =>
f.listFiles.foreach(foreachFileIn(_)(test))
case f if f.getName.endsWith(".effekt") || f.getName.endsWith(".effekt.md") =>
test(f)
case _ => ()
}

runTests()
}

/**
* A "backend" that simply outputs the core module for a given Effekt source program.
*/
class CompileToCore extends Compiler[(Id, symbols.Module, ModuleDecl)] {
def extension = ".effekt-core.ir"

// Support all the feature flags so that we can test all extern declarations
override def supportedFeatureFlags: List[String] = List("vm", "js", "jsNode", "chez", "llvm")

override def prettyIR(source: Source, stage: Stage)(using C: Context): Option[Document] = None

override def treeIR(source: Source, stage: Stage)(using Context): Option[Any] = None

override def compile(source: Source)(using C: Context): Option[(Map[String, String], (Id, symbols.Module, ModuleDecl))] =
Optimized.run(source).map { res => (Map.empty, res) }

lazy val Core = Phase.cached("core") {
Frontend andThen Middleend
}

lazy val Optimized = allToCore(Core) andThen Aggregate map {
case input @ CoreTransformed(source, tree, mod, core) =>
val mainSymbol = Context.ensureMainExists(mod)
(mainSymbol, mod, core)
}
}
Loading